@buoy-gg/zustand 7.0.41 → 7.0.43

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