@buoy-gg/jotai 7.0.16 → 7.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/index.js +1 -0
- package/lib/commonjs/jotai/components/JotaiAtomBrowser.js +11 -3
- package/lib/commonjs/jotai/components/JotaiAtomDetailContent.js +4 -3
- package/lib/commonjs/jotai/components/JotaiModal.js +35 -4
- package/lib/commonjs/jotai/sync/jotaiSyncAdapter.js +139 -5
- package/lib/commonjs/jotai/utils/jotaiStateStore.js +73 -7
- package/lib/commonjs/jotai/utils/snapshotProvider.js +233 -0
- package/lib/commonjs/jotai/utils/watchAtoms.js +5 -3
- package/lib/module/index.js +3 -0
- package/lib/module/jotai/components/JotaiAtomBrowser.js +11 -3
- package/lib/module/jotai/components/JotaiAtomDetailContent.js +4 -2
- package/lib/module/jotai/components/JotaiModal.js +36 -5
- package/lib/module/jotai/sync/jotaiSyncAdapter.js +138 -4
- package/lib/module/jotai/utils/jotaiStateStore.js +73 -7
- package/lib/module/jotai/utils/snapshotProvider.js +232 -0
- package/lib/module/jotai/utils/watchAtoms.js +5 -3
- package/lib/typescript/index.d.ts +2 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/jotai/components/JotaiAtomBrowser.d.ts +3 -1
- package/lib/typescript/jotai/components/JotaiAtomBrowser.d.ts.map +1 -1
- package/lib/typescript/jotai/components/JotaiAtomDetailContent.d.ts +7 -2
- package/lib/typescript/jotai/components/JotaiAtomDetailContent.d.ts.map +1 -1
- package/lib/typescript/jotai/components/JotaiModal.d.ts +1 -1
- package/lib/typescript/jotai/components/JotaiModal.d.ts.map +1 -1
- package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts +54 -2
- package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/jotai/types/index.d.ts +28 -0
- package/lib/typescript/jotai/types/index.d.ts.map +1 -1
- package/lib/typescript/jotai/utils/jotaiStateStore.d.ts +10 -0
- package/lib/typescript/jotai/utils/jotaiStateStore.d.ts.map +1 -1
- package/lib/typescript/jotai/utils/snapshotProvider.d.ts +2 -0
- package/lib/typescript/jotai/utils/snapshotProvider.d.ts.map +1 -0
- package/lib/typescript/jotai/utils/watchAtoms.d.ts.map +1 -1
- package/package.json +7 -4
|
@@ -271,9 +271,11 @@ export function JotaiAtomDetailContent({
|
|
|
271
271
|
changes,
|
|
272
272
|
selectedIndex,
|
|
273
273
|
onIndexChange,
|
|
274
|
-
disableInternalFooter = false
|
|
274
|
+
disableInternalFooter = false,
|
|
275
|
+
initialView = "change"
|
|
275
276
|
}) {
|
|
276
|
-
|
|
277
|
+
// View state (initialView seeds it once; taps own it afterwards)
|
|
278
|
+
const [activeView, setActiveView] = useState(initialView);
|
|
277
279
|
const [diffMode, setDiffMode] = useState("tree");
|
|
278
280
|
const totalChanges = changes.length;
|
|
279
281
|
const atomColor = jotaiStateStore.getAtomColor(change.atomLabel);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - Events tab: Live atom change monitoring with diffs
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { useState, useMemo, useRef, useCallback } from "react";
|
|
11
|
+
import { useState, useMemo, useRef, useCallback, useEffect } from "react";
|
|
12
12
|
import { View, Text, StyleSheet, FlatList, TextInput, TouchableOpacity } from "react-native";
|
|
13
13
|
import { JsModal, ModalHeader, TabSelector, macOSColors, buoyColors, Search, Filter, Power, X, Box, devToolsStorageKeys, ProUpgradeModal, PowerToggleButton, ToolbarCopyButton, ToolbarClearButton, truncatePayload, TickProvider } from "@buoy-gg/shared-ui";
|
|
14
14
|
import { useIsPro } from "@buoy-gg/license";
|
|
@@ -42,11 +42,16 @@ export function JotaiModal({
|
|
|
42
42
|
onBack,
|
|
43
43
|
onMinimize,
|
|
44
44
|
enableSharedModalDimensions = false,
|
|
45
|
-
inline = false
|
|
45
|
+
inline = false,
|
|
46
|
+
initialTab,
|
|
47
|
+
initialSelectedChangeId,
|
|
48
|
+
initialDetailView,
|
|
49
|
+
onChangeSelect,
|
|
50
|
+
onAtomExpand
|
|
46
51
|
}) {
|
|
47
52
|
const isPro = useIsPro();
|
|
48
53
|
const [showProModal, setShowProModal] = useState(false);
|
|
49
|
-
const [activeTab, setActiveTab] = useState("atoms");
|
|
54
|
+
const [activeTab, setActiveTab] = useState(initialTab ?? "atoms");
|
|
50
55
|
const [selectedHistoryAtom, setSelectedHistoryAtom] = useState(null);
|
|
51
56
|
const [showFilters, setShowFilters] = useState(false);
|
|
52
57
|
const [ignoredPatterns, setIgnoredPatterns] = useState(new Set());
|
|
@@ -98,6 +103,30 @@ export function JotaiModal({
|
|
|
98
103
|
}, [selectedChangeId, activeChanges]);
|
|
99
104
|
const selectedChange = selectedChangeIndex !== null ? activeChanges[selectedChangeIndex] : null;
|
|
100
105
|
|
|
106
|
+
// Notify the host on selection (keyed by change id so re-renders with the
|
|
107
|
+
// same selection don't re-fire; remote mirrors fetch atom values here).
|
|
108
|
+
const lastNotifiedSelectionRef = useRef(null);
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (!onChangeSelect) return;
|
|
111
|
+
const id = selectedChange?.id ?? null;
|
|
112
|
+
if (id === null || id === lastNotifiedSelectionRef.current) return;
|
|
113
|
+
lastNotifiedSelectionRef.current = id;
|
|
114
|
+
onChangeSelect(selectedChange);
|
|
115
|
+
}, [selectedChange, onChangeSelect]);
|
|
116
|
+
|
|
117
|
+
// Deep link into a change's detail (see initialSelectedChangeId). Applied
|
|
118
|
+
// at most once per id, so the list re-rendering — or the user pressing Back
|
|
119
|
+
// — never re-opens it.
|
|
120
|
+
const appliedInitialSelectionRef = useRef(null);
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (!initialSelectedChangeId) return;
|
|
123
|
+
if (appliedInitialSelectionRef.current === initialSelectedChangeId) return;
|
|
124
|
+
const exists = activeChanges.some(c => c.id === initialSelectedChangeId);
|
|
125
|
+
if (!exists) return;
|
|
126
|
+
appliedInitialSelectionRef.current = initialSelectedChangeId;
|
|
127
|
+
setSelectedChangeId(initialSelectedChangeId);
|
|
128
|
+
}, [initialSelectedChangeId, activeChanges]);
|
|
129
|
+
|
|
101
130
|
// Search state
|
|
102
131
|
const [searchText, setSearchText] = useState("");
|
|
103
132
|
const [isSearchActive, setIsSearchActive] = useState(false);
|
|
@@ -338,7 +367,8 @@ export function JotaiModal({
|
|
|
338
367
|
changes: activeChanges,
|
|
339
368
|
selectedIndex: selectedChangeIndex,
|
|
340
369
|
onIndexChange: handleIndexChange,
|
|
341
|
-
disableInternalFooter: true
|
|
370
|
+
disableInternalFooter: true,
|
|
371
|
+
initialView: initialDetailView
|
|
342
372
|
});
|
|
343
373
|
}
|
|
344
374
|
if (selectedHistoryAtom) {
|
|
@@ -371,7 +401,8 @@ export function JotaiModal({
|
|
|
371
401
|
return /*#__PURE__*/_jsx(JotaiAtomBrowser, {
|
|
372
402
|
atoms: displayedAtoms,
|
|
373
403
|
searchQuery: searchText,
|
|
374
|
-
onViewHistory: handleViewHistory
|
|
404
|
+
onViewHistory: handleViewHistory,
|
|
405
|
+
onAtomExpand: onAtomExpand
|
|
375
406
|
});
|
|
376
407
|
}
|
|
377
408
|
|
|
@@ -1,6 +1,60 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
import { jotaiStateStore } from "../utils/jotaiStateStore";
|
|
4
|
+
/**
|
|
5
|
+
* Sentinel used in WIRE-FORM snapshots in place of raw prevValue/nextValue.
|
|
6
|
+
* Same hazard as redux/zustand: up to 200 changes × 2 values on every
|
|
7
|
+
* snapshot. Dashboards fetch values via `getChangeDetail`.
|
|
8
|
+
*/
|
|
9
|
+
export const VALUE_ON_DEVICE = Object.freeze({
|
|
10
|
+
__buoyValueOnDevice: true,
|
|
11
|
+
note: "Atom values stay on the device — fetched on demand via getChangeDetail / getAtomValue."
|
|
12
|
+
});
|
|
13
|
+
const MAX_CHANGE_DETAIL_BYTES = 8 * 1024 * 1024;
|
|
14
|
+
const MAX_WIRE_PAYLOAD_BYTES = 16 * 1024;
|
|
15
|
+
function jsonLongerThan(value, limit) {
|
|
16
|
+
let bytes = 0;
|
|
17
|
+
const stack = [value];
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
while (stack.length > 0) {
|
|
20
|
+
if (bytes > limit) return true;
|
|
21
|
+
const v = stack.pop();
|
|
22
|
+
if (v === null || v === undefined) bytes += 4;else if (typeof v === "string") bytes += v.length + 2;else if (typeof v === "number") bytes += 8;else if (typeof v === "boolean") bytes += 5;else if (typeof v === "object") {
|
|
23
|
+
if (seen.has(v)) continue;
|
|
24
|
+
seen.add(v);
|
|
25
|
+
if (Array.isArray(v)) {
|
|
26
|
+
bytes += 2 + v.length;
|
|
27
|
+
for (let i = 0; i < v.length; i++) stack.push(v[i]);
|
|
28
|
+
} else {
|
|
29
|
+
bytes += 2;
|
|
30
|
+
for (const k in v) {
|
|
31
|
+
bytes += k.length + 4;
|
|
32
|
+
stack.push(v[k]);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} else bytes += 4;
|
|
36
|
+
}
|
|
37
|
+
return bytes > limit;
|
|
38
|
+
}
|
|
39
|
+
function toWireChange(change) {
|
|
40
|
+
return {
|
|
41
|
+
...change,
|
|
42
|
+
prevValue: VALUE_ON_DEVICE,
|
|
43
|
+
nextValue: VALUE_ON_DEVICE
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function toWireValue(value) {
|
|
47
|
+
try {
|
|
48
|
+
JSON.stringify(value);
|
|
49
|
+
} catch {
|
|
50
|
+
return VALUE_ON_DEVICE;
|
|
51
|
+
}
|
|
52
|
+
if (jsonLongerThan(value, MAX_WIRE_PAYLOAD_BYTES)) {
|
|
53
|
+
return VALUE_ON_DEVICE;
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
|
|
4
58
|
/**
|
|
5
59
|
* Sync adapter for the jotai tool, consumed by @buoy-gg/external-sync's
|
|
6
60
|
* `useExternalSync` (structurally matches its ToolSyncAdapter interface so
|
|
@@ -11,12 +65,19 @@ import { jotaiStateStore } from "../utils/jotaiStateStore";
|
|
|
11
65
|
* already records. The snapshot carries both the change timeline and the
|
|
12
66
|
* atom registry (with each atom's current value) so the dashboard can render
|
|
13
67
|
* the atom browser.
|
|
68
|
+
*
|
|
69
|
+
* v2: change-log prevValue/nextValue are replaced with on-device markers.
|
|
70
|
+
* Dashboards fetch values via `getChangeDetail`. Oversized / unserializable
|
|
71
|
+
* `atoms[].currentValue` uses the same marker and is fetched via `getAtomValue`.
|
|
14
72
|
*/
|
|
15
73
|
export const jotaiSyncAdapter = {
|
|
16
|
-
version:
|
|
74
|
+
version: 2,
|
|
17
75
|
getSnapshot: () => ({
|
|
18
|
-
changes: jotaiStateStore.getAtomChanges(),
|
|
19
|
-
atoms: jotaiStateStore.getAtomSnapshots()
|
|
76
|
+
changes: jotaiStateStore.getAtomChanges().map(toWireChange),
|
|
77
|
+
atoms: jotaiStateStore.getAtomSnapshots().map(atom => ({
|
|
78
|
+
...atom,
|
|
79
|
+
currentValue: toWireValue(atom.currentValue)
|
|
80
|
+
}))
|
|
20
81
|
}),
|
|
21
82
|
subscribe: onChange => {
|
|
22
83
|
const unsubscribeChanges = jotaiStateStore.subscribe(onChange);
|
|
@@ -30,6 +91,78 @@ export const jotaiSyncAdapter = {
|
|
|
30
91
|
clearEvents: () => {
|
|
31
92
|
jotaiStateStore.clearAtomChanges();
|
|
32
93
|
},
|
|
94
|
+
/**
|
|
95
|
+
* On-demand detail: one change's real prevValue/nextValue.
|
|
96
|
+
* The per-snapshot stream stays value-free; this is the explicit,
|
|
97
|
+
* size-guarded channel for the detail pane.
|
|
98
|
+
*/
|
|
99
|
+
getChangeDetail: params => {
|
|
100
|
+
const {
|
|
101
|
+
id
|
|
102
|
+
} = params ?? {};
|
|
103
|
+
if (!id) return {
|
|
104
|
+
found: false,
|
|
105
|
+
reason: "missing id"
|
|
106
|
+
};
|
|
107
|
+
const change = jotaiStateStore.getAtomChangeById(id);
|
|
108
|
+
if (!change) return {
|
|
109
|
+
found: false,
|
|
110
|
+
reason: "unknown id"
|
|
111
|
+
};
|
|
112
|
+
const cappedNote = {
|
|
113
|
+
__buoyTruncated: true,
|
|
114
|
+
note: `Exceeds the ${MAX_CHANGE_DETAIL_BYTES / (1024 * 1024)}MB detail limit — inspect it on-device.`
|
|
115
|
+
};
|
|
116
|
+
const cap = value => jsonLongerThan(value, MAX_CHANGE_DETAIL_BYTES) ? cappedNote : value;
|
|
117
|
+
return {
|
|
118
|
+
found: true,
|
|
119
|
+
id,
|
|
120
|
+
prevValue: cap(change.prevValue),
|
|
121
|
+
nextValue: cap(change.nextValue)
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
/**
|
|
125
|
+
* On-demand current value for one atom. Wire-form snapshots replace
|
|
126
|
+
* oversized / unserializable `currentValue` with VALUE_ON_DEVICE.
|
|
127
|
+
*/
|
|
128
|
+
getAtomValue: params => {
|
|
129
|
+
const {
|
|
130
|
+
label
|
|
131
|
+
} = params ?? {};
|
|
132
|
+
if (!label) return {
|
|
133
|
+
found: false,
|
|
134
|
+
reason: "missing label"
|
|
135
|
+
};
|
|
136
|
+
const atom = jotaiStateStore.getAtom(label);
|
|
137
|
+
if (!atom) return {
|
|
138
|
+
found: false,
|
|
139
|
+
reason: "unknown label"
|
|
140
|
+
};
|
|
141
|
+
let currentValue;
|
|
142
|
+
try {
|
|
143
|
+
currentValue = atom.getValue();
|
|
144
|
+
} catch {
|
|
145
|
+
currentValue = undefined;
|
|
146
|
+
}
|
|
147
|
+
const cappedNote = {
|
|
148
|
+
__buoyTruncated: true,
|
|
149
|
+
note: `Exceeds the ${MAX_CHANGE_DETAIL_BYTES / (1024 * 1024)}MB detail limit — inspect it on-device.`
|
|
150
|
+
};
|
|
151
|
+
try {
|
|
152
|
+
JSON.stringify(currentValue);
|
|
153
|
+
} catch {
|
|
154
|
+
return {
|
|
155
|
+
found: true,
|
|
156
|
+
label,
|
|
157
|
+
currentValue: VALUE_ON_DEVICE
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
found: true,
|
|
162
|
+
label,
|
|
163
|
+
currentValue: jsonLongerThan(currentValue, MAX_CHANGE_DETAIL_BYTES) ? cappedNote : currentValue
|
|
164
|
+
};
|
|
165
|
+
},
|
|
33
166
|
/**
|
|
34
167
|
* Compact current-value reader for a remote driver (MCP/LLM): registered
|
|
35
168
|
* atoms with light metadata; each atom's `currentValue` stays on the device
|
|
@@ -73,7 +206,8 @@ export const jotaiSyncAdapter = {
|
|
|
73
206
|
const {
|
|
74
207
|
label,
|
|
75
208
|
value
|
|
76
|
-
} = params;
|
|
209
|
+
} = params ?? {};
|
|
210
|
+
if (!label) throw new Error("setAtom requires a `label`");
|
|
77
211
|
jotaiStateStore.setAtomValue(label, value);
|
|
78
212
|
}
|
|
79
213
|
}
|
|
@@ -286,17 +286,83 @@ class JotaiStateStore {
|
|
|
286
286
|
*/
|
|
287
287
|
replaceFromSnapshot(changes, atomSnapshots) {
|
|
288
288
|
if (!this.isEnabled) return;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
289
|
+
|
|
290
|
+
// Preserve values a remote mirror already hydrated — wire-form snapshots
|
|
291
|
+
// always carry markers, and a later snapshot must not blank the detail pane.
|
|
292
|
+
const hydrated = new Map();
|
|
293
|
+
for (const existing of this.atomChanges) {
|
|
294
|
+
const prev = existing.prevValue;
|
|
295
|
+
if (!prev || prev.__buoyValueOnDevice !== true) {
|
|
296
|
+
hydrated.set(existing.id, existing);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
this.atomChanges = changes.slice(0, this.maxChanges).map(change => {
|
|
300
|
+
const kept = hydrated.get(change.id);
|
|
301
|
+
const incoming = change.prevValue;
|
|
302
|
+
return kept && incoming?.__buoyValueOnDevice === true ? {
|
|
303
|
+
...change,
|
|
304
|
+
prevValue: kept.prevValue,
|
|
305
|
+
nextValue: kept.nextValue
|
|
306
|
+
} : change;
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Preserve currentValue a remote mirror already hydrated — oversized
|
|
310
|
+
// atoms arrive as VALUE_ON_DEVICE markers on every snapshot.
|
|
311
|
+
const hydratedAtoms = new Map();
|
|
312
|
+
for (const [label, atom] of this.atoms) {
|
|
313
|
+
try {
|
|
314
|
+
const value = atom.getValue();
|
|
315
|
+
const marker = value;
|
|
316
|
+
if (!marker || marker.__buoyValueOnDevice !== true) {
|
|
317
|
+
hydratedAtoms.set(label, value);
|
|
318
|
+
}
|
|
319
|
+
} catch {
|
|
320
|
+
// ignore
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
this.atoms = new Map(atomSnapshots.map(snapshot => {
|
|
324
|
+
const incoming = snapshot.currentValue;
|
|
325
|
+
const kept = hydratedAtoms.get(snapshot.label);
|
|
326
|
+
const currentValue = kept !== undefined && incoming?.__buoyValueOnDevice === true ? kept : snapshot.currentValue;
|
|
327
|
+
return [snapshot.label, {
|
|
328
|
+
label: snapshot.label,
|
|
329
|
+
changeCount: snapshot.changeCount,
|
|
330
|
+
color: snapshot.color,
|
|
331
|
+
getValue: () => currentValue
|
|
332
|
+
}];
|
|
333
|
+
}));
|
|
296
334
|
this.notifyListeners();
|
|
297
335
|
this.notifyAtomListeners();
|
|
298
336
|
}
|
|
299
337
|
|
|
338
|
+
/**
|
|
339
|
+
* Hydrate one mirrored change with real values fetched on demand from the
|
|
340
|
+
* device via the adapter's `getChangeDetail`. Remote-mirror (desktop) only.
|
|
341
|
+
*/
|
|
342
|
+
hydrateChange(id, patch) {
|
|
343
|
+
const index = this.atomChanges.findIndex(c => c.id === id);
|
|
344
|
+
if (index === -1) return;
|
|
345
|
+
this.atomChanges = [...this.atomChanges.slice(0, index), {
|
|
346
|
+
...this.atomChanges[index],
|
|
347
|
+
...patch
|
|
348
|
+
}, ...this.atomChanges.slice(index + 1)];
|
|
349
|
+
this.notifyListeners();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Hydrate one mirrored atom's currentValue fetched on demand from the
|
|
354
|
+
* device via the adapter's `getAtomValue`. Remote-mirror (desktop) only.
|
|
355
|
+
*/
|
|
356
|
+
hydrateAtomValue(label, currentValue) {
|
|
357
|
+
const atom = this.atoms.get(label);
|
|
358
|
+
if (!atom) return;
|
|
359
|
+
this.atoms.set(label, {
|
|
360
|
+
...atom,
|
|
361
|
+
getValue: () => currentValue
|
|
362
|
+
});
|
|
363
|
+
this.notifyAtomListeners();
|
|
364
|
+
}
|
|
365
|
+
|
|
300
366
|
// ---- Atom Registry ----
|
|
301
367
|
|
|
302
368
|
registerAtom(label, getValue, setValue) {
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Time Machine snapshot provider for Jotai.
|
|
5
|
+
*
|
|
6
|
+
* Registered into @buoy-gg/shared-ui's stateSnapshotRegistry at module init;
|
|
7
|
+
* consumed by @buoy-gg/time-machine.
|
|
8
|
+
*
|
|
9
|
+
* Coverage is opt-in by nature: only atoms the app passed to `watchAtoms` /
|
|
10
|
+
* `watchDefaultStoreAtoms` are visible. Derived (read-only) atoms are captured
|
|
11
|
+
* for inspection but never restored — they recompute from their sources.
|
|
12
|
+
* Caveat (documented): atoms with custom write functions (`atomWithReducer`)
|
|
13
|
+
* receive ACTIONS through `store.set`, so restoring a raw value can misbehave;
|
|
14
|
+
* nothing in the registry distinguishes them from plain atoms.
|
|
15
|
+
*/
|
|
16
|
+
import { registerStateSnapshotProvider } from "@buoy-gg/shared-ui";
|
|
17
|
+
import { boundedDeepEqual } from "@buoy-gg/shared-ui/dataViewer";
|
|
18
|
+
import { jotaiStateStore } from "./jotaiStateStore";
|
|
19
|
+
function status() {
|
|
20
|
+
const atoms = jotaiStateStore.getAtoms();
|
|
21
|
+
if (atoms.length === 0) {
|
|
22
|
+
return {
|
|
23
|
+
canCapture: false,
|
|
24
|
+
canRestore: false,
|
|
25
|
+
detail: "No Jotai atoms registered — pass your atoms to watchAtoms() / watchDefaultStoreAtoms()"
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const writable = atoms.filter(a => a.setValue).length;
|
|
29
|
+
return {
|
|
30
|
+
canCapture: true,
|
|
31
|
+
canRestore: writable > 0,
|
|
32
|
+
detail: writable < atoms.length ? `${atoms.length - writable} derived atom(s) recompute automatically and are not restored` : undefined
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async function capture() {
|
|
36
|
+
const warnings = [];
|
|
37
|
+
const atoms = {};
|
|
38
|
+
let derived = 0;
|
|
39
|
+
for (const info of jotaiStateStore.getAtoms()) {
|
|
40
|
+
try {
|
|
41
|
+
const value = info.getValue();
|
|
42
|
+
if (typeof value === "function") {
|
|
43
|
+
warnings.push(`atom "${info.label}": function value not capturable`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (!info.setValue) derived += 1;
|
|
47
|
+
atoms[info.label] = {
|
|
48
|
+
value,
|
|
49
|
+
writable: Boolean(info.setValue)
|
|
50
|
+
};
|
|
51
|
+
} catch (e) {
|
|
52
|
+
warnings.push(`atom "${info.label}": capture failed (${String(e)})`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (derived > 0) {
|
|
56
|
+
warnings.push(`${derived} derived atom(s) captured for inspection only (recompute on restore)`);
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
data: {
|
|
60
|
+
atoms
|
|
61
|
+
},
|
|
62
|
+
warnings
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
async function restore(data, options) {
|
|
66
|
+
const snapshot = data;
|
|
67
|
+
const excludedSet = new Set(options?.excludeKeys ?? []);
|
|
68
|
+
const includedSet = options?.includeKeys ? new Set(options.includeKeys) : null;
|
|
69
|
+
const touch = key => !excludedSet.has(key) && (!includedSet || includedSet.has(key));
|
|
70
|
+
let excludedHits = 0;
|
|
71
|
+
const result = {
|
|
72
|
+
ok: true,
|
|
73
|
+
applied: 0,
|
|
74
|
+
skipped: [],
|
|
75
|
+
warnings: []
|
|
76
|
+
};
|
|
77
|
+
if (!snapshot?.atoms) {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
applied: 0,
|
|
81
|
+
skipped: [],
|
|
82
|
+
warnings: ["empty jotai snapshot"]
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
let derived = 0;
|
|
86
|
+
for (const [label, entry] of Object.entries(snapshot.atoms)) {
|
|
87
|
+
if (!touch(label)) {
|
|
88
|
+
excludedHits += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (!entry.writable) {
|
|
92
|
+
derived += 1;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const info = jotaiStateStore.getAtom(label);
|
|
96
|
+
if (!info) {
|
|
97
|
+
result.skipped.push({
|
|
98
|
+
id: label,
|
|
99
|
+
reason: "atom not registered (renamed, or unwatched since snapshot?)"
|
|
100
|
+
});
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (!info.setValue) {
|
|
104
|
+
result.skipped.push({
|
|
105
|
+
id: label,
|
|
106
|
+
reason: "atom is now read-only (derived)"
|
|
107
|
+
});
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
info.setValue(entry.value);
|
|
112
|
+
result.applied += 1;
|
|
113
|
+
} catch (e) {
|
|
114
|
+
result.skipped.push({
|
|
115
|
+
id: label,
|
|
116
|
+
reason: String(e)
|
|
117
|
+
});
|
|
118
|
+
result.ok = false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (derived > 0) {
|
|
122
|
+
result.warnings.push(`${derived} derived atom(s) left to recompute`);
|
|
123
|
+
}
|
|
124
|
+
if (excludedHits > 0) {
|
|
125
|
+
result.warnings.push(`${excludedHits} atom(s) excluded by you`);
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Per-atom restore preview. `base` omitted = compare against live atoms. */
|
|
131
|
+
async function previewItems(target, base) {
|
|
132
|
+
const targetAtoms = target?.atoms ?? {};
|
|
133
|
+
const liveMode = base === undefined;
|
|
134
|
+
const baseAtoms = {};
|
|
135
|
+
if (liveMode) {
|
|
136
|
+
for (const info of jotaiStateStore.getAtoms()) {
|
|
137
|
+
try {
|
|
138
|
+
baseAtoms[info.label] = {
|
|
139
|
+
value: info.getValue(),
|
|
140
|
+
writable: Boolean(info.setValue)
|
|
141
|
+
};
|
|
142
|
+
} catch {
|
|
143
|
+
// unreadable atom — treated as absent
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
Object.assign(baseAtoms, base?.atoms ?? {});
|
|
148
|
+
}
|
|
149
|
+
const items = [];
|
|
150
|
+
const notes = [];
|
|
151
|
+
const labels = new Set([...Object.keys(baseAtoms), ...Object.keys(targetAtoms)]);
|
|
152
|
+
for (const label of labels) {
|
|
153
|
+
const inTarget = label in targetAtoms;
|
|
154
|
+
if (!inTarget) {
|
|
155
|
+
if (liveMode) notes.push(`atom "${label}" not in snapshot — untouched`);else items.push({
|
|
156
|
+
key: label,
|
|
157
|
+
label,
|
|
158
|
+
verdict: "removed",
|
|
159
|
+
oldValue: baseAtoms[label]?.value
|
|
160
|
+
});
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const entry = targetAtoms[label];
|
|
164
|
+
const equal = label in baseAtoms && boundedDeepEqual(baseAtoms[label].value, entry.value);
|
|
165
|
+
if (!entry.writable) {
|
|
166
|
+
items.push({
|
|
167
|
+
key: label,
|
|
168
|
+
label,
|
|
169
|
+
verdict: equal ? "unchanged" : "recomputes",
|
|
170
|
+
reason: equal ? undefined : "derived atom — recomputes from its sources",
|
|
171
|
+
oldValue: baseAtoms[label]?.value,
|
|
172
|
+
newValue: entry.value
|
|
173
|
+
});
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (liveMode) {
|
|
177
|
+
const info = jotaiStateStore.getAtom(label);
|
|
178
|
+
if (!info) {
|
|
179
|
+
items.push({
|
|
180
|
+
key: label,
|
|
181
|
+
label,
|
|
182
|
+
verdict: "wont-apply",
|
|
183
|
+
reason: "atom not registered (renamed, or unwatched since snapshot?)"
|
|
184
|
+
});
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (!info.setValue) {
|
|
188
|
+
items.push({
|
|
189
|
+
key: label,
|
|
190
|
+
label,
|
|
191
|
+
verdict: "wont-apply",
|
|
192
|
+
reason: "atom is now read-only (derived)"
|
|
193
|
+
});
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (!(label in baseAtoms)) {
|
|
198
|
+
items.push({
|
|
199
|
+
key: label,
|
|
200
|
+
label,
|
|
201
|
+
verdict: "added",
|
|
202
|
+
newValue: entry.value
|
|
203
|
+
});
|
|
204
|
+
} else if (equal) {
|
|
205
|
+
items.push({
|
|
206
|
+
key: label,
|
|
207
|
+
label,
|
|
208
|
+
verdict: "unchanged"
|
|
209
|
+
});
|
|
210
|
+
} else {
|
|
211
|
+
items.push({
|
|
212
|
+
key: label,
|
|
213
|
+
label,
|
|
214
|
+
verdict: "changed",
|
|
215
|
+
oldValue: baseAtoms[label].value,
|
|
216
|
+
newValue: entry.value
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
items,
|
|
222
|
+
notes
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
registerStateSnapshotProvider({
|
|
226
|
+
id: "jotai",
|
|
227
|
+
label: "Jotai",
|
|
228
|
+
status,
|
|
229
|
+
capture,
|
|
230
|
+
restore,
|
|
231
|
+
previewItems
|
|
232
|
+
});
|
|
@@ -56,9 +56,11 @@ export function watchAtoms(store, atoms, options) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
// Register the atom for the browser view. A setter is captured only when
|
|
59
|
-
// the store supports `set`
|
|
60
|
-
//
|
|
61
|
-
|
|
59
|
+
// the store supports `set` AND the atom itself is writable (has a `write`
|
|
60
|
+
// function — primitive atoms get a default one, read-only derived atoms
|
|
61
|
+
// have none). Deriveds used to get a setter that threw inside jotai at
|
|
62
|
+
// set time; now they're correctly reported as read-only up front.
|
|
63
|
+
const setter = store.set && typeof atom.write === "function" ? value => {
|
|
62
64
|
store.set(atom, value);
|
|
63
65
|
} : undefined;
|
|
64
66
|
jotaiStateStore.registerAtom(label, () => {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
* watchAtoms(myStore, { countAtom, authAtom });
|
|
30
30
|
* ```
|
|
31
31
|
*/
|
|
32
|
+
import "./jotai/utils/snapshotProvider";
|
|
32
33
|
export { jotaiToolPreset, createJotaiTool } from "./preset";
|
|
33
34
|
export { watchAtoms } from "./jotai/utils/watchAtoms";
|
|
34
35
|
export { watchDefaultStoreAtoms } from "./jotai/utils/watchAtoms";
|
|
@@ -41,7 +42,7 @@ export { JotaiAtomChangeItem } from "./jotai/components/JotaiAtomChangeItem";
|
|
|
41
42
|
export { JotaiAtomDetailContent, JotaiAtomDetailFooter, } from "./jotai/components/JotaiAtomDetailContent";
|
|
42
43
|
export { JotaiIcon, JOTAI_ICON_COLOR } from "./jotai/components/JotaiIcon";
|
|
43
44
|
export { jotaiSyncAdapter } from "./jotai/sync/jotaiSyncAdapter";
|
|
44
|
-
export type { JotaiModalProps, JotaiAtomChange, JotaiAtomInfo, JotaiAtomSnapshot, JotaiFilter, AtomChangeCategory, WatchAtomsOptions, } from "./jotai/types";
|
|
45
|
+
export type { JotaiModalProps, JotaiDetailViewMode, JotaiAtomChange, JotaiAtomInfo, JotaiAtomSnapshot, JotaiFilter, AtomChangeCategory, WatchAtomsOptions, } from "./jotai/types";
|
|
45
46
|
/** @internal */
|
|
46
47
|
export { jotaiStateStore } from "./jotai/utils/jotaiStateStore";
|
|
47
48
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAGH,OAAO,gCAAgC,CAAC;AAKxC,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAK5D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAKtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAKlE,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAK9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AACxE,YAAY,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAOnF,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAC;AAC7E,OAAO,EACL,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAK3E,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAKjE,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,eAAe,EACf,aAAa,EACb,iBAAiB,EACjB,WAAW,EACX,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,eAAe,CAAC;AAKvB,gBAAgB;AAChB,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC"}
|
|
@@ -9,7 +9,9 @@ interface JotaiAtomBrowserProps {
|
|
|
9
9
|
atoms: JotaiAtomInfo[];
|
|
10
10
|
searchQuery: string;
|
|
11
11
|
onViewHistory: (atomLabel: string) => void;
|
|
12
|
+
/** Fired when an atom row is expanded (not when collapsed). */
|
|
13
|
+
onAtomExpand?: (label: string) => void;
|
|
12
14
|
}
|
|
13
|
-
export declare function JotaiAtomBrowser({ atoms, searchQuery, onViewHistory, }: JotaiAtomBrowserProps): import("react").JSX.Element;
|
|
15
|
+
export declare function JotaiAtomBrowser({ atoms, searchQuery, onViewHistory, onAtomExpand, }: JotaiAtomBrowserProps): import("react").JSX.Element;
|
|
14
16
|
export {};
|
|
15
17
|
//# sourceMappingURL=JotaiAtomBrowser.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JotaiAtomBrowser.d.ts","sourceRoot":"","sources":["../../../../src/jotai/components/JotaiAtomBrowser.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAoBH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAG9C,UAAU,qBAAqB;IAC7B,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"JotaiAtomBrowser.d.ts","sourceRoot":"","sources":["../../../../src/jotai/components/JotaiAtomBrowser.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAoBH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAG9C,UAAU,qBAAqB;IAC7B,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,+DAA+D;IAC/D,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC;AAgHD,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,WAAW,EACX,aAAa,EACb,YAAY,GACb,EAAE,qBAAqB,+BAwFvB"}
|
|
@@ -9,15 +9,20 @@
|
|
|
9
9
|
* Mirrors ZustandStateDetailContent.tsx from @buoy-gg/zustand
|
|
10
10
|
*/
|
|
11
11
|
import React from "react";
|
|
12
|
-
import type { JotaiAtomChange } from "../types";
|
|
12
|
+
import type { JotaiAtomChange, JotaiDetailViewMode } from "../types";
|
|
13
13
|
interface JotaiAtomDetailContentProps {
|
|
14
14
|
change: JotaiAtomChange;
|
|
15
15
|
changes: JotaiAtomChange[];
|
|
16
16
|
selectedIndex: number;
|
|
17
17
|
onIndexChange: (index: number) => void;
|
|
18
18
|
disableInternalFooter?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Seeds the CHANGE / VALUE / DIFF toggle once on mount. Host deep-links
|
|
21
|
+
* (docs live tour) pass this; the user's own toggle taps take over after.
|
|
22
|
+
*/
|
|
23
|
+
initialView?: JotaiDetailViewMode;
|
|
19
24
|
}
|
|
20
|
-
export declare function JotaiAtomDetailContent({ change, changes, selectedIndex, onIndexChange, disableInternalFooter, }: JotaiAtomDetailContentProps): React.JSX.Element;
|
|
25
|
+
export declare function JotaiAtomDetailContent({ change, changes, selectedIndex, onIndexChange, disableInternalFooter, initialView, }: JotaiAtomDetailContentProps): React.JSX.Element;
|
|
21
26
|
export declare function JotaiAtomDetailFooter({ change, changes, selectedIndex, onIndexChange, }: {
|
|
22
27
|
change: JotaiAtomChange;
|
|
23
28
|
changes: JotaiAtomChange[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JotaiAtomDetailContent.d.ts","sourceRoot":"","sources":["../../../../src/jotai/components/JotaiAtomDetailContent.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAA+C,MAAM,OAAO,CAAC;AA0BpE,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"JotaiAtomDetailContent.d.ts","sourceRoot":"","sources":["../../../../src/jotai/components/JotaiAtomDetailContent.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAA+C,MAAM,OAAO,CAAC;AA0BpE,OAAO,KAAK,EACV,eAAe,EAEf,mBAAmB,EACpB,MAAM,UAAU,CAAC;AAiQlB,UAAU,2BAA2B;IACnC,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;OAGG;IACH,WAAW,CAAC,EAAE,mBAAmB,CAAC;CACnC;AAED,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,OAAO,EACP,aAAa,EACb,aAAa,EACb,qBAA6B,EAC7B,WAAsB,GACvB,EAAE,2BAA2B,qBA2J7B;AAED,wBAAgB,qBAAqB,CAAC,EACpC,MAAM,EACN,OAAO,EACP,aAAa,EACb,aAAa,GACd,EAAE;IACD,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC,4BAcA"}
|