UNPKG

jotai

Version:

👻 Primitive and flexible state management for React

627 lines (624 loc) • 20.5 kB
let keyCount = 0; function atom(read, write) { const key = `atom${++keyCount}`; const config = { toString: () => key }; if (typeof read === "function") { config.read = read; } else { config.init = read; config.read = defaultRead; config.write = defaultWrite; } if (write) { config.write = write; } return config; } function defaultRead(get) { return get(this); } function defaultWrite(get, set, arg) { return set( this, typeof arg === "function" ? arg(get(this)) : arg ); } const isSelfAtom = (atom, a) => atom.unstable_is ? atom.unstable_is(a) : a === atom; const hasInitialValue = (atom) => "init" in atom; const isActuallyWritableAtom = (atom) => !!atom.write; const cancelPromiseMap = /* @__PURE__ */ new WeakMap(); const registerCancelPromise = (promise, cancel) => { cancelPromiseMap.set(promise, cancel); promise.catch(() => { }).finally(() => cancelPromiseMap.delete(promise)); }; const cancelPromise = (promise, next) => { const cancel = cancelPromiseMap.get(promise); if (cancel) { cancelPromiseMap.delete(promise); cancel(next); } }; const resolvePromise = (promise, value) => { promise.status = "fulfilled"; promise.value = value; }; const rejectPromise = (promise, e) => { promise.status = "rejected"; promise.reason = e; }; const isPromiseLike = (x) => typeof (x == null ? void 0 : x.then) === "function"; const isEqualAtomValue = (a, b) => !!a && "v" in a && "v" in b && Object.is(a.v, b.v); const isEqualAtomError = (a, b) => !!a && "e" in a && "e" in b && Object.is(a.e, b.e); const hasPromiseAtomValue = (a) => !!a && "v" in a && a.v instanceof Promise; const isEqualPromiseAtomValue = (a, b) => "v" in a && "v" in b && a.v.orig && a.v.orig === b.v.orig; const returnAtomValue = (atomState) => { if ("e" in atomState) { throw atomState.e; } return atomState.v; }; const createStore = () => { const atomStateMap = /* @__PURE__ */ new WeakMap(); const mountedMap = /* @__PURE__ */ new WeakMap(); const pendingStack = []; const pendingMap = /* @__PURE__ */ new WeakMap(); let storeListenersRev2; let mountedAtoms; if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") { storeListenersRev2 = /* @__PURE__ */ new Set(); mountedAtoms = /* @__PURE__ */ new Set(); } const getAtomState = (atom) => atomStateMap.get(atom); const addPendingDependent = (atom, atomState) => { atomState.d.forEach((_, a) => { var _a; if (!pendingMap.has(a)) { const aState = getAtomState(a); (_a = pendingStack[pendingStack.length - 1]) == null ? void 0 : _a.add(a); pendingMap.set(a, [aState, /* @__PURE__ */ new Set()]); if (aState) { addPendingDependent(a, aState); } } pendingMap.get(a)[1].add(atom); }); }; const setAtomState = (atom, atomState) => { var _a; if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") { Object.freeze(atomState); } const prevAtomState = getAtomState(atom); atomStateMap.set(atom, atomState); if (!pendingMap.has(atom)) { (_a = pendingStack[pendingStack.length - 1]) == null ? void 0 : _a.add(atom); pendingMap.set(atom, [prevAtomState, /* @__PURE__ */ new Set()]); addPendingDependent(atom, atomState); } if (hasPromiseAtomValue(prevAtomState)) { const next = "v" in atomState ? atomState.v instanceof Promise ? atomState.v : Promise.resolve(atomState.v) : Promise.reject(atomState.e); if (prevAtomState.v !== next) { cancelPromise(prevAtomState.v, next); } } }; const updateDependencies = (atom, nextAtomState, nextDependencies, keepPreviousDependencies) => { const dependencies = new Map( keepPreviousDependencies ? nextAtomState.d : null ); let changed = false; nextDependencies.forEach((aState, a) => { if (!aState && isSelfAtom(atom, a)) { aState = nextAtomState; } if (aState) { dependencies.set(a, aState); if (nextAtomState.d.get(a) !== aState) { changed = true; } } else if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") { console.warn("[Bug] atom state not found"); } }); if (changed || nextAtomState.d.size !== dependencies.size) { nextAtomState.d = dependencies; } }; const setAtomValue = (atom, value, nextDependencies, keepPreviousDependencies) => { const prevAtomState = getAtomState(atom); const nextAtomState = { d: (prevAtomState == null ? void 0 : prevAtomState.d) || /* @__PURE__ */ new Map(), v: value }; if (nextDependencies) { updateDependencies( atom, nextAtomState, nextDependencies, keepPreviousDependencies ); } if (isEqualAtomValue(prevAtomState, nextAtomState) && prevAtomState.d === nextAtomState.d) { return prevAtomState; } if (hasPromiseAtomValue(prevAtomState) && hasPromiseAtomValue(nextAtomState) && isEqualPromiseAtomValue(prevAtomState, nextAtomState)) { if (prevAtomState.d === nextAtomState.d) { return prevAtomState; } else { nextAtomState.v = prevAtomState.v; } } setAtomState(atom, nextAtomState); return nextAtomState; }; const setAtomValueOrPromise = (atom, valueOrPromise, nextDependencies, abortPromise) => { if (isPromiseLike(valueOrPromise)) { let continuePromise; const updatePromiseDependencies = () => { const prevAtomState = getAtomState(atom); if (!hasPromiseAtomValue(prevAtomState) || prevAtomState.v !== promise) { return; } const nextAtomState = setAtomValue( atom, promise, nextDependencies ); if (mountedMap.has(atom) && prevAtomState.d !== nextAtomState.d) { mountDependencies(atom, nextAtomState, prevAtomState.d); } }; const promise = new Promise((resolve, reject) => { let settled = false; valueOrPromise.then( (v) => { if (!settled) { settled = true; resolvePromise(promise, v); resolve(v); updatePromiseDependencies(); } }, (e) => { if (!settled) { settled = true; rejectPromise(promise, e); reject(e); updatePromiseDependencies(); } } ); continuePromise = (next) => { if (!settled) { settled = true; next.then( (v) => resolvePromise(promise, v), (e) => rejectPromise(promise, e) ); resolve(next); } }; }); promise.orig = valueOrPromise; promise.status = "pending"; registerCancelPromise(promise, (next) => { if (next) { continuePromise(next); } abortPromise == null ? void 0 : abortPromise(); }); return setAtomValue(atom, promise, nextDependencies, true); } return setAtomValue(atom, valueOrPromise, nextDependencies); }; const setAtomError = (atom, error, nextDependencies) => { const prevAtomState = getAtomState(atom); const nextAtomState = { d: (prevAtomState == null ? void 0 : prevAtomState.d) || /* @__PURE__ */ new Map(), e: error }; if (nextDependencies) { updateDependencies(atom, nextAtomState, nextDependencies); } if (isEqualAtomError(prevAtomState, nextAtomState) && prevAtomState.d === nextAtomState.d) { return prevAtomState; } setAtomState(atom, nextAtomState); return nextAtomState; }; const readAtomState = (atom, force) => { const atomState = getAtomState(atom); if (!force && atomState) { if (mountedMap.has(atom)) { return atomState; } if (Array.from(atomState.d).every(([a, s]) => { if (a === atom) { return true; } const aState = readAtomState(a); return aState === s || isEqualAtomValue(aState, s); })) { return atomState; } } const nextDependencies = /* @__PURE__ */ new Map(); let isSync = true; const getter = (a) => { if (isSelfAtom(atom, a)) { const aState2 = getAtomState(a); if (aState2) { nextDependencies.set(a, aState2); return returnAtomValue(aState2); } if (hasInitialValue(a)) { nextDependencies.set(a, void 0); return a.init; } throw new Error("no atom init"); } const aState = readAtomState(a); nextDependencies.set(a, aState); return returnAtomValue(aState); }; let controller; let setSelf; const options = { get signal() { if (!controller) { controller = new AbortController(); } return controller.signal; }, get setSelf() { if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production" && !isActuallyWritableAtom(atom)) { console.warn("setSelf function cannot be used with read-only atom"); } if (!setSelf && isActuallyWritableAtom(atom)) { setSelf = (...args) => { if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production" && isSync) { console.warn("setSelf function cannot be called in sync"); } if (!isSync) { return writeAtom(atom, ...args); } }; } return setSelf; } }; try { const valueOrPromise = atom.read(getter, options); return setAtomValueOrPromise( atom, valueOrPromise, nextDependencies, () => controller == null ? void 0 : controller.abort() ); } catch (error) { return setAtomError(atom, error, nextDependencies); } finally { isSync = false; } }; const readAtom = (atom) => returnAtomValue(readAtomState(atom)); const addAtom = (atom) => { let mounted = mountedMap.get(atom); if (!mounted) { mounted = mountAtom(atom); } return mounted; }; const canUnmountAtom = (atom, mounted) => !mounted.l.size && (!mounted.t.size || mounted.t.size === 1 && mounted.t.has(atom)); const delAtom = (atom) => { const mounted = mountedMap.get(atom); if (mounted && canUnmountAtom(atom, mounted)) { unmountAtom(atom); } }; const recomputeDependents = (atom) => { const getDependents = (a) => { var _a, _b; const dependents = new Set((_a = mountedMap.get(a)) == null ? void 0 : _a.t); (_b = pendingMap.get(a)) == null ? void 0 : _b[1].forEach((dependent) => { dependents.add(dependent); }); return dependents; }; const topsortedAtoms = new Array(); const markedAtoms = /* @__PURE__ */ new Set(); const visit = (n) => { if (markedAtoms.has(n)) { return; } markedAtoms.add(n); for (const m of getDependents(n)) { if (n !== m) { visit(m); } } topsortedAtoms.push(n); }; visit(atom); const changedAtoms = /* @__PURE__ */ new Set([atom]); for (let i = topsortedAtoms.length - 1; i >= 0; --i) { const a = topsortedAtoms[i]; const prevAtomState = getAtomState(a); if (!prevAtomState) { continue; } let hasChangedDeps = false; for (const dep of prevAtomState.d.keys()) { if (dep !== a && changedAtoms.has(dep)) { hasChangedDeps = true; break; } } if (hasChangedDeps) { const nextAtomState = readAtomState(a, true); if (!isEqualAtomValue(prevAtomState, nextAtomState)) { changedAtoms.add(a); } } } }; const writeAtomState = (atom, ...args) => { let isSync = true; const getter = (a) => returnAtomValue(readAtomState(a)); const setter = (a, ...args2) => { let r; if (isSelfAtom(atom, a)) { if (!hasInitialValue(a)) { throw new Error("atom not writable"); } const prevAtomState = getAtomState(a); const nextAtomState = setAtomValueOrPromise(a, args2[0]); if (!isEqualAtomValue(prevAtomState, nextAtomState)) { recomputeDependents(a); } } else { r = writeAtomState(a, ...args2); } if (!isSync) { const flushed = flushPending([a]); if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") { storeListenersRev2.forEach( (l) => l({ type: "async-write", flushed }) ); } } return r; }; const result = atom.write(getter, setter, ...args); isSync = false; return result; }; const writeAtom = (atom, ...args) => { pendingStack.push(/* @__PURE__ */ new Set([atom])); const result = writeAtomState(atom, ...args); const flushed = flushPending(pendingStack.pop()); if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") { storeListenersRev2.forEach((l) => l({ type: "write", flushed })); } return result; }; const mountAtom = (atom, initialDependent, onMountQueue) => { var _a; const queue = onMountQueue || []; (_a = getAtomState(atom)) == null ? void 0 : _a.d.forEach((_, a) => { const aMounted = mountedMap.get(a); if (aMounted) { aMounted.t.add(atom); } else { if (a !== atom) { mountAtom(a, atom, queue); } } }); readAtomState(atom); const mounted = { t: new Set(initialDependent && [initialDependent]), l: /* @__PURE__ */ new Set() }; mountedMap.set(atom, mounted); if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") { mountedAtoms.add(atom); } if (isActuallyWritableAtom(atom) && atom.onMount) { const { onMount } = atom; queue.push(() => { const onUnmount = onMount((...args) => writeAtom(atom, ...args)); if (onUnmount) { mounted.u = onUnmount; } }); } if (!onMountQueue) { queue.forEach((f) => f()); } return mounted; }; const unmountAtom = (atom) => { var _a; const onUnmount = (_a = mountedMap.get(atom)) == null ? void 0 : _a.u; if (onUnmount) { onUnmount(); } mountedMap.delete(atom); if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") { mountedAtoms.delete(atom); } const atomState = getAtomState(atom); if (atomState) { if (hasPromiseAtomValue(atomState)) { cancelPromise(atomState.v); } atomState.d.forEach((_, a) => { if (a !== atom) { const mounted = mountedMap.get(a); if (mounted) { mounted.t.delete(atom); if (canUnmountAtom(a, mounted)) { unmountAtom(a); } } } }); } else if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") { console.warn("[Bug] could not find atom state to unmount", atom); } }; const mountDependencies = (atom, atomState, prevDependencies) => { const depSet = new Set(atomState.d.keys()); const maybeUnmountAtomSet = /* @__PURE__ */ new Set(); prevDependencies == null ? void 0 : prevDependencies.forEach((_, a) => { if (depSet.has(a)) { depSet.delete(a); return; } maybeUnmountAtomSet.add(a); const mounted = mountedMap.get(a); if (mounted) { mounted.t.delete(atom); } }); depSet.forEach((a) => { const mounted = mountedMap.get(a); if (mounted) { mounted.t.add(atom); } else if (mountedMap.has(atom)) { mountAtom(a, atom); } }); maybeUnmountAtomSet.forEach((a) => { const mounted = mountedMap.get(a); if (mounted && canUnmountAtom(a, mounted)) { unmountAtom(a); } }); }; const flushPending = (pendingAtoms) => { let flushed; if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") { flushed = /* @__PURE__ */ new Set(); } const pending = []; const collectPending = (pendingAtom) => { var _a; if (!pendingMap.has(pendingAtom)) { return; } const [prevAtomState, dependents] = pendingMap.get(pendingAtom); pendingMap.delete(pendingAtom); pending.push([pendingAtom, prevAtomState]); dependents.forEach(collectPending); (_a = getAtomState(pendingAtom)) == null ? void 0 : _a.d.forEach((_, a) => collectPending(a)); }; pendingAtoms.forEach(collectPending); pending.forEach(([atom, prevAtomState]) => { const atomState = getAtomState(atom); if (!atomState) { if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") { console.warn("[Bug] no atom state to flush"); } return; } if (atomState !== prevAtomState) { const mounted = mountedMap.get(atom); if (mounted && atomState.d !== (prevAtomState == null ? void 0 : prevAtomState.d)) { mountDependencies(atom, atomState, prevAtomState == null ? void 0 : prevAtomState.d); } if (mounted && !// TODO This seems pretty hacky. Hope to fix it. // Maybe we could `mountDependencies` in `setAtomState`? (!hasPromiseAtomValue(prevAtomState) && (isEqualAtomValue(prevAtomState, atomState) || isEqualAtomError(prevAtomState, atomState)))) { mounted.l.forEach((listener) => listener()); if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") { flushed.add(atom); } } } }); if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") { return flushed; } }; const subscribeAtom = (atom, listener) => { const mounted = addAtom(atom); const flushed = flushPending([atom]); const listeners = mounted.l; listeners.add(listener); if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") { storeListenersRev2.forEach( (l) => l({ type: "sub", flushed }) ); } return () => { listeners.delete(listener); delAtom(atom); if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") { storeListenersRev2.forEach((l) => l({ type: "unsub" })); } }; }; if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") { return { get: readAtom, set: writeAtom, sub: subscribeAtom, // store dev methods (these are tentative and subject to change without notice) dev_subscribe_store: (l, rev) => { if (rev !== 2) { throw new Error("The current StoreListener revision is 2."); } storeListenersRev2.add(l); return () => { storeListenersRev2.delete(l); }; }, dev_get_mounted_atoms: () => mountedAtoms.values(), dev_get_atom_state: (a) => atomStateMap.get(a), dev_get_mounted: (a) => mountedMap.get(a), dev_restore_atoms: (values) => { pendingStack.push(/* @__PURE__ */ new Set()); for (const [atom, valueOrPromise] of values) { if (hasInitialValue(atom)) { setAtomValueOrPromise(atom, valueOrPromise); recomputeDependents(atom); } } const flushed = flushPending(pendingStack.pop()); storeListenersRev2.forEach( (l) => l({ type: "restore", flushed }) ); } }; } return { get: readAtom, set: writeAtom, sub: subscribeAtom }; }; let defaultStore; if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") { if (typeof globalThis.__NUMBER_OF_JOTAI_INSTANCES__ === "number") { ++globalThis.__NUMBER_OF_JOTAI_INSTANCES__; } else { globalThis.__NUMBER_OF_JOTAI_INSTANCES__ = 1; } } const getDefaultStore = () => { if (!defaultStore) { if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production" && globalThis.__NUMBER_OF_JOTAI_INSTANCES__ !== 1) { console.warn( "Detected multiple Jotai instances. It may cause unexpected behavior with the default store. https://github.com/pmndrs/jotai/discussions/2044" ); } defaultStore = createStore(); } return defaultStore; }; export { atom, createStore, getDefaultStore };