@buoy-gg/jotai 7.0.33 → 7.0.34

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 (32) hide show
  1. package/lib/commonjs/jotai/components/JotaiAtomChangeItem.js +7 -52
  2. package/lib/commonjs/jotai/components/JotaiAtomDetailContent.js +8 -87
  3. package/lib/commonjs/jotai/components/JotaiEventFilterView.js +21 -299
  4. package/lib/commonjs/jotai/components/JotaiModal.js +20 -85
  5. package/lib/commonjs/jotai/hooks/useJotaiAtomChanges.js +33 -49
  6. package/lib/commonjs/jotai/sync/jotaiSyncAdapter.js +19 -66
  7. package/lib/commonjs/jotai/utils/jotaiStateStore.js +23 -188
  8. package/lib/commonjs/jotai/utils/snapshotProvider.js +7 -25
  9. package/lib/commonjs/preset.js +14 -22
  10. package/lib/module/jotai/components/JotaiAtomChangeItem.js +9 -54
  11. package/lib/module/jotai/components/JotaiAtomDetailContent.js +9 -88
  12. package/lib/module/jotai/components/JotaiEventFilterView.js +24 -302
  13. package/lib/module/jotai/components/JotaiModal.js +22 -87
  14. package/lib/module/jotai/hooks/useJotaiAtomChanges.js +34 -50
  15. package/lib/module/jotai/sync/jotaiSyncAdapter.js +20 -67
  16. package/lib/module/jotai/utils/jotaiStateStore.js +23 -188
  17. package/lib/module/jotai/utils/snapshotProvider.js +8 -26
  18. package/lib/module/preset.js +13 -20
  19. package/lib/typescript/jotai/components/JotaiAtomChangeItem.d.ts.map +1 -1
  20. package/lib/typescript/jotai/components/JotaiAtomDetailContent.d.ts.map +1 -1
  21. package/lib/typescript/jotai/components/JotaiEventFilterView.d.ts +2 -2
  22. package/lib/typescript/jotai/components/JotaiEventFilterView.d.ts.map +1 -1
  23. package/lib/typescript/jotai/components/JotaiModal.d.ts.map +1 -1
  24. package/lib/typescript/jotai/hooks/useJotaiAtomChanges.d.ts +2 -1
  25. package/lib/typescript/jotai/hooks/useJotaiAtomChanges.d.ts.map +1 -1
  26. package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts +54 -68
  27. package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts.map +1 -1
  28. package/lib/typescript/jotai/utils/jotaiStateStore.d.ts +2 -25
  29. package/lib/typescript/jotai/utils/jotaiStateStore.d.ts.map +1 -1
  30. package/lib/typescript/preset.d.ts +2 -8
  31. package/lib/typescript/preset.d.ts.map +1 -1
  32. package/package.json +3 -3
@@ -5,78 +5,62 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.useJotaiAtomChanges = useJotaiAtomChanges;
7
7
  var _react = require("react");
8
+ var _sharedUi = require("@buoy-gg/shared-ui");
8
9
  var _jotaiStateStore = require("../utils/jotaiStateStore");
9
10
  /**
10
11
  * Hook for consuming Jotai atom changes from the store
11
12
  *
12
- * Mirrors useZustandStateChanges.ts from @buoy-gg/zustand
13
+ * The subscribe / filter / pause / clear plumbing is shared `useChangeLog`;
14
+ * this file owns the jotai-specific filter, stats and atom registry.
13
15
  */
14
16
 
