@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
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
 
3
- import AsyncStorage from "@react-native-async-storage/async-storage";
3
+ import { asyncStorage as AsyncStorage, asyncStorageCaps, readMany } from "./asyncStorageCompat";
4
4
 
5
5
  // AsyncStorage method signatures - matching the actual AsyncStorage API
6
6
 
7
+ // v3-only batch methods (replace multiSet/multiRemove)
8
+
7
9
  // Event types for AsyncStorage operations
8
10
 
9
11
  /**
@@ -14,6 +16,12 @@ import AsyncStorage from "@react-native-async-storage/async-storage";
14
16
  * and emits events to registered listeners. It maintains the original functionality
15
17
  * while providing observability for debugging and development tools.
16
18
  *
19
+ * Supports both async-storage v2 (multiGet/multiSet/multiRemove/mergeItem) and
20
+ * v3 (getMany/setMany/removeMany, no merge). On v3 the renamed batch methods are
21
+ * swizzled and normalized back to the same v2-shaped events, so the rest of the
22
+ * tool — event store, time-travel, desktop sync — is version-agnostic. v3 dropped
23
+ * the merge API entirely, so no merge events can occur there.
24
+ *
17
25
  * @example
18
26
  * ```typescript
19
27
  * // Start listening to all AsyncStorage operations
@@ -54,9 +62,13 @@ class AsyncStorageListener {
54
62
  originalRemoveItem = null;
55
63
  originalMergeItem = null;
56
64
  originalClear = null;
65
+ // v2 batch originals
57
66
  originalMultiSet = null;
58
67
  originalMultiRemove = null;
59
68
  originalMultiMerge = null;
69
+ // v3 batch originals
70
+ originalSetMany = null;
71
+ originalRemoveMany = null;
60
72
 
61
73
  /**
62
74
  * Determines if a storage key should be ignored to prevent infinite loops
@@ -100,14 +112,22 @@ class AsyncStorageListener {
100
112
  return false;
101
113
  }
102
114
 
103
- // Store original methods (these should be the real AsyncStorage methods)
115
+ // Store original methods (these should be the real AsyncStorage methods).
116
+ // Single-item + clear are identical across v2/v3.
104
117
  this.originalSetItem = AsyncStorage.setItem.bind(AsyncStorage);
105
118
  this.originalRemoveItem = AsyncStorage.removeItem.bind(AsyncStorage);
106
- this.originalMergeItem = AsyncStorage.mergeItem.bind(AsyncStorage);
107
119
  this.originalClear = AsyncStorage.clear.bind(AsyncStorage);
108
- this.originalMultiSet = AsyncStorage.multiSet.bind(AsyncStorage);
109
- this.originalMultiRemove = AsyncStorage.multiRemove.bind(AsyncStorage);
110
- this.originalMultiMerge = AsyncStorage.multiMerge ? AsyncStorage.multiMerge.bind(AsyncStorage) : null;
120
+ if (asyncStorageCaps.hasLegacyMultiApi) {
121
+ // v2: multiSet/multiRemove + merge API
122
+ this.originalMultiSet = AsyncStorage.multiSet.bind(AsyncStorage);
123
+ this.originalMultiRemove = AsyncStorage.multiRemove.bind(AsyncStorage);
124
+ this.originalMergeItem = AsyncStorage.mergeItem.bind(AsyncStorage);
125
+ this.originalMultiMerge = AsyncStorage.multiMerge ? AsyncStorage.multiMerge.bind(AsyncStorage) : null;
126
+ } else {
127
+ // v3: setMany/removeMany; merge API was removed (nothing to capture)
128
+ this.originalSetMany = AsyncStorage.setMany.bind(AsyncStorage);
129
+ this.originalRemoveMany = AsyncStorage.removeMany.bind(AsyncStorage);
130
+ }
111
131
 
112
132
  // Original methods stored successfully
113
133
  this.isInitialized = true;
@@ -128,12 +148,13 @@ class AsyncStorageListener {
128
148
  if (this.originalRemoveItem) {
129
149
  AsyncStorage.removeItem = this.originalRemoveItem;
130
150
  }
131
- if (this.originalMergeItem) {
132
- AsyncStorage.mergeItem = this.originalMergeItem;
133
- }
134
151
  if (this.originalClear) {
135
152
  AsyncStorage.clear = this.originalClear;
136
153
  }
154
+ // v2 batch + merge
155
+ if (this.originalMergeItem) {
156
+ AsyncStorage.mergeItem = this.originalMergeItem;
157
+ }
137
158
  if (this.originalMultiSet) {
138
159
  AsyncStorage.multiSet = this.originalMultiSet;
139
160
  }
@@ -143,6 +164,13 @@ class AsyncStorageListener {
143
164
  if (this.originalMultiMerge) {
144
165
  AsyncStorage.multiMerge = this.originalMultiMerge;
145
166
  }
167
+ // v3 batch
168
+ if (this.originalSetMany) {
169
+ AsyncStorage.setMany = this.originalSetMany;
170
+ }
171
+ if (this.originalRemoveMany) {
172
+ AsyncStorage.removeMany = this.originalRemoveMany;
173
+ }
146
174
  }
147
175
 
148
176
  /**
@@ -171,6 +199,77 @@ class AsyncStorageListener {
171
199
  });
172
200
  }
173
201
 
202
+ /**
203
+ * Shared core for batch writes (v2 multiSet/multiMerge, v3 setMany).
204
+ *
205
+ * Captures previous values for the affected keys, runs the original native
206
+ * operation, and emits a normalized event. `action` distinguishes a plain set
207
+ * from a merge so time-travel can replay them differently.
208
+ */
209
+ async handleBatchSet(pairs, runOriginal, action = "multiSet") {
210
+ // Filter out ignored keys
211
+ const filteredPairs = pairs.filter(([key]) => !this.shouldIgnoreKey(key));
212
+ if (filteredPairs.length === 0) {
213
+ return runOriginal();
214
+ }
215
+
216
+ // Capture previous values for all keys being written
217
+ const keysToSet = filteredPairs.map(([key]) => key);
218
+ let prevPairs = [];
219
+ try {
220
+ prevPairs = await readMany(keysToSet);
221
+ } catch {
222
+ // Failed to capture previous values
223
+ }
224
+
225
+ // Execute the operation
226
+ const result = await runOriginal();
227
+
228
+ // Emit event with previous values
229
+ this.emit({
230
+ action,
231
+ timestamp: new Date(),
232
+ data: {
233
+ pairs: filteredPairs,
234
+ prevPairs
235
+ }
236
+ });
237
+ return result;
238
+ }
239
+
240
+ /**
241
+ * Shared core for batch removals (v2 multiRemove, v3 removeMany).
242
+ */
243
+ async handleBatchRemove(keys, runOriginal) {
244
+ // Filter out ignored keys
245
+ const filteredKeys = keys.filter(key => !this.shouldIgnoreKey(key));
246
+ if (filteredKeys.length === 0) {
247
+ return runOriginal();
248
+ }
249
+
250
+ // Capture previous values for all keys being removed
251
+ let prevPairs = [];
252
+ try {
253
+ prevPairs = await readMany(filteredKeys);
254
+ } catch {
255
+ // Failed to capture previous values
256
+ }
257
+
258
+ // Execute the operation
259
+ const result = await runOriginal();
260
+
261
+ // Emit event with previous values
262
+ this.emit({
263
+ action: "multiRemove",
264
+ timestamp: new Date(),
265
+ data: {
266
+ keys: filteredKeys,
267
+ prevPairs
268
+ }
269
+ });
270
+ return result;
271
+ }
272
+
174
273
  /**
175
274
  * Start intercepting AsyncStorage operations by swizzling methods
176
275
  *
@@ -255,8 +354,8 @@ class AsyncStorageListener {
255
354
  };
256
355
  }
257
356
 
258
- // Swizzle mergeItem
259
- if (AsyncStorage) {
357
+ // Swizzle mergeItem (v2 only — v3 removed the merge API)
358
+ if (asyncStorageCaps.hasMergeApi && AsyncStorage) {
260
359
  AsyncStorage.mergeItem = async (key, value) => {
261
360
  // Only capture and emit if key is not ignored
262
361
  if (!this.shouldIgnoreKey(key)) {
@@ -292,8 +391,7 @@ class AsyncStorageListener {
292
391
  // Filter out ignored keys
293
392
  const keysToCapture = allKeys.filter(key => !this.shouldIgnoreKey(key));
294
393
  if (keysToCapture.length > 0) {
295
- const keyValuePairs = await AsyncStorage.multiGet(keysToCapture);
296
- prevPairs = keyValuePairs;
394
+ prevPairs = await readMany(keysToCapture);
297
395
  }
298
396
  } catch {
299
397
  // Failed to capture previous state
@@ -314,105 +412,18 @@ class AsyncStorageListener {
314
412
  };
315
413
  }
316
414
 
317
- // Swizzle multiSet
318
- if (AsyncStorage) {
319
- AsyncStorage.multiSet = async keyValuePairs => {
320
- // Filter out ignored keys
321
- const filteredPairs = keyValuePairs.filter(([key]) => !this.shouldIgnoreKey(key));
322
- if (filteredPairs.length > 0) {
323
- // Capture previous values for all keys being set
324
- const keysToSet = filteredPairs.map(([key]) => key);
325
- let prevPairs = [];
326
- try {
327
- const existingValues = await AsyncStorage.multiGet(keysToSet);
328
- prevPairs = existingValues;
329
- } catch {
330
- // Failed to capture previous values
331
- }
332
-
333
- // Execute the operation
334
- const result = this.originalMultiSet ? await this.originalMultiSet(keyValuePairs) : undefined;
335
-
336
- // Emit event with previous values
337
- this.emit({
338
- action: "multiSet",
339
- timestamp: new Date(),
340
- data: {
341
- pairs: filteredPairs,
342
- prevPairs
343
- }
344
- });
345
- return result;
346
- }
347
- return this.originalMultiSet ? this.originalMultiSet(keyValuePairs) : Promise.resolve();
348
- };
349
- }
350
-
351
- // Swizzle multiRemove
352
- if (AsyncStorage) {
353
- AsyncStorage.multiRemove = async keys => {
354
- // Filter out ignored keys
355
- const filteredKeys = keys.filter(key => !this.shouldIgnoreKey(key));
356
- if (filteredKeys.length > 0) {
357
- // Capture previous values for all keys being removed
358
- let prevPairs = [];
359
- try {
360
- const existingValues = await AsyncStorage.multiGet(filteredKeys);
361
- prevPairs = existingValues;
362
- } catch {
363
- // Failed to capture previous values
364
- }
365
-
366
- // Execute the operation
367
- const result = this.originalMultiRemove ? await this.originalMultiRemove(keys) : undefined;
368
-
369
- // Emit event with previous values
370
- this.emit({
371
- action: "multiRemove",
372
- timestamp: new Date(),
373
- data: {
374
- keys: filteredKeys,
375
- prevPairs
376
- }
377
- });
378
- return result;
379
- }
380
- return this.originalMultiRemove ? this.originalMultiRemove(keys) : Promise.resolve();
381
- };
382
- }
383
-
384
- // Swizzle multiMerge if available
385
- if (this.originalMultiMerge && AsyncStorage) {
386
- AsyncStorage.multiMerge = async keyValuePairs => {
387
- // Filter out ignored keys
388
- const filteredPairs = keyValuePairs.filter(([key]) => !this.shouldIgnoreKey(key));
389
- if (filteredPairs.length > 0) {
390
- // Capture previous values for all keys being merged
391
- const keysToMerge = filteredPairs.map(([key]) => key);
392
- let prevPairs = [];
393
- try {
394
- const existingValues = await AsyncStorage.multiGet(keysToMerge);
395
- prevPairs = existingValues;
396
- } catch {
397
- // Failed to capture previous values
398
- }
399
-
400
- // Execute the operation
401
- const result = this.originalMultiMerge ? await this.originalMultiMerge(keyValuePairs) : undefined;
402
-
403
- // Emit event with previous values
404
- this.emit({
405
- action: "multiMerge",
406
- timestamp: new Date(),
407
- data: {
408
- pairs: filteredPairs,
409
- prevPairs
410
- }
411
- });
412
- return result;
413
- }
414
- return this.originalMultiMerge ? this.originalMultiMerge(keyValuePairs) : Promise.resolve();
415
- };
415
+ // Swizzle the batch methods for whichever API the installed version exposes.
416
+ if (asyncStorageCaps.hasLegacyMultiApi && AsyncStorage) {
417
+ // ── v2: multiSet / multiRemove / multiMerge ──
418
+ AsyncStorage.multiSet = keyValuePairs => this.handleBatchSet(keyValuePairs, () => this.originalMultiSet ? this.originalMultiSet(keyValuePairs) : Promise.resolve());
419
+ AsyncStorage.multiRemove = keys => this.handleBatchRemove(keys, () => this.originalMultiRemove ? this.originalMultiRemove(keys) : Promise.resolve());
420
+ if (this.originalMultiMerge) {
421
+ AsyncStorage.multiMerge = keyValuePairs => this.handleBatchSet(keyValuePairs, () => this.originalMultiMerge ? this.originalMultiMerge(keyValuePairs) : Promise.resolve(), "multiMerge");
422
+ }
423
+ } else if (AsyncStorage) {
424
+ // ── v3: setMany / removeMany (normalized back to multiSet/multiRemove events) ──
425
+ AsyncStorage.setMany = entries => this.handleBatchSet(Object.entries(entries), () => this.originalSetMany ? this.originalSetMany(entries) : Promise.resolve());
426
+ AsyncStorage.removeMany = keys => this.handleBatchRemove(keys, () => this.originalRemoveMany ? this.originalRemoveMany(keys) : Promise.resolve());
416
427
  }
417
428
  this.isListening = true;
418
429
  // Started listening successfully
@@ -433,29 +444,7 @@ class AsyncStorageListener {
433
444
  }
434
445
 
435
446
  // Stopping listener and restoring original methods
436
-
437
- // Restore original methods
438
- if (this.originalSetItem) {
439
- AsyncStorage.setItem = this.originalSetItem;
440
- }
441
- if (this.originalRemoveItem) {
442
- AsyncStorage.removeItem = this.originalRemoveItem;
443
- }
444
- if (this.originalMergeItem) {
445
- AsyncStorage.mergeItem = this.originalMergeItem;
446
- }
447
- if (this.originalClear) {
448
- AsyncStorage.clear = this.originalClear;
449
- }
450
- if (this.originalMultiSet) {
451
- AsyncStorage.multiSet = this.originalMultiSet;
452
- }
453
- if (this.originalMultiRemove) {
454
- AsyncStorage.multiRemove = this.originalMultiRemove;
455
- }
456
- if (this.originalMultiMerge) {
457
- AsyncStorage.multiMerge = this.originalMultiMerge;
458
- }
447
+ this.restoreOriginalMethods();
459
448
  this.isListening = false;
460
449
  // Stopped listening successfully
461
450
  }
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * AsyncStorage compatibility adapter (v2 + v3)
5
+ *
6
+ * `@react-native-async-storage/async-storage` 3.x reshaped the default-export
7
+ * API. The single-item methods kept their v2 signatures, but the batch + merge
8
+ * methods changed:
9
+ *
10
+ * v2 (what this package was written against) v3
11
+ * ------------------------------------------ --------------------------------
12
+ * multiGet(keys): [key, value|null][] getMany(keys): Record<key, value|null>
13
+ * multiSet([[k, v]]): void setMany({ k: v }): void
14
+ * multiRemove(keys): void removeMany(keys): void
15
+ * mergeItem / multiMerge removed entirely
16
+ *
17
+ * This module is the single seam between the storage tool and whichever
18
+ * async-storage version the host app installed. The rest of the package always
19
+ * speaks the v2 tuple-shaped API; we translate to v3 here when needed.
20
+ *
21
+ * Reads (`readMany`) are routed through the *unswizzled* native methods, so the
22
+ * event listener can safely call them while capturing previous values without
23
+ * re-triggering itself.
24
+ */
25
+ import RawAsyncStorage from "@react-native-async-storage/async-storage";
26
+
27
+ // Loosely typed because the surface differs across major versions; callers go
28
+ // through the helpers below rather than touching this directly.
29
+
30
+ /** The live default-export object (methods may be swizzled by the listener). */
31
+ export const asyncStorage = RawAsyncStorage;
32
+
33
+ /**
34
+ * Capability flags describing which API surface the installed version exposes.
35
+ * `multiGet` only exists on v2; `mergeItem` was removed in v3.
36
+ */
37
+ export const asyncStorageCaps = {
38
+ /** v2 exposes multiGet/multiSet/multiRemove; v3 renamed these to *Many. */
39
+ hasLegacyMultiApi: typeof asyncStorage?.multiGet === "function",
40
+ /** v3 removed mergeItem/multiMerge with no replacement. */
41
+ hasMergeApi: typeof asyncStorage?.mergeItem === "function"
42
+ };
43
+
44
+ /**
45
+ * Batch-read keys, normalized to the v2 `[key, value|null][]` tuple shape and
46
+ * preserving the requested key order regardless of installed version.
47
+ */
48
+ export async function readMany(keys) {
49
+ if (typeof asyncStorage.multiGet === "function") {
50
+ return await asyncStorage.multiGet(keys);
51
+ }
52
+ // v3: getMany returns a Record; rebuild ordered tuples ("what you request is
53
+ // what you get" — missing keys come back as null).
54
+ const record = await asyncStorage.getMany(keys);
55
+ return keys.map(key => [key, record[key] ?? null]);
56
+ }
57
+
58
+ /**
59
+ * Batch-write key/value pairs. Calls the live method (which may be swizzled by
60
+ * the listener on v2's `multiSet` / v3's `setMany`) so writes still emit events.
61
+ */
62
+ export async function writeMany(pairs) {
63
+ if (typeof asyncStorage.multiSet === "function") {
64
+ return asyncStorage.multiSet(pairs);
65
+ }
66
+ const record = {};
67
+ for (const [key, value] of pairs) record[key] = value;
68
+ return asyncStorage.setMany(record);
69
+ }
70
+
71
+ /**
72
+ * Batch-remove keys via the live method (swizzled on v2's `multiRemove` /
73
+ * v3's `removeMany`).
74
+ */
75
+ export async function removeMany(keys) {
76
+ if (typeof asyncStorage.multiRemove === "function") {
77
+ return asyncStorage.multiRemove(keys);
78
+ }
79
+ return asyncStorage.removeMany(keys);
80
+ }
81
+ export default asyncStorage;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  import AsyncStorage from "@react-native-async-storage/async-storage";
4
+ import { removeMany } from "./asyncStorageCompat";
4
5
  import { isDevToolsStorageKey } from "@buoy-gg/shared-ui";
