@buoy-gg/storage 3.0.1 → 4.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 (71) hide show
  1. package/README.md +1 -1
  2. package/lib/commonjs/index.js +7 -0
  3. package/lib/commonjs/storage/components/GameUIStorageBrowser.js +25 -4
  4. package/lib/commonjs/storage/components/GameUIStorageStats.js +2 -2
  5. package/lib/commonjs/storage/components/SelectionActionBar.js +16 -3
  6. package/lib/commonjs/storage/components/StorageBrowserMode.js +6 -2
  7. package/lib/commonjs/storage/components/StorageEventDetailContent.js +2 -2
  8. package/lib/commonjs/storage/components/StorageKeyCard.js +5 -5
  9. package/lib/commonjs/storage/components/StorageKeyRow.js +97 -8
  10. package/lib/commonjs/storage/components/StorageKeySection.js +10 -4
  11. package/lib/commonjs/storage/components/StorageModalWithTabs.js +47 -1
  12. package/lib/commonjs/storage/hooks/useAsyncStorageKeys.js +15 -3
  13. package/lib/commonjs/storage/hooks/useMMKVKeys.js +20 -2
  14. package/lib/commonjs/storage/stores/storageEventStore.js +84 -0
  15. package/lib/commonjs/storage/sync/storageSyncAdapter.js +53 -0
  16. package/lib/commonjs/storage/utils/AsyncStorageListener.js +148 -160
  17. package/lib/commonjs/storage/utils/asyncStorageCompat.js +89 -0
  18. package/lib/commonjs/storage/utils/clearAllStorage.js +2 -1
  19. package/lib/commonjs/storage/utils/mmkvTypeDetection.js +20 -5
  20. package/lib/commonjs/storage/utils/storageTimeTravelUtils.js +3 -2
  21. package/lib/commonjs/storage/utils/valueType.js +41 -0
  22. package/lib/module/index.js +5 -0
  23. package/lib/module/storage/components/GameUIStorageBrowser.js +26 -4
  24. package/lib/module/storage/components/GameUIStorageStats.js +3 -2
  25. package/lib/module/storage/components/SelectionActionBar.js +17 -3
  26. package/lib/module/storage/components/StorageBrowserMode.js +6 -2
  27. package/lib/module/storage/components/StorageEventDetailContent.js +2 -2
  28. package/lib/module/storage/components/StorageKeyCard.js +5 -5
  29. package/lib/module/storage/components/StorageKeyRow.js +99 -10
  30. package/lib/module/storage/components/StorageKeySection.js +10 -4
  31. package/lib/module/storage/components/StorageModalWithTabs.js +47 -1
  32. package/lib/module/storage/hooks/useAsyncStorageKeys.js +16 -4
  33. package/lib/module/storage/hooks/useMMKVKeys.js +21 -3
  34. package/lib/module/storage/stores/storageEventStore.js +84 -0
  35. package/lib/module/storage/sync/storageSyncAdapter.js +48 -0
  36. package/lib/module/storage/utils/AsyncStorageListener.js +124 -135
  37. package/lib/module/storage/utils/asyncStorageCompat.js +81 -0
  38. package/lib/module/storage/utils/clearAllStorage.js +2 -1
  39. package/lib/module/storage/utils/mmkvTypeDetection.js +20 -5
  40. package/lib/module/storage/utils/storageTimeTravelUtils.js +3 -2
  41. package/lib/module/storage/utils/valueType.js +39 -0
  42. package/lib/typescript/index.d.ts +1 -0
  43. package/lib/typescript/index.d.ts.map +1 -1
  44. package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts +5 -1
  45. package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts.map +1 -1
  46. package/lib/typescript/storage/components/GameUIStorageStats.d.ts.map +1 -1
  47. package/lib/typescript/storage/components/SelectionActionBar.d.ts +3 -1
  48. package/lib/typescript/storage/components/SelectionActionBar.d.ts.map +1 -1
  49. package/lib/typescript/storage/components/StorageBrowserMode.d.ts +3 -1
  50. package/lib/typescript/storage/components/StorageBrowserMode.d.ts.map +1 -1
  51. package/lib/typescript/storage/components/StorageKeyRow.d.ts +7 -1
  52. package/lib/typescript/storage/components/StorageKeyRow.d.ts.map +1 -1
  53. package/lib/typescript/storage/components/StorageKeySection.d.ts +7 -1
  54. package/lib/typescript/storage/components/StorageKeySection.d.ts.map +1 -1
  55. package/lib/typescript/storage/components/StorageModalWithTabs.d.ts.map +1 -1
  56. package/lib/typescript/storage/hooks/useAsyncStorageKeys.d.ts.map +1 -1
  57. package/lib/typescript/storage/hooks/useMMKVKeys.d.ts.map +1 -1
  58. package/lib/typescript/storage/stores/storageEventStore.d.ts +8 -0
  59. package/lib/typescript/storage/stores/storageEventStore.d.ts.map +1 -1
  60. package/lib/typescript/storage/sync/storageSyncAdapter.d.ts +31 -0
  61. package/lib/typescript/storage/sync/storageSyncAdapter.d.ts.map +1 -0
  62. package/lib/typescript/storage/utils/AsyncStorageListener.d.ts +20 -0
  63. package/lib/typescript/storage/utils/AsyncStorageListener.d.ts.map +1 -1
  64. package/lib/typescript/storage/utils/asyncStorageCompat.d.ts +30 -0
  65. package/lib/typescript/storage/utils/asyncStorageCompat.d.ts.map +1 -0
  66. package/lib/typescript/storage/utils/clearAllStorage.d.ts.map +1 -1
  67. package/lib/typescript/storage/utils/mmkvTypeDetection.d.ts.map +1 -1
  68. package/lib/typescript/storage/utils/storageTimeTravelUtils.d.ts.map +1 -1
  69. package/lib/typescript/storage/utils/valueType.d.ts +13 -0
  70. package/lib/typescript/storage/utils/valueType.d.ts.map +1 -1
  71. package/package.json +6 -6
