@buoy-gg/storage 3.0.2 → 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 (56) hide show
  1. package/README.md +1 -1
  2. package/lib/commonjs/storage/components/GameUIStorageBrowser.js +24 -3
  3. package/lib/commonjs/storage/components/SelectionActionBar.js +16 -3
  4. package/lib/commonjs/storage/components/StorageBrowserMode.js +6 -2
  5. package/lib/commonjs/storage/components/StorageKeyRow.js +95 -6
  6. package/lib/commonjs/storage/components/StorageKeySection.js +8 -2
  7. package/lib/commonjs/storage/components/StorageModalWithTabs.js +47 -1
  8. package/lib/commonjs/storage/hooks/useAsyncStorageKeys.js +3 -2
  9. package/lib/commonjs/storage/stores/storageEventStore.js +7 -4
  10. package/lib/commonjs/storage/sync/storageSyncAdapter.js +7 -3
  11. package/lib/commonjs/storage/utils/AsyncStorageListener.js +148 -160
  12. package/lib/commonjs/storage/utils/asyncStorageCompat.js +89 -0
  13. package/lib/commonjs/storage/utils/clearAllStorage.js +2 -1
  14. package/lib/commonjs/storage/utils/mmkvTypeDetection.js +20 -5
  15. package/lib/commonjs/storage/utils/storageTimeTravelUtils.js +3 -2
  16. package/lib/commonjs/storage/utils/valueType.js +41 -0
  17. package/lib/module/storage/components/GameUIStorageBrowser.js +24 -3
  18. package/lib/module/storage/components/SelectionActionBar.js +17 -3
  19. package/lib/module/storage/components/StorageBrowserMode.js +6 -2
  20. package/lib/module/storage/components/StorageKeyRow.js +97 -8
  21. package/lib/module/storage/components/StorageKeySection.js +8 -2
  22. package/lib/module/storage/components/StorageModalWithTabs.js +47 -1
  23. package/lib/module/storage/hooks/useAsyncStorageKeys.js +3 -2
  24. package/lib/module/storage/stores/storageEventStore.js +7 -4
  25. package/lib/module/storage/sync/storageSyncAdapter.js +7 -3
  26. package/lib/module/storage/utils/AsyncStorageListener.js +124 -135
  27. package/lib/module/storage/utils/asyncStorageCompat.js +81 -0
  28. package/lib/module/storage/utils/clearAllStorage.js +2 -1
  29. package/lib/module/storage/utils/mmkvTypeDetection.js +20 -5
  30. package/lib/module/storage/utils/storageTimeTravelUtils.js +3 -2
  31. package/lib/module/storage/utils/valueType.js +39 -0
  32. package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts +5 -1
  33. package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts.map +1 -1
  34. package/lib/typescript/storage/components/SelectionActionBar.d.ts +3 -1
  35. package/lib/typescript/storage/components/SelectionActionBar.d.ts.map +1 -1
  36. package/lib/typescript/storage/components/StorageBrowserMode.d.ts +3 -1
  37. package/lib/typescript/storage/components/StorageBrowserMode.d.ts.map +1 -1
  38. package/lib/typescript/storage/components/StorageKeyRow.d.ts +7 -1
  39. package/lib/typescript/storage/components/StorageKeyRow.d.ts.map +1 -1
  40. package/lib/typescript/storage/components/StorageKeySection.d.ts +7 -1
  41. package/lib/typescript/storage/components/StorageKeySection.d.ts.map +1 -1
  42. package/lib/typescript/storage/components/StorageModalWithTabs.d.ts.map +1 -1
  43. package/lib/typescript/storage/hooks/useAsyncStorageKeys.d.ts.map +1 -1
  44. package/lib/typescript/storage/stores/storageEventStore.d.ts.map +1 -1
  45. package/lib/typescript/storage/sync/storageSyncAdapter.d.ts +1 -1
  46. package/lib/typescript/storage/sync/storageSyncAdapter.d.ts.map +1 -1
  47. package/lib/typescript/storage/utils/AsyncStorageListener.d.ts +20 -0
  48. package/lib/typescript/storage/utils/AsyncStorageListener.d.ts.map +1 -1
  49. package/lib/typescript/storage/utils/asyncStorageCompat.d.ts +30 -0
  50. package/lib/typescript/storage/utils/asyncStorageCompat.d.ts.map +1 -0
  51. package/lib/typescript/storage/utils/clearAllStorage.d.ts.map +1 -1
  52. package/lib/typescript/storage/utils/mmkvTypeDetection.d.ts.map +1 -1
  53. package/lib/typescript/storage/utils/storageTimeTravelUtils.d.ts.map +1 -1
  54. package/lib/typescript/storage/utils/valueType.d.ts +13 -0
  55. package/lib/typescript/storage/utils/valueType.d.ts.map +1 -1
  56. package/package.json +6 -6
