@buoy-gg/zustand 7.0.33 → 7.0.35

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 (45) hide show
  1. package/lib/commonjs/zustand/components/ZustandActionButton.js +1 -1
  2. package/lib/commonjs/zustand/components/ZustandDetailViewToggle.js +13 -107
  3. package/lib/commonjs/zustand/components/ZustandEventFilterView.js +24 -288
  4. package/lib/commonjs/zustand/components/ZustandModal.js +20 -85
  5. package/lib/commonjs/zustand/components/ZustandStateChangeItem.js +8 -61
  6. package/lib/commonjs/zustand/components/ZustandStateDetailContent.js +5 -5
  7. package/lib/commonjs/zustand/components/ZustandStateInfoView.js +8 -52
  8. package/lib/commonjs/zustand/components/ZustandStoreBrowser.js +2 -2
  9. package/lib/commonjs/zustand/components/ZustandStoreDetailContent.js +4 -4
  10. package/lib/commonjs/zustand/components/ZustandStoreEditorScreen.js +3 -3
  11. package/lib/commonjs/zustand/hooks/useZustandStateChanges.js +33 -53
  12. package/lib/commonjs/zustand/sync/zustandSyncAdapter.js +24 -78
  13. package/lib/commonjs/zustand/utils/snapshotProvider.js +17 -60
  14. package/lib/commonjs/zustand/utils/zustandStateStore.js +37 -223
  15. package/lib/module/zustand/components/ZustandActionButton.js +2 -2
  16. package/lib/module/zustand/components/ZustandDetailViewToggle.js +15 -109
  17. package/lib/module/zustand/components/ZustandEventFilterView.js +28 -292
  18. package/lib/module/zustand/components/ZustandModal.js +22 -87
  19. package/lib/module/zustand/components/ZustandStateChangeItem.js +10 -63
  20. package/lib/module/zustand/components/ZustandStateDetailContent.js +6 -6
  21. package/lib/module/zustand/components/ZustandStateInfoView.js +7 -51
  22. package/lib/module/zustand/components/ZustandStoreBrowser.js +3 -3
  23. package/lib/module/zustand/components/ZustandStoreDetailContent.js +5 -5
  24. package/lib/module/zustand/components/ZustandStoreEditorScreen.js +4 -4
  25. package/lib/module/zustand/hooks/useZustandStateChanges.js +34 -54
  26. package/lib/module/zustand/sync/zustandSyncAdapter.js +25 -79
  27. package/lib/module/zustand/utils/snapshotProvider.js +18 -61
  28. package/lib/module/zustand/utils/zustandStateStore.js +37 -223
  29. package/lib/typescript/zustand/components/ZustandDetailViewToggle.d.ts +2 -1
  30. package/lib/typescript/zustand/components/ZustandDetailViewToggle.d.ts.map +1 -1
  31. package/lib/typescript/zustand/components/ZustandEventFilterView.d.ts +6 -0
  32. package/lib/typescript/zustand/components/ZustandEventFilterView.d.ts.map +1 -1
  33. package/lib/typescript/zustand/components/ZustandModal.d.ts.map +1 -1
  34. package/lib/typescript/zustand/components/ZustandStateChangeItem.d.ts.map +1 -1
  35. package/lib/typescript/zustand/components/ZustandStateDetailContent.d.ts.map +1 -1
  36. package/lib/typescript/zustand/components/ZustandStateInfoView.d.ts.map +1 -1
  37. package/lib/typescript/zustand/components/ZustandStoreDetailContent.d.ts.map +1 -1
  38. package/lib/typescript/zustand/components/ZustandStoreEditorScreen.d.ts.map +1 -1
  39. package/lib/typescript/zustand/hooks/useZustandStateChanges.d.ts +2 -1
  40. package/lib/typescript/zustand/hooks/useZustandStateChanges.d.ts.map +1 -1
  41. package/lib/typescript/zustand/sync/zustandSyncAdapter.d.ts +103 -117
  42. package/lib/typescript/zustand/sync/zustandSyncAdapter.d.ts.map +1 -1
  43. package/lib/typescript/zustand/utils/zustandStateStore.d.ts +6 -26
  44. package/lib/typescript/zustand/utils/zustandStateStore.d.ts.map +1 -1
  45. package/package.json +3 -3
