@buoy-gg/zustand 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.
Files changed (31) hide show
  1. package/lib/commonjs/index.js +1 -0
  2. package/lib/commonjs/zustand/components/ZustandModal.js +35 -4
  3. package/lib/commonjs/zustand/components/ZustandStateDetailContent.js +3 -2
  4. package/lib/commonjs/zustand/components/ZustandStoreBrowser.js +11 -3
  5. package/lib/commonjs/zustand/sync/zustandSyncAdapter.js +178 -6
  6. package/lib/commonjs/zustand/utils/snapshotProvider.js +279 -0
  7. package/lib/commonjs/zustand/utils/zustandStateStore.js +105 -14
  8. package/lib/module/index.js +3 -0
  9. package/lib/module/zustand/components/ZustandModal.js +36 -5
  10. package/lib/module/zustand/components/ZustandStateDetailContent.js +3 -2
  11. package/lib/module/zustand/components/ZustandStoreBrowser.js +11 -3
  12. package/lib/module/zustand/sync/zustandSyncAdapter.js +177 -5
  13. package/lib/module/zustand/utils/snapshotProvider.js +278 -0
  14. package/lib/module/zustand/utils/zustandStateStore.js +105 -14
  15. package/lib/typescript/index.d.ts +1 -0
  16. package/lib/typescript/index.d.ts.map +1 -1
  17. package/lib/typescript/zustand/components/ZustandModal.d.ts +1 -1
  18. package/lib/typescript/zustand/components/ZustandModal.d.ts.map +1 -1
  19. package/lib/typescript/zustand/components/ZustandStateDetailContent.d.ts +4 -1
  20. package/lib/typescript/zustand/components/ZustandStateDetailContent.d.ts.map +1 -1
  21. package/lib/typescript/zustand/components/ZustandStoreBrowser.d.ts +3 -1
  22. package/lib/typescript/zustand/components/ZustandStoreBrowser.d.ts.map +1 -1
  23. package/lib/typescript/zustand/sync/zustandSyncAdapter.d.ts +71 -3
  24. package/lib/typescript/zustand/sync/zustandSyncAdapter.d.ts.map +1 -1
  25. package/lib/typescript/zustand/types/index.d.ts +26 -0
  26. package/lib/typescript/zustand/types/index.d.ts.map +1 -1
  27. package/lib/typescript/zustand/utils/snapshotProvider.d.ts +2 -0
  28. package/lib/typescript/zustand/utils/snapshotProvider.d.ts.map +1 -0
  29. package/lib/typescript/zustand/utils/zustandStateStore.d.ts +18 -0
  30. package/lib/typescript/zustand/utils/zustandStateStore.d.ts.map +1 -1
  31. package/package.json +7 -4
@@ -93,6 +93,7 @@ Object.defineProperty(exports, "zustandToolPreset", {
93
93
  return _preset.zustandToolPreset;
94
94
  }
95
95
  });
96
+ require("./zustand/utils/snapshotProvider");
96
97
  var _preset = require("./preset");
97
98
  var _buoyZustandMiddleware = require("./zustand/utils/buoyZustandMiddleware");
98
99
  var _useZustandStateChanges = require("./zustand/hooks/useZustandStateChanges");