17
+ function filterChanges(changes, filter) {
18
+ let filtered = changes;
19
+ if (filter.searchText) {
20
+ const search = filter.searchText.toLowerCase();
21
+ filtered = filtered.filter(c => c.atomLabel.toLowerCase().includes(search) || c.valuePreview.toLowerCase().includes(search) || c.changedKeys.some(k => k.toLowerCase().includes(search)));
22
+ }
23
+ if (filter.atomLabels && filter.atomLabels.length > 0) {
24
+ filtered = filtered.filter(c => filter.atomLabels.includes(c.atomLabel));
25
+ }
26
+ if (filter.onlyWithChanges) {
27
+ filtered = filtered.filter(c => c.hasValueChange);
28
+ }
29
+ return filtered;
30
+ }
31
+ const readAtoms = () => _jotaiStateStore.jotaiStateStore.getAtoms();
32
+ const subscribeToAtoms = listener => _jotaiStateStore.jotaiStateStore.subscribeToAtoms(listener);
15
33
  function useJotaiAtomChanges() {
16
- const [atomChanges, setAtomChanges] = (0, _react.useState)(() => _jotaiStateStore.jotaiStateStore.getAtomChanges());
17
- const [atoms, setAtoms] = (0, _react.useState)(() => _jotaiStateStore.jotaiStateStore.getAtoms());
18
- const [filter, setFilter] = (0, _react.useState)({});
19
- const [isEnabled, setIsEnabled] = (0, _react.useState)(() => _jotaiStateStore.jotaiStateStore.getEnabled());
20
- (0, _react.useEffect)(() => {
21
- const unsubChanges = _jotaiStateStore.jotaiStateStore.subscribe(newChanges => {
22
- setAtomChanges(newChanges);
23
- });
24
- const unsubAtoms = _jotaiStateStore.jotaiStateStore.subscribeToAtoms(newAtoms => {
25
- setAtoms(newAtoms);
26
- });
27
- return () => {
28
- unsubChanges();
29
- unsubAtoms();
30
- };
31
- }, []);
32
- const filteredChanges = (0, _react.useMemo)(() => {
33
- let filtered = atomChanges;
34
- if (filter.searchText) {
35
- const search = filter.searchText.toLowerCase();
36
- filtered = filtered.filter(c => c.atomLabel.toLowerCase().includes(search) || c.valuePreview.toLowerCase().includes(search) || c.changedKeys.some(k => k.toLowerCase().includes(search)));
37
- }
38
- if (filter.atomLabels && filter.atomLabels.length > 0) {
39
- filtered = filtered.filter(c => filter.atomLabels.includes(c.atomLabel));
40
- }
41
- if (filter.onlyWithChanges) {
42
- filtered = filtered.filter(c => c.hasValueChange);
43
- }
44
- return filtered;
45
- }, [atomChanges, filter]);
34
+ const log = (0, _sharedUi.useChangeLog)(_jotaiStateStore.jotaiStateStore, {
35
+ filterItems: filterChanges
36
+ });
37
+ const atoms = (0, _sharedUi.useSubscribedValue)(readAtoms, subscribeToAtoms);
46
38
  const stats = (0, _react.useMemo)(() => {
47
- const total = atomChanges.length;
48
- const withChanges = atomChanges.filter(c => c.hasValueChange).length;
39
+ const total = log.items.length;
40
+ const withChanges = log.items.filter(c => c.hasValueChange).length;
49
41
  return {
50
42
  totalChanges: total,
51
43
  changesWithValueChange: withChanges,
52
44
  changesWithoutValueChange: total - withChanges,
53
45
  atomCount: atoms.length
54
46
  };
55
- }, [atomChanges, atoms]);
47
+ }, [log.items, atoms]);
56
48
  const atomLabels = (0, _react.useMemo)(() => {
57
49
  return _jotaiStateStore.jotaiStateStore.getUniqueAtomLabels();
58
50
  }, [atoms]);
59
- const clearChanges = (0, _react.useCallback)(() => {
60
- _jotaiStateStore.jotaiStateStore.clearAtomChanges();
61
- }, []);
62
- const toggleCapture = (0, _react.useCallback)(() => {
63
- const newEnabled = !isEnabled;
64
- _jotaiStateStore.jotaiStateStore.setEnabled(newEnabled);
65
- setIsEnabled(newEnabled);
66
- }, [isEnabled]);
67
51
  const getChangeById = (0, _react.useCallback)(id => {
68
52
  return _jotaiStateStore.jotaiStateStore.getAtomChangeById(id);
69
53
  }, []);
