@buoy-gg/storage 7.0.4 → 7.0.5
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 +77 -1
- package/lib/commonjs/storage/components/DiffViewer/TreeDiffViewer.js +10 -3
- package/lib/commonjs/storage/components/DiffViewer/modes/ThemedSplitView.js +26 -8
- package/lib/commonjs/storage/components/GameUIStorageBrowser.js +263 -115
- package/lib/commonjs/storage/components/GameUIStorageStats.js +7 -7
- package/lib/commonjs/storage/components/MMKVInstanceInfoPanel.js +1 -1
- package/lib/commonjs/storage/components/MMKVInstanceSelector.js +1 -1
- package/lib/commonjs/storage/components/SelectionActionBar.js +2 -36
- package/lib/commonjs/storage/components/StorageActions.js +6 -6
- package/lib/commonjs/storage/components/StorageBrowserMode.js +16 -2
- package/lib/commonjs/storage/components/StorageEventDetailContent.js +106 -149
- package/lib/commonjs/storage/components/StorageEventListener.js +13 -13
- package/lib/commonjs/storage/components/StorageFilterCards.js +13 -135
- package/lib/commonjs/storage/components/StorageKeyCard.js +1 -1
- package/lib/commonjs/storage/components/StorageKeyEditorScreen.js +392 -0
- package/lib/commonjs/storage/components/StorageKeyRow.js +53 -15
- package/lib/commonjs/storage/components/StorageKeySection.js +10 -4
- package/lib/commonjs/storage/components/StorageKeyStats.js +10 -10
- package/lib/commonjs/storage/components/StorageModalWithTabs.js +272 -129
- package/lib/commonjs/storage/components/StorageScalarKeyEditor.js +46 -0
- package/lib/commonjs/storage/components/StorageTypeFilterBar.js +141 -0
- package/lib/commonjs/storage/components/StorageValueEditModal.js +361 -0
- package/lib/commonjs/storage/hooks/useKeyEditor.js +104 -0
- package/lib/commonjs/storage/hooks/useMeasuredWidth.js +74 -0
- package/lib/commonjs/storage/hooks/useSecureStoreKeys.js +70 -15
- package/lib/commonjs/storage/hooks/useTreeDraft.js +107 -0
- package/lib/commonjs/storage/index.js +29 -0
- package/lib/commonjs/storage/stores/storageEventStore.js +4 -2
- package/lib/commonjs/storage/sync/storageSyncAdapter.js +125 -3
- package/lib/commonjs/storage/utils/AsyncStorageListener.js +76 -12
- package/lib/commonjs/storage/utils/MMKVInstanceRegistry.js +10 -9
- package/lib/commonjs/storage/utils/MMKVListener.js +40 -4
- package/lib/commonjs/storage/utils/SecureStoreRegistry.js +52 -1
- package/lib/commonjs/storage/utils/deleteStorageKeys.js +53 -0
- package/lib/commonjs/storage/utils/index.js +13 -0
- package/lib/commonjs/storage/utils/lineDiff.js +21 -354
- package/lib/commonjs/storage/utils/setStorageValue.js +218 -0
- package/lib/commonjs/storage/utils/storageBrowserRefresh.js +49 -0
- package/lib/commonjs/storage/utils/storageOps.js +53 -0
- package/lib/commonjs/storage/utils/storageTimeTravelUtils.js +60 -38
- package/lib/commonjs/storage/utils/valueType.js +24 -0
- package/lib/module/index.js +18 -1
- package/lib/module/storage/components/DiffViewer/TreeDiffViewer.js +10 -3
- package/lib/module/storage/components/DiffViewer/modes/ThemedSplitView.js +26 -8
- package/lib/module/storage/components/GameUIStorageBrowser.js +266 -118
- package/lib/module/storage/components/GameUIStorageStats.js +7 -7
- package/lib/module/storage/components/MMKVInstanceInfoPanel.js +1 -1
- package/lib/module/storage/components/MMKVInstanceSelector.js +1 -1
- package/lib/module/storage/components/SelectionActionBar.js +2 -36
- package/lib/module/storage/components/StorageActions.js +7 -7
- package/lib/module/storage/components/StorageBrowserMode.js +16 -2
- package/lib/module/storage/components/StorageEventDetailContent.js +110 -151
- package/lib/module/storage/components/StorageEventListener.js +14 -14
- package/lib/module/storage/components/StorageFilterCards.js +13 -135
- package/lib/module/storage/components/StorageKeyCard.js +1 -1
- package/lib/module/storage/components/StorageKeyEditorScreen.js +388 -0
- package/lib/module/storage/components/StorageKeyRow.js +55 -17
- package/lib/module/storage/components/StorageKeySection.js +11 -5
- package/lib/module/storage/components/StorageKeyStats.js +11 -10
- package/lib/module/storage/components/StorageModalWithTabs.js +272 -130
- package/lib/module/storage/components/StorageScalarKeyEditor.js +42 -0
- package/lib/module/storage/components/StorageTypeFilterBar.js +136 -0
- package/lib/module/storage/components/StorageValueEditModal.js +357 -0
- package/lib/module/storage/hooks/useKeyEditor.js +100 -0
- package/lib/module/storage/hooks/useMeasuredWidth.js +71 -0
- package/lib/module/storage/hooks/useSecureStoreKeys.js +71 -16
- package/lib/module/storage/hooks/useTreeDraft.js +103 -0
- package/lib/module/storage/index.js +3 -0
- package/lib/module/storage/stores/storageEventStore.js +4 -2
- package/lib/module/storage/sync/storageSyncAdapter.js +125 -3
- package/lib/module/storage/utils/AsyncStorageListener.js +76 -12
- package/lib/module/storage/utils/MMKVInstanceRegistry.js +8 -7
- package/lib/module/storage/utils/MMKVListener.js +40 -4
- package/lib/module/storage/utils/SecureStoreRegistry.js +52 -1
- package/lib/module/storage/utils/deleteStorageKeys.js +49 -0
- package/lib/module/storage/utils/index.js +3 -0
- package/lib/module/storage/utils/lineDiff.js +7 -354
- package/lib/module/storage/utils/setStorageValue.js +209 -0
- package/lib/module/storage/utils/storageBrowserRefresh.js +44 -0
- package/lib/module/storage/utils/storageOps.js +46 -0
- package/lib/module/storage/utils/storageTimeTravelUtils.js +61 -40
- package/lib/module/storage/utils/valueType.js +23 -0
- package/lib/typescript/index.d.ts +12 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/storage/components/DiffViewer/TreeDiffViewer.d.ts +4 -1
- package/lib/typescript/storage/components/DiffViewer/TreeDiffViewer.d.ts.map +1 -1
- package/lib/typescript/storage/components/DiffViewer/modes/ThemedSplitView.d.ts +7 -1
- package/lib/typescript/storage/components/DiffViewer/modes/ThemedSplitView.d.ts.map +1 -1
- package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts +27 -1
- package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts.map +1 -1
- package/lib/typescript/storage/components/SelectionActionBar.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageBrowserMode.d.ts +17 -1
- package/lib/typescript/storage/components/StorageBrowserMode.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageEventDetailContent.d.ts +4 -1
- package/lib/typescript/storage/components/StorageEventDetailContent.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageFilterCards.d.ts +9 -3
- package/lib/typescript/storage/components/StorageFilterCards.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageKeyEditorScreen.d.ts +42 -0
- package/lib/typescript/storage/components/StorageKeyEditorScreen.d.ts.map +1 -0
- package/lib/typescript/storage/components/StorageKeyRow.d.ts +17 -0
- package/lib/typescript/storage/components/StorageKeyRow.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageKeySection.d.ts +7 -1
- package/lib/typescript/storage/components/StorageKeySection.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageKeyStats.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageModalWithTabs.d.ts +28 -1
- package/lib/typescript/storage/components/StorageModalWithTabs.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageScalarKeyEditor.d.ts +24 -0
- package/lib/typescript/storage/components/StorageScalarKeyEditor.d.ts.map +1 -0
- package/lib/typescript/storage/components/StorageTypeFilterBar.d.ts +29 -0
- package/lib/typescript/storage/components/StorageTypeFilterBar.d.ts.map +1 -0
- package/lib/typescript/storage/components/StorageValueEditModal.d.ts +36 -0
- package/lib/typescript/storage/components/StorageValueEditModal.d.ts.map +1 -0
- package/lib/typescript/storage/hooks/useKeyEditor.d.ts +51 -0
- package/lib/typescript/storage/hooks/useKeyEditor.d.ts.map +1 -0
- package/lib/typescript/storage/hooks/useMeasuredWidth.d.ts +37 -0
- package/lib/typescript/storage/hooks/useMeasuredWidth.d.ts.map +1 -0
- package/lib/typescript/storage/hooks/useSecureStoreKeys.d.ts +8 -1
- package/lib/typescript/storage/hooks/useSecureStoreKeys.d.ts.map +1 -1
- package/lib/typescript/storage/hooks/useTreeDraft.d.ts +67 -0
- package/lib/typescript/storage/hooks/useTreeDraft.d.ts.map +1 -0
- package/lib/typescript/storage/index.d.ts +2 -0
- package/lib/typescript/storage/index.d.ts.map +1 -1
- package/lib/typescript/storage/sync/storageSyncAdapter.d.ts +41 -0
- package/lib/typescript/storage/sync/storageSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/storage/utils/AsyncStorageListener.d.ts +15 -0
- package/lib/typescript/storage/utils/AsyncStorageListener.d.ts.map +1 -1
- package/lib/typescript/storage/utils/MMKVInstanceRegistry.d.ts.map +1 -1
- package/lib/typescript/storage/utils/MMKVListener.d.ts +7 -0
- package/lib/typescript/storage/utils/MMKVListener.d.ts.map +1 -1
- package/lib/typescript/storage/utils/SecureStoreRegistry.d.ts +20 -0
- package/lib/typescript/storage/utils/SecureStoreRegistry.d.ts.map +1 -1
- package/lib/typescript/storage/utils/deleteStorageKeys.d.ts +27 -0
- package/lib/typescript/storage/utils/deleteStorageKeys.d.ts.map +1 -0
- package/lib/typescript/storage/utils/index.d.ts +1 -0
- package/lib/typescript/storage/utils/index.d.ts.map +1 -1
- package/lib/typescript/storage/utils/lineDiff.d.ts +7 -31
- package/lib/typescript/storage/utils/lineDiff.d.ts.map +1 -1
- package/lib/typescript/storage/utils/setStorageValue.d.ts +64 -0
- package/lib/typescript/storage/utils/setStorageValue.d.ts.map +1 -0
- package/lib/typescript/storage/utils/storageBrowserRefresh.d.ts +28 -0
- package/lib/typescript/storage/utils/storageBrowserRefresh.d.ts.map +1 -0
- package/lib/typescript/storage/utils/storageOps.d.ts +25 -0
- package/lib/typescript/storage/utils/storageOps.d.ts.map +1 -0
- package/lib/typescript/storage/utils/storageTimeTravelUtils.d.ts +15 -5
- package/lib/typescript/storage/utils/storageTimeTravelUtils.d.ts.map +1 -1
- package/lib/typescript/storage/utils/valueType.d.ts +13 -0
- package/lib/typescript/storage/utils/valueType.d.ts.map +1 -1
- package/package.json +5 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useCallback } from "react";
|
|
3
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
4
4
|
import { secureStoreRegistry } from "../utils/SecureStoreRegistry";
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -9,6 +9,23 @@ import { secureStoreRegistry } from "../utils/SecureStoreRegistry";
|
|
|
9
9
|
* auto-reads them — it just shows they exist.
|
|
10
10
|
*/
|
|
11
11
|
const BIOMETRIC_PLACEHOLDER = "•••••• (biometric protected)";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* How often to re-read registered SecureStore values.
|
|
15
|
+
*
|
|
16
|
+
* SecureStore is the one backend with no change feed at all: AsyncStorage and
|
|
17
|
+
* MMKV writes are intercepted (and MMKV has a native value-changed listener),
|
|
18
|
+
* but the keychain gives no notification, so a value written by the app while
|
|
19
|
+
* the tool is open would otherwise stay stale until the browser remounts.
|
|
20
|
+
* Polling is the only option — kept cheap by re-reading a handful of registered
|
|
21
|
+
* keys and only committing state when a value actually changed.
|
|
22
|
+
*/
|
|
23
|
+
const DEFAULT_POLL_MS = 3000;
|
|
24
|
+
/** Identity signature used to skip no-op state updates. */
|
|
25
|
+
function signature(keys) {
|
|
26
|
+
return JSON.stringify(keys);
|
|
27
|
+
}
|
|
28
|
+
|
|
12
29
|
/**
|
|
13
30
|
* Read all registered SecureStore keys into StorageKeyInfo[] for the browser.
|
|
14
31
|
*
|
|
@@ -16,22 +33,47 @@ const BIOMETRIC_PLACEHOLDER = "•••••• (biometric protected)";
|
|
|
16
33
|
* `registerSecureStoreKeys()` (see SecureStoreRegistry). Values are read with
|
|
17
34
|
* the options each key was registered with (keychainService must match), and
|
|
18
35
|
* biometric-protected keys are listed without being read.
|
|
36
|
+
*
|
|
37
|
+
* Values are re-read on a poll and whenever the registry changes, so the
|
|
38
|
+
* browser tracks writes the app makes while the tool is open.
|
|
19
39
|
*/
|
|
20
|
-
export function useSecureStoreKeys() {
|
|
40
|
+
export function useSecureStoreKeys(options = {}) {
|
|
41
|
+
const {
|
|
42
|
+
pollMs = DEFAULT_POLL_MS
|
|
43
|
+
} = options;
|
|
21
44
|
const [storageKeys, setStorageKeys] = useState([]);
|
|
22
45
|
const [isLoading, setIsLoading] = useState(true);
|
|
23
46
|
const [error, setError] = useState(null);
|
|
47
|
+
|
|
48
|
+
// Last committed content, so a poll that finds nothing new costs zero
|
|
49
|
+
// renders (this hook feeds a non-virtualized list of every storage key).
|
|
50
|
+
const signatureRef = useRef(signature([]));
|
|
51
|
+
const hasLoadedRef = useRef(false);
|
|
52
|
+
const inFlightRef = useRef(false);
|
|
53
|
+
const commit = useCallback(keys => {
|
|
54
|
+
const next = signature(keys);
|
|
55
|
+
if (next === signatureRef.current) return;
|
|
56
|
+
signatureRef.current = next;
|
|
57
|
+
setStorageKeys(keys);
|
|
58
|
+
}, []);
|
|
24
59
|
const fetchSecureStoreData = useCallback(async () => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
setStorageKeys([]);
|
|
29
|
-
setIsLoading(false);
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
setIsLoading(true);
|
|
33
|
-
setError(null);
|
|
60
|
+
// Reads are async and the poll is unconditional — never stack them.
|
|
61
|
+
if (inFlightRef.current) return;
|
|
62
|
+
inFlightRef.current = true;
|
|
34
63
|
try {
|
|
64
|
+
const module = secureStoreRegistry.getModule();
|
|
65
|
+
const entries = secureStoreRegistry.getAll();
|
|
66
|
+
if (!module || entries.length === 0) {
|
|
67
|
+
commit([]);
|
|
68
|
+
hasLoadedRef.current = true;
|
|
69
|
+
setIsLoading(false);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Only the first read is a "loading" state; polls refresh in place so the
|
|
74
|
+
// browser never flashes its spinner over data it already has.
|
|
75
|
+
if (!hasLoadedRef.current) setIsLoading(true);
|
|
76
|
+
setError(null);
|
|
35
77
|
const keys = [];
|
|
36
78
|
for (const entry of entries) {
|
|
37
79
|
if (entry.options.requireAuthentication) {
|
|
@@ -79,19 +121,32 @@ export function useSecureStoreKeys() {
|
|
|
79
121
|
})
|
|
80
122
|
});
|
|
81
123
|
}
|
|
82
|
-
|
|
124
|
+
commit(keys);
|
|
83
125
|
} catch (err) {
|
|
84
126
|
setError(err instanceof Error ? err : new Error("Failed to read SecureStore"));
|
|
85
|
-
|
|
127
|
+
commit([]);
|
|
86
128
|
} finally {
|
|
129
|
+
hasLoadedRef.current = true;
|
|
130
|
+
inFlightRef.current = false;
|
|
87
131
|
setIsLoading(false);
|
|
88
132
|
}
|
|
89
|
-
}, []);
|
|
133
|
+
}, [commit]);
|
|
90
134
|
|
|
91
|
-
// Initial fetch
|
|
135
|
+
// Initial fetch + poll. Registration usually happens at app startup, but the
|
|
136
|
+
// registry is also refilled on the dashboard every time a device syncs, so
|
|
137
|
+
// subscribe as well rather than assuming the key list is final at mount.
|
|
92
138
|
useEffect(() => {
|
|
93
139
|
fetchSecureStoreData();
|
|
94
|
-
|
|
140
|
+
const unsubscribe = secureStoreRegistry.subscribe(() => {
|
|
141
|
+
fetchSecureStoreData();
|
|
142
|
+
});
|
|
143
|
+
if (pollMs <= 0) return unsubscribe;
|
|
144
|
+
const interval = setInterval(fetchSecureStoreData, pollMs);
|
|
145
|
+
return () => {
|
|
146
|
+
unsubscribe();
|
|
147
|
+
clearInterval(interval);
|
|
148
|
+
};
|
|
149
|
+
}, [fetchSecureStoreData, pollMs]);
|
|
95
150
|
return {
|
|
96
151
|
storageKeys,
|
|
97
152
|
isLoading,
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A local draft of a storage value, edited in place and written ONCE.
|
|
5
|
+
*
|
|
6
|
+
* Applying each tree op straight to the device was the obvious first cut and it
|
|
7
|
+
* behaves badly for two compounding reasons:
|
|
8
|
+
*
|
|
9
|
+
* 1. **Every op is a round trip.** The write lands, the device emits a storage
|
|
10
|
+
* event, a snapshot comes back, and the browser re-reads every backend — so
|
|
11
|
+
* a six-tap reorder is six writes and six full refreshes. The tree rebuilds
|
|
12
|
+
* under the user mid-gesture.
|
|
13
|
+
* 2. **The highlight and the value fall out of step.** Selection is local and
|
|
14
|
+
* moves instantly; the value it points at only catches up when the round
|
|
15
|
+
* trip completes, so for a beat the highlight sits on the old contents.
|
|
16
|
+
*
|
|
17
|
+
* Optimistic updates — the React Query pattern of rendering the expected result
|
|
18
|
+
* and reconciling later — fix (2) but not (1): they still write per change.
|
|
19
|
+
* Buffering fixes both, and it's what every editor that owns a document does
|
|
20
|
+
* (a form's dirty state, an IDE's unsaved buffer, JSON Editor's in-memory doc
|
|
21
|
+
* with an explicit save).
|
|
22
|
+
*
|
|
23
|
+
* The trade is that the buffer can go stale if the app writes the same key
|
|
24
|
+
* while you're editing. That's the same last-write-wins a text editor has, and
|
|
25
|
+
* `isDirty` at least makes it visible that you're holding unsaved changes.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { useCallback, useState } from "react";
|
|
29
|
+
import { applyJsonOp, selectionPathAfterOp } from "@buoy-gg/shared-ui/dataViewer";
|
|
30
|
+
/**
|
|
31
|
+
* @param liveValue the value as storage currently holds it (already parsed)
|
|
32
|
+
* @param save persists the serialized draft; rejecting keeps edit mode open
|
|
33
|
+
*/
|
|
34
|
+
export function useTreeDraft(liveValue, save) {
|
|
35
|
+
const [draft, setDraft] = useState(undefined);
|
|
36
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
37
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
38
|
+
const [error, setError] = useState(null);
|
|
39
|
+
const [selectPath, setSelectPath] = useState();
|
|
40
|
+
/**
|
|
41
|
+
* The value as it was when editing began. Compared against for dirtiness
|
|
42
|
+
* rather than against `liveValue`, which can move underneath us if the app
|
|
43
|
+
* writes the same key — that would otherwise flip a clean draft to dirty.
|
|
44
|
+
*/
|
|
45
|
+
const [baseline, setBaseline] = useState("");
|
|
46
|
+
const value = isEditing && draft !== undefined ? draft : liveValue;
|
|
47
|
+
const isDirty = isEditing && JSON.stringify(draft) !== baseline;
|
|
48
|
+
const begin = useCallback(() => {
|
|
49
|
+
setDraft(liveValue);
|
|
50
|
+
setBaseline(JSON.stringify(liveValue));
|
|
51
|
+
setSelectPath(undefined);
|
|
52
|
+
setError(null);
|
|
53
|
+
setIsEditing(true);
|
|
54
|
+
}, [liveValue]);
|
|
55
|
+
const apply = useCallback(op => {
|
|
56
|
+
// Purely local — this is the whole point. No write, so no event, no
|
|
57
|
+
// snapshot, no refresh, and the tree re-renders from state synchronously.
|
|
58
|
+
setDraft(prev => applyJsonOp(prev, op));
|
|
59
|
+
const next = selectionPathAfterOp(op);
|
|
60
|
+
if (next !== undefined) setSelectPath(next);
|
|
61
|
+
}, []);
|
|
62
|
+
const selectAt = useCallback(path => {
|
|
63
|
+
setSelectPath(path);
|
|
64
|
+
}, []);
|
|
65
|
+
const reset = useCallback(() => {
|
|
66
|
+
setIsEditing(false);
|
|
67
|
+
setDraft(undefined);
|
|
68
|
+
setSelectPath(undefined);
|
|
69
|
+
setError(null);
|
|
70
|
+
}, []);
|
|
71
|
+
const commit = useCallback(async () => {
|
|
72
|
+
if (!isDirty || !save) {
|
|
73
|
+
reset();
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
setIsSaving(true);
|
|
77
|
+
setError(null);
|
|
78
|
+
try {
|
|
79
|
+
await save(JSON.stringify(draft));
|
|
80
|
+
reset();
|
|
81
|
+
return true;
|
|
82
|
+
} catch (e) {
|
|
83
|
+
// Stay in edit mode so the work isn't lost to a failed write.
|
|
84
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
85
|
+
return false;
|
|
86
|
+
} finally {
|
|
87
|
+
setIsSaving(false);
|
|
88
|
+
}
|
|
89
|
+
}, [draft, isDirty, reset, save]);
|
|
90
|
+
return {
|
|
91
|
+
isEditing,
|
|
92
|
+
value,
|
|
93
|
+
isDirty,
|
|
94
|
+
isSaving,
|
|
95
|
+
error,
|
|
96
|
+
selectPath,
|
|
97
|
+
begin,
|
|
98
|
+
apply,
|
|
99
|
+
selectAt,
|
|
100
|
+
commit,
|
|
101
|
+
discard: reset
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -8,6 +8,9 @@ export { StorageKeyStatsSection } from "./components/StorageKeyStats";
|
|
|
8
8
|
export { StorageKeySection } from "./components/StorageKeySection";
|
|
9
9
|
export { StorageBrowserMode } from "./components/StorageBrowserMode";
|
|
10
10
|
export { StorageEventsSection } from "./components/StorageEventsSection";
|
|
11
|
+
// The five display operations a storage event collapses onto — semantics, not
|
|
12
|
+
// presentation, so hosts can filter or group by the same vocabulary the UI uses.
|
|
13
|
+
export { STORAGE_OPS, storageOpOf, storageOpDot, isInitialScan } from "./utils/storageOps";
|
|
11
14
|
export { StorageEventCard, getValueType } from "./components/StorageEventCard";
|
|
12
15
|
export { StorageEventDetailContent, StorageEventDetailFooter } from "./components/StorageEventDetailContent";
|
|
13
16
|
|
|
@@ -98,7 +98,8 @@ class StorageEventStore extends BaseEventStore {
|
|
|
98
98
|
data: {
|
|
99
99
|
key,
|
|
100
100
|
value,
|
|
101
|
-
valueType
|
|
101
|
+
valueType,
|
|
102
|
+
initialScan: true
|
|
102
103
|
},
|
|
103
104
|
storageType: "mmkv",
|
|
104
105
|
id: nextStorageEventId()
|
|
@@ -125,7 +126,8 @@ class StorageEventStore extends BaseEventStore {
|
|
|
125
126
|
timestamp: now,
|
|
126
127
|
data: {
|
|
127
128
|
key,
|
|
128
|
-
value: rawValue ?? undefined
|
|
129
|
+
value: rawValue ?? undefined,
|
|
130
|
+
initialScan: true
|
|
129
131
|
},
|
|
130
132
|
storageType: "async",
|
|
131
133
|
id: nextStorageEventId()
|
|
@@ -5,6 +5,7 @@ import { isDevToolsStorageKey } from "@buoy-gg/shared-ui";
|
|
|
5
5
|
import { readMany, writeMany, removeMany } from "../utils/asyncStorageCompat";
|
|
6
6
|
import { storageEventStore } from "../stores/storageEventStore";
|
|
7
7
|
import { clearAllAppStorage } from "../utils/clearAllStorage";
|
|
8
|
+
import { undoOperation, jumpToState, canUndo } from "../utils/storageTimeTravelUtils";
|
|
8
9
|
import { mmkvInstanceRegistry } from "../utils/MMKVInstanceRegistry";
|
|
9
10
|
import { detectMMKVType } from "../utils/mmkvTypeDetection";
|
|
10
11
|
import { secureStoreRegistry } from "../utils/SecureStoreRegistry";
|
|
@@ -13,6 +14,28 @@ import { secureStoreRegistry } from "../utils/SecureStoreRegistry";
|
|
|
13
14
|
|
|
14
15
|
/** One MMKV instance in a `mmkv.snapshot` result. */
|
|
15
16
|
|
|
17
|
+
/** One registered key in a `secure.snapshot` result. */
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve an MMKV instance for WRITING, refusing the ones the app registered
|
|
21
|
+
* read-only.
|
|
22
|
+
*
|
|
23
|
+
* The registry has always tracked `readOnly`, but nothing enforced it: any sync
|
|
24
|
+
* client — dashboard or MCP — could write to an instance the app explicitly
|
|
25
|
+
* declared immutable. That was harmless while the tool could only read; it
|
|
26
|
+
* stops being harmless the moment there's an edit UI pointed at it.
|
|
27
|
+
*/
|
|
28
|
+
function assertMMKVWritable(instanceId) {
|
|
29
|
+
const registered = mmkvInstanceRegistry.get(instanceId);
|
|
30
|
+
if (!registered) {
|
|
31
|
+
throw new Error(`No MMKV instance registered as "${instanceId}"`);
|
|
32
|
+
}
|
|
33
|
+
if (registered.instance?.isReadOnly) {
|
|
34
|
+
throw new Error(`MMKV instance "${instanceId}" is registered read-only`);
|
|
35
|
+
}
|
|
36
|
+
return registered.instance;
|
|
37
|
+
}
|
|
38
|
+
|
|
16
39
|
/**
|
|
17
40
|
* Sync adapter for the storage tool, consumed by @buoy-gg/external-sync's
|
|
18
41
|
* `useExternalSync` (structurally matches its ToolSyncAdapter interface so
|
|
@@ -42,6 +65,58 @@ export const storageSyncAdapter = {
|
|
|
42
65
|
},
|
|
43
66
|
/** Clears all app storage keys, preserving dev tools settings. */
|
|
44
67
|
clearAppStorage: () => clearAllAppStorage(),
|
|
68
|
+
// ── Time travel (same code path as the modal's UNDO / JUMP buttons) ──
|
|
69
|
+
// The UI swallows failures silently; these THROW so a caller (desktop, MCP)
|
|
70
|
+
// gets the actual reason back instead of a no-op.
|
|
71
|
+
|
|
72
|
+
/** Undo one event, addressed by the `id` the store assigned it. */
|
|
73
|
+
"timeTravel.undo": async params => {
|
|
74
|
+
const id = params?.id;
|
|
75
|
+
if (!id) throw new Error("timeTravel.undo requires an event `id`");
|
|
76
|
+
const event = storageEventStore.getEvents().find(e => e.id === id);
|
|
77
|
+
if (!event) throw new Error(`No storage event with id "${id}"`);
|
|
78
|
+
if (event.storageType === "mmkv") {
|
|
79
|
+
throw new Error("Time travel supports AsyncStorage events only");
|
|
80
|
+
}
|
|
81
|
+
const asyncEvent = event;
|
|
82
|
+
if (!canUndo(asyncEvent)) {
|
|
83
|
+
throw new Error(`Event "${id}" (${asyncEvent.action}) has no previous value captured, so it cannot be undone`);
|
|
84
|
+
}
|
|
85
|
+
await undoOperation(asyncEvent);
|
|
86
|
+
return {
|
|
87
|
+
undone: true,
|
|
88
|
+
action: asyncEvent.action,
|
|
89
|
+
key: asyncEvent.data?.key
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
/**
|
|
93
|
+
* Jump storage back to the state as of one event. `scope: "key"` (default)
|
|
94
|
+
* replays only that key's history, matching the modal's JUMP button;
|
|
95
|
+
* `scope: "all"` replays every captured AsyncStorage event.
|
|
96
|
+
*/
|
|
97
|
+
"timeTravel.jump": async params => {
|
|
98
|
+
const {
|
|
99
|
+
id,
|
|
100
|
+
scope = "key"
|
|
101
|
+
} = params ?? {};
|
|
102
|
+
if (!id) throw new Error("timeTravel.jump requires an event `id`");
|
|
103
|
+
|
|
104
|
+
// Oldest-first, which is the order jumpToState replays in.
|
|
105
|
+
const all = storageEventStore.getEvents().filter(e => e.storageType !== "mmkv").slice().reverse();
|
|
106
|
+
const target = all.find(e => e.id === id);
|
|
107
|
+
if (!target) throw new Error(`No AsyncStorage event with id "${id}"`);
|
|
108
|
+
const timeline = scope === "all" ? all : all.filter(e => e.data?.key === target.data?.key);
|
|
109
|
+
const index = timeline.findIndex(e => e.id === id);
|
|
110
|
+
if (index === -1) throw new Error(`Event "${id}" is not in the timeline`);
|
|
111
|
+
await jumpToState(timeline, index);
|
|
112
|
+
return {
|
|
113
|
+
jumped: true,
|
|
114
|
+
scope,
|
|
115
|
+
replayed: index + 1,
|
|
116
|
+
of: timeline.length,
|
|
117
|
+
key: target.data?.key
|
|
118
|
+
};
|
|
119
|
+
},
|
|
45
120
|
// ── Remote AsyncStorage proxy (desktop browse/edit mode) ──
|
|
46
121
|
// Single-item methods share the same signature across async-storage v2/v3;
|
|
47
122
|
// batch methods go through the compat helpers (which translate to v3's
|
|
@@ -104,16 +179,16 @@ export const storageSyncAdapter = {
|
|
|
104
179
|
key,
|
|
105
180
|
value
|
|
106
181
|
} = params;
|
|
107
|
-
|
|
182
|
+
assertMMKVWritable(instanceId).set(key, value);
|
|
108
183
|
},
|
|
109
184
|
"mmkv.remove": params => {
|
|
110
185
|
const {
|
|
111
186
|
instanceId,
|
|
112
187
|
key
|
|
113
188
|
} = params;
|
|
114
|
-
const instance =
|
|
189
|
+
const instance = assertMMKVWritable(instanceId);
|
|
115
190
|
// v4 renamed delete() to remove() — support both
|
|
116
|
-
if (instance
|
|
191
|
+
if (instance.remove) instance.remove(key);else instance.delete?.(key);
|
|
117
192
|
},
|
|
118
193
|
// ── Remote SecureStore proxy ──
|
|
119
194
|
// Only registered keys are known (SecureStore has no enumeration API).
|
|
@@ -128,6 +203,40 @@ export const storageSyncAdapter = {
|
|
|
128
203
|
keychainService: options.keychainService,
|
|
129
204
|
requireAuthentication: options.requireAuthentication ?? false
|
|
130
205
|
})),
|
|
206
|
+
/**
|
|
207
|
+
* Every registered key WITH its value, in one round trip — the SecureStore
|
|
208
|
+
* counterpart to `mmkv.snapshot`. The dashboard polls this (the keychain
|
|
209
|
+
* has no change feed) and serves the browser from the result, instead of
|
|
210
|
+
* firing one `secure.get` per key per refresh.
|
|
211
|
+
*/
|
|
212
|
+
"secure.snapshot": async () => {
|
|
213
|
+
const module = secureStoreRegistry.getModule();
|
|
214
|
+
const entries = secureStoreRegistry.getAll();
|
|
215
|
+
const result = [];
|
|
216
|
+
for (const {
|
|
217
|
+
key,
|
|
218
|
+
options,
|
|
219
|
+
description
|
|
220
|
+
} of entries) {
|
|
221
|
+
let value = null;
|
|
222
|
+
// Biometric-protected keys are never read — that would fire an auth prompt.
|
|
223
|
+
if (module && !options.requireAuthentication) {
|
|
224
|
+
try {
|
|
225
|
+
value = await module.getItemAsync(key, options);
|
|
226
|
+
} catch {
|
|
227
|
+
// Unreadable (e.g. keychain state changed) — report as unset
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
result.push({
|
|
231
|
+
key,
|
|
232
|
+
description,
|
|
233
|
+
keychainService: options.keychainService,
|
|
234
|
+
requireAuthentication: options.requireAuthentication ?? false,
|
|
235
|
+
value
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return result;
|
|
239
|
+
},
|
|
131
240
|
"secure.get": params => {
|
|
132
241
|
const {
|
|
133
242
|
key
|
|
@@ -139,6 +248,19 @@ export const storageSyncAdapter = {
|
|
|
139
248
|
}
|
|
140
249
|
return module.getItemAsync(key, entry.options);
|
|
141
250
|
},
|
|
251
|
+
/**
|
|
252
|
+
* Write a registered SecureStore key. Goes through the registry so the
|
|
253
|
+
* value is written with the SAME options it was registered with — writing
|
|
254
|
+
* with different `keychainService` options creates a second entry the app
|
|
255
|
+
* can't read. Biometric-protected keys throw rather than prompting.
|
|
256
|
+
*/
|
|
257
|
+
"secure.set": params => {
|
|
258
|
+
const {
|
|
259
|
+
key,
|
|
260
|
+
value
|
|
261
|
+
} = params;
|
|
262
|
+
return secureStoreRegistry.setValue(key, value);
|
|
263
|
+
},
|
|
142
264
|
"secure.delete": params => secureStoreRegistry.deleteValue(params.key)
|
|
143
265
|
}
|
|
144
266
|
};
|
|
@@ -6,6 +6,28 @@ import { asyncStorage as AsyncStorage, asyncStorageCaps, readMany } from "./asyn
|
|
|
6
6
|
|
|
7
7
|
// v3-only batch methods (replace multiSet/multiRemove)
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* The pristine (un-swizzled) AsyncStorage methods, stashed on globalThis under a
|
|
11
|
+
* cross-realm Symbol.
|
|
12
|
+
*
|
|
13
|
+
* Why this exists: the listener below is a module-scope singleton. Metro Fast
|
|
14
|
+
* Refresh re-evaluates the module and builds a NEW instance whose `original*`
|
|
15
|
+
* fields are all null — but `AsyncStorage.setItem` still points at the OLD
|
|
16
|
+
* instance's `swizzled_setItem`. Without a stash, `initialize()` sees the
|
|
17
|
+
* existing swizzle, has no way to recover the real methods, and bails; the
|
|
18
|
+
* listener then never attaches, so storage events stop for the rest of the
|
|
19
|
+
* session (writes still work, they just go unrecorded). Keeping the originals
|
|
20
|
+
* off the instance lets a fresh instance re-attach cleanly.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const ORIGINALS_KEY = Symbol.for("@buoy-gg/storage.asyncStorageOriginals");
|
|
24
|
+
function readStashedOriginals() {
|
|
25
|
+
return globalThis[ORIGINALS_KEY] ?? null;
|
|
26
|
+
}
|
|
27
|
+
function stashOriginals(originals) {
|
|
28
|
+
globalThis[ORIGINALS_KEY] = originals;
|
|
29
|
+
}
|
|
30
|
+
|
|
9
31
|
// Event types for AsyncStorage operations
|
|
10
32
|
|
|
11
33
|
/**
|
|
@@ -90,6 +112,25 @@ class AsyncStorageListener {
|
|
|
90
112
|
return false;
|
|
91
113
|
}
|
|
92
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Copy a set of pristine methods onto this instance's `original*` fields.
|
|
117
|
+
*
|
|
118
|
+
* Used both when capturing them for the first time and when adopting the
|
|
119
|
+
* global stash after a Fast Refresh, so `restoreOriginalMethods()` and the
|
|
120
|
+
* swizzles always have real methods to call through to.
|
|
121
|
+
*/
|
|
122
|
+
adoptOriginals(originals) {
|
|
123
|
+
this.originalSetItem = originals.setItem;
|
|
124
|
+
this.originalRemoveItem = originals.removeItem;
|
|
125
|
+
this.originalClear = originals.clear;
|
|
126
|
+
this.originalMergeItem = originals.mergeItem;
|
|
127
|
+
this.originalMultiSet = originals.multiSet;
|
|
128
|
+
this.originalMultiRemove = originals.multiRemove;
|
|
129
|
+
this.originalMultiMerge = originals.multiMerge;
|
|
130
|
+
this.originalSetMany = originals.setMany;
|
|
131
|
+
this.originalRemoveMany = originals.removeMany;
|
|
132
|
+
}
|
|
133
|
+
|
|
93
134
|
/**
|
|
94
135
|
* Initialize the listener by loading AsyncStorage and storing original methods
|
|
95
136
|
*
|
|
@@ -106,29 +147,52 @@ class AsyncStorageListener {
|
|
|
106
147
|
return true;
|
|
107
148
|
}
|
|
108
149
|
|
|
109
|
-
//
|
|
150
|
+
// Already swizzled — by a previous instance of this class that Fast Refresh
|
|
151
|
+
// replaced, or by a duplicate copy of the package. Adopt the stashed
|
|
152
|
+
// originals so this instance can restore + re-swizzle. Bailing here is what
|
|
153
|
+
// used to kill storage events until a full app reload.
|
|
110
154
|
if (AsyncStorage.setItem.name === "swizzled_setItem") {
|
|
111
|
-
|
|
112
|
-
|
|
155
|
+
const stashed = readStashedOriginals();
|
|
156
|
+
if (!stashed) {
|
|
157
|
+
// Swizzled by something we didn't install and whose originals we never
|
|
158
|
+
// captured — taking the current methods as "originals" would bake the
|
|
159
|
+
// foreign swizzle in permanently, so stay out of the way.
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
this.adoptOriginals(stashed);
|
|
163
|
+
this.isInitialized = true;
|
|
164
|
+
return true;
|
|
113
165
|
}
|
|
114
166
|
|
|
115
167
|
// Store original methods (these should be the real AsyncStorage methods).
|
|
116
168
|
// Single-item + clear are identical across v2/v3.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
169
|
+
const originals = {
|
|
170
|
+
setItem: AsyncStorage.setItem.bind(AsyncStorage),
|
|
171
|
+
removeItem: AsyncStorage.removeItem.bind(AsyncStorage),
|
|
172
|
+
clear: AsyncStorage.clear.bind(AsyncStorage),
|
|
173
|
+
mergeItem: null,
|
|
174
|
+
multiSet: null,
|
|
175
|
+
multiRemove: null,
|
|
176
|
+
multiMerge: null,
|
|
177
|
+
setMany: null,
|
|
178
|
+
removeMany: null
|
|
179
|
+
};
|
|
120
180
|
if (asyncStorageCaps.hasLegacyMultiApi) {
|
|
121
181
|
// v2: multiSet/multiRemove + merge API
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
182
|
+
originals.multiSet = AsyncStorage.multiSet.bind(AsyncStorage);
|
|
183
|
+
originals.multiRemove = AsyncStorage.multiRemove.bind(AsyncStorage);
|
|
184
|
+
originals.mergeItem = AsyncStorage.mergeItem.bind(AsyncStorage);
|
|
185
|
+
originals.multiMerge = AsyncStorage.multiMerge ? AsyncStorage.multiMerge.bind(AsyncStorage) : null;
|
|
126
186
|
} else {
|
|
127
187
|
// v3: setMany/removeMany; merge API was removed (nothing to capture)
|
|
128
|
-
|
|
129
|
-
|
|
188
|
+
originals.setMany = AsyncStorage.setMany.bind(AsyncStorage);
|
|
189
|
+
originals.removeMany = AsyncStorage.removeMany.bind(AsyncStorage);
|
|
130
190
|
}
|
|
131
191
|
|
|
192
|
+
// Keep them reachable across a module re-evaluation, then adopt.
|
|
193
|
+
stashOriginals(originals);
|
|
194
|
+
this.adoptOriginals(originals);
|
|
195
|
+
|
|
132
196
|
// Original methods stored successfully
|
|
133
197
|
this.isInitialized = true;
|
|
134
198
|
return true;
|
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
* @since 1.0.0
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
// Static ESM import. This was previously a dynamic `require('./MMKVListener')`
|
|
14
|
+
// inside register/unregister "to avoid a circular dependency" — but MMKVListener
|
|
15
|
+
// imports only ./mmkvTypeDetection, never this file, so there is no cycle. The
|
|
16
|
+
// require() form threw `ReferenceError: require is not defined` when this source
|
|
17
|
+
// is bundled as ESM (the desktop dashboard imports it through Vite), crashing on
|
|
18
|
+
// StorageInline unmount. A static import behaves identically on Metro and Vite.
|
|
19
|
+
import { mmkvListener } from "./MMKVListener";
|
|
20
|
+
|
|
13
21
|
/**
|
|
14
22
|
* Type-safe representation of an MMKV instance.
|
|
15
23
|
* Uses conditional typing to avoid hard dependency on react-native-mmkv.
|
|
@@ -161,10 +169,6 @@ export function registerMMKVInstance(id, instance, config) {
|
|
|
161
169
|
|
|
162
170
|
// Add listener for real-time monitoring (CRITICAL: enables event emission)
|
|
163
171
|
// v4: Use ONLY native listener (addOnValueChangedListener), skip method wrapping
|
|
164
|
-
// Import mmkvListener dynamically to avoid circular dependency
|
|
165
|
-
const {
|
|
166
|
-
mmkvListener
|
|
167
|
-
} = require('./MMKVListener');
|
|
168
172
|
mmkvListener.addInstance(instance, id);
|
|
169
173
|
}
|
|
170
174
|
|
|
@@ -186,9 +190,6 @@ export function registerMMKVInstance(id, instance, config) {
|
|
|
186
190
|
*/
|
|
187
191
|
export function unregisterMMKVInstance(id) {
|
|
188
192
|
// Remove listener first
|
|
189
|
-
const {
|
|
190
|
-
mmkvListener
|
|
191
|
-
} = require('./MMKVListener');
|
|
192
193
|
mmkvListener.removeInstance(id);
|
|
193
194
|
|
|
194
195
|
// Then remove from registry
|