3.4 KB121 lines
Blame
1import { useEffect, useRef } from 'react';
2import { AnyAtom, AnyAtomValue, AtomsSnapshot } from '../types';
3import {
4 Connection,
5 createReduxConnection,
6} from './redux-extension/createReduxConnection';
7import { getReduxExtension } from './redux-extension/getReduxExtension';
8import { SnapshotOptions, useAtomsSnapshot } from './useAtomsSnapshot';
9import { useGotoAtomsSnapshot } from './useGotoAtomsSnapshot';
10
11const atomToPrintable = (atom: AnyAtom) =>
12 atom.debugLabel ? `${atom}:${atom.debugLabel}` : `${atom}`;
13
14const getDevtoolsState = (atomsSnapshot: AtomsSnapshot) => {
15 const values: Record<string, AnyAtomValue> = {};
16 atomsSnapshot.values.forEach((v, atom) => {
17 values[atomToPrintable(atom)] = v;
18 });
19 const dependents: Record<string, string[]> = {};
20 atomsSnapshot.dependents.forEach((d, atom) => {
21 dependents[atomToPrintable(atom)] = Array.from(d).map(atomToPrintable);
22 });
23 return {
24 values,
25 dependents,
26 };
27};
28
29type DevtoolsOptions = SnapshotOptions & {
30 enabled?: boolean;
31};
32
33export function useAtomsDevtools(
34 name: string,
35 options?: DevtoolsOptions,
36): void {
37 const { enabled } = options || {};
38
39 const extension = getReduxExtension(enabled);
40
41 // This an exception, we don't usually use utils in themselves!
42 const atomsSnapshot = useAtomsSnapshot(options);
43 const goToSnapshot = useGotoAtomsSnapshot(options);
44
45 const isTimeTraveling = useRef(false);
46 const isRecording = useRef(true);
47 const devtools = useRef<Connection>();
48
49 const snapshots = useRef<AtomsSnapshot[]>([]);
50
51 useEffect(() => {
52 if (!extension) {
53 return;
54 }
55 const getSnapshotAt = (index = snapshots.current.length - 1) => {
56 // index 0 is @@INIT, so we need to return the next action (0)
57 const snapshot = snapshots.current[index >= 0 ? index : 0];
58 if (!snapshot) {
59 throw new Error('snapshot index out of bounds');
60 }
61 return snapshot;
62 };
63
64 devtools.current = createReduxConnection(extension, name);
65
66 const devtoolsUnsubscribe = devtools.current?.subscribe((message) => {
67 switch (message.type) {
68 case 'DISPATCH':
69 switch (message.payload?.type) {
70 case 'RESET':
71 // TODO
72 break;
73
74 case 'COMMIT':
75 devtools.current?.init(getDevtoolsState(getSnapshotAt()));
76 snapshots.current = [];
77 break;
78
79 case 'JUMP_TO_ACTION':
80 case 'JUMP_TO_STATE':
81 isTimeTraveling.current = true;
82 goToSnapshot(getSnapshotAt(message.payload.actionId - 1));
83 break;
84
85 case 'PAUSE_RECORDING':
86 isRecording.current = !isRecording.current;
87 break;
88 }
89 }
90 });
91
92 return () => {
93 extension?.disconnect?.();
94 devtoolsUnsubscribe?.();
95 };
96 }, [extension, goToSnapshot, name]);
97
98 useEffect(() => {
99 if (!devtools.current) {
100 return;
101 }
102 if (devtools.current.shouldInit) {
103 devtools.current.init(undefined);
104 devtools.current.shouldInit = false;
105 return;
106 }
107 if (isTimeTraveling.current) {
108 isTimeTraveling.current = false;
109 } else if (isRecording.current) {
110 snapshots.current.push(atomsSnapshot);
111 devtools.current.send(
112 {
113 type: `${snapshots.current.length}`,
114 updatedAt: new Date().toLocaleString(),
115 } as any,
116 getDevtoolsState(atomsSnapshot),
117 );
118 }
119 }, [atomsSnapshot]);
120}
121