UNPKG

jotai-devtools

Version:

A powerful toolkit to enhance your development experience with Jotai

516 lines (503 loc) 16 kB
// src/utils/useAtomsSnapshot.ts import { useEffect, useLayoutEffect, useRef, useState } from "react"; // src/utils/hooks/useDevToolsStore.ts import { useStore } from "jotai"; // src/utils/internals/compose-with-devtools.ts import { INTERNAL_overrideCreateStore } from "jotai/vanilla"; import { INTERNAL_buildStoreRev1 as INTERNAL_buildStore, INTERNAL_initializeStoreHooks } from "jotai/vanilla/internals"; var isDevToolsStore = (store) => { return "subscribeStore" in store; }; var __composeWithDevTools = (store) => { const { sub, set, get } = store; const storeListeners = /* @__PURE__ */ new Set(); const recentlySetAtomsMap = /* @__PURE__ */ new WeakMap(); const reduceCountOrRemoveRecentlySetAtom = (atom, onFound) => { const foundCount = recentlySetAtomsMap.get(atom); if (typeof foundCount === "number") { if (foundCount > 1) { recentlySetAtomsMap.set(atom, foundCount - 1); } else { recentlySetAtomsMap.delete(atom); } onFound?.(); } }; const increaseCountRecentlySetAtom = (atom) => { const foundCount = recentlySetAtomsMap.get(atom); recentlySetAtomsMap.set(atom, (foundCount || 0) + 1); }; store.sub = (...args) => { const unsub = sub(...args); storeListeners.forEach((l) => l({ type: "sub" })); return () => { unsub(); reduceCountOrRemoveRecentlySetAtom(args[0]); storeListeners.forEach((l) => l({ type: "unsub" })); }; }; store.get = (...args) => { const value = get(...args); reduceCountOrRemoveRecentlySetAtom(args[0], () => { if (value instanceof Promise) { value.finally(() => { Promise.resolve().then(() => { storeListeners.forEach((l) => l({ type: "async-get" })); }); }); } }); return value; }; store.set = (...args) => { const value = set(...args); increaseCountRecentlySetAtom(args[0]); storeListeners.forEach((l) => l({ type: "set" })); return value; }; store.subscribeStore = (l) => { storeListeners.add(l); return () => { storeListeners.delete(l); }; }; store.getMountedAtoms = () => { return store.get_mounted_atoms(); }; store.getAtomState = (atom) => { const aState = store.get_internal_weak_map().get(atom); if (aState) { return { v: aState.v, e: aState.e, d: new Set(aState.d.keys()) }; } return void 0; }; store.getMountedAtomState = (atom) => { const aState = store.get_internal_weak_map().get(atom); if (aState && "m" in aState) { return { l: aState.m.l, t: aState.m.t }; } return void 0; }; store.restoreAtoms = (values) => { store.restore_atoms(values); storeListeners.forEach((l) => l({ type: "restore" })); }; return store; }; var createDevStore = () => { let inRestoreAtom = 0; const storeHooks = INTERNAL_initializeStoreHooks({}); const atomStateMap = /* @__PURE__ */ new WeakMap(); const mountedAtoms = /* @__PURE__ */ new WeakMap(); const store = INTERNAL_buildStore( atomStateMap, mountedAtoms, void 0, void 0, void 0, void 0, storeHooks, void 0, (atom, get, set, ...args) => { if (inRestoreAtom) { return set(atom, ...args); } return atom.write(get, set, ...args); } ); const debugMountedAtoms = /* @__PURE__ */ new Set(); storeHooks.m.add(void 0, (atom) => { debugMountedAtoms.add(atom); const atomState = atomStateMap.get(atom); atomState.m = mountedAtoms.get(atom); }); storeHooks.u.add(void 0, (atom) => { debugMountedAtoms.delete(atom); const atomState = atomStateMap.get(atom); delete atomState.m; }); const devStore = { // store dev methods (these are tentative and subject to change without notice) get_internal_weak_map: () => atomStateMap, get_mounted_atoms: () => debugMountedAtoms, restore_atoms: (values) => { const restoreAtom = { read: () => null, write: (_get, set) => { ++inRestoreAtom; try { for (const [atom, value] of values) { if ("init" in atom) { set(atom, value); } } } finally { --inRestoreAtom; } } }; store.set(restoreAtom); } }; return { ...store, ...devStore }; }; var isDevStore = (store) => { return "get_internal_weak_map" in store; }; INTERNAL_overrideCreateStore((prev) => { return createDevStore; }); var composeWithDevTools = (store) => { if (isDevToolsStore(store)) { return store; } if (isDevStore(store)) { return __composeWithDevTools(store); } return store; }; // src/utils/hooks/useDevToolsStore.ts var useDevToolsStore = (options) => { const store = useStore(options); return composeWithDevTools(store); }; // src/utils/useAtomsSnapshot.ts var isEqualAtomsValues = (left, right) => left.size === right.size && Array.from(left).every(([left2, v]) => Object.is(right.get(left2), v)); var isEqualAtomsDependents = (left, right) => left.size === right.size && Array.from(left).every(([a, dLeft]) => { const dRight = right.get(a); return dRight && dLeft.size === dRight.size && Array.from(dLeft).every((d) => dRight.has(d)); }); function useAtomsSnapshot({ shouldShowPrivateAtoms = false, ...options } = {}) { const store = useDevToolsStore(options); const [atomsSnapshot, setAtomsSnapshot] = useState(() => ({ values: /* @__PURE__ */ new Map(), dependents: /* @__PURE__ */ new Map() })); const duringReactRenderPhase = useRef(true); duringReactRenderPhase.current = true; useLayoutEffect(() => { duringReactRenderPhase.current = false; }); useEffect(() => { if (!isDevToolsStore(store)) return; let prevValues = /* @__PURE__ */ new Map(); let prevDependents = /* @__PURE__ */ new Map(); const callback = () => { const values = /* @__PURE__ */ new Map(); const dependents = /* @__PURE__ */ new Map(); for (const atom of store.getMountedAtoms() || []) { if (!shouldShowPrivateAtoms && atom.debugPrivate) { continue; } const atomState = store.getAtomState(atom); if (atomState) { if ("v" in atomState) { values.set(atom, atomState.v); } } const mounted = store.getMountedAtomState(atom); if (mounted) { let atomDependents = mounted.t; if (!shouldShowPrivateAtoms) { atomDependents = new Set( Array.from(atomDependents.values()).filter( /* NOTE: This just removes private atoms from the dependents list, instead of hiding them from the dependency chain and showing the nested dependents of the private atoms. */ (dependent) => !dependent.debugPrivate ) ); } dependents.set(atom, atomDependents); } } if (isEqualAtomsValues(prevValues, values) && isEqualAtomsDependents(prevDependents, dependents)) { return; } prevValues = values; prevDependents = dependents; const deferrableAtomSetAction = () => setAtomsSnapshot({ values, dependents }); if (duringReactRenderPhase.current) { Promise.resolve().then(deferrableAtomSetAction); } else { deferrableAtomSetAction(); } }; const unsubscribe = store.subscribeStore(callback); callback(); return unsubscribe; }, [store, shouldShowPrivateAtoms]); return atomsSnapshot; } // src/utils/useGotoAtomsSnapshot.ts import { useCallback } from "react"; function useGotoAtomsSnapshot(options) { const store = useDevToolsStore(options); return useCallback( (snapshot) => { if (isDevToolsStore(store)) { store.restoreAtoms(snapshot.values); } }, [store] ); } // src/utils/useAtomsDebugValue.ts import { useDebugValue, useEffect as useEffect2, useLayoutEffect as useLayoutEffect2, useRef as useRef2, useState as useState2 } from "react"; var atomToPrintable = (atom) => atom.debugLabel || atom.toString(); var stateToPrintable = ([store, atoms]) => Object.fromEntries( atoms.flatMap((atom) => { const mounted = isDevToolsStore(store) && store.getMountedAtomState(atom); if (!mounted) { return []; } const dependents = mounted.t; const atomState = store.getAtomState(atom) || {}; return [ [ atomToPrintable(atom), { ..."e" in atomState && { error: atomState.e }, ..."v" in atomState && { value: atomState.v }, dependents: Array.from(dependents).map(atomToPrintable) } ] ]; }) ); var useAtomsDebugValue = (options) => { const enabled = options?.enabled ?? process.env.NODE_ENV !== "production"; const store = useDevToolsStore(options); const [atoms, setAtoms] = useState2([]); const duringReactRenderPhase = useRef2(true); duringReactRenderPhase.current = true; useLayoutEffect2(() => { duringReactRenderPhase.current = false; }); useEffect2(() => { if (!enabled || !isDevToolsStore(store)) { return; } const callback = () => { const deferrableAtomSetAction = () => setAtoms(Array.from(store.getMountedAtoms() || [])); if (duringReactRenderPhase.current) { Promise.resolve().then(deferrableAtomSetAction); } else { deferrableAtomSetAction(); } }; const unsubscribe = store.subscribeStore(callback); callback(); return unsubscribe; }, [enabled, store]); useDebugValue([store, atoms], stateToPrintable); }; // src/utils/useAtomDevtools.ts import { useEffect as useEffect3, useRef as useRef3 } from "react"; import { useAtom } from "jotai/react"; // src/utils/redux-extension/createReduxConnection.ts var createReduxConnection = (extension, name) => { if (!extension) return void 0; const connection = extension.connect({ name }); return Object.assign(connection, { shouldInit: true }); }; // src/utils/redux-extension/getReduxExtension.ts var getReduxExtension = (enabled = process.env.NODE_ENV !== "production") => { if (!enabled) { return void 0; } const reduxExtension = window.__REDUX_DEVTOOLS_EXTENSION__; if (!reduxExtension && process.env.NODE_ENV !== "production") { console.warn("Please install/enable Redux devtools extension"); return void 0; } return reduxExtension; }; // src/utils/useAtomDevtools.ts function useAtomDevtools(anAtom, options) { const { enabled, name } = options || {}; const extension = getReduxExtension(enabled); const [value, setValue] = useAtom(anAtom, options); const lastValue = useRef3(value); const isTimeTraveling = useRef3(false); const devtools = useRef3(); const atomName = name || anAtom.debugLabel || anAtom.toString(); useEffect3(() => { if (!extension) { return; } const setValueIfWritable = (value2) => { if (typeof setValue === "function") { setValue(value2); return; } console.warn( "[Warn] you cannot do write operations (Time-travelling, etc) in read-only atoms\n", anAtom ); }; devtools.current = createReduxConnection(extension, atomName); const unsubscribe = devtools.current?.subscribe((message) => { if (message.type === "ACTION" && message.payload) { try { setValueIfWritable(JSON.parse(message.payload)); } catch (e) { console.error( "please dispatch a serializable value that JSON.parse() support\n", e ); } } else if (message.type === "DISPATCH" && message.state) { if (message.payload?.type === "JUMP_TO_ACTION" || message.payload?.type === "JUMP_TO_STATE") { isTimeTraveling.current = true; setValueIfWritable(JSON.parse(message.state)); } } else if (message.type === "DISPATCH" && message.payload?.type === "COMMIT") { devtools.current?.init(lastValue.current); } else if (message.type === "DISPATCH" && message.payload?.type === "IMPORT_STATE") { const computedStates = message.payload.nextLiftedState?.computedStates || []; computedStates.forEach(({ state }, index) => { if (index === 0) { devtools.current?.init(state); } else { setValueIfWritable(state); } }); } }); return unsubscribe; }, [anAtom, extension, atomName, setValue]); useEffect3(() => { if (!devtools.current) { return; } 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} - ${(/* @__PURE__ */ new Date()).toLocaleString()}`, value ); } }, [anAtom, extension, atomName, value]); } // src/utils/useAtomsDevtools.ts import { useEffect as useEffect4, useRef as useRef4 } from "react"; var atomToPrintable2 = (atom) => atom.debugLabel && !String(atom).includes(":") ? `${atom}:${atom.debugLabel}` : `${atom}`; var getDevtoolsState = (atomsSnapshot) => { const values = {}; atomsSnapshot.values.forEach((v, atom) => { values[atomToPrintable2(atom)] = v; }); const dependents = {}; atomsSnapshot.dependents.forEach((d, atom) => { dependents[atomToPrintable2(atom)] = Array.from(d).map(atomToPrintable2); }); return { values, dependents }; }; function useAtomsDevtools(name, options) { const { enabled } = options || {}; const extension = getReduxExtension(enabled); const atomsSnapshot = useAtomsSnapshot(options); const goToSnapshot = useGotoAtomsSnapshot(options); const isTimeTraveling = useRef4(false); const isRecording = useRef4(true); const devtools = useRef4(); const snapshots = useRef4([]); useEffect4(() => { if (!extension) { return; } const getSnapshotAt = (index = snapshots.current.length - 1) => { const snapshot = snapshots.current[index >= 0 ? index : 0]; if (!snapshot) { throw new Error("snapshot index out of bounds"); } return snapshot; }; devtools.current = createReduxConnection(extension, name); const devtoolsUnsubscribe = devtools.current?.subscribe((message) => { switch (message.type) { case "DISPATCH": switch (message.payload?.type) { case "RESET": break; case "COMMIT": devtools.current?.init(getDevtoolsState(getSnapshotAt())); snapshots.current = []; break; case "JUMP_TO_ACTION": case "JUMP_TO_STATE": isTimeTraveling.current = true; goToSnapshot(getSnapshotAt(message.payload.actionId - 1)); break; case "PAUSE_RECORDING": isRecording.current = !isRecording.current; break; } } }); return () => { extension?.disconnect?.(); devtoolsUnsubscribe?.(); }; }, [extension, goToSnapshot, name]); useEffect4(() => { if (!devtools.current) { return; } if (devtools.current.shouldInit) { devtools.current.init(void 0); devtools.current.shouldInit = false; return; } if (isTimeTraveling.current) { isTimeTraveling.current = false; } else if (isRecording.current) { snapshots.current.push(atomsSnapshot); devtools.current.send( { type: `${snapshots.current.length}`, updatedAt: (/* @__PURE__ */ new Date()).toLocaleString() }, getDevtoolsState(atomsSnapshot) ); } }, [atomsSnapshot]); } export { isDevToolsStore, useDevToolsStore, useAtomsSnapshot, useGotoAtomsSnapshot, useAtomsDebugValue, useAtomDevtools, useAtomsDevtools };