@buoy-gg/storage 5.0.0 → 6.0.1

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 (43) hide show
  1. package/README.md +45 -572
  2. package/lib/commonjs/index.js +13 -0
  3. package/lib/commonjs/storage/components/GameUIStorageBrowser.js +21 -18
  4. package/lib/commonjs/storage/components/SelectionActionBar.js +8 -0
  5. package/lib/commonjs/storage/components/StorageEventCard.js +3 -2
  6. package/lib/commonjs/storage/components/StorageFilterCards.js +1 -2
  7. package/lib/commonjs/storage/components/StorageKeyRow.js +8 -3
  8. package/lib/commonjs/storage/hooks/useMMKVInstances.js +9 -20
  9. package/lib/commonjs/storage/hooks/useMMKVKeys.js +12 -21
  10. package/lib/commonjs/storage/hooks/useSecureStoreKeys.js +104 -0
  11. package/lib/commonjs/storage/hooks/useStorageEvents.js +25 -11
  12. package/lib/commonjs/storage/sync/storageSyncAdapter.js +82 -1
  13. package/lib/commonjs/storage/utils/SecureStoreRegistry.js +149 -0
  14. package/lib/module/index.js +5 -0
  15. package/lib/module/storage/components/GameUIStorageBrowser.js +20 -18
  16. package/lib/module/storage/components/SelectionActionBar.js +8 -0
  17. package/lib/module/storage/components/StorageEventCard.js +4 -3
  18. package/lib/module/storage/components/StorageFilterCards.js +1 -2
  19. package/lib/module/storage/components/StorageKeyRow.js +7 -2
  20. package/lib/module/storage/hooks/useMMKVInstances.js +6 -18
  21. package/lib/module/storage/hooks/useMMKVKeys.js +7 -18
  22. package/lib/module/storage/hooks/useSecureStoreKeys.js +101 -0
  23. package/lib/module/storage/hooks/useStorageEvents.js +25 -11
  24. package/lib/module/storage/sync/storageSyncAdapter.js +83 -1
  25. package/lib/module/storage/utils/SecureStoreRegistry.js +143 -0
  26. package/lib/typescript/index.d.ts +2 -0
  27. package/lib/typescript/index.d.ts.map +1 -1
  28. package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts.map +1 -1
  29. package/lib/typescript/storage/components/SelectionActionBar.d.ts.map +1 -1
  30. package/lib/typescript/storage/components/StorageEventCard.d.ts.map +1 -1
  31. package/lib/typescript/storage/components/StorageFilterCards.d.ts.map +1 -1
  32. package/lib/typescript/storage/components/StorageKeyRow.d.ts +1 -1
  33. package/lib/typescript/storage/components/StorageKeyRow.d.ts.map +1 -1
  34. package/lib/typescript/storage/hooks/useMMKVInstances.d.ts.map +1 -1
  35. package/lib/typescript/storage/hooks/useMMKVKeys.d.ts.map +1 -1
  36. package/lib/typescript/storage/hooks/useSecureStoreKeys.d.ts +18 -0
  37. package/lib/typescript/storage/hooks/useSecureStoreKeys.d.ts.map +1 -0
  38. package/lib/typescript/storage/hooks/useStorageEvents.d.ts.map +1 -1
  39. package/lib/typescript/storage/sync/storageSyncAdapter.d.ts +24 -0
  40. package/lib/typescript/storage/sync/storageSyncAdapter.d.ts.map +1 -1
  41. package/lib/typescript/storage/utils/SecureStoreRegistry.d.ts +115 -0
  42. package/lib/typescript/storage/utils/SecureStoreRegistry.d.ts.map +1 -0
  43. package/package.json +6 -6
@@ -13,18 +13,15 @@ var _StorageFilterCards = require("./StorageFilterCards");
13
13
  var _useAsyncStorageKeys = require("../hooks/useAsyncStorageKeys");