@@ -23,7 +23,10 @@ export function StorageKeySection({
23
23
  selectedKeys = new Set(),
24
24
  onSelectionChange,
25
25
  eventCountByKey,
26
- onViewHistory
26
+ onViewHistory,
27
+ onHideKey,
28
+ pinnedKeys,
29
+ onTogglePin
27
30
  }) {
28
31
  const [expandedKey, setExpandedKey] = useState(null);
29
32
  const handleKeyPress = useCallback(storageKey => {
@@ -56,14 +59,14 @@ export function StorageKeySection({
56
59
  if (keys.length === 0) return null;
57
60
  return /*#__PURE__*/_jsxs(View, {
58
61
  style: styles.sectionContainer,
59
- children: [title && /*#__PURE__*/_jsxs(SectionHeader, {
62
+ children: [title ? /*#__PURE__*/_jsxs(SectionHeader, {
60
63
  children: [/*#__PURE__*/_jsx(SectionHeader.Title, {
61
64
  children: title
62
65
  }), count >= 0 && /*#__PURE__*/_jsx(SectionHeader.Badge, {
63
66
  count: count,
64
67
  color: headerColor
65
68
  })]
66
- }), /*#__PURE__*/_jsx(View, {
69
+ }) : null, /*#__PURE__*/_jsx(View, {
67
70
  style: styles.sectionContent,
68
71
  children: keys.map(storageKey => {
69
72
  // Create unique key by combining storage type, instance ID (if present), and key name
@@ -76,7 +79,10 @@ export function StorageKeySection({
76
79
  isSelected: selectedKeys.has(uniqueKey),
77
80
  onSelectionChange: onSelectionChange,
78
81
  eventCount: eventCountByKey?.[storageKey.key],
79
- onViewHistory: onViewHistory ? () => onViewHistory(storageKey.key) : undefined
82
+ onViewHistory: onViewHistory ? () => onViewHistory(storageKey.key) : undefined,
83
+ onHideKey: onHideKey,
84
+ isPinned: pinnedKeys?.has(storageKey.key),
85
+ onTogglePin: onTogglePin
80
86
  }, uniqueKey);
81
87
  })
82
88
  })]
@@ -56,8 +56,10 @@ export function StorageModalWithTabs({
56
56
  );
57
57
  const [enabledStorageTypes, setEnabledStorageTypes] = useState(new Set(['async', 'mmkv', 'secure']) // All enabled by default
58
58
  );
59
+ const [pinnedKeys, setPinnedKeys] = useState(new Set());
59
60
  const lastEventRef = useRef(null);
60
61
  const hasLoadedFilters = useRef(false);
62
+ const hasLoadedPins = useRef(false);
61
63
  const hasLoadedTabState = useRef(false);
62
64
  const hasLoadedMonitoringState = useRef(false);
63
65
 
@@ -172,6 +174,37 @@ export function StorageModalWithTabs({
172
174
  saveFilters();
173
175
  }, [ignoredPatterns]);
174
176
 
177
+ // Load persisted pinned keys on mount
178
+ useEffect(() => {
179
+ if (!visible || hasLoadedPins.current) return;
180
+ const loadPins = async () => {
181
+ try {
182
+ const stored = await AsyncStorage.getItem(devToolsStorageKeys.storage.pinnedKeys());
183
+ if (stored) {
184
+ setPinnedKeys(new Set(JSON.parse(stored)));
185
+ }
186
+ hasLoadedPins.current = true;
187
+ } catch (error) {
188
+ // Failed to load pinned keys
189
+ }
190
+ };
191
+ loadPins();
192
+ }, [visible]);
193
+
194
+ // Save pinned keys when they change
195
+ useEffect(() => {
196
+ if (!hasLoadedPins.current) return; // Don't save on initial load
197
+
198
+ const savePins = async () => {
199
+ try {
200
+ await AsyncStorage.setItem(devToolsStorageKeys.storage.pinnedKeys(), JSON.stringify(Array.from(pinnedKeys)));
201
+ } catch (error) {
202
+ // Failed to save pinned keys
203
+ }
204
+ };
205
+ savePins();
206
+ }, [pinnedKeys]);
207
+
175
208
  // Event listener setup - now handled by useStorageEvents hook
176
209
  // The hook subscribes to the centralized storageEventStore
177
210
 
@@ -206,6 +239,17 @@ export function StorageModalWithTabs({
206
239
  const handleAddPattern = useCallback(pattern => {
207
240
  setIgnoredPatterns(prev => new Set([...prev, pattern]));
208
241
  }, []);
242
+ const handleTogglePin = useCallback(key => {
243
+ setPinnedKeys(prev => {
244
+ const next = new Set(prev);
245
+ if (next.has(key)) {
246
+ next.delete(key);
247
+ } else {
248
+ next.add(key);
249
+ }
250
+ return next;
251
+ });
252
+ }, []);
209
253
  const handleToggleFilters = useCallback(() => {
210
254
  setShowFilters(!showFilters);
211
255
  }, [showFilters]);
@@ -504,7 +548,9 @@ export function StorageModalWithTabs({
504
548
  storageDataRef: storageDataRef,
505
549
  eventCountByKey: eventCountByKey,
506
550
  onViewHistory: handleViewHistoryFromBrowser,
507
- enabledStorageTypes: enabledStorageTypes
551
+ enabledStorageTypes: enabledStorageTypes,
552
+ pinnedKeys: pinnedKeys,
553
+ onTogglePin: handleTogglePin
508
554
  });
509
555
  }
510
556
 
@@ -1,15 +1,26 @@
1
1
  "use strict";
2
2
 
3
- import { useState, useEffect, useCallback } from 'react';
3
+ import { useState, useEffect, useCallback, useRef } from 'react';
4
4
  import AsyncStorage from '@react-native-async-storage/async-storage';
5
+ import { readMany } from '../utils/asyncStorageCompat';
5
6
  export function useAsyncStorageKeys(requiredStorageKeys = []) {
6
7
  // State management
7
8
  const [storageKeys, setStorageKeys] = useState([]);
8
9
  const [isLoading, setIsLoading] = useState(true);
9
10
  const [error, setError] = useState(null);
10
11
 
12
+ // Callers routinely pass a fresh array literal (or rely on the `= []`
13
+ // default, which is a new array every render). Depending on its identity
14
+ // would recreate `fetchStorageData` each render, re-fire the effect, and
15
+ // loop AsyncStorage.getAllKeys/multiGet forever. Depend on a stable content
16
+ // signature instead, and read the latest array via a ref inside the fetch.
17
+ const requiredSignature = JSON.stringify(requiredStorageKeys);
18
+ const requiredRef = useRef(requiredStorageKeys);
19
+ requiredRef.current = requiredStorageKeys;
20
+
11
21
  // Fetch all keys and values from AsyncStorage
12
22
  const fetchStorageData = useCallback(async () => {
23
+ const requiredStorageKeys = requiredRef.current;
13
24
  setIsLoading(true);
14
25
  setError(null);
15
26
  try {
@@ -21,8 +32,8 @@ export function useAsyncStorageKeys(requiredStorageKeys = []) {
21
32
  return;
22
33
  }
23
34
 
24
- // 2. Get all values using multiGet
25
- const allKeyValuePairs = await AsyncStorage.multiGet(allKeys);
35
+ // 2. Get all values (compat helper normalizes v2 multiGet / v3 getMany)
36
+ const allKeyValuePairs = await readMany(allKeys);
26
37
 
27
38
  // 3. Process keys into StorageKeyInfo format
28
39
  const allStorageKeys = [];
@@ -106,7 +117,8 @@ export function useAsyncStorageKeys(requiredStorageKeys = []) {
106
117
  } finally {
107
118
  setIsLoading(false);
108
119
  }
109
- }, [requiredStorageKeys]);
120
+ // eslint-disable-next-line react-hooks/exhaustive-deps
121
+ }, [requiredSignature]);
110
122
 
111
123
  // Initial fetch
112
124
  useEffect(() => {
@@ -13,7 +13,7 @@
13
13
  * - No multiGet - must fetch keys individually
14
14
  */
15
15
 
16
- import { useState, useEffect, useCallback } from 'react';
16
+ import { useState, useEffect, useCallback, useRef } from 'react';
17
17
  import { isMMKVAvailable } from '../utils/mmkvAvailability';
18
18
 
19
19
  // Conditionally import MMKV types
@@ -73,8 +73,16 @@ export function useMMKVKeys(instance, instanceId, requiredStorageKeys = []) {
73
73
  const [isLoading, setIsLoading] = useState(true);
74
74
  const [error, setError] = useState(null);
75
75
 
76
+ // Depend on a stable content signature rather than the array identity — a
77
+ // fresh `requiredStorageKeys` literal each render would otherwise loop the
78
+ // fetch effect. Read the latest array via ref inside the fetch.
79
+ const requiredSignature = JSON.stringify(requiredStorageKeys);
80
+ const requiredRef = useRef(requiredStorageKeys);
81
+ requiredRef.current = requiredStorageKeys;
82
+
76
83
  // Fetch all keys and values from MMKV instance
77
84
  const fetchStorageData = useCallback(() => {
85
+ const requiredStorageKeys = requiredRef.current;
78
86
  setIsLoading(true);
79
87
  setError(null);
80
88
  try {
@@ -206,7 +214,8 @@ export function useMMKVKeys(instance, instanceId, requiredStorageKeys = []) {
206
214
  } finally {
207
215
  setIsLoading(false);
208
216
  }
209
- }, [instance, instanceId, requiredStorageKeys]);
217
+ // eslint-disable-next-line react-hooks/exhaustive-deps
218
+ }, [instance, instanceId, requiredSignature]);
210
219
 
211
220
  // Initial fetch
212
221
  useEffect(() => {
@@ -252,7 +261,15 @@ export function useMultiMMKVKeys(instances, requiredStorageKeys = []) {
252
261
  const [storageKeys, setStorageKeys] = useState([]);
253
262
  const [isLoading, setIsLoading] = useState(true);
254
263
  const [error, setError] = useState(null);
264
+
265
+ // Depend on a stable content signature rather than the array identity — a
266
+ // fresh `requiredStorageKeys` literal each render would otherwise loop the
267
+ // fetch effect. Read the latest array via ref inside the fetch.
268
+ const requiredSignature = JSON.stringify(requiredStorageKeys);
269
+ const requiredRef = useRef(requiredStorageKeys);
270
+ requiredRef.current = requiredStorageKeys;
255
271
  const fetchStorageData = useCallback(() => {
272
+ const requiredStorageKeys = requiredRef.current;
256
273
  setIsLoading(true);
257
274
  setError(null);
258
275
  try {
@@ -346,7 +363,8 @@ export function useMultiMMKVKeys(instances, requiredStorageKeys = []) {
346
363
  } finally {
347
364
  setIsLoading(false);
348
365
  }
349
- }, [instances, requiredStorageKeys]);
366
+ // eslint-disable-next-line react-hooks/exhaustive-deps
367
+ }, [instances, requiredSignature]);
350
368
  useEffect(() => {
351
369
  fetchStorageData();
352
370
  }, [fetchStorageData]);
@@ -25,6 +25,8 @@
25
25
  import { BaseEventStore } from "@buoy-gg/shared-ui";
26
26
  import { startListening as startAsyncStorageListening, addListener as addAsyncStorageListener, isListening as isAsyncStorageListening } from "../utils/AsyncStorageListener";
27
27
  import { addMMKVListener } from "../utils/MMKVListener";
28
+ import { mmkvInstanceRegistry } from "../utils/MMKVInstanceRegistry";
29
+ import { detectMMKVType } from "../utils/mmkvTypeDetection";
28
30
 
29
31
  /**
30
32
  * Unified storage event type combining AsyncStorage and MMKV events.
@@ -50,6 +52,7 @@ class StorageEventStore extends BaseEventStore {
50
52
  // Unsubscribe functions for raw listeners
51
53
  asyncStorageUnsubscribe = null;
52
54
  mmkvUnsubscribe = null;
55
+ hasScannedInitialState = false;
53
56
  constructor() {
54
57
  super({
55
58
  storeName: "storage",
@@ -57,6 +60,84 @@ class StorageEventStore extends BaseEventStore {
57
60
  });
58
61
  }
59
62
 
63
+ /**
64
+ * Scan all registered MMKV instances and AsyncStorage for existing keys,
65
+ * creating synthetic events so that getEvents() includes the current state
66
+ * (not just changes made after capture started). Only runs once per store
67
+ * lifetime to avoid duplicates.
68
+ */
69
+ async scanExistingState() {
70
+ if (this.hasScannedInitialState) return;
71
+ this.hasScannedInitialState = true;
72
+ const now = new Date();
73
+
74
+ // ── MMKV: synchronous scan ──
75
+ const instances = mmkvInstanceRegistry.getAll();
76
+ for (const {
77
+ id: instanceId,
78
+ instance
79
+ } of instances) {
80
+ try {
81
+ const keys = instance.getAllKeys();
82
+ for (const key of keys) {
83
+ const {
84
+ value,
85
+ type: valueType
86
+ } = detectMMKVType(instance, key);
87
+ const actionMap = {
88
+ string: "set.string",
89
+ number: "set.number",
90
+ boolean: "set.boolean",
91
+ buffer: "set.buffer"
92
+ };
93
+ const action = actionMap[valueType] ?? "set.string";
94
+ this.addEvent({
95
+ action,
96
+ timestamp: now,
97
+ instanceId,
98
+ data: {
99
+ key,
100
+ value,
101
+ valueType
102
+ },
103
+ storageType: "mmkv",
104
+ id: nextStorageEventId()
105
+ });
106
+ }
107
+ } catch {
108
+ // Instance may not be accessible — skip silently
109
+ }
110
+ }
111
+
112
+ // ── AsyncStorage: async scan ──
113
+ try {
114
+ const {
115
+ asyncStorage,
116
+ readMany
117
+ } = require("../utils/asyncStorageCompat");
118
+ if (asyncStorage) {
119
+ const allKeys = await asyncStorage.getAllKeys();
120
+ if (allKeys.length > 0) {
121
+ const pairs = await readMany(allKeys);
122
+ for (const [key, rawValue] of pairs) {
123
+ this.addEvent({
124
+ action: "setItem",
125
+ timestamp: now,
126
+ data: {
127
+ key,
128
+ value: rawValue ?? undefined
129
+ },
130
+ storageType: "async",
131
+ id: nextStorageEventId()
132
+ });
133
+ }
134
+ }
135
+ }
136
+ } catch {
137
+ // AsyncStorage not available — skip silently
138
+ }
139
+ }
140
+
60
141
  /**
61
142
  * Start capturing storage events from both AsyncStorage and MMKV
62
143
  */
@@ -89,6 +170,9 @@ class StorageEventStore extends BaseEventStore {
89
170
  this.addEvent(storageEvent);
90
171
  });
91
172
  }
173
+
174
+ // Scan existing keys so getEvents() includes pre-existing state
175
+ await this.scanExistingState();
92
176
  }
93
177
 
94
178
  /**
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+
3
+ import AsyncStorage from "@react-native-async-storage/async-storage";
4
+ import { readMany, writeMany, removeMany } from "../utils/asyncStorageCompat";
5
+ import { storageEventStore } from "../stores/storageEventStore";
6
+ import { clearAllAppStorage } from "../utils/clearAllStorage";
7
+ /**
8
+ * Sync adapter for the storage tool, consumed by @buoy-gg/external-sync's
9
+ * `useExternalSync` (structurally matches its ToolSyncAdapter interface so
10
+ * this package doesn't need a dependency on it).
11
+ *
12
+ * Subscribing starts the underlying capture lifecycle (including the initial
13
+ * key scan), so storage events are only recorded while a dashboard is
14
+ * watching.
15
+ *
16
+ * The `async.*` actions mirror the AsyncStorage API so the desktop dashboard
17
+ * can proxy its browse/edit mode against the device's real storage.
18
+ */
19
+ export const storageSyncAdapter = {
20
+ version: 1,
21
+ getSnapshot: () => storageEventStore.getEvents(),
22
+ subscribe: onChange => storageEventStore.subscribeToEvents(onChange),
23
+ actions: {
24
+ clearEvents: () => {
25
+ storageEventStore.clearEvents();
26
+ },
27
+ /** Clears all app storage keys, preserving dev tools settings. */
28
+ clearAppStorage: () => clearAllAppStorage(),
29
+ // ── Remote AsyncStorage proxy (desktop browse/edit mode) ──
30
+ // Single-item methods share the same signature across async-storage v2/v3;
31
+ // batch methods go through the compat helpers (which translate to v3's
32
+ // getMany/setMany/removeMany) and keep the v2 tuple/pairs shape on the wire.
33
+ "async.getAllKeys": () => AsyncStorage.getAllKeys(),
34
+ "async.multiGet": params => readMany(params.keys),
35
+ "async.getItem": params => AsyncStorage.getItem(params.key),
36
+ "async.setItem": params => {
37
+ const {
38
+ key,
39
+ value
40
+ } = params;
41
+ return AsyncStorage.setItem(key, value);
42
+ },
43
+ "async.removeItem": params => AsyncStorage.removeItem(params.key),
44
+ "async.multiRemove": params => removeMany(params.keys),
45
+ "async.multiSet": params => writeMany(params.pairs),
46
+ "async.clear": () => AsyncStorage.clear()
47
+ }
48
+ };