@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
@@ -126,6 +126,20 @@ class MMKVListener {
126
126
  // The native listener (addOnValueChangedListener) is sufficient for monitoring writes
127
127
 
128
128
  let nativeListenerUnsubscribe;
129
+
130
+ // Seed the shadow copy so the FIRST observed write already knows what it
131
+ // replaced. Without this, every key's first event would claim the key never
132
+ // existed even when it was carried over from a previous launch.
133
+ const lastKnownValues = new Map();
134
+ try {
135
+ for (const key of instance.getAllKeys() ?? []) {
136
+ if (this.shouldIgnoreKey(key)) continue;
137
+ lastKnownValues.set(key, detectMMKVType(instance, key).value);
138
+ }
139
+ } catch {
140
+ // Key scan unavailable — prevValue is simply absent until each key is
141
+ // written once, which is strictly better than reporting a wrong value.
142
+ }
129
143
  if (typeof instance.addOnValueChangedListener === "function") {
130
144
  try {
131
145
  nativeListenerUnsubscribe = instance.addOnValueChangedListener(key => {
@@ -138,9 +152,15 @@ class MMKVListener {
138
152
  type
139
153
  } = detectMMKVType(instance, key);
140
154
 
155
+ // Read the shadow copy BEFORE overwriting it — this is the value the
156
+ // write replaced. `undefined` means the key did not exist.
157
+ const prevValue = lastKnownValues.get(key);
158
+
141
159
  // Determine action: if value is undefined/unknown, it was deleted
142
160
  const isDelete = type === "unknown" || value === undefined;
143
161
  if (isDelete) {
162
+ lastKnownValues.delete(key);
163
+
144
164
  // Key was deleted
145
165
  const event = {
146
166
  action: "delete",
@@ -148,11 +168,14 @@ class MMKVListener {
148
168
  instanceId,
149
169
  data: {
150
170
  key,
171
+ prevValue,
151
172
  success: true
152
173
  }
153
174
  };
154
175
  this.emit(event);
155
176
  } else {
177
+ lastKnownValues.set(key, value);
178
+
156
179
  // Key was set/updated
157
180
  const action = type === "string" ? "set.string" : type === "number" ? "set.number" : type === "boolean" ? "set.boolean" : type === "buffer" ? "set.buffer" : "set.string"; // fallback
158
181
 
@@ -163,6 +186,7 @@ class MMKVListener {
163
186
  data: {
164
187
  key,
165
188
  value,
189
+ prevValue,
166
190
  valueType: type,
167
191
  success: true
168
192
  }
@@ -181,7 +205,8 @@ class MMKVListener {
181
205
  instanceId,
182
206
  originalMethods: {},
183
207
  // Not used in v4
184
- nativeListenerUnsubscribe
208
+ nativeListenerUnsubscribe,
209
+ lastKnownValues
185
210
  });
186
211
  }
187
212
 
@@ -201,9 +226,20 @@ class MMKVListener {
201
226
  nativeListenerUnsubscribe
202
227
  } = tracked;
203
228
 
204
- // Remove native listener
205
- if (nativeListenerUnsubscribe) {
206
- nativeListenerUnsubscribe();
229
+ // Remove native listener. The return of addOnValueChangedListener varies by
230
+ // implementation: a bare unsubscribe function, or a Listener object with a
231
+ // `.remove()` method (react-native-mmkv v4 and the desktop RemoteMMKV
232
+ // back-end). Handle both, and never let teardown throw — this runs during
233
+ // React unmount, where an exception escapes every error boundary and blanks
234
+ // the whole app.
235
+ try {
236
+ if (typeof nativeListenerUnsubscribe === "function") {
237
+ nativeListenerUnsubscribe();
238
+ } else if (typeof nativeListenerUnsubscribe?.remove === "function") {
239
+ nativeListenerUnsubscribe.remove();
240
+ }
241
+ } catch {
242
+ /* listener already gone / back-end offline — nothing to clean up */
207
243
  }
208
244
 
209
245
  // Remove from tracking
@@ -49,12 +49,14 @@
49
49
  class SecureStoreRegistry {
50
50
  module = null;
51
51
  keys = new Map();
52
+ listeners = new Set();
52
53
 
53
54
  /**
54
55
  * Register the SecureStore module and the keys to expose. Callable multiple
55
56
  * times — keys accumulate, the latest module wins.
56
57
  */
57
58
  register(module, descriptors) {
59
+ const before = this.signature();
58
60
  this.module = module;
59
61
  for (const d of descriptors) {
60
62
  const entry = typeof d === "string" ? {
@@ -71,11 +73,41 @@ class SecureStoreRegistry {
71
73
  };
72
74
  this.keys.set(entry.key, entry);
73
75
  }
76
+ // Only announce a real change. The desktop dashboard re-registers the same
77
+ // key set on every device snapshot, and subscribers re-read the keychain on
78
+ // notify — signature-comparing keeps that from turning into a read storm.
79
+ if (this.signature() !== before) this.notify();
74
80
  }
75
81
 
76
82
  /** Remove a key from the registry (does not delete the stored value). */
77
83
  unregister(key) {
78
- this.keys.delete(key);
84
+ if (this.keys.delete(key)) this.notify();
85
+ }
86
+
87
+ /**
88
+ * Subscribe to registry changes (keys registered/unregistered). Lets the
89
+ * browser pick up keys that register after the tool is already open.
90
+ * Returns an unsubscribe function.
91
+ */
92
+ subscribe(listener) {
93
+ this.listeners.add(listener);
94
+ return () => {
95
+ this.listeners.delete(listener);
96
+ };
97
+ }
98
+
99
+ /** Content signature of the registered entries (module identity excluded). */
100
+ signature() {
101
+ return JSON.stringify(Array.from(this.keys.values()));
102
+ }
103
+ notify() {
104
+ this.listeners.forEach(listener => {
105
+ try {
106
+ listener();
107
+ } catch {
108
+ // Listener errors are not ours
109
+ }
110
+ });
79
111
  }
80
112
 
81
113
  /** The registered SecureStore module, or null if none registered yet. */
@@ -102,6 +134,25 @@ class SecureStoreRegistry {
102
134
  if (!this.module || !entry) return;
103
135
  await this.module.deleteItemAsync(key, entry.options);
104
136
  }
137
+
138
+ /**
139
+ * Write a stored value via the registered module, using the options the key
140
+ * was registered with — writing with different `keychainService` options than
141
+ * the value was created with silently creates a SECOND entry the app can't
142
+ * read, so the registered options are the only safe ones to use.
143
+ *
144
+ * Throws rather than no-ops: unlike a delete, a write that quietly did
145
+ * nothing would leave the UI showing a value that was never stored.
146
+ */
147
+ async setValue(key, value) {
148
+ const entry = this.keys.get(key);
149
+ if (!this.module) throw new Error("No SecureStore module is registered");
150
+ if (!entry) throw new Error(`SecureStore key "${key}" is not registered`);
151
+ if (entry.options.requireAuthentication) {
152
+ throw new Error(`SecureStore key "${key}" is biometric-protected — DevTools never writes it`);
153
+ }
154
+ await this.module.setItemAsync(key, value, entry.options);
155
+ }
105
156
  }
106
157
 
107
158
  /**
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Deleting storage keys, in one place.
5
+ *
6
+ * Each backend removes keys differently — AsyncStorage takes a batch,
7
+ * SecureStore has to go through the registry (only it knows each key's
8
+ * `keychainService` options), and MMKV deletes per registered instance. Two call
9
+ * sites need this (the selection action bar and a single row's "Delete key"),
10
+ * and a second copy of that fan-out is exactly how one backend quietly stops
11
+ * being cleaned up.
12
+ *
13
+ * Pro gating and confirmation are deliberately NOT here: they belong to the UI
14
+ * that has the user's attention. This function assumes the caller already asked.
15
+ */
16
+
17
+ import { removeMany } from "./asyncStorageCompat";
18
+ import { secureStoreRegistry } from "./SecureStoreRegistry";
19
+ /**
20
+ * Removes every key from whichever backend holds it. Rejects on the first
21
+ * failure — callers surface that, rather than reporting a partial delete as
22
+ * success.
23
+ */
24
+ export async function deleteStorageKeys(keys, mmkvInstances) {
25
+ if (keys.length === 0) return;
26
+ const asyncKeys = keys.filter(k => k.storageType === "async");
27
+ const mmkvKeys = keys.filter(k => k.storageType === "mmkv");
28
+ const secureKeys = keys.filter(k => k.storageType === "secure");
29
+ if (asyncKeys.length > 0) {
30
+ await removeMany(asyncKeys.map(k => k.key));
31
+ }
32
+ for (const k of secureKeys) {
33
+ await secureStoreRegistry.deleteValue(k.key);
34
+ }
35
+ if (mmkvKeys.length > 0) {
36
+ const byInstance = {};
37
+ mmkvKeys.forEach(k => {
38
+ const instanceId = k.instanceId || "default";
39
+ if (!byInstance[instanceId]) byInstance[instanceId] = [];
40
+ byInstance[instanceId].push(k.key);
41
+ });
42
+ Object.entries(byInstance).forEach(([instanceId, instanceKeys]) => {
43
+ const inst = mmkvInstances.find(i => i.id === instanceId);
44
+ if (inst) {
45
+ instanceKeys.forEach(key => inst.instance.delete(key));
46
+ }
47
+ });
48
+ }
49
+ }
@@ -21,5 +21,8 @@ export { registerMMKVInstance, unregisterMMKVInstance, mmkvInstanceRegistry } fr
21
21
  // MMKV Listener
22
22
  export { addMMKVInstance, removeMMKVInstance, removeAllMMKVInstances, addMMKVListener, removeAllMMKVListeners, isMMKVInstanceMonitored, getMonitoredMMKVInstances, getMMKVInstanceCount, getMMKVListenerCount, isMMKVListening, mmkvListener } from "./MMKVListener";
23
23
 
24
+ // Storage browser refresh signal (remote-mirror mode)
25
+ export { onStorageBrowserRefresh, requestStorageBrowserRefresh } from "./storageBrowserRefresh";
26
+
24
27
  // MMKV Type Detection
25
28
  export { detectMMKVType, formatMMKVValue, isTypeMatch } from "./mmkvTypeDetection";
@@ -1,359 +1,12 @@
1
1
  "use strict";
2
2
 
3
3
  /**
4
- * Line-by-line diff computation for React Native
5
- * Adapted from react-diff-viewer for object comparison
4
+ * Line-by-line diff computation.
5
+ *
6
+ * This used to be a verbatim copy of the shared implementation, which meant a
7
+ * diff fix had to be made twice. It now re-exports the canonical one so the
8
+ * storage split view, the tree view, and every other tool's diff (redux, jotai,
9
+ * highlight-updates) share a single algorithm.
6
10
  */
7
11
 
8
- export let DiffType = /*#__PURE__*/function (DiffType) {
9
- DiffType["DEFAULT"] = "unchanged";
10
- DiffType["ADDED"] = "added";
11
- DiffType["REMOVED"] = "removed";
12
- DiffType["MODIFIED"] = "modified";
13
- return DiffType;
14
- }({});
15
- /**
16
- * Compute diff based on method - for word-level diff within lines
17
- */
18
- function computeDiffByMethod(oldStr, newStr, method) {
19
- let oldParts;
20
- let newParts;
21
- switch (method) {
22
- case "chars":
23
- // Split into individual characters
24
- oldParts = oldStr.split("");
25
- newParts = newStr.split("");
26
- break;
27
- case "words":
28
- // Split by word boundaries, keeping whitespace
29
- oldParts = oldStr.match(/\S+|\s+/g) || [];
30
- newParts = newStr.match(/\S+|\s+/g) || [];
31
- break;
32
- case "trimmedLines":
33
- {
34
- // For trimmedLines, compare without leading/trailing whitespace
35
- const oldTrimmed = oldStr.trim();
36
- const newTrimmed = newStr.trim();
37
- // But still do word-level diff on the trimmed content
38
- oldParts = oldTrimmed.match(/\S+|\s+/g) || [];
39
- newParts = newTrimmed.match(/\S+|\s+/g) || [];
40
- break;
41
- }
42
- case "lines":
43
- default:
44
- // For lines mode, don't do word diff - just show the whole line
45
- return {
46
- left: [{
47
- value: oldStr,
48
- type: DiffType.REMOVED
49
- }],
50
- right: [{
51
- value: newStr,
52
- type: DiffType.ADDED
53
- }]
54
- };
55
- }
56
-
57
- // Simple LCS-like algorithm for better diff
58
- const left = [];
59
- const right = [];
60
- let i = 0,
61
- j = 0;
62
-
63
- // Find matching parts
64
- while (i < oldParts.length && j < newParts.length) {
65
- if (oldParts[i] === newParts[j]) {
66
- // Parts match
67
- left.push({
68
- value: oldParts[i],
69
- type: DiffType.DEFAULT
70
- });
71
- right.push({
72
- value: newParts[j],
73
- type: DiffType.DEFAULT
74
- });
75
- i++;
76
- j++;
77
- } else {
78
- // Look ahead for matches
79
- let foundMatch = false;
80
-
81
- // Check if we can find newParts[j] in upcoming oldParts
82
- for (let k = i + 1; k < Math.min(i + 5, oldParts.length); k++) {
83
- if (oldParts[k] === newParts[j]) {
84
- // Mark everything from i to k-1 as removed
85
- for (let m = i; m < k; m++) {
86
- left.push({
87
- value: oldParts[m],
88
- type: DiffType.REMOVED
89
- });
90
- }
91
- i = k;
92
- foundMatch = true;
93
- break;
94
- }
95
- }
96
- if (!foundMatch) {
97
- // Check if we can find oldParts[i] in upcoming newParts
98
- for (let k = j + 1; k < Math.min(j + 5, newParts.length); k++) {
99
- if (newParts[k] === oldParts[i]) {
100
- // Mark everything from j to k-1 as added
101
- for (let m = j; m < k; m++) {
102
- right.push({
103
- value: newParts[m],
104
- type: DiffType.ADDED
105
- });
106
- }
107
- j = k;
108
- foundMatch = true;
109
- break;
110
- }
111
- }
112
- }
113
- if (!foundMatch) {
114
- // No match found nearby, mark as changed
115
- left.push({
116
- value: oldParts[i],
117
- type: DiffType.REMOVED
118
- });
119
- right.push({
120
- value: newParts[j],
121
- type: DiffType.ADDED
122
- });
123
- i++;
124
- j++;
125
- }
126
- }
127
- }
128
-
129
- // Handle remaining parts
130
- while (i < oldParts.length) {
131
- left.push({
132
- value: oldParts[i],
133
- type: DiffType.REMOVED
134
- });
135
- i++;
136
- }
137
- while (j < newParts.length) {
138
- right.push({
139
- value: newParts[j],
140
- type: DiffType.ADDED
141
- });
142
- j++;
143
- }
144
- return {
145
- left,
146
- right
147
- };
148
- }
149
-
150
- /**
151
- * Convert object to formatted JSON lines
152
- */
153
- function objectToLines(obj) {
154
- if (obj === null || obj === undefined) {
155
- return [String(obj)];
156
- }
157
- try {
158
- // Pretty print JSON with 2 space indentation
159
- const jsonStr = JSON.stringify(obj, null, 2);
160
- // Split into lines and filter out empty lines that appear between array elements
161
- const lines = jsonStr.split("\n");
162
-
163
- // Remove empty lines but keep structure
164
- const filteredLines = [];
165
- for (let i = 0; i < lines.length; i++) {
166
- const line = lines[i];
167
- const trimmed = line.trim();
168
-
169
- // Keep the line if it's not empty or if it's meaningful whitespace
170
- if (trimmed !== "") {
171
- filteredLines.push(line);
172
- } else if (i === 0 || i === lines.length - 1) {
173
- // Keep first and last empty lines if they exist (though they shouldn't)
174
- filteredLines.push(line);
175
- }
176
- // Skip other empty lines
177
- }
178
- return filteredLines;
179
- } catch {
180
- return [String(obj)];
181
- }
182
- }
183
- /**
184
- * Apply showDiffOnly filter with context lines
185
- */
186
- function filterDiffWithContext(diffs, contextLines) {
187
- if (contextLines < 0) return diffs;
188
- const result = [];
189
- const changedIndices = [];
190
-
191
- // Find all changed lines
192
- diffs.forEach((diff, idx) => {
193
- if (diff.type !== DiffType.DEFAULT) {
194
- changedIndices.push(idx);
195
- }
196
- });
197
-
198
- // If no changes, return empty
199
- if (changedIndices.length === 0) {
200
- return [];
201
- }
202
-
203
- // Build ranges to include
204
- const ranges = [];
205
- let currentStart = Math.max(0, changedIndices[0] - contextLines);
206
- let currentEnd = Math.min(diffs.length - 1, changedIndices[0] + contextLines);
207
- for (let i = 1; i < changedIndices.length; i++) {
208
- const idx = changedIndices[i];
209
- const rangeStart = Math.max(0, idx - contextLines);
210
- const rangeEnd = Math.min(diffs.length - 1, idx + contextLines);
211
-
212
- // If ranges overlap or are adjacent, merge them
213
- if (rangeStart <= currentEnd + 1) {
214
- currentEnd = Math.max(currentEnd, rangeEnd);
215
- } else {
216
- // Save current range and start a new one
217
- ranges.push([currentStart, currentEnd]);
218
- currentStart = rangeStart;
219
- currentEnd = rangeEnd;
220
- }
221
- }
222
-
223
- // Don't forget the last range
224
- ranges.push([currentStart, currentEnd]);
225
-
226
- // Build result from ranges
227
- ranges.forEach(([start, end]) => {
228
- for (let i = start; i <= end; i++) {
229
- result.push(diffs[i]);
230
- }
231
- });
232
- return result;
233
- }
234
-
235
- /**
236
- * Compare lines based on method
237
- */
238
- function compareLinesWithMethod(line1, line2, method) {
239
- switch (method) {
240
- case "trimmedLines":
241
- return line1.trim() === line2.trim();
242
- case "chars":
243
- case "words":
244
- case "lines":
245
- default:
246
- return line1 === line2;
247
- }
248
- }
249
-
250
- /**
251
- * Compute line-by-line diff between two objects
252
- */
253
- export function computeLineDiff(oldValue, newValue, options = {}) {
254
- const {
255
- compareMethod = "words",
256
- disableWordDiff = false,
257
- showDiffOnly = false,
258
- contextLines = 3
259
- } = options;
260
- const oldLines = objectToLines(oldValue);
261
- const newLines = objectToLines(newValue);
262
- const result = [];
263
- let leftLineNum = 1;
264
- let rightLineNum = 1;
265
-
266
- // Simple line diff algorithm (can be improved with LCS)
267
- let i = 0,
268
- j = 0;
269
- while (i < oldLines.length || j < newLines.length) {
270
- if (i >= oldLines.length) {
271
- // Rest are additions
272
- result.push({
273
- rightLineNumber: rightLineNum++,
274
- type: DiffType.ADDED,
275
- rightContent: newLines[j],
276
- rightRaw: newLines[j]
277
- });
278
- j++;
279
- } else if (j >= newLines.length) {
280
- // Rest are removals
281
- result.push({
282
- leftLineNumber: leftLineNum++,
283
- type: DiffType.REMOVED,
284
- leftContent: oldLines[i],
285
- leftRaw: oldLines[i]
286
- });
287
- i++;
288
- } else if (compareLinesWithMethod(oldLines[i], newLines[j], compareMethod === "trimmedLines" ? "trimmedLines" : "lines")) {
289
- // Lines match (possibly after trimming if trimmedLines)
290
- result.push({
291
- leftLineNumber: leftLineNum++,
292
- rightLineNumber: rightLineNum++,
293
- type: DiffType.DEFAULT,
294
- leftContent: oldLines[i],
295
- rightContent: newLines[j],
296
- leftRaw: oldLines[i],
297
- rightRaw: newLines[j]
298
- });
299
- i++;
300
- j++;
301
- } else {
302
- // Lines differ - check if it's a modification or separate add/remove
303
- const oldTrimmed = oldLines[i].trim();
304
- const newTrimmed = newLines[j].trim();
305
-
306
- // Simple heuristic: if lines start similarly, treat as modification
307
- if (oldTrimmed && newTrimmed && (oldTrimmed.startsWith(newTrimmed.substring(0, 3)) || newTrimmed.startsWith(oldTrimmed.substring(0, 3)))) {
308
- // Treat as modification - compute word diff if enabled
309
- if (!disableWordDiff && compareMethod !== "lines") {
310
- const wordDiff = computeDiffByMethod(oldLines[i], newLines[j], compareMethod);
311
- result.push({
312
- leftLineNumber: leftLineNum++,
313
- rightLineNumber: rightLineNum++,
314
- type: DiffType.MODIFIED,
315
- leftContent: wordDiff.left,
316
- rightContent: wordDiff.right,
317
- leftRaw: oldLines[i],
318
- rightRaw: newLines[j]
319
- });
320
- } else {
321
- // No word diff - just mark lines as different
322
- result.push({
323
- leftLineNumber: leftLineNum++,
324
- rightLineNumber: rightLineNum++,
325
- type: DiffType.MODIFIED,
326
- leftContent: oldLines[i],
327
- rightContent: newLines[j],
328
- leftRaw: oldLines[i],
329
- rightRaw: newLines[j]
330
- });
331
- }
332
- i++;
333
- j++;
334
- } else {
335
- // Treat as separate remove and add
336
- result.push({
337
- leftLineNumber: leftLineNum++,
338
- type: DiffType.REMOVED,
339
- leftContent: oldLines[i],
340
- leftRaw: oldLines[i]
341
- });
342
- result.push({
343
- rightLineNumber: rightLineNum++,
344
- type: DiffType.ADDED,
345
- rightContent: newLines[j],
346
- rightRaw: newLines[j]
347
- });
348
- i++;
349
- j++;
350
- }
351
- }
352
- }
353
-
354
- // Apply showDiffOnly filter if enabled
355
- if (showDiffOnly) {
356
- return filterDiffWithContext(result, contextLines);
357
- }
358
- return result;
359
- }
12
+ export { computeLineDiff, DiffType, ABSENT_VALUE, isAbsent } from "@buoy-gg/shared-ui/dataViewer";