@@ -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)("stores");
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)("change");
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 => prev === store.name ? null : store.name);
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,12 +3,84 @@
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
+ /**
9
+ * Sentinel used in WIRE-FORM snapshots in place of raw prevState/nextState.
10
+ * Same hazard as redux: up to 200 changes × 2 full trees on every snapshot
11
+ * freeze/OOM-killed large apps. Dashboards fetch trees via `getChangeDetail`.
12
+ */
13
+ const STATE_ON_DEVICE = exports.STATE_ON_DEVICE = Object.freeze({
14
+ __buoyStateOnDevice: true,
15
+ note: "State trees stay on the device — fetched on demand via getChangeDetail / getStoreState."
16
+ });
17
+
18
+ /** Wire-form sentinel for an oversized `partial` (replace can carry a whole tree). */
19
+ const PAYLOAD_ON_DEVICE = exports.PAYLOAD_ON_DEVICE = Object.freeze({
20
+ __buoyPayloadOnDevice: true,
21
+ note: "Partial stays on the device — fetched on demand via getChangeDetail."
22
+ });
23
+ const MAX_CHANGE_DETAIL_BYTES = 8 * 1024 * 1024;
24
+ const MAX_WIRE_PAYLOAD_BYTES = 16 * 1024;
25
+ function jsonLongerThan(value, limit) {
26
+ let bytes = 0;
27
+ const stack = [value];
28
+ const seen = new Set();
29
+ while (stack.length > 0) {
30
+ if (bytes > limit) return true;
31
+ const v = stack.pop();
32
+ 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") {
33
+ if (seen.has(v)) continue;
34
+ seen.add(v);
35
+ if (Array.isArray(v)) {
36
+ bytes += 2 + v.length;
37
+ for (let i = 0; i < v.length; i++) stack.push(v[i]);
38
+ } else {
39
+ bytes += 2;
40
+ for (const k in v) {
41
+ bytes += k.length + 4;
42
+ stack.push(v[k]);
43
+ }
44
+ }
45
+ } else bytes += 4;
46
+ }
47
+ return bytes > limit;
48
+ }
8
49
  /** Top-level keys of an object state, for the compact (no-values) view. */
9
50
  function topLevelKeys(state) {
10
51
  return state && typeof state === "object" && !Array.isArray(state) ? Object.keys(state) : undefined;
11
52
  }
53
+ function toWireChange(change) {
54
+ return {
55
+ ...change,
56
+ prevState: STATE_ON_DEVICE,
57
+ nextState: STATE_ON_DEVICE,
58
+ partial: toWirePartial(change.partial)
59
+ };
60
+ }
61
+ function toWirePartial(value) {
62
+ if (value === undefined) return value;
63
+ try {
64
+ JSON.stringify(value);
65
+ } catch {
66
+ return PAYLOAD_ON_DEVICE;
67
+ }
68
+ if (jsonLongerThan(value, MAX_WIRE_PAYLOAD_BYTES)) {
69
+ return PAYLOAD_ON_DEVICE;
70
+ }
71
+ return value;
72
+ }
73
+ function toWireState(value) {
74
+ try {
75
+ JSON.stringify(value);
76
+ } catch {
77
+ return STATE_ON_DEVICE;
78
+ }
79
+ if (jsonLongerThan(value, MAX_WIRE_PAYLOAD_BYTES)) {
80
+ return STATE_ON_DEVICE;
81
+ }
82
+ return value;
83
+ }
12
84
 
13
85
  /**
14
86
  * Sync adapter for the zustand tool, consumed by @buoy-gg/external-sync's
@@ -21,13 +93,21 @@ function topLevelKeys(state) {
21
93
  * change timeline and the store registry (with each store's current state)
22
94
  * so the dashboard can render the store browser.
23
95
  *
96
+ * v2: change-log prevState/nextState are replaced with on-device markers
97
+ * (see STATE_ON_DEVICE). Dashboards fetch trees via `getChangeDetail`.
98
+ * Oversized / unserializable `stores[].currentState` uses the same marker
99
+ * and is fetched via `getStoreState`.
100
+ *
24
101
  * The `setState` action powers remote time-travel/reset from the dashboard.
25
102
  */
