@buoy-gg/storage 7.0.17 → 7.0.19
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 +80 -14
- 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 +11 -6
|
@@ -36,7 +36,9 @@ function status() {
|
|
|
36
36
|
canRestore: true
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
|
-
|
|
39
|
+
|
|
40
|
+
/** Shared by capture() and the live side of previewItems(). */
|
|
41
|
+
async function captureStorageData() {
|
|
40
42
|
const warnings = [];
|
|
41
43
|
const appKeys = (await _asyncStorage.default.getAllKeys()).filter(k => !(0, _sharedUi.isDevToolsStorageKey)(k));
|
|
42
44
|
const asyncPairs = (await (0, _asyncStorageCompat.readMany)(appKeys)).filter(pair => pair[1] !== null);
|
|
@@ -94,8 +96,23 @@ async function capture() {
|
|
|
94
96
|
warnings
|
|
95
97
|
};
|
|
96
98
|
}
|
|
97
|
-
async function
|
|
99
|
+
async function capture() {
|
|
100
|
+
const {
|
|
101
|
+
data,
|
|
102
|
+
warnings
|
|
103
|
+
} = await captureStorageData();
|
|
104
|
+
return {
|
|
105
|
+
data,
|
|
106
|
+
warnings
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
async function restore(data, options) {
|
|
98
110
|
const snapshot = data;
|
|
111
|
+
const excludedSet = new Set(options?.excludeKeys ?? []);
|
|
112
|
+
const includedSet = options?.includeKeys ? new Set(options.includeKeys) : null;
|
|
113
|
+
/** True when this item may be touched (scope + exclusions agree). */
|
|
114
|
+
const touch = key => !excludedSet.has(key) && (!includedSet || includedSet.has(key));
|
|
115
|
+
let excludedHits = 0;
|
|
99
116
|
const result = {
|
|
100
117
|
ok: true,
|
|
101
118
|
applied: 0,
|
|
@@ -113,10 +130,11 @@ async function restore(data) {
|
|
|
113
130
|
|
|
114
131
|
// ── AsyncStorage: delete app keys missing from the snapshot, write the rest ──
|
|
115
132
|
try {
|
|
116
|
-
const snapPairs = (snapshot.async ?? []).filter(([k]) => !(0, _sharedUi.isDevToolsStorageKey)(k));
|
|
133
|
+
const snapPairs = (snapshot.async ?? []).filter(([k]) => !(0, _sharedUi.isDevToolsStorageKey)(k) && touch(`async:${k}`));
|
|
117
134
|
const snapKeys = new Set(snapPairs.map(([k]) => k));
|
|
118
135
|
const liveKeys = (await _asyncStorage.default.getAllKeys()).filter(k => !(0, _sharedUi.isDevToolsStorageKey)(k));
|
|
119
|
-
const toRemove = liveKeys.filter(k => !snapKeys.has(k));
|
|
136
|
+
const toRemove = liveKeys.filter(k => !snapKeys.has(k) && touch(`async:${k}`));
|
|
137
|
+
excludedHits += (snapshot.async ?? []).length - snapPairs.length;
|
|
120
138
|
if (toRemove.length > 0) await (0, _asyncStorageCompat.removeMany)(toRemove);
|
|
121
139
|
if (snapPairs.length > 0) await (0, _asyncStorageCompat.writeMany)(snapPairs);
|
|
122
140
|
result.applied += snapPairs.length + toRemove.length;
|
|
@@ -149,6 +167,10 @@ async function restore(data) {
|
|
|
149
167
|
const snapKeys = new Set(entries.map(e => e.key));
|
|
150
168
|
const liveKeys = instance.getAllKeys().filter(key => !(0, _sharedUi.isDevToolsStorageKey)(key));
|
|
151
169
|
for (const key of liveKeys) {
|
|
170
|
+
if (!touch(`mmkv:${id}/${key}`)) {
|
|
171
|
+
excludedHits += 1;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
152
174
|
if (!snapKeys.has(key)) {
|
|
153
175
|
deleteMMKVKey(instance, key);
|
|
154
176
|
result.applied += 1;
|
|
@@ -156,6 +178,7 @@ async function restore(data) {
|
|
|
156
178
|
}
|
|
157
179
|
for (const entry of entries) {
|
|
158
180
|
if ((0, _sharedUi.isDevToolsStorageKey)(entry.key)) continue;
|
|
181
|
+
if (!touch(`mmkv:${id}/${entry.key}`)) continue;
|
|
159
182
|
if (entry.valueType === "buffer") {
|
|
160
183
|
result.skipped.push({
|
|
161
184
|
id: `mmkv:${id}/${entry.key}`,
|
|
@@ -178,6 +201,10 @@ async function restore(data) {
|
|
|
178
201
|
key,
|
|
179
202
|
options
|
|
180
203
|
} of _SecureStoreRegistry.secureStoreRegistry.getAll()) {
|
|
204
|
+
if (!touch(`secure:${key}`)) {
|
|
205
|
+
excludedHits += 1;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
181
208
|
if (options.requireAuthentication) {
|
|
182
209
|
if (key in secureSnap) {
|
|
183
210
|
result.skipped.push({
|
|
@@ -202,6 +229,7 @@ async function restore(data) {
|
|
|
202
229
|
}
|
|
203
230
|
}
|
|
204
231
|
for (const key of Object.keys(secureSnap)) {
|
|
232
|
+
if (!touch(`secure:${key}`)) continue;
|
|
205
233
|
if (!_SecureStoreRegistry.secureStoreRegistry.getAll().some(e => e.key === key)) {
|
|
206
234
|
result.skipped.push({
|
|
207
235
|
id: `secure:${key}`,
|
|
@@ -209,6 +237,9 @@ async function restore(data) {
|
|
|
209
237
|
});
|
|
210
238
|
}
|
|
211
239
|
}
|
|
240
|
+
if (excludedHits > 0) {
|
|
241
|
+
result.warnings.push(`${excludedHits} item(s) excluded by you`);
|
|
242
|
+
}
|
|
212
243
|
return result;
|
|
213
244
|
}
|
|
214
245
|
|
|
@@ -269,11 +300,148 @@ async function clear() {
|
|
|
269
300
|
}
|
|
270
301
|
return result;
|
|
271
302
|
}
|
|
303
|
+
const ITEM_RENDER_CHAR_LIMIT = 256 * 1024;
|
|
304
|
+
function classifyValue(key, label, oldValue, newValue, inOld, inNew) {
|
|
305
|
+
if (!inNew) return {
|
|
306
|
+
key,
|
|
307
|
+
label,
|
|
308
|
+
verdict: "removed",
|
|
309
|
+
oldValue
|
|
310
|
+
};
|
|
311
|
+
if (!inOld) return {
|
|
312
|
+
key,
|
|
313
|
+
label,
|
|
314
|
+
verdict: "added",
|
|
315
|
+
newValue
|
|
316
|
+
};
|
|
317
|
+
if (Object.is(oldValue, newValue)) return {
|
|
318
|
+
key,
|
|
319
|
+
label,
|
|
320
|
+
verdict: "unchanged"
|
|
321
|
+
};
|
|
322
|
+
const tooLarge = typeof oldValue === "string" && oldValue.length > ITEM_RENDER_CHAR_LIMIT || typeof newValue === "string" && newValue.length > ITEM_RENDER_CHAR_LIMIT;
|
|
323
|
+
if (tooLarge) {
|
|
324
|
+
return {
|
|
325
|
+
key,
|
|
326
|
+
label,
|
|
327
|
+
verdict: "too-large",
|
|
328
|
+
reason: "value too large to render a diff — restore still applies it"
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
key,
|
|
333
|
+
label,
|
|
334
|
+
verdict: "changed",
|
|
335
|
+
oldValue,
|
|
336
|
+
newValue
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Per-key restore preview across all three backends. */
|
|
341
|
+
async function previewItems(target, base) {
|
|
342
|
+
const targetData = target ?? {
|
|
343
|
+
async: [],
|
|
344
|
+
mmkv: {},
|
|
345
|
+
secure: {}
|
|
346
|
+
};
|
|
347
|
+
const liveMode = base === undefined;
|
|
348
|
+
const baseData = liveMode ? (await captureStorageData()).data : base ?? {
|
|
349
|
+
async: [],
|
|
350
|
+
mmkv: {},
|
|
351
|
+
secure: {}
|
|
352
|
+
};
|
|
353
|
+
const items = [];
|
|
354
|
+
const notes = [];
|
|
355
|
+
|
|
356
|
+
// ── AsyncStorage ──
|
|
357
|
+
const oldAsync = new Map(baseData.async ?? []);
|
|
358
|
+
const newAsync = new Map(targetData.async ?? []);
|
|
359
|
+
for (const key of new Set([...oldAsync.keys(), ...newAsync.keys()])) {
|
|
360
|
+
if ((0, _sharedUi.isDevToolsStorageKey)(key)) continue;
|
|
361
|
+
items.push(classifyValue(`async:${key}`, key, oldAsync.get(key), newAsync.get(key), oldAsync.has(key), newAsync.has(key)));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ── MMKV ──
|
|
365
|
+
const instanceIds = new Set([...Object.keys(baseData.mmkv ?? {}), ...Object.keys(targetData.mmkv ?? {})]);
|
|
366
|
+
for (const id of instanceIds) {
|
|
367
|
+
const registered = liveMode ? _MMKVInstanceRegistry.mmkvInstanceRegistry.get(id) : null;
|
|
368
|
+
const instanceUnavailable = liveMode && !registered;
|
|
369
|
+
const readOnly = liveMode && registered?.instance.isReadOnly === true;
|
|
370
|
+
const oldEntries = new Map((baseData.mmkv?.[id] ?? []).map(e => [e.key, e]));
|
|
371
|
+
const newEntries = new Map((targetData.mmkv?.[id] ?? []).map(e => [e.key, e]));
|
|
372
|
+
for (const key of new Set([...oldEntries.keys(), ...newEntries.keys()])) {
|
|
373
|
+
if ((0, _sharedUi.isDevToolsStorageKey)(key)) continue;
|
|
374
|
+
const itemKey = `mmkv:${id}/${key}`;
|
|
375
|
+
const label = `${id} · ${key}`;
|
|
376
|
+
const oldE = oldEntries.get(key);
|
|
377
|
+
const newE = newEntries.get(key);
|
|
378
|
+
let item = classifyValue(itemKey, label, oldE?.value, newE?.value, oldEntries.has(key), newEntries.has(key));
|
|
379
|
+
if (item.verdict !== "unchanged") {
|
|
380
|
+
if (instanceUnavailable) {
|
|
381
|
+
item = {
|
|
382
|
+
...item,
|
|
383
|
+
verdict: "wont-apply",
|
|
384
|
+
reason: "MMKV instance not registered"
|
|
385
|
+
};
|
|
386
|
+
} else if (readOnly) {
|
|
387
|
+
item = {
|
|
388
|
+
...item,
|
|
389
|
+
verdict: "wont-apply",
|
|
390
|
+
reason: "MMKV instance registered read-only"
|
|
391
|
+
};
|
|
392
|
+
} else if (newE?.valueType === "buffer" || oldE?.valueType === "buffer") {
|
|
393
|
+
item = {
|
|
394
|
+
...item,
|
|
395
|
+
verdict: "wont-apply",
|
|
396
|
+
reason: "ArrayBuffer values cannot be restored"
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
items.push(item);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ── SecureStore ──
|
|
405
|
+
const oldSecure = baseData.secure ?? {};
|
|
406
|
+
const newSecure = targetData.secure ?? {};
|
|
407
|
+
const registeredSecure = liveMode ? new Map(_SecureStoreRegistry.secureStoreRegistry.getAll().map(e => [e.key, e])) : null;
|
|
408
|
+
for (const key of new Set([...Object.keys(oldSecure), ...Object.keys(newSecure)])) {
|
|
409
|
+
let item = classifyValue(`secure:${key}`, key, oldSecure[key], newSecure[key], key in oldSecure, key in newSecure);
|
|
410
|
+
if (liveMode && item.verdict !== "unchanged") {
|
|
411
|
+
const entry = registeredSecure?.get(key);
|
|
412
|
+
if (!entry) {
|
|
413
|
+
item = {
|
|
414
|
+
...item,
|
|
415
|
+
verdict: "wont-apply",
|
|
416
|
+
reason: "key no longer registered with registerSecureStoreKeys"
|
|
417
|
+
};
|
|
418
|
+
} else if (entry.options.requireAuthentication) {
|
|
419
|
+
item = {
|
|
420
|
+
...item,
|
|
421
|
+
verdict: "wont-apply",
|
|
422
|
+
reason: "biometric-protected keys are never written"
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
items.push(item);
|
|
427
|
+
}
|
|
428
|
+
if (liveMode) {
|
|
429
|
+
const biometric = _SecureStoreRegistry.secureStoreRegistry.getAll().filter(e => e.options.requireAuthentication).length;
|
|
430
|
+
if (biometric > 0) {
|
|
431
|
+
notes.push(`${biometric} biometric-protected secure key(s) are never read or written`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
items,
|
|
436
|
+
notes
|
|
437
|
+
};
|
|
438
|
+
}
|
|
272
439
|
(0, _sharedUi.registerStateSnapshotProvider)({
|
|
273
440
|
id: "storage",
|
|
274
441
|
label: "Storage",
|
|
275
442
|
status,
|
|
276
443
|
capture,
|
|
277
444
|
restore,
|
|
278
|
-
clear
|
|
445
|
+
clear,
|
|
446
|
+
previewItems
|
|
279
447
|
});
|
|
@@ -16,6 +16,10 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
|
|
|
16
16
|
* Provides jump and undo functionality for AsyncStorage operations.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
function isOmittedStorageValue(value) {
|
|
20
|
+
return !!value && typeof value === "object" && value.__buoyValueOnDevice === true;
|
|
21
|
+
}
|
|
22
|
+
|
|
19
23
|
/**
|
|
20
24
|
* Undo a single storage operation
|
|
21
25
|
*
|
|
@@ -35,6 +39,9 @@ async function undoOperation(event) {
|
|
|
35
39
|
action,
|
|
36
40
|
data
|
|
37
41
|
} = event;
|
|
42
|
+
if (isOmittedStorageValue(data?.prevValue) || isOmittedStorageValue(data?.prevPairs)) {
|
|
43
|
+
throw new Error("Cannot undo: previous value is still on the device — fetch getEventDetail first");
|
|
44
|
+
}
|
|
38
45
|
{
|
|
39
46
|
switch (action) {
|
|
40
47
|
case "setItem":
|
|
@@ -113,6 +120,12 @@ async function jumpToState(events, targetEventIndex) {
|
|
|
113
120
|
if (targetEventIndex < 0 || targetEventIndex >= events.length) {
|
|
114
121
|
throw new Error(`Invalid event index: ${targetEventIndex}`);
|
|
115
122
|
}
|
|
123
|
+
for (const event of events) {
|
|
124
|
+
const data = event.data;
|
|
125
|
+
if (isOmittedStorageValue(data?.value) || isOmittedStorageValue(data?.prevValue) || isOmittedStorageValue(data?.pairs) || isOmittedStorageValue(data?.prevPairs)) {
|
|
126
|
+
throw new Error("Cannot jump: some event values are still on the device — fetch getEventDetail first");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
116
129
|
|
|
117
130
|
// Build up the state by applying each event up to and including the target.
|
|
118
131
|
// We use the final values from each event, not the operations themselves.
|
|
@@ -260,14 +273,15 @@ function canUndo(event) {
|
|
|
260
273
|
case "setItem":
|
|
261
274
|
case "mergeItem":
|
|
262
275
|
case "removeItem":
|
|
263
|
-
// Can undo if we have the key
|
|
264
|
-
|
|
276
|
+
// Can undo if we have the key — but not while prevValue is still a
|
|
277
|
+
// wire-form on-device marker (restoring that would write garbage).
|
|
278
|
+
return !!data?.key && !isOmittedStorageValue(data.prevValue);
|
|
265
279
|
case "multiSet":
|
|
266
280
|
case "multiRemove":
|
|
267
281
|
case "multiMerge":
|
|
268
282
|
case "clear":
|
|
269
283
|
// Can undo if we have previous pairs
|
|
270
|
-
return !!data?.prevPairs && data.prevPairs.length > 0;
|
|
284
|
+
return !!data?.prevPairs && data.prevPairs.length > 0 && !isOmittedStorageValue(data.prevPairs);
|
|
271
285
|
default:
|
|
272
286
|
return false;
|
|
273
287
|
}
|
package/lib/module/index.js
CHANGED
|
@@ -82,7 +82,12 @@ export { storageSyncAdapter } from "./storage/sync/storageSyncAdapter";
|
|
|
82
82
|
// INTERNAL EXPORTS (For @buoy-gg/* packages only - not part of public API)
|
|
83
83
|
// =============================================================================
|
|
84
84
|
/** @internal */
|
|
85
|
-
export { storageEventStore
|
|
85
|
+
export { storageEventStore,
|
|
86
|
+
// Exported so remote mirrors (Buoy Desktop) can ask "did the device withhold
|
|
87
|
+
// this value?" without re-deriving the wire contract — and, in the case of
|
|
88
|
+
// `batchPairsOf`, without re-introducing the union-narrowing bug that reading
|
|
89
|
+
// `pairs`/`prevPairs` off a StorageEvent invites.
|
|
90
|
+
isValueOnDevice, batchPairsOf } from "./storage/stores/storageEventStore";
|
|
86
91
|
/**
|
|
87
92
|
* Tell a mounted storage browser to re-read every backend. Used by the desktop
|
|
88
93
|
* dashboard, where local storage listeners are suppressed and device data
|