@@ -4,10 +4,11 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.stopListening = exports.startListening = exports.resumeCapture = exports.removeAllListeners = exports.pauseCapture = exports.isPaused = exports.isListening = exports.getListenerCount = exports.default = exports.addListener = void 0;
7
- var _asyncStorage = _interopRequireDefault(require("@react-native-async-storage/async-storage"));
8
- function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
7
+ var _asyncStorageCompat = require("./asyncStorageCompat");
9
8
  // AsyncStorage method signatures - matching the actual AsyncStorage API
10
9
 
10
+ // v3-only batch methods (replace multiSet/multiRemove)
11
+
11
12
  // Event types for AsyncStorage operations
12
13
 
13
14
  /**
@@ -18,6 +19,12 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
18
19
  * and emits events to registered listeners. It maintains the original functionality
19
20
  * while providing observability for debugging and development tools.
20
21
  *
22
+ * Supports both async-storage v2 (multiGet/multiSet/multiRemove/mergeItem) and
23
+ * v3 (getMany/setMany/removeMany, no merge). On v3 the renamed batch methods are
24
+ * swizzled and normalized back to the same v2-shaped events, so the rest of the
25
+ * tool — event store, time-travel, desktop sync — is version-agnostic. v3 dropped
26
+ * the merge API entirely, so no merge events can occur there.
27
+ *
21
28
  * @example
22
29
  * ```typescript
23
30
  * // Start listening to all AsyncStorage operations
@@ -58,9 +65,13 @@ class AsyncStorageListener {
58
65
  originalRemoveItem = null;
59
66
  originalMergeItem = null;
60
67
  originalClear = null;
68
+ // v2 batch originals
61
69
  originalMultiSet = null;
62
70
  originalMultiRemove = null;
63
71
  originalMultiMerge = null;
72
+ // v3 batch originals
73
+ originalSetMany = null;
74
+ originalRemoveMany = null;
64
75
 
65
76
  /**
66
77
  * Determines if a storage key should be ignored to prevent infinite loops
@@ -99,19 +110,27 @@ class AsyncStorageListener {
99
110
  }
100
111
 
101
112
  // Check if methods are already swizzled by checking the function name
102
- if (_asyncStorage.default.setItem.name === "swizzled_setItem") {
113
+ if (_asyncStorageCompat.asyncStorage.setItem.name === "swizzled_setItem") {
103
114
  // Don't store swizzled methods as originals
104
115
  return false;
105
116
  }
106
117
 
107
- // Store original methods (these should be the real AsyncStorage methods)
108
- this.originalSetItem = _asyncStorage.default.setItem.bind(_asyncStorage.default);
109
- this.originalRemoveItem = _asyncStorage.default.removeItem.bind(_asyncStorage.default);
110
- this.originalMergeItem = _asyncStorage.default.mergeItem.bind(_asyncStorage.default);
111
- this.originalClear = _asyncStorage.default.clear.bind(_asyncStorage.default);
112
- this.originalMultiSet = _asyncStorage.default.multiSet.bind(_asyncStorage.default);
113
- this.originalMultiRemove = _asyncStorage.default.multiRemove.bind(_asyncStorage.default);
114
- this.originalMultiMerge = _asyncStorage.default.multiMerge ? _asyncStorage.default.multiMerge.bind(_asyncStorage.default) : null;
118
+ // Store original methods (these should be the real AsyncStorage methods).
119
+ // Single-item + clear are identical across v2/v3.
120
+ this.originalSetItem = _asyncStorageCompat.asyncStorage.setItem.bind(_asyncStorageCompat.asyncStorage);
121
+ this.originalRemoveItem = _asyncStorageCompat.asyncStorage.removeItem.bind(_asyncStorageCompat.asyncStorage);
122
+ this.originalClear = _asyncStorageCompat.asyncStorage.clear.bind(_asyncStorageCompat.asyncStorage);
123
+ if (_asyncStorageCompat.asyncStorageCaps.hasLegacyMultiApi) {
124
+ // v2: multiSet/multiRemove + merge API
125
+ this.originalMultiSet = _asyncStorageCompat.asyncStorage.multiSet.bind(_asyncStorageCompat.asyncStorage);
126
+ this.originalMultiRemove = _asyncStorageCompat.asyncStorage.multiRemove.bind(_asyncStorageCompat.asyncStorage);
127
+ this.originalMergeItem = _asyncStorageCompat.asyncStorage.mergeItem.bind(_asyncStorageCompat.asyncStorage);
128
+ this.originalMultiMerge = _asyncStorageCompat.asyncStorage.multiMerge ? _asyncStorageCompat.asyncStorage.multiMerge.bind(_asyncStorageCompat.asyncStorage) : null;
129
+ } else {
130
+ // v3: setMany/removeMany; merge API was removed (nothing to capture)
131
+ this.originalSetMany = _asyncStorageCompat.asyncStorage.setMany.bind(_asyncStorageCompat.asyncStorage);
132
+ this.originalRemoveMany = _asyncStorageCompat.asyncStorage.removeMany.bind(_asyncStorageCompat.asyncStorage);
133
+ }
115
134
 
116
135
  // Original methods stored successfully
117
136
  this.isInitialized = true;
@@ -125,27 +144,35 @@ class AsyncStorageListener {
125
144
  * AsyncStorage methods that were saved during initialization.
126
145
  */
