@buoy-gg/storage 7.0.17 → 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.
- package/lib/commonjs/index.js +12 -0
- package/lib/commonjs/storage/components/DiffViewer/modes/ThemedSplitView.js +5 -602
- package/lib/commonjs/storage/components/StorageModalWithTabs.js +17 -1
- package/lib/commonjs/storage/hooks/useAsyncStorageKeys.js +13 -10
- package/lib/commonjs/storage/hooks/useMMKVKeys.js +28 -23
- package/lib/commonjs/storage/stores/storageEventStore.js +85 -1
- package/lib/commonjs/storage/sync/storageSyncAdapter.js +214 -6
- package/lib/commonjs/storage/utils/requiredStorageKeyScope.js +17 -0
- package/lib/commonjs/storage/utils/snapshotProvider.js +173 -5
- package/lib/commonjs/storage/utils/storageTimeTravelUtils.js +17 -3
- package/lib/module/index.js +6 -1
- package/lib/module/storage/components/DiffViewer/modes/ThemedSplitView.js +7 -603
- package/lib/module/storage/components/StorageModalWithTabs.js +17 -1
- package/lib/module/storage/hooks/useAsyncStorageKeys.js +13 -10
- package/lib/module/storage/hooks/useMMKVKeys.js +28 -23
- package/lib/module/storage/stores/storageEventStore.js +81 -0
- package/lib/module/storage/sync/storageSyncAdapter.js +215 -7
- package/lib/module/storage/utils/requiredStorageKeyScope.js +13 -0
- package/lib/module/storage/utils/snapshotProvider.js +173 -5
- package/lib/module/storage/utils/storageTimeTravelUtils.js +17 -3
- package/lib/typescript/index.d.ts +2 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/storage/components/DiffViewer/modes/ThemedSplitView.d.ts +7 -18
- package/lib/typescript/storage/components/DiffViewer/modes/ThemedSplitView.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageModalWithTabs.d.ts +8 -1
- package/lib/typescript/storage/components/StorageModalWithTabs.d.ts.map +1 -1
- package/lib/typescript/storage/hooks/useAsyncStorageKeys.d.ts.map +1 -1
- package/lib/typescript/storage/hooks/useMMKVKeys.d.ts.map +1 -1
- package/lib/typescript/storage/stores/storageEventStore.d.ts +26 -0
- package/lib/typescript/storage/stores/storageEventStore.d.ts.map +1 -1
- package/lib/typescript/storage/sync/storageSyncAdapter.d.ts +69 -3
- package/lib/typescript/storage/sync/storageSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/storage/types.d.ts +9 -9
- package/lib/typescript/storage/types.d.ts.map +1 -1
- package/lib/typescript/storage/utils/requiredStorageKeyScope.d.ts +10 -0
- package/lib/typescript/storage/utils/requiredStorageKeyScope.d.ts.map +1 -0
- package/lib/typescript/storage/utils/storageTimeTravelUtils.d.ts.map +1 -1
- package/package.json +5 -5
|
@@ -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' &&
|
|
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' &&
|
|
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' &&
|
|
84
|
+
...(typeof requiredConfig === 'object' && requiredConfig.expectedValue !== undefined && {
|
|
83
85
|
expectedValue: requiredConfig.expectedValue
|
|
84
86
|
}),
|
|
85
|
-
...(typeof requiredConfig === 'object' &&
|
|
87
|
+
...(typeof requiredConfig === 'object' && requiredConfig.expectedType !== undefined && {
|
|
86
88
|
expectedType: requiredConfig.expectedType
|
|
87
89
|
}),
|
|
88
|
-
...(typeof requiredConfig === 'object' &&
|
|
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' &&
|
|
109
|
+
...(typeof req === 'object' && req.expectedValue !== undefined && {
|
|
107
110
|
expectedValue: req.expectedValue
|
|
108
111
|
}),
|
|
109
|
-
...(typeof req === 'object' &&
|
|
112
|
+
...(typeof req === 'object' && req.expectedType !== undefined && {
|
|
110
113
|
expectedType: req.expectedType
|
|
111
114
|
}),
|
|
112
|
-
...(typeof req === 'object' &&
|
|
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'
|
|
104
|
-
const expectedValue = typeof req === 'object'
|
|
105
|
-
const expectedType = typeof req === 'object'
|
|
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' &&
|
|
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' &&
|
|
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' &&
|
|
168
|
+
...(typeof requiredConfig === 'object' && requiredConfig.expectedValue !== undefined && {
|
|
167
169
|
expectedValue: requiredConfig.expectedValue
|
|
168
170
|
}),
|
|
169
|
-
...(typeof requiredConfig === 'object' &&
|
|
171
|
+
...(typeof requiredConfig === 'object' && requiredConfig.expectedType !== undefined && {
|
|
170
172
|
expectedType: requiredConfig.expectedType
|
|
171
173
|
}),
|
|
172
|
-
...(typeof requiredConfig === 'object' &&
|
|
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' &&
|
|
195
|
+
...(typeof req === 'object' && req.expectedValue !== undefined && {
|
|
193
196
|
expectedValue: req.expectedValue
|
|
194
197
|
}),
|
|
195
|
-
...(typeof req === 'object' &&
|
|
198
|
+
...(typeof req === 'object' && req.expectedType !== undefined && {
|
|
196
199
|
expectedType: req.expectedType
|
|
197
200
|
}),
|
|
198
|
-
...(typeof req === 'object' &&
|
|
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' &&
|
|
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' &&
|
|
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' &&
|
|
315
|
+
...(typeof requiredConfig === 'object' && requiredConfig.expectedValue !== undefined && {
|
|
312
316
|
expectedValue: requiredConfig.expectedValue
|
|
313
317
|
}),
|
|
314
|
-
...(typeof requiredConfig === 'object' &&
|
|
318
|
+
...(typeof requiredConfig === 'object' && requiredConfig.expectedType !== undefined && {
|
|
315
319
|
expectedType: requiredConfig.expectedType
|
|
316
320
|
}),
|
|
317
|
-
...(typeof requiredConfig === 'object' &&
|
|
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' &&
|
|
346
|
+
...(typeof req === 'object' && req.expectedValue !== undefined && {
|
|
342
347
|
expectedValue: req.expectedValue
|
|
343
348
|
}),
|
|
344
|
-
...(typeof req === 'object' &&
|
|
349
|
+
...(typeof req === 'object' && req.expectedType !== undefined && {
|
|
345
350
|
expectedType: req.expectedType
|
|
346
351
|
}),
|
|
347
|
-
...(typeof req === 'object' &&
|
|
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.
|
|
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
|
-
|
|
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
|
+
}
|