@@ -13,7 +13,7 @@
13
13
 
14
14
  import React, { memo, useCallback, useState, useEffect } from "react";
15
15
  import { View, Text, StyleSheet, ScrollView } from "react-native";
16
- import { buoyColors, night, Clock, AlertTriangle, Zap, Info, Box, Database, parseValue, ProUpgradeModal, copyToClipboard } from "@buoy-gg/shared-ui";
16
+ import { buoyColors, night, Clock, AlertTriangle, Zap, Info, Box, Database, parseValue, ProUpgradeModal, copyToClipboard, InfoRow, monoFont } from "@buoy-gg/shared-ui";
17
17
  import { useIsPro } from "@buoy-gg/license";
18
18
  import { DataViewer } from "@buoy-gg/shared-ui/dataViewer";
19
19
  import { ZustandActionButton } from "./ZustandActionButton";
@@ -74,29 +74,6 @@ function SectionHeader({
74
74
  }), badge]
75
75
  });
76
76
  }
77
- function InfoRow({
78
- label,
79
- value,
80
- valueColor,
81
- mono = false,
82
- testID
83
- }) {
84
- return /*#__PURE__*/_jsxs(View, {
85
- style: styles.infoRow,
86
- children: [/*#__PURE__*/_jsx(Text, {
87
- style: styles.infoLabel,
88
- children: label
89
- }), /*#__PURE__*/_jsx(Text, {
90
- testID: testID,
91
- style: [styles.infoValue, mono && styles.infoValueMono, valueColor ? {
92
- color: valueColor
93
- } : undefined],
94
- numberOfLines: 1,
95
- ellipsizeMode: "middle",
96
- children: value
97
- })]
98
- });
99
- }
100
77
  function formatTime(timestamp) {
101
78
  const date = new Date(timestamp);
102
79
  const hours = date.getHours().toString().padStart(2, "0");
@@ -355,33 +332,12 @@ const styles = StyleSheet.create({
355
332
  fontWeight: "600",
356
333
  letterSpacing: 0.5,
357
334
  color: night.color.accent,
358
- fontFamily: "monospace"
335
+ fontFamily: monoFont
359
336
  },
360
337
  cardContent: {
361
338
  padding: 14,
362
339
  gap: 10
363
340
  },
364
- infoRow: {
365
- flexDirection: "row",
366
- alignItems: "center",
367
- justifyContent: "space-between"
368
- },
369
- infoLabel: {
370
- fontSize: 11,
371
- color: night.color.textTertiary,
372
- fontWeight: "500"
373
- },
374
- infoValue: {
375
- fontSize: 11,
376
- color: night.color.text,
377
- fontWeight: "500",
378
- flex: 1,
379
- textAlign: "right",
380
- marginLeft: 12
381
- },
382
- infoValueMono: {
383
- fontFamily: "monospace"
384
- },
385
341
  badgeRow: {
386
342
  flexDirection: "row",
387
343
  flexWrap: "wrap",
@@ -414,7 +370,7 @@ const styles = StyleSheet.create({
414
370
  fontSize: 9,
415
371
  fontWeight: "600",
416
372
  color: night.color.textSecondary,
417
- fontFamily: "monospace"
373
+ fontFamily: monoFont
418
374
  },
419
375
  durationTextSlow: {
420
376
  color: buoyColors.error
@@ -476,7 +432,7 @@ const styles = StyleSheet.create({
476
432
  fontSize: 11,
477
433
  fontWeight: "600",
478
434
  color: night.color.accent,
479
- fontFamily: "monospace"
435
+ fontFamily: monoFont
480
436
  },
481
437
  countBadge: {
482
438
  paddingHorizontal: 8,
@@ -488,7 +444,7 @@ const styles = StyleSheet.create({
488
444
  fontSize: 9,
489
445
  fontWeight: "700",
490
446
  color: night.color.accent,
491
- fontFamily: "monospace"
447
+ fontFamily: monoFont
492
448
  },
493
449
  typeBadge: {
494
450
  paddingHorizontal: 6,
@@ -500,7 +456,7 @@ const styles = StyleSheet.create({
500
456
  fontSize: 8,
501
457
  fontWeight: "600",
502
458
  color: night.color.textTertiary,
503
- fontFamily: "monospace"
459
+ fontFamily: monoFont
504
460
  },
505
461
  dataViewerContainer: {
506
462
  // Inset display panel — darker than the card, the night track color.
@@ -510,7 +466,7 @@ const styles = StyleSheet.create({
510
466
  updaterText: {
511
467
  fontSize: 12,
512
468
  color: night.color.text,
513
- fontFamily: "monospace"
469
+ fontFamily: monoFont
514
470
  },
515
471
  updaterSubtext: {
516
472
  fontSize: 11,
@@ -11,7 +11,7 @@
11
11
 
12
12
  import { useMemo } from "react";
13
13
  import { View, Text, StyleSheet, ScrollView } from "react-native";
14
- import { CompactRow, night, ToolEmptyState, resolveListEmptyState } from "@buoy-gg/shared-ui";
14
+ import { CompactRow, night, ToolEmptyState, resolveListEmptyState, monoFont } from "@buoy-gg/shared-ui";
15
15
  import { zustandStateStore } from "../utils/zustandStateStore";
16
16
  import { ZUSTAND_TEST_ID, storeRowTestID } from "../utils/testIDs";
17
17
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
@@ -166,7 +166,7 @@ const styles = StyleSheet.create({
166
166
  absCountText: {
167
167
  fontSize: 9,
168
168
  fontWeight: "700",
169
- fontFamily: "monospace"
169
+ fontFamily: monoFont
170
170
  },
171
171
  scrollContent: {
172
172
  paddingTop: 8,
@@ -195,6 +195,6 @@ const styles = StyleSheet.create({
195
195
  fontSize: 10,
196
196
  fontWeight: "700",
197
197
  color: night.color.accent,
198
- fontFamily: "monospace"
198
+ fontFamily: monoFont
199
199
  }
200
200
  });
@@ -34,7 +34,7 @@
34
34
 
35
35
  import { useCallback, useEffect, useMemo, useState } from "react";
36
36
  import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from "react-native";
37
- import { buoyColors, Database, ExpandedInfoRow, FileJson, night, PillBadge, parseValue } from "@buoy-gg/shared-ui";
37
+ import { buoyColors, Database, ExpandedInfoRow, FileJson, night, PillBadge, parseValue, monoFont } from "@buoy-gg/shared-ui";
38
38
  import { DataViewer, LiveExplorer, ValueEditModal, updateNestedDataByPath, deleteNestedDataByPath } from "@buoy-gg/shared-ui/dataViewer";
39
39
  import { computeMergePartial, editableStateOf, isPlainObjectLike } from "../utils/editableState";
40
40
  import { ZUSTAND_TEST_ID, storeRowTestID } from "../utils/testIDs";
@@ -394,7 +394,7 @@ const styles = StyleSheet.create({
394
394
  color: night.color.accent,
395
395
  letterSpacing: 0.5,
396
396
  textTransform: "uppercase",
397
- fontFamily: "monospace"
397
+ fontFamily: monoFont
398
398
  },
399
399
  rawButton: {
400
400
  flexDirection: "row",
@@ -441,7 +441,7 @@ const styles = StyleSheet.create({
441
441
  borderBottomColor: night.color.border,
442
442
  letterSpacing: 0.5,
443
443
  textTransform: "uppercase",
444
- fontFamily: "monospace"
444
+ fontFamily: monoFont
445
445
  },
446
446
  infoBody: {
447
447
  padding: 12,
@@ -457,13 +457,13 @@ const styles = StyleSheet.create({
457
457
  inlineLinkText: {
458
458
  fontSize: 10,
459
459
  fontWeight: "600",
460
- fontFamily: "monospace"
460
+ fontFamily: monoFont
461
461
  },
462
462
  actionsText: {
463
463
  flex: 1,
464
464
  color: night.color.textSecondary,
465
465
  fontSize: 10,
466
- fontFamily: "monospace",
466
+ fontFamily: monoFont,
467
467
  lineHeight: 15
468
468
  }
469
469
  });
@@ -31,7 +31,7 @@
31
31
 
32
32
  import { useCallback, useMemo, useState } from "react";
33
33
  import { Alert, ScrollView, StyleSheet, Text, TouchableOpacity, View } from "react-native";
34
- import { buoyColors, Database, FileJson, night, PillBadge, ToolBackground, useSafeAreaInsets, X } from "@buoy-gg/shared-ui";
34
+ import { buoyColors, Database, FileJson, night, PillBadge, ToolBackground, useSafeAreaInsets, X, monoFont } from "@buoy-gg/shared-ui";
35
35
  import { DataTreeActionDock, DataViewer, getAtPath, useTreeEditor, ValueEditModal } from "@buoy-gg/shared-ui/dataViewer";
36
36
  import { computeMergePartial, editableStateOf } from "../utils/editableState";
37
37
  import { ZUSTAND_TEST_ID } from "../utils/testIDs";
@@ -297,7 +297,7 @@ const styles = StyleSheet.create({
297
297
  color: night.color.text,
298
298
  fontSize: 13,
299
299
  fontWeight: "600",
300
- fontFamily: "monospace"
300
+ fontFamily: monoFont
301
301
  },
302
302
  headerState: {
303
303
  color: night.color.textTertiary,
@@ -316,7 +316,7 @@ const styles = StyleSheet.create({
316
316
  metaText: {
317
317
  color: night.color.textTertiary,
318
318
  fontSize: 11,
319
- fontFamily: "monospace"
319
+ fontFamily: monoFont
320
320
  },
321
321
  treeScroll: {
322
322
  flex: 1
@@ -341,7 +341,7 @@ const styles = StyleSheet.create({
341
341
  error: {
342
342
  color: night.color.danger,
343
343
  fontSize: 11,
344
- fontFamily: "monospace",
344
+ fontFamily: monoFont,
345
345
  paddingHorizontal: 12,
346
346
  paddingBottom: 6
347
347
  },
@@ -3,51 +3,39 @@
3
3
  /**
4
4
  * Hook for consuming Zustand state changes from the store
5
5
  *
6
- * Mirrors useReduxActions.ts from @buoy-gg/redux
6
+ * The subscribe / filter / pause / clear plumbing is shared `useChangeLog`;
7
+ * this file owns the zustand-specific filter, stats and store registry.
7
8
  */
8
9
 
9
- import { useState, useEffect, useMemo, useCallback } from "react";
10
+ import { useCallback, useMemo } from "react";
11
+ import { useChangeLog, useSubscribedValue } from "@buoy-gg/shared-ui";
10
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);
11
29
  export function useZustandStateChanges() {
12
- const [stateChanges, setStateChanges] = useState(() => zustandStateStore.getStateChanges());
13
- const [stores, setStores] = useState(() => zustandStateStore.getStores());
14
- const [filter, setFilter] = useState({});
15
- const [isEnabled, setIsEnabled] = useState(() => zustandStateStore.getEnabled());
16
-
17
- // Subscribe to state changes
18
- useEffect(() => {
19
- const unsubChanges = zustandStateStore.subscribe(newChanges => {
20
- setStateChanges(newChanges);
21
- });
22
- const unsubStores = zustandStateStore.subscribeToStores(newStores => {
23
- setStores(newStores);
24
- });
25
- return () => {
26
- unsubChanges();
27
- unsubStores();
28
- };
29
- }, []);
30
-
31
- // Filter state changes — derive directly from state to avoid stale reads
32
- const filteredChanges = useMemo(() => {
33
- let filtered = stateChanges;
34
- if (filter.searchText) {
35
- const search = filter.searchText.toLowerCase();
36
- filtered = filtered.filter(c => c.storeName.toLowerCase().includes(search) || c.partialPreview.toLowerCase().includes(search) || c.changedKeys.some(k => k.toLowerCase().includes(search)));
37
- }
38
- if (filter.storeNames && filter.storeNames.length > 0) {
39
- filtered = filtered.filter(c => filter.storeNames.includes(c.storeName));
40
- }
41
- if (filter.onlyWithChanges) {
42
- filtered = filtered.filter(c => c.hasStateChange);
43
- }
44
- return filtered;
45
- }, [stateChanges, filter]);
30
+ const log = useChangeLog(zustandStateStore, {
31
+ filterItems: filterChanges
32
+ });
33
+ const stores = useSubscribedValue(readStores, subscribeToStores);
46
34
 
47
35
  // Get stats — derive directly from state
48
36
  const stats = useMemo(() => {
49
- const total = stateChanges.length;
50
- const withChanges = stateChanges.filter(c => c.hasStateChange).length;
37
+ const total = log.items.length;
38
+ const withChanges = log.items.filter(c => c.hasStateChange).length;
51
39
  return {
52
40
  totalChanges: total,
53
41
  changesWithStateChange: withChanges,
@@ -55,33 +43,25 @@ export function useZustandStateChanges() {
55
43
  storeCount: stores.length,
56
44
  averageDuration: 0
57
45
  };
58
- }, [stateChanges, stores]);
46
+ }, [log.items, stores]);
59
47
 
60
48
  // Get unique store names
61
49
  const storeNames = useMemo(() => {
62
50
  return zustandStateStore.getUniqueStoreNames();
63
51
  }, [stores]);
64
- const clearChanges = useCallback(() => {
65
- zustandStateStore.clearStateChanges();
66
- }, []);
67
- const toggleCapture = useCallback(() => {
68
- const newEnabled = !isEnabled;
69
- zustandStateStore.setEnabled(newEnabled);
70
- setIsEnabled(newEnabled);
71
- }, [isEnabled]);
72
52
  const getChangeById = useCallback(id => {
73
53
  return zustandStateStore.getStateChangeById(id);
74
54
  }, []);
75
55
  return {
76
- stateChanges,
77
- filteredChanges,
78
- filter,
79
- setFilter,
56
+ stateChanges: log.items,
57
+ filteredChanges: log.filteredItems,
58
+ filter: log.filter,
59
+ setFilter: log.setFilter,
80
60
  stats,
81
61
  stores,
82
- clearChanges,
83
- isEnabled,
84
- toggleCapture,
62
+ clearChanges: log.clear,
63
+ isEnabled: log.isEnabled,
64
+ toggleCapture: log.toggleCapture,
85
65
  storeNames,
86
66
  getChangeById
87
67
  };
@@ -1,25 +1,17 @@
1
1
  "use strict";
2
2
 
3
3
  import { zustandStateStore } from "../utils/zustandStateStore";
4
- import { isJsonSerializable, isOverWireBudget } from "@buoy-gg/shared-ui";
4
+ import { WIRE_DETAIL_LIMIT_BYTES, createSyncAdapter, detailByIdAction, isJsonSerializable, isOverWireBudget, memoByIdentity, onDeviceMarker, readParams, toWire, truncatedNote } from "@buoy-gg/shared-ui";
5
5
 
6
6
  /**
7
7
  * Sentinel used in WIRE-FORM snapshots in place of raw prevState/nextState.
8
8
  * Same hazard as redux: up to 200 changes × 2 full trees on every snapshot
9
9
  * freeze/OOM-killed large apps. Dashboards fetch trees via `getChangeDetail`.
10
10
  */
11
- export const STATE_ON_DEVICE = Object.freeze({
12
- __buoyStateOnDevice: true,
13
- note: "State trees stay on the device — fetched on demand via getChangeDetail / getStoreState."
14
- });
11
+ export const STATE_ON_DEVICE = onDeviceMarker("State", "State trees stay on the device — fetched on demand via getChangeDetail / getStoreState.");
15
12
 
16
13
  /** Wire-form sentinel for an oversized `partial` (replace can carry a whole tree). */
17
- export const PAYLOAD_ON_DEVICE = Object.freeze({
18
- __buoyPayloadOnDevice: true,
19
- note: "Partial stays on the device — fetched on demand via getChangeDetail."
20
- });
21
- const MAX_CHANGE_DETAIL_BYTES = 8 * 1024 * 1024;
22
- const MAX_WIRE_PAYLOAD_BYTES = 16 * 1024;
14
+ export const PAYLOAD_ON_DEVICE = onDeviceMarker("Payload", "Partial stays on the device — fetched on demand via getChangeDetail.");
23
15
  /** Top-level keys of an object state, for the compact (no-values) view. */
24
16
  function topLevelKeys(state) {
25
17
  return state && typeof state === "object" && !Array.isArray(state) ? Object.keys(state) : undefined;
@@ -30,31 +22,17 @@ function topLevelKeys(state) {
30
22
  * change, so keying on identity is correct and self-invalidating — without it
31
23
  * every `partial` in the list is re-measured on all ~5 snapshots per second.
32
24
  */
33
- const changeCache = new WeakMap();
34
- function toWireChange(change) {
35
- const cached = changeCache.get(change);
36
- if (cached) return cached;
37
- const wire = {
38
- ...change,
39
- prevState: STATE_ON_DEVICE,
40
- nextState: STATE_ON_DEVICE,
41
- partial: toWirePartial(change.partial)
42
- };
43
- changeCache.set(change, wire);
44
- return wire;
45
- }
46
- function toWirePartial(value) {
47
- if (value === undefined) return value;
48
- return isOverWireBudget(value, MAX_WIRE_PAYLOAD_BYTES) ? PAYLOAD_ON_DEVICE : value;
49
- }
50
- function toWireState(value) {
51
- return isOverWireBudget(value, MAX_WIRE_PAYLOAD_BYTES) ? STATE_ON_DEVICE : value;
52
- }
25
+ const toWireChange = memoByIdentity(change => ({
26
+ ...change,
27
+ prevState: STATE_ON_DEVICE,
28
+ nextState: STATE_ON_DEVICE,
29
+ partial: toWire(change.partial, PAYLOAD_ON_DEVICE)
30
+ }));
31
+ const toWireState = value => toWire(value, STATE_ON_DEVICE);
53
32
 
54
33
  /**
55
34
  * Sync adapter for the zustand tool, consumed by @buoy-gg/external-sync's
56
- * `useExternalSync` (structurally matches its ToolSyncAdapter interface so
57
- * this package doesn't need a dependency on it).
35
+ * `useExternalSync`.
58
36
  *
59
37
  * State changes are captured by the middleware (watchStores/buoyDevTools),
60
38
  * independent of whether a dashboard is watching — subscribing here only
@@ -69,7 +47,7 @@ function toWireState(value) {
69
47
  *
70
48
  * The `setState` action powers remote time-travel/reset from the dashboard.
71
49
  */
72
- export const zustandSyncAdapter = {
50
+ export const zustandSyncAdapter = createSyncAdapter({
73
51
  version: 2,
74
52
  getSnapshot: () => ({
75
53
  changes: zustandStateStore.getStateChanges().map(toWireChange),
@@ -78,14 +56,7 @@ export const zustandSyncAdapter = {
78
56
  currentState: toWireState(store.currentState)
79
57
  }))
80
58
  }),
81
- subscribe: onChange => {
82
- const unsubscribeChanges = zustandStateStore.subscribe(onChange);
83
- const unsubscribeStores = zustandStateStore.subscribeToStores(onChange);
84
- return () => {
85
- unsubscribeChanges();
86
- unsubscribeStores();
87
- };
88
- },
59
+ subscribe: [onChange => zustandStateStore.subscribe(onChange), onChange => zustandStateStore.subscribeToStores(onChange)],
89
60
  actions: {
90
61
  clearEvents: () => {
91
62
  zustandStateStore.clearStateChanges();
@@ -95,32 +66,11 @@ export const zustandSyncAdapter = {
95
66
  * The per-snapshot stream stays tree-free; this is the explicit,
96
67
  * size-guarded channel for the detail pane.
97
68
  */
98
- getChangeDetail: params => {
99
- const {
100
- id
101
- } = params ?? {};
102
- if (!id) return {
103
- found: false,
104
- reason: "missing id"
105
- };
106
- const change = zustandStateStore.getStateChangeById(id);
107
- if (!change) return {
108
- found: false,
109
- reason: "unknown id"
110
- };
111
- const cappedNote = {
112
- __buoyTruncated: true,
113
- note: `Exceeds the ${MAX_CHANGE_DETAIL_BYTES / (1024 * 1024)}MB detail limit — inspect it on-device.`
114
- };
115
- const cap = value => isOverWireBudget(value, MAX_CHANGE_DETAIL_BYTES) ? cappedNote : value;
116
- return {
117
- found: true,
118
- id,
119
- prevState: cap(change.prevState),
120
- nextState: cap(change.nextState),
121
- partial: cap(change.partial)
122
- };
123
- },
69
+ getChangeDetail: detailByIdAction(id => zustandStateStore.getStateChangeById(id), (change, cap) => ({
70
+ prevState: cap(change.prevState),
71
+ nextState: cap(change.nextState),
72
+ partial: cap(change.partial)
73
+ })),
124
74
  /**
125
75
  * On-demand current state for one store. Wire-form snapshots replace
126
76
  * oversized / unserializable `currentState` with STATE_ON_DEVICE.
@@ -128,7 +78,7 @@ export const zustandSyncAdapter = {
128
78
  getStoreState: params => {
129
79
  const {
130
80
  storeName
131
- } = params ?? {};
81
+ } = readParams(params);
132
82
  if (!storeName) return {
133
83
  found: false,
134
84
  reason: "missing storeName"
@@ -144,11 +94,7 @@ export const zustandSyncAdapter = {
144
94
  } catch {
145
95
  currentState = undefined;
146
96
  }
147
- const cappedNote = {
148
- __buoyTruncated: true,
149
- note: `Exceeds the ${MAX_CHANGE_DETAIL_BYTES / (1024 * 1024)}MB detail limit — inspect it on-device.`
150
- };
151
- if (!isOverWireBudget(currentState, MAX_CHANGE_DETAIL_BYTES)) {
97
+ if (!isOverWireBudget(currentState, WIRE_DETAIL_LIMIT_BYTES)) {
152
98
  return {
153
99
  found: true,
154
100
  storeName,
@@ -158,7 +104,7 @@ export const zustandSyncAdapter = {
158
104
  return {
159
105
  found: true,
160
106
  storeName,
161
- currentState: isJsonSerializable(currentState) ? cappedNote : STATE_ON_DEVICE
107
+ currentState: isJsonSerializable(currentState) ? truncatedNote(WIRE_DETAIL_LIMIT_BYTES) : STATE_ON_DEVICE
162
108
  };
163
109
  },
164
110
  /**
@@ -171,7 +117,7 @@ export const zustandSyncAdapter = {
171
117
  const {
172
118
  includeValues = false,
173
119
  limit
174
- } = params ?? {};
120
+ } = readParams(params);
175
121
  const all = zustandStateStore.getStoreSnapshots();
176
122
  const capped = typeof limit === "number" && limit > 0 ? all.slice(0, limit) : all;
177
123
  const stores = capped.map(s => ({
@@ -198,7 +144,7 @@ export const zustandSyncAdapter = {
198
144
  storeName,
199
145
  state,
200
146
  replace
201
- } = params ?? {};
147
+ } = readParams(params);
202
148
  if (!storeName) return {
203
149
  ok: false,
204
150
  error: "Missing storeName."
@@ -237,7 +183,7 @@ export const zustandSyncAdapter = {
237
183
  rehydrate: async params => {
238
184
  const {
239
185
  storeName
240
- } = params ?? {};
186
+ } = readParams(params);
241
187
  if (!storeName) return {
242
188
  ok: false,
243
189
  error: "Missing storeName."
@@ -267,4 +213,4 @@ export const zustandSyncAdapter = {
267
213
  };
268
214
  }
269
215
  }
270
- };
216
+ });
@@ -13,8 +13,7 @@
13
13
  * strips functions (recording where they were), and restore re-grafts the
14
14
  * LIVE store's functions onto the snapshot before replacing.
15
15
  */
16
- import { registerStateSnapshotProvider } from "@buoy-gg/shared-ui";
17
- import { boundedDeepEqual, jsonLikelyLargerThan } from "@buoy-gg/shared-ui/dataViewer";
16
+ import { classifyPreviewItem, createRestoreScope, emptyRestoreResult, failedRestore, registerStateSnapshotProvider, unionKeys } from "@buoy-gg/shared-ui";
18
17
  import { zustandStateStore } from "./zustandStateStore";
19
18
  import { isPlainObjectLike, stripFunctions, STRIP_MAX_DEPTH } from "./editableState";
20
19
 
@@ -93,29 +92,13 @@ async function capture() {
93
92
  }
94
93
  async function restore(data, options) {
95
94
  const snapshot = data;
96
- const excludedSet = new Set(options?.excludeKeys ?? []);
97
- const includedSet = options?.includeKeys ? new Set(options.includeKeys) : null;
98
- const touch = key => !excludedSet.has(key) && (!includedSet || includedSet.has(key));
99
- let excludedHits = 0;
100
- const result = {
101
- ok: true,
102
- applied: 0,
103
- skipped: [],
104
- warnings: []
105
- };
95
+ const scope = createRestoreScope(options);
96
+ const result = emptyRestoreResult();
106
97
  if (!snapshot?.stores) {
107
- return {
108
- ok: false,
109
- applied: 0,
110
- skipped: [],
111
- warnings: ["empty zustand snapshot"]
112
- };
98
+ return failedRestore("empty zustand snapshot");
113
99
  }
114
100
  for (const [name, envelope] of Object.entries(snapshot.stores)) {
115
- if (!touch(name)) {
116
- excludedHits += 1;
117
- continue;
118
- }
101
+ if (!scope.allows(name)) continue;
119
102
  const info = zustandStateStore.getStore(name);
120
103
  if (!info) {
121
104
  result.skipped.push({
@@ -142,12 +125,10 @@ async function restore(data, options) {
142
125
  result.ok = false;
143
126
  }
144
127
  }
145
- if (excludedHits > 0) {
146
- result.warnings.push(`${excludedHits} store(s) excluded by you`);
147
- }
128
+ const excludedWarning = scope.excludedWarning("store(s)");
129
+ if (excludedWarning) result.warnings.push(excludedWarning);
148
130
  return result;
149
131
  }
150
- const ITEM_RENDER_BYTE_LIMIT = 256 * 1024;
151
132
 
152
133
  /**
153
134
  * Per-store restore preview. Both sides are function-stripped through the
@@ -176,8 +157,7 @@ async function previewItems(target, base) {
176
157
  const items = [];
177
158
  const notes = [];
178
159
  let preservedFns = 0;
179
- const names = new Set([...Object.keys(baseStores), ...Object.keys(targetStores)]);
180
- for (const name of names) {
160
+ for (const name of unionKeys(baseStores, targetStores)) {
181
161
  const inTarget = name in targetStores;
182
162
  const inBase = name in baseStores;
183
163
  if (!inTarget) {
@@ -200,39 +180,16 @@ async function previewItems(target, base) {
200
180
  });
201
181
  continue;
202
182
  }
203
- if (!inBase) {
204
- items.push({
205
- key: name,
206
- label: name,
207
- verdict: "added",
208
- newValue: targetStores[name].state
209
- });
210
- continue;
211
- }
212
- const oldState = baseStores[name].state;
213
- const newState = targetStores[name].state;
214
- if (boundedDeepEqual(oldState, newState)) {
215
- items.push({
216
- key: name,
217
- label: name,
218
- verdict: "unchanged"
219
- });
220
- } else if (jsonLikelyLargerThan(oldState, ITEM_RENDER_BYTE_LIMIT) || jsonLikelyLargerThan(newState, ITEM_RENDER_BYTE_LIMIT)) {
221
- items.push({
222
- key: name,
223
- label: name,
224
- verdict: "too-large",
225
- reason: "store too large to render a diff — restore still applies it"
226
- });
227
- } else {
228
- items.push({
229
- key: name,
230
- label: name,
231
- verdict: "changed",
232
- oldValue: oldState,
233
- newValue: newState
234
- });
235
- }
183
+ items.push(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
+ }));
236
193
  }
237
194
  if (preservedFns > 0) {
238
195
  notes.push(`${preservedFns} store action function${preservedFns === 1 ? "" : "s"} preserved on restore (not diffed)`);