UNPKG

9.98 kBJavaScriptView Raw
1import { useRef, useEffect, useContext, useState, useCallback } from 'react';
2import { useAtom, SECRET_INTERNAL_getScopeContext } from 'jotai';
3
4function useAtomDevtools(anAtom, name, scope) {
5 let extension;
6 try {
7 extension = window.__REDUX_DEVTOOLS_EXTENSION__;
8 } catch {
9 }
10 if (!extension) {
11 if ((import.meta.env && import.meta.env.MODE) !== "production" && typeof window !== "undefined") {
12 console.warn("Please install/enable Redux devtools extension");
13 }
14 }
15 const [value, setValue] = useAtom(anAtom, scope);
16 const lastValue = useRef(value);
17 const isTimeTraveling = useRef(false);
18 const devtools = useRef();
19 const atomName = name || anAtom.debugLabel || anAtom.toString();
20 useEffect(() => {
21 if (extension) {
22 const setValueIfWritable = (value2) => {
23 if (typeof setValue === "function") {
24 setValue(value2);
25 return;
26 }
27 console.warn("[Warn] you cannot do write operations (Time-travelling, etc) in read-only atoms\n", anAtom);
28 };
29 devtools.current = extension.connect({ name: atomName });
30 const unsubscribe = devtools.current.subscribe((message) => {
31 var _a, _b, _c, _d, _e, _f;
32 if (message.type === "ACTION" && message.payload) {
33 try {
34 setValueIfWritable(JSON.parse(message.payload));
35 } catch (e) {
36 console.error("please dispatch a serializable value that JSON.parse() support\n", e);
37 }
38 } else if (message.type === "DISPATCH" && message.state) {
39 if (((_a = message.payload) == null ? void 0 : _a.type) === "JUMP_TO_ACTION" || ((_b = message.payload) == null ? void 0 : _b.type) === "JUMP_TO_STATE") {
40 isTimeTraveling.current = true;
41 setValueIfWritable(JSON.parse(message.state));
42 }
43 } else if (message.type === "DISPATCH" && ((_c = message.payload) == null ? void 0 : _c.type) === "COMMIT") {
44 (_d = devtools.current) == null ? void 0 : _d.init(lastValue.current);
45 } else if (message.type === "DISPATCH" && ((_e = message.payload) == null ? void 0 : _e.type) === "IMPORT_STATE") {
46 const computedStates = ((_f = message.payload.nextLiftedState) == null ? void 0 : _f.computedStates) || [];
47 computedStates.forEach(({ state }, index) => {
48 var _a2;
49 if (index === 0) {
50 (_a2 = devtools.current) == null ? void 0 : _a2.init(state);
51 } else {
52 setValueIfWritable(state);
53 }
54 });
55 }
56 });
57 devtools.current.shouldInit = true;
58 return unsubscribe;
59 }
60 }, [anAtom, extension, atomName, setValue]);
61 useEffect(() => {
62 if (devtools.current) {
63 lastValue.current = value;
64 if (devtools.current.shouldInit) {
65 devtools.current.init(value);
66 devtools.current.shouldInit = false;
67 } else if (isTimeTraveling.current) {
68 isTimeTraveling.current = false;
69 } else {
70 devtools.current.send(`${atomName} - ${new Date().toLocaleString()}`, value);
71 }
72 }
73 }, [anAtom, extension, atomName, value]);
74}
75
76const RESTORE_ATOMS = "h";
77const DEV_SUBSCRIBE_STATE = "n";
78const DEV_GET_MOUNTED_ATOMS = "l";
79const DEV_GET_ATOM_STATE = "a";
80const DEV_GET_MOUNTED = "m";
81
82const createAtomsSnapshot = (store, atoms) => {
83 const tuples = atoms.map((atom) => {
84 var _a, _b;
85 const atomState = (_b = (_a = store[DEV_GET_ATOM_STATE]) == null ? void 0 : _a.call(store, atom)) != null ? _b : {};
86 return [atom, "v" in atomState ? atomState.v : void 0];
87 });
88 return new Map(tuples);
89};
90function useAtomsSnapshot(scope) {
91 const ScopeContext = SECRET_INTERNAL_getScopeContext(scope);
92 const scopeContainer = useContext(ScopeContext);
93 const store = scopeContainer.s;
94 if (!store[DEV_SUBSCRIBE_STATE]) {
95 throw new Error("useAtomsSnapshot can only be used in dev mode.");
96 }
97 const [atomsSnapshot, setAtomsSnapshot] = useState(() => /* @__PURE__ */ new Map());
98 useEffect(() => {
99 var _a;
100 const callback = () => {
101 var _a2;
102 const atoms = Array.from(((_a2 = store[DEV_GET_MOUNTED_ATOMS]) == null ? void 0 : _a2.call(store)) || []);
103 setAtomsSnapshot(createAtomsSnapshot(store, atoms));
104 };
105 const unsubscribe = (_a = store[DEV_SUBSCRIBE_STATE]) == null ? void 0 : _a.call(store, callback);
106 callback();
107 return unsubscribe;
108 }, [store]);
109 return atomsSnapshot;
110}
111
112function useGotoAtomsSnapshot(scope) {
113 const ScopeContext = SECRET_INTERNAL_getScopeContext(scope);
114 const scopeContainer = useContext(ScopeContext);
115 const store = scopeContainer.s;
116 if (!store[DEV_SUBSCRIBE_STATE]) {
117 throw new Error("useGotoAtomsSnapshot can only be used in dev mode.");
118 }
119 return useCallback((values) => {
120 store[RESTORE_ATOMS](values);
121 }, [store]);
122}
123
124const isEqualAtomsValues = (left, right) => left.size === right.size && Array.from(left).every(([left2, v]) => Object.is(right.get(left2), v));
125const isEqualAtomsDependents = (left, right) => left.size === right.size && Array.from(left).every(([a, dLeft]) => {
126 const dRight = right.get(a);
127 return dRight && dLeft.size === dRight.size && Array.from(dLeft).every((d) => dRight.has(d));
128});
129const atomToPrintable = (atom) => atom.debugLabel ? `${atom}:${atom.debugLabel}` : `${atom}`;
130const getDevtoolsState = (atomsSnapshot) => {
131 const values = {};
132 atomsSnapshot[0].forEach((v, atom) => {
133 values[atomToPrintable(atom)] = v;
134 });
135 const dependents = {};
136 atomsSnapshot[1].forEach((d, atom) => {
137 dependents[atomToPrintable(atom)] = Array.from(d).map(atomToPrintable);
138 });
139 return {
140 values,
141 dependents
142 };
143};
144function useAtomsDevtools(name, scope) {
145 const ScopeContext = SECRET_INTERNAL_getScopeContext(scope);
146 const { s: store, w: versionedWrite } = useContext(ScopeContext);
147 if (!store[DEV_SUBSCRIBE_STATE]) {
148 throw new Error("useAtomsDevtools can only be used in dev mode.");
149 }
150 const [atomsSnapshot, setAtomsSnapshot] = useState(() => [
151 /* @__PURE__ */ new Map(),
152 /* @__PURE__ */ new Map()
153 ]);
154 useEffect(() => {
155 var _a;
156 const callback = () => {
157 var _a2, _b, _c;
158 const values = /* @__PURE__ */ new Map();
159 const dependents = /* @__PURE__ */ new Map();
160 for (const atom of ((_a2 = store[DEV_GET_MOUNTED_ATOMS]) == null ? void 0 : _a2.call(store)) || []) {
161 const atomState = (_b = store[DEV_GET_ATOM_STATE]) == null ? void 0 : _b.call(store, atom);
162 if (atomState) {
163 if (atomState.r === atomState.i) {
164 return;
165 }
166 if ("v" in atomState) {
167 values.set(atom, atomState.v);
168 }
169 }
170 const mounted = (_c = store[DEV_GET_MOUNTED]) == null ? void 0 : _c.call(store, atom);
171 if (mounted) {
172 dependents.set(atom, mounted.t);
173 }
174 }
175 setAtomsSnapshot((prev) => {
176 if (isEqualAtomsValues(prev[0], values) && isEqualAtomsDependents(prev[1], dependents)) {
177 return prev;
178 }
179 return [values, dependents];
180 });
181 };
182 const unsubscribe = (_a = store[DEV_SUBSCRIBE_STATE]) == null ? void 0 : _a.call(store, callback);
183 callback();
184 return unsubscribe;
185 }, [store]);
186 const goToSnapshot = useCallback((values) => {
187 if (versionedWrite) {
188 versionedWrite((version) => {
189 store[RESTORE_ATOMS](values, version);
190 });
191 } else {
192 store[RESTORE_ATOMS](values);
193 }
194 }, [store, versionedWrite]);
195 let extension;
196 try {
197 extension = window.__REDUX_DEVTOOLS_EXTENSION__;
198 } catch {
199 }
200 if (!extension) {
201 if ((import.meta.env && import.meta.env.MODE) !== "production" && typeof window !== "undefined") {
202 console.warn("Please install/enable Redux devtools extension");
203 }
204 }
205 if (!store[DEV_SUBSCRIBE_STATE]) {
206 throw new Error("useAtomsSnapshot can only be used in dev mode.");
207 }
208 const isTimeTraveling = useRef(false);
209 const isRecording = useRef(true);
210 const devtools = useRef();
211 const snapshots = useRef([]);
212 useEffect(() => {
213 if (extension) {
214 const getSnapshotAt = (index = snapshots.current.length - 1) => {
215 const snapshot = snapshots.current[index >= 0 ? index : 0];
216 if (!snapshot) {
217 throw new Error("snaphost index out of bounds");
218 }
219 return snapshot;
220 };
221 const connection = extension.connect({ name });
222 const devtoolsUnsubscribe = connection.subscribe((message) => {
223 var _a;
224 switch (message.type) {
225 case "DISPATCH":
226 switch ((_a = message.payload) == null ? void 0 : _a.type) {
227 case "RESET":
228 break;
229 case "COMMIT":
230 connection.init(getDevtoolsState(getSnapshotAt()));
231 snapshots.current = [];
232 break;
233 case "JUMP_TO_ACTION":
234 case "JUMP_TO_STATE":
235 isTimeTraveling.current = true;
236 goToSnapshot(getSnapshotAt(message.payload.actionId - 1)[0]);
237 break;
238 case "PAUSE_RECORDING":
239 isRecording.current = !isRecording.current;
240 break;
241 }
242 }
243 });
244 devtools.current = connection;
245 devtools.current.shouldInit = true;
246 return devtoolsUnsubscribe;
247 }
248 }, [extension, goToSnapshot, name]);
249 useEffect(() => {
250 if (!devtools.current) {
251 return;
252 }
253 if (devtools.current.shouldInit) {
254 devtools.current.init(void 0);
255 devtools.current.shouldInit = false;
256 return;
257 }
258 if (isTimeTraveling.current) {
259 isTimeTraveling.current = false;
260 } else if (isRecording.current) {
261 snapshots.current.push(atomsSnapshot);
262 devtools.current.send({
263 type: `${snapshots.current.length}`,
264 updatedAt: new Date().toLocaleString()
265 }, getDevtoolsState(atomsSnapshot));
266 }
267 }, [atomsSnapshot]);
268}
269
270export { useAtomDevtools, useAtomsDevtools, useAtomsSnapshot, useGotoAtomsSnapshot };