14
14
  var _AsyncStorageListener = require("../utils/AsyncStorageListener");
15
15
  var _useMMKVKeys = require("../hooks/useMMKVKeys");
16
+ var _useSecureStoreKeys = require("../hooks/useSecureStoreKeys");
16
17
  var _useMMKVInstances = require("../hooks/useMMKVInstances");
17
- var _mmkvAvailability = require("../utils/mmkvAvailability");
18
18
  var _StorageActionButtons = require("./StorageActionButtons");
19
19
  var _SelectionActionBar = require("./SelectionActionBar");
20
+ var _MMKVListener = require("../utils/MMKVListener");
20
21
  var _license = require("@buoy-gg/license");
21
22
  var _jsxRuntime = require("react/jsx-runtime");
22
- // Conditionally import MMKV listener
23
- let addMMKVListener;
24
- if ((0, _mmkvAvailability.isMMKVAvailable)()) {
25
- const listener = require("../utils/MMKVListener");
26
- addMMKVListener = listener.addMMKVListener;
27
- }
23
+ // Pure JS (no react-native-mmkv dependency) — works with any registered
24
+ // instance, including remote-backed wrappers on the desktop dashboard.
28
25
 
29
26
  // Import shared Game UI components
30
27
 
@@ -123,20 +120,29 @@ function GameUIStorageBrowser({
123
120
  return allMMKVKeys.filter(k => k.instanceId === selectedMMKVInstance);
124
121
  }, [allMMKVKeys, selectedMMKVInstance]);
125
122
 
126
- // Merge storage keys from AsyncStorage and MMKV
123
+ // Use SecureStore hook — only surfaces keys declared via registerSecureStoreKeys()
124
+ const {
125
+ storageKeys: secureStorageKeys,
126
+ isLoading: isLoadingSecure,
127
+ error: secureError,
128
+ refresh: refreshSecure
129
+ } = (0, _useSecureStoreKeys.useSecureStoreKeys)();
130
+
131
+ // Merge storage keys from AsyncStorage, MMKV, and SecureStore
127
132
  const allStorageKeys = (0, _react.useMemo)(() => {
128
- return [...asyncStorageKeys, ...mmkvStorageKeys];
129
- }, [asyncStorageKeys, mmkvStorageKeys]);
133
+ return [...asyncStorageKeys, ...mmkvStorageKeys, ...secureStorageKeys];
134
+ }, [asyncStorageKeys, mmkvStorageKeys, secureStorageKeys]);
130
135
 
131
136
  // Determine loading and error states
132
- const isLoading = isLoadingAsync || isLoadingMMKV;
133
- const error = asyncError || mmkvError;
137
+ const isLoading = isLoadingAsync || isLoadingMMKV || isLoadingSecure;
138
+ const error = asyncError || mmkvError || secureError;
134
139
 
135
140
  // Combined refresh function
136
141
  const refresh = (0, _react.useCallback)(() => {
137
142
  refreshAsync();
138
143
  refreshMMKV();
139
- }, [refreshAsync, refreshMMKV]);
144
+ refreshSecure();
145
+ }, [refreshAsync, refreshMMKV, refreshSecure]);
140
146
 
141
147
  // Generate unique identifier for a storage key
