UNPKG

jotai

Version:

👻 Next gen state management that will spook you

594 lines (586 loc) • 20.4 kB
import { useRef, useEffect, useContext, useCallback } from 'react'; import { useAtom, SECRET_INTERNAL_getScopeContext, SECRET_INTERNAL_useMutableSource } from 'jotai'; function useAtomDevtools(anAtom, name, scope) { let extension; try { extension = window.__REDUX_DEVTOOLS_EXTENSION__; } catch { } if (!extension) { if (typeof process === "object" && process.env.NODE_ENV === "development" && typeof window !== "undefined") { console.warn("Please install/enable Redux devtools extension"); } } const [value, setValue] = useAtom(anAtom, scope); const lastValue = useRef(value); const isTimeTraveling = useRef(false); const devtools = useRef(); const atomName = name || anAtom.debugLabel || anAtom.toString(); useEffect(() => { if (extension) { devtools.current = extension.connect({ name: atomName }); const unsubscribe = devtools.current.subscribe((message) => { var _a, _b, _c, _d, _e, _f; if (message.type === "DISPATCH" && message.state) { if (((_a = message.payload) == null ? void 0 : _a.type) === "JUMP_TO_ACTION" || ((_b = message.payload) == null ? void 0 : _b.type) === "JUMP_TO_STATE") { isTimeTraveling.current = true; } setValue(JSON.parse(message.state)); } else if (message.type === "DISPATCH" && ((_c = message.payload) == null ? void 0 : _c.type) === "COMMIT") { (_d = devtools.current) == null ? void 0 : _d.init(lastValue.current); } else if (message.type === "DISPATCH" && ((_e = message.payload) == null ? void 0 : _e.type) === "IMPORT_STATE") { const computedStates = ((_f = message.payload.nextLiftedState) == null ? void 0 : _f.computedStates) || []; computedStates.forEach(({ state }, index) => { var _a2; if (index === 0) { (_a2 = devtools.current) == null ? void 0 : _a2.init(state); } else { setValue(state); } }); } }); devtools.current.shouldInit = true; return unsubscribe; } }, [anAtom, extension, atomName, setValue]); useEffect(() => { if (devtools.current) { lastValue.current = value; if (devtools.current.shouldInit) { devtools.current.init(value); devtools.current.shouldInit = false; } else if (isTimeTraveling.current) { isTimeTraveling.current = false; } else { devtools.current.send(`${atomName} - ${new Date().toLocaleString()}`, value); } } }, [anAtom, extension, atomName, value]); } var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; var __getOwnPropDescs = Object.getOwnPropertyDescriptors; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); const hasInitialValue = (atom) => "init" in atom; const IS_EQUAL_PROMISE = Symbol(); const INTERRUPT_PROMISE = Symbol(); const isInterruptablePromise = (promise) => !!promise[INTERRUPT_PROMISE]; const createInterruptablePromise = (promise) => { let interrupt; const interruptablePromise = new Promise((resolve, reject) => { interrupt = resolve; promise.then(resolve, reject); }); interruptablePromise[IS_EQUAL_PROMISE] = (p) => p === interruptablePromise || p === promise; interruptablePromise[INTERRUPT_PROMISE] = interrupt; return interruptablePromise; }; const GET_VERSION$1 = "v"; const READ_ATOM = "r"; const WRITE_ATOM = "w"; const FLUSH_PENDING = "f"; const SUBSCRIBE_ATOM = "s"; const RESTORE_ATOMS = "h"; const DEV_GET_ATOM_STATE = "a"; const DEV_GET_MOUNTED = "m"; const createStore = (initialValues, stateListener) => { let version = 0; const atomStateMap = new WeakMap(); const mountedMap = new WeakMap(); const pendingMap = new Map(); if (initialValues) { for (const [atom, value] of initialValues) { const atomState = { v: value, r: 0, d: new Map() }; if (typeof process === "object" && process.env.NODE_ENV !== "production") { Object.freeze(atomState); if (!hasInitialValue(atom)) { console.warn("Found initial value for derived atom which can cause unexpected behavior", atom); } } atomStateMap.set(atom, atomState); } } const getAtomState = (atom) => atomStateMap.get(atom); const wipAtomState = (atom, dependencies) => { const atomState = getAtomState(atom); const nextAtomState = __spreadProps(__spreadValues({ r: 0 }, atomState), { d: dependencies ? new Map(Array.from(dependencies).map((a) => { var _a, _b; return [a, (_b = (_a = getAtomState(a)) == null ? void 0 : _a.r) != null ? _b : 0]; })) : (atomState == null ? void 0 : atomState.d) || new Map() }); return [nextAtomState, (atomState == null ? void 0 : atomState.d) || new Map()]; }; const setAtomValue = (atom, value, dependencies, promise) => { var _a, _b; const [atomState, prevDependencies] = wipAtomState(atom, dependencies); if (promise && !((_a = atomState.p) == null ? void 0 : _a[IS_EQUAL_PROMISE](promise))) { return; } (_b = atomState.c) == null ? void 0 : _b.call(atomState); delete atomState.e; delete atomState.p; delete atomState.c; delete atomState.i; if (!("v" in atomState) || !Object.is(atomState.v, value)) { atomState.v = value; ++atomState.r; } commitAtomState(atom, atomState, dependencies && prevDependencies); }; const setAtomReadError = (atom, error, dependencies, promise) => { var _a, _b; const [atomState, prevDependencies] = wipAtomState(atom, dependencies); if (promise && !((_a = atomState.p) == null ? void 0 : _a[IS_EQUAL_PROMISE](promise))) { return; } (_b = atomState.c) == null ? void 0 : _b.call(atomState); delete atomState.p; delete atomState.c; delete atomState.i; atomState.e = error; commitAtomState(atom, atomState, prevDependencies); }; const setAtomReadPromise = (atom, promise, dependencies) => { var _a, _b; const [atomState, prevDependencies] = wipAtomState(atom, dependencies); if ((_a = atomState.p) == null ? void 0 : _a[IS_EQUAL_PROMISE](promise)) { return; } (_b = atomState.c) == null ? void 0 : _b.call(atomState); if (isInterruptablePromise(promise)) { atomState.p = promise; delete atomState.c; } else { const interruptablePromise = createInterruptablePromise(promise); atomState.p = interruptablePromise; atomState.c = interruptablePromise[INTERRUPT_PROMISE]; } commitAtomState(atom, atomState, prevDependencies); }; const setAtomInvalidated = (atom) => { const [atomState] = wipAtomState(atom); atomState.i = atomState.r; commitAtomState(atom, atomState); }; const setAtomWritePromise = (atom, promise) => { const [atomState] = wipAtomState(atom); if (promise) { atomState.w = promise; } else { delete atomState.w; } commitAtomState(atom, atomState); }; const scheduleReadAtomState = (atom, promise) => { promise.finally(() => { readAtomState(atom, true); }); }; const readAtomState = (atom, force) => { if (!force) { const atomState = getAtomState(atom); if (atomState) { atomState.d.forEach((_, a) => { if (a !== atom) { const aState = getAtomState(a); if (aState && !aState.e && !aState.p && aState.r === aState.i) { readAtomState(a, true); } } }); if (Array.from(atomState.d.entries()).every(([a, r]) => { const aState = getAtomState(a); return aState && !aState.e && !aState.p && aState.r !== aState.i && aState.r === r; })) { return atomState; } } } let error; let promise; let value; const dependencies = new Set(); try { const promiseOrValue = atom.read((a) => { dependencies.add(a); const aState = a === atom ? getAtomState(a) : readAtomState(a); if (aState) { if (aState.e) { throw aState.e; } if (aState.p) { throw aState.p; } return aState.v; } if (hasInitialValue(a)) { return a.init; } throw new Error("no atom init"); }); if (promiseOrValue instanceof Promise) { promise = promiseOrValue.then((value2) => { setAtomValue(atom, value2, dependencies, promise); flushPending(); }).catch((e) => { if (e instanceof Promise) { scheduleReadAtomState(atom, e); return e; } setAtomReadError(atom, e instanceof Error ? e : new Error(e), dependencies, promise); flushPending(); }); } else { value = promiseOrValue; } } catch (errorOrPromise) { if (errorOrPromise instanceof Promise) { promise = errorOrPromise; } else if (errorOrPromise instanceof Error) { error = errorOrPromise; } else { error = new Error(errorOrPromise); } } if (error) { setAtomReadError(atom, error, dependencies); } else if (promise) { setAtomReadPromise(atom, promise, dependencies); } else { setAtomValue(atom, value, dependencies); } return getAtomState(atom); }; const readAtom = (readingAtom) => { const atomState = readAtomState(readingAtom); return atomState; }; const addAtom = (addingAtom) => { let mounted = mountedMap.get(addingAtom); if (!mounted) { mounted = mountAtom(addingAtom); } flushPending(); return mounted; }; const canUnmountAtom = (atom, mounted) => !mounted.l.size && (!mounted.d.size || mounted.d.size === 1 && mounted.d.has(atom)); const delAtom = (deletingAtom) => { const mounted = mountedMap.get(deletingAtom); if (mounted && canUnmountAtom(deletingAtom, mounted)) { unmountAtom(deletingAtom); } flushPending(); }; const invalidateDependents = (atom) => { const mounted = mountedMap.get(atom); mounted == null ? void 0 : mounted.d.forEach((dependent) => { if (dependent === atom) { return; } setAtomInvalidated(dependent); invalidateDependents(dependent); }); }; const writeAtomState = (atom, update) => { var _a; const writePromise = (_a = getAtomState(atom)) == null ? void 0 : _a.w; if (writePromise) { writePromise.then(() => { writeAtomState(atom, update); flushPending(); }); return; } const writeGetter = (a, unstable_promise = false) => { const aState = readAtomState(a); if (aState.e) { throw aState.e; } if (aState.p) { if (typeof process === "object" && process.env.NODE_ENV !== "production") { if (unstable_promise) { console.info("promise option in getter is an experimental feature.", a); } else { console.warn("Reading pending atom state in write operation. We throw a promise for now.", a); } } if (unstable_promise) { return aState.p.then(() => writeGetter(a, unstable_promise)); } throw aState.p; } if ("v" in aState) { return aState.v; } if (typeof process === "object" && process.env.NODE_ENV !== "production") { console.warn("[Bug] no value found while reading atom in write operation. This is probably a bug.", a); } throw new Error("no value found"); }; const promiseOrVoid = atom.write(writeGetter, (a, v) => { if (a === atom) { if (!hasInitialValue(a)) { throw new Error("no atom init"); } if (v instanceof Promise) { const promise = v.then((resolvedValue) => { setAtomValue(a, resolvedValue); invalidateDependents(a); flushPending(); }).catch((e) => { setAtomReadError(atom, e instanceof Error ? e : new Error(e)); flushPending(); }); setAtomReadPromise(atom, promise); } else { setAtomValue(a, v); } invalidateDependents(a); } else { writeAtomState(a, v); } flushPending(); }, update); if (promiseOrVoid instanceof Promise) { const promise = promiseOrVoid.finally(() => { setAtomWritePromise(atom); flushPending(); }); setAtomWritePromise(atom, promise); } }; const writeAtom = (writingAtom, update) => { writeAtomState(writingAtom, update); flushPending(); }; const isActuallyWritableAtom = (atom) => !!atom.write; const mountAtom = (atom, initialDependent) => { const atomState = readAtomState(atom); atomState.d.forEach((_, a) => { if (a !== atom) { const aMounted = mountedMap.get(a); if (aMounted) { aMounted.d.add(atom); } else { mountAtom(a, atom); } } }); const mounted = { d: new Set(initialDependent && [initialDependent]), l: new Set(), u: void 0 }; mountedMap.set(atom, mounted); if (isActuallyWritableAtom(atom) && atom.onMount) { const setAtom = (update) => writeAtom(atom, update); mounted.u = atom.onMount(setAtom); } return mounted; }; const unmountAtom = (atom) => { var _a; const onUnmount = (_a = mountedMap.get(atom)) == null ? void 0 : _a.u; if (onUnmount) { onUnmount(); } mountedMap.delete(atom); const atomState = getAtomState(atom); if (atomState) { atomState.d.forEach((_, a) => { if (a !== atom) { const mounted = mountedMap.get(a); if (mounted) { mounted.d.delete(atom); if (canUnmountAtom(a, mounted)) { unmountAtom(a); } } } }); } else if (typeof process === "object" && process.env.NODE_ENV !== "production") { console.warn("[Bug] could not find atom state to unmount", atom); } }; const mountDependencies = (atom, atomState, prevDependencies) => { const dependencies = new Set(atomState.d.keys()); prevDependencies.forEach((_, a) => { if (dependencies.has(a)) { dependencies.delete(a); return; } const mounted = mountedMap.get(a); if (mounted) { mounted.d.delete(atom); if (canUnmountAtom(a, mounted)) { unmountAtom(a); } } }); dependencies.forEach((a) => { const mounted = mountedMap.get(a); if (mounted) { const dependents = mounted.d; dependents.add(atom); } else { mountAtom(a, atom); } }); }; const commitAtomState = (atom, atomState, prevDependencies) => { if (typeof process === "object" && process.env.NODE_ENV !== "production") { Object.freeze(atomState); } const isNewAtom = !atomStateMap.has(atom); atomStateMap.set(atom, atomState); if (stateListener) { stateListener(atom, isNewAtom); } ++version; if (!pendingMap.has(atom)) { pendingMap.set(atom, prevDependencies); } }; const flushPending = () => { const pending = Array.from(pendingMap); pendingMap.clear(); pending.forEach(([atom, prevDependencies]) => { const atomState = getAtomState(atom); if (atomState) { if (prevDependencies) { mountDependencies(atom, atomState, prevDependencies); } } else if (typeof process === "object" && process.env.NODE_ENV !== "production") { console.warn("[Bug] atom state not found in flush", atom); } const mounted = mountedMap.get(atom); mounted == null ? void 0 : mounted.l.forEach((listener) => listener()); }); }; const subscribeAtom = (atom, callback) => { const mounted = addAtom(atom); const listeners = mounted.l; listeners.add(callback); return () => { listeners.delete(callback); delAtom(atom); }; }; const restoreAtoms = (values) => { for (const [atom, value] of values) { if (hasInitialValue(atom)) { setAtomValue(atom, value); invalidateDependents(atom); } } flushPending(); }; if (typeof process === "object" && process.env.NODE_ENV !== "production") { return { [GET_VERSION$1]: () => version, [READ_ATOM]: readAtom, [WRITE_ATOM]: writeAtom, [FLUSH_PENDING]: flushPending, [SUBSCRIBE_ATOM]: subscribeAtom, [RESTORE_ATOMS]: restoreAtoms, [DEV_GET_ATOM_STATE]: (a) => atomStateMap.get(a), [DEV_GET_MOUNTED]: (a) => mountedMap.get(a) }; } return { [GET_VERSION$1]: () => version, [READ_ATOM]: readAtom, [WRITE_ATOM]: writeAtom, [FLUSH_PENDING]: flushPending, [SUBSCRIBE_ATOM]: subscribeAtom, [RESTORE_ATOMS]: restoreAtoms }; }; const TARGET = "_uMS_T"; const GET_VERSION = "_uMS_V"; const createMutableSource = (target, getVersion) => ({ [TARGET]: target, [GET_VERSION]: getVersion }); const createScopeContainerForProduction = (initialValues) => { const store = createStore(initialValues); const mutableSource = createMutableSource(store, store[GET_VERSION$1]); return [store, mutableSource]; }; const createScopeContainerForDevelopment = (initialValues) => { let devVersion = 0; const devListeners = new Set(); const devContainer = { atoms: Array.from(initialValues != null ? initialValues : []).map(([a]) => a) }; const stateListener = (updatedAtom, isNewAtom) => { ++devVersion; if (isNewAtom) { devContainer.atoms = [...devContainer.atoms, updatedAtom]; } Promise.resolve().then(() => { devListeners.forEach((listener) => listener()); }); }; const store = createStore(initialValues, stateListener); const mutableSource = createMutableSource(store, store[GET_VERSION$1]); const devMutableSource = createMutableSource(devContainer, () => devVersion); const devSubscribe = (_, callback) => { devListeners.add(callback); return () => devListeners.delete(callback); }; return [store, mutableSource, devMutableSource, devSubscribe]; }; const isDevScopeContainer = (scopeContainer) => { return scopeContainer.length > 2; }; typeof process === "object" && process.env.NODE_ENV !== "production" ? createScopeContainerForDevelopment : createScopeContainerForProduction; function useAtomsSnapshot(scope) { const ScopeContext = SECRET_INTERNAL_getScopeContext(scope); const scopeContainer = useContext(ScopeContext); if (!isDevScopeContainer(scopeContainer)) { throw Error("useAtomsSnapshot can only be used in dev mode."); } const [store, , devMutableSource, devSubscribe] = scopeContainer; const atoms = SECRET_INTERNAL_useMutableSource(devMutableSource, useCallback((devContainer) => [...devContainer.atoms], []), devSubscribe); const atomToAtomValueTuples = atoms.filter((atom) => { var _a; return !!((_a = store[DEV_GET_MOUNTED]) == null ? void 0 : _a.call(store, atom)); }).map((atom) => { var _a, _b; const atomState = (_b = (_a = store[DEV_GET_ATOM_STATE]) == null ? void 0 : _a.call(store, atom)) != null ? _b : {}; return [atom, atomState.v]; }); return new Map(atomToAtomValueTuples); } function useGotoAtomsSnapshot(scope) { const ScopeContext = SECRET_INTERNAL_getScopeContext(scope); const scopeContainer = useContext(ScopeContext); if (!isDevScopeContainer(scopeContainer)) { throw new Error("useGotoAtomsSnapshot can only be used in dev mode."); } const store = scopeContainer[0]; return useCallback((values) => { store[RESTORE_ATOMS](values); }, [store]); } export { useAtomDevtools, useAtomsSnapshot, useGotoAtomsSnapshot };