70
54
  return {
71
- atomChanges,
72
- filteredChanges,
73
- filter,
74
- setFilter,
55
+ atomChanges: log.items,
56
+ filteredChanges: log.filteredItems,
57
+ filter: log.filter,
58
+ setFilter: log.setFilter,
75
59
  stats,
76
60
  atoms,
77
- clearChanges,
78
- isEnabled,
79
- toggleCapture,
61
+ clearChanges: log.clear,
62
+ isEnabled: log.isEnabled,
63
+ toggleCapture: log.toggleCapture,
80
64
  atomLabels,
81
65
  getChangeById
82
66
  };
@@ -11,36 +11,21 @@ var _sharedUi = require("@buoy-gg/shared-ui");
11
11
  * Same hazard as redux/zustand: up to 200 changes × 2 values on every
12
12
  * snapshot. Dashboards fetch values via `getChangeDetail`.
13
13
  */
14
- const VALUE_ON_DEVICE = exports.VALUE_ON_DEVICE = Object.freeze({
15
- __buoyValueOnDevice: true,
16
- note: "Atom values stay on the device — fetched on demand via getChangeDetail / getAtomValue."
17
- });
18
- const MAX_CHANGE_DETAIL_BYTES = 8 * 1024 * 1024;
19
- const MAX_WIRE_PAYLOAD_BYTES = 16 * 1024;
14
+ const VALUE_ON_DEVICE = exports.VALUE_ON_DEVICE = (0, _sharedUi.onDeviceMarker)("Value", "Atom values stay on the device — fetched on demand via getChangeDetail / getAtomValue.");
20
15
  /**
21
16
  * Per-change wire cache — the store prepends and never mutates a recorded
22
17
  * change, so identity is a correct, self-invalidating key.
23
18
  */
24
- const changeCache = new WeakMap();
25
- function toWireChange(change) {
26
- const cached = changeCache.get(change);
27
- if (cached) return cached;
28
- const wire = {
29
- ...change,
30
- prevValue: VALUE_ON_DEVICE,
31
- nextValue: VALUE_ON_DEVICE
32
- };
33
- changeCache.set(change, wire);
34
- return wire;
35
- }
36
- function toWireValue(value) {
37
- return (0, _sharedUi.isOverWireBudget)(value, MAX_WIRE_PAYLOAD_BYTES) ? VALUE_ON_DEVICE : value;
38
- }
19
+ const toWireChange = (0, _sharedUi.memoByIdentity)(change => ({
20
+ ...change,
21
+ prevValue: VALUE_ON_DEVICE,
22
+ nextValue: VALUE_ON_DEVICE
23
+ }));
24
+ const toWireValue = value => (0, _sharedUi.toWire)(value, VALUE_ON_DEVICE);
39
25
 
40
26
  /**
41
27
  * Sync adapter for the jotai tool, consumed by @buoy-gg/external-sync's
42
- * `useExternalSync` (structurally matches its ToolSyncAdapter interface so
43
- * this package doesn't need a dependency on it).
28
+ * `useExternalSync`.
44
29
  *
45
30
  * Atom changes are captured by watchAtoms(), independent of whether a
46
31
  * dashboard is watching — subscribing here only streams what the store
@@ -52,7 +37,7 @@ function toWireValue(value) {
52
37
  * Dashboards fetch values via `getChangeDetail`. Oversized / unserializable
53
38
  * `atoms[].currentValue` uses the same marker and is fetched via `getAtomValue`.
54
39
  */
55
- const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
40
+ const jotaiSyncAdapter = exports.jotaiSyncAdapter = (0, _sharedUi.createSyncAdapter)({
56
41
  version: 2,
57
42
  getSnapshot: () => ({
58
43
  changes: _jotaiStateStore.jotaiStateStore.getAtomChanges().map(toWireChange),
@@ -61,14 +46,7 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
61
46
  currentValue: toWireValue(atom.currentValue)
62
47
  }))
63
48
  }),
