@buoy-gg/storage 7.0.16 → 7.0.18

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 (40) hide show
  1. package/lib/commonjs/index.js +13 -0
  2. package/lib/commonjs/storage/components/DiffViewer/modes/ThemedSplitView.js +5 -602
  3. package/lib/commonjs/storage/components/StorageModalWithTabs.js +17 -1
  4. package/lib/commonjs/storage/hooks/useAsyncStorageKeys.js +13 -10
  5. package/lib/commonjs/storage/hooks/useMMKVKeys.js +28 -23
  6. package/lib/commonjs/storage/stores/storageEventStore.js +85 -1
  7. package/lib/commonjs/storage/sync/storageSyncAdapter.js +214 -6
  8. package/lib/commonjs/storage/utils/requiredStorageKeyScope.js +17 -0
  9. package/lib/commonjs/storage/utils/snapshotProvider.js +447 -0
  10. package/lib/commonjs/storage/utils/storageTimeTravelUtils.js +17 -3
  11. package/lib/module/index.js +9 -1
  12. package/lib/module/storage/components/DiffViewer/modes/ThemedSplitView.js +7 -603
  13. package/lib/module/storage/components/StorageModalWithTabs.js +17 -1
  14. package/lib/module/storage/hooks/useAsyncStorageKeys.js +13 -10
  15. package/lib/module/storage/hooks/useMMKVKeys.js +28 -23
  16. package/lib/module/storage/stores/storageEventStore.js +81 -0
  17. package/lib/module/storage/sync/storageSyncAdapter.js +215 -7
  18. package/lib/module/storage/utils/requiredStorageKeyScope.js +13 -0
  19. package/lib/module/storage/utils/snapshotProvider.js +445 -0
  20. package/lib/module/storage/utils/storageTimeTravelUtils.js +17 -3
  21. package/lib/typescript/index.d.ts +3 -1
  22. package/lib/typescript/index.d.ts.map +1 -1
  23. package/lib/typescript/storage/components/DiffViewer/modes/ThemedSplitView.d.ts +7 -18
  24. package/lib/typescript/storage/components/DiffViewer/modes/ThemedSplitView.d.ts.map +1 -1
  25. package/lib/typescript/storage/components/StorageModalWithTabs.d.ts +8 -1
  26. package/lib/typescript/storage/components/StorageModalWithTabs.d.ts.map +1 -1
  27. package/lib/typescript/storage/hooks/useAsyncStorageKeys.d.ts.map +1 -1
  28. package/lib/typescript/storage/hooks/useMMKVKeys.d.ts.map +1 -1
  29. package/lib/typescript/storage/stores/storageEventStore.d.ts +26 -0
  30. package/lib/typescript/storage/stores/storageEventStore.d.ts.map +1 -1
  31. package/lib/typescript/storage/sync/storageSyncAdapter.d.ts +71 -5
  32. package/lib/typescript/storage/sync/storageSyncAdapter.d.ts.map +1 -1
  33. package/lib/typescript/storage/types.d.ts +9 -9
  34. package/lib/typescript/storage/types.d.ts.map +1 -1
  35. package/lib/typescript/storage/utils/requiredStorageKeyScope.d.ts +10 -0
  36. package/lib/typescript/storage/utils/requiredStorageKeyScope.d.ts.map +1 -0
  37. package/lib/typescript/storage/utils/snapshotProvider.d.ts +2 -0
  38. package/lib/typescript/storage/utils/snapshotProvider.d.ts.map +1 -0
  39. package/lib/typescript/storage/utils/storageTimeTravelUtils.d.ts.map +1 -1
  40. package/package.json +9 -6
@@ -7,6 +7,7 @@ exports.useAsyncStorageKeys = useAsyncStorageKeys;
7
7
  var _react = require("react");
8
8
  var _asyncStorage = _interopRequireDefault(require("@react-native-async-storage/async-storage"));
9
9
  var _asyncStorageCompat = require("../utils/asyncStorageCompat");
10
+ var _requiredStorageKeyScope = require("../utils/requiredStorageKeyScope");
10
11
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
11
12
  function useAsyncStorageKeys(requiredStorageKeys = []) {
12
13
  // State management
@@ -53,8 +54,9 @@ function useAsyncStorageKeys(requiredStorageKeys = []) {
53
54
  }
54
55
  }
55
56
 
56
- // Check if this is a required key
57
+ // Check if this is a required key scoped to AsyncStorage
57
58
  const requiredConfig = requiredStorageKeys.find(req => {
59
+ if (!(0, _requiredStorageKeyScope.requiredKeyAppliesTo)(req, 'async')) return false;
58
60
  if (typeof req === 'string') return req === key;
59
61
  return req.key === key;
60
62
  });
