@buoy-gg/zustand 7.0.41 → 7.0.42

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 (63) hide show
  1. package/README.md +11 -76
  2. package/lib/commonjs/index.js +1 -105
  3. package/lib/commonjs/preset.js +1 -102
  4. package/lib/commonjs/zustand/components/ZustandActionButton.js +1 -118
  5. package/lib/commonjs/zustand/components/ZustandDetailViewToggle.js +1 -48
  6. package/lib/commonjs/zustand/components/ZustandEventFilterView.js +1 -43
  7. package/lib/commonjs/zustand/components/ZustandIcon.js +1 -18
  8. package/lib/commonjs/zustand/components/ZustandModal.js +1 -772
  9. package/lib/commonjs/zustand/components/ZustandStateChangeItem.js +1 -117
  10. package/lib/commonjs/zustand/components/ZustandStateDetailContent.js +1 -372
  11. package/lib/commonjs/zustand/components/ZustandStateInfoView.js +1 -488
  12. package/lib/commonjs/zustand/components/ZustandStoreBrowser.js +1 -204
  13. package/lib/commonjs/zustand/components/ZustandStoreDetailContent.js +1 -471
  14. package/lib/commonjs/zustand/hooks/useZustandStateChanges.js +1 -74
  15. package/lib/commonjs/zustand/sync/zustandSyncAdapter.js +1 -418
  16. package/lib/commonjs/zustand/types/index.js +1 -1
  17. package/lib/commonjs/zustand/utils/buoyZustandMiddleware.js +1 -284
  18. package/lib/commonjs/zustand/utils/editableState.js +1 -146
  19. package/lib/commonjs/zustand/utils/snapshotProvider.js +1 -209
  20. package/lib/commonjs/zustand/utils/testIDs.js +1 -82
  21. package/lib/commonjs/zustand/utils/zustandStateStore.js +1 -379
  22. package/lib/module/index.js +1 -90
  23. package/lib/module/preset.js +1 -98
  24. package/lib/module/zustand/components/ZustandActionButton.js +1 -114
  25. package/lib/module/zustand/components/ZustandDetailViewToggle.js +1 -43
  26. package/lib/module/zustand/components/ZustandEventFilterView.js +1 -39
  27. package/lib/module/zustand/components/ZustandIcon.js +1 -11
  28. package/lib/module/zustand/components/ZustandModal.js +1 -767
  29. package/lib/module/zustand/components/ZustandStateChangeItem.js +1 -113
  30. package/lib/module/zustand/components/ZustandStateDetailContent.js +1 -368
  31. package/lib/module/zustand/components/ZustandStateInfoView.js +1 -483
  32. package/lib/module/zustand/components/ZustandStoreBrowser.js +1 -200
  33. package/lib/module/zustand/components/ZustandStoreDetailContent.js +1 -469
  34. package/lib/module/zustand/hooks/useZustandStateChanges.js +1 -70
  35. package/lib/module/zustand/sync/zustandSyncAdapter.js +1 -415
  36. package/lib/module/zustand/types/index.js +1 -1
  37. package/lib/module/zustand/utils/buoyZustandMiddleware.js +1 -278
  38. package/lib/module/zustand/utils/editableState.js +1 -138
  39. package/lib/module/zustand/utils/snapshotProvider.js +1 -209
  40. package/lib/module/zustand/utils/testIDs.js +1 -74
  41. package/lib/module/zustand/utils/zustandStateStore.js +1 -375
  42. package/lib/web/index.mjs +2 -3
  43. package/package.json +3 -3
  44. package/lib/typescript/index.d.ts.map +0 -1
  45. package/lib/typescript/preset.d.ts.map +0 -1
  46. package/lib/typescript/zustand/components/ZustandActionButton.d.ts.map +0 -1
  47. package/lib/typescript/zustand/components/ZustandDetailViewToggle.d.ts.map +0 -1
  48. package/lib/typescript/zustand/components/ZustandEventFilterView.d.ts.map +0 -1
  49. package/lib/typescript/zustand/components/ZustandIcon.d.ts.map +0 -1
  50. package/lib/typescript/zustand/components/ZustandModal.d.ts.map +0 -1
  51. package/lib/typescript/zustand/components/ZustandStateChangeItem.d.ts.map +0 -1
  52. package/lib/typescript/zustand/components/ZustandStateDetailContent.d.ts.map +0 -1
  53. package/lib/typescript/zustand/components/ZustandStateInfoView.d.ts.map +0 -1
  54. package/lib/typescript/zustand/components/ZustandStoreBrowser.d.ts.map +0 -1
  55. package/lib/typescript/zustand/components/ZustandStoreDetailContent.d.ts.map +0 -1
  56. package/lib/typescript/zustand/hooks/useZustandStateChanges.d.ts.map +0 -1
  57. package/lib/typescript/zustand/sync/zustandSyncAdapter.d.ts.map +0 -1
  58. package/lib/typescript/zustand/types/index.d.ts.map +0 -1
  59. package/lib/typescript/zustand/utils/buoyZustandMiddleware.d.ts.map +0 -1
  60. package/lib/typescript/zustand/utils/editableState.d.ts.map +0 -1
  61. package/lib/typescript/zustand/utils/snapshotProvider.d.ts.map +0 -1
  62. package/lib/typescript/zustand/utils/testIDs.d.ts.map +0 -1
  63. package/lib/typescript/zustand/utils/zustandStateStore.d.ts.map +0 -1
