UNPKG

jotai

Version:

👻 Primitive and flexible state management for React

828 lines (827 loc) • 34 kB
// Internal functions (subject to change without notice) // In case you rely on them, be sure to pin the version // Building block keys (one-char for bundle size; bundlers inline the consts) // store state const KEY_atomStateMap = 'a'; const KEY_mountedMap = 'm'; const KEY_invalidatedAtoms = 'i'; const KEY_changedAtoms = 'c'; const KEY_mountCallbacks = 'q'; const KEY_unmountCallbacks = 'Q'; const KEY_storeHooks = 'h'; // atom interceptors const KEY_atomRead = 'R'; const KEY_atomWrite = 'W'; const KEY_atomOnInit = 'I'; const KEY_atomOnMount = 'M'; // building-block functions const KEY_ensureAtomState = 'e'; const KEY_flushCallbacks = 'f'; const KEY_recomputeInvalidatedAtoms = 'C'; const KEY_readAtomState = 'r'; const KEY_invalidateDependents = 'd'; const KEY_writeAtomState = 'w'; const KEY_mountDependencies = 'D'; const KEY_mountAtom = 't'; const KEY_unmountAtom = 'T'; const KEY_setAtomStateValueOrPromise = 'v'; // store api const KEY_storeGet = 'g'; const KEY_storeSet = 's'; const KEY_storeSub = 'b'; const KEY_enhanceBuildingBlocks = 'B'; // abortable promise support const KEY_abortHandlersMap = 'p'; const KEY_registerAbortHandler = 'H'; const KEY_abortPromise = 'A'; // store epoch const KEY_storeEpochHolder = 'E'; // // Some util functions // function hasInitialValue(atom) { return 'init' in atom; } function isActuallyWritableAtom(atom) { return typeof atom.write === 'function'; } function hasOnMount(atom) { return !!atom.onMount; } function isAtomStateInitialized(atomState) { return 'v' in atomState || 'e' in atomState; } function returnAtomValue(atomState) { if ('e' in atomState) { throw atomState.e; } if (process.env.NODE_ENV !== 'production' && !('v' in atomState)) { throw new Error('[Bug] atom state is not initialized'); } return atomState.v; } function isPromiseLike(p) { return typeof p?.then === 'function'; } function shouldThrowSynchronously(error) { if (!(error instanceof Error)) { return false; } const name = error.name; const message = error.message.toLowerCase(); return ((name === 'RangeError' || name === 'InternalError') && (message.includes('call stack') || message.includes('too much recursion') || message.includes('stack overflow'))); } function addPendingPromiseToDependency(atom, promise, dependencyAtomState) { if (!dependencyAtomState.p.has(atom)) { dependencyAtomState.p.add(atom); const cleanup = () => dependencyAtomState.p.delete(atom); promise.then(cleanup, cleanup); } } function getMountedOrPendingDependents(atom, atomState, mountedMap) { const mounted = mountedMap.get(atom); const mountedDependents = mounted?.t; const pendingDependents = atomState.p; if (!mountedDependents?.size) { return pendingDependents; } if (!pendingDependents.size) { return mountedDependents; } // only pay the union cost when both sides are non-empty const dependents = new Set(mountedDependents); for (const a of pendingDependents) { dependents.add(a); } return dependents; } const createStoreHook = () => { const callbacks = new Set(); const notify = () => callbacks.forEach((fn) => fn()); notify.add = (fn) => { callbacks.add(fn); return () => callbacks.delete(fn); }; return notify; }; const createStoreHookForAtoms = () => { const all = {}; const callbacks = new WeakMap(); const notify = (atom) => { callbacks.get(all)?.forEach((fn) => fn(atom)); callbacks.get(atom)?.forEach((fn) => fn()); }; notify.add = (atom, fn) => { const key = atom || all; let fns = callbacks.get(key); if (!fns) { fns = new Set(); callbacks.set(key, fns); } fns.add(fn); return () => { fns.delete(fn); if (!fns.size) { callbacks.delete(key); } }; }; return notify; }; function initializeStoreHooks(storeHooks) { var _a, _b, _c, _d, _e, _f; (_a = storeHooks).i || (_a.i = createStoreHookForAtoms()); (_b = storeHooks).r || (_b.r = createStoreHookForAtoms()); (_c = storeHooks).c || (_c.c = createStoreHookForAtoms()); (_d = storeHooks).m || (_d.m = createStoreHookForAtoms()); (_e = storeHooks).u || (_e.u = createStoreHookForAtoms()); (_f = storeHooks).f || (_f.f = createStoreHook()); return storeHooks; } function hasOnInit(atom) { return !!atom.INTERNAL_onInit; } // // Main functions // const BUILDING_BLOCK_atomRead = (_buildingBlocks, _store, atom, ...params) => atom.read(...params); const BUILDING_BLOCK_atomWrite = (_buildingBlocks, _store, atom, ...params) => atom.write(...params); const BUILDING_BLOCK_atomOnInit = (_buildingBlocks, store, atom) => atom.INTERNAL_onInit(store); const BUILDING_BLOCK_atomOnMount = (_buildingBlocks, _store, atom, setAtom) => atom.onMount?.(setAtom); const BUILDING_BLOCK_ensureAtomState = (buildingBlocks, store, atom) => { const atomStateMap = buildingBlocks[KEY_atomStateMap]; let atomState = atomStateMap.get(atom); if (!atomState) { // Because ensureAtomState is a hot spot, we intentionally delay the evaluation even though this pattern is exceptional. // Ref: https://github.com/pmndrs/jotai/pull/3313 const storeHooks = buildingBlocks[KEY_storeHooks]; const atomOnInit = buildingBlocks[KEY_atomOnInit]; atomState = { d: new Map(), p: new Set(), n: 0 }; atomStateMap.set(atom, atomState); storeHooks.i?.(atom); if (hasOnInit(atom)) { atomOnInit(buildingBlocks, store, atom); } } return atomState; }; const BUILDING_BLOCK_flushCallbacks = (buildingBlocks, store) => { const mountedMap = buildingBlocks[KEY_mountedMap]; const changedAtoms = buildingBlocks[KEY_changedAtoms]; const mountCallbacks = buildingBlocks[KEY_mountCallbacks]; const unmountCallbacks = buildingBlocks[KEY_unmountCallbacks]; const storeHooks = buildingBlocks[KEY_storeHooks]; const recomputeInvalidatedAtoms = buildingBlocks[KEY_recomputeInvalidatedAtoms]; if (!storeHooks.f && !changedAtoms.size && !mountCallbacks.size && !unmountCallbacks.size) { return; } const errors = []; const call = (fn) => { try { fn(); } catch (e) { errors.push(e); } }; do { if (storeHooks.f) { call(storeHooks.f); } const callbacks = new Set(); for (const atom of changedAtoms) { const listeners = mountedMap.get(atom)?.l; if (listeners) { for (const listener of listeners) { callbacks.add(listener); } } } changedAtoms.clear(); for (const fn of unmountCallbacks) { callbacks.add(fn); } unmountCallbacks.clear(); for (const fn of mountCallbacks) { callbacks.add(fn); } mountCallbacks.clear(); for (const fn of callbacks) { call(fn); } if (changedAtoms.size) { recomputeInvalidatedAtoms(buildingBlocks, store); } } while (changedAtoms.size || unmountCallbacks.size || mountCallbacks.size); if (errors.length) { if (typeof AggregateError === 'function') { throw new AggregateError(errors); } throw Object.assign(new Error(), { errors }); } }; const BUILDING_BLOCK_recomputeInvalidatedAtoms = (buildingBlocks, store) => { const mountedMap = buildingBlocks[KEY_mountedMap]; const invalidatedAtoms = buildingBlocks[KEY_invalidatedAtoms]; const changedAtoms = buildingBlocks[KEY_changedAtoms]; const ensureAtomState = buildingBlocks[KEY_ensureAtomState]; const readAtomState = buildingBlocks[KEY_readAtomState]; const mountDependencies = buildingBlocks[KEY_mountDependencies]; if (!changedAtoms.size) { return; } // Step 1: traverse the dependency graph to build the topologically sorted atom list // We don't bother to check for cycles, which simplifies the algorithm. // This is a topological sort via depth-first search, slightly modified from // what's described here for simplicity and performance reasons: // https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search const sortedReversedAtoms = []; const sortedReversedStates = []; const visiting = new WeakSet(); const visited = new WeakSet(); const stackAtoms = []; const stackStates = []; // Visit the root atoms. This is the only atom in the dependency graph // without incoming edges, which is one reason we can simplify the algorithm for (const atom of changedAtoms) { stackAtoms.push(atom); stackStates.push(ensureAtomState(buildingBlocks, store, atom)); } while (stackAtoms.length) { const top = stackAtoms.length - 1; const a = stackAtoms[top]; const aState = stackStates[top]; if (visited.has(a)) { // All dependents have been processed, now process this atom stackAtoms.pop(); stackStates.pop(); continue; } if (visiting.has(a)) { // The algorithm calls for pushing onto the front of the list. For // performance, we will simply push onto the end, and then will iterate in // reverse order later. if (invalidatedAtoms.get(a) === aState.n) { sortedReversedAtoms.push(a); sortedReversedStates.push(aState); } else if (process.env.NODE_ENV !== 'production' && invalidatedAtoms.has(a)) { throw new Error('[Bug] invalidated atom exists'); } // Atom has been visited but not yet processed visited.add(a); stackAtoms.pop(); stackStates.pop(); continue; } visiting.add(a); // Push unvisited dependents onto the stack for (const d of getMountedOrPendingDependents(a, aState, mountedMap)) { if (!visiting.has(d)) { stackAtoms.push(d); stackStates.push(ensureAtomState(buildingBlocks, store, d)); } } } // Step 2: use the topSortedReversed atom list to recompute all affected atoms // Track what's changed, so that we can short circuit when possible for (let i = sortedReversedAtoms.length - 1; i >= 0; --i) { const a = sortedReversedAtoms[i]; const aState = sortedReversedStates[i]; let hasChangedDeps = false; for (const dep of aState.d.keys()) { if (dep !== a && changedAtoms.has(dep)) { hasChangedDeps = true; break; } } if (hasChangedDeps) { invalidatedAtoms.set(a, aState.n); readAtomState(buildingBlocks, store, a); mountDependencies(buildingBlocks, store, a); } invalidatedAtoms.delete(a); } }; // Dev only const storeMutationSet = new WeakSet(); const BUILDING_BLOCK_readAtomState = (buildingBlocks, store, atom) => { const mountedMap = buildingBlocks[KEY_mountedMap]; const invalidatedAtoms = buildingBlocks[KEY_invalidatedAtoms]; const changedAtoms = buildingBlocks[KEY_changedAtoms]; const storeHooks = buildingBlocks[KEY_storeHooks]; const atomRead = buildingBlocks[KEY_atomRead]; const ensureAtomState = buildingBlocks[KEY_ensureAtomState]; const flushCallbacks = buildingBlocks[KEY_flushCallbacks]; const recomputeInvalidatedAtoms = buildingBlocks[KEY_recomputeInvalidatedAtoms]; const readAtomState = buildingBlocks[KEY_readAtomState]; const mountDependencies = buildingBlocks[KEY_mountDependencies]; const setAtomStateValueOrPromise = buildingBlocks[KEY_setAtomStateValueOrPromise]; const registerAbortHandler = buildingBlocks[KEY_registerAbortHandler]; const storeEpochHolder = buildingBlocks[KEY_storeEpochHolder]; const atomState = ensureAtomState(buildingBlocks, store, atom); const storeEpochNumber = storeEpochHolder[0]; // See if we can skip recomputing this atom. if (isAtomStateInitialized(atomState)) { if ( // If the atom is mounted, we can use cached atom state, // because it should have been updated by dependencies. // We can't use the cache if the atom is invalidated. (mountedMap.has(atom) && invalidatedAtoms.get(atom) !== atomState.n) || // If atom is not mounted, we can use cached atom state, // only if store hasn't been mutated. atomState.m === storeEpochNumber) { atomState.m = storeEpochNumber; return atomState; } // Otherwise, check if the dependencies have changed. // If all dependencies haven't changed, we can use the cache. let hasChangedDeps = false; for (const [a, n] of atomState.d) { if (readAtomState(buildingBlocks, store, a).n !== n) { hasChangedDeps = true; break; } } if (!hasChangedDeps) { atomState.m = storeEpochNumber; return atomState; } } // Compute a new state for this atom. let isSync = true; const prevDeps = new Set(atomState.d.keys()); const pruneDependencies = () => { for (const a of prevDeps) { atomState.d.delete(a); } }; const mountDependenciesIfAsync = () => { if (mountedMap.has(atom)) { // If changedAtoms is already populated, an outer recompute cycle will handle it const shouldRecompute = !changedAtoms.size; mountDependencies(buildingBlocks, store, atom); if (shouldRecompute) { recomputeInvalidatedAtoms(buildingBlocks, store); flushCallbacks(buildingBlocks, store); } } }; const getter = (a) => { if (a === atom) { const aState = ensureAtomState(buildingBlocks, store, a); if (!isAtomStateInitialized(aState)) { if (hasInitialValue(a)) { setAtomStateValueOrPromise(buildingBlocks, store, a, a.init); } else { // NOTE invalid derived atoms can reach here throw new Error('no atom init'); } } return returnAtomValue(aState); } // a !== atom const aState = readAtomState(buildingBlocks, store, a); try { return returnAtomValue(aState); } finally { prevDeps.delete(a); atomState.d.set(a, aState.n); if (isPromiseLike(atomState.v)) { addPendingPromiseToDependency(atom, atomState.v, aState); } if (mountedMap.has(atom)) { mountedMap.get(a)?.t.add(atom); } if (!isSync) { mountDependenciesIfAsync(); } } }; let controller; const options = { get signal() { if (!controller) { controller = new AbortController(); } return controller.signal; }, }; const prevEpochNumber = atomState.n; const prevInvalidated = invalidatedAtoms.get(atom) === prevEpochNumber; try { if (process.env.NODE_ENV !== 'production') { storeMutationSet.delete(store); } const valueOrPromise = atomRead(buildingBlocks, store, atom, getter, options); if (process.env.NODE_ENV !== 'production' && storeMutationSet.has(store)) { console.warn('Detected store mutation during atom read. This is not supported.'); } setAtomStateValueOrPromise(buildingBlocks, store, atom, valueOrPromise); if (isPromiseLike(valueOrPromise)) { registerAbortHandler(buildingBlocks, store, valueOrPromise, () => controller?.abort()); const settle = () => { pruneDependencies(); mountDependenciesIfAsync(); }; valueOrPromise.then(settle, settle); } else { pruneDependencies(); } storeHooks.r?.(atom); atomState.m = storeEpochNumber; return atomState; } catch (error) { if (shouldThrowSynchronously(error)) { throw error; } delete atomState.v; atomState.e = error; ++atomState.n; atomState.m = storeEpochNumber; return atomState; } finally { isSync = false; if (atomState.n !== prevEpochNumber && prevInvalidated) { invalidatedAtoms.set(atom, atomState.n); changedAtoms.add(atom); storeHooks.c?.(atom); } } }; const BUILDING_BLOCK_invalidateDependents = (buildingBlocks, store, atom) => { const mountedMap = buildingBlocks[KEY_mountedMap]; const invalidatedAtoms = buildingBlocks[KEY_invalidatedAtoms]; const ensureAtomState = buildingBlocks[KEY_ensureAtomState]; const stack = [atom]; while (stack.length) { const a = stack.pop(); const aState = ensureAtomState(buildingBlocks, store, a); for (const d of getMountedOrPendingDependents(a, aState, mountedMap)) { const dState = ensureAtomState(buildingBlocks, store, d); if (invalidatedAtoms.get(d) !== dState.n) { invalidatedAtoms.set(d, dState.n); stack.push(d); } } } }; const BUILDING_BLOCK_writeAtomState = (buildingBlocks, store, atom, args) => { const changedAtoms = buildingBlocks[KEY_changedAtoms]; const storeHooks = buildingBlocks[KEY_storeHooks]; const atomWrite = buildingBlocks[KEY_atomWrite]; const ensureAtomState = buildingBlocks[KEY_ensureAtomState]; const flushCallbacks = buildingBlocks[KEY_flushCallbacks]; const recomputeInvalidatedAtoms = buildingBlocks[KEY_recomputeInvalidatedAtoms]; const readAtomState = buildingBlocks[KEY_readAtomState]; const invalidateDependents = buildingBlocks[KEY_invalidateDependents]; const writeAtomState = buildingBlocks[KEY_writeAtomState]; const mountDependencies = buildingBlocks[KEY_mountDependencies]; const setAtomStateValueOrPromise = buildingBlocks[KEY_setAtomStateValueOrPromise]; const storeEpochHolder = buildingBlocks[KEY_storeEpochHolder]; let isSync = true; const getter = (a) => returnAtomValue(readAtomState(buildingBlocks, store, a)); const setter = (a, ...args) => { const aState = ensureAtomState(buildingBlocks, store, a); try { if (a === atom) { if (!hasInitialValue(a)) { // NOTE technically possible but restricted as it may cause bugs throw new Error('atom not writable'); } if (process.env.NODE_ENV !== 'production') { storeMutationSet.add(store); } const prevEpochNumber = aState.n; const v = args[0]; setAtomStateValueOrPromise(buildingBlocks, store, a, v); mountDependencies(buildingBlocks, store, a); if (prevEpochNumber !== aState.n) { ++storeEpochHolder[0]; changedAtoms.add(a); invalidateDependents(buildingBlocks, store, a); storeHooks.c?.(a); } return undefined; } else { return writeAtomState(buildingBlocks, store, a, args); } } finally { if (!isSync) { recomputeInvalidatedAtoms(buildingBlocks, store); flushCallbacks(buildingBlocks, store); } } }; try { return atomWrite(buildingBlocks, store, atom, getter, setter, ...args); } finally { isSync = false; } }; const BUILDING_BLOCK_mountDependencies = (buildingBlocks, store, atom) => { const mountedMap = buildingBlocks[KEY_mountedMap]; const changedAtoms = buildingBlocks[KEY_changedAtoms]; const storeHooks = buildingBlocks[KEY_storeHooks]; const ensureAtomState = buildingBlocks[KEY_ensureAtomState]; const invalidateDependents = buildingBlocks[KEY_invalidateDependents]; const mountAtom = buildingBlocks[KEY_mountAtom]; const unmountAtom = buildingBlocks[KEY_unmountAtom]; const atomState = ensureAtomState(buildingBlocks, store, atom); const mounted = mountedMap.get(atom); if (mounted && atomState.d.size > 0) { for (const [a, n] of atomState.d) { if (!mounted.d.has(a)) { const aState = ensureAtomState(buildingBlocks, store, a); const aMounted = mountAtom(buildingBlocks, store, a); aMounted.t.add(atom); mounted.d.add(a); if (n !== aState.n) { changedAtoms.add(a); invalidateDependents(buildingBlocks, store, a); storeHooks.c?.(a); } } } for (const a of mounted.d) { if (!atomState.d.has(a)) { mounted.d.delete(a); const aMounted = unmountAtom(buildingBlocks, store, a); aMounted?.t.delete(atom); } } } }; const BUILDING_BLOCK_mountAtom = (buildingBlocks, store, atom) => { const mountedMap = buildingBlocks[KEY_mountedMap]; const mountCallbacks = buildingBlocks[KEY_mountCallbacks]; const storeHooks = buildingBlocks[KEY_storeHooks]; const atomOnMount = buildingBlocks[KEY_atomOnMount]; const ensureAtomState = buildingBlocks[KEY_ensureAtomState]; const flushCallbacks = buildingBlocks[KEY_flushCallbacks]; const recomputeInvalidatedAtoms = buildingBlocks[KEY_recomputeInvalidatedAtoms]; const readAtomState = buildingBlocks[KEY_readAtomState]; const writeAtomState = buildingBlocks[KEY_writeAtomState]; const mountAtom = buildingBlocks[KEY_mountAtom]; const atomState = ensureAtomState(buildingBlocks, store, atom); let mounted = mountedMap.get(atom); if (!mounted) { // recompute atom state readAtomState(buildingBlocks, store, atom); // mount dependencies first for (const a of atomState.d.keys()) { const aMounted = mountAtom(buildingBlocks, store, a); aMounted.t.add(atom); } // mount self mounted = { l: new Set(), d: new Set(atomState.d.keys()), t: new Set(), }; mountedMap.set(atom, mounted); if (isActuallyWritableAtom(atom) && hasOnMount(atom)) { const processOnMount = () => { let isSync = true; const setAtom = (...args) => { try { return writeAtomState(buildingBlocks, store, atom, args); } finally { if (!isSync) { recomputeInvalidatedAtoms(buildingBlocks, store); flushCallbacks(buildingBlocks, store); } } }; try { const onUnmount = atomOnMount(buildingBlocks, store, atom, setAtom); if (onUnmount) { mounted.u = () => { isSync = true; try { onUnmount(); } finally { isSync = false; } }; } } finally { isSync = false; } }; mountCallbacks.add(processOnMount); } storeHooks.m?.(atom); } return mounted; }; const BUILDING_BLOCK_unmountAtom = (buildingBlocks, store, atom) => { const mountedMap = buildingBlocks[KEY_mountedMap]; const unmountCallbacks = buildingBlocks[KEY_unmountCallbacks]; const storeHooks = buildingBlocks[KEY_storeHooks]; const ensureAtomState = buildingBlocks[KEY_ensureAtomState]; const unmountAtom = buildingBlocks[KEY_unmountAtom]; const atomState = ensureAtomState(buildingBlocks, store, atom); let mounted = mountedMap.get(atom); if (!mounted || mounted.l.size) { return mounted; } let isDependent = false; for (const a of mounted.t) { if (mountedMap.get(a)?.d.has(atom)) { isDependent = true; break; } } if (!isDependent) { // unmount self if (mounted.u) { unmountCallbacks.add(mounted.u); } mounted = undefined; mountedMap.delete(atom); // unmount dependencies for (const a of atomState.d.keys()) { const aMounted = unmountAtom(buildingBlocks, store, a); aMounted?.t.delete(atom); } storeHooks.u?.(atom); return undefined; } return mounted; }; const BUILDING_BLOCK_setAtomStateValueOrPromise = (buildingBlocks, store, atom, valueOrPromise) => { const ensureAtomState = buildingBlocks[KEY_ensureAtomState]; const abortPromise = buildingBlocks[KEY_abortPromise]; const atomState = ensureAtomState(buildingBlocks, store, atom); const hasPrevValue = 'v' in atomState; const prevValue = atomState.v; if (isPromiseLike(valueOrPromise)) { for (const a of atomState.d.keys()) { addPendingPromiseToDependency(atom, valueOrPromise, ensureAtomState(buildingBlocks, store, a)); } } atomState.v = valueOrPromise; delete atomState.e; if (!hasPrevValue || !Object.is(prevValue, atomState.v)) { ++atomState.n; if (isPromiseLike(prevValue)) { abortPromise(buildingBlocks, store, prevValue); } } }; const BUILDING_BLOCK_storeGet = (buildingBlocks, store, atom) => { const readAtomState = buildingBlocks[KEY_readAtomState]; return returnAtomValue(readAtomState(buildingBlocks, store, atom)); }; const BUILDING_BLOCK_storeSet = (buildingBlocks, store, atom, ...args) => { const changedAtoms = buildingBlocks[KEY_changedAtoms]; const flushCallbacks = buildingBlocks[KEY_flushCallbacks]; const recomputeInvalidatedAtoms = buildingBlocks[KEY_recomputeInvalidatedAtoms]; const writeAtomState = buildingBlocks[KEY_writeAtomState]; const prevChangedAtomsSize = changedAtoms.size; try { return writeAtomState(buildingBlocks, store, atom, args); } finally { if (changedAtoms.size !== prevChangedAtomsSize) { recomputeInvalidatedAtoms(buildingBlocks, store); flushCallbacks(buildingBlocks, store); } } }; const BUILDING_BLOCK_storeSub = (buildingBlocks, store, atom, listener) => { const flushCallbacks = buildingBlocks[KEY_flushCallbacks]; const recomputeInvalidatedAtoms = buildingBlocks[KEY_recomputeInvalidatedAtoms]; const mountAtom = buildingBlocks[KEY_mountAtom]; const unmountAtom = buildingBlocks[KEY_unmountAtom]; const mounted = mountAtom(buildingBlocks, store, atom); const listeners = mounted.l; listeners.add(listener); recomputeInvalidatedAtoms(buildingBlocks, store); flushCallbacks(buildingBlocks, store); return () => { listeners.delete(listener); unmountAtom(buildingBlocks, store, atom); recomputeInvalidatedAtoms(buildingBlocks, store); flushCallbacks(buildingBlocks, store); }; }; const BUILDING_BLOCK_registerAbortHandler = (buildingBlocks, _store, promise, abortHandler) => { const abortHandlersMap = buildingBlocks[KEY_abortHandlersMap]; let abortHandlers = abortHandlersMap.get(promise); if (!abortHandlers) { abortHandlers = new Set(); abortHandlersMap.set(promise, abortHandlers); const cleanup = () => abortHandlersMap.delete(promise); promise.then(cleanup, cleanup); } abortHandlers.add(abortHandler); }; const BUILDING_BLOCK_abortPromise = (buildingBlocks, _store, promise) => { const abortHandlersMap = buildingBlocks[KEY_abortHandlersMap]; const abortHandlers = abortHandlersMap.get(promise); abortHandlers?.forEach((fn) => fn()); }; const buildingBlockMap = new WeakMap(); function getBuildingBlocks(store) { const buildingBlocks = buildingBlockMap.get(store); if (process.env.NODE_ENV !== 'production' && !buildingBlocks) { throw new Error('Store must be created by buildStore to read its building blocks'); } const enhanceBuildingBlocks = buildingBlocks[KEY_enhanceBuildingBlocks]; if (enhanceBuildingBlocks) { return enhanceBuildingBlocks(buildingBlocks, store); } return buildingBlocks; } function buildStore(partialBuildingBlocks) { const store = { get(atom) { return storeGet(buildingBlocks, store, atom); }, set(atom, ...args) { return storeSet(buildingBlocks, store, atom, ...args); }, sub(atom, listener) { return storeSub(buildingBlocks, store, atom, listener); }, }; const defaultBuildingBlocks = { // store state [KEY_atomStateMap]: new WeakMap(), [KEY_mountedMap]: new WeakMap(), [KEY_invalidatedAtoms]: new WeakMap(), [KEY_changedAtoms]: new Set(), [KEY_mountCallbacks]: new Set(), [KEY_unmountCallbacks]: new Set(), [KEY_storeHooks]: {}, // atom interceptors [KEY_atomRead]: BUILDING_BLOCK_atomRead, [KEY_atomWrite]: BUILDING_BLOCK_atomWrite, [KEY_atomOnInit]: BUILDING_BLOCK_atomOnInit, [KEY_atomOnMount]: BUILDING_BLOCK_atomOnMount, // building-block functions [KEY_ensureAtomState]: BUILDING_BLOCK_ensureAtomState, [KEY_flushCallbacks]: BUILDING_BLOCK_flushCallbacks, [KEY_recomputeInvalidatedAtoms]: BUILDING_BLOCK_recomputeInvalidatedAtoms, [KEY_readAtomState]: BUILDING_BLOCK_readAtomState, [KEY_invalidateDependents]: BUILDING_BLOCK_invalidateDependents, [KEY_writeAtomState]: BUILDING_BLOCK_writeAtomState, [KEY_mountDependencies]: BUILDING_BLOCK_mountDependencies, [KEY_mountAtom]: BUILDING_BLOCK_mountAtom, [KEY_unmountAtom]: BUILDING_BLOCK_unmountAtom, [KEY_setAtomStateValueOrPromise]: BUILDING_BLOCK_setAtomStateValueOrPromise, // store api [KEY_storeGet]: BUILDING_BLOCK_storeGet, [KEY_storeSet]: BUILDING_BLOCK_storeSet, [KEY_storeSub]: BUILDING_BLOCK_storeSub, [KEY_enhanceBuildingBlocks]: undefined, // abortable promise support [KEY_abortHandlersMap]: new WeakMap(), [KEY_registerAbortHandler]: BUILDING_BLOCK_registerAbortHandler, [KEY_abortPromise]: BUILDING_BLOCK_abortPromise, // store epoch [KEY_storeEpochHolder]: [0], }; const buildingBlocks = Object.freeze({ ...defaultBuildingBlocks, ...partialBuildingBlocks, }); buildingBlockMap.set(store, buildingBlocks); const storeGet = buildingBlocks[KEY_storeGet]; const storeSet = buildingBlocks[KEY_storeSet]; const storeSub = buildingBlocks[KEY_storeSub]; return store; } export { // // Export internal functions // buildStore as INTERNAL_buildStoreRev4, getBuildingBlocks as INTERNAL_getBuildingBlocksRev4, initializeStoreHooks as INTERNAL_initializeStoreHooksRev4, // // Export building block keys // KEY_atomStateMap as INTERNAL_KEY_atomStateMap, KEY_mountedMap as INTERNAL_KEY_mountedMap, KEY_invalidatedAtoms as INTERNAL_KEY_invalidatedAtoms, KEY_changedAtoms as INTERNAL_KEY_changedAtoms, KEY_mountCallbacks as INTERNAL_KEY_mountCallbacks, KEY_unmountCallbacks as INTERNAL_KEY_unmountCallbacks, KEY_storeHooks as INTERNAL_KEY_storeHooks, KEY_atomRead as INTERNAL_KEY_atomRead, KEY_atomWrite as INTERNAL_KEY_atomWrite, KEY_atomOnInit as INTERNAL_KEY_atomOnInit, KEY_atomOnMount as INTERNAL_KEY_atomOnMount, KEY_ensureAtomState as INTERNAL_KEY_ensureAtomState, KEY_flushCallbacks as INTERNAL_KEY_flushCallbacks, KEY_recomputeInvalidatedAtoms as INTERNAL_KEY_recomputeInvalidatedAtoms, KEY_readAtomState as INTERNAL_KEY_readAtomState, KEY_invalidateDependents as INTERNAL_KEY_invalidateDependents, KEY_writeAtomState as INTERNAL_KEY_writeAtomState, KEY_mountDependencies as INTERNAL_KEY_mountDependencies, KEY_mountAtom as INTERNAL_KEY_mountAtom, KEY_unmountAtom as INTERNAL_KEY_unmountAtom, KEY_setAtomStateValueOrPromise as INTERNAL_KEY_setAtomStateValueOrPromise, KEY_storeGet as INTERNAL_KEY_storeGet, KEY_storeSet as INTERNAL_KEY_storeSet, KEY_storeSub as INTERNAL_KEY_storeSub, KEY_enhanceBuildingBlocks as INTERNAL_KEY_enhanceBuildingBlocks, KEY_abortHandlersMap as INTERNAL_KEY_abortHandlersMap, KEY_registerAbortHandler as INTERNAL_KEY_registerAbortHandler, KEY_abortPromise as INTERNAL_KEY_abortPromise, KEY_storeEpochHolder as INTERNAL_KEY_storeEpochHolder, // // Still experimental and some of them will be gone soon // hasInitialValue as INTERNAL_hasInitialValue, isActuallyWritableAtom as INTERNAL_isActuallyWritableAtom, isAtomStateInitialized as INTERNAL_isAtomStateInitialized, returnAtomValue as INTERNAL_returnAtomValue, isPromiseLike as INTERNAL_isPromiseLike, shouldThrowSynchronously as INTERNAL_shouldThrowSynchronously, addPendingPromiseToDependency as INTERNAL_addPendingPromiseToDependency, getMountedOrPendingDependents as INTERNAL_getMountedOrPendingDependents, };