127
146
  restoreOriginalMethods() {
128
- if (!_asyncStorage.default || !this.originalSetItem) {
147
+ if (!_asyncStorageCompat.asyncStorage || !this.originalSetItem) {
129
148
  return;
130
149
  }
131
- _asyncStorage.default.setItem = this.originalSetItem;
150
+ _asyncStorageCompat.asyncStorage.setItem = this.originalSetItem;
132
151
  if (this.originalRemoveItem) {
133
- _asyncStorage.default.removeItem = this.originalRemoveItem;
134
- }
135
- if (this.originalMergeItem) {
136
- _asyncStorage.default.mergeItem = this.originalMergeItem;
152
+ _asyncStorageCompat.asyncStorage.removeItem = this.originalRemoveItem;
137
153
  }
138
154
  if (this.originalClear) {
139
- _asyncStorage.default.clear = this.originalClear;
155
+ _asyncStorageCompat.asyncStorage.clear = this.originalClear;
156
+ }
157
+ // v2 batch + merge
158
+ if (this.originalMergeItem) {
159
+ _asyncStorageCompat.asyncStorage.mergeItem = this.originalMergeItem;
140
160
  }
141
161
  if (this.originalMultiSet) {
142
- _asyncStorage.default.multiSet = this.originalMultiSet;
162
+ _asyncStorageCompat.asyncStorage.multiSet = this.originalMultiSet;
143
163
  }
144
164
  if (this.originalMultiRemove) {
145
- _asyncStorage.default.multiRemove = this.originalMultiRemove;
165
+ _asyncStorageCompat.asyncStorage.multiRemove = this.originalMultiRemove;
146
166
  }
147
167
  if (this.originalMultiMerge) {
148
- _asyncStorage.default.multiMerge = this.originalMultiMerge;
168
+ _asyncStorageCompat.asyncStorage.multiMerge = this.originalMultiMerge;
169
+ }
170
+ // v3 batch
171
+ if (this.originalSetMany) {
172
+ _asyncStorageCompat.asyncStorage.setMany = this.originalSetMany;
173
+ }
174
+ if (this.originalRemoveMany) {
175
+ _asyncStorageCompat.asyncStorage.removeMany = this.originalRemoveMany;
149
176
  }
150
177
  }
151
178
 
@@ -175,6 +202,77 @@ class AsyncStorageListener {
175
202
  });
176
203
  }
177
204
 