@@ -1,284 +1 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.buoyDevTools = buoyDevTools;
7
- exports.isStoreInstrumented = isStoreInstrumented;
8
- exports.watchStores = watchStores;
9
- var _zustandStateStore = require("./zustandStateStore");
10
- /**
11
- * Buoy Zustand DevTools — Store instrumentation
12
- *
13
- * Two approaches, from easiest to most detailed:
14
- *
15
- * 1. watchStores() — RECOMMENDED. One line, zero store modifications.
16
- * Uses store.subscribe() externally. Never touches setState.
17
- * Safe to add/remove — cannot break your stores even if our code has bugs.
18
- *
19
- * 2. buoyDevTools() — Opt-in middleware for advanced use.
20
- * Wraps setState to capture the partial argument and timing.
21
- * Requires modifying each store's create() call.
22
- */
23
-
24
- /** Symbol to mark stores that have been watched/instrumented */
25
- const WATCHED_SYMBOL = Symbol.for("@@buoy-zustand/watched");
26
-
27
- /** The slice of zustand's persist API this module uses. */
28
-
29
- /**
30
- * Detect if a store is using the persist middleware by checking for persist API
31
- */
32
- function detectPersist(store) {
33
- const persist = store.persist;
34
- if (persist && typeof persist.getOptions === "function") {
35
- const options = persist.getOptions();
36
- return {
37
- isPersisted: true,
38
- persistName: options?.name,
39
- persistApi: persist
40
- };
41
- }
42
- return {
43
- isPersisted: false
44
- };
45
- }
46
-
47
- // =============================================================================
48
- // watchStores — Primary, non-intrusive approach
49
- // =============================================================================
50
-
51
- /** Minimal store shape — what Zustand's create() returns */
52
-
53
- /**
54
- * Watch multiple Zustand stores for state changes.
55
- *
56
- * This is the recommended, non-intrusive approach. It uses each store's
57
- * `.subscribe()` method to observe changes from the outside — it never
58
- * modifies `setState` or any store internals.
59
- *
60
- * **Safe by design:** Even if our listener code has a bug, it cannot break
61
- * your stores. All listener code is wrapped in try/catch.
62
- *
63
- * @example
64
- * ```tsx
65
- * // In your _layout.tsx or App.tsx — ONE line:
66
- * import { watchStores } from '@buoy-gg/zustand';
67
- * import { useCounterStore } from './stores/counter';
68
- * import { useAuthStore } from './stores/auth';
69
- * import { useCartStore } from './stores/cart';
70
- *
71
- * watchStores({
72
- * counterStore: useCounterStore,
73
- * authStore: useAuthStore,
74
- * cartStore: useCartStore,
75
- * });
76
- * ```
77
- *
78
- * @param stores - Object mapping store names to Zustand store hooks
79
- * @returns Cleanup function that removes all subscriptions
80
- */
81
- function watchStores(stores) {
82
- const cleanups = [];
83
- for (const [name, store] of Object.entries(stores)) {
84
- // Skip if already watched
85
- const storeAny = store;
86
- if (storeAny[WATCHED_SYMBOL]) continue;
87
- storeAny[WATCHED_SYMBOL] = true;
88
- const {
89
- isPersisted,
90
- persistName,
91
- persistApi
92
- } = detectPersist(store);
93
-
94
- // Register the store for overview
95
- _zustandStateStore.zustandStateStore.registerStore(name, {
96
- getState: store.getState,
97
- getInitialState: store.getInitialState,
98
- setState: store.setState,
99
- subscribe: store.subscribe
100
- }, {
101
- isPersisted,
102
- persistName,
103
- // Was detected and thrown away. Without it nothing can re-read the
104
- // saved copy after an outside edit — see ZustandStoreInfo.rehydrate.
105
- rehydrate: typeof persistApi?.rehydrate === "function" ? () => persistApi.rehydrate() : undefined
106
- });
107
-
108
- // The rehydration is a state change nobody's code asked for — the store
109
- // simply becomes what the disk said, seconds into the process. Drawn as an
110
- // ordinary setState it is the most surprising row in the list and the one
111
- // that looks most like a bug, so it gets its own category. Recognised
112
- // rather than announced: `hasHydrated()` is still false while the
113
- // rehydrating `set` is running, and this listener runs inside it.
114
- let sawHydration = false;
115
-
116
- // Subscribe to state changes — completely external, never touches setState
117
- const unsubscribe = store.subscribe((state, prevState) => {
118
- try {
119
- let category;
120
- if (!sawHydration && isPersisted) {
121
- let hydrating = false;
122
- try {
123
- hydrating = persistApi?.hasHydrated?.() === false;
124
- } catch {
125
- // An older persist API without hasHydrated — no worse than before.
126
- }
127
- if (hydrating) {
128
- sawHydration = true;
129
- category = "persist";
130
- }
131
- }
132
- _zustandStateStore.zustandStateStore.addStateChange({
133
- storeName: name,
134
- partial: undefined,
135
- // Not available in subscribe-only mode
136
- replace: false,
137
- prevState,
138
- nextState: state,
139
- duration: undefined,
140
- // Not available in subscribe-only mode
141
- category,
142
- isPersisted
143
- });
144
- } catch {
145
- // Silently catch — our bug must never propagate into the store
146
- }
147
- });
148
- cleanups.push(() => {
149
- unsubscribe();
150
- delete store[WATCHED_SYMBOL];
151
- _zustandStateStore.zustandStateStore.unregisterStore(name);
152
- });
153
- }
154
- return () => {
155
- cleanups.forEach(fn => fn());
156
- };
157
- }
158
-
159
- // =============================================================================
160
- // buoyDevTools — Opt-in middleware (advanced, more detail)
161
- // =============================================================================
162
-
163
- /** Auto-incrementing store name counter for unnamed stores */
164
- let unnamedCounter = 0;
165
-
166
- /**
167
- * Zustand middleware that instruments a store for Buoy DevTools.
168
- *
169
- * This is the advanced approach — it wraps setState to capture:
170
- * - The partial argument passed to setState
171
- * - Precise timing of each setState call
172
- *
173
- * Use this when you want maximum detail. For most cases, `watchStores()`
174
- * is simpler and safer.
175
- *
176
- * @example
177
- * ```tsx
178
- * import { create } from 'zustand';
179
- * import { buoyDevTools } from '@buoy-gg/zustand';
180
- *
181
- * const useCounterStore = create(
182
- * buoyDevTools(
183
- * (set) => ({
184
- * count: 0,
185
- * increment: () => set((s) => ({ count: s.count + 1 })),
186
- * }),
187
- * { name: 'counterStore' }
188
- * )
189
- * );
190
- * ```
191
- */
192
- /**
193
- * Zustand's own `set` shape (v5 types): with `replace: true` the argument must
194
- * be a FULL state, so a single `(partial, replace?: boolean)` signature is
195
- * incompatible in both directions — zustand's set is not assignable to it,
196
- * which made `create(buoyDevTools(...))` fail to typecheck for every strict-TS
197
- * user. Mirroring the overloads keeps buoyDevTools a drop-in for `create()`.
198
- */
199
-
200
- /**
201
- * The store api object zustand hands a middleware, described structurally so
202
- * this package never has to import zustand's own types (which differ between
203
- * v4 and v5). `Record<string, unknown>` was wrong here: `StoreApi<T>` has no
204
- * index signature, so `create(buoyDevTools(...))` failed to typecheck.
205
- */
206
-
207
- function buoyDevTools(config, options) {
208
- return (set, get, api) => {
209
- const storeName = options?.name ?? `store-${++unnamedCounter}`;
210
- const enabled = options?.enabled !== false;
211
-
212
- // Forwarding a plain `replace?: boolean` through the overloads needs one
213
- // widening cast; runtime behavior is unchanged.
214
- const rawSet = set;
215
- const instrumentedSet = (partial, replace) => {
216
- if (!enabled) {
217
- return rawSet(partial, replace);
218
- }
219
- const startTime = performance.now();
220
- const prevState = get();
221
- rawSet(partial, replace);
222
- const nextState = get();
223
- const duration = performance.now() - startTime;
224
- const resolvedPartial = typeof partial === "function" ? "(updater fn)" : partial;
225
- _zustandStateStore.zustandStateStore.addStateChange({
226
- storeName,
227
- partial: resolvedPartial,
228
- replace: replace ?? false,
229
- prevState,
230
- nextState,
231
- duration,
232
- // Read back off the registry rather than hardcoded false. Persistence
233
- // is a property of the STORE, and this middleware can be wrapped
234
- // around a persisted one — every change on such a store used to be
235
- // drawn as un-persisted, which is the opposite of what the badge is
236
- // for. Registration happens a tick after creation, so a set fired in
237
- // that first tick still reports false; the alternative is re-sniffing
238
- // `api.persist` on every set.
239
- isPersisted: _zustandStateStore.zustandStateStore.getStore(storeName)?.isPersisted ?? false
240
- });
241
- };
242
- const result = config(instrumentedSet, get, api);
243
-
244
- // Register the store after creation
245
- setTimeout(() => {
246
- if (api[WATCHED_SYMBOL]) return;
247
- api[WATCHED_SYMBOL] = true;
248
- const {
249
- isPersisted,
250
- persistName
251
- } = detectPersist(api);
252
- _zustandStateStore.zustandStateStore.registerStore(storeName, {
253
- getState: get,
254
- getInitialState: api.getInitialState,
255
- // The INSTRUMENTED set, not the raw one.
256
- //
257
- // This handle is what the tool's own JUMP and RESET STORE call, and
258
- // registering the raw `set` here made both of them invisible: the
259
- // store changed, the app re-rendered, and nothing appeared in the
260
- // log — so the newest row's "state after change" no longer described
261
- // the store, and the next diff compared against a state the device
262
- // had never been in. The watch path never had this problem, because
263
- // it observes the store from outside and cannot be gone around.
264
- setState: instrumentedSet,
265
- subscribe: api.subscribe
266
- }, {
267
- isPersisted,
268
- persistName
269
- });
270
- }, 0);
271
- return result;
272
- };
273
- }
274
-
275
- // =============================================================================
276
- // Utility
277
- // =============================================================================
278
-
279
- /**
280
- * Check if a store is being watched/instrumented
281
- */
282
- function isStoreInstrumented(store) {
283
- return store[WATCHED_SYMBOL] === true;
284
- }
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.buoyDevTools=buoyDevTools;exports.isStoreInstrumented=isStoreInstrumented;exports.watchStores=watchStores;var _zustandStateStore=require("./zustandStateStore");const WATCHED_SYMBOL=Symbol.for("@@buoy-zustand/watched");function detectPersist(o){const e=o.persist;if(e&&typeof e.getOptions==="function"){const s=e.getOptions();return{isPersisted:true,persistName:s?.name,persistApi:e}}return{isPersisted:false}}function watchStores(o){const e=[];for(const[s,t]of Object.entries(o)){const r=t;if(r[WATCHED_SYMBOL])continue;r[WATCHED_SYMBOL]=true;const{isPersisted:a,persistName:f,persistApi:u}=detectPersist(t);_zustandStateStore.zustandStateStore.registerStore(s,{getState:t.getState,getInitialState:t.getInitialState,setState:t.setState,subscribe:t.subscribe},{isPersisted:a,persistName:f,rehydrate:typeof u?.rehydrate==="function"?()=>u.rehydrate():void 0});let c=false;const l=t.subscribe((n,i)=>{try{let d;if(!c&&a){let S=false;try{S=u?.hasHydrated?.()===false}catch{}if(S){c=true;d="persist"}}_zustandStateStore.zustandStateStore.addStateChange({storeName:s,partial:void 0,replace:false,prevState:i,nextState:n,duration:void 0,category:d,isPersisted:a})}catch{}});e.push(()=>{l();delete t[WATCHED_SYMBOL];_zustandStateStore.zustandStateStore.unregisterStore(s)})}return()=>{e.forEach(s=>s())}}let unnamedCounter=0;function buoyDevTools(o,e){return(s,t,r)=>{const a=e?.name??`store-${++unnamedCounter}`;const f=e?.enabled!==false;const u=s;const c=(n,i)=>{if(!f){return u(n,i)}const d=performance.now();const S=t();u(n,i);const b=t();const p=performance.now()-d;const y=typeof n==="function"?"(updater fn)":n;_zustandStateStore.zustandStateStore.addStateChange({storeName:a,partial:y,replace:i??false,prevState:S,nextState:b,duration:p,isPersisted:_zustandStateStore.zustandStateStore.getStore(a)?.isPersisted??false})};const l=o(c,t,r);setTimeout(()=>{if(r[WATCHED_SYMBOL])return;r[WATCHED_SYMBOL]=true;const{isPersisted:n,persistName:i}=detectPersist(r);_zustandStateStore.zustandStateStore.registerStore(a,{getState:t,getInitialState:r.getInitialState,setState:c,subscribe:r.subscribe},{isPersisted:n,persistName:i})},0);return l}}function isStoreInstrumented(o){return o[WATCHED_SYMBOL]===true}
@@ -1,146 +1 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.STRIP_MAX_DEPTH = void 0;
7
- exports.computeMergePartial = computeMergePartial;
8
- exports.editableStateOf = editableStateOf;
9
- exports.isPlainObjectLike = isPlainObjectLike;
10
- exports.stripFunctions = stripFunctions;
11
- /**
12
- * What of a zustand store's state can be EDITED, and how an edit is written.
13
- *
14
- * The two zustand facts every rule here follows from:
15
- *
16
- * 1. Stores conventionally keep their action functions IN state
17
- * (`increment: () => …`). Functions can't be typed into a tree editor and
18
- * don't survive serialization, so the editor works on a function-free view
19
- * of state.
20
- * 2. `setState` is either a TOP-LEVEL SHALLOW MERGE (`Object.assign({}, state,
21
- * partial)`) or a full replace that deletes every key not present — actions
22
- * included. So the editor writes merge-only, always: every edit, however
23
- * deep, is expressed as "replace the top-level key(s) that contain it"
24
- * ({@link computeMergePartial}), and the two edits a merge cannot express —
25
- * removing or renaming a top-level key — are refused rather than silently
26
- * not happening.
27
- *
28
- * `stripFunctions` lives here (the Time Machine snapshot provider imports it)
29
- * because capture and editing need the identical answer to "what is this
30
- * store's data".
31
- */
32
-
33
- /** Bounds every recursive walk over store state. */
34
- const STRIP_MAX_DEPTH = exports.STRIP_MAX_DEPTH = 8;
35
- function isPlainObjectLike(v) {
36
- // Only objects whose own enumerable keys ARE their data. A Map, Set, Date,
37
- // RegExp or typed array has none, so walking it with Object.entries turns
38
- // it into `{}` — which is how a restored store once got `{}` where the app
39
- // kept a Map and crashed on `.values()`. Those go through as-is; the vault's
40
- // superjson knows how to carry them.
41
- if (typeof v !== "object" || v === null || Array.isArray(v)) return false;
42
- const proto = Object.getPrototypeOf(v);
43
- return proto === Object.prototype || proto === null;
44
- }
45
-
46
- /** Clone `value` without functions, recording their paths. */
47
- function stripFunctions(value, path, fnPaths, seen, depth) {
48
- if (typeof value === "function") {
49
- fnPaths.push(path);
50
- return undefined;
51
- }
52
- if (typeof value !== "object" || value === null || depth >= STRIP_MAX_DEPTH) {
53
- return value;
54
- }
55
- if (seen.has(value)) return undefined; // cycle — drop the back-reference
56
- seen.add(value);
57
- if (Array.isArray(value)) {
58
- return value.map((item, i) => typeof item === "function" ? (fnPaths.push(`${path}[${i}]`), null) : stripFunctions(item, `${path}[${i}]`, fnPaths, seen, depth + 1));
59
- }
60
- if (!isPlainObjectLike(value)) return value; // Date/Map/Set/etc. — superjson handles
61
- const out = {};
62
- for (const [k, v] of Object.entries(value)) {
63
- if (typeof v === "function") {
64
- fnPaths.push(path ? `${path}.${k}` : k);
65
- continue;
66
- }
67
- out[k] = stripFunctions(v, path ? `${path}.${k}` : k, fnPaths, seen, depth + 1);
68
- }
69
- return out;
70
- }
71
- /**
72
- * The editor's view of one store's state, with the reasons it may refuse.
73
- *
74
- * - A remote mirror that hasn't hydrated yet holds a marker, not state — the
75
- * real value is one `getStoreState` round trip away, so "wait" rather than
76
- * "can't".
77
- * - Non-object state (`create(() => 5)`) has no top-level keys, so a merge
78
- * write cannot address any part of it. Editing it would require a replace,
79
- * which is the destructive path this editor exists to avoid.
80
- * - A function nested BELOW the top level (rare — the convention keeps actions
81
- * at the top) sits inside the very key an edit would rewrite, and a merge
82
- * write of that key would silently drop it. Refusing the whole store is
83
- * blunt but honest; per-key locks can come later if this ever bites.
84
- */
85
- function editableStateOf(state) {
86
- if (isPlainObjectLike(state) && state.__buoyStateOnDevice === true) {
87
- return {
88
- data: null,
89
- fnPaths: [],
90
- blockedReason: "Store state hasn't synced from the device yet."
91
- };
92
- }
93
- if (!isPlainObjectLike(state)) {
94
- return {
95
- data: null,
96
- fnPaths: [],
97
- blockedReason: "Only stores whose state is a plain object can be edited — " + "a merge write has nothing to address in " + (Array.isArray(state) ? "an array" : `a ${typeof state}`) + "."
98
- };
99
- }
100
- const fnPaths = [];
101
- const data = stripFunctions(state, "", fnPaths, new WeakSet(), 0);
102
- const nested = fnPaths.filter(p => p.includes(".") || p.includes("["));
103
- if (nested.length > 0) {
104
- return {
105
- data: null,
106
- fnPaths,
107
- blockedReason: `State keeps a function inside "${nested[0]}" — a merge write of ` + "that key would drop it, so this store can't be edited here."
108
- };
109
- }
110
- return {
111
- data,
112
- fnPaths,
113
- blockedReason: null
114
- };
115
- }
116
-
117
- /**
118
- * Diff an edited draft against the state it was seeded from, as a `setState`
119
- * MERGE partial: only the top-level keys whose contents changed.
120
- *
121
- * Diffed against the SEED, not the live state at save time, on purpose — the
122
- * partial then carries exactly what the user changed, so a key the app wrote
123
- * while the editor was open and the user never touched keeps the app's value
124
- * (per-key last-write-wins, the same contract the draft's own dirtiness uses).
125
- *
126
- * Throws on the two edits a merge cannot express, naming the key: a draft
127
- * that dropped a top-level key (which is also what a raw-JSON rename produces
128
- * — the old key dropped, a new one added), because `setState(partial, false)`
129
- * would leave it exactly as it was while the editor showed it gone.
130
- */
131
- function computeMergePartial(seed, draft) {
132
- if (!isPlainObjectLike(draft)) {
133
- throw new Error("The store's state must stay an object.");
134
- }
135
- const dropped = Object.keys(seed).filter(k => !(k in draft));
136
- if (dropped.length > 0) {
137
- throw new Error(`Can't remove top-level ${dropped.length === 1 ? "key" : "keys"} ` + `${dropped.map(k => `"${k}"`).join(", ")} — zustand merge writes ` + "can't delete. Edit the value instead.");
138
- }
139
- const partial = {};
140
- for (const key of Object.keys(draft)) {
141
- if (!(key in seed) || JSON.stringify(draft[key]) !== JSON.stringify(seed[key])) {
142
- partial[key] = draft[key];
143
- }
144
- }
145
- return partial;
146
- }
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.STRIP_MAX_DEPTH=void 0;exports.computeMergePartial=computeMergePartial;exports.editableStateOf=editableStateOf;exports.isPlainObjectLike=isPlainObjectLike;exports.stripFunctions=stripFunctions;const STRIP_MAX_DEPTH=exports.STRIP_MAX_DEPTH=8;function isPlainObjectLike(e){if(typeof e!=="object"||e===null||Array.isArray(e))return false;const t=Object.getPrototypeOf(e);return t===Object.prototype||t===null}function stripFunctions(e,t,r,o,n){if(typeof e==="function"){r.push(t);return void 0}if(typeof e!=="object"||e===null||n>=STRIP_MAX_DEPTH){return e}if(o.has(e))return void 0;o.add(e);if(Array.isArray(e)){return e.map((i,s)=>typeof i==="function"?(r.push(`${t}[${s}]`),null):stripFunctions(i,`${t}[${s}]`,r,o,n+1))}if(!isPlainObjectLike(e))return e;const c={};for(const[i,s]of Object.entries(e)){if(typeof s==="function"){r.push(t?`${t}.${i}`:i);continue}c[i]=stripFunctions(s,t?`${t}.${i}`:i,r,o,n+1)}return c}function editableStateOf(e){if(isPlainObjectLike(e)&&e.__buoyStateOnDevice===true){return{data:null,fnPaths:[],blockedReason:"Store state hasn't synced from the device yet."}}if(!isPlainObjectLike(e)){return{data:null,fnPaths:[],blockedReason:"Only stores whose state is a plain object can be edited \u2014 a merge write has nothing to address in "+(Array.isArray(e)?"an array":`a ${typeof e}`)+"."}}const t=[];const r=stripFunctions(e,"",t,new WeakSet,0);const o=t.filter(n=>n.includes(".")||n.includes("["));if(o.length>0){return{data:null,fnPaths:t,blockedReason:`State keeps a function inside "${o[0]}" \u2014 a merge write of that key would drop it, so this store can't be edited here.`}}return{data:r,fnPaths:t,blockedReason:null}}function computeMergePartial(e,t){if(!isPlainObjectLike(t)){throw new Error("The store's state must stay an object.")}const r=Object.keys(e).filter(n=>!(n in t));if(r.length>0){throw new Error(`Can't remove top-level ${r.length===1?"key":"keys"} ${r.map(n=>`"${n}"`).join(", ")} \u2014 zustand merge writes can't delete. Edit the value instead.`)}const o={};for(const n of Object.keys(t)){if(!(n in e)||JSON.stringify(t[n])!==JSON.stringify(e[n])){o[n]=t[n]}}return o}
@@ -1,209 +1 @@
1
- "use strict";
2
-
3
- var _sharedUi = require("@buoy-gg/shared-ui");
4
- var _zustandStateStore = require("./zustandStateStore");
5
- var _editableState = require("./editableState");
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
- // The function-stripping walk is shared with the store editor (editableState
21
- // .ts) — capture and editing must agree on what a store's "data" is.
22
- const MAX_DEPTH = _editableState.STRIP_MAX_DEPTH;
23
- /**
24
- * Copy every function-valued property from the LIVE state onto the restored
25
- * snapshot (creating containers as needed), so store actions keep working
26
- * after a full replace.
27
- */
28
- function graftFunctions(target, live, seen, depth) {
29
- if (!(0, _editableState.isPlainObjectLike)(live) || !(0, _editableState.isPlainObjectLike)(target)) return;
30
- if (depth >= MAX_DEPTH || seen.has(live)) return;
31
- seen.add(live);
32
- for (const [k, v] of Object.entries(live)) {
33
- if (typeof v === "function") {
34
- target[k] = v;
35
- } else if ((0, _editableState.isPlainObjectLike)(v)) {
36
- if (!(0, _editableState.isPlainObjectLike)(target[k])) {
37
- // Branch missing from the snapshot — only recreate it if it holds
38
- // functions somewhere; otherwise the snapshot's absence wins.
39
- if (!containsFunction(v, new WeakSet(), depth + 1)) continue;
40
- target[k] = {};
41
- }
42
- graftFunctions(target[k], v, seen, depth + 1);
43
- }
44
- }
45
- }
46
- function containsFunction(value, seen, depth) {
47
- if (depth >= MAX_DEPTH || seen.has(value)) return false;
48
- seen.add(value);
49
- for (const v of Object.values(value)) {
50
- if (typeof v === "function") return true;
51
- if ((0, _editableState.isPlainObjectLike)(v) && containsFunction(v, seen, depth + 1)) return true;
52
- }
53
- return false;
54
- }
55
- function status() {
56
- const count = _zustandStateStore.zustandStateStore.getStores().length;
57
- if (count === 0) {
58
- return {
59
- canCapture: false,
60
- canRestore: false,
61
- detail: "No Zustand stores registered — pass `zustandStores` to FloatingDevTools or add the buoyDevTools middleware"
62
- };
63
- }
64
- return {
65
- canCapture: true,
66
- canRestore: true
67
- };
68
- }
69
- async function capture() {
70
- const warnings = [];
71
- const stores = {};
72
- for (const info of _zustandStateStore.zustandStateStore.getStores()) {
73
- const fnPaths = [];
74
- let state;
75
- try {
76
- state = (0, _editableState.stripFunctions)(info.api.getState(), "", fnPaths, new WeakSet(), 0);
77
- } catch (e) {
78
- warnings.push(`store "${info.name}": capture failed (${String(e)})`);
79
- continue;
80
- }
81
- stores[info.name] = {
82
- state,
83
- fnPaths
84
- };
85
- }
86
- return {
87
- data: {
88
- stores
89
- },
90
- warnings
91
- };
92
- }
93
- async function restore(data, options) {
94
- const snapshot = data;
95
- const scope = (0, _sharedUi.createRestoreScope)(options);
96
- const result = (0, _sharedUi.emptyRestoreResult)();
97
- if (!snapshot?.stores) {
98
- return (0, _sharedUi.failedRestore)("empty zustand snapshot");
99
- }
100
- for (const [name, envelope] of Object.entries(snapshot.stores)) {
101
- if (!scope.allows(name)) continue;
102
- const info = _zustandStateStore.zustandStateStore.getStore(name);
103
- if (!info) {
104
- result.skipped.push({
105
- id: name,
106
- reason: "store not registered (renamed, or created after snapshot?)"
107
- });
108
- continue;
109
- }
110
- try {
111
- const next = envelope.state;
112
- graftFunctions(next, info.api.getState(), new WeakSet(), 0);
113
- info.api.setState(next, true);
114
- if (info.api.getState() === next) {
115
- result.applied += 1;
116
- } else {
117
- result.warnings.push(`store "${name}": setState did not take effect`);
118
- result.ok = false;
119
- }
120
- } catch (e) {
121
- result.skipped.push({
122
- id: name,
123
- reason: String(e)
124
- });
125
- result.ok = false;
126
- }
127
- }
128
- const excludedWarning = scope.excludedWarning("store(s)");
129
- if (excludedWarning) result.warnings.push(excludedWarning);
130
- return result;
131
- }
132
-
133
- /**
134
- * Per-store restore preview. Both sides are function-stripped through the
135
- * SAME depth-8 normalizer as capture — diffing snapshot-vs-RAW-live would
136
- * report every stripped function and every beyond-depth subtree as removed.
137
- */
138
- async function previewItems(target, base) {
139
- const targetStores = target?.stores ?? {};
140
- const liveMode = base === undefined;
141
- const baseStores = {};
142
- if (liveMode) {
143
- for (const info of _zustandStateStore.zustandStateStore.getStores()) {
144
- const fnPaths = [];
145
- try {
146
- baseStores[info.name] = {
147
- state: (0, _editableState.stripFunctions)(info.api.getState(), "", fnPaths, new WeakSet(), 0),
148
- fnPaths
149
- };
150
- } catch {
151
- // unreadable live store — treated as absent
152
- }
153
- }
154
- } else {
155
- Object.assign(baseStores, base?.stores ?? {});
156
- }
157
- const items = [];
158
- const notes = [];
159
- let preservedFns = 0;
160
- for (const name of (0, _sharedUi.unionKeys)(baseStores, targetStores)) {
161
- const inTarget = name in targetStores;
162
- const inBase = name in baseStores;
163
- if (!inTarget) {
164
- // Restore never touches stores the snapshot doesn't know about.
165
- if (liveMode) notes.push(`store "${name}" not in snapshot — untouched`);else items.push({
166
- key: name,
167
- label: name,
168
- verdict: "removed",
169
- oldValue: baseStores[name].state
170
- });
171
- continue;
172
- }
173
- preservedFns += targetStores[name].fnPaths?.length ?? 0;
174
- if (liveMode && !_zustandStateStore.zustandStateStore.getStore(name)) {
175
- items.push({
176
- key: name,
177
- label: name,
178
- verdict: "wont-apply",
179
- reason: "store not registered (renamed, or created after snapshot?)"
180
- });
181
- continue;
182
- }
183
- items.push((0, _sharedUi.classifyPreviewItem)({
184
- key: name,
185
- label: name,
186
- oldValue: baseStores[name]?.state,
187
- newValue: targetStores[name].state,
188
- inOld: inBase,
189
- inNew: true
190
- }, {
191
- tooLargeReason: "store too large to render a diff — restore still applies it"
192
- }));
193
- }
194
- if (preservedFns > 0) {
195
- notes.push(`${preservedFns} store action function${preservedFns === 1 ? "" : "s"} preserved on restore (not diffed)`);
196
- }
197
- return {
198
- items,
199
- notes
200
- };
201
- }
202
- (0, _sharedUi.registerStateSnapshotProvider)({
203
- id: "zustand",
204
- label: "Zustand",
205
- status,
206
- capture,
207
- restore,
208
- previewItems
209
- });
1
+ "use strict";var _sharedUi=require("@buoy-gg/shared-ui");var _zustandStateStore=require("./zustandStateStore");var _editableState=require("./editableState");const MAX_DEPTH=_editableState.STRIP_MAX_DEPTH;function graftFunctions(s,r,n,o){if(!(0,_editableState.isPlainObjectLike)(r)||!(0,_editableState.isPlainObjectLike)(s))return;if(o>=MAX_DEPTH||n.has(r))return;n.add(r);for(const[e,a]of Object.entries(r)){if(typeof a==="function"){s[e]=a}else if((0,_editableState.isPlainObjectLike)(a)){if(!(0,_editableState.isPlainObjectLike)(s[e])){if(!containsFunction(a,new WeakSet,o+1))continue;s[e]={}}graftFunctions(s[e],a,n,o+1)}}}function containsFunction(s,r,n){if(n>=MAX_DEPTH||r.has(s))return false;r.add(s);for(const o of Object.values(s)){if(typeof o==="function")return true;if((0,_editableState.isPlainObjectLike)(o)&&containsFunction(o,r,n+1))return true}return false}function status(){const s=_zustandStateStore.zustandStateStore.getStores().length;if(s===0){return{canCapture:false,canRestore:false,detail:"No Zustand stores registered \u2014 pass `zustandStores` to FloatingDevTools or add the buoyDevTools middleware"}}return{canCapture:true,canRestore:true}}async function capture(){const s=[];const r={};for(const n of _zustandStateStore.zustandStateStore.getStores()){const o=[];let e;try{e=(0,_editableState.stripFunctions)(n.api.getState(),"",o,new WeakSet,0)}catch(a){s.push(`store "${n.name}": capture failed (${String(a)})`);continue}r[n.name]={state:e,fnPaths:o}}return{data:{stores:r},warnings:s}}async function restore(s,r){const n=s;const o=(0,_sharedUi.createRestoreScope)(r);const e=(0,_sharedUi.emptyRestoreResult)();if(!n?.stores){return(0,_sharedUi.failedRestore)("empty zustand snapshot")}for(const[c,u]of Object.entries(n.stores)){if(!o.allows(c))continue;const t=_zustandStateStore.zustandStateStore.getStore(c);if(!t){e.skipped.push({id:c,reason:"store not registered (renamed, or created after snapshot?)"});continue}try{const i=u.state;graftFunctions(i,t.api.getState(),new WeakSet,0);t.api.setState(i,true);if(t.api.getState()===i){e.applied+=1}else{e.warnings.push(`store "${c}": setState did not take effect`);e.ok=false}}catch(i){e.skipped.push({id:c,reason:String(i)});e.ok=false}}const a=o.excludedWarning("store(s)");if(a)e.warnings.push(a);return e}async function previewItems(s,r){const n=s?.stores??{};const o=r===void 0;const e={};if(o){for(const t of _zustandStateStore.zustandStateStore.getStores()){const i=[];try{e[t.name]={state:(0,_editableState.stripFunctions)(t.api.getState(),"",i,new WeakSet,0),fnPaths:i}}catch{}}}else{Object.assign(e,r?.stores??{})}const a=[];const c=[];let u=0;for(const t of(0,_sharedUi.unionKeys)(e,n)){const i=t in n;const f=t in e;if(!i){if(o)c.push(`store "${t}" not in snapshot \u2014 untouched`);else a.push({key:t,label:t,verdict:"removed",oldValue:e[t].state});continue}u+=n[t].fnPaths?.length??0;if(o&&!_zustandStateStore.zustandStateStore.getStore(t)){a.push({key:t,label:t,verdict:"wont-apply",reason:"store not registered (renamed, or created after snapshot?)"});continue}a.push((0,_sharedUi.classifyPreviewItem)({key:t,label:t,oldValue:e[t]?.state,newValue:n[t].state,inOld:f,inNew:true},{tooLargeReason:"store too large to render a diff \u2014 restore still applies it"}))}if(u>0){c.push(`${u} store action function${u===1?"":"s"} preserved on restore (not diffed)`)}return{items:a,notes:c}}(0,_sharedUi.registerStateSnapshotProvider)({id:"zustand",label:"Zustand",status,capture,restore,previewItems});