UNPKG

jotai-effect

Version:
216 lines 8.4 kB
import { atom } from 'jotai/vanilla'; import { INTERNAL_getBuildingBlocksRev2 as getBuildingBlocks, INTERNAL_hasInitialValue as hasInitialValue, INTERNAL_initializeStoreHooksRev2 as initializeStoreHooks, INTERNAL_isAtomStateInitialized as isAtomStateInitialized, INTERNAL_returnAtomValue as returnAtomValue, } from 'jotai/vanilla/internals'; import { isDev } from './env.js'; export function atomEffect(effect) { const refAtom = atom(() => []); const effectAtom = atom(function effectAtomRead(get) { const [dependencies, atomState, mountedAtoms] = get(refAtom); if (mountedAtoms.has(effectAtom)) { dependencies.forEach(get); ++atomState.n; } }); effectAtom.effect = effect; effectAtom.unstable_onInit = (store) => { const deps = new Set(); let inProgress = 0; let isRecursing = false; let hasChanged = false; let fromCleanup = false; let runCleanup; function runEffect() { if (inProgress) { return; } deps.clear(); let isSync = true; const getter = (a) => { if (fromCleanup) { return store.get(a); } if (a === effectAtom) { const aState = ensureAtomState(store, a); if (!isAtomStateInitialized(aState)) { if (hasInitialValue(a)) { setAtomStateValueOrPromise(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(store, a); try { return returnAtomValue(aState); } finally { atomState.d.set(a, aState.n); mountedAtoms.get(a)?.t.add(effectAtom); if (isSync) { deps.add(a); } else { if (mountedAtoms.has(a)) { mountDependencies(store, effectAtom); recomputeInvalidatedAtoms(store); flushCallbacks(store); } } } }; getter.peek = store.get; const setter = (a, ...args) => { const aState = ensureAtomState(store, a); try { ++inProgress; if (a === effectAtom) { if (!hasInitialValue(a)) { // NOTE technically possible but restricted as it may cause bugs throw new Error('atom not writable'); } const prevEpochNumber = aState.n; const v = args[0]; setAtomStateValueOrPromise(store, a, v); mountDependencies(store, a); if (prevEpochNumber !== aState.n) { changedAtoms.add(a); storeHooks.c?.(a); invalidateDependents(store, a); } return undefined; } else { return writeAtomState(store, a, ...args); } } finally { if (!isSync) { recomputeInvalidatedAtoms(store); flushCallbacks(store); } --inProgress; } }; setter.recurse = (a, ...args) => { if (fromCleanup) { if (isDev()) { throw new Error('set.recurse is not allowed in cleanup'); } return undefined; } try { isRecursing = true; mountDependencies(store, effectAtom); return setter(a, ...args); } finally { recomputeInvalidatedAtoms(store); isRecursing = false; if (hasChanged) { hasChanged = false; runEffect(); } } }; try { runCleanup?.(); const cleanup = effectAtom.effect(getter, setter); if (typeof cleanup !== 'function') { return; } runCleanup = () => { if (inProgress) { return; } try { isSync = true; fromCleanup = true; return cleanup(); } finally { isSync = false; fromCleanup = false; runCleanup = undefined; } }; } finally { isSync = false; deps.forEach((depAtom) => { atomState.d.set(depAtom, ensureAtomState(store, depAtom).n); }); mountDependencies(store, effectAtom); recomputeInvalidatedAtoms(store); } } const buildingBlocks = getBuildingBlocks(store); const mountedAtoms = buildingBlocks[1]; const changedAtoms = buildingBlocks[3]; const storeHooks = initializeStoreHooks(buildingBlocks[6]); const ensureAtomState = buildingBlocks[11]; const flushCallbacks = buildingBlocks[12]; const recomputeInvalidatedAtoms = buildingBlocks[13]; const readAtomState = buildingBlocks[14]; const invalidateDependents = buildingBlocks[15]; const writeAtomState = buildingBlocks[16]; const mountDependencies = buildingBlocks[17]; const setAtomStateValueOrPromise = buildingBlocks[20]; const atomEffectChannel = ensureAtomEffectChannel(store, storeHooks); const atomState = ensureAtomState(store, effectAtom); // initialize atomState atomState.v = undefined; Object.assign(store.get(refAtom), [deps, atomState, mountedAtoms]); storeHooks.m.add(effectAtom, function atomOnMount() { // mounted atomEffectChannel.add(runEffect); if (runCleanup) { atomEffectChannel.delete(runCleanup); } }); storeHooks.u.add(effectAtom, function atomOnUnmount() { // unmounted atomEffectChannel.delete(runEffect); if (runCleanup) { atomEffectChannel.add(runCleanup); } }); storeHooks.c.add(effectAtom, function atomOnUpdate() { // changed if (isRecursing) { hasChanged = true; } else { atomEffectChannel.add(runEffect); } }); }; if (isDev()) { Object.defineProperty(refAtom, 'debugLabel', { get: () => effectAtom.debugLabel ? `${effectAtom.debugLabel}:ref` : undefined, configurable: true, enumerable: true, }); refAtom.debugPrivate = true; } return effectAtom; } const atomEffectChannelStoreMap = new WeakMap(); function ensureAtomEffectChannel(store, storeHooks) { let atomEffectChannel = atomEffectChannelStoreMap.get(store); if (!atomEffectChannel) { atomEffectChannel = new Set(); atomEffectChannelStoreMap.set(store, atomEffectChannel); storeHooks.f.add(function storeOnFlush() { // flush for (const fn of atomEffectChannel) { atomEffectChannel.delete(fn); fn(); } }); } return atomEffectChannel; } //# sourceMappingURL=atomEffect.js.map