26
103
  const zustandSyncAdapter = exports.zustandSyncAdapter = {
27
- version: 1,
104
+ version: 2,
28
105
  getSnapshot: () => ({
29
- changes: _zustandStateStore.zustandStateStore.getStateChanges(),
30
- stores: _zustandStateStore.zustandStateStore.getStoreSnapshots()
106
+ changes: _zustandStateStore.zustandStateStore.getStateChanges().map(toWireChange),
107
+ stores: _zustandStateStore.zustandStateStore.getStoreSnapshots().map(store => ({
108
+ ...store,
109
+ currentState: toWireState(store.currentState)
110
+ }))
31
111
  }),
32
112
  subscribe: onChange => {
33
113
  const unsubscribeChanges = _zustandStateStore.zustandStateStore.subscribe(onChange);
@@ -41,6 +121,86 @@ const zustandSyncAdapter = exports.zustandSyncAdapter = {
41
121
  clearEvents: () => {
42
122
  _zustandStateStore.zustandStateStore.clearStateChanges();
43
123
  },
124
+ /**
125
+ * On-demand detail: one change's real prevState/nextState/partial.
126
+ * The per-snapshot stream stays tree-free; this is the explicit,
127
+ * size-guarded channel for the detail pane.
128
+ */
129
+ getChangeDetail: params => {
130
+ const {
131
+ id
132
+ } = params ?? {};
133
+ if (!id) return {
134
+ found: false,
135
+ reason: "missing id"
136
+ };
137
+ const change = _zustandStateStore.zustandStateStore.getStateChangeById(id);
138
+ if (!change) return {
139
+ found: false,
140
+ reason: "unknown id"
141
+ };
142
+ const cappedNote = {
143
+ __buoyTruncated: true,
144
+ note: `Exceeds the ${MAX_CHANGE_DETAIL_BYTES / (1024 * 1024)}MB detail limit — inspect it on-device.`
145
+ };
146
+ const cap = value => {
147
+ try {
148
+ JSON.stringify(value);
149
+ } catch {
150
+ return cappedNote;
151
+ }
152
+ return jsonLongerThan(value, MAX_CHANGE_DETAIL_BYTES) ? cappedNote : value;
153
+ };
154
+ return {
155
+ found: true,
156
+ id,
157
+ prevState: cap(change.prevState),
158
+ nextState: cap(change.nextState),
159
+ partial: cap(change.partial)
160
+ };
161
+ },
162
+ /**
163
+ * On-demand current state for one store. Wire-form snapshots replace
164
+ * oversized / unserializable `currentState` with STATE_ON_DEVICE.
165
+ */
166
+ getStoreState: params => {
167
+ const {
168
+ storeName
169
+ } = params ?? {};
170
+ if (!storeName) return {
171
+ found: false,
172
+ reason: "missing storeName"
173
+ };
174
+ const store = _zustandStateStore.zustandStateStore.getStore(storeName);
175
+ if (!store) return {
176
+ found: false,
177
+ reason: "unknown storeName"
178
+ };
179
+ let currentState;
180
+ try {
181
+ currentState = store.api.getState();
182
+ } catch {
183
+ currentState = undefined;
184
+ }
185
+ const cappedNote = {
186
+ __buoyTruncated: true,
187
+ note: `Exceeds the ${MAX_CHANGE_DETAIL_BYTES / (1024 * 1024)}MB detail limit — inspect it on-device.`
188
+ };
189
+ try {
190
+ JSON.stringify(currentState);
191
+ } catch {
192
+ return {
193
+ found: true,
194
+ storeName,
195
+ currentState: STATE_ON_DEVICE
196
+ };
197
+ }
198
+ return {
199
+ found: true,
200
+ storeName,
201
+ currentState: jsonLongerThan(currentState, MAX_CHANGE_DETAIL_BYTES) ? cappedNote : currentState
202
+ };
203
+ },
44
204
  /**
45
205
  * Compact current-state reader for a remote driver (MCP/LLM): the registered
46
206
  * stores with light metadata; each store's full `currentState` object stays
@@ -77,8 +237,20 @@ const zustandSyncAdapter = exports.zustandSyncAdapter = {
77
237
  storeName,
78
238
  state,
79
239
  replace
80
- } = params;
81
- _zustandStateStore.zustandStateStore.getStore(storeName)?.api.setState(state, replace ?? true);
240
+ } = params ?? {};
241
+ if (!storeName) return {
242
+ ok: false,
243
+ error: "Missing storeName."
244
+ };
245
+ const store = _zustandStateStore.zustandStateStore.getStore(storeName);
246
+ if (!store) return {
247
+ ok: false,
248
+ error: `No store named "${storeName}".`
249
+ };
250
+ store.api.setState(state, replace ?? true);
251
+ return {
252
+ ok: true
253
+ };
82
254
  }
83
255
  }
84
256
  };
@@ -0,0 +1,279 @@
1
+ "use strict";
2
+
3
+ var _sharedUi = require("@buoy-gg/shared-ui");
4
+ var _dataViewer = require("@buoy-gg/shared-ui/dataViewer");
5
+ var _zustandStateStore = require("./zustandStateStore");
6
+ /**
7
+ * Time Machine snapshot provider for Zustand.
8
+ *
9
+ * Registered into @buoy-gg/shared-ui's stateSnapshotRegistry at module init;
10
+ * consumed by @buoy-gg/time-machine. Neither package imports the other.
11
+ *
12
+ * The Zustand-specific hazard: stores conventionally keep their action
13
+ * functions IN state (`increment: () => ...`). Functions don't survive
14
+ * serialization, and a full `setState(snapshot, true)` replace would wipe
15
+ * them, breaking every `useStore(s => s.increment)` call site. So capture
16
+ * strips functions (recording where they were), and restore re-grafts the
17
+ * LIVE store's functions onto the snapshot before replacing.
18
+ */
19
+
20
+ const MAX_DEPTH = 8;
21
+ function isPlainObjectLike(v) {
22
+ return typeof v === "object" && v !== null && !Array.isArray(v);
23
+ }
24
+
25
+ /** Clone `value` without functions, recording their paths. */
26
+ function stripFunctions(value, path, fnPaths, seen, depth) {
27
+ if (typeof value === "function") {
28
+ fnPaths.push(path);
29
+ return undefined;
30
+ }
31
+ if (typeof value !== "object" || value === null || depth >= MAX_DEPTH) {
32
+ return value;
33
+ }
34
+ if (seen.has(value)) return undefined; // cycle — drop the back-reference
35
+ seen.add(value);
36
+ if (Array.isArray(value)) {
37
+ return value.map((item, i) => typeof item === "function" ? (fnPaths.push(`${path}[${i}]`), null) : stripFunctions(item, `${path}[${i}]`, fnPaths, seen, depth + 1));
38
+ }
39
+ if (!isPlainObjectLike(value)) return value; // Date/Map/Set/etc. — superjson handles
40
+ const out = {};
41
+ for (const [k, v] of Object.entries(value)) {
42
+ if (typeof v === "function") {
43
+ fnPaths.push(path ? `${path}.${k}` : k);
44
+ continue;
45
+ }
46
+ out[k] = stripFunctions(v, path ? `${path}.${k}` : k, fnPaths, seen, depth + 1);
47
+ }
48
+ return out;
49
+ }
50
+
51
+ /**
52
+ * Copy every function-valued property from the LIVE state onto the restored
53
+ * snapshot (creating containers as needed), so store actions keep working
54
+ * after a full replace.
55
+ */
56
+ function graftFunctions(target, live, seen, depth) {
57
+ if (!isPlainObjectLike(live) || !isPlainObjectLike(target)) return;
58
+ if (depth >= MAX_DEPTH || seen.has(live)) return;
59
+ seen.add(live);
60
+ for (const [k, v] of Object.entries(live)) {
61
+ if (typeof v === "function") {
62
+ target[k] = v;
63
+ } else if (isPlainObjectLike(v)) {
64
+ if (!isPlainObjectLike(target[k])) {
65
+ // Branch missing from the snapshot — only recreate it if it holds
66
+ // functions somewhere; otherwise the snapshot's absence wins.
67
+ if (!containsFunction(v, new WeakSet(), depth + 1)) continue;
68
+ target[k] = {};
69
+ }
70
+ graftFunctions(target[k], v, seen, depth + 1);
71
+ }
72
+ }
73
+ }
74
+ function containsFunction(value, seen, depth) {
75
+ if (depth >= MAX_DEPTH || seen.has(value)) return false;
76
+ seen.add(value);
77
+ for (const v of Object.values(value)) {
78
+ if (typeof v === "function") return true;
79
+ if (isPlainObjectLike(v) && containsFunction(v, seen, depth + 1)) return true;
80
+ }
81
+ return false;
82
+ }
83
+ function status() {
84
+ const count = _zustandStateStore.zustandStateStore.getStores().length;
85
+ if (count === 0) {
86
+ return {
87
+ canCapture: false,
88
+ canRestore: false,
89
+ detail: "No Zustand stores registered — pass `zustandStores` to FloatingDevTools or add the buoyDevTools middleware"
90
+ };
91
+ }
92
+ return {
93
+ canCapture: true,
94
+ canRestore: true
95
+ };
96
+ }
97
+ async function capture() {
98
+ const warnings = [];
99
+ const stores = {};
100
+ for (const info of _zustandStateStore.zustandStateStore.getStores()) {
101
+ const fnPaths = [];
102
+ let state;
103
+ try {
104
+ state = stripFunctions(info.api.getState(), "", fnPaths, new WeakSet(), 0);
105
+ } catch (e) {
106
+ warnings.push(`store "${info.name}": capture failed (${String(e)})`);
107
+ continue;
108
+ }
109
+ stores[info.name] = {
110
+ state,
111
+ fnPaths
112
+ };
113
+ }
114
+ return {
115
+ data: {
116
+ stores
117
+ },
118
+ warnings
119
+ };
120
+ }
121
+ async function restore(data, options) {
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;
127
+ const result = {
128
+ ok: true,
129
+ applied: 0,
130
+ skipped: [],
131
+ warnings: []
132
+ };
133
+ if (!snapshot?.stores) {
134
+ return {
135
+ ok: false,
136
+ applied: 0,
137
+ skipped: [],
138
+ warnings: ["empty zustand snapshot"]
139
+ };
140
+ }
141
+ for (const [name, envelope] of Object.entries(snapshot.stores)) {
142
+ if (!touch(name)) {
143
+ excludedHits += 1;
144
+ continue;
145
+ }
146
+ const info = _zustandStateStore.zustandStateStore.getStore(name);
147
+ if (!info) {
148
+ result.skipped.push({
149
+ id: name,
150
+ reason: "store not registered (renamed, or created after snapshot?)"
151
+ });
152
+ continue;
153
+ }
154
+ try {
155
+ const next = envelope.state;
156
+ graftFunctions(next, info.api.getState(), new WeakSet(), 0);
157
+ info.api.setState(next, true);
158
+ if (info.api.getState() === next) {
159
+ result.applied += 1;
160
+ } else {
161
+ result.warnings.push(`store "${name}": setState did not take effect`);
162
+ result.ok = false;
163
+ }
164
+ } catch (e) {
165
+ result.skipped.push({
166
+ id: name,
167
+ reason: String(e)
168
+ });
169
+ result.ok = false;
170
+ }
171
+ }
172
+ if (excludedHits > 0) {
173
+ result.warnings.push(`${excludedHits} store(s) excluded by you`);
174
+ }
175
+ return result;
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
+ }
272
+ (0, _sharedUi.registerStateSnapshotProvider)({
273
+ id: "zustand",
274
+ label: "Zustand",
275
+ status,
276
+ capture,
277
+ restore,
278
+ previewItems
279
+ });