1 | import { useRef, useEffect, useContext, useState, useCallback } from 'react';
|
2 | import { useAtom, SECRET_INTERNAL_getScopeContext } from 'jotai';
|
3 |
|
4 | function 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 |
|
76 | const RESTORE_ATOMS = "h";
|
77 | const DEV_SUBSCRIBE_STATE = "n";
|
78 | const DEV_GET_MOUNTED_ATOMS = "l";
|
79 | const DEV_GET_ATOM_STATE = "a";
|
80 | const DEV_GET_MOUNTED = "m";
|
81 |
|
82 | const 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 | };
|
90 | function 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(() => 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 |
|
112 | function 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 |
|
124 | const isEqualAtomsValues = (left, right) => left.size === right.size && Array.from(left).every(([left2, v]) => Object.is(right.get(left2), v));
|
125 | const 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 | });
|
129 | const atomToPrintable = (atom) => atom.debugLabel ? `${atom}:${atom.debugLabel}` : `${atom}`;
|
130 | const 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 | };
|
144 | function 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 | new Map(),
|
152 | new Map()
|
153 | ]);
|
154 | useEffect(() => {
|
155 | var _a;
|
156 | const callback = () => {
|
157 | var _a2, _b, _c;
|
158 | const values = new Map();
|
159 | const dependents = 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 |
|
270 | export { useAtomDevtools, useAtomsDevtools, useAtomsSnapshot, useGotoAtomsSnapshot };
|