5
6
 
6
7
  /**
@@ -25,7 +26,7 @@ export async function clearAllAppStorage() {
25
26
  // Clearing ${keysToRemove.length} app storage keys
26
27
 
27
28
  // Remove all non-dev-tool keys
28
- await AsyncStorage.multiRemove(keysToRemove);
29
+ await removeMany(keysToRemove);
29
30
 
30
31
  // Successfully cleared app storage
31
32
  }
@@ -36,16 +36,25 @@
36
36
  * ```
37
37
  */
38
38
  export function detectMMKVType(instance, key) {
39
- // Try string (most common)
39
+ // MMKV stores no type metadata, so the native getters can cross-read: in
40
+ // particular `getString` returns "" (an empty string, NOT undefined) for many
41
+ // non-string values like numbers and `false`. Trying getString first and
42
+ // trusting its result therefore mis-detects those as empty strings — e.g.
43
+ // a key set to 42 or false would render as "". Probe the getters and only let
44
+ // getString win when it returns a NON-empty string; treat an empty result as
45
+ // ambiguous and prefer the number/boolean getters.
40
46
  const stringValue = instance.getString(key);
41
- if (stringValue !== undefined) {
47
+
48
+ // A non-empty string is unambiguously a string.
49
+ if (stringValue !== undefined && stringValue !== '') {
42
50
  return {
43
51
  value: stringValue,
44
52
  type: 'string'
45
53
  };
46
54
  }
47
55
 
48
- // Try number
56
+ // Empty getString result is ambiguous (genuine "" vs a number/boolean the
57
+ // native layer decoded to ""). Prefer typed getters when they have a value.
49
58
  const numberValue = instance.getNumber(key);
50
59
  if (numberValue !== undefined) {
51
60
  return {
@@ -53,8 +62,6 @@ export function detectMMKVType(instance, key) {
53
62
  type: 'number'
54
63
  };
55
64
  }
56
-
57
- // Try boolean
58
65
  const booleanValue = instance.getBoolean(key);
59
66
  if (booleanValue !== undefined) {
60
67
  return {
@@ -63,6 +70,14 @@ export function detectMMKVType(instance, key) {
63
70
  };
64
71
  }
65
72
 
73
+ // getString returned "" and nothing else matched -> a genuine empty string.
74
+ if (stringValue === '') {
75
+ return {
76
+ value: '',
77
+ type: 'string'
78
+ };
79
+ }
80
+
66
81
  // Try buffer (least common)
67
82
  const bufferValue = instance.getBuffer(key);
68
83
  if (bufferValue !== undefined) {
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import AsyncStorage from "@react-native-async-storage/async-storage";
10
+ import { writeMany } from "./asyncStorageCompat";
10
11
  import { pauseCapture, resumeCapture } from "./AsyncStorageListener";
11
12
 
12
13
  /**
@@ -73,7 +74,7 @@ export async function undoOperation(event) {
73
74
  // Restore all cleared key-value pairs
74
75
  const pairsToRestore = data.prevPairs.filter(([, value]) => value !== null);
75
76
  if (pairsToRestore.length > 0) {
76
- await AsyncStorage.multiSet(pairsToRestore);
77
+ await writeMany(pairsToRestore);
77
78
  }
78
79
  }
79
80
  break;
@@ -125,7 +126,7 @@ export async function jumpToState(events, targetEventIndex) {
125
126
  }
126
127
  }
127
128
  if (pairsToSet.length > 0) {
128
- await AsyncStorage.multiSet(pairsToSet);
129
+ await writeMany(pairsToSet);
129
130
  }
130
131
  } finally {
131
132
  // Always resume capture
@@ -11,4 +11,43 @@ export function getValueTypeLabel(value) {
11
11
  if (type === "number") return "number";
12
12
  if (type === "string") return "string";
13
13
  return "unknown";
14
+ }
15
+
16
+ /** Longest value preview rendered inline next to the type label. */
17
+ const MAX_INLINE_PREVIEW = 40;
18
+
19
+ /**
20
+ * Short, human-readable preview of a value for inline display next to the type
21
+ * label, or `null` when the value is too large/complex to preview (long
22
+ * strings, objects, arrays). Lets the user read simple values without opening
23
+ * the card.
24
+ */
25
+ export function getValuePreview(value) {
26
+ switch (typeof value) {
27
+ case "boolean":
28
+ return value ? "true" : "false";
29
+ case "number":
30
+ return Number.isFinite(value) ? String(value) : null;
31
+ case "string":
32
+ {
33
+ if (value.length === 0) return '""';
34
+ if (value.length > MAX_INLINE_PREVIEW) return null;
35
+ // Collapse whitespace so multi-line/tabbed values stay on one line.
36
+ return `"${value.replace(/\s+/g, " ")}"`;
37
+ }
38
+ default:
39
+ return null;
40
+ // objects, arrays, null, undefined
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Type label optionally followed by a short value preview — e.g. `boolean · true`,
46
+ * `number · 42`, `string · "hello"`. Falls back to just the type label when the
47
+ * value is too large to preview.
48
+ */
49
+ export function getValueTypeWithPreview(value) {
50
+ const label = getValueTypeLabel(value);
51
+ const preview = getValuePreview(value);
52
+ return preview ? `${label} · ${preview}` : label;
14
53
  }
@@ -13,7 +13,11 @@ interface GameUIStorageBrowserProps {
13
13
  eventCountByKey?: Record<string, number>;
14
14
  onViewHistory?: (key: string) => void;
15
15
  enabledStorageTypes?: Set<'async' | 'mmkv' | 'secure'>;
16
+ /** Keys pinned to the top of the list */
17
+ pinnedKeys?: Set<string>;
18
+ /** Toggle a key's pinned state */
19
+ onTogglePin?: (key: string) => void;
16
20
  }
17
- export declare function GameUIStorageBrowser({ requiredStorageKeys, showFilters, ignoredPatterns, onTogglePattern, onAddPattern, searchQuery, storageDataRef, eventCountByKey, onViewHistory, enabledStorageTypes, }: GameUIStorageBrowserProps): import("react").JSX.Element;
21
+ export declare function GameUIStorageBrowser({ requiredStorageKeys, showFilters, ignoredPatterns, onTogglePattern, onAddPattern, searchQuery, storageDataRef, eventCountByKey, onViewHistory, enabledStorageTypes, pinnedKeys, onTogglePin, }: GameUIStorageBrowserProps): import("react").JSX.Element;
18
22
  export {};
19
23
  //# sourceMappingURL=GameUIStorageBrowser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"GameUIStorageBrowser.d.ts","sourceRoot":"","sources":["../../../../src/storage/components/GameUIStorageBrowser.tsx"],"names":[],"mappings":"AAAA,OAAO,EAKL,gBAAgB,EACjB,MAAM,OAAO,CAAC;AAUf,OAAO,EAAkB,kBAAkB,EAAmB,MAAM,UAAU,CAAC;AAgD/E,uCAAuC;AACvC,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC,UAAU,yBAAyB;IACjC,mBAAmB,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAC3C,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,gBAAgB,CAAC,GAAG,EAAE,CAAC,CAAC;IACzC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,mBAAmB,CAAC,EAAE,GAAG,CAAC,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC,CAAC;CACxD;AAED,wBAAgB,oBAAoB,CAAC,EACnC,mBAAwB,EACxB,WAAmB,EACnB,eAA0C,EAC1C,eAAe,EACf,YAAY,EACZ,WAAgB,EAChB,cAAc,EACd,eAAe,EACf,aAAa,EACb,mBAAmB,GACpB,EAAE,yBAAyB,+BAqxB3B"}
1
+ {"version":3,"file":"GameUIStorageBrowser.d.ts","sourceRoot":"","sources":["../../../../src/storage/components/GameUIStorageBrowser.tsx"],"names":[],"mappings":"AAAA,OAAO,EAKL,gBAAgB,EACjB,MAAM,OAAO,CAAC;AAUf,OAAO,EAAkB,kBAAkB,EAAmB,MAAM,UAAU,CAAC;AAgD/E,uCAAuC;AACvC,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC,UAAU,yBAAyB;IACjC,mBAAmB,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAC3C,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,gBAAgB,CAAC,GAAG,EAAE,CAAC,CAAC;IACzC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,mBAAmB,CAAC,EAAE,GAAG,CAAC,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC,CAAC;IACvD,yCAAyC;IACzC,UAAU,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACzB,kCAAkC;IAClC,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC;AAED,wBAAgB,oBAAoB,CAAC,EACnC,mBAAwB,EACxB,WAAmB,EACnB,eAA0C,EAC1C,eAAe,EACf,YAAY,EACZ,WAAgB,EAChB,cAAc,EACd,eAAe,EACf,aAAa,EACb,mBAAmB,EACnB,UAAU,EACV,WAAW,GACZ,EAAE,yBAAyB,+BA6yB3B"}
@@ -9,6 +9,8 @@ interface SelectionActionBarProps {
9
9
  }>;
10
10
  /** Callback when deletion is complete */
11
11
  onDeleteComplete?: () => void;
12
+ /** Hide the selected keys by adding them to the filter store */
13
+ onHideKeys?: (keys: StorageKeyInfo[]) => void;
12
14
  /** Callback to select all visible keys */
13
15
  onSelectAll?: () => void;
14
16
  /** Callback to clear selection */
@@ -16,6 +18,6 @@ interface SelectionActionBarProps {
16
18
  /** Total number of visible keys */
17
19
  totalVisibleKeys: number;
18
20
  }
19
- export declare function SelectionActionBar({ selectedKeys, mmkvInstances, onDeleteComplete, onSelectAll, onClearSelection, totalVisibleKeys, }: SelectionActionBarProps): import("react").JSX.Element | null;
21
+ export declare function SelectionActionBar({ selectedKeys, mmkvInstances, onDeleteComplete, onHideKeys, onSelectAll, onClearSelection, totalVisibleKeys, }: SelectionActionBarProps): import("react").JSX.Element | null;
20
22
  export {};
21
23
  //# sourceMappingURL=SelectionActionBar.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"SelectionActionBar.d.ts","sourceRoot":"","sources":["../../../../src/storage/components/SelectionActionBar.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAG1C,UAAU,uBAAuB;IAC/B,4BAA4B;IAC5B,YAAY,EAAE,cAAc,EAAE,CAAC;IAC/B,kCAAkC;IAClC,aAAa,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,GAAG,CAAA;KAAE,CAAC,CAAC;IACpD,yCAAyC;IACzC,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B,0CAA0C;IAC1C,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,kCAAkC;IAClC,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B,mCAAmC;IACnC,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,kBAAkB,CAAC,EACjC,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,WAAW,EACX,gBAAgB,EAChB,gBAAgB,GACjB,EAAE,uBAAuB,sCA2IzB"}
1
+ {"version":3,"file":"SelectionActionBar.d.ts","sourceRoot":"","sources":["../../../../src/storage/components/SelectionActionBar.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAG1C,UAAU,uBAAuB;IAC/B,4BAA4B;IAC5B,YAAY,EAAE,cAAc,EAAE,CAAC;IAC/B,kCAAkC;IAClC,aAAa,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,GAAG,CAAA;KAAE,CAAC,CAAC;IACpD,yCAAyC;IACzC,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B,gEAAgE;IAChE,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,cAAc,EAAE,KAAK,IAAI,CAAC;IAC9C,0CAA0C;IAC1C,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,kCAAkC;IAClC,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B,mCAAmC;IACnC,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,kBAAkB,CAAC,EACjC,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,UAAU,EACV,WAAW,EACX,gBAAgB,EAChB,gBAAgB,GACjB,EAAE,uBAAuB,sCAwJzB"}
@@ -11,11 +11,13 @@ interface StorageBrowserModeProps {
11
11
  eventCountByKey?: Record<string, number>;
12
12
  onViewHistory?: (key: string) => void;
13
13
  enabledStorageTypes?: Set<'async' | 'mmkv' | 'secure'>;
14
+ pinnedKeys?: Set<string>;
15
+ onTogglePin?: (key: string) => void;
14
16
  }
15
17
  /**
16
18
  * Storage browser mode component
17
19
  * Displays storage keys with game UI styled interface
18
20
  */
19
- export declare function StorageBrowserMode({ requiredStorageKeys, showFilters, ignoredPatterns, onTogglePattern, onAddPattern, searchQuery, storageDataRef, eventCountByKey, onViewHistory, enabledStorageTypes, }: StorageBrowserModeProps): import("react").JSX.Element;
21
+ export declare function StorageBrowserMode({ requiredStorageKeys, showFilters, ignoredPatterns, onTogglePattern, onAddPattern, searchQuery, storageDataRef, eventCountByKey, onViewHistory, enabledStorageTypes, pinnedKeys, onTogglePin, }: StorageBrowserModeProps): import("react").JSX.Element;
20
22
  export {};
21
23
  //# sourceMappingURL=StorageBrowserMode.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"StorageBrowserMode.d.ts","sourceRoot":"","sources":["../../../../src/storage/components/StorageBrowserMode.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAE9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAEzC,UAAU,uBAAuB;IAC/B,mBAAmB,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAC3C,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,gBAAgB,CAAC,GAAG,EAAE,CAAC,CAAC;IACzC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,mBAAmB,CAAC,EAAE,GAAG,CAAC,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC,CAAC;CACxD;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,mBAAwB,EACxB,WAAmB,EACnB,eAA2B,EAC3B,eAAe,EACf,YAAY,EACZ,WAAgB,EAChB,cAAc,EACd,eAAe,EACf,aAAa,EACb,mBAAmB,GACpB,EAAE,uBAAuB,+BAezB"}
1
+ {"version":3,"file":"StorageBrowserMode.d.ts","sourceRoot":"","sources":["../../../../src/storage/components/StorageBrowserMode.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAE9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAEzC,UAAU,uBAAuB;IAC/B,mBAAmB,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAC3C,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,gBAAgB,CAAC,GAAG,EAAE,CAAC,CAAC;IACzC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,mBAAmB,CAAC,EAAE,GAAG,CAAC,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC,CAAC;IACvD,UAAU,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACzB,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,mBAAwB,EACxB,WAAmB,EACnB,eAA2B,EAC3B,eAAe,EACf,YAAY,EACZ,WAAgB,EAChB,cAAc,EACd,eAAe,EACf,aAAa,EACb,mBAAmB,EACnB,UAAU,EACV,WAAW,GACZ,EAAE,uBAAuB,+BAiBzB"}
@@ -13,7 +13,13 @@ interface StorageKeyRowProps {
13
13
  eventCount?: number;
14
14
  /** Called when "view history" is pressed */
15
15
  onViewHistory?: () => void;
16
+ /** Called when "hide / filter out" is pressed for this key */
17
+ onHideKey?: (storageKey: StorageKeyInfo) => void;
18
+ /** Whether this key is pinned to the top */
19
+ isPinned?: boolean;
20
+ /** Toggle this key's pinned state */
21
+ onTogglePin?: (key: string) => void;
16
22
  }
17
- export declare function StorageKeyRow({ storageKey, isExpanded, onPress, isSelectMode, isSelected, onSelectionChange, eventCount, onViewHistory, }: StorageKeyRowProps): import("react").JSX.Element;
23
+ export declare function StorageKeyRow({ storageKey, isExpanded, onPress, isSelectMode, isSelected, onSelectionChange, eventCount, onViewHistory, onHideKey, isPinned, onTogglePin, }: StorageKeyRowProps): import("react").JSX.Element;
18
24
  export {};
19
25
  //# sourceMappingURL=StorageKeyRow.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"StorageKeyRow.d.ts","sourceRoot":"","sources":["../../../../src/storage/components/StorageKeyRow.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAyB1C,UAAU,kBAAkB;IAC1B,UAAU,EAAE,cAAc,CAAC;IAC3B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,cAAc,KAAK,IAAI,CAAC;IAC/C,uCAAuC;IACvC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,oCAAoC;IACpC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,sCAAsC;IACtC,iBAAiB,CAAC,EAAE,CAAC,UAAU,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;IAC5E,6CAA6C;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;CAC5B;AA6CD,wBAAgB,aAAa,CAAC,EAC5B,UAAU,EACV,UAAU,EACV,OAAO,EACP,YAAoB,EACpB,UAAkB,EAClB,iBAAiB,EACjB,UAAU,EACV,aAAa,GACd,EAAE,kBAAkB,+BAiKpB"}
1
+ {"version":3,"file":"StorageKeyRow.d.ts","sourceRoot":"","sources":["../../../../src/storage/components/StorageKeyRow.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAyB1C,UAAU,kBAAkB;IAC1B,UAAU,EAAE,cAAc,CAAC;IAC3B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,cAAc,KAAK,IAAI,CAAC;IAC/C,uCAAuC;IACvC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,oCAAoC;IACpC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,sCAAsC;IACtC,iBAAiB,CAAC,EAAE,CAAC,UAAU,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;IAC5E,6CAA6C;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,8DAA8D;IAC9D,SAAS,CAAC,EAAE,CAAC,UAAU,EAAE,cAAc,KAAK,IAAI,CAAC;IACjD,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qCAAqC;IACrC,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC;AA6CD,wBAAgB,aAAa,CAAC,EAC5B,UAAU,EACV,UAAU,EACV,OAAO,EACP,YAAoB,EACpB,UAAkB,EAClB,iBAAiB,EACjB,UAAU,EACV,aAAa,EACb,SAAS,EACT,QAAgB,EAChB,WAAW,GACZ,EAAE,kBAAkB,+BA2NpB"}