@buoy-gg/storage 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 (49) hide show
  1. package/lib/commonjs/preset.js +8 -22
  2. package/lib/commonjs/storage/components/DiffViewer/DiffOptionsPanel.js +10 -10
  3. package/lib/commonjs/storage/components/DiffViewer/components/DiffSummary.js +4 -3
  4. package/lib/commonjs/storage/components/GameUIStorageBrowser.js +5 -5
  5. package/lib/commonjs/storage/components/MMKVInstanceInfoPanel.js +1 -1
  6. package/lib/commonjs/storage/components/MMKVInstanceSelector.js +2 -2
  7. package/lib/commonjs/storage/components/SelectionActionBar.js +1 -1
  8. package/lib/commonjs/storage/components/StorageActionButtons.js +1 -1
  9. package/lib/commonjs/storage/components/StorageEventActionButton.js +1 -1
  10. package/lib/commonjs/storage/components/StorageEventDetailContent.js +2 -2
  11. package/lib/commonjs/storage/components/StorageFilterCards.js +1 -1
  12. package/lib/commonjs/storage/components/StorageKeyCard.js +4 -4
  13. package/lib/commonjs/storage/components/StorageKeyDetailScreen.js +8 -8
  14. package/lib/commonjs/storage/components/StorageKeyEditorScreen.js +3 -3
  15. package/lib/commonjs/storage/components/StorageKeyRow.js +6 -6
  16. package/lib/commonjs/storage/components/StorageModalWithTabs.js +45 -147
  17. package/lib/commonjs/storage/components/StorageTypeFilterBar.js +1 -1
  18. package/lib/commonjs/storage/hooks/useStorageEvents.js +15 -52
  19. package/lib/commonjs/storage/utils/snapshotProvider.js +37 -100
  20. package/lib/module/preset.js +7 -20
  21. package/lib/module/storage/components/DiffViewer/DiffOptionsPanel.js +11 -11
  22. package/lib/module/storage/components/DiffViewer/components/DiffSummary.js +4 -3
  23. package/lib/module/storage/components/GameUIStorageBrowser.js +6 -6
  24. package/lib/module/storage/components/MMKVInstanceInfoPanel.js +2 -2
  25. package/lib/module/storage/components/MMKVInstanceSelector.js +3 -3
  26. package/lib/module/storage/components/SelectionActionBar.js +2 -2
  27. package/lib/module/storage/components/StorageActionButtons.js +2 -2
  28. package/lib/module/storage/components/StorageEventActionButton.js +2 -2
  29. package/lib/module/storage/components/StorageEventDetailContent.js +3 -3
  30. package/lib/module/storage/components/StorageFilterCards.js +2 -2
  31. package/lib/module/storage/components/StorageKeyCard.js +5 -5
  32. package/lib/module/storage/components/StorageKeyDetailScreen.js +9 -9
  33. package/lib/module/storage/components/StorageKeyEditorScreen.js +4 -4
  34. package/lib/module/storage/components/StorageKeyRow.js +7 -7
  35. package/lib/module/storage/components/StorageModalWithTabs.js +46 -148
  36. package/lib/module/storage/components/StorageTypeFilterBar.js +2 -2
  37. package/lib/module/storage/hooks/useStorageEvents.js +16 -53
  38. package/lib/module/storage/utils/snapshotProvider.js +38 -101
  39. package/lib/typescript/preset.d.ts +2 -8
  40. package/lib/typescript/preset.d.ts.map +1 -1
  41. package/lib/typescript/storage/components/DiffViewer/DiffOptionsPanel.d.ts.map +1 -1
  42. package/lib/typescript/storage/components/DiffViewer/components/DiffSummary.d.ts.map +1 -1
  43. package/lib/typescript/storage/components/StorageEventDetailContent.d.ts.map +1 -1
  44. package/lib/typescript/storage/components/StorageKeyCard.d.ts.map +1 -1
  45. package/lib/typescript/storage/components/StorageKeyDetailScreen.d.ts.map +1 -1
  46. package/lib/typescript/storage/components/StorageKeyEditorScreen.d.ts.map +1 -1
  47. package/lib/typescript/storage/components/StorageModalWithTabs.d.ts.map +1 -1
  48. package/lib/typescript/storage/hooks/useStorageEvents.d.ts.map +1 -1
  49. package/package.json +5 -5
@@ -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
 
@@ -993,7 +891,7 @@ const styles = _reactNative.StyleSheet.create({
993
891
  fontWeight: "600",
994
892
  marginTop: 16,
995
893
  marginBottom: 8,
996
- fontFamily: "monospace",
894
+ fontFamily: _sharedUi.monoFont,
997
895
  letterSpacing: 0.5,
998
896
  textTransform: "uppercase"
999
897
  },
@@ -1001,7 +899,7 @@ const styles = _reactNative.StyleSheet.create({
1001
899
  color: _sharedUi.night.color.textSecondary,
1002
900
  fontSize: 14,
1003
901
  textAlign: "center",
1004
- fontFamily: "monospace"
902
+ fontFamily: _sharedUi.monoFont
1005
903
  },
1006
904
  eventNavigation: {
1007
905
  flexDirection: "row",
@@ -1020,7 +918,7 @@ const styles = _reactNative.StyleSheet.create({
1020
918
  color: _sharedUi.night.color.text,
1021
919
  fontSize: 12,
1022
920
  fontWeight: "600",
1023
- fontFamily: "monospace",
921
+ fontFamily: _sharedUi.monoFont,
1024
922
  paddingHorizontal: 8
1025
923
  },
1026
924
  headerTopRow: {
@@ -1047,7 +945,7 @@ const styles = _reactNative.StyleSheet.create({
1047
945
  fontSize: 13,
1048
946
  fontWeight: "600",
1049
947
  color: _sharedUi.night.color.accent,
1050
- fontFamily: "monospace",
948
+ fontFamily: _sharedUi.monoFont,
1051
949
  letterSpacing: 0.5
1052
950
  },
1053
951
  contentWrapper: {
@@ -137,7 +137,7 @@ const styles = _reactNative.StyleSheet.create({
137
137
  value: {
138
138
  fontSize: 12,
139
139
  fontWeight: "600",
140
- fontFamily: "monospace",
140
+ fontFamily: _sharedUi.monoFont,
141
141
  lineHeight: 14
142
142
  }
143
143
  });
@@ -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
+ }));