64
- subscribe: onChange => {
65
- const unsubscribeChanges = _jotaiStateStore.jotaiStateStore.subscribe(onChange);
66
- const unsubscribeAtoms = _jotaiStateStore.jotaiStateStore.subscribeToAtoms(onChange);
67
- return () => {
68
- unsubscribeChanges();
69
- unsubscribeAtoms();
70
- };
71
- },
49
+ subscribe: [onChange => _jotaiStateStore.jotaiStateStore.subscribe(onChange), onChange => _jotaiStateStore.jotaiStateStore.subscribeToAtoms(onChange)],
72
50
  actions: {
73
51
  clearEvents: () => {
74
52
  _jotaiStateStore.jotaiStateStore.clearAtomChanges();
@@ -78,31 +56,10 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
78
56
  * The per-snapshot stream stays value-free; this is the explicit,
79
57
  * size-guarded channel for the detail pane.
80
58
  */
81
- getChangeDetail: params => {
82
- const {
83
- id
84
- } = params ?? {};
85
- if (!id) return {
86
- found: false,
87
- reason: "missing id"
88
- };
89
- const change = _jotaiStateStore.jotaiStateStore.getAtomChangeById(id);
90
- if (!change) return {
91
- found: false,
92
- reason: "unknown id"
93
- };
94
- const cappedNote = {
95
- __buoyTruncated: true,
96
- note: `Exceeds the ${MAX_CHANGE_DETAIL_BYTES / (1024 * 1024)}MB detail limit — inspect it on-device.`
97
- };
98
- const cap = value => (0, _sharedUi.isOverWireBudget)(value, MAX_CHANGE_DETAIL_BYTES) ? cappedNote : value;
99
- return {
100
- found: true,
101
- id,
102
- prevValue: cap(change.prevValue),
103
- nextValue: cap(change.nextValue)
104
- };
105
- },
59
+ getChangeDetail: (0, _sharedUi.detailByIdAction)(id => _jotaiStateStore.jotaiStateStore.getAtomChangeById(id), (change, cap) => ({
60
+ prevValue: cap(change.prevValue),
61
+ nextValue: cap(change.nextValue)
62
+ })),
106
63
  /**
107
64
  * On-demand current value for one atom. Wire-form snapshots replace
108
65
  * oversized / unserializable `currentValue` with VALUE_ON_DEVICE.
@@ -110,7 +67,7 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
110
67
  getAtomValue: params => {
111
68
  const {
112
69
  label
113
- } = params ?? {};
70
+ } = (0, _sharedUi.readParams)(params);
114
71
  if (!label) return {
115
72
  found: false,
116
73
  reason: "missing label"
@@ -126,10 +83,6 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
126
83
  } catch {
127
84
  currentValue = undefined;
128
85
  }
129
- const cappedNote = {
130
- __buoyTruncated: true,
131
- note: `Exceeds the ${MAX_CHANGE_DETAIL_BYTES / (1024 * 1024)}MB detail limit — inspect it on-device.`
132
- };
133
86
  try {
134
87
  JSON.stringify(currentValue);
135
88
  } catch {
@@ -142,7 +95,7 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
142
95
  return {
143
96
  found: true,
144
97
  label,
145
- currentValue: (0, _sharedUi.isOverWireBudget)(currentValue, MAX_CHANGE_DETAIL_BYTES) ? cappedNote : currentValue
98
+ currentValue: (0, _sharedUi.capDetail)(currentValue, _sharedUi.WIRE_DETAIL_LIMIT_BYTES)
146
99
  };
147
100
  },
148
101
  /**
@@ -155,7 +108,7 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
155
108
  const {
156
109
  includeValues = false,
157
110
  limit
158
- } = params ?? {};
111
+ } = (0, _sharedUi.readParams)(params);
159
112
  const all = _jotaiStateStore.jotaiStateStore.getAtoms();
160
113
  const capped = typeof limit === "number" && limit > 0 ? all.slice(0, limit) : all;
161
114
  const atoms = capped.map(a => {
@@ -188,9 +141,9 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
188
141
  const {
189
142
  label,
190
143
  value
191
- } = params ?? {};
144
+ } = (0, _sharedUi.readParams)(params);
192
145
  if (!label) throw new Error("setAtom requires a `label`");
193
146
  _jotaiStateStore.jotaiStateStore.setAtomValue(label, value);
194
147
  }
195
148
  }
196
- };
149
+ });
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.jotaiStateStore = void 0;
7
+ var _sharedUi = require("@buoy-gg/shared-ui");
7
8
  /**
8
9
  * Jotai state store — captures and stores Jotai atom changes
9
10
  *
@@ -40,44 +41,7 @@ const ATOM_COLORS = {
40
41
  filter: "#84CC16" // lime
41
42
  };
42
43
  function getAtomColor(label) {
43
- const lower = label.toLowerCase();
44
- if (ATOM_COLORS[lower]) return ATOM_COLORS[lower];
45
- for (const [key, color] of Object.entries(ATOM_COLORS)) {
46
- if (lower.includes(key)) return color;
47
- }
48
-
49
- // Generate consistent hex from name hash
50
- const hash = label.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
51
- const hue = hash * 137 % 360;
52
- const s = 0.7;
53
- const l = 0.6;
54
- const c = (1 - Math.abs(2 * l - 1)) * s;
55
- const x = c * (1 - Math.abs(hue / 60 % 2 - 1));
56
- const m = l - c / 2;
57
- let r = 0,
58
- g = 0,
59
- b = 0;
60
- if (hue < 60) {
61
- r = c;
62
- g = x;
63
- } else if (hue < 120) {
64
- r = x;
65
- g = c;
66
- } else if (hue < 180) {
67
- g = c;
68
- b = x;
69
- } else if (hue < 240) {
70
- g = x;
71
- b = c;
72
- } else if (hue < 300) {
73
- r = x;
74
- b = c;
75
- } else {
76
- r = c;
77
- b = x;
78
- }
79
- const toHex = v => Math.round((v + m) * 255).toString(16).padStart(2, "0");
80
- return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
44
+ return (0, _sharedUi.entityColor)(label, ATOM_COLORS);
81
45
  }
82
46
 
83
47
  // ============================================
@@ -85,97 +49,25 @@ function getAtomColor(label) {
85
49
  // ============================================
86
50
 
87
51
  function formatValuePreview(value, maxLength = 40) {
88
- if (value === undefined) return "undefined";
89
- if (value === null) return "null";
90
- try {
91
- if (typeof value === "function") return "(fn)";
92
- if (typeof value === "string") {
93
- return value.length > maxLength ? `"${value.slice(0, maxLength - 3)}..."` : `"${value}"`;
94
- }
95
- if (typeof value === "number" || typeof value === "boolean") {
96
- return String(value);
97
- }
98
- if (Array.isArray(value)) {
99
- if (value.length === 0) return "[]";
100
- const preview = JSON.stringify(value);
101
- return preview.length > maxLength ? `[${value.length} items]` : preview;
102
- }
103
- if (typeof value === "object") {
104
- const keys = Object.keys(value);
105
- if (keys.length === 0) return "{}";
106
- const preview = JSON.stringify(value);
107
- if (preview.length <= maxLength) return preview;
108
- return `{ ${keys.length} keys }`;
109
- }
110
- return String(value).slice(0, maxLength);
111
- } catch {
112
- return "[complex]";
113
- }
114
- }
115
- function getValueDiffSummary(prevValue, nextValue) {
116
- if (prevValue === nextValue) {
117
- return {
118
- summary: "no change",
119
- changedKeys: [],
120
- changedCount: 0
121
- };
122
- }
123
- if (typeof prevValue !== "object" || typeof nextValue !== "object" || prevValue === null || nextValue === null) {
124
- return {
125
- summary: "changed",
126
- changedKeys: [],
127
- changedCount: 0
128
- };
129
- }
130
- const prevKeys = Object.keys(prevValue);
131
- const nextKeys = Object.keys(nextValue);
132
- const added = nextKeys.filter(k => !prevKeys.includes(k));
133
- const removed = prevKeys.filter(k => !nextKeys.includes(k));
134
- const changed = [];
135
- for (const key of prevKeys) {
136
- if (nextKeys.includes(key) && prevValue[key] !== nextValue[key]) {
137
- changed.push(key);
138
- }
139
- }
140
- const allChangedKeys = [...added, ...removed, ...changed];
141
- const parts = [];
142
- if (added.length > 0) parts.push(`+${added.length}`);
143
- if (removed.length > 0) parts.push(`-${removed.length}`);
144
- if (changed.length > 0) parts.push(`~${changed.length}`);
145
- const total = allChangedKeys.length;
146
- if (parts.length === 0) {
147
- return {
148
- summary: "nested change",
149
- changedKeys: [],
150
- changedCount: 0
151
- };
152
- }
153
- return {
154
- summary: `${parts.join(" ")} ${total === 1 ? "key" : "keys"}`,
155
- changedKeys: allChangedKeys,
156
- changedCount: total
157
- };
52
+ return (0, _sharedUi.formatValuePreview)(value, {
53
+ maxLength,
54
+ functionLabel: "(fn)"
55
+ });
158
56
  }
57
+ const getValueDiffSummary = _sharedUi.shallowDiffSummary;
159
58
 
160
59
  // ============================================
161
60
  // Jotai State Store
162
61
  // ============================================
163
62
 
164
- class JotaiStateStore {
165
- atomChanges = [];
63
+ class JotaiStateStore extends _sharedUi.ChangeLog {
166
64
  atoms = new Map();
167
- listeners = new Set();
168
65
  atomListeners = new Set();
169
- clearListeners = new Set();
170
- maxChanges = 200;
171
- idCounter = 0;
172
- isEnabled = true;
173
- captureSuppressed = false;
174
66
 
175
67
  // ---- Change Tracking ----
176
68
 
177
69
  addAtomChange(params) {
178
- if (!this.isEnabled || this.captureSuppressed) return;
70
+ if (!this.canRecord()) return;
179
71
  const {
180
72
  atomLabel,
181
73
  prevValue,
@@ -190,7 +82,7 @@ class JotaiStateStore {
190
82
  } = getValueDiffSummary(prevValue, nextValue);
191
83
  const valuePreview = formatValuePreview(nextValue);
192
84
  const change = {
193
- id: `${Date.now()}-${++this.idCounter}`,
85
+ id: this.nextId(),
194
86
  atomLabel,
195
87
  timestamp: Date.now(),
196
88
  prevValue,
@@ -203,7 +95,7 @@ class JotaiStateStore {
203
95
  valuePreview,
204
96
  isSlowUpdate: false
205
97
  };
206
- this.atomChanges = [change, ...this.atomChanges].slice(0, this.maxChanges);
98
+ this.push(change);
207
99
  const atomInfo = this.atoms.get(atomLabel);
208
100
  if (atomInfo) {
209
101
  atomInfo.changeCount++;
@@ -212,54 +104,21 @@ class JotaiStateStore {
212
104
  this.notifyAtomListeners();
213
105
  }
214
106
  getAtomChanges() {
215
- return [...this.atomChanges];
107
+ return this.getAll();
216
108
  }
217
109
  getAtomChangeById(id) {
218
- return this.atomChanges.find(c => c.id === id);
110
+ return this.getById(id);
219
111
  }
220
112
  clearAtomChanges() {
221
- this.atomChanges = [];
222
113
  for (const atom of this.atoms.values()) {
223
114
  atom.changeCount = 0;
224
115
  }
225
- this.notifyListeners();
116
+ this.clear();
226
117
  this.notifyAtomListeners();
227
- this.clearListeners.forEach(listener => {
228
- try {
229
- listener();
230
- } catch {
231
- // Ignore listener errors
232
- }
233
- });
234
- }
235
-
236
- /**
237
- * Listen for clearAtomChanges() calls. Used in remote mirror mode to
238
- * forward a clear performed in the dashboard UI to the synced device.
239
- */
240
- onClear(listener) {
241
- this.clearListeners.add(listener);
242
- return () => {
243
- this.clearListeners.delete(listener);
244
- };
245
118
  }
246
119
 
247
120
  // ---- Remote Mirror Mode ----
248
121
 
249
- /**
250
- * Permanently suppress local capture. Use when this store acts as a mirror
251
- * of a remote device's atom changes (e.g. the desktop dashboard): data
252
- * arrives via replaceFromSnapshot() and local watchers must never record.
253
- */
254
- disableCapture() {
255
- this.captureSuppressed = true;
256
- }
257
-
258
- /** Whether the store is in remote mirror mode (capture suppressed). */
259
- isCaptureSuppressed() {
260
- return this.captureSuppressed;
261
- }
262
-
263
122
  /**
264
123
  * Serializable snapshot of the tracked atoms (registry metadata plus each
265
124
  * atom's current value). Used by the sync adapter on the device side —
@@ -290,18 +149,18 @@ class JotaiStateStore {
290
149
  * freezes the mirror.
291
150
  */
292
151
  replaceFromSnapshot(changes, atomSnapshots) {
293
- if (!this.isEnabled) return;
152
+ if (!this.getEnabled()) return;
294
153
 
295
154
  // Preserve values a remote mirror already hydrated — wire-form snapshots
296
155
  // always carry markers, and a later snapshot must not blank the detail pane.
297
156
  const hydrated = new Map();
298
- for (const existing of this.atomChanges) {
157
+ for (const existing of this.items) {
299
158
  const prev = existing.prevValue;
300
159
  if (!prev || prev.__buoyValueOnDevice !== true) {
301
160
  hydrated.set(existing.id, existing);
302
161
  }
303
162
  }
304
- this.atomChanges = changes.slice(0, this.maxChanges).map(change => {
163
+ const merged = changes.slice(0, this.max).map(change => {
305
164
  const kept = hydrated.get(change.id);
306
165
  const incoming = change.prevValue;
307
166
  return kept && incoming?.__buoyValueOnDevice === true ? {
@@ -339,7 +198,7 @@ class JotaiStateStore {
339
198
  writeKind: snapshot.writeKind
340
199
  }];
341
200
  }));
342
- this.notifyListeners();
201
+ this.setItems(merged);
343
202
  this.notifyAtomListeners();
344
203
  }
345
204
 
@@ -348,13 +207,7 @@ class JotaiStateStore {
348
207
  * device via the adapter's `getChangeDetail`. Remote-mirror (desktop) only.
349
208
  */
350
209
  hydrateChange(id, patch) {
351
- const index = this.atomChanges.findIndex(c => c.id === id);
352
- if (index === -1) return;
353
- this.atomChanges = [...this.atomChanges.slice(0, index), {
354
- ...this.atomChanges[index],
355
- ...patch
356
- }, ...this.atomChanges.slice(index + 1)];
357
- this.notifyListeners();
210
+ this.hydrateById(id, patch);
358
211
  }
359
212
 
360
213
  /**
@@ -418,7 +271,7 @@ class JotaiStateStore {
418
271
  // ---- Filtering ----
419
272
 
420
273
  filterAtomChanges(filter) {
421
- let filtered = [...this.atomChanges];
274
+ let filtered = this.getAll();
422
275
  if (filter.searchText) {
423
276
  const search = filter.searchText.toLowerCase();
424
277
  filtered = filtered.filter(c => c.atomLabel.toLowerCase().includes(search) || c.valuePreview.toLowerCase().includes(search) || c.changedKeys.some(k => k.toLowerCase().includes(search)));
@@ -435,8 +288,8 @@ class JotaiStateStore {
435
288
  // ---- Stats ----
436
289
 
437
290
  getStats() {
438
- const total = this.atomChanges.length;
439
- const withChanges = this.atomChanges.filter(c => c.hasValueChange).length;
291
+ const total = this.items.length;
292
+ const withChanges = this.items.filter(c => c.hasValueChange).length;
440
293
  return {
441
294
  totalChanges: total,
442
295
  changesWithValueChange: withChanges,
@@ -450,34 +303,16 @@ class JotaiStateStore {
450
303
 
451
304
  // ---- Enable / Disable ----
452
305
 
453
- setEnabled(enabled) {
454
- this.isEnabled = enabled;
455
- }
456
- getEnabled() {
457
- return this.isEnabled;
458
- }
459
306
  setMaxChanges(max) {
460
- this.maxChanges = max;
461
- if (this.atomChanges.length > max) {
462
- this.atomChanges = this.atomChanges.slice(0, max);
463
- this.notifyListeners();
464
- }
307
+ this.setMax(max);
465
308
  }
466
309
 
467
310
  // ---- Subscriptions ----
468
311
 
469
- subscribe(listener) {
470
- this.listeners.add(listener);
471
- return () => this.listeners.delete(listener);
472
- }
473
312
  subscribeToAtoms(listener) {
474
313
  this.atomListeners.add(listener);
475
314
  return () => this.atomListeners.delete(listener);
476
315
  }
477
- notifyListeners() {
478
- const changes = this.getAtomChanges();
479
- this.listeners.forEach(listener => listener(changes));
480
- }
481
316
  notifyAtomListeners() {
482
317
  const atoms = this.getAtoms();
483
318
  this.atomListeners.forEach(listener => listener(atoms));
@@ -65,30 +65,14 @@ async function capture() {
65
65
  }
66
66
  async function restore(data, options) {
67
67
  const snapshot = data;
68
- const excludedSet = new Set(options?.excludeKeys ?? []);
69
- const includedSet = options?.includeKeys ? new Set(options.includeKeys) : null;
70
- const touch = key => !excludedSet.has(key) && (!includedSet || includedSet.has(key));
71
- let excludedHits = 0;
72
- const result = {
73
- ok: true,
74
- applied: 0,
75
- skipped: [],
76
- warnings: []
77
- };
68
+ const scope = (0, _sharedUi.createRestoreScope)(options);
69
+ const result = (0, _sharedUi.emptyRestoreResult)();
78
70
  if (!snapshot?.atoms) {
79
- return {
80
- ok: false,
81
- applied: 0,
82
- skipped: [],
83
- warnings: ["empty jotai snapshot"]
84
- };
71
+ return (0, _sharedUi.failedRestore)("empty jotai snapshot");
85
72
  }
86
73
  let derived = 0;
87
74
  for (const [label, entry] of Object.entries(snapshot.atoms)) {
88
- if (!touch(label)) {
89
- excludedHits += 1;
90
- continue;
91
- }
75
+ if (!scope.allows(label)) continue;
92
76
  if (!entry.writable) {
93
77
  derived += 1;
94
78
  continue;
@@ -122,9 +106,8 @@ async function restore(data, options) {
122
106
  if (derived > 0) {
123
107
  result.warnings.push(`${derived} derived atom(s) left to recompute`);
124
108
  }
125
- if (excludedHits > 0) {
126
- result.warnings.push(`${excludedHits} atom(s) excluded by you`);
127
- }
109
+ const excludedWarning = scope.excludedWarning("atom(s)");
110
+ if (excludedWarning) result.warnings.push(excludedWarning);
128
111
  return result;
129
112
  }
130
113
 
@@ -149,8 +132,7 @@ async function previewItems(target, base) {
149
132
  }
150
133
  const items = [];
151
134
  const notes = [];
152
- const labels = new Set([...Object.keys(baseAtoms), ...Object.keys(targetAtoms)]);
153
- for (const label of labels) {
135
+ for (const label of (0, _sharedUi.unionKeys)(baseAtoms, targetAtoms)) {
154
136
  const inTarget = label in targetAtoms;
155
137
  if (!inTarget) {
156
138
  if (liveMode) notes.push(`atom "${label}" not in snapshot — untouched`);else items.push({