{"version":3,"file":"useUpdatableDisposableState.mjs","names":["UNASSIGNED_STATE: unique symbol","ici: ICI<T>"],"sources":["../src/useUpdatableDisposableState.ts"],"sourcesContent":["import type { ItemCleanupPair } from '@isograph/disposable-types';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { useHasCommittedRef } from './useHasCommittedRef';\n\nexport const UNASSIGNED_STATE: unique symbol = Symbol();\nexport type UnassignedState = typeof UNASSIGNED_STATE;\n\ntype UseUpdatableDisposableStateReturnValue<T> = {\n  state: T | UnassignedState;\n  setState: (pair: ItemCleanupPair<Exclude<T, UnassignedState>>) => void;\n};\n\n/**\n * ICI stands for ItemCleanupIndex. Use a short name because it shows up a lot.\n *\n * Like an ItemCleanupPair<T>, but with an additional index and as a struct\n * for readability.\n *\n * Interally, we must keep track of the index of the state change. The disposable\n * items that are set in state are not guaranteed to be distinct according to ===,\n * as in `setState([1, dispose1]); setState([1, dispose2])`. Following this, we\n * would expect dispose1 to be called on commit. If state items were compared for\n * ===, then dispose1 would not be called in such a situation.\n *\n * Note that this comes at a runtime cost, and we may want to expose APIs that do\n * not incur this runtime cost, for example in cases where every disposable item is\n * distinguishable via ===.\n */\ntype ICI<T> = { item: T; cleanup: () => void; index: number };\n\n/**\n * useUpdatableDisposableState\n * - Returns a { state, setItem } object.\n * - setItem accepts an ItemCleanupPair<T>, and throws if called before commit.\n * - setItem sets the T in state and adds it to a set.\n * - React's behavior is that when the hook commits, whatever item is currently\n *   returned from the useState hook is the oldest item which will ever be returned\n *   from that useState hook. (More newly created ones can later be returned with\n *   concurrent mode.)\n * - When this hook commits, all items up to, but not including, the item currently\n *   returned from the useState hook are disposed and removed from the set.\n *\n * Calling setState before the hook commits:\n * - Calling setState before the hook commits is disallowed because until the hook\n *   commits, React will not schedule any unmount callbacks, meaning that if this\n *   hook never commits, any disposable items passed to setState will never be\n *   disposed.\n * - We also cannot store them in some cache, because multiple components can share\n *   the same cache location (for example, if they are loading the same query from\n *   multiple components), so updating the cache with the disposable item will cause\n *   both components to show the updated data, which is almost certainly a bug.\n * - Note that calling setState before commit is probably an anti-pattern! Consider\n *   not doing it.\n * - If you must, the workaround is to lazily load the disposable item with\n *   useDisposableState or useLazyDisposableState, and update the cache location\n *   instead of calling setState before commit. One can update the cache location\n *   by calling setState on some parent component that has already mounted, and\n *   therefore passing in different props.\n *   - This may only work in concurrent mode, though.\n */\nexport function useUpdatableDisposableState<\n  T = never,\n>(): UseUpdatableDisposableStateReturnValue<T> {\n  const hasCommittedRef = useHasCommittedRef();\n\n  const undisposedICIs = useRef(new Set<ICI<T>>());\n  const setStateCountRef = useRef(0);\n\n  const [stateICI, setStateICI] = useState<ICI<T> | UnassignedState>(\n    UNASSIGNED_STATE,\n  );\n\n  const setStateAfterCommit = useCallback(\n    (itemCleanupPair: ItemCleanupPair<T>) => {\n      if (!hasCommittedRef.current) {\n        throw new Error(\n          'Calling setState before the component has committed is unsafe and disallowed.',\n        );\n      }\n\n      const ici: ICI<T> = {\n        item: itemCleanupPair[0],\n        cleanup: itemCleanupPair[1],\n        index: setStateCountRef.current,\n      };\n      setStateCountRef.current++;\n      undisposedICIs.current.add(ici);\n      setStateICI(ici);\n    },\n    [setStateICI],\n  );\n\n  useEffect(function cleanupUnreachableItems() {\n    const indexInState = stateICI !== UNASSIGNED_STATE ? stateICI.index : 0;\n\n    if (indexInState === 0) {\n      return;\n    }\n\n    for (const undisposedICI of undisposedICIs.current) {\n      if (undisposedICI.index === indexInState) {\n        break;\n      }\n      undisposedICIs.current.delete(undisposedICI);\n      undisposedICI.cleanup();\n    }\n  });\n\n  useEffect(() => {\n    return function disposeAllRemainingItems() {\n      for (const undisposedICI of undisposedICIs.current) {\n        undisposedICI.cleanup();\n      }\n    };\n  }, []);\n\n  return {\n    setState: setStateAfterCommit,\n    state: stateICI !== UNASSIGNED_STATE ? stateICI.item : UNASSIGNED_STATE,\n  };\n}\n\n// @ts-ignore\nfunction tsTests() {\n  const a = useUpdatableDisposableState();\n  // @ts-expect-error\n  a.setState([UNASSIGNED_STATE, () => {}]);\n  // @ts-expect-error\n  a.setState(['asdf', () => {}]);\n  const b = useUpdatableDisposableState<string | UnassignedState>();\n  // @ts-expect-error\n  b.setState([UNASSIGNED_STATE, () => {}]);\n  b.setState(['asdf', () => {}]);\n}\n"],"mappings":";;;;AAIA,MAAaA,mBAAkC,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDvD,SAAgB,8BAE+B;CAC7C,MAAM,kBAAkB,oBAAoB;CAE5C,MAAM,iBAAiB,uBAAO,IAAI,KAAa,CAAC;CAChD,MAAM,mBAAmB,OAAO,EAAE;CAElC,MAAM,CAAC,UAAU,eAAe,SAC9B,iBACD;CAED,MAAM,sBAAsB,aACzB,oBAAwC;AACvC,MAAI,CAAC,gBAAgB,QACnB,OAAM,IAAI,MACR,gFACD;EAGH,MAAMC,MAAc;GAClB,MAAM,gBAAgB;GACtB,SAAS,gBAAgB;GACzB,OAAO,iBAAiB;GACzB;AACD,mBAAiB;AACjB,iBAAe,QAAQ,IAAI,IAAI;AAC/B,cAAY,IAAI;IAElB,CAAC,YAAY,CACd;AAED,WAAU,SAAS,0BAA0B;EAC3C,MAAM,eAAe,aAAa,mBAAmB,SAAS,QAAQ;AAEtE,MAAI,iBAAiB,EACnB;AAGF,OAAK,MAAM,iBAAiB,eAAe,SAAS;AAClD,OAAI,cAAc,UAAU,aAC1B;AAEF,kBAAe,QAAQ,OAAO,cAAc;AAC5C,iBAAc,SAAS;;GAEzB;AAEF,iBAAgB;AACd,SAAO,SAAS,2BAA2B;AACzC,QAAK,MAAM,iBAAiB,eAAe,QACzC,eAAc,SAAS;;IAG1B,EAAE,CAAC;AAEN,QAAO;EACL,UAAU;EACV,OAAO,aAAa,mBAAmB,SAAS,OAAO;EACxD"}