142
148
  const getKeyIdentifier = (0, _react.useCallback)(storageKey => {
@@ -249,13 +255,10 @@ function GameUIStorageBrowser({
249
255
  };
250
256
  }, [refreshAsync]);
251
257
 
252
- // Auto-refresh on MMKV storage events (only if MMKV is available)
258
+ // Auto-refresh on MMKV storage events
253
259
  (0, _react.useEffect)(() => {
254
- if (!(0, _mmkvAvailability.isMMKVAvailable)() || !addMMKVListener) {
255
- return; // Skip if MMKV not available
256
- }
257
260
  let timeoutId;
258
- const unsubscribe = addMMKVListener(event => {
261
+ const unsubscribe = (0, _MMKVListener.addMMKVListener)(event => {
259
262
  // Only refresh if event is for the selected instance
260
263
  if (event.instanceId === selectedMMKVInstance) {
261
264
  clearTimeout(timeoutId);
@@ -9,6 +9,7 @@ var _reactNative = require("react-native");
9
9
  var _sharedUi = require("@buoy-gg/shared-ui");
10
10
  var _license = require("@buoy-gg/license");
11
11
  var _asyncStorageCompat = require("../utils/asyncStorageCompat");
12
+ var _SecureStoreRegistry = require("../utils/SecureStoreRegistry");
12
13
  var _jsxRuntime = require("react/jsx-runtime");
13
14
  function SelectionActionBar({
14
15
  selectedKeys,
@@ -71,12 +72,19 @@ function SelectionActionBar({
71
72
  // Group keys by storage type
72
73
  const asyncKeys = selectedKeys.filter(k => k.storageType === "async");
73
74
  const mmkvKeys = selectedKeys.filter(k => k.storageType === "mmkv");
75
+ const secureKeys = selectedKeys.filter(k => k.storageType === "secure");
74
76
 
75
77
  // Delete AsyncStorage keys
76
78
  if (asyncKeys.length > 0) {
77
79
  await (0, _asyncStorageCompat.removeMany)(asyncKeys.map(k => k.key));
78
80
  }
79
81
 
82
+ // Delete SecureStore keys (via the registry, which knows each
83
+ // key's keychainService options)
84
+ for (const k of secureKeys) {
85
+ await _SecureStoreRegistry.secureStoreRegistry.deleteValue(k.key);
86
+ }
87
+
80
88
  // Delete MMKV keys
81
89
  if (mmkvKeys.length > 0) {
82
90
  // Group by instance
@@ -87,7 +87,6 @@ const StorageEventCard = exports.StorageEventCard = /*#__PURE__*/(0, _react.memo
87
87
  const actionColor = getActionColor(data.lastAction);
88
88
  const actionLabel = (0, _storageActionHelpers.translateStorageAction)(data.lastAction);
89
89
  const totalOps = data.totalOperations ?? 1;
90
- const relativeTime = (0, _sharedUi.useRelativeTime)(data.lastEventTimestamp);
91
90
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.CompactRow
92
91
  // Status section (left) - shows "Storage" with action-colored dot
93
92
  , {
@@ -104,7 +103,9 @@ const StorageEventCard = exports.StorageEventCard = /*#__PURE__*/(0, _react.memo
104
103
  showChevron: showChevron
105
104
  // Bottom right - timestamp
106
105
  ,
107
- bottomRightText: relativeTime
106
+ bottomRightText: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.RelativeTime, {
107
+ timestamp: data.lastEventTimestamp
108
+ })
108
109
  // Interaction
109
110
  ,
110
111
  isSelected: isSelected,
@@ -10,7 +10,6 @@ var _mmkvAvailability = require("../utils/mmkvAvailability");
10
10
  var _jsxRuntime = require("react/jsx-runtime");
11
11
  // Feature flags - set to true when implemented
12
12
  const IS_MMKV_IMPLEMENTED = true; // ✅ MMKV support is now implemented
13
- const IS_SECURE_STORE_IMPLEMENTED = false;
14
13
 
15
14
  // Runtime availability checks
16
15
  const IS_MMKV_AVAILABLE = (0, _mmkvAvailability.isMMKVAvailable)();
@@ -183,7 +182,7 @@ function StorageFilterCards({
183
182
  } : {
184
183
  color: _sharedUi.macOSColors.text.muted
185
184
  }],
186
- children: "Coming Soon"
185
+ children: displayStats.secureCount
187
186
  })]
188
187
  })]
189
188
  })]
@@ -3,7 +3,8 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.StorageKeyRow = StorageKeyRow;
6
+ exports.StorageKeyRow = void 0;
7
+ var _react = require("react");
7
8
  var _reactNative = require("react-native");
8
9
  var _sharedUi = require("@buoy-gg/shared-ui");
9
10
  var _storageQueryUtils = require("../utils/storageQueryUtils");
@@ -73,7 +74,11 @@ const formatValue = value => {
73
74
  const str = typeof value === "string" ? value : JSON.stringify(value);
74
75
  return str;
75
76
  };
76
- function StorageKeyRow({
77
+
78
+ // Memo justified by profiling (tool-bench 2026-07-06): the browser tab
79
+ // renders every key row on each parent render (ScrollView, no
80
+ // virtualization) — memo keeps unchanged keys from re-rendering.
81
+ const StorageKeyRow = exports.StorageKeyRow = /*#__PURE__*/(0, _react.memo)(function StorageKeyRow({
77
82
  storageKey,
78
83
  isExpanded,
79
84
  onPress,
@@ -315,7 +320,7 @@ function StorageKeyRow({
315
320
  })]
316
321
  })]
317
322
  });
318
- }
323
+ });
319
324
  const getStorageTypeColor = storageType => {
320
325
  switch (storageType) {
321
326
  case "mmkv":
@@ -7,7 +7,7 @@ exports.useMMKVInstance = useMMKVInstance;
7
7
  exports.useMMKVInstanceExists = useMMKVInstanceExists;
8
8
  exports.useMMKVInstances = useMMKVInstances;
9
9
  var _react = require("react");
10
- var _mmkvAvailability = require("../utils/mmkvAvailability");
10
+ var _MMKVInstanceRegistry = require("../utils/MMKVInstanceRegistry");
11
11
  /**
12
12
  * useMMKVInstances Hook
13
13
  *
@@ -22,12 +22,9 @@ var _mmkvAvailability = require("../utils/mmkvAvailability");
22
22
  * @see MMKVInstanceRegistry.ts for instance registration
23
23
  */
24
24
 
25
- // Conditionally import registry
26
- let mmkvInstanceRegistry;
27
- if ((0, _mmkvAvailability.isMMKVAvailable)()) {
28
- const registry = require('../utils/MMKVInstanceRegistry');
29
- mmkvInstanceRegistry = registry.mmkvInstanceRegistry;
30
- }
25
+ // The registry is pure JS (no react-native-mmkv dependency) — import it
26
+ // unconditionally so registered instances show up even without the native
27
+ // module (e.g. remote-backed wrappers on the desktop dashboard).
31
28
 
32
29
  /**
33
30
  * Metadata about an MMKV instance with additional runtime information
@@ -72,13 +69,7 @@ function useMMKVInstances(autoRefresh = false) {
72
69
  const fetchInstances = () => {
73
70
  setIsLoading(true);
74
71
  try {
75
- // Guard: Check if MMKV is available
76
- if (!(0, _mmkvAvailability.isMMKVAvailable)() || !mmkvInstanceRegistry) {
77
- setInstances([]);
78
- setIsLoading(false);
79
- return; // Silently return empty when MMKV not available
80
- }
81
- const allInstances = mmkvInstanceRegistry.getAll();
72
+ const allInstances = _MMKVInstanceRegistry.mmkvInstanceRegistry.getAll();
82
73
 
83
74
  // Enhance with metadata
84
75
  const instancesWithMetadata = allInstances.map(instanceInfo => {
@@ -165,12 +156,11 @@ function useMMKVInstances(autoRefresh = false) {
165
156
  function useMMKVInstance(instanceId) {
166
157
  const [instance, setInstance] = (0, _react.useState)(null);
167
158
  (0, _react.useEffect)(() => {
168
- // Guard: Check if MMKV is available
169
- if (!(0, _mmkvAvailability.isMMKVAvailable)() || !mmkvInstanceRegistry) {
159
+ if (!_MMKVInstanceRegistry.mmkvInstanceRegistry) {
170
160
  setInstance(null);
171
161
  return;
172
162
  }
173
- const instanceInfo = mmkvInstanceRegistry.get(instanceId);
163
+ const instanceInfo = _MMKVInstanceRegistry.mmkvInstanceRegistry.get(instanceId);
174
164
  if (!instanceInfo) {
175
165
  setInstance(null);
176
166
  return;
@@ -210,12 +200,11 @@ function useMMKVInstance(instanceId) {
210
200
  function useMMKVInstanceExists(instanceId) {
211
201
  const [exists, setExists] = (0, _react.useState)(false);
212
202
  (0, _react.useEffect)(() => {
213
- // Guard: Check if MMKV is available
214
- if (!(0, _mmkvAvailability.isMMKVAvailable)() || !mmkvInstanceRegistry) {
203
+ if (!_MMKVInstanceRegistry.mmkvInstanceRegistry) {
215
204
  setExists(false);
216
205
  return;
217
206
  }
218
- setExists(mmkvInstanceRegistry.has(instanceId));
207
+ setExists(_MMKVInstanceRegistry.mmkvInstanceRegistry.has(instanceId));
219
208
  }, [instanceId]);
220
209
  return exists;
221
210
  }
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.useMMKVKeys = useMMKVKeys;
7
7
  exports.useMultiMMKVKeys = useMultiMMKVKeys;
8
8
  var _react = require("react");
9
- var _mmkvAvailability = require("../utils/mmkvAvailability");
9
+ var _mmkvTypeDetection = require("../utils/mmkvTypeDetection");
10
10
  /**
11
11
  * useMMKVKeys Hook
12
12
  *
@@ -20,17 +20,13 @@ var _mmkvAvailability = require("../utils/mmkvAvailability");
20
20
  * - No multiGet - must fetch keys individually
21
21
  */
22
22
 
23
+ // Pure-JS utilities (no react-native-mmkv dependency) — safe to import
24
+ // unconditionally. They operate on whatever instance objects are registered,
25
+ // including remote-backed wrappers on the desktop dashboard.
26
+
23
27
  // Conditionally import MMKV types
24
28
  // Use 'any' to avoid hard dependency on react-native-mmkv
25
29
 
26
- // Conditionally import utilities only if MMKV is available
27
- let detectMMKVType;
28
- let isTypeMatch;
29
- if ((0, _mmkvAvailability.isMMKVAvailable)()) {
30
- const typeDetection = require('../utils/mmkvTypeDetection');
31
- detectMMKVType = typeDetection.detectMMKVType;
32
- isTypeMatch = typeDetection.isTypeMatch;
33
- }
34
30
  /**
35
31
  * Hook to fetch and monitor keys from an MMKV instance
36
32
  *
@@ -89,14 +85,9 @@ function useMMKVKeys(instance, instanceId, requiredStorageKeys = []) {
89
85
  setIsLoading(true);
90
86
  setError(null);
91
87
  try {
92
- // Guard: Check if MMKV is available
93
- if (!(0, _mmkvAvailability.isMMKVAvailable)()) {
94
- setStorageKeys([]);
95
- setIsLoading(false);
96
- return; // Silently return empty when MMKV not available
97
- }
98
-
99
- // Guard: Check if instance is provided
88
+ // Guard: Check if instance is provided. (No isMMKVAvailable() gate —
89
+ // registered instances may be remote-backed wrappers with no native
90
+ // module, e.g. on the desktop dashboard.)
100
91
  if (!instance) {
101
92
  setStorageKeys([]);
102
93
  setIsLoading(false);
@@ -137,7 +128,7 @@ function useMMKVKeys(instance, instanceId, requiredStorageKeys = []) {
137
128
  const {
138
129
  value,
139
130
  type
140
- } = detectMMKVType(instance, key);
131
+ } = (0, _mmkvTypeDetection.detectMMKVType)(instance, key);
141
132
 
142
133
  // Check if this is a required key
143
134
  const requiredConfig = requiredStorageKeys.find(req => {
@@ -155,7 +146,7 @@ function useMMKVKeys(instance, instanceId, requiredStorageKeys = []) {
155
146
  status = String(value) === String(requiredConfig.expectedValue) ? 'required_present' : 'required_wrong_value';
156
147
  } else if (typeof requiredConfig === 'object' && 'expectedType' in requiredConfig) {
157
148
  // Type validation
158
- status = isTypeMatch(type, requiredConfig.expectedType) ? 'required_present' : 'required_wrong_type';
149
+ status = (0, _mmkvTypeDetection.isTypeMatch)(type, requiredConfig.expectedType) ? 'required_present' : 'required_wrong_type';
159
150
  } else {
160
151
  // Key exists and is required (no specific validation)
161
152
  status = 'required_present';
@@ -291,7 +282,7 @@ function useMultiMMKVKeys(instances, requiredStorageKeys = []) {
291
282
  const {
292
283
  value,
293
284
  type
294
- } = detectMMKVType(instance, key);
285
+ } = (0, _mmkvTypeDetection.detectMMKVType)(instance, key);
295
286
  const requiredConfig = requiredStorageKeys.find(req => {
296
287
  if (typeof req === 'string') return req === key;
297
288
  return req.key === key;
@@ -303,7 +294,7 @@ function useMultiMMKVKeys(instances, requiredStorageKeys = []) {
303
294
  } else if (typeof requiredConfig === 'object' && 'expectedValue' in requiredConfig) {
304
295
  status = String(value) === String(requiredConfig.expectedValue) ? 'required_present' : 'required_wrong_value';
305
296
  } else if (typeof requiredConfig === 'object' && 'expectedType' in requiredConfig) {
306
- status = isTypeMatch(type, requiredConfig.expectedType) ? 'required_present' : 'required_wrong_type';
297
+ status = (0, _mmkvTypeDetection.isTypeMatch)(type, requiredConfig.expectedType) ? 'required_present' : 'required_wrong_type';
307
298
  } else {
308
299
  status = 'required_present';
309
300
  }
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.useSecureStoreKeys = useSecureStoreKeys;
7
+ var _react = require("react");
8
+ var _SecureStoreRegistry = require("../utils/SecureStoreRegistry");
9
+ /**
10
+ * Placeholder value shown for `requireAuthentication` keys. Reading those
11
+ * triggers a biometric (Face ID / fingerprint) prompt, so the tool never
12
+ * auto-reads them — it just shows they exist.
13
+ */
14
+ const BIOMETRIC_PLACEHOLDER = "•••••• (biometric protected)";
15
+ /**
16
+ * Read all registered SecureStore keys into StorageKeyInfo[] for the browser.
17
+ *
18
+ * SecureStore has no enumeration API, so this only surfaces keys declared via
19
+ * `registerSecureStoreKeys()` (see SecureStoreRegistry). Values are read with
20
+ * the options each key was registered with (keychainService must match), and
21
+ * biometric-protected keys are listed without being read.
22
+ */
23
+ function useSecureStoreKeys() {
24
+ const [storageKeys, setStorageKeys] = (0, _react.useState)([]);
25
+ const [isLoading, setIsLoading] = (0, _react.useState)(true);
26
+ const [error, setError] = (0, _react.useState)(null);
27
+ const fetchSecureStoreData = (0, _react.useCallback)(async () => {
28
+ const module = _SecureStoreRegistry.secureStoreRegistry.getModule();
29
+ const entries = _SecureStoreRegistry.secureStoreRegistry.getAll();
30
+ if (!module || entries.length === 0) {
31
+ setStorageKeys([]);
32
+ setIsLoading(false);
33
+ return;
34
+ }
35
+ setIsLoading(true);
36
+ setError(null);
37
+ try {
38
+ const keys = [];
39
+ for (const entry of entries) {
40
+ if (entry.options.requireAuthentication) {
41
+ // Never auto-read: reading would fire a biometric prompt.
42
+ keys.push({
43
+ key: entry.key,
44
+ value: BIOMETRIC_PLACEHOLDER,
45
+ storageType: "secure",
46
+ status: "optional_present",
47
+ category: "optional",
48
+ description: entry.description ?? "Protected by biometric authentication — value not read by DevTools",
49
+ ...(entry.options.keychainService && {
50
+ instanceId: entry.options.keychainService
51
+ })
52
+ });
53
+ continue;
54
+ }
55
+ let rawValue = null;
56
+ try {
57
+ rawValue = await module.getItemAsync(entry.key, entry.options);
58
+ } catch {
59
+ // Key unreadable (e.g. keychain state changed) — treat as missing.
60
+ }
61
+
62
+ // Parse JSON values for the DataViewer, same as the AsyncStorage hook.
63
+ let parsedValue = rawValue;
64
+ if (rawValue) {
65
+ try {
66
+ parsedValue = JSON.parse(rawValue);
67
+ } catch {
68
+ parsedValue = rawValue;
69
+ }
70
+ }
71
+ keys.push({
72
+ key: entry.key,
73
+ value: rawValue === null ? undefined : parsedValue,
74
+ storageType: "secure",
75
+ status: rawValue === null ? "required_missing" : "optional_present",
76
+ category: rawValue === null ? "required" : "optional",
77
+ ...(entry.description && {
78
+ description: entry.description
79
+ }),
80
+ ...(entry.options.keychainService && {
81
+ instanceId: entry.options.keychainService
82
+ })
83
+ });
84
+ }
85
+ setStorageKeys(keys);
86
+ } catch (err) {
87
+ setError(err instanceof Error ? err : new Error("Failed to read SecureStore"));
88
+ setStorageKeys([]);
89
+ } finally {
90
+ setIsLoading(false);
91
+ }
92
+ }, []);
93
+
94
+ // Initial fetch (registration happens at app startup, before the tool opens)
95
+ (0, _react.useEffect)(() => {
96
+ fetchSecureStoreData();
97
+ }, [fetchSecureStoreData]);
98
+ return {
99
+ storageKeys,
100
+ isLoading,
101
+ error,
102
+ refresh: fetchSecureStoreData
103
+ };
104
+ }
@@ -51,24 +51,38 @@ function useStorageEvents(options = {}) {
51
51
  setIsCapturing(false);
52
52
  return;
53
53
  }
54
- const unsubscribe = _storageEventStore.storageEventStore.subscribeToEvents(allEvents => {
55
- // Filter by storage type if specified
56
- let filtered = allEvents;
57
- if (storageType !== "all") {
58
- filtered = allEvents.filter(e => e.storageType === storageType);
59
- }
60
54
 
61
- // Limit events
62
- if (filtered.length > maxEvents) {
63
- filtered = filtered.slice(0, maxEvents);
64
- }
65
- setEvents(filtered);
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
+ });
66
79
  });
67
80
 
68
81
  // Mark as capturing after successful subscription
69
82
  setIsCapturing(true);
70
83
  return () => {
71
84
  unsubscribe();
85
+ if (rafId != null) cancelAnimationFrame(rafId);
72
86
  // Note: Don't set isCapturing=false here because we might be
73
87
  // re-subscribing due to dependency change, not actually stopping
74
88
  };
@@ -9,7 +9,14 @@ var _sharedUi = require("@buoy-gg/shared-ui");
9
9
  var _asyncStorageCompat = require("../utils/asyncStorageCompat");
10
10
  var _storageEventStore = require("../stores/storageEventStore");
11
11
  var _clearAllStorage = require("../utils/clearAllStorage");
12
+ var _MMKVInstanceRegistry = require("../utils/MMKVInstanceRegistry");
13
+ var _mmkvTypeDetection = require("../utils/mmkvTypeDetection");
14
+ var _SecureStoreRegistry = require("../utils/SecureStoreRegistry");
12
15
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
16
+ /** One MMKV entry in a `mmkv.snapshot` result. */
17
+
18
+ /** One MMKV instance in a `mmkv.snapshot` result. */
19
+
13
20
  /**
14
21
  * Sync adapter for the storage tool, consumed by @buoy-gg/external-sync's
15
22
  * `useExternalSync` (structurally matches its ToolSyncAdapter interface so
@@ -62,6 +69,80 @@ const storageSyncAdapter = exports.storageSyncAdapter = {
62
69
  "async.removeItem": params => _asyncStorage.default.removeItem(params.key),
63
70
  "async.multiRemove": params => (0, _asyncStorageCompat.removeMany)(params.keys),
64
71
  "async.multiSet": params => (0, _asyncStorageCompat.writeMany)(params.pairs),
65
- "async.clear": () => _asyncStorage.default.clear()
72
+ "async.clear": () => _asyncStorage.default.clear(),
73
+ // ── Remote MMKV proxy (desktop browse mode + MCP) ──
74
+ // MMKV has no remote presence off-device; these read/write the registered
75
+ // instances so sync clients can browse them like AsyncStorage.
76
+ "mmkv.snapshot": () => _MMKVInstanceRegistry.mmkvInstanceRegistry.getAll().map(({
77
+ id,
78
+ instance,
79
+ encrypted,
80
+ readOnly
81
+ }) => {
82
+ let entries = [];
83
+ try {
84
+ entries = instance.getAllKeys().filter(key => !(0, _sharedUi.isDevToolsStorageKey)(key)).map(key => {
85
+ const {
86
+ value,
87
+ type: valueType
88
+ } = (0, _mmkvTypeDetection.detectMMKVType)(instance, key);
89
+ return {
90
+ key,
91
+ value,
92
+ valueType
93
+ };
94
+ });
95
+ } catch {
96
+ // Instance unreadable — return it with no entries rather than failing
97
+ }
98
+ return {
99
+ id,
100
+ encrypted,
101
+ readOnly,
102
+ entries
103
+ };
104
+ }),
105
+ "mmkv.set": params => {
106
+ const {
107
+ instanceId,
108
+ key,
109
+ value
110
+ } = params;
111
+ _MMKVInstanceRegistry.mmkvInstanceRegistry.get(instanceId)?.instance.set(key, value);
112
+ },
113
+ "mmkv.remove": params => {
114
+ const {
115
+ instanceId,
116
+ key
117
+ } = params;
118
+ const instance = _MMKVInstanceRegistry.mmkvInstanceRegistry.get(instanceId)?.instance;
119
+ // v4 renamed delete() to remove() — support both
120
+ if (instance?.remove) instance.remove(key);else instance?.delete?.(key);
121
+ },
122
+ // ── Remote SecureStore proxy ──
123
+ // Only registered keys are known (SecureStore has no enumeration API).
124
+ // Biometric-protected keys are never read — reads would fire an auth prompt.
125
+ "secure.keys": () => _SecureStoreRegistry.secureStoreRegistry.getAll().map(({
126
+ key,
127
+ options,
128
+ description
129
+ }) => ({
130
+ key,
131
+ description,
132
+ keychainService: options.keychainService,
133
+ requireAuthentication: options.requireAuthentication ?? false
134
+ })),
135
+ "secure.get": params => {
136
+ const {
137
+ key
138
+ } = params;
139
+ const entry = _SecureStoreRegistry.secureStoreRegistry.getAll().find(e => e.key === key);
140
+ const module = _SecureStoreRegistry.secureStoreRegistry.getModule();
141
+ if (!entry || !module || entry.options.requireAuthentication) {
142
+ return Promise.resolve(null);
143
+ }
144
+ return module.getItemAsync(key, entry.options);
145
+ },
146
+ "secure.delete": params => _SecureStoreRegistry.secureStoreRegistry.deleteValue(params.key)
66
147
  }
67
148
  };