205
+ /**
206
+ * Shared core for batch writes (v2 multiSet/multiMerge, v3 setMany).
207
+ *
208
+ * Captures previous values for the affected keys, runs the original native
209
+ * operation, and emits a normalized event. `action` distinguishes a plain set
210
+ * from a merge so time-travel can replay them differently.
211
+ */
212
+ async handleBatchSet(pairs, runOriginal, action = "multiSet") {
213
+ // Filter out ignored keys
214
+ const filteredPairs = pairs.filter(([key]) => !this.shouldIgnoreKey(key));
215
+ if (filteredPairs.length === 0) {
216
+ return runOriginal();
217
+ }
218
+
219
+ // Capture previous values for all keys being written
220
+ const keysToSet = filteredPairs.map(([key]) => key);
221
+ let prevPairs = [];
222
+ try {
223
+ prevPairs = await (0, _asyncStorageCompat.readMany)(keysToSet);
224
+ } catch {
225
+ // Failed to capture previous values
226
+ }
227
+
228
+ // Execute the operation
229
+ const result = await runOriginal();
230
+
231
+ // Emit event with previous values
232
+ this.emit({
233
+ action,
234
+ timestamp: new Date(),
235
+ data: {
236
+ pairs: filteredPairs,
237
+ prevPairs
238
+ }
239
+ });
240
+ return result;
241
+ }
242
+
243
+ /**
244
+ * Shared core for batch removals (v2 multiRemove, v3 removeMany).
245
+ */
246
+ async handleBatchRemove(keys, runOriginal) {
247
+ // Filter out ignored keys
248
+ const filteredKeys = keys.filter(key => !this.shouldIgnoreKey(key));
249
+ if (filteredKeys.length === 0) {
250
+ return runOriginal();
251
+ }
252
+
253
+ // Capture previous values for all keys being removed
254
+ let prevPairs = [];
255
+ try {
256
+ prevPairs = await (0, _asyncStorageCompat.readMany)(filteredKeys);
257
+ } catch {
258
+ // Failed to capture previous values
259
+ }
260
+
261
+ // Execute the operation
262
+ const result = await runOriginal();
263
+
264
+ // Emit event with previous values
265
+ this.emit({
266
+ action: "multiRemove",
267
+ timestamp: new Date(),
268
+ data: {
269
+ keys: filteredKeys,
270
+ prevPairs
271
+ }
272
+ });
273
+ return result;
274
+ }
275
+
178
276
  /**
179
277
  * Start intercepting AsyncStorage operations by swizzling methods
180
278
  *
@@ -196,7 +294,7 @@ class AsyncStorageListener {
196
294
  }
197
295
 
198
296
  // Check if methods are already swizzled (this can happen if initialize was called twice somehow)
199
- if (_asyncStorage.default && _asyncStorage.default.setItem.name === "swizzled_setItem") {
297
+ if (_asyncStorageCompat.asyncStorage && _asyncStorageCompat.asyncStorage.setItem.name === "swizzled_setItem") {
200
298
  this.restoreOriginalMethods();
201
299
  }
202
300
 
@@ -207,7 +305,7 @@ class AsyncStorageListener {
207
305
  // Only capture and emit if key is not ignored
208
306
  if (!this.shouldIgnoreKey(key)) {
209
307
  // Capture previous value before the operation
210
- const prevValue = this.originalSetItem ? await _asyncStorage.default.getItem(key) : null;
308
+ const prevValue = this.originalSetItem ? await _asyncStorageCompat.asyncStorage.getItem(key) : null;
211
309
 
212
310
  // Execute the operation
213
311
  const result = this.originalSetItem ? await this.originalSetItem(key, value) : undefined;
@@ -229,17 +327,17 @@ class AsyncStorageListener {
229
327
  Object.defineProperty(swizzled_setItem, "name", {
230
328
  value: "swizzled_setItem"
231
329
  });
232
- if (_asyncStorage.default) {
233
- _asyncStorage.default.setItem = swizzled_setItem;
330
+ if (_asyncStorageCompat.asyncStorage) {
331
+ _asyncStorageCompat.asyncStorage.setItem = swizzled_setItem;
234
332
  }
235
333
 
236
334
  // Swizzle removeItem
237
- if (_asyncStorage.default) {
238
- _asyncStorage.default.removeItem = async key => {
335
+ if (_asyncStorageCompat.asyncStorage) {
336
+ _asyncStorageCompat.asyncStorage.removeItem = async key => {
239
337
  // Only capture and emit if key is not ignored
240
338
  if (!this.shouldIgnoreKey(key)) {
241
339
  // Capture previous value before the operation
242
- const prevValue = this.originalRemoveItem ? await _asyncStorage.default.getItem(key) : null;
340
+ const prevValue = this.originalRemoveItem ? await _asyncStorageCompat.asyncStorage.getItem(key) : null;
243
341
 
244
342
  // Execute the operation
245
343
  const result = this.originalRemoveItem ? await this.originalRemoveItem(key) : undefined;
@@ -259,13 +357,13 @@ class AsyncStorageListener {
259
357
  };
260
358
  }
261
359
 
262
- // Swizzle mergeItem
263
- if (_asyncStorage.default) {
264
- _asyncStorage.default.mergeItem = async (key, value) => {
360
+ // Swizzle mergeItem (v2 only — v3 removed the merge API)
361
+ if (_asyncStorageCompat.asyncStorageCaps.hasMergeApi && _asyncStorageCompat.asyncStorage) {
362
+ _asyncStorageCompat.asyncStorage.mergeItem = async (key, value) => {
265
363
  // Only capture and emit if key is not ignored
266
364
  if (!this.shouldIgnoreKey(key)) {
267
365
  // Capture previous value before the operation
268
- const prevValue = this.originalMergeItem ? await _asyncStorage.default.getItem(key) : null;
366
+ const prevValue = this.originalMergeItem ? await _asyncStorageCompat.asyncStorage.getItem(key) : null;
269
367
 
270
368
  // Execute the operation
271
369
  const result = this.originalMergeItem ? await this.originalMergeItem(key, value) : undefined;
@@ -287,17 +385,16 @@ class AsyncStorageListener {
287
385
  }
288
386
 
289
387
  // Swizzle clear
290
- if (_asyncStorage.default) {
291
- _asyncStorage.default.clear = async () => {
388
+ if (_asyncStorageCompat.asyncStorage) {
389
+ _asyncStorageCompat.asyncStorage.clear = async () => {
292
390
  // Capture all key-value pairs before clearing
293
391
  let prevPairs = [];
294
392
  try {
295
- const allKeys = await _asyncStorage.default.getAllKeys();
393
+ const allKeys = await _asyncStorageCompat.asyncStorage.getAllKeys();
296
394
  // Filter out ignored keys
297
395
  const keysToCapture = allKeys.filter(key => !this.shouldIgnoreKey(key));
298
396
  if (keysToCapture.length > 0) {
299
- const keyValuePairs = await _asyncStorage.default.multiGet(keysToCapture);
300
- prevPairs = keyValuePairs;
397
+ prevPairs = await (0, _asyncStorageCompat.readMany)(keysToCapture);
301
398
  }
302
399
  } catch {
303
400
  // Failed to capture previous state
@@ -318,105 +415,18 @@ class AsyncStorageListener {
318
415
  };
319
416
  }
320
417
 
321
- // Swizzle multiSet
322
- if (_asyncStorage.default) {
323
- _asyncStorage.default.multiSet = async keyValuePairs => {
324
- // Filter out ignored keys
325
- const filteredPairs = keyValuePairs.filter(([key]) => !this.shouldIgnoreKey(key));
326
- if (filteredPairs.length > 0) {
327
- // Capture previous values for all keys being set
328
- const keysToSet = filteredPairs.map(([key]) => key);
329
- let prevPairs = [];
330
- try {
331
- const existingValues = await _asyncStorage.default.multiGet(keysToSet);
332
- prevPairs = existingValues;
333
- } catch {
334
- // Failed to capture previous values
335
- }
336
-
337
- // Execute the operation
338
- const result = this.originalMultiSet ? await this.originalMultiSet(keyValuePairs) : undefined;
339
-
340
- // Emit event with previous values
341
- this.emit({
342
- action: "multiSet",
343
- timestamp: new Date(),
344
- data: {
345
- pairs: filteredPairs,
346
- prevPairs
347
- }
348
- });
349
- return result;
350
- }
351
- return this.originalMultiSet ? this.originalMultiSet(keyValuePairs) : Promise.resolve();
352
- };
353
- }
354
-
355
- // Swizzle multiRemove
356
- if (_asyncStorage.default) {
357
- _asyncStorage.default.multiRemove = async keys => {
358
- // Filter out ignored keys
359
- const filteredKeys = keys.filter(key => !this.shouldIgnoreKey(key));
360
- if (filteredKeys.length > 0) {
361
- // Capture previous values for all keys being removed
362
- let prevPairs = [];
363
- try {
364
- const existingValues = await _asyncStorage.default.multiGet(filteredKeys);
365
- prevPairs = existingValues;
366
- } catch {
367
- // Failed to capture previous values
368
- }
369
-
370
- // Execute the operation
371
- const result = this.originalMultiRemove ? await this.originalMultiRemove(keys) : undefined;
372
-
373
- // Emit event with previous values
374
- this.emit({
375
- action: "multiRemove",
376
- timestamp: new Date(),
377
- data: {
378
- keys: filteredKeys,
379
- prevPairs
380
- }
381
- });
382
- return result;
383
- }
384
- return this.originalMultiRemove ? this.originalMultiRemove(keys) : Promise.resolve();
385
- };
386
- }
387
-
388
- // Swizzle multiMerge if available
389
- if (this.originalMultiMerge && _asyncStorage.default) {
390
- _asyncStorage.default.multiMerge = async keyValuePairs => {
391
- // Filter out ignored keys
392
- const filteredPairs = keyValuePairs.filter(([key]) => !this.shouldIgnoreKey(key));
393
- if (filteredPairs.length > 0) {
394
- // Capture previous values for all keys being merged
395
- const keysToMerge = filteredPairs.map(([key]) => key);
396
- let prevPairs = [];
397
- try {
398
- const existingValues = await _asyncStorage.default.multiGet(keysToMerge);
399
- prevPairs = existingValues;
400
- } catch {
401
- // Failed to capture previous values
402
- }
403
-
404
- // Execute the operation
405
- const result = this.originalMultiMerge ? await this.originalMultiMerge(keyValuePairs) : undefined;
406
-
407
- // Emit event with previous values
408
- this.emit({
409
- action: "multiMerge",
410
- timestamp: new Date(),
411
- data: {
412
- pairs: filteredPairs,
413
- prevPairs
414
- }
415
- });
416
- return result;
417
- }
418
- return this.originalMultiMerge ? this.originalMultiMerge(keyValuePairs) : Promise.resolve();
419
- };
418
+ // Swizzle the batch methods for whichever API the installed version exposes.
419
+ if (_asyncStorageCompat.asyncStorageCaps.hasLegacyMultiApi && _asyncStorageCompat.asyncStorage) {
420
+ // ── v2: multiSet / multiRemove / multiMerge ──
421
+ _asyncStorageCompat.asyncStorage.multiSet = keyValuePairs => this.handleBatchSet(keyValuePairs, () => this.originalMultiSet ? this.originalMultiSet(keyValuePairs) : Promise.resolve());
422
+ _asyncStorageCompat.asyncStorage.multiRemove = keys => this.handleBatchRemove(keys, () => this.originalMultiRemove ? this.originalMultiRemove(keys) : Promise.resolve());
423
+ if (this.originalMultiMerge) {
424
+ _asyncStorageCompat.asyncStorage.multiMerge = keyValuePairs => this.handleBatchSet(keyValuePairs, () => this.originalMultiMerge ? this.originalMultiMerge(keyValuePairs) : Promise.resolve(), "multiMerge");
425
+ }
426
+ } else if (_asyncStorageCompat.asyncStorage) {
427
+ // ── v3: setMany / removeMany (normalized back to multiSet/multiRemove events) ──
428
+ _asyncStorageCompat.asyncStorage.setMany = entries => this.handleBatchSet(Object.entries(entries), () => this.originalSetMany ? this.originalSetMany(entries) : Promise.resolve());
429
+ _asyncStorageCompat.asyncStorage.removeMany = keys => this.handleBatchRemove(keys, () => this.originalRemoveMany ? this.originalRemoveMany(keys) : Promise.resolve());
420
430
  }
421
431
  this.isListening = true;
422
432
  // Started listening successfully
@@ -432,34 +442,12 @@ class AsyncStorageListener {
432
442
  if (!this.isListening) {
433
443
  return;
434
444
  }
435
- if (!_asyncStorage.default) {
445
+ if (!_asyncStorageCompat.asyncStorage) {
436
446
  return;
437
447
  }
438
448
 
439
449
  // Stopping listener and restoring original methods
440
-
441
- // Restore original methods
442
- if (this.originalSetItem) {
443
- _asyncStorage.default.setItem = this.originalSetItem;
444
- }
445
- if (this.originalRemoveItem) {
446
- _asyncStorage.default.removeItem = this.originalRemoveItem;
447
- }
448
- if (this.originalMergeItem) {
449
- _asyncStorage.default.mergeItem = this.originalMergeItem;
450
- }
451
- if (this.originalClear) {
452
- _asyncStorage.default.clear = this.originalClear;
453
- }
454
- if (this.originalMultiSet) {
455
- _asyncStorage.default.multiSet = this.originalMultiSet;
456
- }
457
- if (this.originalMultiRemove) {
458
- _asyncStorage.default.multiRemove = this.originalMultiRemove;
459
- }
460
- if (this.originalMultiMerge) {
461
- _asyncStorage.default.multiMerge = this.originalMultiMerge;
462
- }
450
+ this.restoreOriginalMethods();
463
451
  this.isListening = false;
464
452
  // Stopped listening successfully
465
453
  }
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = exports.asyncStorageCaps = exports.asyncStorage = void 0;
7
+ exports.readMany = readMany;
8
+ exports.removeMany = removeMany;
9
+ exports.writeMany = writeMany;
10
+ var _asyncStorage = _interopRequireDefault(require("@react-native-async-storage/async-storage"));
11
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
12
+ /**
13
+ * AsyncStorage compatibility adapter (v2 + v3)
14
+ *
15
+ * `@react-native-async-storage/async-storage` 3.x reshaped the default-export
16
+ * API. The single-item methods kept their v2 signatures, but the batch + merge
17
+ * methods changed:
18
+ *
19
+ * v2 (what this package was written against) v3
20
+ * ------------------------------------------ --------------------------------
21
+ * multiGet(keys): [key, value|null][] getMany(keys): Record<key, value|null>
22
+ * multiSet([[k, v]]): void setMany({ k: v }): void
23
+ * multiRemove(keys): void removeMany(keys): void
24
+ * mergeItem / multiMerge removed entirely
25
+ *
26
+ * This module is the single seam between the storage tool and whichever
27
+ * async-storage version the host app installed. The rest of the package always
28
+ * speaks the v2 tuple-shaped API; we translate to v3 here when needed.
29
+ *
30
+ * Reads (`readMany`) are routed through the *unswizzled* native methods, so the
31
+ * event listener can safely call them while capturing previous values without
32
+ * re-triggering itself.
33
+ */
34
+
35
+ // Loosely typed because the surface differs across major versions; callers go
36
+ // through the helpers below rather than touching this directly.
37
+
38
+ /** The live default-export object (methods may be swizzled by the listener). */
39
+ const asyncStorage = exports.asyncStorage = _asyncStorage.default;
40
+
41
+ /**
42
+ * Capability flags describing which API surface the installed version exposes.
43
+ * `multiGet` only exists on v2; `mergeItem` was removed in v3.
44
+ */
45
+ const asyncStorageCaps = exports.asyncStorageCaps = {
46
+ /** v2 exposes multiGet/multiSet/multiRemove; v3 renamed these to *Many. */
47
+ hasLegacyMultiApi: typeof asyncStorage?.multiGet === "function",
48
+ /** v3 removed mergeItem/multiMerge with no replacement. */
49
+ hasMergeApi: typeof asyncStorage?.mergeItem === "function"
50
+ };
51
+
52
+ /**
53
+ * Batch-read keys, normalized to the v2 `[key, value|null][]` tuple shape and
54
+ * preserving the requested key order regardless of installed version.
55
+ */
56
+ async function readMany(keys) {
57
+ if (typeof asyncStorage.multiGet === "function") {
58
+ return await asyncStorage.multiGet(keys);
59
+ }
60
+ // v3: getMany returns a Record; rebuild ordered tuples ("what you request is
61
+ // what you get" — missing keys come back as null).
62
+ const record = await asyncStorage.getMany(keys);
63
+ return keys.map(key => [key, record[key] ?? null]);
64
+ }
65
+
66
+ /**
67
+ * Batch-write key/value pairs. Calls the live method (which may be swizzled by
68
+ * the listener on v2's `multiSet` / v3's `setMany`) so writes still emit events.
69
+ */
70
+ async function writeMany(pairs) {
71
+ if (typeof asyncStorage.multiSet === "function") {
72
+ return asyncStorage.multiSet(pairs);
73
+ }
74
+ const record = {};
75
+ for (const [key, value] of pairs) record[key] = value;
76
+ return asyncStorage.setMany(record);
77
+ }
78
+
79
+ /**
80
+ * Batch-remove keys via the live method (swizzled on v2's `multiRemove` /
81
+ * v3's `removeMany`).
82
+ */
83
+ async function removeMany(keys) {
84
+ if (typeof asyncStorage.multiRemove === "function") {
85
+ return asyncStorage.multiRemove(keys);
86
+ }
87
+ return asyncStorage.removeMany(keys);
88
+ }
89
+ var _default = exports.default = asyncStorage;
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.clearAllAppStorage = clearAllAppStorage;
7
7
  exports.clearAllStorageIncludingDevTools = clearAllStorageIncludingDevTools;
