@buoy-gg/zustand 7.0.40 → 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 (67) hide show
  1. package/README.md +11 -72
  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 -382
  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 -380
  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 -0
  43. package/package.json +35 -12
  44. package/lib/commonjs/zustand/components/ZustandStoreEditorScreen.js +0 -393
  45. package/lib/module/zustand/components/ZustandStoreEditorScreen.js +0 -389
  46. package/lib/typescript/index.d.ts.map +0 -1
  47. package/lib/typescript/preset.d.ts.map +0 -1
  48. package/lib/typescript/zustand/components/ZustandActionButton.d.ts.map +0 -1
  49. package/lib/typescript/zustand/components/ZustandDetailViewToggle.d.ts.map +0 -1
  50. package/lib/typescript/zustand/components/ZustandEventFilterView.d.ts.map +0 -1
  51. package/lib/typescript/zustand/components/ZustandIcon.d.ts.map +0 -1
  52. package/lib/typescript/zustand/components/ZustandModal.d.ts.map +0 -1
  53. package/lib/typescript/zustand/components/ZustandStateChangeItem.d.ts.map +0 -1
  54. package/lib/typescript/zustand/components/ZustandStateDetailContent.d.ts.map +0 -1
  55. package/lib/typescript/zustand/components/ZustandStateInfoView.d.ts.map +0 -1
  56. package/lib/typescript/zustand/components/ZustandStoreBrowser.d.ts.map +0 -1
  57. package/lib/typescript/zustand/components/ZustandStoreDetailContent.d.ts.map +0 -1
  58. package/lib/typescript/zustand/components/ZustandStoreEditorScreen.d.ts +0 -42
  59. package/lib/typescript/zustand/components/ZustandStoreEditorScreen.d.ts.map +0 -1
  60. package/lib/typescript/zustand/hooks/useZustandStateChanges.d.ts.map +0 -1
  61. package/lib/typescript/zustand/sync/zustandSyncAdapter.d.ts.map +0 -1
  62. package/lib/typescript/zustand/types/index.d.ts.map +0 -1
  63. package/lib/typescript/zustand/utils/buoyZustandMiddleware.d.ts.map +0 -1
  64. package/lib/typescript/zustand/utils/editableState.d.ts.map +0 -1
  65. package/lib/typescript/zustand/utils/snapshotProvider.d.ts.map +0 -1
  66. package/lib/typescript/zustand/utils/testIDs.d.ts.map +0 -1
  67. package/lib/typescript/zustand/utils/zustandStateStore.d.ts.map +0 -1
