@buoy-gg/storage 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.
@@ -3,9 +3,9 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.createStorageTool = createStorageTool;
7
- exports.storageToolPreset = void 0;
6
+ exports.storageToolPreset = exports.createStorageTool = void 0;
8
7
  var _floatingToolsCore = require("@buoy-gg/floating-tools-core");
8
+ var _sharedUi = require("@buoy-gg/shared-ui");
9
9
  var _StorageModalWithTabs = require("./storage/components/StorageModalWithTabs");
10
10
  var _jsxRuntime = require("react/jsx-runtime");
11
11
  /**
@@ -33,7 +33,7 @@ var _jsxRuntime = require("react/jsx-runtime");
33
33
  * - Live storage event monitoring
34
34
  * - Required key validation
35
35
  */
36
- const storageToolPreset = exports.storageToolPreset = {
36
+ const storageToolPreset = exports.storageToolPreset = (0, _sharedUi.defineToolPreset)({
37
37
  id: "storage",
38
38
  name: "STORAGE",
39
39
  description: "AsyncStorage browser",
@@ -48,7 +48,7 @@ const storageToolPreset = exports.storageToolPreset = {
48
48
  requiredStorageKeys: [],
49
49
  enableSharedModalDimensions: false
50
50
  }
51
- };
51
+ });
52
52
 
53
53
  /**
54
54
  * Create a custom AsyncStorage browser tool configuration.
@@ -74,21 +74,7 @@ const storageToolPreset = exports.storageToolPreset = {
74
74
  * });
75
75
  * ```
76
76
  */
77
- function createStorageTool(options) {
78
- return {
79
- id: options?.id || "storage",
80
- name: options?.name || "STORAGE",
81
- description: options?.description || "AsyncStorage browser",
82
- slot: "both",
83
- icon: ({
84
- size
85
- }) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_floatingToolsCore.StorageIcon, {
86
- size: size
87
- }),
88
- component: _StorageModalWithTabs.StorageModalWithTabs,
89
- props: {
90
- requiredStorageKeys: options?.requiredStorageKeys || [],
91
- enableSharedModalDimensions: options?.enableSharedModalDimensions !== undefined ? options.enableSharedModalDimensions : false
92
- }
93
- };
94
- }
77
+ const createStorageTool = exports.createStorageTool = (0, _sharedUi.createToolFactory)(storageToolPreset, options => ({
78
+ requiredStorageKeys: options.requiredStorageKeys || [],
79
+ enableSharedModalDimensions: options.enableSharedModalDimensions ?? false
80
+ }));
@@ -29,6 +29,41 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
29
29
  * because it is part of the package's public exports.
30
30
  */
31
31
  const FREE_TIER_EVENT_LIMIT = exports.FREE_TIER_EVENT_LIMIT = _sharedUi.TIER_LIMITS["storage.events"].free;
