@buoy-gg/jotai 7.0.17 → 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/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 +111 -2
- 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 +111 -2
- package/lib/typescript/index.d.ts +1 -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/package.json +3 -3
|
@@ -20,6 +20,9 @@ var _jsxRuntime = require("react/jsx-runtime");
|
|
|
20
20
|
function getValuePreview(atom) {
|
|
21
21
|
try {
|
|
22
22
|
const value = atom.getValue();
|
|
23
|
+
if (value && typeof value === "object" && value.__buoyValueOnDevice === true) {
|
|
24
|
+
return "on device";
|
|
25
|
+
}
|
|
23
26
|
if (value === undefined) return "undefined";
|
|
24
27
|
if (value === null) return "null";
|
|
25
28
|
if (Array.isArray(value)) {
|
|
@@ -125,7 +128,8 @@ function EmptyBrowserState() {
|
|
|
125
128
|
function JotaiAtomBrowser({
|
|
126
129
|
atoms,
|
|
127
130
|
searchQuery,
|
|
128
|
-
onViewHistory
|
|
131
|
+
onViewHistory,
|
|
132
|
+
onAtomExpand
|
|
129
133
|
}) {
|
|
130
134
|
const [expandedAtom, setExpandedAtom] = (0, _react.useState)(null);
|
|
131
135
|
const filteredAtoms = (0, _react.useMemo)(() => {
|
|
@@ -134,8 +138,12 @@ function JotaiAtomBrowser({
|
|
|
134
138
|
return atoms.filter(a => a.label.toLowerCase().includes(search));
|
|
135
139
|
}, [atoms, searchQuery]);
|
|
136
140
|
const handleAtomPress = (0, _react.useCallback)(atom => {
|
|
137
|
-
setExpandedAtom(prev =>
|
|
138
|
-
|
|
141
|
+
setExpandedAtom(prev => {
|
|
142
|
+
const next = prev === atom.label ? null : atom.label;
|
|
143
|
+
if (next) onAtomExpand?.(atom.label);
|
|
144
|
+
return next;
|
|
145
|
+
});
|
|
146
|
+
}, [onAtomExpand]);
|
|
139
147
|
if (filteredAtoms.length === 0 && !searchQuery) {
|
|
140
148
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(EmptyBrowserState, {});
|
|
141
149
|
}
|
|
@@ -25,7 +25,6 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
// ---- View Toggle ----
|
|
28
|
-
|
|
29
28
|
const VIEW_CONFIGS = [{
|
|
30
29
|
key: "change",
|
|
31
30
|
label: "CHANGE",
|
|
@@ -277,9 +276,11 @@ function JotaiAtomDetailContent({
|
|
|
277
276
|
changes,
|
|
278
277
|
selectedIndex,
|
|
279
278
|
onIndexChange,
|
|
280
|
-
disableInternalFooter = false
|
|
279
|
+
disableInternalFooter = false,
|
|
280
|
+
initialView = "change"
|
|
281
281
|
}) {
|
|
282
|
-
|
|
282
|
+
// View state (initialView seeds it once; taps own it afterwards)
|
|
283
|
+
const [activeView, setActiveView] = (0, _react.useState)(initialView);
|
|
283
284
|
const [diffMode, setDiffMode] = (0, _react.useState)("tree");
|
|
284
285
|
const totalChanges = changes.length;
|
|
285
286
|
const atomColor = _jotaiStateStore.jotaiStateStore.getAtomColor(change.atomLabel);
|
|
@@ -47,11 +47,16 @@ function JotaiModal({
|
|
|
47
47
|
onBack,
|
|
48
48
|
onMinimize,
|
|
49
49
|
enableSharedModalDimensions = false,
|
|
50
|
-
inline = false
|
|
50
|
+
inline = false,
|
|
51
|
+
initialTab,
|
|
52
|
+
initialSelectedChangeId,
|
|
53
|
+
initialDetailView,
|
|
54
|
+
onChangeSelect,
|
|
55
|
+
onAtomExpand
|
|
51
56
|
}) {
|
|
52
57
|
const isPro = (0, _license.useIsPro)();
|
|
53
58
|
const [showProModal, setShowProModal] = (0, _react.useState)(false);
|
|
54
|
-
const [activeTab, setActiveTab] = (0, _react.useState)("atoms");
|
|
59
|
+
const [activeTab, setActiveTab] = (0, _react.useState)(initialTab ?? "atoms");
|
|
55
60
|
const [selectedHistoryAtom, setSelectedHistoryAtom] = (0, _react.useState)(null);
|
|
56
61
|
const [showFilters, setShowFilters] = (0, _react.useState)(false);
|
|
57
62
|
const [ignoredPatterns, setIgnoredPatterns] = (0, _react.useState)(new Set());
|
|
@@ -103,6 +108,30 @@ function JotaiModal({
|
|
|
103
108
|
}, [selectedChangeId, activeChanges]);
|
|
104
109
|
const selectedChange = selectedChangeIndex !== null ? activeChanges[selectedChangeIndex] : null;
|
|
105
110
|
|
|
111
|
+
// Notify the host on selection (keyed by change id so re-renders with the
|
|
112
|
+
// same selection don't re-fire; remote mirrors fetch atom values here).
|
|
113
|
+
const lastNotifiedSelectionRef = (0, _react.useRef)(null);
|
|
114
|
+
(0, _react.useEffect)(() => {
|
|
115
|
+
if (!onChangeSelect) return;
|
|
116
|
+
const id = selectedChange?.id ?? null;
|
|
117
|
+
if (id === null || id === lastNotifiedSelectionRef.current) return;
|
|
118
|
+
lastNotifiedSelectionRef.current = id;
|
|
119
|
+
onChangeSelect(selectedChange);
|
|
120
|
+
}, [selectedChange, onChangeSelect]);
|
|
121
|
+
|
|
122
|
+
// Deep link into a change's detail (see initialSelectedChangeId). Applied
|
|
123
|
+
// at most once per id, so the list re-rendering — or the user pressing Back
|
|
124
|
+
// — never re-opens it.
|
|
125
|
+
const appliedInitialSelectionRef = (0, _react.useRef)(null);
|
|
126
|
+
(0, _react.useEffect)(() => {
|
|
127
|
+
if (!initialSelectedChangeId) return;
|
|
128
|
+
if (appliedInitialSelectionRef.current === initialSelectedChangeId) return;
|
|
129
|
+
const exists = activeChanges.some(c => c.id === initialSelectedChangeId);
|
|
130
|
+
if (!exists) return;
|
|
131
|
+
appliedInitialSelectionRef.current = initialSelectedChangeId;
|
|
132
|
+
setSelectedChangeId(initialSelectedChangeId);
|
|
133
|
+
}, [initialSelectedChangeId, activeChanges]);
|
|
134
|
+
|
|
106
135
|
// Search state
|
|
107
136
|
const [searchText, setSearchText] = (0, _react.useState)("");
|
|
108
137
|
const [isSearchActive, setIsSearchActive] = (0, _react.useState)(false);
|
|
@@ -343,7 +372,8 @@ function JotaiModal({
|
|
|
343
372
|
changes: activeChanges,
|
|
344
373
|
selectedIndex: selectedChangeIndex,
|
|
345
374
|
onIndexChange: handleIndexChange,
|
|
346
|
-
disableInternalFooter: true
|
|
375
|
+
disableInternalFooter: true,
|
|
376
|
+
initialView: initialDetailView
|
|
347
377
|
});
|
|
348
378
|
}
|
|
349
379
|
if (selectedHistoryAtom) {
|
|
@@ -376,7 +406,8 @@ function JotaiModal({
|
|
|
376
406
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_JotaiAtomBrowser.JotaiAtomBrowser, {
|
|
377
407
|
atoms: displayedAtoms,
|
|
378
408
|
searchQuery: searchText,
|
|
379
|
-
onViewHistory: handleViewHistory
|
|
409
|
+
onViewHistory: handleViewHistory,
|
|
410
|
+
onAtomExpand: onAtomExpand
|
|
380
411
|
});
|
|
381
412
|
}
|
|
382
413
|
|
|
@@ -3,8 +3,62 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.jotaiSyncAdapter = void 0;
|
|
6
|
+
exports.jotaiSyncAdapter = exports.VALUE_ON_DEVICE = void 0;
|
|
7
7
|
var _jotaiStateStore = require("../utils/jotaiStateStore");
|
|
8
|
+
/**
|
|
9
|
+
* Sentinel used in WIRE-FORM snapshots in place of raw prevValue/nextValue.
|
|
10
|
+
* Same hazard as redux/zustand: up to 200 changes × 2 values on every
|
|
11
|
+
* snapshot. Dashboards fetch values via `getChangeDetail`.
|
|
12
|
+
*/
|
|
13
|
+
const VALUE_ON_DEVICE = exports.VALUE_ON_DEVICE = Object.freeze({
|
|
14
|
+
__buoyValueOnDevice: true,
|
|
15
|
+
note: "Atom values stay on the device — fetched on demand via getChangeDetail / getAtomValue."
|
|
16
|
+
});
|
|
17
|
+
const MAX_CHANGE_DETAIL_BYTES = 8 * 1024 * 1024;
|
|
18
|
+
const MAX_WIRE_PAYLOAD_BYTES = 16 * 1024;
|
|
19
|
+
function jsonLongerThan(value, limit) {
|
|
20
|
+
let bytes = 0;
|
|
21
|
+
const stack = [value];
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
while (stack.length > 0) {
|
|
24
|
+
if (bytes > limit) return true;
|
|
25
|
+
const v = stack.pop();
|
|
26
|
+
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") {
|
|
27
|
+
if (seen.has(v)) continue;
|
|
28
|
+
seen.add(v);
|
|
29
|
+
if (Array.isArray(v)) {
|
|
30
|
+
bytes += 2 + v.length;
|
|
31
|
+
for (let i = 0; i < v.length; i++) stack.push(v[i]);
|
|
32
|
+
} else {
|
|
33
|
+
bytes += 2;
|
|
34
|
+
for (const k in v) {
|
|
35
|
+
bytes += k.length + 4;
|
|
36
|
+
stack.push(v[k]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} else bytes += 4;
|
|
40
|
+
}
|
|
41
|
+
return bytes > limit;
|
|
42
|
+
}
|
|
43
|
+
function toWireChange(change) {
|
|
44
|
+
return {
|
|
45
|
+
...change,
|
|
46
|
+
prevValue: VALUE_ON_DEVICE,
|
|
47
|
+
nextValue: VALUE_ON_DEVICE
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function toWireValue(value) {
|
|
51
|
+
try {
|
|
52
|
+
JSON.stringify(value);
|
|
53
|
+
} catch {
|
|
54
|
+
return VALUE_ON_DEVICE;
|
|
55
|
+
}
|
|
56
|
+
if (jsonLongerThan(value, MAX_WIRE_PAYLOAD_BYTES)) {
|
|
57
|
+
return VALUE_ON_DEVICE;
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
8
62
|
/**
|
|
9
63
|
* Sync adapter for the jotai tool, consumed by @buoy-gg/external-sync's
|
|
10
64
|
* `useExternalSync` (structurally matches its ToolSyncAdapter interface so
|
|
@@ -15,12 +69,19 @@ var _jotaiStateStore = require("../utils/jotaiStateStore");
|
|
|
15
69
|
* already records. The snapshot carries both the change timeline and the
|
|
16
70
|
* atom registry (with each atom's current value) so the dashboard can render
|
|
17
71
|
* the atom browser.
|
|
72
|
+
*
|
|
73
|
+
* v2: change-log prevValue/nextValue are replaced with on-device markers.
|
|
74
|
+
* Dashboards fetch values via `getChangeDetail`. Oversized / unserializable
|
|
75
|
+
* `atoms[].currentValue` uses the same marker and is fetched via `getAtomValue`.
|
|
18
76
|
*/
|
|
19
77
|
const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
|
|
20
|
-
version:
|
|
78
|
+
version: 2,
|
|
21
79
|
getSnapshot: () => ({
|
|
22
|
-
changes: _jotaiStateStore.jotaiStateStore.getAtomChanges(),
|
|
23
|
-
atoms: _jotaiStateStore.jotaiStateStore.getAtomSnapshots()
|
|
80
|
+
changes: _jotaiStateStore.jotaiStateStore.getAtomChanges().map(toWireChange),
|
|
81
|
+
atoms: _jotaiStateStore.jotaiStateStore.getAtomSnapshots().map(atom => ({
|
|
82
|
+
...atom,
|
|
83
|
+
currentValue: toWireValue(atom.currentValue)
|
|
84
|
+
}))
|
|
24
85
|
}),
|
|
25
86
|
subscribe: onChange => {
|
|
26
87
|
const unsubscribeChanges = _jotaiStateStore.jotaiStateStore.subscribe(onChange);
|
|
@@ -34,6 +95,78 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
|
|
|
34
95
|
clearEvents: () => {
|
|
35
96
|
_jotaiStateStore.jotaiStateStore.clearAtomChanges();
|
|
36
97
|
},
|
|
98
|
+
/**
|
|
99
|
+
* On-demand detail: one change's real prevValue/nextValue.
|
|
100
|
+
* The per-snapshot stream stays value-free; this is the explicit,
|
|
101
|
+
* size-guarded channel for the detail pane.
|
|
102
|
+
*/
|
|
103
|
+
getChangeDetail: params => {
|
|
104
|
+
const {
|
|
105
|
+
id
|
|
106
|
+
} = params ?? {};
|
|
107
|
+
if (!id) return {
|
|
108
|
+
found: false,
|
|
109
|
+
reason: "missing id"
|
|
110
|
+
};
|
|
111
|
+
const change = _jotaiStateStore.jotaiStateStore.getAtomChangeById(id);
|
|
112
|
+
if (!change) return {
|
|
113
|
+
found: false,
|
|
114
|
+
reason: "unknown id"
|
|
115
|
+
};
|
|
116
|
+
const cappedNote = {
|
|
117
|
+
__buoyTruncated: true,
|
|
118
|
+
note: `Exceeds the ${MAX_CHANGE_DETAIL_BYTES / (1024 * 1024)}MB detail limit — inspect it on-device.`
|
|
119
|
+
};
|
|
120
|
+
const cap = value => jsonLongerThan(value, MAX_CHANGE_DETAIL_BYTES) ? cappedNote : value;
|
|
121
|
+
return {
|
|
122
|
+
found: true,
|
|
123
|
+
id,
|
|
124
|
+
prevValue: cap(change.prevValue),
|
|
125
|
+
nextValue: cap(change.nextValue)
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
/**
|
|
129
|
+
* On-demand current value for one atom. Wire-form snapshots replace
|
|
130
|
+
* oversized / unserializable `currentValue` with VALUE_ON_DEVICE.
|
|
131
|
+
*/
|
|
132
|
+
getAtomValue: params => {
|
|
133
|
+
const {
|
|
134
|
+
label
|
|
135
|
+
} = params ?? {};
|
|
136
|
+
if (!label) return {
|
|
137
|
+
found: false,
|
|
138
|
+
reason: "missing label"
|
|
139
|
+
};
|
|
140
|
+
const atom = _jotaiStateStore.jotaiStateStore.getAtom(label);
|
|
141
|
+
if (!atom) return {
|
|
142
|
+
found: false,
|
|
143
|
+
reason: "unknown label"
|
|
144
|
+
};
|
|
145
|
+
let currentValue;
|
|
146
|
+
try {
|
|
147
|
+
currentValue = atom.getValue();
|
|
148
|
+
} catch {
|
|
149
|
+
currentValue = undefined;
|
|
150
|
+
}
|
|
151
|
+
const cappedNote = {
|
|
152
|
+
__buoyTruncated: true,
|
|
153
|
+
note: `Exceeds the ${MAX_CHANGE_DETAIL_BYTES / (1024 * 1024)}MB detail limit — inspect it on-device.`
|
|
154
|
+
};
|
|
155
|
+
try {
|
|
156
|
+
JSON.stringify(currentValue);
|
|
157
|
+
} catch {
|
|
158
|
+
return {
|
|
159
|
+
found: true,
|
|
160
|
+
label,
|
|
161
|
+
currentValue: VALUE_ON_DEVICE
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
found: true,
|
|
166
|
+
label,
|
|
167
|
+
currentValue: jsonLongerThan(currentValue, MAX_CHANGE_DETAIL_BYTES) ? cappedNote : currentValue
|
|
168
|
+
};
|
|
169
|
+
},
|
|
37
170
|
/**
|
|
38
171
|
* Compact current-value reader for a remote driver (MCP/LLM): registered
|
|
39
172
|
* atoms with light metadata; each atom's `currentValue` stays on the device
|
|
@@ -77,7 +210,8 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
|
|
|
77
210
|
const {
|
|
78
211
|
label,
|
|
79
212
|
value
|
|
80
|
-
} = params;
|
|
213
|
+
} = params ?? {};
|
|
214
|
+
if (!label) throw new Error("setAtom requires a `label`");
|
|
81
215
|
_jotaiStateStore.jotaiStateStore.setAtomValue(label, value);
|
|
82
216
|
}
|
|
83
217
|
}
|
|
@@ -290,17 +290,83 @@ class JotaiStateStore {
|
|
|
290
290
|
*/
|
|
291
291
|
replaceFromSnapshot(changes, atomSnapshots) {
|
|
292
292
|
if (!this.isEnabled) return;
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
293
|
+
|
|
294
|
+
// Preserve values a remote mirror already hydrated — wire-form snapshots
|
|
295
|
+
// always carry markers, and a later snapshot must not blank the detail pane.
|
|
296
|
+
const hydrated = new Map();
|
|
297
|
+
for (const existing of this.atomChanges) {
|
|
298
|
+
const prev = existing.prevValue;
|
|
299
|
+
if (!prev || prev.__buoyValueOnDevice !== true) {
|
|
300
|
+
hydrated.set(existing.id, existing);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
this.atomChanges = changes.slice(0, this.maxChanges).map(change => {
|
|
304
|
+
const kept = hydrated.get(change.id);
|
|
305
|
+
const incoming = change.prevValue;
|
|
306
|
+
return kept && incoming?.__buoyValueOnDevice === true ? {
|
|
307
|
+
...change,
|
|
308
|
+
prevValue: kept.prevValue,
|
|
309
|
+
nextValue: kept.nextValue
|
|
310
|
+
} : change;
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Preserve currentValue a remote mirror already hydrated — oversized
|
|
314
|
+
// atoms arrive as VALUE_ON_DEVICE markers on every snapshot.
|
|
315
|
+
const hydratedAtoms = new Map();
|
|
316
|
+
for (const [label, atom] of this.atoms) {
|
|
317
|
+
try {
|
|
318
|
+
const value = atom.getValue();
|
|
319
|
+
const marker = value;
|
|
320
|
+
if (!marker || marker.__buoyValueOnDevice !== true) {
|
|
321
|
+
hydratedAtoms.set(label, value);
|
|
322
|
+
}
|
|
323
|
+
} catch {
|
|
324
|
+
// ignore
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
this.atoms = new Map(atomSnapshots.map(snapshot => {
|
|
328
|
+
const incoming = snapshot.currentValue;
|
|
329
|
+
const kept = hydratedAtoms.get(snapshot.label);
|
|
330
|
+
const currentValue = kept !== undefined && incoming?.__buoyValueOnDevice === true ? kept : snapshot.currentValue;
|
|
331
|
+
return [snapshot.label, {
|
|
332
|
+
label: snapshot.label,
|
|
333
|
+
changeCount: snapshot.changeCount,
|
|
334
|
+
color: snapshot.color,
|
|
335
|
+
getValue: () => currentValue
|
|
336
|
+
}];
|
|
337
|
+
}));
|
|
300
338
|
this.notifyListeners();
|
|
301
339
|
this.notifyAtomListeners();
|
|
302
340
|
}
|
|
303
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Hydrate one mirrored change with real values fetched on demand from the
|
|
344
|
+
* device via the adapter's `getChangeDetail`. Remote-mirror (desktop) only.
|
|
345
|
+
*/
|
|
346
|
+
hydrateChange(id, patch) {
|
|
347
|
+
const index = this.atomChanges.findIndex(c => c.id === id);
|
|
348
|
+
if (index === -1) return;
|
|
349
|
+
this.atomChanges = [...this.atomChanges.slice(0, index), {
|
|
350
|
+
...this.atomChanges[index],
|
|
351
|
+
...patch
|
|
352
|
+
}, ...this.atomChanges.slice(index + 1)];
|
|
353
|
+
this.notifyListeners();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Hydrate one mirrored atom's currentValue fetched on demand from the
|
|
358
|
+
* device via the adapter's `getAtomValue`. Remote-mirror (desktop) only.
|
|
359
|
+
*/
|
|
360
|
+
hydrateAtomValue(label, currentValue) {
|
|
361
|
+
const atom = this.atoms.get(label);
|
|
362
|
+
if (!atom) return;
|
|
363
|
+
this.atoms.set(label, {
|
|
364
|
+
...atom,
|
|
365
|
+
getValue: () => currentValue
|
|
366
|
+
});
|
|
367
|
+
this.notifyAtomListeners();
|
|
368
|
+
}
|
|
369
|
+
|
|
304
370
|
// ---- Atom Registry ----
|
|
305
371
|
|
|
306
372
|
registerAtom(label, getValue, setValue) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
var _sharedUi = require("@buoy-gg/shared-ui");
|
|
4
|
+
var _dataViewer = require("@buoy-gg/shared-ui/dataViewer");
|
|
4
5
|
var _jotaiStateStore = require("./jotaiStateStore");
|
|
5
6
|
/**
|
|
6
7
|
* Time Machine snapshot provider for Jotai.
|
|
@@ -62,8 +63,12 @@ async function capture() {
|
|
|
62
63
|
warnings
|
|
63
64
|
};
|
|
64
65
|
}
|
|
65
|
-
async function restore(data) {
|
|
66
|
+
async function restore(data, options) {
|
|
66
67
|
const snapshot = data;
|
|
68
|
+
const excludedSet = new Set(options?.excludeKeys ?? []);
|
|
69
|
+
const includedSet = options?.includeKeys ? new Set(options.includeKeys) : null;
|
|
70
|
+
const touch = key => !excludedSet.has(key) && (!includedSet || includedSet.has(key));
|
|
71
|
+
let excludedHits = 0;
|
|
67
72
|
const result = {
|
|
68
73
|
ok: true,
|
|
69
74
|
applied: 0,
|
|
@@ -80,6 +85,10 @@ async function restore(data) {
|
|
|
80
85
|
}
|
|
81
86
|
let derived = 0;
|
|
82
87
|
for (const [label, entry] of Object.entries(snapshot.atoms)) {
|
|
88
|
+
if (!touch(label)) {
|
|
89
|
+
excludedHits += 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
83
92
|
if (!entry.writable) {
|
|
84
93
|
derived += 1;
|
|
85
94
|
continue;
|
|
@@ -113,12 +122,112 @@ async function restore(data) {
|
|
|
113
122
|
if (derived > 0) {
|
|
114
123
|
result.warnings.push(`${derived} derived atom(s) left to recompute`);
|
|
115
124
|
}
|
|
125
|
+
if (excludedHits > 0) {
|
|
126
|
+
result.warnings.push(`${excludedHits} atom(s) excluded by you`);
|
|
127
|
+
}
|
|
116
128
|
return result;
|
|
117
129
|
}
|
|
130
|
+
|
|
131
|
+
/** Per-atom restore preview. `base` omitted = compare against live atoms. */
|
|
132
|
+
async function previewItems(target, base) {
|
|
133
|
+
const targetAtoms = target?.atoms ?? {};
|
|
134
|
+
const liveMode = base === undefined;
|
|
135
|
+
const baseAtoms = {};
|
|
136
|
+
if (liveMode) {
|
|
137
|
+
for (const info of _jotaiStateStore.jotaiStateStore.getAtoms()) {
|
|
138
|
+
try {
|
|
139
|
+
baseAtoms[info.label] = {
|
|
140
|
+
value: info.getValue(),
|
|
141
|
+
writable: Boolean(info.setValue)
|
|
142
|
+
};
|
|
143
|
+
} catch {
|
|
144
|
+
// unreadable atom — treated as absent
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
Object.assign(baseAtoms, base?.atoms ?? {});
|
|
149
|
+
}
|
|
150
|
+
const items = [];
|
|
151
|
+
const notes = [];
|
|
152
|
+
const labels = new Set([...Object.keys(baseAtoms), ...Object.keys(targetAtoms)]);
|
|
153
|
+
for (const label of labels) {
|
|
154
|
+
const inTarget = label in targetAtoms;
|
|
155
|
+
if (!inTarget) {
|
|
156
|
+
if (liveMode) notes.push(`atom "${label}" not in snapshot — untouched`);else items.push({
|
|
157
|
+
key: label,
|
|
158
|
+
label,
|
|
159
|
+
verdict: "removed",
|
|
160
|
+
oldValue: baseAtoms[label]?.value
|
|
161
|
+
});
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const entry = targetAtoms[label];
|
|
165
|
+
const equal = label in baseAtoms && (0, _dataViewer.boundedDeepEqual)(baseAtoms[label].value, entry.value);
|
|
166
|
+
if (!entry.writable) {
|
|
167
|
+
items.push({
|
|
168
|
+
key: label,
|
|
169
|
+
label,
|
|
170
|
+
verdict: equal ? "unchanged" : "recomputes",
|
|
171
|
+
reason: equal ? undefined : "derived atom — recomputes from its sources",
|
|
172
|
+
oldValue: baseAtoms[label]?.value,
|
|
173
|
+
newValue: entry.value
|
|
174
|
+
});
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (liveMode) {
|
|
178
|
+
const info = _jotaiStateStore.jotaiStateStore.getAtom(label);
|
|
179
|
+
if (!info) {
|
|
180
|
+
items.push({
|
|
181
|
+
key: label,
|
|
182
|
+
label,
|
|
183
|
+
verdict: "wont-apply",
|
|
184
|
+
reason: "atom not registered (renamed, or unwatched since snapshot?)"
|
|
185
|
+
});
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (!info.setValue) {
|
|
189
|
+
items.push({
|
|
190
|
+
key: label,
|
|
191
|
+
label,
|
|
192
|
+
verdict: "wont-apply",
|
|
193
|
+
reason: "atom is now read-only (derived)"
|
|
194
|
+
});
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (!(label in baseAtoms)) {
|
|
199
|
+
items.push({
|
|
200
|
+
key: label,
|
|
201
|
+
label,
|
|
202
|
+
verdict: "added",
|
|
203
|
+
newValue: entry.value
|
|
204
|
+
});
|
|
205
|
+
} else if (equal) {
|
|
206
|
+
items.push({
|
|
207
|
+
key: label,
|
|
208
|
+
label,
|
|
209
|
+
verdict: "unchanged"
|
|
210
|
+
});
|
|
211
|
+
} else {
|
|
212
|
+
items.push({
|
|
213
|
+
key: label,
|
|
214
|
+
label,
|
|
215
|
+
verdict: "changed",
|
|
216
|
+
oldValue: baseAtoms[label].value,
|
|
217
|
+
newValue: entry.value
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
items,
|
|
223
|
+
notes
|
|
224
|
+
};
|
|
225
|
+
}
|
|
118
226
|
(0, _sharedUi.registerStateSnapshotProvider)({
|
|
119
227
|
id: "jotai",
|
|
120
228
|
label: "Jotai",
|
|
121
229
|
status,
|
|
122
230
|
capture,
|
|
123
|
-
restore
|
|
231
|
+
restore,
|
|
232
|
+
previewItems
|
|
124
233
|
});
|
|
@@ -16,6 +16,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
16
16
|
function getValuePreview(atom) {
|
|
17
17
|
try {
|
|
18
18
|
const value = atom.getValue();
|
|
19
|
+
if (value && typeof value === "object" && value.__buoyValueOnDevice === true) {
|
|
20
|
+
return "on device";
|
|
21
|
+
}
|
|
19
22
|
if (value === undefined) return "undefined";
|
|
20
23
|
if (value === null) return "null";
|
|
21
24
|
if (Array.isArray(value)) {
|
|
@@ -121,7 +124,8 @@ function EmptyBrowserState() {
|
|
|
121
124
|
export function JotaiAtomBrowser({
|
|
122
125
|
atoms,
|
|
123
126
|
searchQuery,
|
|
124
|
-
onViewHistory
|
|
127
|
+
onViewHistory,
|
|
128
|
+
onAtomExpand
|
|
125
129
|
}) {
|
|
126
130
|
const [expandedAtom, setExpandedAtom] = useState(null);
|
|
127
131
|
const filteredAtoms = useMemo(() => {
|
|
@@ -130,8 +134,12 @@ export function JotaiAtomBrowser({
|
|
|
130
134
|
return atoms.filter(a => a.label.toLowerCase().includes(search));
|
|
131
135
|
}, [atoms, searchQuery]);
|
|
132
136
|
const handleAtomPress = useCallback(atom => {
|
|
133
|
-
setExpandedAtom(prev =>
|
|
134
|
-
|
|
137
|
+
setExpandedAtom(prev => {
|
|
138
|
+
const next = prev === atom.label ? null : atom.label;
|
|
139
|
+
if (next) onAtomExpand?.(atom.label);
|
|
140
|
+
return next;
|
|
141
|
+
});
|
|
142
|
+
}, [onAtomExpand]);
|
|
135
143
|
if (filteredAtoms.length === 0 && !searchQuery) {
|
|
136
144
|
return /*#__PURE__*/_jsx(EmptyBrowserState, {});
|
|
137
145
|
}
|
|
@@ -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
|
|