UNPKG

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