@@ -1,382 +1 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.zustandSyncAdapter = exports.STATE_ON_DEVICE = exports.PAYLOAD_ON_DEVICE = void 0;
7
- var _sharedUi = require("@buoy-gg/shared-ui");
8
- var _zustandStateStore = require("../utils/zustandStateStore");
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 = (0, _sharedUi.onDeviceMarker)("State", "State trees stay on the device — fetched on demand via getChangeDetail / getStoreState.");
15
-
16
- /** Wire-form sentinel for an oversized `partial` (replace can carry a whole tree). */
17
- const PAYLOAD_ON_DEVICE = exports.PAYLOAD_ON_DEVICE = (0, _sharedUi.onDeviceMarker)("Payload", "Partial stays on the device — fetched on demand via getChangeDetail.");
18
- const isPlainObject = v => !!v && typeof v === "object" && !Array.isArray(v);
19
- /**
20
- * Does this patch write into the list at `path`? Only then is an unchecked-shape
21
- * warning about that list worth saying — a patch that changes `promoCode` should
22
- * not be told about an empty `lines`.
23
- */
24
- function touchesList(patch, path) {
25
- let cur = patch;
26
- for (const seg of path.split(".")) {
27
- if (!cur || typeof cur !== "object" || Array.isArray(cur)) return false;
28
- if (!(seg in cur)) return false;
29
- cur = cur[seg];
30
- }
31
- // An empty list stays empty; there is no new item to be unsure about.
32
- return Array.isArray(cur) ? cur.length > 0 : !!cur && typeof cur === "object";
33
- }
34
-
35
- /** Top-level keys of an object state, for the compact (no-values) view. */
36
- function topLevelKeys(state) {
37
- return state && typeof state === "object" && !Array.isArray(state) ? Object.keys(state) : undefined;
38
- }
39
-
40
- /**
41
- * Per-change wire cache. The store prepends and never mutates a recorded
42
- * change, so keying on identity is correct and self-invalidating — without it
43
- * every `partial` in the list is re-measured on all ~5 snapshots per second.
44
- */
45
- const toWireChange = (0, _sharedUi.memoByIdentity)(change => ({
46
- ...change,
47
- prevState: STATE_ON_DEVICE,
48
- nextState: STATE_ON_DEVICE,
49
- partial: (0, _sharedUi.toWire)(change.partial, PAYLOAD_ON_DEVICE)
50
- }));
51
- const toWireState = value => (0, _sharedUi.toWire)(value, STATE_ON_DEVICE);
52
-
53
- /**
54
- * Sync adapter for the zustand tool, consumed by @buoy-gg/external-sync's
55
- * `useExternalSync`.
56
- *
57
- * State changes are captured by the middleware (watchStores/buoyDevTools),
58
- * independent of whether a dashboard is watching — subscribing here only
59
- * streams what the store already records. The snapshot carries both the
60
- * change timeline and the store registry (with each store's current state)
61
- * so the dashboard can render the store browser.
62
- *
63
- * v2: change-log prevState/nextState are replaced with on-device markers
64
- * (see STATE_ON_DEVICE). Dashboards fetch trees via `getChangeDetail`.
65
- * Oversized / unserializable `stores[].currentState` uses the same marker
66
- * and is fetched via `getStoreState`.
67
- *
68
- * The `setState` action powers remote time-travel/reset from the dashboard.
69
- */
70
- const zustandSyncAdapter = exports.zustandSyncAdapter = (0, _sharedUi.createSyncAdapter)({
71
- version: 2,
72
- getSnapshot: () => ({
73
- changes: _zustandStateStore.zustandStateStore.getStateChanges().map(toWireChange),
74
- stores: _zustandStateStore.zustandStateStore.getStoreSnapshots().map(store => ({
75
- ...store,
76
- currentState: toWireState(store.currentState)
77
- }))
78
- }),
79
- subscribe: [onChange => _zustandStateStore.zustandStateStore.subscribe(onChange), onChange => _zustandStateStore.zustandStateStore.subscribeToStores(onChange)],
80
- actions: {
81
- clearEvents: () => {
82
- _zustandStateStore.zustandStateStore.clearStateChanges();
83
- },
84
- /**
85
- * On-demand detail: one change's real prevState/nextState/partial.
86
- * The per-snapshot stream stays tree-free; this is the explicit,
87
- * size-guarded channel for the detail pane.
88
- */
89
- getChangeDetail: (0, _sharedUi.detailByIdAction)(id => _zustandStateStore.zustandStateStore.getStateChangeById(id), (change, cap) => ({
90
- prevState: cap(change.prevState),
91
- nextState: cap(change.nextState),
92
- partial: cap(change.partial)
93
- })),
94
- /**
95
- * On-demand current state for one store. Wire-form snapshots replace
96
- * oversized / unserializable `currentState` with STATE_ON_DEVICE.
97
- */
98
- getStoreState: params => {
99
- const {
100
- storeName,
101
- path
102
- } = (0, _sharedUi.readParams)(params);
103
- if (!storeName) return {
104
- found: false,
105
- reason: "missing storeName"
106
- };
107
- const store = _zustandStateStore.zustandStateStore.getStore(storeName);
108
- if (!store) return {
109
- found: false,
110
- reason: "unknown storeName"
111
- };
112
- let currentState;
113
- try {
114
- currentState = store.api.getState();
115
- } catch {
116
- currentState = undefined;
117
- }
118
- // One slice instead of the whole store. The reason to want it is the
119
- // engine's 24,000-character result cap: a caller that never saw the field
120
- // it means to edit is left inventing a shape, which is the failure this
121
- // whole surface keeps producing.
122
- if (path) {
123
- const at = (0, _sharedUi.readPath)(currentState, path);
124
- if (!at.ok) return {
125
- found: false,
126
- reason: at.error
127
- };
128
- return {
129
- found: true,
130
- storeName,
131
- path,
132
- shape: (0, _sharedUi.shapeSummary)(at.value),
133
- currentState: at.value
134
- };
135
- }
136
- // The sketch goes BEFORE currentState, deliberately. The engine truncates
137
- // a result by slicing the encoded string at 24,000 characters, so whatever
138
- // is serialised first is what survives — and the reads most in need of a
139
- // shape are exactly the long ones that get cut. It costs ~150 characters
140
- // and does not grow with the data.
141
- const shape = (0, _sharedUi.shapeSummary)(currentState);
142
- if (!(0, _sharedUi.isOverWireBudget)(currentState, _sharedUi.WIRE_DETAIL_LIMIT_BYTES)) {
143
- return {
144
- found: true,
145
- storeName,
146
- ...(shape ? {
147
- shape
148
- } : {}),
149
- currentState
150
- };
151
- }
152
- return {
153
- found: true,
154
- storeName,
155
- ...(shape ? {
156
- shape
157
- } : {}),
158
- currentState: (0, _sharedUi.isJsonSerializable)(currentState) ? (0, _sharedUi.truncatedNote)(_sharedUi.WIRE_DETAIL_LIMIT_BYTES) : STATE_ON_DEVICE
159
- };
160
- },
161
- /**
162
- * Compact current-state reader for a remote driver (MCP/LLM): the registered
163
- * stores with light metadata; each store's full `currentState` object stays
164
- * on the device unless `includeValues` is set (compact view returns just the
165
- * top-level `keys` so the shape is visible cheaply).
166
- */
167
- listStores: params => {
168
- const {
169
- includeValues = false,
170
- limit
171
- } = (0, _sharedUi.readParams)(params);
172
- const all = _zustandStateStore.zustandStateStore.getStoreSnapshots();
173
- const capped = typeof limit === "number" && limit > 0 ? all.slice(0, limit) : all;
174
- const stores = capped.map(s => ({
175
- name: s.name,
176
- changes: s.stateChangeCount,
177
- isPersisted: s.isPersisted,
178
- persistName: s.persistName,
179
- canRehydrate: s.canRehydrate,
180
- ...(includeValues ? {
181
- currentState: s.currentState
182
- } : {
183
- keys: topLevelKeys(s.currentState)
184
- }),
185
- // Cheap enough to always include: this is the compact view a caller
186
- // reaches for precisely because it does not want to pull the data, and
187
- // the item shape of a list is the one thing it cannot infer without it.
188
- ...((0, _sharedUi.shapeSummary)(s.currentState) ? {
189
- shape: (0, _sharedUi.shapeSummary)(s.currentState)
190
- } : {})
191
- }));
192
- return {
193
- stores,
194
- total: all.length,
195
- returned: stores.length,
196
- includedValues: includeValues
197
- };
198
- },
199
- setState: params => {
200
- const {
201
- storeName,
202
- state,
203
- replace,
204
- force,
205
- path,
206
- value
207
- } = (0, _sharedUi.readParams)(params);
208
- let effective = state;
209
- let keyedNotes = [];
210
- let unchecked = [];
211
- if (!storeName) return {
212
- ok: false,
213
- error: "Missing storeName."
214
- };
215
- const store = _zustandStateStore.zustandStateStore.getStore(storeName);
216
- if (!store) return {
217
- ok: false,
218
- error: `No store named "${storeName}".`
219
- };
220
- // `path` is sugar over a merge: it can only ever produce a patch, which
221
- // then goes through the guard below like any other. Its value is that
222
- // there is no nesting depth for the caller to get wrong.
223
- // `state` is no longer schema-required, because `path`+`value` is a valid
224
- // alternative — so the handler has to be the thing that rejects NEITHER.
225
- // Without this, `state` arrives undefined and falls through to
226
- // `setState(undefined, true)`, which is zustand's whole-state replace: the
227
- // store's data AND its action functions, gone, from a call that looks like
228
- // a typo. This is the most destructive thing in this file.
229
- if (state === undefined && path === undefined) {
230
- return {
231
- ok: false,
232
- error: `Refused: no change was described. Send \`state\` with the fields to merge, or \`path\` and \`value\` to set one value.`
233
- };
234
- }
235
- let pathed = false;
236
- if (path !== undefined) {
237
- if (value === undefined) {
238
- return {
239
- ok: false,
240
- error: `Refused: sent \`path\` without \`value\`. Say what to set "${path}" to; if you mean to clear it, send value:null and the field must already be nullable.`
241
- };
242
- }
243
- let currentForPath;
244
- try {
245
- currentForPath = store.api.getState();
246
- } catch {
247
- currentForPath = undefined;
248
- }
249
- const built = (0, _sharedUi.patchFromPath)(currentForPath, path, value);
250
- if (!built.ok) return {
251
- ok: false,
252
- error: `Refused: ${built.error}`
253
- };
254
- effective = built.patch;
255
- pathed = true;
256
- }
257
- // On a MERGE (replace:false), constrain it to a typed edit so a remote
258
- // caller can't hand the store a field of the wrong type or a malformed
259
- // new list item — the same protection the query cache has. A replace
260
- // (whole-store reset) and an explicit `force` skip this.
261
- if (!force && (replace === false || pathed)) {
262
- let current;
263
- try {
264
- current = store.api.getState();
265
- } catch {
266
- current = undefined;
267
- }
268
- // Expand id-addressed list edits BEFORE validating, so the typed-edit
269
- // guard below sees a real array and every existing protection still
270
- // applies to the result.
271
- if (current !== undefined) {
272
- const expanded = (0, _sharedUi.expandKeyedArrays)(current, effective);
273
- // See keyedArrays.ts: an added item whose id disagrees with the key
274
- // it was sent under is ambiguous, and only this layer can say so.
275
- if (expanded.error) {
276
- return {
277
- ok: false,
278
- error: `Refused: ${expanded.error}`
279
- };
280
- }
281
- effective = expanded.patch;
282
- keyedNotes = expanded.notes;
283
- }
284
- // The guard builds its template from `list[0]`, so a list that is EMPTY
285
- // has no template and an added item is written with nothing to check it
286
- // against. That is not a rare corner: it is every "add the first thing"
287
- // flow, and it is where models invent shapes, because on an empty list
288
- // the item shape genuinely exists nowhere in the running app. We cannot
289
- // refuse it — refusing would break adding the first item to anything —
290
- // so we say so instead of letting the guess through unremarked.
291
- if (current !== undefined) {
292
- unchecked = (0, _sharedUi.unshapedLists)(current).filter(path => touchesList(effective, path));
293
- }
294
- if (current !== undefined) {
295
- const violations = (0, _sharedUi.typedEditViolations)(current, effective, {
296
- noun: "store"
297
- });
298
- if (violations.length > 0) {
299
- return {
300
- ok: false,
301
- error: (0, _sharedUi.refusalText)(violations, "store", (0, _sharedUi.suggestedPatch)(effective, violations)),
302
- ...((0, _sharedUi.suggestedPatch)(effective, violations) ? {
303
- suggestedState: (0, _sharedUi.suggestedPatch)(effective, violations)
304
- } : {}),
305
- violations
306
- };
307
- }
308
- }
309
- }
310
- // A pathed write is a merge by construction — it names one value, so it
311
- // must never fall through to zustand's default whole-state replace.
312
- store.api.setState(effective, pathed ? false : replace ?? true);
313
- // Say what an addressed edit resolved to. A caller that meant to change one
314
- // line and sees "17 item(s) remain" has caught its own mistake.
315
- return {
316
- ok: true,
317
- ...(path !== undefined ? {
318
- path
319
- } : {}),
320
- ...(keyedNotes.length ? {
321
- keyed: keyedNotes
322
- } : {}),
323
- ...(unchecked.length ? {
324
- unchecked: unchecked.map(path => `\`${path}\` was empty, so nothing in the app knows what its items look like and the shape of what you added was NOT checked. If the app renders a field you left out, it will render undefined — read the store back and compare against how the screen uses it.`)
325
- } : {})
326
- };
327
- },
328
- /**
329
- * Re-read a persisted store's saved value and merge it into the live store.
330
- *
331
- * This is what makes editing a store's storage key mean anything. The app
332
- * reads that key once at startup and then holds the state in memory, so an
333
- * outside write — the Storage tool, a scenario, an agent — changes the disk
334
- * and nothing else, and the next time the store saves it writes its own
335
- * copy straight back over the edit.
336
- *
337
- * `persist.rehydrate()` is plain zustand, not a devtools hook, so unlike
338
- * reloading the app this also works in a release build. It merges the saved
339
- * value OVER current state (zustand's default merge is
340
- * `{...current, ...persisted}`), so the store's action functions survive,
341
- * and it does not write back to storage unless a version migration ran —
342
- * the edit stays exactly as it was typed.
343
- *
344
- * Two honest limits, both inherent to persist rather than to this action:
345
- * a key the store's `partialize` excludes is not in the saved value and so
346
- * cannot be applied, and because the merge is shallow, DELETING a top-level
347
- * key from the saved JSON does not remove it from the live store.
348
- */
349
- rehydrate: async params => {
350
- const {
351
- storeName
352
- } = (0, _sharedUi.readParams)(params);
353
- if (!storeName) return {
354
- ok: false,
355
- error: "Missing storeName."
356
- };
357
- const store = _zustandStateStore.zustandStateStore.getStore(storeName);
358
- if (!store) return {
359
- ok: false,
360
- error: `No store named "${storeName}".`
361
- };
362
- if (!store.isPersisted) {
363
- return {
364
- ok: false,
365
- error: `"${storeName}" is not a persisted store — it has no saved copy to re-read.`
366
- };
367
- }
368
- if (!store.rehydrate) {
369
- return {
370
- ok: false,
371
- error: `"${storeName}" is persisted but Buoy has no rehydrate handle for it. It was probably registered before the app's persist middleware attached.`
372
- };
373
- }
374
- await store.rehydrate();
375
- return {
376
- ok: true,
377
- storeName,
378
- persistName: store.persistName
379
- };
380
- }
381
- }
382
- });
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.zustandSyncAdapter=exports.STATE_ON_DEVICE=exports.PAYLOAD_ON_DEVICE=void 0;var _sharedUi=require("@buoy-gg/shared-ui");var _zustandStateStore=require("../utils/zustandStateStore");const isPlainRecord=e=>typeof e==="object"&&e!==null&&!Array.isArray(e)&&Object.getPrototypeOf(e)===Object.prototype;function deepMergeInto(e,r){if(!isPlainRecord(e)||!isPlainRecord(r))return r;const t={...e};for(const[s,i]of Object.entries(r))t[s]=s in e?deepMergeInto(e[s],i):i;return t}const STATE_ON_DEVICE=exports.STATE_ON_DEVICE=(0,_sharedUi.onDeviceMarker)("State","State trees stay on the device \u2014 fetched on demand via getChangeDetail / getStoreState.");const PAYLOAD_ON_DEVICE=exports.PAYLOAD_ON_DEVICE=(0,_sharedUi.onDeviceMarker)("Payload","Partial stays on the device \u2014 fetched on demand via getChangeDetail.");const isPlainObject=e=>!!e&&typeof e==="object"&&!Array.isArray(e);function touchesList(e,r){let t=e;for(const s of r.split(".")){if(!t||typeof t!=="object"||Array.isArray(t))return false;if(!(s in t))return false;t=t[s]}return Array.isArray(t)?t.length>0:!!t&&typeof t==="object"}function topLevelKeys(e){return e&&typeof e==="object"&&!Array.isArray(e)?Object.keys(e):void 0}const toWireChange=(0,_sharedUi.memoByIdentity)(e=>({...e,prevState:STATE_ON_DEVICE,nextState:STATE_ON_DEVICE,partial:(0,_sharedUi.toWire)(e.partial,PAYLOAD_ON_DEVICE)}));const toWireState=e=>(0,_sharedUi.toWire)(e,STATE_ON_DEVICE);const zustandSyncAdapter=exports.zustandSyncAdapter=(0,_sharedUi.createSyncAdapter)({version:2,getSnapshot:()=>({changes:_zustandStateStore.zustandStateStore.getStateChanges().map(toWireChange),stores:_zustandStateStore.zustandStateStore.getStoreSnapshots().map(e=>({...e,currentState:toWireState(e.currentState)}))}),subscribe:[e=>_zustandStateStore.zustandStateStore.subscribe(e),e=>_zustandStateStore.zustandStateStore.subscribeToStores(e)],actions:{clearEvents:()=>{_zustandStateStore.zustandStateStore.clearStateChanges()},getChangeDetail:(0,_sharedUi.detailByIdAction)(e=>_zustandStateStore.zustandStateStore.getStateChangeById(e),(e,r)=>({prevState:r(e.prevState),nextState:r(e.nextState),partial:r(e.partial)})),getStoreState:e=>{const{storeName:r,path:t}=(0,_sharedUi.readParams)(e);if(!r)return{found:false,reason:"missing storeName"};const s=_zustandStateStore.zustandStateStore.getStore(r);if(!s)return{found:false,reason:"unknown storeName"};let i;try{i=s.api.getState()}catch{i=void 0}if(t){const o=(0,_sharedUi.readPath)(i,t);if(!o.ok)return{found:false,reason:o.error};return{found:true,storeName:r,path:t,shape:(0,_sharedUi.shapeSummary)(o.value),currentState:o.value}}const d=(0,_sharedUi.shapeSummary)(i);if(!(0,_sharedUi.isOverWireBudget)(i,_sharedUi.WIRE_DETAIL_LIMIT_BYTES)){return{found:true,storeName:r,...d?{shape:d}:{},currentState:i}}return{found:true,storeName:r,...d?{shape:d}:{},currentState:(0,_sharedUi.isJsonSerializable)(i)?(0,_sharedUi.truncatedNote)(_sharedUi.WIRE_DETAIL_LIMIT_BYTES):STATE_ON_DEVICE}},listStores:e=>{const{includeValues:r=false,limit:t}=(0,_sharedUi.readParams)(e);const s=_zustandStateStore.zustandStateStore.getStoreSnapshots();const i=typeof t==="number"&&t>0?s.slice(0,t):s;const d=i.map(o=>({name:o.name,changes:o.stateChangeCount,isPersisted:o.isPersisted,persistName:o.persistName,canRehydrate:o.canRehydrate,...r?{currentState:o.currentState}:{keys:topLevelKeys(o.currentState)},...(0,_sharedUi.shapeSummary)(o.currentState)?{shape:(0,_sharedUi.shapeSummary)(o.currentState)}:{}}));return{stores:d,total:s.length,returned:d.length,includedValues:r}},setState:e=>{const{storeName:r,state:t,replace:s,force:i,path:d,value:o}=(0,_sharedUi.readParams)(e);let u=t;let f=[];let l=[];if(!r)return{ok:false,error:"Missing storeName."};const c=_zustandStateStore.zustandStateStore.getStore(r);if(!c)return{ok:false,error:`No store named "${r}".`};if(t===void 0&&d===void 0){return{ok:false,error:`Refused: no change was described. Send \`state\` with the fields to merge, or \`path\` and \`value\` to set one value.`}}let S=false;if(d!==void 0){if(o===void 0){return{ok:false,error:`Refused: sent \`path\` without \`value\`. Say what to set "${d}" to; if you mean to clear it, send value:null and the field must already be nullable.`}}let a;try{a=c.api.getState()}catch{a=void 0}const n=(0,_sharedUi.patchFromPath)(a,d,o);if(!n.ok)return{ok:false,error:`Refused: ${n.error}`};u=n.patch;S=true}if(!i&&(s===false||S)){let a;try{a=c.api.getState()}catch{a=void 0}if(a!==void 0){const n=(0,_sharedUi.expandKeyedArrays)(a,u);if(n.error){return{ok:false,error:`Refused: ${n.error}`}}u=n.patch;f=n.notes}if(a!==void 0){l=(0,_sharedUi.unshapedLists)(a).filter(n=>touchesList(u,n))}if(a!==void 0){const n=(0,_sharedUi.typedEditViolations)(a,u,{noun:"store"});if(n.length>0){return{ok:false,error:(0,_sharedUi.refusalText)(n,"store",(0,_sharedUi.suggestedPatch)(u,n)),...(0,_sharedUi.suggestedPatch)(u,n)?{suggestedState:(0,_sharedUi.suggestedPatch)(u,n)}:{},violations:n}}}}const h=S||s===false;if(h){let a;try{a=c.api.getState()}catch{a=void 0}if(isPlainRecord(a)&&isPlainRecord(u)){const n={};for(const[p,y]of Object.entries(u))n[p]=p in a?deepMergeInto(a[p],y):y;u=n}}c.api.setState(u,h?false:s??true);return{ok:true,...d!==void 0?{path:d}:{},...f.length?{keyed:f}:{},...l.length?{unchecked:l.map(a=>`\`${a}\` was empty, so nothing in the app knows what its items look like and the shape of what you added was NOT checked. If the app renders a field you left out, it will render undefined \u2014 read the store back and compare against how the screen uses it.`)}:{}}},rehydrate:async e=>{const{storeName:r}=(0,_sharedUi.readParams)(e);if(!r)return{ok:false,error:"Missing storeName."};const t=_zustandStateStore.zustandStateStore.getStore(r);if(!t)return{ok:false,error:`No store named "${r}".`};if(!t.isPersisted){return{ok:false,error:`"${r}" is not a persisted store \u2014 it has no saved copy to re-read.`}}if(!t.rehydrate){return{ok:false,error:`"${r}" is persisted but Buoy has no rehydrate handle for it. It was probably registered before the app's persist middleware attached.`}}await t.rehydrate();return{ok:true,storeName:r,persistName:t.persistName}}}});
@@ -1 +1 @@
1
- "use strict";
1
+ "use strict";
@@ -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}