@@ -64,9 +66,9 @@ function useAsyncStorageKeys(requiredStorageKeys = []) {
64
66
  if (requiredConfig) {
65
67
  if (parsedValue === undefined || parsedValue === null) {
66
68
  status = 'required_missing';
67
- } else if (typeof requiredConfig === 'object' && 'expectedValue' in requiredConfig) {
69
+ } else if (typeof requiredConfig === 'object' && requiredConfig.expectedValue !== undefined) {
68
70
  status = parsedValue === requiredConfig.expectedValue ? 'required_present' : 'required_wrong_value';
69
- } else if (typeof requiredConfig === 'object' && 'expectedType' in requiredConfig) {
71
+ } else if (typeof requiredConfig === 'object' && requiredConfig.expectedType !== undefined) {
70
72
  const actualType = parsedValue === null ? 'null' : typeof parsedValue;
71
73
  status = actualType.toLowerCase() === requiredConfig.expectedType.toLowerCase() ? 'required_present' : 'required_wrong_type';
72
74
  } else {
@@ -79,21 +81,22 @@ function useAsyncStorageKeys(requiredStorageKeys = []) {
79
81
  storageType: 'async',
80
82
  status,
81
83
  category: requiredConfig ? 'required' : 'optional',
82
- ...(typeof requiredConfig === 'object' && 'expectedValue' in requiredConfig && {
84
+ ...(typeof requiredConfig === 'object' && requiredConfig.expectedValue !== undefined && {
83
85
  expectedValue: requiredConfig.expectedValue
84
86
  }),
85
- ...(typeof requiredConfig === 'object' && 'expectedType' in requiredConfig && {
87
+ ...(typeof requiredConfig === 'object' && requiredConfig.expectedType !== undefined && {
86
88
  expectedType: requiredConfig.expectedType
87
89
  }),
88
- ...(typeof requiredConfig === 'object' && 'description' in requiredConfig && {
90
+ ...(typeof requiredConfig === 'object' && requiredConfig.description !== undefined && {
89
91
  description: requiredConfig.description
90
92
  })
91
93
  };
92
94
  allStorageKeys.push(keyInfo);
93
95
  });
94
96
 
95
- // 4. Add missing required keys
97
+ // 4. Add missing required keys (Async-scoped only)
96
98
  requiredStorageKeys.forEach(req => {
99
+ if (!(0, _requiredStorageKeyScope.requiredKeyAppliesTo)(req, 'async')) return;
97
100
  const key = typeof req === 'string' ? req : req.key;
98
101
  const exists = allStorageKeys.some(k => k.key === key);
99
102
  if (!exists) {
@@ -103,13 +106,13 @@ function useAsyncStorageKeys(requiredStorageKeys = []) {
103
106
  storageType: 'async',
104
107
  status: 'required_missing',
105
108
  category: 'required',
106
- ...(typeof req === 'object' && 'expectedValue' in req && {
109
+ ...(typeof req === 'object' && req.expectedValue !== undefined && {
107
110
  expectedValue: req.expectedValue
108
111
  }),
109
- ...(typeof req === 'object' && 'expectedType' in req && {
112
+ ...(typeof req === 'object' && req.expectedType !== undefined && {
110
113
  expectedType: req.expectedType
111
114
  }),
112
- ...(typeof req === 'object' && 'description' in req && {
115
+ ...(typeof req === 'object' && req.description !== undefined && {
113
116
  description: req.description
114
117
  })
115
118
  });
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.useMMKVKeys = useMMKVKeys;
7
7
  exports.useMultiMMKVKeys = useMultiMMKVKeys;
8
8
  var _react = require("react");
9
+ var _requiredStorageKeyScope = require("../utils/requiredStorageKeyScope");
9
10
  var _mmkvTypeDetection = require("../utils/mmkvTypeDetection");
10
11
  /**
11
12
  * useMMKVKeys Hook
@@ -97,12 +98,12 @@ function useMMKVKeys(instance, instanceId, requiredStorageKeys = []) {
97
98
  // 1. Get all keys from MMKV (synchronous)
98
99
  const allKeys = instance.getAllKeys();
99
100
  if (!allKeys || allKeys.length === 0) {
100
- // Still need to check for missing required keys
101
- const missingRequiredKeys = requiredStorageKeys.map(req => {
101
+ // Still need to check for missing required keys scoped to MMKV
102
+ const missingRequiredKeys = requiredStorageKeys.filter(req => (0, _requiredStorageKeyScope.requiredKeyAppliesTo)(req, 'mmkv')).map(req => {
102
103
  const key = typeof req === 'string' ? req : req.key;
103
- const description = typeof req === 'object' && 'description' in req ? req.description : undefined;
104
- const expectedValue = typeof req === 'object' && 'expectedValue' in req ? req.expectedValue : undefined;
105
- const expectedType = typeof req === 'object' && 'expectedType' in req ? req.expectedType : undefined;
104
+ const description = typeof req === 'object' ? req.description : undefined;
105
+ const expectedValue = typeof req === 'object' ? req.expectedValue : undefined;
106
+ const expectedType = typeof req === 'object' ? req.expectedType : undefined;
106
107
  return {
107
108
  key,
108
109
  value: undefined,
@@ -130,8 +131,9 @@ function useMMKVKeys(instance, instanceId, requiredStorageKeys = []) {
130
131
  type
131
132
  } = (0, _mmkvTypeDetection.detectMMKVType)(instance, key);
132
133
 
133
- // Check if this is a required key
134
+ // Check if this is a required key scoped to MMKV
134
135
  const requiredConfig = requiredStorageKeys.find(req => {
136
+ if (!(0, _requiredStorageKeyScope.requiredKeyAppliesTo)(req, 'mmkv')) return false;
135
137
  if (typeof req === 'string') return req === key;
136
138
  return req.key === key;
137
139
  });
@@ -141,10 +143,10 @@ function useMMKVKeys(instance, instanceId, requiredStorageKeys = []) {
141
143
  if (requiredConfig) {
142
144
  if (value === undefined) {
143
145
  status = 'required_missing';
144
- } else if (typeof requiredConfig === 'object' && 'expectedValue' in requiredConfig) {
146
+ } else if (typeof requiredConfig === 'object' && requiredConfig.expectedValue !== undefined) {
145
147
  // Value validation
146
148
  status = String(value) === String(requiredConfig.expectedValue) ? 'required_present' : 'required_wrong_value';
147
- } else if (typeof requiredConfig === 'object' && 'expectedType' in requiredConfig) {
149
+ } else if (typeof requiredConfig === 'object' && requiredConfig.expectedType !== undefined) {
148
150
  // Type validation
149
151
  status = (0, _mmkvTypeDetection.isTypeMatch)(type, requiredConfig.expectedType) ? 'required_present' : 'required_wrong_type';
150
152
  } else {
@@ -163,13 +165,13 @@ function useMMKVKeys(instance, instanceId, requiredStorageKeys = []) {
163
165
  status,
164
166
  category: requiredConfig ? 'required' : 'optional',
165
167
  lastUpdated: new Date(),
166
- ...(typeof requiredConfig === 'object' && 'expectedValue' in requiredConfig && {
168
+ ...(typeof requiredConfig === 'object' && requiredConfig.expectedValue !== undefined && {
167
169
  expectedValue: requiredConfig.expectedValue
168
170
  }),
169
- ...(typeof requiredConfig === 'object' && 'expectedType' in requiredConfig && {
171
+ ...(typeof requiredConfig === 'object' && requiredConfig.expectedType !== undefined && {
170
172
  expectedType: requiredConfig.expectedType
171
173
  }),
172
- ...(typeof requiredConfig === 'object' && 'description' in requiredConfig && {
174
+ ...(typeof requiredConfig === 'object' && requiredConfig.description !== undefined && {
173
175
  description: requiredConfig.description
174
176
  })
175
177
  };
@@ -178,6 +180,7 @@ function useMMKVKeys(instance, instanceId, requiredStorageKeys = []) {
178
180
 
179
181
  // 3. Add missing required keys that weren't found in storage
180
182
  requiredStorageKeys.forEach(req => {
183
+ if (!(0, _requiredStorageKeyScope.requiredKeyAppliesTo)(req, 'mmkv')) return;
181
184
  const key = typeof req === 'string' ? req : req.key;
182
185
  const exists = allStorageKeys.some(k => k.key === key);
183
186
  if (!exists) {
@@ -189,13 +192,13 @@ function useMMKVKeys(instance, instanceId, requiredStorageKeys = []) {
189
192
  instanceId,
190
193
  status: 'required_missing',
191
194
  category: 'required',
192
- ...(typeof req === 'object' && 'expectedValue' in req && {
195
+ ...(typeof req === 'object' && req.expectedValue !== undefined && {
193
196
  expectedValue: req.expectedValue
194
197
  }),
195
- ...(typeof req === 'object' && 'expectedType' in req && {
198
+ ...(typeof req === 'object' && req.expectedType !== undefined && {
196
199
  expectedType: req.expectedType
197
200
  }),
198
- ...(typeof req === 'object' && 'description' in req && {
201
+ ...(typeof req === 'object' && req.description !== undefined && {
199
202
  description: req.description
200
203
  })
201
204
  });
@@ -284,6 +287,7 @@ function useMultiMMKVKeys(instances, requiredStorageKeys = []) {
284
287
  type
285
288
  } = (0, _mmkvTypeDetection.detectMMKVType)(instance, key);
286
289
  const requiredConfig = requiredStorageKeys.find(req => {
290
+ if (!(0, _requiredStorageKeyScope.requiredKeyAppliesTo)(req, 'mmkv')) return false;
287
291
  if (typeof req === 'string') return req === key;
288
292
  return req.key === key;
289
293
  });
@@ -291,9 +295,9 @@ function useMultiMMKVKeys(instances, requiredStorageKeys = []) {
291
295
  if (requiredConfig) {
292
296
  if (value === undefined) {
293
297
  status = 'required_missing';
294
- } else if (typeof requiredConfig === 'object' && 'expectedValue' in requiredConfig) {
298
+ } else if (typeof requiredConfig === 'object' && requiredConfig.expectedValue !== undefined) {
295
299
  status = String(value) === String(requiredConfig.expectedValue) ? 'required_present' : 'required_wrong_value';
296
- } else if (typeof requiredConfig === 'object' && 'expectedType' in requiredConfig) {
300
+ } else if (typeof requiredConfig === 'object' && requiredConfig.expectedType !== undefined) {
297
301
  status = (0, _mmkvTypeDetection.isTypeMatch)(type, requiredConfig.expectedType) ? 'required_present' : 'required_wrong_type';
298
302
  } else {
299
303
  status = 'required_present';
@@ -308,13 +312,13 @@ function useMultiMMKVKeys(instances, requiredStorageKeys = []) {
308
312
  status,
309
313
  category: requiredConfig ? 'required' : 'optional',
310
314
  lastUpdated: new Date(),
311
- ...(typeof requiredConfig === 'object' && 'expectedValue' in requiredConfig && {
315
+ ...(typeof requiredConfig === 'object' && requiredConfig.expectedValue !== undefined && {
312
316
  expectedValue: requiredConfig.expectedValue
313
317
  }),
314
- ...(typeof requiredConfig === 'object' && 'expectedType' in requiredConfig && {
318
+ ...(typeof requiredConfig === 'object' && requiredConfig.expectedType !== undefined && {
315
319
  expectedType: requiredConfig.expectedType
316
320
  }),
317
- ...(typeof requiredConfig === 'object' && 'description' in requiredConfig && {
321
+ ...(typeof requiredConfig === 'object' && requiredConfig.description !== undefined && {
318
322
  description: requiredConfig.description
319
323
  })
320
324
  };
@@ -322,8 +326,9 @@ function useMultiMMKVKeys(instances, requiredStorageKeys = []) {
322
326
  });
323
327
  });
324
328
 
325
- // Add missing required keys
329
+ // Add missing required keys scoped to MMKV
326
330
  requiredStorageKeys.forEach(req => {
331
+ if (!(0, _requiredStorageKeyScope.requiredKeyAppliesTo)(req, 'mmkv')) return;
327
332
  const key = typeof req === 'string' ? req : req.key;
328
333
  const exists = allStorageKeys.some(k => k.key === key);
329
334
  if (!exists) {
@@ -338,13 +343,13 @@ function useMultiMMKVKeys(instances, requiredStorageKeys = []) {
338
343
  instanceId: firstInstanceId,
339
344
  status: 'required_missing',
340
345
  category: 'required',
341
- ...(typeof req === 'object' && 'expectedValue' in req && {
346
+ ...(typeof req === 'object' && req.expectedValue !== undefined && {
342
347
  expectedValue: req.expectedValue
343
348
  }),
344
- ...(typeof req === 'object' && 'expectedType' in req && {
349
+ ...(typeof req === 'object' && req.expectedType !== undefined && {
345
350
  expectedType: req.expectedType
346
351
  }),
347
- ...(typeof req === 'object' && 'description' in req && {
352
+ ...(typeof req === 'object' && req.description !== undefined && {
348
353
  description: req.description
349
354
  })
350
355
  });
@@ -3,7 +3,10 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.subscribeToStorageEvents = exports.storageEventStore = exports.onStorageEvent = exports.isStorageCapturing = exports.getStorageEvents = exports.clearStorageEvents = void 0;
6
+ exports.batchPairsOf = batchPairsOf;
7
+ exports.isStorageCapturing = exports.getStorageEvents = exports.clearStorageEvents = void 0;
8
+ exports.isValueOnDevice = isValueOnDevice;
9
+ exports.subscribeToStorageEvents = exports.storageEventStore = exports.onStorageEvent = void 0;
7
10
  var _sharedUi = require("@buoy-gg/shared-ui");
8
11
  var _AsyncStorageListener = require("../utils/AsyncStorageListener");
9
12
  var _MMKVListener = require("../utils/MMKVListener");
@@ -38,6 +41,35 @@ var _mmkvTypeDetection = require("../utils/mmkvTypeDetection");
38
41
  * millisecond timestamp), and React keys must be unique per render.
39
42
  */
40
43
 
44
+ /** True when a field is the wire-form VALUE_ON_DEVICE marker. */
45
+ function isValueOnDevice(value) {
46
+ return !!value && typeof value === "object" && value.__buoyValueOnDevice === true;
47
+ }
48
+
49
+ /**
50
+ * `pairs` / `prevPairs` are AsyncStorage-only — they carry the batch
51
+ * operations (multiSet / multiRemove / multiMerge / clear), and MMKV has no
52
+ * batch API, so its `data` has no such fields. Reading them off the
53
+ * `StorageEvent` union needs narrowing, and this is the one place that does
54
+ * it; writing them unconditionally would also graft meaningless keys onto
55
+ * every MMKV event on the wire.
56
+ */
57
+ function batchPairsOf(event) {
58
+ if (event.storageType !== "async" || !event.data) return {};
59
+ return {
60
+ pairs: event.data.pairs,
61
+ prevPairs: event.data.prevPairs
62
+ };
63
+ }
64
+ function eventHasOnDeviceValues(event) {
65
+ const data = event.data;
66
+ if (!data) return false;
67
+ const {
68
+ pairs,
69
+ prevPairs
70
+ } = batchPairsOf(event);
71
+ return isValueOnDevice(data.value) || isValueOnDevice(data.prevValue) || isValueOnDevice(pairs) || isValueOnDevice(prevPairs);
72
+ }
41
73
  let storageEventIdCounter = 0;
42
74
  function nextStorageEventId() {
43
75
  return `se-${Date.now()}-${++storageEventIdCounter}`;
@@ -210,6 +242,58 @@ class StorageEventStore extends _sharedUi.BaseEventStore {
210
242
  getEventsByType(storageType) {
211
243
  return this.events.filter(event => event.storageType === storageType);
212
244
  }
245
+
246
+ /**
247
+ * Replace the event list, keeping values a remote mirror already hydrated.
248
+ * Wire-form snapshots always carry VALUE_ON_DEVICE markers for oversized
249
+ * payloads; a later snapshot must not blank the detail pane (or wipe
250
+ * `prevValue` that time-travel undo needs).
251
+ */
252
+ replaceEvents(events) {
253
+ const hydrated = new Map();
254
+ for (const existing of this.events) {
255
+ if (!eventHasOnDeviceValues(existing)) {
256
+ hydrated.set(existing.id, existing);
257
+ }
258
+ }
259
+ const merged = events.map(incoming => {
260
+ const kept = hydrated.get(incoming.id);
261
+ if (!kept || !eventHasOnDeviceValues(incoming)) return incoming;
262
+ // Restore only the fields this event's storage type actually has, so an
263
+ // MMKV event never grows `pairs`/`prevPairs` it has no meaning for.
264
+ return {
265
+ ...incoming,
266
+ data: {
267
+ ...incoming.data,
268
+ value: kept.data?.value,
269
+ prevValue: kept.data?.prevValue,
270
+ ...batchPairsOf(kept)
271
+ }
272
+ };
273
+ });
274
+ super.replaceEvents(merged);
275
+ }
276
+
277
+ /**
278
+ * Hydrate one mirrored event with real values fetched on demand from the
279
+ * device via the adapter's `getEventDetail`. Remote-mirror (desktop) only.
280
+ */
281
+ hydrateEvent(event) {
282
+ const index = this.events.findIndex(e => e.id === event.id);
283
+ if (index === -1) return;
284
+ const current = this.events[index];
285
+ const next = this.events.slice();
286
+ next[index] = {
287
+ ...current,
288
+ ...event,
289
+ timestamp: current.timestamp,
290
+ data: {
291
+ ...current.data,
292
+ ...event.data
293
+ }
294
+ };
295
+ super.replaceEvents(next);
296
+ }
213
297
  }
214
298
 
215
299
  // Singleton instance
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.storageSyncAdapter = void 0;
6
+ exports.storageSyncAdapter = exports.VALUE_ON_DEVICE = exports.SNAPSHOT_VALUE_INLINE_LIMIT = exports.SNAPSHOT_EVENTS_BUDGET = void 0;
7
7
  var _asyncStorage = _interopRequireDefault(require("@react-native-async-storage/async-storage"));
8
8
  var _sharedUi = require("@buoy-gg/shared-ui");
9
9
  var _asyncStorageCompat = require("../utils/asyncStorageCompat");
@@ -14,6 +14,148 @@ var _MMKVInstanceRegistry = require("../utils/MMKVInstanceRegistry");
14
14
  var _mmkvTypeDetection = require("../utils/mmkvTypeDetection");
15
15
  var _SecureStoreRegistry = require("../utils/SecureStoreRegistry");
16
16
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
17
+ /**
18
+ * Sentinel used in WIRE-FORM snapshots in place of oversized values.
19
+ * Storage events, MMKV browse dumps, and SecureStore dumps all reuse this
20
+ * marker. Dashboards fetch the real payload via `getEventDetail` / `mmkv.get`
21
+ * / `secure.get`.
22
+ */
23
+ const VALUE_ON_DEVICE = exports.VALUE_ON_DEVICE = Object.freeze({
24
+ __buoyValueOnDevice: true,
25
+ note: "Value stays on the device — fetched on demand via getEventDetail / mmkv.get / secure.get."
26
+ });
27
+
28
+ /** Max serialized bytes of one storage value on the list snapshot. */
29
+ const SNAPSHOT_VALUE_INLINE_LIMIT = exports.SNAPSHOT_VALUE_INLINE_LIMIT = 16 * 1024;
30
+
31
+ /**
32
+ * Total inline budget for one snapshot's event list, held under the emit
33
+ * layer's 2MB so a snapshot is never DROPPED (a drop leaves the whole panel
34
+ * stale). Per-value caps alone don't get there: the store keeps 500 events, so
35
+ * 500 writes of an individually-legal 9KB value still total ~4.7MB. Newest
36
+ * events keep their values inline; once the budget is spent, older ones
37
+ * degrade to metadata + fetch-on-demand.
38
+ */
39
+ const SNAPSHOT_EVENTS_BUDGET = exports.SNAPSHOT_EVENTS_BUDGET = 1.25 * 1024 * 1024;
40
+ const MAX_EVENT_DETAIL_BYTES = 8 * 1024 * 1024;
41
+ function toWireValue(value) {
42
+ if (value === undefined) return value;
43
+ return (0, _sharedUi.isOverWireBudget)(value, SNAPSHOT_VALUE_INLINE_LIMIT) ? VALUE_ON_DEVICE : value;
44
+ }
45
+
46
+ /**
47
+ * Apply `map` to every payload field an event carries, preserving object
48
+ * identity when nothing changed (the desktop mirror feeds these into effect
49
+ * dependencies, so a fresh object per snapshot would re-run them).
50
+ *
51
+ * The cast is deliberate and load-bearing: the wire form intentionally puts a
52
+ * marker OBJECT where the domain type declares a string, because the real
53
+ * value stayed on the device. `StorageEvent` describes on-device truth; this
54
+ * is its wire projection.
55
+ */
56
+ function mapEventValues(event, map) {
57
+ const data = event.data;
58
+ if (!data) return event;
59
+ const value = map(data.value);
60
+ const prevValue = map(data.prevValue);
61
+ const batch = (0, _storageEventStore.batchPairsOf)(event);
62
+ const pairs = map(batch.pairs);
63
+ const prevPairs = map(batch.prevPairs);
64
+ if (value === data.value && prevValue === data.prevValue && pairs === batch.pairs && prevPairs === batch.prevPairs) {
65
+ return event;
66
+ }
67
+ return {
68
+ ...event,
69
+ data: {
70
+ ...data,
71
+ value,
72
+ prevValue,
73
+ // Only AsyncStorage events have these; see batchPairsOf.
74
+ ...(event.storageType === "async" ? {
75
+ pairs,
76
+ prevPairs
77
+ } : {})
78
+ }
79
+ };
80
+ }
81
+ function capDetailValue(value) {
82
+ if (value === undefined) return value;
83
+ if (!(0, _sharedUi.isOverWireBudget)(value, MAX_EVENT_DETAIL_BYTES)) return value;
84
+ return (0, _sharedUi.isJsonSerializable)(value) ? {
85
+ __buoyTruncated: true,
86
+ note: `Exceeds the ${MAX_EVENT_DETAIL_BYTES / (1024 * 1024)}MB detail limit — inspect it on-device.`
87
+ } : VALUE_ON_DEVICE;
88
+ }
89
+
90
+ /** One event's wire form plus its measured cost, so the budget pass is free. */
91
+
92
+ /**
93
+ * Per-event wire cache. The store never mutates an event in place — `addEvent`
94
+ * prepends and `hydrateEvent` replaces — so keying on identity is correct and
95
+ * self-invalidating. Without it every value is re-measured on all ~5 snapshots
96
+ * per second for as long as it stays in the list, which for storage means
97
+ * re-walking megabytes of strings per tick.
98
+ */
99
+ const wireCache = new WeakMap();
100
+ const strippedCache = new WeakMap();
101
+ function wireFor(event) {
102
+ const cached = wireCache.get(event);
103
+ if (cached) return cached;
104
+ let entry;
105
+ try {
106
+ const wire = mapEventValues(event, toWireValue);
107
+ entry = {
108
+ event: wire,
109
+ bytes: (0, _sharedUi.approxJsonSize)(wire, SNAPSHOT_EVENTS_BUDGET).bytes
110
+ };
111
+ } catch {
112
+ // A hostile getter can blow up the walk itself. Degrading this ONE event
113
+ // beats letting getSnapshot throw and taking the whole panel with it.
114
+ const wire = mapEventValues(event, () => VALUE_ON_DEVICE);
115
+ entry = {
116
+ event: wire,
117
+ bytes: 1024
118
+ };
119
+ }
120
+ wireCache.set(event, entry);
121
+ return entry;
122
+ }
123
+
124
+ /** Metadata-only form for the tail of the list once the budget is spent. */
125
+ function strippedFor(event) {
126
+ const cached = strippedCache.get(event);
127
+ if (cached) return cached;
128
+ const wire = mapEventValues(event, v => v === undefined ? undefined : VALUE_ON_DEVICE);
129
+ const entry = {
130
+ event: wire,
131
+ bytes: (0, _sharedUi.approxJsonSize)(wire, SNAPSHOT_EVENTS_BUDGET).bytes
132
+ };
133
+ strippedCache.set(event, entry);
134
+ return entry;
135
+ }
136
+
137
+ /**
138
+ * Spend the snapshot budget newest-first. The store keeps events newest-first,
139
+ * so the developer keeps full detail on what they are actually looking at and
140
+ * the tail degrades rather than the whole panel being dropped.
141
+ */
142
+ function fitEventsToBudget(events) {
143
+ const out = new Array(events.length);
144
+ let spent = 0;
145
+ for (let i = 0; i < events.length; i++) {
146
+ const full = wireFor(events[i]);
147
+ if (spent + full.bytes <= SNAPSHOT_EVENTS_BUDGET) {
148
+ out[i] = full.event;
149
+ spent += full.bytes;
150
+ } else {
151
+ const lean = strippedFor(events[i]);
152
+ out[i] = lean.event;
153
+ spent += lean.bytes;
154
+ }
155
+ }
156
+ return out;
157
+ }
158
+
17
159
  /** One MMKV entry in a `mmkv.snapshot` result. */
18
160
 
19
161
  /** One MMKV instance in a `mmkv.snapshot` result. */
@@ -51,22 +193,53 @@ function assertMMKVWritable(instanceId) {
51
193
  *
52
194
  * The `async.*` actions mirror the AsyncStorage API so the desktop dashboard
53
195
  * can proxy its browse/edit mode against the device's real storage.
196
+ *
197
+ * v2: oversized / unserializable event values are replaced with
198
+ * VALUE_ON_DEVICE. Dashboards fetch them via `getEventDetail`.
54
199
  */
55
200
  const storageSyncAdapter = exports.storageSyncAdapter = {
56
- version: 1,
201
+ // v3: a total snapshot budget, so a burst of individually-legal values
202
+ // degrades the oldest events instead of the emit layer dropping the panel.
203
+ version: 3,
57
204
  // Strip Buoy's OWN devtool keys (@react_buoy*, @buoy*, buoy-*, legacy) at the
58
205
  // source so no sync client (desktop, MCP, raw socket) ever sees them as if
59
206
  // they were app data — matching the tools' default key filters, but
60
207
  // authoritative instead of UI-only.
61
- getSnapshot: () => _storageEventStore.storageEventStore.getEvents().filter(e => {
208
+ getSnapshot: () => fitEventsToBudget(_storageEventStore.storageEventStore.getEvents().filter(e => {
62
209
  const key = e.data?.key;
63
210
  return !(typeof key === "string" && (0, _sharedUi.isDevToolsStorageKey)(key));
64
- }),
211
+ })),
65
212
  subscribe: onChange => _storageEventStore.storageEventStore.subscribeToEvents(onChange),
66
213
  actions: {
67
214
  clearEvents: () => {
68
215
  _storageEventStore.storageEventStore.clearEvents();
69
216
  },
217
+ /**
218
+ * On-demand detail: one event's real value/prevValue/pairs.
219
+ * The per-snapshot stream stays payload-light; this is the explicit,
220
+ * size-guarded channel for the detail pane (and for time-travel undo,
221
+ * which needs `prevValue`).
222
+ */
223
+ getEventDetail: params => {
224
+ const {
225
+ id
226
+ } = params ?? {};
227
+ if (!id) return {
228
+ found: false,
229
+ reason: "missing id"
230
+ };
231
+ const event = _storageEventStore.storageEventStore.getEvents().find(e => e.id === id);
232
+ if (!event) return {
233
+ found: false,
234
+ reason: "unknown id"
235
+ };
236
+ const detailed = mapEventValues(event, capDetailValue);
237
+ return {
238
+ found: true,
239
+ id,
240
+ event: detailed
241
+ };
242
+ },
70
243
  /** Clears all app storage keys, preserving dev tools settings. */
71
244
  clearAppStorage: () => (0, _clearAllStorage.clearAllAppStorage)(),
72
245
  // ── Time travel (same code path as the modal's UNDO / JUMP buttons) ──
@@ -163,7 +336,7 @@ const storageSyncAdapter = exports.storageSyncAdapter = {
163
336
  } = (0, _mmkvTypeDetection.detectMMKVType)(instance, key);
164
337
  return {
165
338
  key,
166
- value,
339
+ value: toWireValue(value),
167
340
  valueType
168
341
  };
169
342
  });
@@ -177,6 +350,41 @@ const storageSyncAdapter = exports.storageSyncAdapter = {
177
350
  entries
178
351
  };
179
352
  }),
353
+ /**
354
+ * On-demand MMKV value. `mmkv.snapshot` omits oversized strings so a
355
+ * browse poll cannot blow the action-result budget; this is the
356
+ * size-guarded channel for one key.
357
+ */
358
+ "mmkv.get": params => {
359
+ const {
360
+ instanceId,
361
+ key
362
+ } = params ?? {};
363
+ if (!instanceId || !key) {
364
+ return {
365
+ found: false,
366
+ reason: "missing instanceId/key"
367
+ };
368
+ }
369
+ const registered = _MMKVInstanceRegistry.mmkvInstanceRegistry.get(instanceId);
370
+ if (!registered) {
371
+ return {
372
+ found: false,
373
+ reason: "unknown instance"
374
+ };
375
+ }
376
+ const {
377
+ value,
378
+ type: valueType
379
+ } = (0, _mmkvTypeDetection.detectMMKVType)(registered.instance, key);
380
+ return {
381
+ found: true,
382
+ instanceId,
383
+ key,
384
+ value: capDetailValue(value),
385
+ valueType
386
+ };
387
+ },
180
388
  "mmkv.set": params => {
181
389
  const {
182
390
  instanceId,
@@ -236,7 +444,7 @@ const storageSyncAdapter = exports.storageSyncAdapter = {
236
444
  description,
237
445
  keychainService: options.keychainService,
238
446
  requireAuthentication: options.requireAuthentication ?? false,
239
- value
447
+ value: value == null ? value : toWireValue(value)
240
448
  });
241
449
  }
242
450
  return result;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.requiredKeyAppliesTo = requiredKeyAppliesTo;
7
+ /**
8
+ * Whether a required-key declaration should be enforced by a given backend
9
+ * hook. Explicit `storageType` wins; bare string keys default to AsyncStorage
10
+ * (the historical zero-config backend). Objects without `storageType` still
11
+ * apply to every backend — matching prior behavior for apps that never set it.
12
+ */
13
+ function requiredKeyAppliesTo(req, backend) {
14
+ if (typeof req === "string") return backend === "async";
15
+ if (req.storageType) return req.storageType === backend;
16
+ return true;
17
+ }