8
8
  var _asyncStorage = _interopRequireDefault(require("@react-native-async-storage/async-storage"));
9
+ var _asyncStorageCompat = require("./asyncStorageCompat");
9
10
  var _sharedUi = require("@buoy-gg/shared-ui");
10
11
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
11
12
  /**
@@ -30,7 +31,7 @@ async function clearAllAppStorage() {
30
31
  // Clearing ${keysToRemove.length} app storage keys
31
32
 
32
33
  // Remove all non-dev-tool keys
33
- await _asyncStorage.default.multiRemove(keysToRemove);
34
+ await (0, _asyncStorageCompat.removeMany)(keysToRemove);
34
35
 
35
36
  // Successfully cleared app storage
36
37
  }
@@ -42,16 +42,25 @@ exports.isTypeMatch = isTypeMatch;
42
42
  * ```
43
43
  */
44
44
  function detectMMKVType(instance, key) {
45
- // Try string (most common)
45
+ // MMKV stores no type metadata, so the native getters can cross-read: in
46
+ // particular `getString` returns "" (an empty string, NOT undefined) for many
47
+ // non-string values like numbers and `false`. Trying getString first and
48
+ // trusting its result therefore mis-detects those as empty strings — e.g.
49
+ // a key set to 42 or false would render as "". Probe the getters and only let
50
+ // getString win when it returns a NON-empty string; treat an empty result as
51
+ // ambiguous and prefer the number/boolean getters.
46
52
  const stringValue = instance.getString(key);
47
- if (stringValue !== undefined) {
53
+
54
+ // A non-empty string is unambiguously a string.
55
+ if (stringValue !== undefined && stringValue !== '') {
48
56
  return {
49
57
  value: stringValue,
50
58
  type: 'string'
51
59
  };
52
60
  }
53
61
 
54
- // Try number
62
+ // Empty getString result is ambiguous (genuine "" vs a number/boolean the
63
+ // native layer decoded to ""). Prefer typed getters when they have a value.
55
64
  const numberValue = instance.getNumber(key);
56
65
  if (numberValue !== undefined) {
57
66
  return {
@@ -59,8 +68,6 @@ function detectMMKVType(instance, key) {
59
68
  type: 'number'
60
69
  };
61
70
  }
62
-
63
- // Try boolean
64
71
  const booleanValue = instance.getBoolean(key);
65
72
  if (booleanValue !== undefined) {
66
73
  return {
@@ -69,6 +76,14 @@ function detectMMKVType(instance, key) {
69
76
  };
70
77
  }
71
78
 
79
+ // getString returned "" and nothing else matched -> a genuine empty string.
80
+ if (stringValue === '') {
81
+ return {
82
+ value: '',
83
+ type: 'string'
84
+ };
85
+ }
86
+
72
87
  // Try buffer (least common)
73
88
  const bufferValue = instance.getBuffer(key);
74
89
  if (bufferValue !== undefined) {
@@ -7,6 +7,7 @@ exports.canUndo = canUndo;
7
7
  exports.jumpToState = jumpToState;
8
8
  exports.undoOperation = undoOperation;
9
9
  var _asyncStorage = _interopRequireDefault(require("@react-native-async-storage/async-storage"));
10
+ var _asyncStorageCompat = require("./asyncStorageCompat");
10
11
  var _AsyncStorageListener = require("./AsyncStorageListener");
11
12
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
12
13
  /**
@@ -79,7 +80,7 @@ async function undoOperation(event) {
79
80
  // Restore all cleared key-value pairs
80
81
  const pairsToRestore = data.prevPairs.filter(([, value]) => value !== null);
81
82
  if (pairsToRestore.length > 0) {
82
- await _asyncStorage.default.multiSet(pairsToRestore);
83
+ await (0, _asyncStorageCompat.writeMany)(pairsToRestore);
83
84
  }
84
85
  }
85
86
  break;
@@ -131,7 +132,7 @@ async function jumpToState(events, targetEventIndex) {
131
132
  }
132
133
  }
133
134
  if (pairsToSet.length > 0) {
134
- await _asyncStorage.default.multiSet(pairsToSet);
135
+ await (0, _asyncStorageCompat.writeMany)(pairsToSet);
135
136
  }
136
137
  } finally {
137
138
  // Always resume capture
@@ -3,7 +3,9 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ exports.getValuePreview = getValuePreview;
6
7
  exports.getValueTypeLabel = getValueTypeLabel;
8
+ exports.getValueTypeWithPreview = getValueTypeWithPreview;
7
9
  /** Return a concise string describing the JavaScript type of the provided value. */
8
10
  function getValueTypeLabel(value) {
9
11
  if (value === null) return "null";
@@ -15,4 +17,43 @@ function getValueTypeLabel(value) {
15
17
  if (type === "number") return "number";
16
18
  if (type === "string") return "string";
17
19
  return "unknown";
20
+ }
21
+
22
+ /** Longest value preview rendered inline next to the type label. */
23
+ const MAX_INLINE_PREVIEW = 40;
24
+
25
+ /**
26
+ * Short, human-readable preview of a value for inline display next to the type
27
+ * label, or `null` when the value is too large/complex to preview (long
28
+ * strings, objects, arrays). Lets the user read simple values without opening
29
+ * the card.
30
+ */
31
+ function getValuePreview(value) {
32
+ switch (typeof value) {
33
+ case "boolean":
34
+ return value ? "true" : "false";
35
+ case "number":
36
+ return Number.isFinite(value) ? String(value) : null;
37
+ case "string":
38
+ {
39
+ if (value.length === 0) return '""';
40
+ if (value.length > MAX_INLINE_PREVIEW) return null;
41
+ // Collapse whitespace so multi-line/tabbed values stay on one line.
42
+ return `"${value.replace(/\s+/g, " ")}"`;
43
+ }
44
+ default:
45
+ return null;
46
+ // objects, arrays, null, undefined
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Type label optionally followed by a short value preview — e.g. `boolean · true`,
52
+ * `number · 42`, `string · "hello"`. Falls back to just the type label when the
53
+ * value is too large to preview.
54
+ */
55
+ function getValueTypeWithPreview(value) {
56
+ const label = getValueTypeLabel(value);
57
+ const preview = getValuePreview(value);
58
+ return preview ? `${label} · ${preview}` : label;
18
59
  }