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