32
+ /**
33
+ * Persistence recipes. These keys were always written straight to
34
+ * AsyncStorage (not the FileSystem-first persistentStorage), so they keep
35
+ * that backend — a user's saved tab, filters and pins survive the upgrade.
36
+ */
37
+ const ASYNC_STORAGE_BACKEND = {
38
+ getItem: key => _asyncStorage.default.getItem(key),
39
+ setItem: (key, value) => _asyncStorage.default.setItem(key, value)
40
+ };
41
+ const TAB_PERSISTENCE = {
42
+ storage: ASYNC_STORAGE_BACKEND,
43
+ // Stored as the bare tab name, not JSON.
44
+ parse: raw => raw === "browser" || raw === "events" ? raw : undefined,
45
+ serialize: tab => tab
46
+ };
47
+ const BOOLEAN_PERSISTENCE = {
48
+ storage: ASYNC_STORAGE_BACKEND,
49
+ // Stored as "true" / "false".
50
+ parse: raw => raw === "true",
51
+ serialize: value => value.toString()
52
+ };
53
+ const STRING_SET_PERSISTENCE = {
54
+ storage: ASYNC_STORAGE_BACKEND,
55
+ parse: raw => new Set(JSON.parse(raw)),
56
+ serialize: set => JSON.stringify(Array.from(set))
57
+ };
58
+ const IGNORED_PATTERNS_PERSISTENCE = {
59
+ ...STRING_SET_PERSISTENCE,
60
+ parse: raw => {
61
+ const filters = JSON.parse(raw);
62
+ // Always ensure @react_buoy is included to hide dev tool keys
63
+ if (!filters.includes("@react_buoy")) filters.push("@react_buoy");
64
+ return new Set(filters);
65
+ }
66
+ };
32
67
  function StorageModalWithTabs({
33
68
  visible,
34
69
  onClose,
@@ -43,7 +78,7 @@ function StorageModalWithTabs({
43
78
  }) {
44
79
  const isPro = (0, _license.useIsPro)();
45
80
  const eventLimit = (0, _sharedUi.useLimit)("storage.events");
46
- const [activeTab, setActiveTab] = (0, _react.useState)("browser");
81
+ const [activeTab, setActiveTab] = (0, _sharedUi.usePersistedState)(_sharedUi.devToolsStorageKeys.storage.activeTab(), "browser", TAB_PERSISTENCE);
47
82
  (0, _react.useEffect)(() => {
48
83
  onTabChange?.(activeTab);
49
84
  }, [activeTab, onTabChange]);
@@ -59,7 +94,7 @@ function StorageModalWithTabs({
59
94
  const [isSearchActive, setIsSearchActive] = (0, _react.useState)(false);
60
95
 
61
96
  // Local state to control subscription (true = subscribed, false = unsubscribed)
62
- const [isListeningEnabled, setIsListeningEnabled] = (0, _react.useState)(true);
97
+ const [isListeningEnabled, setIsListeningEnabled] = (0, _sharedUi.usePersistedState)(_sharedUi.devToolsStorageKeys.storage.isMonitoring(), true, BOOLEAN_PERSISTENCE);
63
98
 
64
99
  // Event Listener state - using centralized store via hook
65
100
  // Pass enabled option to control subscription - this allows events tool to
@@ -76,8 +111,9 @@ function StorageModalWithTabs({
76
111
  const [selectedHistoryEventIndex, setSelectedHistoryEventIndex] = (0, _react.useState)(null);
77
112
  const [selectedEventIndex, setSelectedEventIndex] = (0, _react.useState)(0);
78
113
  const [showFilters, setShowFilters] = (0, _react.useState)(false);
79
- const [ignoredPatterns, setIgnoredPatterns] = (0, _react.useState)(new Set(["@react_buoy", "@RNAsyncStorage", "redux-persist", "persist:"]) // Auto-hide dev tool keys by default
80
- );
114
+ const [ignoredPatterns, setIgnoredPatterns] = (0, _sharedUi.usePersistedState)(_sharedUi.devToolsStorageKeys.storage.eventFilters(),
115
+ // Auto-hide dev tool keys by default
116
+ new Set(["@react_buoy", "@RNAsyncStorage", "redux-persist", "persist:"]), IGNORED_PATTERNS_PERSISTENCE);
81
117
  /**
82
118
  * Which backend both tabs are looking at.
83
119
  *
@@ -93,12 +129,8 @@ function StorageModalWithTabs({
93
129
  const [storageTypeFilter, setStorageTypeFilter] = (0, _react.useState)("all");
94
130
  const [enabledStorageTypes, setEnabledStorageTypes] = (0, _react.useState)(new Set(['async', 'mmkv', 'secure']) // All enabled by default
95
131
  );
96
- const [pinnedKeys, setPinnedKeys] = (0, _react.useState)(new Set());
132
+ const [pinnedKeys, setPinnedKeys] = (0, _sharedUi.usePersistedState)(_sharedUi.devToolsStorageKeys.storage.pinnedKeys(), new Set(), STRING_SET_PERSISTENCE);
97
133
  const lastEventRef = (0, _react.useRef)(null);
98
- const hasLoadedFilters = (0, _react.useRef)(false);
99
- const hasLoadedPins = (0, _react.useRef)(false);
100
- const hasLoadedTabState = (0, _react.useRef)(false);
101
- const hasLoadedMonitoringState = (0, _react.useRef)(false);
102
134
 
103
135
  // isListening = enabled and capturing (has subscribers)
104
136
  const isListening = isListeningEnabled && isCapturing;
@@ -113,140 +145,6 @@ function StorageModalWithTabs({
113
145
 
114
146
  // Timer removed - using useTickEveryMinute hook instead
115
147
 
116
- // Load persisted tab state on mount
117
- (0, _react.useEffect)(() => {
118
- if (!visible || hasLoadedTabState.current) return;
119
- const loadTabState = async () => {
120
- try {
121
- const storedTab = await _asyncStorage.default.getItem(_sharedUi.devToolsStorageKeys.storage.activeTab());
122
- if (storedTab && (storedTab === "browser" || storedTab === "events")) {
123
- setActiveTab(storedTab);
124
- }
125
- hasLoadedTabState.current = true;
126
- } catch (error) {
127
- // Failed to load tab state
128
- }
129
- };
130
- loadTabState();
131
- }, [visible]);
132
-
133
- // Load persisted monitoring state on mount
134
- (0, _react.useEffect)(() => {
135
- if (!visible || hasLoadedMonitoringState.current) return;
136
- const loadMonitoringState = async () => {
137
- try {
138
- const storedMonitoring = await _asyncStorage.default.getItem(_sharedUi.devToolsStorageKeys.storage.isMonitoring());
139
- if (storedMonitoring !== null) {
140
- const shouldMonitor = storedMonitoring === "true";
141
- setIsListeningEnabled(shouldMonitor);
142
- }
143
- hasLoadedMonitoringState.current = true;
144
- } catch (error) {
145
- // Failed to load monitoring state
146
- }
147
- };
148
- loadMonitoringState();
149
- }, [visible]);
150
-
151
- // Note: Conversations will appear when storage events are triggered
152
- // Click on any conversation to see the unified view with toggle cards
153
-
154
- // Save tab state when it changes
155
- (0, _react.useEffect)(() => {
156
- if (!hasLoadedTabState.current) return; // Don't save on initial load
157
-
158
- const saveTabState = async () => {
159
- try {
160
- await _asyncStorage.default.setItem(_sharedUi.devToolsStorageKeys.storage.activeTab(), activeTab);
161
- } catch (error) {
162
- // Failed to save tab state
163
- }
164
- };
165
- saveTabState();
166
- }, [activeTab]);
167
-
168
- // Save monitoring state when it changes
169
- (0, _react.useEffect)(() => {
170
- if (!hasLoadedMonitoringState.current) return; // Don't save on initial load
171
-
172
- const saveMonitoringState = async () => {
173
- try {
174
- await _asyncStorage.default.setItem(_sharedUi.devToolsStorageKeys.storage.isMonitoring(), isListening.toString());
175
- } catch (error) {
176
- // Failed to save monitoring state
177
- }
178
- };
179
- saveMonitoringState();
180
- }, [isListening]);
181
-
182
- // Load persisted filters on mount
183
- (0, _react.useEffect)(() => {
184
- if (!visible || hasLoadedFilters.current) return;
185
- const loadFilters = async () => {
186
- try {
187
- const storedFilters = await _asyncStorage.default.getItem(_sharedUi.devToolsStorageKeys.storage.eventFilters());
188
- if (storedFilters) {
189
- const filters = JSON.parse(storedFilters);
190
- // Always ensure @react_buoy is included to hide dev tool keys
191
- if (!filters.includes("@react_buoy")) {
192
- filters.push("@react_buoy");
193
- }
194
- setIgnoredPatterns(new Set(filters));
195
- }
196
- hasLoadedFilters.current = true;
197
- } catch (error) {
198
- // Failed to load filters
199
- }
200
- };
201
- loadFilters();
202
- }, [visible]);
203
-
204
- // Save filters when they change
205
- (0, _react.useEffect)(() => {
206
- if (!hasLoadedFilters.current) return; // Don't save on initial load
207
-
208
- const saveFilters = async () => {
209
- try {
210
- const filters = Array.from(ignoredPatterns);
211
- await _asyncStorage.default.setItem(_sharedUi.devToolsStorageKeys.storage.eventFilters(), JSON.stringify(filters));
212
- } catch (error) {
213
- // Failed to save filters
214
- }
215
- };
216
- saveFilters();
217
- }, [ignoredPatterns]);
218
-
219
- // Load persisted pinned keys on mount
220
- (0, _react.useEffect)(() => {
221
- if (!visible || hasLoadedPins.current) return;
222
- const loadPins = async () => {
223
- try {
224
- const stored = await _asyncStorage.default.getItem(_sharedUi.devToolsStorageKeys.storage.pinnedKeys());
225
- if (stored) {
226
- setPinnedKeys(new Set(JSON.parse(stored)));
227
- }
228
- hasLoadedPins.current = true;
229
- } catch (error) {
230
- // Failed to load pinned keys
231
- }
232
- };
233
- loadPins();
234
- }, [visible]);
235
-
236
- // Save pinned keys when they change
237
- (0, _react.useEffect)(() => {
238
- if (!hasLoadedPins.current) return; // Don't save on initial load
239
-
240
- const savePins = async () => {
241
- try {
242
- await _asyncStorage.default.setItem(_sharedUi.devToolsStorageKeys.storage.pinnedKeys(), JSON.stringify(Array.from(pinnedKeys)));
243
- } catch (error) {
244
- // Failed to save pinned keys
245
- }
246
- };
247
- savePins();
248
- }, [pinnedKeys]);
249
-
250
148
  // Event listener setup - now handled by useStorageEvents hook
251
149
  // The hook subscribes to the centralized storageEventStore
252
150
 
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.useStorageEvents = useStorageEvents;
7
7
  var _react = require("react");
8
+ var _sharedUi = require("@buoy-gg/shared-ui");
8
9
  var _storageEventStore = require("../stores/storageEventStore");
9
10
  /**
10
11
  * useStorageEvents Hook
@@ -39,60 +40,22 @@ function useStorageEvents(options = {}) {
39
40
  maxEvents = 500,
40
41
  enabled = true
41
42
  } = options;
42
- const [events, setEvents] = (0, _react.useState)([]);
43
- // Track capturing state locally so it updates reactively with the subscription
44
- const [isCapturing, setIsCapturing] = (0, _react.useState)(enabled);
45
-
46
- // Subscribe to store - this automatically starts/stops the storage listener
47
- // based on subscriber count (Subscribable pattern from TanStack Query)
48
- // Only subscribe when enabled is true
49
- (0, _react.useEffect)(() => {
50
- if (!enabled) {
51
- setIsCapturing(false);
52
- return;
53
- }
54
-
55
- // Frame-coalesced (same pattern as network's useNetworkEventList):
56
- // bursts of storage writes land as ONE commit per frame instead of
57
- // one per emission — the dominant live-churn cost measured on the
58
- // network tool (tool-bench 2026-07-06).
59
- let rafId = null;
60
- let pending = null;
61
- const unsubscribe = _storageEventStore.storageEventStore.subscribeToEvents(allEvents => {
62
- pending = allEvents;
63
- if (rafId != null) return;
64
- rafId = requestAnimationFrame(() => {
65
- rafId = null;
66
- if (!pending) return;
67
- // Filter by storage type if specified
68
- let filtered = pending;
69
- pending = null;
70
- if (storageType !== "all") {
71
- filtered = filtered.filter(e => e.storageType === storageType);
72
- }
73
- // Limit events
74
- if (filtered.length > maxEvents) {
75
- filtered = filtered.slice(0, maxEvents);
76
- }
77
- setEvents(filtered);
78
- });
79
- });
80
-
81
- // Mark as capturing after successful subscription
82
- setIsCapturing(true);
83
- return () => {
84
- unsubscribe();
85
- if (rafId != null) cancelAnimationFrame(rafId);
86
- // Note: Don't set isCapturing=false here because we might be
87
- // re-subscribing due to dependency change, not actually stopping
88
- };
89
- }, [storageType, maxEvents, enabled]);
90
- const clearEvents = (0, _react.useCallback)(() => {
91
- _storageEventStore.storageEventStore.clearEvents();
92
- }, []);
43
+ // Stable per storageType so the shared hook doesn't resubscribe every render.
44
+ const select = (0, _react.useMemo)(() => storageType === "all" ? undefined : events => events.filter(e => e.storageType === storageType), [storageType]);
45
+ // Subscribing auto-starts the storage listener (Subscribable pattern), and
46
+ // the shared hook frame-coalesces write bursts into one commit.
47
+ const {
48
+ events,
49
+ clearEvents,
50
+ isActive
51
+ } = (0, _sharedUi.useEventStoreEvents)(_storageEventStore.storageEventStore, {
52
+ maxEvents,
53
+ enabled,
54
+ select
55
+ });
93
56
  return {
94
57
  events,
95
58
  clearEvents,
96
- isCapturing
59
+ isCapturing: isActive
97
60
  };
98
61
  }
@@ -108,24 +108,12 @@ async function capture() {
108
108
  }
109
109
  async function restore(data, options) {
110
110
  const snapshot = data;
111
- const excludedSet = new Set(options?.excludeKeys ?? []);
112
- const includedSet = options?.includeKeys ? new Set(options.includeKeys) : null;
111
+ const scope = (0, _sharedUi.createRestoreScope)(options);
113
112
  /** True when this item may be touched (scope + exclusions agree). */
114
- const touch = key => !excludedSet.has(key) && (!includedSet || includedSet.has(key));
115
- let excludedHits = 0;
116
- const result = {
117
- ok: true,
118
- applied: 0,
119
- skipped: [],
120
- warnings: []
121
- };
113
+ const touch = scope.touch;
114
+ const result = (0, _sharedUi.emptyRestoreResult)();
122
115
  if (!snapshot) {
123
- return {
124
- ok: false,
125
- applied: 0,
126
- skipped: [],
127
- warnings: ["empty storage snapshot"]
128
- };
116
+ return (0, _sharedUi.failedRestore)("empty storage snapshot");
129
117
  }
130
118
 
131
119
  // ── AsyncStorage: delete app keys missing from the snapshot, write the rest ──
@@ -134,7 +122,7 @@ async function restore(data, options) {
134
122
  const snapKeys = new Set(snapPairs.map(([k]) => k));
135
123
  const liveKeys = (await _asyncStorage.default.getAllKeys()).filter(k => !(0, _sharedUi.isDevToolsStorageKey)(k));
136
124
  const toRemove = liveKeys.filter(k => !snapKeys.has(k) && touch(`async:${k}`));
137
- excludedHits += (snapshot.async ?? []).length - snapPairs.length;
125
+ scope.countExcluded((snapshot.async ?? []).length - snapPairs.length);
138
126
  if (toRemove.length > 0) await (0, _asyncStorageCompat.removeMany)(toRemove);
139
127
  if (snapPairs.length > 0) await (0, _asyncStorageCompat.writeMany)(snapPairs);
140
128
  result.applied += snapPairs.length + toRemove.length;
@@ -167,10 +155,7 @@ async function restore(data, options) {
167
155
  const snapKeys = new Set(entries.map(e => e.key));
168
156
  const liveKeys = instance.getAllKeys().filter(key => !(0, _sharedUi.isDevToolsStorageKey)(key));
169
157
  for (const key of liveKeys) {
170
- if (!touch(`mmkv:${id}/${key}`)) {
171
- excludedHits += 1;
172
- continue;
173
- }
158
+ if (!scope.allows(`mmkv:${id}/${key}`)) continue;
174
159
  if (!snapKeys.has(key)) {
175
160
  deleteMMKVKey(instance, key);
176
161
  result.applied += 1;
@@ -201,10 +186,7 @@ async function restore(data, options) {
201
186
  key,
202
187
  options
203
188
  } of _SecureStoreRegistry.secureStoreRegistry.getAll()) {
204
- if (!touch(`secure:${key}`)) {
205
- excludedHits += 1;
206
- continue;
207
- }
189
+ if (!scope.allows(`secure:${key}`)) continue;
208
190
  if (options.requireAuthentication) {
209
191
  if (key in secureSnap) {
210
192
  result.skipped.push({
@@ -237,20 +219,14 @@ async function restore(data, options) {
237
219
  });
238
220
  }
239
221
  }
240
- if (excludedHits > 0) {
241
- result.warnings.push(`${excludedHits} item(s) excluded by you`);
242
- }
222
+ const excludedWarning = scope.excludedWarning("item(s)");
223
+ if (excludedWarning) result.warnings.push(excludedWarning);
243
224
  return result;
244
225
  }
245
226
 
246
227
  /** Wipe to fresh-install state (Buoy keys preserved). Used by baseline restore. */
247
228
  async function clear() {
248
- const result = {
249
- ok: true,
250
- applied: 0,
251
- skipped: [],
252
- warnings: []
253
- };
229
+ const result = (0, _sharedUi.emptyRestoreResult)();
254
230
  try {
255
231
  await (0, _clearAllStorage.clearAllAppStorage)();
256
232
  result.applied += 1;
@@ -301,40 +277,24 @@ async function clear() {
301
277
  return result;
302
278
  }
303
279
  const ITEM_RENDER_CHAR_LIMIT = 256 * 1024;
280
+
281
+ /**
282
+ * Storage values are strings (or MMKV primitives): identity equality and a
283
+ * character-length size gate are the right tests, not a deep walk.
284
+ */
304
285
  function classifyValue(key, label, oldValue, newValue, inOld, inNew) {
305
- if (!inNew) return {
286
+ return (0, _sharedUi.classifyPreviewItem)({
306
287
  key,
307
288
  label,
308
- verdict: "removed",
309
- oldValue
310
- };
311
- if (!inOld) return {
312
- key,
313
- label,
314
- verdict: "added",
315
- newValue
316
- };
317
- if (Object.is(oldValue, newValue)) return {
318
- key,
319
- label,
320
- verdict: "unchanged"
321
- };
322
- const tooLarge = typeof oldValue === "string" && oldValue.length > ITEM_RENDER_CHAR_LIMIT || typeof newValue === "string" && newValue.length > ITEM_RENDER_CHAR_LIMIT;
323
- if (tooLarge) {
324
- return {
325
- key,
326
- label,
327
- verdict: "too-large",
328
- reason: "value too large to render a diff — restore still applies it"
329
- };
330
- }
331
- return {
332
- key,
333
- label,
334
- verdict: "changed",
335
289
  oldValue,
336
- newValue
337
- };
290
+ newValue,
291
+ inOld,
292
+ inNew
293
+ }, {
294
+ equal: Object.is,
295
+ tooLarge: value => typeof value === "string" && value.length > ITEM_RENDER_CHAR_LIMIT,
296
+ tooLargeReason: "value too large to render a diff — restore still applies it"
297
+ });
338
298
  }
339
299
 
340
300
  /** Per-key restore preview across all three backends. */
@@ -356,46 +316,31 @@ async function previewItems(target, base) {
356
316
  // ── AsyncStorage ──
357
317
  const oldAsync = new Map(baseData.async ?? []);
358
318
  const newAsync = new Map(targetData.async ?? []);
359
- for (const key of new Set([...oldAsync.keys(), ...newAsync.keys()])) {
319
+ for (const key of (0, _sharedUi.unionKeys)(oldAsync, newAsync)) {
360
320
  if ((0, _sharedUi.isDevToolsStorageKey)(key)) continue;
361
321
  items.push(classifyValue(`async:${key}`, key, oldAsync.get(key), newAsync.get(key), oldAsync.has(key), newAsync.has(key)));
362
322
  }
363
323
 
364
324
  // ── MMKV ──
365
- const instanceIds = new Set([...Object.keys(baseData.mmkv ?? {}), ...Object.keys(targetData.mmkv ?? {})]);
366
- for (const id of instanceIds) {
325
+ for (const id of (0, _sharedUi.unionKeys)(baseData.mmkv, targetData.mmkv)) {
367
326
  const registered = liveMode ? _MMKVInstanceRegistry.mmkvInstanceRegistry.get(id) : null;
368
327
  const instanceUnavailable = liveMode && !registered;
369
328
  const readOnly = liveMode && registered?.instance.isReadOnly === true;
370
329
  const oldEntries = new Map((baseData.mmkv?.[id] ?? []).map(e => [e.key, e]));
371
330
  const newEntries = new Map((targetData.mmkv?.[id] ?? []).map(e => [e.key, e]));
372
- for (const key of new Set([...oldEntries.keys(), ...newEntries.keys()])) {
331
+ for (const key of (0, _sharedUi.unionKeys)(oldEntries, newEntries)) {
373
332
  if ((0, _sharedUi.isDevToolsStorageKey)(key)) continue;
374
333
  const itemKey = `mmkv:${id}/${key}`;
375
334
  const label = `${id} · ${key}`;
376
335
  const oldE = oldEntries.get(key);
377
336
  const newE = newEntries.get(key);
378
337
  let item = classifyValue(itemKey, label, oldE?.value, newE?.value, oldEntries.has(key), newEntries.has(key));
379
- if (item.verdict !== "unchanged") {
380
- if (instanceUnavailable) {
381
- item = {
382
- ...item,
383
- verdict: "wont-apply",
384
- reason: "MMKV instance not registered"
385
- };
386
- } else if (readOnly) {
387
- item = {
388
- ...item,
389
- verdict: "wont-apply",
390
- reason: "MMKV instance registered read-only"
391
- };
392
- } else if (newE?.valueType === "buffer" || oldE?.valueType === "buffer") {
393
- item = {
394
- ...item,
395
- verdict: "wont-apply",
396
- reason: "ArrayBuffer values cannot be restored"
397
- };
398
- }
338
+ if (instanceUnavailable) {
339
+ item = (0, _sharedUi.wontApply)(item, "MMKV instance not registered");
340
+ } else if (readOnly) {
341
+ item = (0, _sharedUi.wontApply)(item, "MMKV instance registered read-only");
342
+ } else if (newE?.valueType === "buffer" || oldE?.valueType === "buffer") {
343
+ item = (0, _sharedUi.wontApply)(item, "ArrayBuffer values cannot be restored");
399
344
  }
400
345
  items.push(item);
401
346
  }
@@ -405,22 +350,14 @@ async function previewItems(target, base) {
405
350
  const oldSecure = baseData.secure ?? {};
406
351
  const newSecure = targetData.secure ?? {};
407
352
  const registeredSecure = liveMode ? new Map(_SecureStoreRegistry.secureStoreRegistry.getAll().map(e => [e.key, e])) : null;
408
- for (const key of new Set([...Object.keys(oldSecure), ...Object.keys(newSecure)])) {
353
+ for (const key of (0, _sharedUi.unionKeys)(oldSecure, newSecure)) {
409
354
  let item = classifyValue(`secure:${key}`, key, oldSecure[key], newSecure[key], key in oldSecure, key in newSecure);
410
- if (liveMode && item.verdict !== "unchanged") {
355
+ if (liveMode) {
411
356
  const entry = registeredSecure?.get(key);
412
357
  if (!entry) {
413
- item = {
414
- ...item,
415
- verdict: "wont-apply",
416
- reason: "key no longer registered with registerSecureStoreKeys"
417
- };
358
+ item = (0, _sharedUi.wontApply)(item, "key no longer registered with registerSecureStoreKeys");
418
359
  } else if (entry.options.requireAuthentication) {
419
- item = {
420
- ...item,
421
- verdict: "wont-apply",
422
- reason: "biometric-protected keys are never written"
423
- };
360
+ item = (0, _sharedUi.wontApply)(item, "biometric-protected keys are never written");
424
361
  }
425
362
  }
426
363
  items.push(item);
@@ -18,6 +18,7 @@
18
18
  */
19
19
 
20
20
  import { StorageIcon } from "@buoy-gg/floating-tools-core";
21
+ import { createToolFactory, defineToolPreset } from "@buoy-gg/shared-ui";
21
22
  import { StorageModalWithTabs } from "./storage/components/StorageModalWithTabs";
22
23
  import { jsx as _jsx } from "react/jsx-runtime";
23
24
  /**
@@ -28,7 +29,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
28
29
  * - Live storage event monitoring
29
30
  * - Required key validation
30
31
  */
31
- export const storageToolPreset = {
32
+ export const storageToolPreset = defineToolPreset({
32
33
  id: "storage",
33
34
  name: "STORAGE",
34
35
  description: "AsyncStorage browser",
@@ -43,7 +44,7 @@ export const storageToolPreset = {
43
44
  requiredStorageKeys: [],
44
45
  enableSharedModalDimensions: false
45
46
  }
46
- };
47
+ });
47
48
 
48
49
  /**
49
50
  * Create a custom AsyncStorage browser tool configuration.
@@ -69,21 +70,7 @@ export const storageToolPreset = {
69
70
  * });
70
71
  * ```
71
72
  */
72
- export function createStorageTool(options) {
73
- return {
74
- id: options?.id || "storage",
75
- name: options?.name || "STORAGE",
76
- description: options?.description || "AsyncStorage browser",
77
- slot: "both",
78
- icon: ({
79
- size
80
- }) => /*#__PURE__*/_jsx(StorageIcon, {
81
- size: size
82
- }),
83
- component: StorageModalWithTabs,
84
- props: {
85
- requiredStorageKeys: options?.requiredStorageKeys || [],
86
- enableSharedModalDimensions: options?.enableSharedModalDimensions !== undefined ? options.enableSharedModalDimensions : false
87
- }
88
- };
89
- }
73
+ export const createStorageTool = createToolFactory(storageToolPreset, options => ({
74
+ requiredStorageKeys: options.requiredStorageKeys || [],
75
+ enableSharedModalDimensions: options.enableSharedModalDimensions ?? false
76
+ }));
@@ -3,7 +3,7 @@
3
3
  import { useState, useCallback, useEffect, useRef, useMemo } from "react";
4
4
  import { View, TouchableOpacity, StyleSheet, FlatList, Alert } from "react-native";
5
5
  import AsyncStorage from "@react-native-async-storage/async-storage";
6
- import { JsModal, ModalHeader, TabSelector, parseValue, devToolsStorageKeys, macOSColors, night, ToolEmptyState, Filter, RefreshCw, Search, SearchBar, PowerToggleButton, ToolbarCopyButton, ToolbarClearButton, truncatePayload, TickProvider, useLimit, TIER_LIMITS, windowDragProps } from "@buoy-gg/shared-ui";
6
+ import { JsModal, ModalHeader, TabSelector, parseValue, devToolsStorageKeys, macOSColors, night, ToolEmptyState, Filter, RefreshCw, Search, SearchBar, PowerToggleButton, ToolbarCopyButton, ToolbarClearButton, truncatePayload, TickProvider, useLimit, TIER_LIMITS, windowDragProps, usePersistedState } from "@buoy-gg/shared-ui";
7
7
  import { StorageBrowserMode } from "./StorageBrowserMode";
8
8
  import { StorageTypeFilterBar, countByStorageType } from "./StorageTypeFilterBar";
9
9
  import { clearAllAppStorage, clearAllStorageIncludingDevTools } from "../utils/clearAllStorage";
@@ -24,6 +24,41 @@ import { useIsPro } from "@buoy-gg/license";
24
24
  */
25
25
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
26
26
  export const FREE_TIER_EVENT_LIMIT = TIER_LIMITS["storage.events"].free;
27
+ /**
28
+ * Persistence recipes. These keys were always written straight to
29
+ * AsyncStorage (not the FileSystem-first persistentStorage), so they keep
30
+ * that backend — a user's saved tab, filters and pins survive the upgrade.
31
+ */
32
+ const ASYNC_STORAGE_BACKEND = {
33
+ getItem: key => AsyncStorage.getItem(key),
34
+ setItem: (key, value) => AsyncStorage.setItem(key, value)
35
+ };
36
+ const TAB_PERSISTENCE = {
37
+ storage: ASYNC_STORAGE_BACKEND,
38
+ // Stored as the bare tab name, not JSON.
39
+ parse: raw => raw === "browser" || raw === "events" ? raw : undefined,
40
+ serialize: tab => tab
41
+ };
42
+ const BOOLEAN_PERSISTENCE = {
43
+ storage: ASYNC_STORAGE_BACKEND,
44
+ // Stored as "true" / "false".
45
+ parse: raw => raw === "true",
46
+ serialize: value => value.toString()
47
+ };
48
+ const STRING_SET_PERSISTENCE = {
49
+ storage: ASYNC_STORAGE_BACKEND,
50
+ parse: raw => new Set(JSON.parse(raw)),
51
+ serialize: set => JSON.stringify(Array.from(set))
52
+ };
53
+ const IGNORED_PATTERNS_PERSISTENCE = {
54
+ ...STRING_SET_PERSISTENCE,
55
+ parse: raw => {
56
+ const filters = JSON.parse(raw);
57
+ // Always ensure @react_buoy is included to hide dev tool keys
58
+ if (!filters.includes("@react_buoy")) filters.push("@react_buoy");
59
+ return new Set(filters);
60
+ }
61
+ };
27
62
  export function StorageModalWithTabs({
28
63
  visible,
29
64
  onClose,
@@ -38,7 +73,7 @@ export function StorageModalWithTabs({
38
73
  }) {
39
74
  const isPro = useIsPro();
40
75
  const eventLimit = useLimit("storage.events");
41
- const [activeTab, setActiveTab] = useState("browser");
76
+ const [activeTab, setActiveTab] = usePersistedState(devToolsStorageKeys.storage.activeTab(), "browser", TAB_PERSISTENCE);
42
77
  useEffect(() => {
43
78
  onTabChange?.(activeTab);
44
79
  }, [activeTab, onTabChange]);
@@ -54,7 +89,7 @@ export function StorageModalWithTabs({
54
89
  const [isSearchActive, setIsSearchActive] = useState(false);
55
90
 
56
91
  // Local state to control subscription (true = subscribed, false = unsubscribed)
57
- const [isListeningEnabled, setIsListeningEnabled] = useState(true);
92
+ const [isListeningEnabled, setIsListeningEnabled] = usePersistedState(devToolsStorageKeys.storage.isMonitoring(), true, BOOLEAN_PERSISTENCE);
58
93
 
59
94
  // Event Listener state - using centralized store via hook
60
95
  // Pass enabled option to control subscription - this allows events tool to
@@ -71,8 +106,9 @@ export function StorageModalWithTabs({
71
106
  const [selectedHistoryEventIndex, setSelectedHistoryEventIndex] = useState(null);
72
107
  const [selectedEventIndex, setSelectedEventIndex] = useState(0);
73
108
  const [showFilters, setShowFilters] = useState(false);
74
- const [ignoredPatterns, setIgnoredPatterns] = useState(new Set(["@react_buoy", "@RNAsyncStorage", "redux-persist", "persist:"]) // Auto-hide dev tool keys by default
75
- );
109
+ const [ignoredPatterns, setIgnoredPatterns] = usePersistedState(devToolsStorageKeys.storage.eventFilters(),
110
+ // Auto-hide dev tool keys by default
111
+ new Set(["@react_buoy", "@RNAsyncStorage", "redux-persist", "persist:"]), IGNORED_PATTERNS_PERSISTENCE);
76
112
  /**
77
113
  * Which backend both tabs are looking at.
78
114
  *
@@ -88,12 +124,8 @@ export function StorageModalWithTabs({
88
124
  const [storageTypeFilter, setStorageTypeFilter] = useState("all");
89
125
  const [enabledStorageTypes, setEnabledStorageTypes] = useState(new Set(['async', 'mmkv', 'secure']) // All enabled by default
90
126
  );
91
- const [pinnedKeys, setPinnedKeys] = useState(new Set());
127
+ const [pinnedKeys, setPinnedKeys] = usePersistedState(devToolsStorageKeys.storage.pinnedKeys(), new Set(), STRING_SET_PERSISTENCE);
92
128
  const lastEventRef = useRef(null);
93
- const hasLoadedFilters = useRef(false);
94
- const hasLoadedPins = useRef(false);
95
- const hasLoadedTabState = useRef(false);
96
- const hasLoadedMonitoringState = useRef(false);
97
129
 
98
130
  // isListening = enabled and capturing (has subscribers)
99
131
  const isListening = isListeningEnabled && isCapturing;
@@ -108,140 +140,6 @@ export function StorageModalWithTabs({
108
140
 
109
141
  // Timer removed - using useTickEveryMinute hook instead
110
142
 
111
- // Load persisted tab state on mount
112
- useEffect(() => {
113
- if (!visible || hasLoadedTabState.current) return;
114
- const loadTabState = async () => {
115
- try {
116
- const storedTab = await AsyncStorage.getItem(devToolsStorageKeys.storage.activeTab());
117
- if (storedTab && (storedTab === "browser" || storedTab === "events")) {
118
- setActiveTab(storedTab);
119
- }
120
- hasLoadedTabState.current = true;
121
- } catch (error) {
122
- // Failed to load tab state
123
- }
124
- };
125
- loadTabState();
126
- }, [visible]);
127
-
128
- // Load persisted monitoring state on mount
129
- useEffect(() => {
130
- if (!visible || hasLoadedMonitoringState.current) return;
131
- const loadMonitoringState = async () => {
132
- try {
133
- const storedMonitoring = await AsyncStorage.getItem(devToolsStorageKeys.storage.isMonitoring());
134
- if (storedMonitoring !== null) {
135
- const shouldMonitor = storedMonitoring === "true";
136
- setIsListeningEnabled(shouldMonitor);
137
- }
138
- hasLoadedMonitoringState.current = true;
139
- } catch (error) {
140
- // Failed to load monitoring state
141
- }
142
- };
143
- loadMonitoringState();
144
- }, [visible]);
145
-
146
- // Note: Conversations will appear when storage events are triggered
147
- // Click on any conversation to see the unified view with toggle cards
148
-
149
- // Save tab state when it changes
150
- useEffect(() => {
151
- if (!hasLoadedTabState.current) return; // Don't save on initial load
152
-
153
- const saveTabState = async () => {
154
- try {
155
- await AsyncStorage.setItem(devToolsStorageKeys.storage.activeTab(), activeTab);
156
- } catch (error) {
157
- // Failed to save tab state
158
- }
159
- };
160
- saveTabState();
161
- }, [activeTab]);
162
-
163
- // Save monitoring state when it changes
164
- useEffect(() => {
165
- if (!hasLoadedMonitoringState.current) return; // Don't save on initial load
166
-
167
- const saveMonitoringState = async () => {
168
- try {
169
- await AsyncStorage.setItem(devToolsStorageKeys.storage.isMonitoring(), isListening.toString());
170
- } catch (error) {
171
- // Failed to save monitoring state
172
- }
173
- };
174
- saveMonitoringState();
175
- }, [isListening]);
176
-
177
- // Load persisted filters on mount
178
- useEffect(() => {
179
- if (!visible || hasLoadedFilters.current) return;
180
- const loadFilters = async () => {
181
- try {
182
- const storedFilters = await AsyncStorage.getItem(devToolsStorageKeys.storage.eventFilters());
183
- if (storedFilters) {
184
- const filters = JSON.parse(storedFilters);
185
- // Always ensure @react_buoy is included to hide dev tool keys
186
- if (!filters.includes("@react_buoy")) {
187
- filters.push("@react_buoy");
188
- }
189
- setIgnoredPatterns(new Set(filters));
190
- }
191
- hasLoadedFilters.current = true;
192
- } catch (error) {
193
- // Failed to load filters
194
- }
195
- };
196
- loadFilters();
197
- }, [visible]);
198
-
199
- // Save filters when they change
200
- useEffect(() => {
201
- if (!hasLoadedFilters.current) return; // Don't save on initial load
202
-
203
- const saveFilters = async () => {
204
- try {
205
- const filters = Array.from(ignoredPatterns);
206
- await AsyncStorage.setItem(devToolsStorageKeys.storage.eventFilters(), JSON.stringify(filters));
207
- } catch (error) {
208
- // Failed to save filters
209
- }
210
- };
211
- saveFilters();
212
- }, [ignoredPatterns]);
213
-
214
- // Load persisted pinned keys on mount
215
- useEffect(() => {
216
- if (!visible || hasLoadedPins.current) return;
217
- const loadPins = async () => {
218
- try {
219
- const stored = await AsyncStorage.getItem(devToolsStorageKeys.storage.pinnedKeys());
220
- if (stored) {
221
- setPinnedKeys(new Set(JSON.parse(stored)));
222
- }
223
- hasLoadedPins.current = true;
224
- } catch (error) {
225
- // Failed to load pinned keys
226
- }
227
- };
228
- loadPins();
229
- }, [visible]);
230
-
231
- // Save pinned keys when they change
232
- useEffect(() => {
233
- if (!hasLoadedPins.current) return; // Don't save on initial load
234
-
235
- const savePins = async () => {
236
- try {
237
- await AsyncStorage.setItem(devToolsStorageKeys.storage.pinnedKeys(), JSON.stringify(Array.from(pinnedKeys)));
238
- } catch (error) {
239
- // Failed to save pinned keys
240
- }
241
- };
242
- savePins();
243
- }, [pinnedKeys]);
244
-
245
143
  // Event listener setup - now handled by useStorageEvents hook
246
144
  // The hook subscribes to the centralized storageEventStore
247
145
 
@@ -25,7 +25,8 @@
25
25
  * ```
26
26
  */
27
27
 
28
- import { useState, useEffect, useCallback } from "react";
28
+ import { useMemo } from "react";
29
+ import { useEventStoreEvents } from "@buoy-gg/shared-ui";
29
30
  import { storageEventStore } from "../stores/storageEventStore";
30
31
 
31
32
  // Re-export StorageEvent for convenience
@@ -36,60 +37,22 @@ export function useStorageEvents(options = {}) {
36
37
  maxEvents = 500,
37
38
  enabled = true
38
39
  } = options;
39
- const [events, setEvents] = useState([]);
40
- // Track capturing state locally so it updates reactively with the subscription
41
- const [isCapturing, setIsCapturing] = useState(enabled);
42
-
43
- // Subscribe to store - this automatically starts/stops the storage listener
44
- // based on subscriber count (Subscribable pattern from TanStack Query)
45
- // Only subscribe when enabled is true
46
- useEffect(() => {
47
- if (!enabled) {
48
- setIsCapturing(false);
49
- return;
50
- }
51
-
52
- // Frame-coalesced (same pattern as network's useNetworkEventList):
53
- // bursts of storage writes land as ONE commit per frame instead of
54
- // one per emission — the dominant live-churn cost measured on the
55
- // network tool (tool-bench 2026-07-06).
56
- let rafId = null;
57
- let pending = null;
58
- const unsubscribe = storageEventStore.subscribeToEvents(allEvents => {
59
- pending = allEvents;
60
- if (rafId != null) return;
61
- rafId = requestAnimationFrame(() => {
62
- rafId = null;
63
- if (!pending) return;
64
- // Filter by storage type if specified
65
- let filtered = pending;
66
- pending = null;
67
- if (storageType !== "all") {
68
- filtered = filtered.filter(e => e.storageType === storageType);
69
- }
70
- // Limit events
71
- if (filtered.length > maxEvents) {
72
- filtered = filtered.slice(0, maxEvents);
73
- }
74
- setEvents(filtered);
75
- });
76
- });
77
-
78
- // Mark as capturing after successful subscription
79
- setIsCapturing(true);
80
- return () => {
81
- unsubscribe();
82
- if (rafId != null) cancelAnimationFrame(rafId);
83
- // Note: Don't set isCapturing=false here because we might be
84
- // re-subscribing due to dependency change, not actually stopping
85
- };
86
- }, [storageType, maxEvents, enabled]);
87
- const clearEvents = useCallback(() => {
88
- storageEventStore.clearEvents();
89
- }, []);
40
+ // Stable per storageType so the shared hook doesn't resubscribe every render.
41
+ const select = useMemo(() => storageType === "all" ? undefined : events => events.filter(e => e.storageType === storageType), [storageType]);
42
+ // Subscribing auto-starts the storage listener (Subscribable pattern), and
43
+ // the shared hook frame-coalesces write bursts into one commit.
44
+ const {
45
+ events,
46
+ clearEvents,
47
+ isActive
48
+ } = useEventStoreEvents(storageEventStore, {
49
+ maxEvents,
50
+ enabled,
51
+ select
52
+ });
90
53
  return {
91
54
  events,
92
55
  clearEvents,
93
- isCapturing
56
+ isCapturing: isActive
94
57
  };
95
58
  }
@@ -18,7 +18,7 @@
18
18
  * - SecureStore keys outside the registered descriptor list are invisible.
19
19
  */
20
20
  import AsyncStorage from "@react-native-async-storage/async-storage";
21
- import { isDevToolsStorageKey, registerStateSnapshotProvider } from "@buoy-gg/shared-ui";
21
+ import { classifyPreviewItem, createRestoreScope, emptyRestoreResult, failedRestore, isDevToolsStorageKey, registerStateSnapshotProvider, unionKeys, wontApply } from "@buoy-gg/shared-ui";
22
22
  import { readMany, writeMany, removeMany } from "./asyncStorageCompat";
23
23
  import { clearAllAppStorage } from "./clearAllStorage";
24
24
  import { mmkvInstanceRegistry } from "./MMKVInstanceRegistry";
@@ -106,24 +106,12 @@ async function capture() {
106
106
  }
107
107
  async function restore(data, options) {
108
108
  const snapshot = data;
109
- const excludedSet = new Set(options?.excludeKeys ?? []);
110
- const includedSet = options?.includeKeys ? new Set(options.includeKeys) : null;
109
+ const scope = createRestoreScope(options);
111
110
  /** True when this item may be touched (scope + exclusions agree). */
112
- const touch = key => !excludedSet.has(key) && (!includedSet || includedSet.has(key));
113
- let excludedHits = 0;
114
- const result = {
115
- ok: true,
116
- applied: 0,
117
- skipped: [],
118
- warnings: []
119
- };
111
+ const touch = scope.touch;
112
+ const result = emptyRestoreResult();
120
113
  if (!snapshot) {
121
- return {
122
- ok: false,
123
- applied: 0,
124
- skipped: [],
125
- warnings: ["empty storage snapshot"]
126
- };
114
+ return failedRestore("empty storage snapshot");
127
115
  }
128
116
 
129
117
  // ── AsyncStorage: delete app keys missing from the snapshot, write the rest ──
@@ -132,7 +120,7 @@ async function restore(data, options) {
132
120
  const snapKeys = new Set(snapPairs.map(([k]) => k));
133
121
  const liveKeys = (await AsyncStorage.getAllKeys()).filter(k => !isDevToolsStorageKey(k));
134
122
  const toRemove = liveKeys.filter(k => !snapKeys.has(k) && touch(`async:${k}`));
135
- excludedHits += (snapshot.async ?? []).length - snapPairs.length;
123
+ scope.countExcluded((snapshot.async ?? []).length - snapPairs.length);
136
124
  if (toRemove.length > 0) await removeMany(toRemove);
137
125
  if (snapPairs.length > 0) await writeMany(snapPairs);
138
126
  result.applied += snapPairs.length + toRemove.length;
@@ -165,10 +153,7 @@ async function restore(data, options) {
165
153
  const snapKeys = new Set(entries.map(e => e.key));
166
154
  const liveKeys = instance.getAllKeys().filter(key => !isDevToolsStorageKey(key));
167
155
  for (const key of liveKeys) {
168
- if (!touch(`mmkv:${id}/${key}`)) {
169
- excludedHits += 1;
170
- continue;
171
- }
156
+ if (!scope.allows(`mmkv:${id}/${key}`)) continue;
172
157
  if (!snapKeys.has(key)) {
173
158
  deleteMMKVKey(instance, key);
174
159
  result.applied += 1;
@@ -199,10 +184,7 @@ async function restore(data, options) {
199
184
  key,
200
185
  options
201
186
  } of secureStoreRegistry.getAll()) {
202
- if (!touch(`secure:${key}`)) {
203
- excludedHits += 1;
204
- continue;
205
- }
187
+ if (!scope.allows(`secure:${key}`)) continue;
206
188
  if (options.requireAuthentication) {
207
189
  if (key in secureSnap) {
208
190
  result.skipped.push({
@@ -235,20 +217,14 @@ async function restore(data, options) {
235
217
  });
236
218
  }
237
219
  }
238
- if (excludedHits > 0) {
239
- result.warnings.push(`${excludedHits} item(s) excluded by you`);
240
- }
220
+ const excludedWarning = scope.excludedWarning("item(s)");
221
+ if (excludedWarning) result.warnings.push(excludedWarning);
241
222
  return result;
242
223
  }
243
224
 
244
225
  /** Wipe to fresh-install state (Buoy keys preserved). Used by baseline restore. */
245
226
  async function clear() {
246
- const result = {
247
- ok: true,
248
- applied: 0,
249
- skipped: [],
250
- warnings: []
251
- };
227
+ const result = emptyRestoreResult();
252
228
  try {
253
229
  await clearAllAppStorage();
254
230
  result.applied += 1;
@@ -299,40 +275,24 @@ async function clear() {
299
275
  return result;
300
276
  }
301
277
  const ITEM_RENDER_CHAR_LIMIT = 256 * 1024;
278
+
279
+ /**
280
+ * Storage values are strings (or MMKV primitives): identity equality and a
281
+ * character-length size gate are the right tests, not a deep walk.
282
+ */
302
283
  function classifyValue(key, label, oldValue, newValue, inOld, inNew) {
303
- if (!inNew) return {
284
+ return classifyPreviewItem({
304
285
  key,
305
286
  label,
306
- verdict: "removed",
307
- oldValue
308
- };
309
- if (!inOld) return {
310
- key,
311
- label,
312
- verdict: "added",
313
- newValue
314
- };
315
- if (Object.is(oldValue, newValue)) return {
316
- key,
317
- label,
318
- verdict: "unchanged"
319
- };
320
- const tooLarge = typeof oldValue === "string" && oldValue.length > ITEM_RENDER_CHAR_LIMIT || typeof newValue === "string" && newValue.length > ITEM_RENDER_CHAR_LIMIT;
321
- if (tooLarge) {
322
- return {
323
- key,
324
- label,
325
- verdict: "too-large",
326
- reason: "value too large to render a diff — restore still applies it"
327
- };
328
- }
329
- return {
330
- key,
331
- label,
332
- verdict: "changed",
333
287
  oldValue,
334
- newValue
335
- };
288
+ newValue,
289
+ inOld,
290
+ inNew
291
+ }, {
292
+ equal: Object.is,
293
+ tooLarge: value => typeof value === "string" && value.length > ITEM_RENDER_CHAR_LIMIT,
294
+ tooLargeReason: "value too large to render a diff — restore still applies it"
295
+ });
336
296
  }
337
297
 
338
298
  /** Per-key restore preview across all three backends. */
@@ -354,46 +314,31 @@ async function previewItems(target, base) {
354
314
  // ── AsyncStorage ──
355
315
  const oldAsync = new Map(baseData.async ?? []);
356
316
  const newAsync = new Map(targetData.async ?? []);
357
- for (const key of new Set([...oldAsync.keys(), ...newAsync.keys()])) {
317
+ for (const key of unionKeys(oldAsync, newAsync)) {
358
318
  if (isDevToolsStorageKey(key)) continue;
359
319
  items.push(classifyValue(`async:${key}`, key, oldAsync.get(key), newAsync.get(key), oldAsync.has(key), newAsync.has(key)));
360
320
  }
361
321
 
362
322
  // ── MMKV ──
363
- const instanceIds = new Set([...Object.keys(baseData.mmkv ?? {}), ...Object.keys(targetData.mmkv ?? {})]);
364
- for (const id of instanceIds) {
323
+ for (const id of unionKeys(baseData.mmkv, targetData.mmkv)) {
365
324
  const registered = liveMode ? mmkvInstanceRegistry.get(id) : null;
366
325
  const instanceUnavailable = liveMode && !registered;
367
326
  const readOnly = liveMode && registered?.instance.isReadOnly === true;
368
327
  const oldEntries = new Map((baseData.mmkv?.[id] ?? []).map(e => [e.key, e]));
369
328
  const newEntries = new Map((targetData.mmkv?.[id] ?? []).map(e => [e.key, e]));
370
- for (const key of new Set([...oldEntries.keys(), ...newEntries.keys()])) {
329
+ for (const key of unionKeys(oldEntries, newEntries)) {
371
330
  if (isDevToolsStorageKey(key)) continue;
372
331
  const itemKey = `mmkv:${id}/${key}`;
373
332
  const label = `${id} · ${key}`;
374
333
  const oldE = oldEntries.get(key);
375
334
  const newE = newEntries.get(key);
376
335
  let item = classifyValue(itemKey, label, oldE?.value, newE?.value, oldEntries.has(key), newEntries.has(key));
377
- if (item.verdict !== "unchanged") {
378
- if (instanceUnavailable) {
379
- item = {
380
- ...item,
381
- verdict: "wont-apply",
382
- reason: "MMKV instance not registered"
383
- };
384
- } else if (readOnly) {
385
- item = {
386
- ...item,
387
- verdict: "wont-apply",
388
- reason: "MMKV instance registered read-only"
389
- };
390
- } else if (newE?.valueType === "buffer" || oldE?.valueType === "buffer") {
391
- item = {
392
- ...item,
393
- verdict: "wont-apply",
394
- reason: "ArrayBuffer values cannot be restored"
395
- };
396
- }
336
+ if (instanceUnavailable) {
337
+ item = wontApply(item, "MMKV instance not registered");
338
+ } else if (readOnly) {
339
+ item = wontApply(item, "MMKV instance registered read-only");
340
+ } else if (newE?.valueType === "buffer" || oldE?.valueType === "buffer") {
341
+ item = wontApply(item, "ArrayBuffer values cannot be restored");
397
342
  }
398
343
  items.push(item);
399
344
  }
@@ -403,22 +348,14 @@ async function previewItems(target, base) {
403
348
  const oldSecure = baseData.secure ?? {};
404
349
  const newSecure = targetData.secure ?? {};
405
350
  const registeredSecure = liveMode ? new Map(secureStoreRegistry.getAll().map(e => [e.key, e])) : null;
406
- for (const key of new Set([...Object.keys(oldSecure), ...Object.keys(newSecure)])) {
351
+ for (const key of unionKeys(oldSecure, newSecure)) {
407
352
  let item = classifyValue(`secure:${key}`, key, oldSecure[key], newSecure[key], key in oldSecure, key in newSecure);
408
- if (liveMode && item.verdict !== "unchanged") {
353
+ if (liveMode) {
409
354
  const entry = registeredSecure?.get(key);
410
355
  if (!entry) {
411
- item = {
412
- ...item,
413
- verdict: "wont-apply",
414
- reason: "key no longer registered with registerSecureStoreKeys"
415
- };
356
+ item = wontApply(item, "key no longer registered with registerSecureStoreKeys");
416
357
  } else if (entry.options.requireAuthentication) {
417
- item = {
418
- ...item,
419
- verdict: "wont-apply",
420
- reason: "biometric-protected keys are never written"
421
- };
358
+ item = wontApply(item, "biometric-protected keys are never written");
422
359
  }
423
360
  }
424
361
  items.push(item);
@@ -62,18 +62,12 @@ export declare const storageToolPreset: {
62
62
  * });
63
63
  * ```
64
64
  */
65
- export declare function createStorageTool(options?: {
66
- /** Tool name (default: "STORAGE") */
67
- name?: string;
68
- /** Tool description */
69
- description?: string;
70
- /** Custom tool ID (default: "storage") */
71
- id?: string;
65
+ export declare const createStorageTool: (options?: ({
72
66
  /** Array of required storage keys to validate */
73
67
  requiredStorageKeys?: RequiredStorageKey[];
74
68
  /** Enable shared modal dimensions */
75
69
  enableSharedModalDimensions?: boolean;
76
- }): {
70
+ } & import("@buoy-gg/shared-ui").ToolPresetOverrides) | undefined) => {
77
71
  id: string;
78
72
  name: string;
79
73
  description: string;
@@ -1 +1 @@
1
- {"version":3,"file":"preset.d.ts","sourceRoot":"","sources":["../../src/preset.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAE1D;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB;;;;;qBAKX;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE;;;6BAGJ,kBAAkB,EAAE;;;CAGlD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE;IAC1C,qCAAqC;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0CAA0C;IAC1C,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,iDAAiD;IACjD,mBAAmB,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAC3C,qCAAqC;IACrC,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC;;;;;qBAMoB;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE;;;;;;EAUpC"}
1
+ {"version":3,"file":"preset.d.ts","sourceRoot":"","sources":["../../src/preset.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAE1D;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB;;;;;qBAKX;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE;;;6BAGJ,kBAAkB,EAAE;;;CAGjD,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,iBAAiB;IAG1B,iDAAiD;0BAC3B,kBAAkB,EAAE;IAC1C,qCAAqC;kCACP,OAAO;;;;;;qBAtCtB;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE;;;6BAGJ,kBAAkB,EAAE;;;CAwClD,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"StorageModalWithTabs.d.ts","sourceRoot":"","sources":["../../../../src/storage/components/StorageModalWithTabs.tsx"],"names":[],"mappings":"AAkCA,OAAO,EAAE,kBAAkB,EAAkB,MAAM,UAAU,CAAC;AAY9D,OAAO,EAAoB,KAAK,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAchF;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,QAAqC,CAAC;AAExE,UAAU,yBAAyB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,UAAU,CAAC,EAAE,CAAC,UAAU,EAAE,GAAG,KAAK,IAAI,CAAC;IACvC,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC,mBAAmB,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAC3C;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,SAAS,GAAG,QAAQ,KAAK,IAAI,CAAC;IAClD;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;CAC/C;AAqBD,wBAAgB,oBAAoB,CAAC,EACnC,OAAO,EACP,OAAO,EACP,MAAM,EACN,UAAU,EACV,2BAAmC,EACnC,mBAAwB,EACxB,MAAc,EACd,WAAW,EACX,SAAS,EACT,aAAa,GACd,EAAE,yBAAyB,sCAsiC3B;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,GAAE,IAAI,CAAC,yBAAyB,EAAE,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAM,+BAG9E"}
1
+ {"version":3,"file":"StorageModalWithTabs.d.ts","sourceRoot":"","sources":["../../../../src/storage/components/StorageModalWithTabs.tsx"],"names":[],"mappings":"AAmCA,OAAO,EAAE,kBAAkB,EAAkB,MAAM,UAAU,CAAC;AAY9D,OAAO,EAAoB,KAAK,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAchF;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,QAAqC,CAAC;AAExE,UAAU,yBAAyB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,UAAU,CAAC,EAAE,CAAC,UAAU,EAAE,GAAG,KAAK,IAAI,CAAC;IACvC,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC,mBAAmB,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAC3C;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,SAAS,GAAG,QAAQ,KAAK,IAAI,CAAC;IAClD;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;CAC/C;AA0DD,wBAAgB,oBAAoB,CAAC,EACnC,OAAO,EACP,OAAO,EACP,MAAM,EACN,UAAU,EACV,2BAAmC,EACnC,mBAAwB,EACxB,MAAc,EACd,WAAW,EACX,SAAS,EACT,aAAa,GACd,EAAE,yBAAyB,sCA24B3B;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,GAAE,IAAI,CAAC,yBAAyB,EAAE,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAM,+BAG9E"}
@@ -1 +1 @@
1
- {"version":3,"file":"useStorageEvents.d.ts","sourceRoot":"","sources":["../../../../src/storage/hooks/useStorageEvents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,EAEL,KAAK,YAAY,EAClB,MAAM,6BAA6B,CAAC;AAGrC,YAAY,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAEhE,MAAM,WAAW,uBAAuB;IACtC,6BAA6B;IAC7B,WAAW,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC;IACvC,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mFAAmF;IACnF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,sBAAsB;IACrC,wCAAwC;IACxC,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,uBAAuB;IACvB,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,0DAA0D;IAC1D,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,wBAAgB,gBAAgB,CAC9B,OAAO,GAAE,uBAA4B,GACpC,sBAAsB,CA8DxB"}
1
+ {"version":3,"file":"useStorageEvents.d.ts","sourceRoot":"","sources":["../../../../src/storage/hooks/useStorageEvents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,OAAO,EAEL,KAAK,YAAY,EAClB,MAAM,6BAA6B,CAAC;AAGrC,YAAY,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAEhE,MAAM,WAAW,uBAAuB;IACtC,6BAA6B;IAC7B,WAAW,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC;IACvC,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mFAAmF;IACnF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,sBAAsB;IACrC,wCAAwC;IACxC,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,uBAAuB;IACvB,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,0DAA0D;IAC1D,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,wBAAgB,gBAAgB,CAC9B,OAAO,GAAE,uBAA4B,GACpC,sBAAsB,CAkBxB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buoy-gg/storage",
3
- "version": "7.0.33",
3
+ "version": "7.0.34",
4
4
  "description": "Browse & edit AsyncStorage, MMKV & SecureStore inside your React Native app, with change history. Part of Buoy devtools.",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -29,11 +29,11 @@
29
29
  "**/snapshotProvider.ts"
30
30
  ],
31
31
  "dependencies": {
32
- "@buoy-gg/floating-tools-core": "^7.0.33",
33
- "@buoy-gg/shared-ui": "^7.0.33"
32
+ "@buoy-gg/floating-tools-core": "^7.0.34",
33
+ "@buoy-gg/shared-ui": "^7.0.34"
34
34
  },
35
35
  "peerDependencies": {
36
- "@buoy-gg/license": "7.0.33",
36
+ "@buoy-gg/license": "7.0.34",
37
37
  "@react-native-async-storage/async-storage": "^2.0.0 || ^3.1.0",
38
38
  "react": "*",
39
39
  "react-native": "*"
@@ -43,7 +43,7 @@
43
43
  "@types/react": "^19.1.0",
44
44
  "@types/react-native": "^0.73.0",
45
45
  "typescript": "~5.8.3",
46
- "@buoy-gg/license": "^7.0.33"
46
+ "@buoy-gg/license": "^7.0.34"
47
47
  },
48
48
  "react-native-builder-bob": {
49
49
  "source": "src",