@buoy-gg/zustand 7.0.17 → 7.0.19
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/zustand/components/ZustandModal.js +35 -4
- package/lib/commonjs/zustand/components/ZustandStateDetailContent.js +3 -2
- package/lib/commonjs/zustand/components/ZustandStoreBrowser.js +11 -3
- package/lib/commonjs/zustand/sync/zustandSyncAdapter.js +141 -6
- package/lib/commonjs/zustand/utils/snapshotProvider.js +110 -2
- package/lib/commonjs/zustand/utils/zustandStateStore.js +105 -14
- package/lib/module/zustand/components/ZustandModal.js +36 -5
- package/lib/module/zustand/components/ZustandStateDetailContent.js +3 -2
- package/lib/module/zustand/components/ZustandStoreBrowser.js +11 -3
- package/lib/module/zustand/sync/zustandSyncAdapter.js +141 -5
- package/lib/module/zustand/utils/snapshotProvider.js +110 -2
- package/lib/module/zustand/utils/zustandStateStore.js +105 -14
- package/lib/typescript/zustand/components/ZustandModal.d.ts +1 -1
- package/lib/typescript/zustand/components/ZustandModal.d.ts.map +1 -1
- package/lib/typescript/zustand/components/ZustandStateDetailContent.d.ts +4 -1
- package/lib/typescript/zustand/components/ZustandStateDetailContent.d.ts.map +1 -1
- package/lib/typescript/zustand/components/ZustandStoreBrowser.d.ts +3 -1
- package/lib/typescript/zustand/components/ZustandStoreBrowser.d.ts.map +1 -1
- package/lib/typescript/zustand/sync/zustandSyncAdapter.d.ts +72 -4
- package/lib/typescript/zustand/sync/zustandSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/zustand/types/index.d.ts +26 -0
- package/lib/typescript/zustand/types/index.d.ts.map +1 -1
- package/lib/typescript/zustand/utils/zustandStateStore.d.ts +18 -0
- package/lib/typescript/zustand/utils/zustandStateStore.d.ts.map +1 -1
- package/package.json +9 -4
|
@@ -49,11 +49,16 @@ function ZustandModal({
|
|
|
49
49
|
onMinimize,
|
|
50
50
|
enableSharedModalDimensions = false,
|
|
51
51
|
inline = false,
|
|
52
|
-
storesLoading = false
|
|
52
|
+
storesLoading = false,
|
|
53
|
+
initialTab = "stores",
|
|
54
|
+
initialSelectedChangeId,
|
|
55
|
+
initialDetailView,
|
|
56
|
+
onChangeSelect,
|
|
57
|
+
onStoreExpand
|
|
53
58
|
}) {
|
|
54
59
|
const isPro = (0, _license.useIsPro)();
|
|
55
60
|
const [showProModal, setShowProModal] = (0, _react.useState)(false);
|
|
56
|
-
const [activeTab, setActiveTab] = (0, _react.useState)(
|
|
61
|
+
const [activeTab, setActiveTab] = (0, _react.useState)(initialTab);
|
|
57
62
|
const [selectedHistoryStore, setSelectedHistoryStore] = (0, _react.useState)(null);
|
|
58
63
|
const [showFilters, setShowFilters] = (0, _react.useState)(false);
|
|
59
64
|
const [ignoredPatterns, setIgnoredPatterns] = (0, _react.useState)(new Set());
|
|
@@ -108,6 +113,30 @@ function ZustandModal({
|
|
|
108
113
|
}, [selectedChangeId, activeChanges]);
|
|
109
114
|
const selectedChange = selectedChangeIndex !== null ? activeChanges[selectedChangeIndex] : null;
|
|
110
115
|
|
|
116
|
+
// Notify the host on selection (keyed by change id so re-renders with the
|
|
117
|
+
// same selection don't re-fire; remote mirrors fetch state trees here).
|
|
118
|
+
const lastNotifiedSelectionRef = (0, _react.useRef)(null);
|
|
119
|
+
(0, _react.useEffect)(() => {
|
|
120
|
+
if (!onChangeSelect) return;
|
|
121
|
+
const id = selectedChange?.id ?? null;
|
|
122
|
+
if (id === null || id === lastNotifiedSelectionRef.current) return;
|
|
123
|
+
lastNotifiedSelectionRef.current = id;
|
|
124
|
+
onChangeSelect(selectedChange);
|
|
125
|
+
}, [selectedChange, onChangeSelect]);
|
|
126
|
+
|
|
127
|
+
// Deep link into a change's detail (see initialSelectedChangeId). Applied
|
|
128
|
+
// at most once per id, so the list re-rendering — or the user pressing Back
|
|
129
|
+
// — never re-opens it.
|
|
130
|
+
const appliedInitialSelectionRef = (0, _react.useRef)(null);
|
|
131
|
+
(0, _react.useEffect)(() => {
|
|
132
|
+
if (!initialSelectedChangeId) return;
|
|
133
|
+
if (appliedInitialSelectionRef.current === initialSelectedChangeId) return;
|
|
134
|
+
const exists = activeChanges.some(c => c.id === initialSelectedChangeId);
|
|
135
|
+
if (!exists) return;
|
|
136
|
+
appliedInitialSelectionRef.current = initialSelectedChangeId;
|
|
137
|
+
setSelectedChangeId(initialSelectedChangeId);
|
|
138
|
+
}, [initialSelectedChangeId, activeChanges]);
|
|
139
|
+
|
|
111
140
|
// Search state — shared across tabs
|
|
112
141
|
const [searchText, setSearchText] = (0, _react.useState)("");
|
|
113
142
|
const [isSearchActive, setIsSearchActive] = (0, _react.useState)(false);
|
|
@@ -370,7 +399,8 @@ function ZustandModal({
|
|
|
370
399
|
changes: activeChanges,
|
|
371
400
|
selectedIndex: selectedChangeIndex,
|
|
372
401
|
onIndexChange: handleIndexChange,
|
|
373
|
-
disableInternalFooter: true
|
|
402
|
+
disableInternalFooter: true,
|
|
403
|
+
initialView: initialDetailView
|
|
374
404
|
});
|
|
375
405
|
}
|
|
376
406
|
|
|
@@ -408,7 +438,8 @@ function ZustandModal({
|
|
|
408
438
|
stores: displayedStores,
|
|
409
439
|
searchQuery: searchText,
|
|
410
440
|
onViewHistory: handleViewHistory,
|
|
411
|
-
isLoading: storesLoading
|
|
441
|
+
isLoading: storesLoading,
|
|
442
|
+
onStoreExpand: onStoreExpand
|
|
412
443
|
});
|
|
413
444
|
}
|
|
414
445
|
|
|
@@ -66,9 +66,10 @@ function ZustandStateDetailContent({
|
|
|
66
66
|
changes,
|
|
67
67
|
selectedIndex,
|
|
68
68
|
onIndexChange,
|
|
69
|
-
disableInternalFooter = false
|
|
69
|
+
disableInternalFooter = false,
|
|
70
|
+
initialView = "change"
|
|
70
71
|
}) {
|
|
71
|
-
const [activeView, setActiveView] = (0, _react.useState)(
|
|
72
|
+
const [activeView, setActiveView] = (0, _react.useState)(initialView);
|
|
72
73
|
const [diffMode, setDiffMode] = (0, _react.useState)("tree");
|
|
73
74
|
const totalChanges = changes.length;
|
|
74
75
|
const changeColor = getChangeColor(change);
|
|
@@ -26,6 +26,9 @@ var _jsxRuntime = require("react/jsx-runtime");
|
|
|
26
26
|
function getKeysPreview(store) {
|
|
27
27
|
try {
|
|
28
28
|
const state = store.api.getState();
|
|
29
|
+
if (state && typeof state === "object" && state.__buoyStateOnDevice === true) {
|
|
30
|
+
return "on device";
|
|
31
|
+
}
|
|
29
32
|
if (state && typeof state === "object") {
|
|
30
33
|
const keys = Object.keys(state);
|
|
31
34
|
const dataKeys = keys.filter(k => {
|
|
@@ -146,7 +149,8 @@ function ZustandStoreBrowser({
|
|
|
146
149
|
stores,
|
|
147
150
|
searchQuery,
|
|
148
151
|
onViewHistory,
|
|
149
|
-
isLoading = false
|
|
152
|
+
isLoading = false,
|
|
153
|
+
onStoreExpand
|
|
150
154
|
}) {
|
|
151
155
|
const [expandedStore, setExpandedStore] = (0, _react.useState)(null);
|
|
152
156
|
|
|
@@ -157,8 +161,12 @@ function ZustandStoreBrowser({
|
|
|
157
161
|
return stores.filter(s => s.name.toLowerCase().includes(search) || s.persistName && s.persistName.toLowerCase().includes(search));
|
|
158
162
|
}, [stores, searchQuery]);
|
|
159
163
|
const handleStorePress = (0, _react.useCallback)(store => {
|
|
160
|
-
setExpandedStore(prev =>
|
|
161
|
-
|
|
164
|
+
setExpandedStore(prev => {
|
|
165
|
+
const next = prev === store.name ? null : store.name;
|
|
166
|
+
if (next) onStoreExpand?.(store.name);
|
|
167
|
+
return next;
|
|
168
|
+
});
|
|
169
|
+
}, [onStoreExpand]);
|
|
162
170
|
if (filteredStores.length === 0 && !searchQuery) {
|
|
163
171
|
return isLoading ? /*#__PURE__*/(0, _jsxRuntime.jsx)(LoadingBrowserState, {}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(EmptyBrowserState, {});
|
|
164
172
|
}
|
|
@@ -3,13 +3,57 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.zustandSyncAdapter = void 0;
|
|
6
|
+
exports.zustandSyncAdapter = exports.STATE_ON_DEVICE = exports.PAYLOAD_ON_DEVICE = void 0;
|
|
7
7
|
var _zustandStateStore = require("../utils/zustandStateStore");
|
|
8
|
+
var _sharedUi = require("@buoy-gg/shared-ui");
|
|
9
|
+
/**
|
|
10
|
+
* Sentinel used in WIRE-FORM snapshots in place of raw prevState/nextState.
|
|
11
|
+
* Same hazard as redux: up to 200 changes × 2 full trees on every snapshot
|
|
12
|
+
* freeze/OOM-killed large apps. Dashboards fetch trees via `getChangeDetail`.
|
|
13
|
+
*/
|
|
14
|
+
const STATE_ON_DEVICE = exports.STATE_ON_DEVICE = Object.freeze({
|
|
15
|
+
__buoyStateOnDevice: true,
|
|
16
|
+
note: "State trees stay on the device — fetched on demand via getChangeDetail / getStoreState."
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/** Wire-form sentinel for an oversized `partial` (replace can carry a whole tree). */
|
|
20
|
+
const PAYLOAD_ON_DEVICE = exports.PAYLOAD_ON_DEVICE = Object.freeze({
|
|
21
|
+
__buoyPayloadOnDevice: true,
|
|
22
|
+
note: "Partial stays on the device — fetched on demand via getChangeDetail."
|
|
23
|
+
});
|
|
24
|
+
const MAX_CHANGE_DETAIL_BYTES = 8 * 1024 * 1024;
|
|
25
|
+
const MAX_WIRE_PAYLOAD_BYTES = 16 * 1024;
|
|
8
26
|
/** Top-level keys of an object state, for the compact (no-values) view. */
|
|
9
27
|
function topLevelKeys(state) {
|
|
10
28
|
return state && typeof state === "object" && !Array.isArray(state) ? Object.keys(state) : undefined;
|
|
11
29
|
}
|
|
12
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Per-change wire cache. The store prepends and never mutates a recorded
|
|
33
|
+
* change, so keying on identity is correct and self-invalidating — without it
|
|
34
|
+
* every `partial` in the list is re-measured on all ~5 snapshots per second.
|
|
35
|
+
*/
|
|
36
|
+
const changeCache = new WeakMap();
|
|
37
|
+
function toWireChange(change) {
|
|
38
|
+
const cached = changeCache.get(change);
|
|
39
|
+
if (cached) return cached;
|
|
40
|
+
const wire = {
|
|
41
|
+
...change,
|
|
42
|
+
prevState: STATE_ON_DEVICE,
|
|
43
|
+
nextState: STATE_ON_DEVICE,
|
|
44
|
+
partial: toWirePartial(change.partial)
|
|
45
|
+
};
|
|
46
|
+
changeCache.set(change, wire);
|
|
47
|
+
return wire;
|
|
48
|
+
}
|
|
49
|
+
function toWirePartial(value) {
|
|
50
|
+
if (value === undefined) return value;
|
|
51
|
+
return (0, _sharedUi.isOverWireBudget)(value, MAX_WIRE_PAYLOAD_BYTES) ? PAYLOAD_ON_DEVICE : value;
|
|
52
|
+
}
|
|
53
|
+
function toWireState(value) {
|
|
54
|
+
return (0, _sharedUi.isOverWireBudget)(value, MAX_WIRE_PAYLOAD_BYTES) ? STATE_ON_DEVICE : value;
|
|
55
|
+
}
|
|
56
|
+
|
|
13
57
|
/**
|
|
14
58
|
* Sync adapter for the zustand tool, consumed by @buoy-gg/external-sync's
|
|
15
59
|
* `useExternalSync` (structurally matches its ToolSyncAdapter interface so
|
|
@@ -21,13 +65,21 @@ function topLevelKeys(state) {
|
|
|
21
65
|
* change timeline and the store registry (with each store's current state)
|
|
22
66
|
* so the dashboard can render the store browser.
|
|
23
67
|
*
|
|
68
|
+
* v2: change-log prevState/nextState are replaced with on-device markers
|
|
69
|
+
* (see STATE_ON_DEVICE). Dashboards fetch trees via `getChangeDetail`.
|
|
70
|
+
* Oversized / unserializable `stores[].currentState` uses the same marker
|
|
71
|
+
* and is fetched via `getStoreState`.
|
|
72
|
+
*
|
|
24
73
|
* The `setState` action powers remote time-travel/reset from the dashboard.
|
|
25
74
|
*/
|
|
26
75
|
const zustandSyncAdapter = exports.zustandSyncAdapter = {
|
|
27
|
-
version:
|
|
76
|
+
version: 2,
|
|
28
77
|
getSnapshot: () => ({
|
|
29
|
-
changes: _zustandStateStore.zustandStateStore.getStateChanges(),
|
|
30
|
-
stores: _zustandStateStore.zustandStateStore.getStoreSnapshots()
|
|
78
|
+
changes: _zustandStateStore.zustandStateStore.getStateChanges().map(toWireChange),
|
|
79
|
+
stores: _zustandStateStore.zustandStateStore.getStoreSnapshots().map(store => ({
|
|
80
|
+
...store,
|
|
81
|
+
currentState: toWireState(store.currentState)
|
|
82
|
+
}))
|
|
31
83
|
}),
|
|
32
84
|
subscribe: onChange => {
|
|
33
85
|
const unsubscribeChanges = _zustandStateStore.zustandStateStore.subscribe(onChange);
|
|
@@ -41,6 +93,77 @@ const zustandSyncAdapter = exports.zustandSyncAdapter = {
|
|
|
41
93
|
clearEvents: () => {
|
|
42
94
|
_zustandStateStore.zustandStateStore.clearStateChanges();
|
|
43
95
|
},
|
|
96
|
+
/**
|
|
97
|
+
* On-demand detail: one change's real prevState/nextState/partial.
|
|
98
|
+
* The per-snapshot stream stays tree-free; this is the explicit,
|
|
99
|
+
* size-guarded channel for the detail pane.
|
|
100
|
+
*/
|
|
101
|
+
getChangeDetail: params => {
|
|
102
|
+
const {
|
|
103
|
+
id
|
|
104
|
+
} = params ?? {};
|
|
105
|
+
if (!id) return {
|
|
106
|
+
found: false,
|
|
107
|
+
reason: "missing id"
|
|
108
|
+
};
|
|
109
|
+
const change = _zustandStateStore.zustandStateStore.getStateChangeById(id);
|
|
110
|
+
if (!change) return {
|
|
111
|
+
found: false,
|
|
112
|
+
reason: "unknown id"
|
|
113
|
+
};
|
|
114
|
+
const cappedNote = {
|
|
115
|
+
__buoyTruncated: true,
|
|
116
|
+
note: `Exceeds the ${MAX_CHANGE_DETAIL_BYTES / (1024 * 1024)}MB detail limit — inspect it on-device.`
|
|
117
|
+
};
|
|
118
|
+
const cap = value => (0, _sharedUi.isOverWireBudget)(value, MAX_CHANGE_DETAIL_BYTES) ? cappedNote : value;
|
|
119
|
+
return {
|
|
120
|
+
found: true,
|
|
121
|
+
id,
|
|
122
|
+
prevState: cap(change.prevState),
|
|
123
|
+
nextState: cap(change.nextState),
|
|
124
|
+
partial: cap(change.partial)
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
/**
|
|
128
|
+
* On-demand current state for one store. Wire-form snapshots replace
|
|
129
|
+
* oversized / unserializable `currentState` with STATE_ON_DEVICE.
|
|
130
|
+
*/
|
|
131
|
+
getStoreState: params => {
|
|
132
|
+
const {
|
|
133
|
+
storeName
|
|
134
|
+
} = params ?? {};
|
|
135
|
+
if (!storeName) return {
|
|
136
|
+
found: false,
|
|
137
|
+
reason: "missing storeName"
|
|
138
|
+
};
|
|
139
|
+
const store = _zustandStateStore.zustandStateStore.getStore(storeName);
|
|
140
|
+
if (!store) return {
|
|
141
|
+
found: false,
|
|
142
|
+
reason: "unknown storeName"
|
|
143
|
+
};
|
|
144
|
+
let currentState;
|
|
145
|
+
try {
|
|
146
|
+
currentState = store.api.getState();
|
|
147
|
+
} catch {
|
|
148
|
+
currentState = undefined;
|
|
149
|
+
}
|
|
150
|
+
const cappedNote = {
|
|
151
|
+
__buoyTruncated: true,
|
|
152
|
+
note: `Exceeds the ${MAX_CHANGE_DETAIL_BYTES / (1024 * 1024)}MB detail limit — inspect it on-device.`
|
|
153
|
+
};
|
|
154
|
+
if (!(0, _sharedUi.isOverWireBudget)(currentState, MAX_CHANGE_DETAIL_BYTES)) {
|
|
155
|
+
return {
|
|
156
|
+
found: true,
|
|
157
|
+
storeName,
|
|
158
|
+
currentState
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
found: true,
|
|
163
|
+
storeName,
|
|
164
|
+
currentState: (0, _sharedUi.isJsonSerializable)(currentState) ? cappedNote : STATE_ON_DEVICE
|
|
165
|
+
};
|
|
166
|
+
},
|
|
44
167
|
/**
|
|
45
168
|
* Compact current-state reader for a remote driver (MCP/LLM): the registered
|
|
46
169
|
* stores with light metadata; each store's full `currentState` object stays
|
|
@@ -77,8 +200,20 @@ const zustandSyncAdapter = exports.zustandSyncAdapter = {
|
|
|
77
200
|
storeName,
|
|
78
201
|
state,
|
|
79
202
|
replace
|
|
80
|
-
} = params;
|
|
81
|
-
|
|
203
|
+
} = params ?? {};
|
|
204
|
+
if (!storeName) return {
|
|
205
|
+
ok: false,
|
|
206
|
+
error: "Missing storeName."
|
|
207
|
+
};
|
|
208
|
+
const store = _zustandStateStore.zustandStateStore.getStore(storeName);
|
|
209
|
+
if (!store) return {
|
|
210
|
+
ok: false,
|
|
211
|
+
error: `No store named "${storeName}".`
|
|
212
|
+
};
|
|
213
|
+
store.api.setState(state, replace ?? true);
|
|
214
|
+
return {
|
|
215
|
+
ok: true
|
|
216
|
+
};
|
|
82
217
|
}
|
|
83
218
|
}
|
|
84
219
|
};
|
|
@@ -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 _zustandStateStore = require("./zustandStateStore");
|
|
5
6
|
/**
|
|
6
7
|
* Time Machine snapshot provider for Zustand.
|
|
@@ -117,8 +118,12 @@ async function capture() {
|
|
|
117
118
|
warnings
|
|
118
119
|
};
|
|
119
120
|
}
|
|
120
|
-
async function restore(data) {
|
|
121
|
+
async function restore(data, options) {
|
|
121
122
|
const snapshot = data;
|
|
123
|
+
const excludedSet = new Set(options?.excludeKeys ?? []);
|
|
124
|
+
const includedSet = options?.includeKeys ? new Set(options.includeKeys) : null;
|
|
125
|
+
const touch = key => !excludedSet.has(key) && (!includedSet || includedSet.has(key));
|
|
126
|
+
let excludedHits = 0;
|
|
122
127
|
const result = {
|
|
123
128
|
ok: true,
|
|
124
129
|
applied: 0,
|
|
@@ -134,6 +139,10 @@ async function restore(data) {
|
|
|
134
139
|
};
|
|
135
140
|
}
|
|
136
141
|
for (const [name, envelope] of Object.entries(snapshot.stores)) {
|
|
142
|
+
if (!touch(name)) {
|
|
143
|
+
excludedHits += 1;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
137
146
|
const info = _zustandStateStore.zustandStateStore.getStore(name);
|
|
138
147
|
if (!info) {
|
|
139
148
|
result.skipped.push({
|
|
@@ -160,12 +169,111 @@ async function restore(data) {
|
|
|
160
169
|
result.ok = false;
|
|
161
170
|
}
|
|
162
171
|
}
|
|
172
|
+
if (excludedHits > 0) {
|
|
173
|
+
result.warnings.push(`${excludedHits} store(s) excluded by you`);
|
|
174
|
+
}
|
|
163
175
|
return result;
|
|
164
176
|
}
|
|
177
|
+
const ITEM_RENDER_BYTE_LIMIT = 256 * 1024;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Per-store restore preview. Both sides are function-stripped through the
|
|
181
|
+
* SAME depth-8 normalizer as capture — diffing snapshot-vs-RAW-live would
|
|
182
|
+
* report every stripped function and every beyond-depth subtree as removed.
|
|
183
|
+
*/
|
|
184
|
+
async function previewItems(target, base) {
|
|
185
|
+
const targetStores = target?.stores ?? {};
|
|
186
|
+
const liveMode = base === undefined;
|
|
187
|
+
const baseStores = {};
|
|
188
|
+
if (liveMode) {
|
|
189
|
+
for (const info of _zustandStateStore.zustandStateStore.getStores()) {
|
|
190
|
+
const fnPaths = [];
|
|
191
|
+
try {
|
|
192
|
+
baseStores[info.name] = {
|
|
193
|
+
state: stripFunctions(info.api.getState(), "", fnPaths, new WeakSet(), 0),
|
|
194
|
+
fnPaths
|
|
195
|
+
};
|
|
196
|
+
} catch {
|
|
197
|
+
// unreadable live store — treated as absent
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
Object.assign(baseStores, base?.stores ?? {});
|
|
202
|
+
}
|
|
203
|
+
const items = [];
|
|
204
|
+
const notes = [];
|
|
205
|
+
let preservedFns = 0;
|
|
206
|
+
const names = new Set([...Object.keys(baseStores), ...Object.keys(targetStores)]);
|
|
207
|
+
for (const name of names) {
|
|
208
|
+
const inTarget = name in targetStores;
|
|
209
|
+
const inBase = name in baseStores;
|
|
210
|
+
if (!inTarget) {
|
|
211
|
+
// Restore never touches stores the snapshot doesn't know about.
|
|
212
|
+
if (liveMode) notes.push(`store "${name}" not in snapshot — untouched`);else items.push({
|
|
213
|
+
key: name,
|
|
214
|
+
label: name,
|
|
215
|
+
verdict: "removed",
|
|
216
|
+
oldValue: baseStores[name].state
|
|
217
|
+
});
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
preservedFns += targetStores[name].fnPaths?.length ?? 0;
|
|
221
|
+
if (liveMode && !_zustandStateStore.zustandStateStore.getStore(name)) {
|
|
222
|
+
items.push({
|
|
223
|
+
key: name,
|
|
224
|
+
label: name,
|
|
225
|
+
verdict: "wont-apply",
|
|
226
|
+
reason: "store not registered (renamed, or created after snapshot?)"
|
|
227
|
+
});
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (!inBase) {
|
|
231
|
+
items.push({
|
|
232
|
+
key: name,
|
|
233
|
+
label: name,
|
|
234
|
+
verdict: "added",
|
|
235
|
+
newValue: targetStores[name].state
|
|
236
|
+
});
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const oldState = baseStores[name].state;
|
|
240
|
+
const newState = targetStores[name].state;
|
|
241
|
+
if ((0, _dataViewer.boundedDeepEqual)(oldState, newState)) {
|
|
242
|
+
items.push({
|
|
243
|
+
key: name,
|
|
244
|
+
label: name,
|
|
245
|
+
verdict: "unchanged"
|
|
246
|
+
});
|
|
247
|
+
} else if ((0, _dataViewer.jsonLikelyLargerThan)(oldState, ITEM_RENDER_BYTE_LIMIT) || (0, _dataViewer.jsonLikelyLargerThan)(newState, ITEM_RENDER_BYTE_LIMIT)) {
|
|
248
|
+
items.push({
|
|
249
|
+
key: name,
|
|
250
|
+
label: name,
|
|
251
|
+
verdict: "too-large",
|
|
252
|
+
reason: "store too large to render a diff — restore still applies it"
|
|
253
|
+
});
|
|
254
|
+
} else {
|
|
255
|
+
items.push({
|
|
256
|
+
key: name,
|
|
257
|
+
label: name,
|
|
258
|
+
verdict: "changed",
|
|
259
|
+
oldValue: oldState,
|
|
260
|
+
newValue: newState
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (preservedFns > 0) {
|
|
265
|
+
notes.push(`${preservedFns} store action function${preservedFns === 1 ? "" : "s"} preserved on restore (not diffed)`);
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
items,
|
|
269
|
+
notes
|
|
270
|
+
};
|
|
271
|
+
}
|
|
165
272
|
(0, _sharedUi.registerStateSnapshotProvider)({
|
|
166
273
|
id: "zustand",
|
|
167
274
|
label: "Zustand",
|
|
168
275
|
status,
|
|
169
276
|
capture,
|
|
170
|
-
restore
|
|
277
|
+
restore,
|
|
278
|
+
previewItems
|
|
171
279
|
});
|
|
@@ -276,6 +276,27 @@ class ZustandStateStore {
|
|
|
276
276
|
|
|
277
277
|
// ---- Remote Mirror Mode ----
|
|
278
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Replace the change log without touching the store registry. Used when a
|
|
281
|
+
* host keeps real registered stores (JUMP / RESET stay wired) but needs to
|
|
282
|
+
* reseed the event list — the docs live tour, or any mirror that already
|
|
283
|
+
* called `registerStore`. Does not fire `onClear`. Respects the enabled
|
|
284
|
+
* flag so the UI's pause button freezes the reseed.
|
|
285
|
+
*/
|
|
286
|
+
replaceStateChanges(changes) {
|
|
287
|
+
if (!this.isEnabled) return;
|
|
288
|
+
this.stateChanges = changes.slice(0, this.maxChanges);
|
|
289
|
+
for (const store of this.stores.values()) {
|
|
290
|
+
store.stateChangeCount = 0;
|
|
291
|
+
}
|
|
292
|
+
for (const change of this.stateChanges) {
|
|
293
|
+
const store = this.stores.get(change.storeName);
|
|
294
|
+
if (store) store.stateChangeCount++;
|
|
295
|
+
}
|
|
296
|
+
this.notifyListeners();
|
|
297
|
+
this.notifyStoreListeners();
|
|
298
|
+
}
|
|
299
|
+
|
|
279
300
|
/**
|
|
280
301
|
* Permanently suppress local capture. Use when this store acts as a mirror
|
|
281
302
|
* of a remote device's state changes (e.g. the desktop dashboard): data
|
|
@@ -330,22 +351,92 @@ class ZustandStateStore {
|
|
|
330
351
|
*/
|
|
331
352
|
replaceFromSnapshot(changes, storeSnapshots) {
|
|
332
353
|
if (!this.isEnabled) return;
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
354
|
+
|
|
355
|
+
// Preserve trees a remote mirror already hydrated — wire-form snapshots
|
|
356
|
+
// always carry markers, and a later snapshot must not blank the detail pane.
|
|
357
|
+
const hydrated = new Map();
|
|
358
|
+
for (const existing of this.stateChanges) {
|
|
359
|
+
const prev = existing.prevState;
|
|
360
|
+
if (!prev || prev.__buoyStateOnDevice !== true) {
|
|
361
|
+
hydrated.set(existing.id, existing);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
this.stateChanges = changes.slice(0, this.maxChanges).map(change => {
|
|
365
|
+
const kept = hydrated.get(change.id);
|
|
366
|
+
const incoming = change.prevState;
|
|
367
|
+
return kept && incoming?.__buoyStateOnDevice === true ? {
|
|
368
|
+
...change,
|
|
369
|
+
prevState: kept.prevState,
|
|
370
|
+
nextState: kept.nextState,
|
|
371
|
+
partial: kept.partial
|
|
372
|
+
} : change;
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Preserve currentState a remote mirror already hydrated — oversized
|
|
376
|
+
// stores arrive as STATE_ON_DEVICE markers on every snapshot.
|
|
377
|
+
const hydratedStores = new Map();
|
|
378
|
+
for (const [name, store] of this.stores) {
|
|
379
|
+
try {
|
|
380
|
+
const state = store.api.getState();
|
|
381
|
+
const marker = state;
|
|
382
|
+
if (!marker || marker.__buoyStateOnDevice !== true) {
|
|
383
|
+
hydratedStores.set(name, state);
|
|
384
|
+
}
|
|
385
|
+
} catch {
|
|
386
|
+
// ignore
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
this.stores = new Map(storeSnapshots.map(snapshot => {
|
|
390
|
+
const incoming = snapshot.currentState;
|
|
391
|
+
const kept = hydratedStores.get(snapshot.name);
|
|
392
|
+
const currentState = kept !== undefined && incoming?.__buoyStateOnDevice === true ? kept : snapshot.currentState;
|
|
393
|
+
return [snapshot.name, {
|
|
394
|
+
name: snapshot.name,
|
|
395
|
+
api: {
|
|
396
|
+
getState: () => currentState,
|
|
397
|
+
setState: (partial, replace) => {
|
|
398
|
+
this.remoteSetStateHandler?.(snapshot.name, partial, replace);
|
|
399
|
+
},
|
|
400
|
+
subscribe: () => () => {}
|
|
340
401
|
},
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
402
|
+
stateChangeCount: snapshot.stateChangeCount,
|
|
403
|
+
isPersisted: snapshot.isPersisted,
|
|
404
|
+
persistName: snapshot.persistName,
|
|
405
|
+
color: snapshot.color
|
|
406
|
+
}];
|
|
407
|
+
}));
|
|
408
|
+
this.notifyListeners();
|
|
409
|
+
this.notifyStoreListeners();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Hydrate one mirrored change with real trees fetched on demand from the
|
|
414
|
+
* device via the adapter's `getChangeDetail`. Remote-mirror (desktop) only.
|
|
415
|
+
*/
|
|
416
|
+
hydrateChange(id, patch) {
|
|
417
|
+
const index = this.stateChanges.findIndex(c => c.id === id);
|
|
418
|
+
if (index === -1) return;
|
|
419
|
+
this.stateChanges = [...this.stateChanges.slice(0, index), {
|
|
420
|
+
...this.stateChanges[index],
|
|
421
|
+
...patch
|
|
422
|
+
}, ...this.stateChanges.slice(index + 1)];
|
|
348
423
|
this.notifyListeners();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Hydrate one mirrored store's currentState fetched on demand from the
|
|
428
|
+
* device via the adapter's `getStoreState`. Remote-mirror (desktop) only.
|
|
429
|
+
*/
|
|
430
|
+
hydrateStoreState(storeName, currentState) {
|
|
431
|
+
const store = this.stores.get(storeName);
|
|
432
|
+
if (!store) return;
|
|
433
|
+
this.stores.set(storeName, {
|
|
434
|
+
...store,
|
|
435
|
+
api: {
|
|
436
|
+
...store.api,
|
|
437
|
+
getState: () => currentState
|
|
438
|
+
}
|
|
439
|
+
});
|
|
349
440
|
this.notifyStoreListeners();
|
|
350
441
|
}
|
|
351
442
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - Events tab: Live state 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, Lock, Box, devToolsStorageKeys, ProUpgradeModal, PowerToggleButton, ToolbarCopyButton, ToolbarClearButton, truncatePayload, TickProvider } from "@buoy-gg/shared-ui";
|
|
14
14
|
import { useIsPro } from "@buoy-gg/license";
|
|
@@ -44,11 +44,16 @@ export function ZustandModal({
|
|
|
44
44
|
onMinimize,
|
|
45
45
|
enableSharedModalDimensions = false,
|
|
46
46
|
inline = false,
|
|
47
|
-
storesLoading = false
|
|
47
|
+
storesLoading = false,
|
|
48
|
+
initialTab = "stores",
|
|
49
|
+
initialSelectedChangeId,
|
|
50
|
+
initialDetailView,
|
|
51
|
+
onChangeSelect,
|
|
52
|
+
onStoreExpand
|
|
48
53
|
}) {
|
|
49
54
|
const isPro = useIsPro();
|
|
50
55
|
const [showProModal, setShowProModal] = useState(false);
|
|
51
|
-
const [activeTab, setActiveTab] = useState(
|
|
56
|
+
const [activeTab, setActiveTab] = useState(initialTab);
|
|
52
57
|
const [selectedHistoryStore, setSelectedHistoryStore] = useState(null);
|
|
53
58
|
const [showFilters, setShowFilters] = useState(false);
|
|
54
59
|
const [ignoredPatterns, setIgnoredPatterns] = useState(new Set());
|
|
@@ -103,6 +108,30 @@ export function ZustandModal({
|
|
|
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 state trees here).
|
|
113
|
+
const lastNotifiedSelectionRef = useRef(null);
|
|
114
|
+
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 = useRef(null);
|
|
126
|
+
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 — shared across tabs
|
|
107
136
|
const [searchText, setSearchText] = useState("");
|
|
108
137
|
const [isSearchActive, setIsSearchActive] = useState(false);
|
|
@@ -365,7 +394,8 @@ export function ZustandModal({
|
|
|
365
394
|
changes: activeChanges,
|
|
366
395
|
selectedIndex: selectedChangeIndex,
|
|
367
396
|
onIndexChange: handleIndexChange,
|
|
368
|
-
disableInternalFooter: true
|
|
397
|
+
disableInternalFooter: true,
|
|
398
|
+
initialView: initialDetailView
|
|
369
399
|
});
|
|
370
400
|
}
|
|
371
401
|
|
|
@@ -403,7 +433,8 @@ export function ZustandModal({
|
|
|
403
433
|
stores: displayedStores,
|
|
404
434
|
searchQuery: searchText,
|
|
405
435
|
onViewHistory: handleViewHistory,
|
|
406
|
-
isLoading: storesLoading
|
|
436
|
+
isLoading: storesLoading,
|
|
437
|
+
onStoreExpand: onStoreExpand
|
|
407
438
|
});
|
|
408
439
|
}
|
|
409
440
|
|
|
@@ -62,9 +62,10 @@ export function ZustandStateDetailContent({
|
|
|
62
62
|
changes,
|
|
63
63
|
selectedIndex,
|
|
64
64
|
onIndexChange,
|
|
65
|
-
disableInternalFooter = false
|
|
65
|
+
disableInternalFooter = false,
|
|
66
|
+
initialView = "change"
|
|
66
67
|
}) {
|
|
67
|
-
const [activeView, setActiveView] = useState(
|
|
68
|
+
const [activeView, setActiveView] = useState(initialView);
|
|
68
69
|
const [diffMode, setDiffMode] = useState("tree");
|
|
69
70
|
const totalChanges = changes.length;
|
|
70
71
|
const changeColor = getChangeColor(change);
|