@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
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.setStorageValue = setStorageValue;
7
+ exports.storageEditBlockedReason = storageEditBlockedReason;
8
+ exports.storageValueToEditableText = storageValueToEditableText;
9
+ exports.structuredStorageValue = structuredStorageValue;
10
+ exports.validateStorageValue = validateStorageValue;
11
+ var _asyncStorage = _interopRequireDefault(require("@react-native-async-storage/async-storage"));
12
+ var _SecureStoreRegistry = require("./SecureStoreRegistry");
13
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
14
+ /**
15
+ * Writing a storage value back, in one place.
16
+ *
17
+ * The counterpart to `deleteStorageKeys` — and the same reasoning applies: each
18
+ * backend writes differently, and a second copy of that fan-out is how one
19
+ * backend quietly starts corrupting values.
20
+ *
21
+ * The hard part is NOT the write, it's the types:
22
+ *
23
+ * - **AsyncStorage** stores strings only. A value that was stored as JSON must
24
+ * stay valid JSON, or an object silently becomes a quoted string and the app
25
+ * that reads it breaks in a way that looks nothing like a devtools edit.
26
+ * - **MMKV** is natively typed (string / number / boolean / buffer). Writing
27
+ * `"42"` over a number turns it into a string, so the original type is
28
+ * restored from `valueType`. Buffers can't round-trip at all — the snapshot
29
+ * only ever carried a `<ArrayBuffer n bytes>` placeholder.
30
+ * - **SecureStore** stores strings, and biometric-protected keys are never
31
+ * touched (writing needs the same auth prompt reading does).
32
+ *
33
+ * Pro gating and confirmation are deliberately NOT here; they belong to the UI
34
+ * that has the user's attention. This assumes the caller already asked.
35
+ */
36
+
37
+ /**
38
+ * Why a key can't be edited, or null when it can. UI uses this both to hide the
39
+ * edit affordance and to explain itself, rather than failing at save time.
40
+ */
41
+ function storageEditBlockedReason(storageKey, mmkvInstances = []) {
42
+ if (storageKey.storageType === "mmkv") {
43
+ if (storageKey.valueType === "buffer") {
44
+ return "Buffer values can't be edited — the tool only ever sees their byte length, not their contents.";
45
+ }
46
+ const instance = mmkvInstances.find(i => i.id === (storageKey.instanceId || "default"));
47
+ if (instance?.readOnly) {
48
+ return `MMKV instance "${instance.id}" is registered read-only.`;
49
+ }
50
+ }
51
+ if (storageKey.storageType === "secure") {
52
+ const entry = _SecureStoreRegistry.secureStoreRegistry.getAll().find(e => e.key === storageKey.key);
53
+ if (entry?.options.requireAuthentication) {
54
+ return "Biometric-protected keys can't be edited — writing one would fire an auth prompt.";
55
+ }
56
+ if (!_SecureStoreRegistry.secureStoreRegistry.getModule()) {
57
+ return "No SecureStore module is registered.";
58
+ }
59
+ }
60
+ return null;
61
+ }
62
+
63
+ /**
64
+ * Validate `raw` for this key WITHOUT writing. Returns an error message, or
65
+ * null when the text is acceptable. The editor calls this as you type.
66
+ */
67
+ function validateStorageValue(storageKey, raw) {
68
+ if (storageKey.storageType === "mmkv") {
69
+ const type = mmkvTargetType(storageKey);
70
+ if (type === "number") {
71
+ if (raw.trim() === "" || Number.isNaN(Number(raw))) {
72
+ return "Must be a number — this MMKV key is stored as a number.";
73
+ }
74
+ }
75
+ if (type === "boolean") {
76
+ const t = raw.trim().toLowerCase();
77
+ if (t !== "true" && t !== "false") {
78
+ return 'Must be "true" or "false" — this MMKV key is stored as a boolean.';
79
+ }
80
+ }
81
+ return null;
82
+ }
83
+
84
+ // AsyncStorage + SecureStore are string-valued. Only guard the case where
85
+ // the CURRENT value is structured: silently flattening an object into a
86
+ // string is the corruption worth preventing. A plain string stays free-form.
87
+ if (isStructured(storageKey.value)) {
88
+ try {
89
+ JSON.parse(raw);
90
+ } catch (e) {
91
+ return `This key holds JSON — ${e instanceof Error ? e.message : "invalid JSON"}`;
92
+ }
93
+ }
94
+ return null;
95
+ }
96
+
97
+ /**
98
+ * The text an editor should open with: JSON pretty-printed so it's actually
99
+ * editable, everything else verbatim.
100
+ */
101
+ function storageValueToEditableText(value) {
102
+ if (value === undefined || value === null) return "";
103
+ if (typeof value === "string") {
104
+ // Values arrive already parsed when they were JSON, so a string here is a
105
+ // real string — except AsyncStorage hands back raw JSON text sometimes.
106
+ const trimmed = value.trim();
107
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
108
+ try {
109
+ return JSON.stringify(JSON.parse(value), null, 2);
110
+ } catch {
111
+ return value;
112
+ }
113
+ }
114
+ return value;
115
+ }
116
+ if (typeof value === "object") return JSON.stringify(value, null, 2);
117
+ return String(value);
118
+ }
119
+
120
+ /**
121
+ * The value as a STRUCTURE, for the tree editor to apply ops against.
122
+ *
123
+ * Backends disagree on how far they've already parsed: `useAsyncStorageKeys`
124
+ * hands back a parsed object, while MMKV reports its native type so a JSON blob
125
+ * arrives as the raw string. Both have to end up as the same thing before an op
126
+ * can address a path inside them.
127
+ *
128
+ * Returns `undefined` when the value isn't a container — scalars are edited
129
+ * through the plain value editor, not the tree.
130
+ */
131
+ function structuredStorageValue(storageKey) {
132
+ const {
133
+ value
134
+ } = storageKey;
135
+ if (value !== null && typeof value === "object") return value;
136
+ if (typeof value === "string") {
137
+ const trimmed = value.trim();
138
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return undefined;
139
+ try {
140
+ const parsed = JSON.parse(value);
141
+ return parsed !== null && typeof parsed === "object" ? parsed : undefined;
142
+ } catch {
143
+ return undefined;
144
+ }
145
+ }
146
+ return undefined;
147
+ }
148
+
149
+ /**
150
+ * Write `raw` to whichever backend holds this key, restoring the value's
151
+ * original type. Rejects (rather than writing something lossy) when the key
152
+ * isn't editable or the text doesn't fit the type — callers surface that.
153
+ */
154
+ async function setStorageValue(storageKey, raw, mmkvInstances = []) {
155
+ const blocked = storageEditBlockedReason(storageKey, mmkvInstances);
156
+ if (blocked) throw new Error(blocked);
157
+ const invalid = validateStorageValue(storageKey, raw);
158
+ if (invalid) throw new Error(invalid);
159
+ switch (storageKey.storageType) {
160
+ case "async":
161
+ {
162
+ await _asyncStorage.default.setItem(storageKey.key, normalizeJsonText(storageKey, raw));
163
+ return;
164
+ }
165
+ case "secure":
166
+ {
167
+ await _SecureStoreRegistry.secureStoreRegistry.setValue(storageKey.key, normalizeJsonText(storageKey, raw));
168
+ return;
169
+ }
170
+ case "mmkv":
171
+ {
172
+ const instanceId = storageKey.instanceId || "default";
173
+ const target = mmkvInstances.find(i => i.id === instanceId);
174
+ if (!target) {
175
+ throw new Error(`MMKV instance "${instanceId}" is not registered.`);
176
+ }
177
+ target.instance.set(storageKey.key, coerceMMKV(storageKey, raw));
178
+ return;
179
+ }
180
+ default:
181
+ {
182
+ throw new Error(`Unsupported storage type "${String(storageKey.storageType)}"`);
183
+ }
184
+ }
185
+ }
186
+
187
+ /** True when the value is an object/array (so its text must stay valid JSON). */
188
+ function isStructured(value) {
189
+ return value !== null && typeof value === "object";
190
+ }
191
+
192
+ /**
193
+ * Re-serialize JSON so the stored string is compact and canonical, matching
194
+ * what the app itself would have written with `JSON.stringify`. Free-form
195
+ * strings pass through untouched.
196
+ */
197
+ function normalizeJsonText(storageKey, raw) {
198
+ if (!isStructured(storageKey.value)) return raw;
199
+ return JSON.stringify(JSON.parse(raw));
200
+ }
201
+
202
+ /** The MMKV native type to write back, defaulting to the current JS type. */
203
+ function mmkvTargetType(storageKey) {
204
+ if (storageKey.valueType) return storageKey.valueType;
205
+ if (typeof storageKey.value === "number") return "number";
206
+ if (typeof storageKey.value === "boolean") return "boolean";
207
+ return "string";
208
+ }
209
+ function coerceMMKV(storageKey, raw) {
210
+ switch (mmkvTargetType(storageKey)) {
211
+ case "number":
212
+ return Number(raw);
213
+ case "boolean":
214
+ return raw.trim().toLowerCase() === "true";
215
+ default:
216
+ return raw;
217
+ }
218
+ }
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.onStorageBrowserRefresh = onStorageBrowserRefresh;
7
+ exports.requestStorageBrowserRefresh = requestStorageBrowserRefresh;
8
+ /**
9
+ * A "the underlying storage changed, re-read it" signal for the storage
10
+ * browser.
11
+ *
12
+ * On device the browser refreshes itself from the AsyncStorage/MMKV listeners,
13
+ * but those never fire in remote-mirror mode (the desktop dashboard suppresses
14
+ * local capture and receives the device's events as snapshots instead). Without
15
+ * this seam the browse tab is a one-shot read at mount — values stay frozen
16
+ * until the panel remounts, which is exactly the "shows old data until I switch
17
+ * tools and come back" behaviour.
18
+ *
19
+ * The dashboard calls {@link requestStorageBrowserRefresh} whenever a fresh
20
+ * device snapshot lands; the browser re-reads all three backends (debounced).
21
+ * It is a no-op when nothing is listening.
22
+ */
23
+
24
+ const listeners = new Set();
25
+
26
+ /**
27
+ * Subscribe to external refresh requests. Returns an unsubscribe function.
28
+ * @internal — used by the storage browser.
29
+ */
30
+ function onStorageBrowserRefresh(listener) {
31
+ listeners.add(listener);
32
+ return () => {
33
+ listeners.delete(listener);
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Ask any mounted storage browser to re-read AsyncStorage, MMKV and
39
+ * SecureStore. Safe to call at any rate — the browser debounces.
40
+ */
41
+ function requestStorageBrowserRefresh() {
42
+ listeners.forEach(listener => {
43
+ try {
44
+ listener();
45
+ } catch {
46
+ // Listener errors are not ours
47
+ }
48
+ });
49
+ }
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.STORAGE_OPS = void 0;
7
+ exports.isInitialScan = isInitialScan;
8
+ exports.storageOpDot = storageOpDot;
9
+ exports.storageOpOf = storageOpOf;
10
+ var _sharedUi = require("@buoy-gg/shared-ui");
11
+ /**
12
+ * The five operations a storage event can be, and how to recognise one.
13
+ *
14
+ * AsyncStorage and MMKV each have their own action union, and neither maps
15
+ * cleanly onto what a reader wants to see — `multiRemove`, `removeItem` and
16
+ * `delete` are one idea wearing three names. These collapse both onto a single
17
+ * vocabulary so every surface labels, filters and colours events the same way.
18
+ *
19
+ * This is semantics, not presentation: it lives in `utils` rather than beside
20
+ * any one view, so a view can be deleted without taking the vocabulary with it.
21
+ */
22
+
23
+ const STORAGE_OPS = exports.STORAGE_OPS = ["SET", "GET", "MERGE", "DELETE", "CLEAR"];
24
+ const OP_DOT = {
25
+ SET: _sharedUi.macOSColors.semantic.info,
26
+ GET: _sharedUi.macOSColors.text.muted,
27
+ MERGE: _sharedUi.macOSColors.semantic.debug,
28
+ DELETE: _sharedUi.macOSColors.semantic.error,
29
+ CLEAR: _sharedUi.macOSColors.semantic.warning
30
+ };
31
+ function storageOpDot(op) {
32
+ return OP_DOT[op];
33
+ }
34
+
35
+ /** Both backends' action unions, collapsed onto the five display operations. */
36
+ function storageOpOf(event) {
37
+ const action = event.action;
38
+ if (action === "clear" || action === "clearAll") return "CLEAR";
39
+ if (action.startsWith("get.")) return "GET";
40
+ if (action === "removeItem" || action === "multiRemove" || action === "delete") return "DELETE";
41
+ if (action === "mergeItem" || action === "multiMerge") return "MERGE";
42
+ return "SET";
43
+ }
44
+
45
+ /**
46
+ * The store synthesises one event per existing key when it first subscribes, so
47
+ * the list isn't empty on open. Those are a SNAPSHOT, not a write: the key
48
+ * already held that value, so there is no "before" to diff against and showing
49
+ * one would invent a change that never happened.
50
+ */
51
+ function isInitialScan(event) {
52
+ return Boolean(event.data?.initialScan);
53
+ }
@@ -6,9 +6,9 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.canUndo = canUndo;
7
7
  exports.jumpToState = jumpToState;
8
8
  exports.undoOperation = undoOperation;
9
+ var _sharedUi = require("@buoy-gg/shared-ui");
9
10
  var _asyncStorage = _interopRequireDefault(require("@react-native-async-storage/async-storage"));
10
11
  var _asyncStorageCompat = require("./asyncStorageCompat");
11
- var _AsyncStorageListener = require("./AsyncStorageListener");
12
12
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
13
13
  /**
14
14
  * Storage Time-Travel Utilities
@@ -20,7 +20,13 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
20
20
  * Undo a single storage operation
21
21
  *
22
22
  * Restores the previous value(s) before the operation occurred.
23
- * This pauses event capture to avoid creating new events during restoration.
23
+ *
24
+ * The restore is deliberately NOT capture-paused. Pausing it meant the write
25
+ * landed but emitted no event, so the modal's event list and current-value
26
+ * panel never changed — UNDO looked like a dead button even though it had
27
+ * worked. Letting the restore emit normally makes it visible and keeps the
28
+ * history honest about what the storage actually did. There is no feedback
29
+ * loop: a write emits an event, and an event never triggers a write.
24
30
  *
25
31
  * @param event - The storage event to undo
26
32
  */
@@ -29,10 +35,7 @@ async function undoOperation(event) {
29
35
  action,
30
36
  data
31
37
  } = event;
32
-
33
- // Pause capture to avoid creating events during restoration
34
- (0, _AsyncStorageListener.pauseCapture)();
35
- try {
38
+ {
36
39
  switch (action) {
37
40
  case "setItem":
38
41
  case "mergeItem":
@@ -87,20 +90,21 @@ async function undoOperation(event) {
87
90
  default:
88
91
  throw new Error(`Cannot undo unknown action type: ${action}`);
89
92
  }
90
- } finally {
91
- // Always resume capture, even if an error occurred
92
- (0, _AsyncStorageListener.resumeCapture)();
93
93
  }
94
94
  }
95
95
 
96
96
  /**
97
97
  * Jump to a specific point in storage history
98
98
  *
99
- * This reconstructs the storage state as it was after a specific event
100
- * by clearing storage and replaying all events up to that point.
99
+ * Reconstructs the state as of `targetEventIndex` by replaying `events` up to
100
+ * that point, then reconciling ONLY the keys that timeline governs.
101
101
  *
102
- * Note: This is a destructive operation - it will clear current storage
103
- * and replay events to reach the target state.
102
+ * This used to begin with `AsyncStorage.clear()`, which was badly out of
103
+ * proportion: the modal's JUMP button passes one key's history, so clearing
104
+ * wiped every unrelated app key — and Buoy's own `@react_buoy*` settings with
105
+ * them — to restore a single value. Now a jump touches exactly the keys that
106
+ * appear in the events it was given, and never Buoy's own keys. Like
107
+ * [undoOperation], the writes emit normally so the change is visible.
104
108
  *
105
109
  * @param events - All events in chronological order (oldest first)
106
110
  * @param targetEventIndex - The index of the event to jump to (0-based)
@@ -110,34 +114,52 @@ async function jumpToState(events, targetEventIndex) {
110
114
  throw new Error(`Invalid event index: ${targetEventIndex}`);
111
115
  }
112
116
 
113
- // Pause capture to avoid creating events during replay
114
- (0, _AsyncStorageListener.pauseCapture)();
115
- try {
116
- // Clear all storage first
117
- await _asyncStorage.default.clear();
118
-
119
- // Build up the state by applying each event up to and including the target
120
- // We use the final values from each event, not the operations themselves
121
- const stateMap = new Map();
122
- for (let i = 0; i <= targetEventIndex; i++) {
123
- const event = events[i];
124
- applyEventToStateMap(event, stateMap);
125
- }
117
+ // Build up the state by applying each event up to and including the target.
118
+ // We use the final values from each event, not the operations themselves.
119
+ const stateMap = new Map();
120
+ for (let i = 0; i <= targetEventIndex; i++) {
121
+ applyEventToStateMap(events[i], stateMap);
122
+ }
126
123
 
127
- // Apply the final state
128
- const pairsToSet = [];
129
- for (const [key, value] of stateMap) {
130
- if (value !== null) {
131
- pairsToSet.push([key, value]);
132
- }
133
- }
134
- if (pairsToSet.length > 0) {
135
- await (0, _asyncStorageCompat.writeMany)(pairsToSet);
124
+ // Every key the WHOLE timeline touches — including events after the target,
125
+ // so a key created later is removed rather than left behind.
126
+ const governedKeys = new Set();
127
+ for (const event of events) {
128
+ collectEventKeys(event, governedKeys);
129
+ }
130
+ const pairsToSet = [];
131
+ const keysToRemove = [];
132
+ for (const key of governedKeys) {
133
+ // Buoy's own settings are never app state to travel through.
134
+ if ((0, _sharedUi.isDevToolsStorageKey)(key)) continue;
135
+ const value = stateMap.get(key);
136
+ if (value === undefined || value === null) {
137
+ keysToRemove.push(key);
138
+ } else {
139
+ pairsToSet.push([key, value]);
136
140
  }
137
- } finally {
138
- // Always resume capture
139
- (0, _AsyncStorageListener.resumeCapture)();
140
141
  }
142
+ if (keysToRemove.length > 0) {
143
+ await (0, _asyncStorageCompat.removeMany)(keysToRemove);
144
+ }
145
+ if (pairsToSet.length > 0) {
146
+ await (0, _asyncStorageCompat.writeMany)(pairsToSet);
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Collect every key an event refers to, across all action shapes — including
152
+ * `prevPairs`, so keys a `clear` removed are still governed by the timeline.
153
+ */
154
+ function collectEventKeys(event, into) {
155
+ const {
156
+ data
157
+ } = event;
158
+ if (!data) return;
159
+ if (data.key) into.add(data.key);
160
+ if (data.keys) for (const key of data.keys) into.add(key);
161
+ if (data.pairs) for (const [key] of data.pairs) into.add(key);
162
+ if (data.prevPairs) for (const [key] of data.prevPairs) into.add(key);
141
163
  }
142
164
 
143
165
  /**
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.getValuePreview = getValuePreview;
7
7
  exports.getValueTypeLabel = getValueTypeLabel;
8
8
  exports.getValueTypeWithPreview = getValueTypeWithPreview;
9
+ exports.parseStorageValue = parseStorageValue;
9
10
  /** Return a concise string describing the JavaScript type of the provided value. */
10
11
  function getValueTypeLabel(value) {
11
12
  if (value === null) return "null";
@@ -19,6 +20,29 @@ function getValueTypeLabel(value) {
19
20
  return "unknown";
20
21
  }
21
22
 
23
+ /**
24
+ * A stored value as a tree can show it.
25
+ *
26
+ * AsyncStorage and SecureStore hold strings, so an object arrives as the text
27
+ * `{"a":1}` — displaying that as a string is technically true and useless. Any
28
+ * surface that renders or edits a value structurally has to do this same
29
+ * un-stringify first, so it lives here rather than being re-derived (slightly
30
+ * differently) in each one.
31
+ *
32
+ * A string that doesn't parse comes back untouched: it was just a string that
33
+ * happened to start with a brace.
34
+ */
35
+ function parseStorageValue(value) {
36
+ if (typeof value !== "string") return value;
37
+ const trimmed = value.trim();
38
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return value;
39
+ try {
40
+ return JSON.parse(value);
41
+ } catch {
42
+ return value;
43
+ }
44
+ }
45
+
22
46
  /** Longest value preview rendered inline next to the type label. */
23
47
  const MAX_INLINE_PREVIEW = 40;
24
48
 
@@ -20,12 +20,19 @@ export { storageToolPreset, createStorageTool } from "./preset";
20
20
  // COMPONENTS (For custom UI implementations)
21
21
  // =============================================================================
22
22
  export { StorageSection } from "./storage/components/StorageSection";
23
- export { StorageModalWithTabs } from "./storage/components/StorageModalWithTabs";
23
+ // `StorageToolView` is the same tool rendered inline (header bar + body +
24
+ // footer) instead of inside a JsModal — for hosts that show tools in a panel.
25
+ export { StorageModalWithTabs, StorageToolView } from "./storage/components/StorageModalWithTabs";
24
26
  export { StorageKeyCard } from "./storage/components/StorageKeyCard";
25
27
  export { StorageKeyStatsSection } from "./storage/components/StorageKeyStats";
26
28
  export { StorageKeySection } from "./storage/components/StorageKeySection";
27
29
  export { StorageBrowserMode } from "./storage/components/StorageBrowserMode";
30
+ // The backend filter, shared by the Keys and Events tabs.
31
+ export { StorageTypeFilterBar, countByStorageType } from "./storage/components/StorageTypeFilterBar";
28
32
  export { StorageEventsSection } from "./storage/components/StorageEventsSection";
33
+ // The five display operations a storage event collapses onto — semantics, not
34
+ // presentation, so hosts can filter or group by the same vocabulary the UI uses.
35
+ export { STORAGE_OPS, storageOpOf, storageOpDot, isInitialScan } from "./storage/utils/storageOps";
29
36
  export { StorageEventCard, getValueType } from "./storage/components/StorageEventCard";
30
37
  export { StorageEventDetailContent, StorageEventDetailFooter } from "./storage/components/StorageEventDetailContent";
31
38
  export { MMKVInstanceSelector } from "./storage/components/MMKVInstanceSelector";
@@ -47,6 +54,9 @@ export { useStorageEvents } from "./storage/hooks/useStorageEvents";
47
54
  // UTILITIES (Public helpers only)
48
55
  // =============================================================================
49
56
  export { clearAllAppStorage } from "./storage/utils/clearAllStorage";
57
+ // Writing a value back, with the per-backend type rules in one place. Exported
58
+ // so a host with its own storage UI edits through the same path the tool does.
59
+ export { setStorageValue, storageEditBlockedReason, validateStorageValue, storageValueToEditableText } from "./storage/utils/setStorageValue";
50
60
  export { isMMKVAvailable, getMMKVClass, getMMKVUnavailableMessage } from "./storage/utils/mmkvAvailability";
51
61
  export { detectMMKVType, formatMMKVValue, isTypeMatch } from "./storage/utils/mmkvTypeDetection";
52
62
  export { undoOperation, jumpToState, canUndo } from "./storage/utils/storageTimeTravelUtils";
@@ -70,6 +80,13 @@ export { storageSyncAdapter } from "./storage/sync/storageSyncAdapter";
70
80
  // =============================================================================
71
81
  /** @internal */
72
82
  export { storageEventStore } from "./storage/stores/storageEventStore";
83
+ /**
84
+ * Tell a mounted storage browser to re-read every backend. Used by the desktop
85
+ * dashboard, where local storage listeners are suppressed and device data
86
+ * arrives as snapshots instead.
87
+ * @internal
88
+ */
89
+ export { requestStorageBrowserRefresh } from "./storage/utils/storageBrowserRefresh";
73
90
 
74
91
  // =============================================================================
75
92
  // NOT EXPORTED (Internal capture controls)
@@ -6,14 +6,16 @@ import { TreeDiffViewer as SharedTreeDiffViewer } from "@buoy-gg/shared-ui/dataV
6
6
  import { jsx as _jsx } from "react/jsx-runtime";
7
7
  export function TreeDiffViewer({
8
8
  oldValue,
9
- newValue
9
+ newValue,
10
+ scrollEnabled = true
10
11
  }) {
11
12
  return /*#__PURE__*/_jsx(View, {
12
- style: styles.container,
13
+ style: scrollEnabled ? styles.container : styles.containerInline,
13
14
  children: /*#__PURE__*/_jsx(SharedTreeDiffViewer, {
14
15
  oldValue: oldValue,
15
16
  newValue: newValue,
16
- theme: "dark"
17
+ theme: "dark",
18
+ scrollEnabled: scrollEnabled
17
19
  })
18
20
  });
19
21
  }
@@ -21,5 +23,10 @@ const styles = StyleSheet.create({
21
23
  container: {
22
24
  flex: 1,
23
25
  backgroundColor: gameUIColors.background
26
+ },
27
+ // Inline: size to the tree so the page scroll reveals it, rather than
28
+ // stretching to the pane and clipping the rows.
29
+ containerInline: {
30
+ backgroundColor: gameUIColors.background
24
31
  }
25
32
  });
@@ -18,9 +18,12 @@ export function ThemedSplitView({
18
18
  newValue,
19
19
  theme,
20
20
  options = DEFAULT_OPTIONS,
21
- showThemeName = false
21
+ showThemeName = false,
22
+ scrollEnabled = true
22
23
  }) {
23
- const dynamicStyles = useMemo(() => createDynamicStyles(theme), [theme]);
24
+ // Inline mode drops the fixed 400pt pane so the container grows with the
25
+ // rows; the ancestor ScrollView is what scrolls them.
26
+ const dynamicStyles = useMemo(() => createDynamicStyles(theme, scrollEnabled), [theme, scrollEnabled]);
24
27
  const diffComputeOptions = useMemo(() => ({
25
28
  compareMethod: options.compareMethod,
26
29
  disableWordDiff: options.disableWordDiff,
@@ -69,9 +72,7 @@ export function ThemedSplitView({
69
72
  });
70
73
  }, []);
71
74
  const keyExtractor = useCallback(item => item.kind === "row" ? `r:${item.diffIndex}` : `f:${item.foldId}`, []);
72
- const renderItem = useCallback(({
73
- item
74
- }) => {
75
+ const renderSplitItem = useCallback(item => {
75
76
  if (item.kind === "fold") {
76
77
  return /*#__PURE__*/_jsx(FoldHeader, {
77
78
  foldId: item.foldId,
@@ -90,6 +91,9 @@ export function ThemedSplitView({
90
91
  styles: dynamicStyles
91
92
  });
92
93
  }, [theme, options, dynamicStyles, toggleFold]);
94
+ const renderItem = useCallback(({
95
+ item
96
+ }) => renderSplitItem(item), [renderSplitItem]);
93
97
  return /*#__PURE__*/_jsxs(View, {
94
98
  style: [dynamicStyles.container, theme.glowColor && {
95
99
  shadowColor: theme.glowColor,
@@ -137,7 +141,7 @@ export function ThemedSplitView({
137
141
  style: dynamicStyles.emptyText,
138
142
  children: options.showDiffOnly ? "No differences found" : "No content to display"
139
143
  })
140
- }) : /*#__PURE__*/_jsx(FlatList, {
144
+ }) : scrollEnabled ? /*#__PURE__*/_jsx(FlatList, {
141
145
  style: dynamicStyles.scrollView,
142
146
  contentContainerStyle: dynamicStyles.scrollContent,
143
147
  data: items,
@@ -148,6 +152,16 @@ export function ThemedSplitView({
148
152
  maxToRenderPerBatch: 20,
149
153
  windowSize: 7,
150
154
  removeClippedSubviews: true
155
+ }) :
156
+ /*#__PURE__*/
157
+ // A FlatList can't virtualize inside a page ScrollView — it has no
158
+ // viewport of its own to measure against — so render the rows directly.
159
+ // Folding already keeps this list short (unchanged runs collapse).
160
+ _jsx(View, {
161
+ style: dynamicStyles.scrollContent,
162
+ children: items.map(item => /*#__PURE__*/_jsx(Fragment, {
163
+ children: renderSplitItem(item)
164
+ }, keyExtractor(item)))
151
165
  })]
152
166
  });
153
167
  }
@@ -399,10 +413,14 @@ function getDiffColors(type, theme) {
399
413
  }
400
414
 
401
415
  // Create dynamic styles based on theme
402
- function createDynamicStyles(theme) {
416
+ function createDynamicStyles(theme, scrollEnabled) {
403
417
  return StyleSheet.create({
404
418
  container: {
405
- height: 400,
419
+ // Fixed pane only when the rows scroll themselves; inline mode sizes to
420
+ // content so the page scroll reveals it.
421
+ ...(scrollEnabled ? {
422
+ height: 400
423
+ } : null),
406
424
  backgroundColor: theme.background,
407
425
  borderRadius: 8,
408
426
  overflow: "hidden",