@buoy-gg/storage 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +607 -0
- package/lib/commonjs/index.js +34 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/preset.js +94 -0
- package/lib/commonjs/storage/components/DiffViewer/DiffOptionsPanel.js +356 -0
- package/lib/commonjs/storage/components/DiffViewer/TreeDiffViewer.js +29 -0
- package/lib/commonjs/storage/components/DiffViewer/components/DiffSummary.js +121 -0
- package/lib/commonjs/storage/components/DiffViewer/modes/ThemedSplitView.js +419 -0
- package/lib/commonjs/storage/components/DiffViewer/themes/diffThemes.js +122 -0
- package/lib/commonjs/storage/components/GameUIStorageBrowser.js +924 -0
- package/lib/commonjs/storage/components/GameUIStorageStats.js +746 -0
- package/lib/commonjs/storage/components/MMKVInstanceInfoPanel.js +257 -0
- package/lib/commonjs/storage/components/MMKVInstanceSelector.js +418 -0
- package/lib/commonjs/storage/components/SelectionActionBar.js +224 -0
- package/lib/commonjs/storage/components/StorageActionButtons.js +239 -0
- package/lib/commonjs/storage/components/StorageActions.js +192 -0
- package/lib/commonjs/storage/components/StorageBrowserMode.js +31 -0
- package/lib/commonjs/storage/components/StorageEventDetailContent.js +1025 -0
- package/lib/commonjs/storage/components/StorageEventFilterView.js +141 -0
- package/lib/commonjs/storage/components/StorageEventListener.js +357 -0
- package/lib/commonjs/storage/components/StorageEventsSection.js +24 -0
- package/lib/commonjs/storage/components/StorageFilterCards.js +345 -0
- package/lib/commonjs/storage/components/StorageFilterViewV2.js +42 -0
- package/lib/commonjs/storage/components/StorageKeyCard.js +516 -0
- package/lib/commonjs/storage/components/StorageKeyRow.js +356 -0
- package/lib/commonjs/storage/components/StorageKeySection.js +105 -0
- package/lib/commonjs/storage/components/StorageKeyStats.js +344 -0
- package/lib/commonjs/storage/components/StorageModalWithTabs.js +871 -0
- package/lib/commonjs/storage/components/StorageSection.js +43 -0
- package/lib/commonjs/storage/hooks/useAsyncStorageKeys.js +126 -0
- package/lib/commonjs/storage/hooks/useMMKVInstances.js +221 -0
- package/lib/commonjs/storage/hooks/useMMKVKeys.js +362 -0
- package/lib/commonjs/storage/hooks/useTickEverySecond.js +21 -0
- package/lib/commonjs/storage/index.js +148 -0
- package/lib/commonjs/storage/types.js +5 -0
- package/lib/commonjs/storage/utils/AsyncStorageListener.js +510 -0
- package/lib/commonjs/storage/utils/MMKVInstanceRegistry.js +202 -0
- package/lib/commonjs/storage/utils/MMKVListener.js +380 -0
- package/lib/commonjs/storage/utils/clearAllStorage.js +47 -0
- package/lib/commonjs/storage/utils/index.js +180 -0
- package/lib/commonjs/storage/utils/lineDiff.js +363 -0
- package/lib/commonjs/storage/utils/mmkvAvailability.js +62 -0
- package/lib/commonjs/storage/utils/mmkvTypeDetection.js +139 -0
- package/lib/commonjs/storage/utils/objectDiff.js +157 -0
- package/lib/commonjs/storage/utils/safeAsyncStorage.js +140 -0
- package/lib/commonjs/storage/utils/storageActionHelpers.js +46 -0
- package/lib/commonjs/storage/utils/storageQueryUtils.js +35 -0
- package/lib/commonjs/storage/utils/valueType.js +18 -0
- package/lib/module/index.js +7 -0
- package/lib/module/preset.js +89 -0
- package/lib/module/storage/components/DiffViewer/DiffOptionsPanel.js +352 -0
- package/lib/module/storage/components/DiffViewer/TreeDiffViewer.js +25 -0
- package/lib/module/storage/components/DiffViewer/components/DiffSummary.js +117 -0
- package/lib/module/storage/components/DiffViewer/modes/ThemedSplitView.js +415 -0
- package/lib/module/storage/components/DiffViewer/themes/diffThemes.js +118 -0
- package/lib/module/storage/components/GameUIStorageBrowser.js +922 -0
- package/lib/module/storage/components/GameUIStorageStats.js +742 -0
- package/lib/module/storage/components/MMKVInstanceInfoPanel.js +253 -0
- package/lib/module/storage/components/MMKVInstanceSelector.js +414 -0
- package/lib/module/storage/components/SelectionActionBar.js +221 -0
- package/lib/module/storage/components/StorageActionButtons.js +236 -0
- package/lib/module/storage/components/StorageActions.js +189 -0
- package/lib/module/storage/components/StorageBrowserMode.js +27 -0
- package/lib/module/storage/components/StorageEventDetailContent.js +1020 -0
- package/lib/module/storage/components/StorageEventFilterView.js +137 -0
- package/lib/module/storage/components/StorageEventListener.js +354 -0
- package/lib/module/storage/components/StorageEventsSection.js +20 -0
- package/lib/module/storage/components/StorageFilterCards.js +341 -0
- package/lib/module/storage/components/StorageFilterViewV2.js +38 -0
- package/lib/module/storage/components/StorageKeyCard.js +513 -0
- package/lib/module/storage/components/StorageKeyRow.js +353 -0
- package/lib/module/storage/components/StorageKeySection.js +101 -0
- package/lib/module/storage/components/StorageKeyStats.js +340 -0
- package/lib/module/storage/components/StorageModalWithTabs.js +867 -0
- package/lib/module/storage/components/StorageSection.js +40 -0
- package/lib/module/storage/hooks/useAsyncStorageKeys.js +121 -0
- package/lib/module/storage/hooks/useMMKVInstances.js +216 -0
- package/lib/module/storage/hooks/useMMKVKeys.js +359 -0
- package/lib/module/storage/hooks/useTickEverySecond.js +18 -0
- package/lib/module/storage/index.js +25 -0
- package/lib/module/storage/types.js +3 -0
- package/lib/module/storage/utils/AsyncStorageListener.js +500 -0
- package/lib/module/storage/utils/MMKVInstanceRegistry.js +196 -0
- package/lib/module/storage/utils/MMKVListener.js +367 -0
- package/lib/module/storage/utils/clearAllStorage.js +42 -0
- package/lib/module/storage/utils/index.js +22 -0
- package/lib/module/storage/utils/lineDiff.js +359 -0
- package/lib/module/storage/utils/mmkvAvailability.js +56 -0
- package/lib/module/storage/utils/mmkvTypeDetection.js +133 -0
- package/lib/module/storage/utils/objectDiff.js +153 -0
- package/lib/module/storage/utils/safeAsyncStorage.js +134 -0
- package/lib/module/storage/utils/storageActionHelpers.js +42 -0
- package/lib/module/storage/utils/storageQueryUtils.js +30 -0
- package/lib/module/storage/utils/valueType.js +14 -0
- package/lib/typescript/index.d.ts +3 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/preset.d.ts +90 -0
- package/lib/typescript/preset.d.ts.map +1 -0
- package/lib/typescript/storage/components/DiffViewer/DiffOptionsPanel.d.ts +18 -0
- package/lib/typescript/storage/components/DiffViewer/DiffOptionsPanel.d.ts.map +1 -0
- package/lib/typescript/storage/components/DiffViewer/TreeDiffViewer.d.ts +7 -0
- package/lib/typescript/storage/components/DiffViewer/TreeDiffViewer.d.ts.map +1 -0
- package/lib/typescript/storage/components/DiffViewer/components/DiffSummary.d.ts +12 -0
- package/lib/typescript/storage/components/DiffViewer/components/DiffSummary.d.ts.map +1 -0
- package/lib/typescript/storage/components/DiffViewer/modes/ThemedSplitView.d.ts +13 -0
- package/lib/typescript/storage/components/DiffViewer/modes/ThemedSplitView.d.ts.map +1 -0
- package/lib/typescript/storage/components/DiffViewer/themes/diffThemes.d.ts +64 -0
- package/lib/typescript/storage/components/DiffViewer/themes/diffThemes.d.ts.map +1 -0
- package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts +16 -0
- package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts.map +1 -0
- package/lib/typescript/storage/components/GameUIStorageStats.d.ts +7 -0
- package/lib/typescript/storage/components/GameUIStorageStats.d.ts.map +1 -0
- package/lib/typescript/storage/components/MMKVInstanceInfoPanel.d.ts +42 -0
- package/lib/typescript/storage/components/MMKVInstanceInfoPanel.d.ts.map +1 -0
- package/lib/typescript/storage/components/MMKVInstanceSelector.d.ts +35 -0
- package/lib/typescript/storage/components/MMKVInstanceSelector.d.ts.map +1 -0
- package/lib/typescript/storage/components/SelectionActionBar.d.ts +21 -0
- package/lib/typescript/storage/components/SelectionActionBar.d.ts.map +1 -0
- package/lib/typescript/storage/components/StorageActionButtons.d.ts +21 -0
- package/lib/typescript/storage/components/StorageActionButtons.d.ts.map +1 -0
- package/lib/typescript/storage/components/StorageActions.d.ts +10 -0
- package/lib/typescript/storage/components/StorageActions.d.ts.map +1 -0
- package/lib/typescript/storage/components/StorageBrowserMode.d.ts +18 -0
- package/lib/typescript/storage/components/StorageBrowserMode.d.ts.map +1 -0
- package/lib/typescript/storage/components/StorageEventDetailContent.d.ts +40 -0
- package/lib/typescript/storage/components/StorageEventDetailContent.d.ts.map +1 -0
- package/lib/typescript/storage/components/StorageEventFilterView.d.ts +11 -0
- package/lib/typescript/storage/components/StorageEventFilterView.d.ts.map +1 -0
- package/lib/typescript/storage/components/StorageEventListener.d.ts +6 -0
- package/lib/typescript/storage/components/StorageEventListener.d.ts.map +1 -0
- package/lib/typescript/storage/components/StorageEventsSection.d.ts +7 -0
- package/lib/typescript/storage/components/StorageEventsSection.d.ts.map +1 -0
- package/lib/typescript/storage/components/StorageFilterCards.d.ts +36 -0
- package/lib/typescript/storage/components/StorageFilterCards.d.ts.map +1 -0
- package/lib/typescript/storage/components/StorageFilterViewV2.d.ts +9 -0
- package/lib/typescript/storage/components/StorageFilterViewV2.d.ts.map +1 -0
- package/lib/typescript/storage/components/StorageKeyCard.d.ts +17 -0
- package/lib/typescript/storage/components/StorageKeyCard.d.ts.map +1 -0
- package/lib/typescript/storage/components/StorageKeyRow.d.ts +15 -0
- package/lib/typescript/storage/components/StorageKeyRow.d.ts.map +1 -0
- package/lib/typescript/storage/components/StorageKeySection.d.ts +25 -0
- package/lib/typescript/storage/components/StorageKeySection.d.ts.map +1 -0
- package/lib/typescript/storage/components/StorageKeyStats.d.ts +15 -0
- package/lib/typescript/storage/components/StorageKeyStats.d.ts.map +1 -0
- package/lib/typescript/storage/components/StorageModalWithTabs.d.ts +13 -0
- package/lib/typescript/storage/components/StorageModalWithTabs.d.ts.map +1 -0
- package/lib/typescript/storage/components/StorageSection.d.ts +10 -0
- package/lib/typescript/storage/components/StorageSection.d.ts.map +1 -0
- package/lib/typescript/storage/hooks/useAsyncStorageKeys.d.ts +10 -0
- package/lib/typescript/storage/hooks/useAsyncStorageKeys.d.ts.map +1 -0
- package/lib/typescript/storage/hooks/useMMKVInstances.d.ts +114 -0
- package/lib/typescript/storage/hooks/useMMKVInstances.d.ts.map +1 -0
- package/lib/typescript/storage/hooks/useMMKVKeys.d.ts +94 -0
- package/lib/typescript/storage/hooks/useMMKVKeys.d.ts.map +1 -0
- package/lib/typescript/storage/hooks/useTickEverySecond.d.ts +6 -0
- package/lib/typescript/storage/hooks/useTickEverySecond.d.ts.map +1 -0
- package/lib/typescript/storage/index.d.ts +15 -0
- package/lib/typescript/storage/index.d.ts.map +1 -0
- package/lib/typescript/storage/types.d.ts +41 -0
- package/lib/typescript/storage/types.d.ts.map +1 -0
- package/lib/typescript/storage/utils/AsyncStorageListener.d.ts +195 -0
- package/lib/typescript/storage/utils/AsyncStorageListener.d.ts.map +1 -0
- package/lib/typescript/storage/utils/MMKVInstanceRegistry.d.ts +224 -0
- package/lib/typescript/storage/utils/MMKVInstanceRegistry.d.ts.map +1 -0
- package/lib/typescript/storage/utils/MMKVListener.d.ts +218 -0
- package/lib/typescript/storage/utils/MMKVListener.d.ts.map +1 -0
- package/lib/typescript/storage/utils/clearAllStorage.d.ts +11 -0
- package/lib/typescript/storage/utils/clearAllStorage.d.ts.map +1 -0
- package/lib/typescript/storage/utils/index.d.ts +8 -0
- package/lib/typescript/storage/utils/index.d.ts.map +1 -0
- package/lib/typescript/storage/utils/lineDiff.d.ts +34 -0
- package/lib/typescript/storage/utils/lineDiff.d.ts.map +1 -0
- package/lib/typescript/storage/utils/mmkvAvailability.d.ts +23 -0
- package/lib/typescript/storage/utils/mmkvAvailability.d.ts.map +1 -0
- package/lib/typescript/storage/utils/mmkvTypeDetection.d.ts +71 -0
- package/lib/typescript/storage/utils/mmkvTypeDetection.d.ts.map +1 -0
- package/lib/typescript/storage/utils/objectDiff.d.ts +35 -0
- package/lib/typescript/storage/utils/objectDiff.d.ts.map +1 -0
- package/lib/typescript/storage/utils/safeAsyncStorage.d.ts +56 -0
- package/lib/typescript/storage/utils/safeAsyncStorage.d.ts.map +1 -0
- package/lib/typescript/storage/utils/storageActionHelpers.d.ts +5 -0
- package/lib/typescript/storage/utils/storageActionHelpers.d.ts.map +1 -0
- package/lib/typescript/storage/utils/storageQueryUtils.d.ts +6 -0
- package/lib/typescript/storage/utils/storageQueryUtils.d.ts.map +1 -0
- package/lib/typescript/storage/utils/valueType.d.ts +3 -0
- package/lib/typescript/storage/utils/valueType.d.ts.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Line-by-line diff computation for React Native
|
|
5
|
+
* Adapted from react-diff-viewer for object comparison
|
|
6
|
+
*/
|
|
7
|
+
|
|
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
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MMKV Availability Detection
|
|
5
|
+
*
|
|
6
|
+
* Safely detects if react-native-mmkv is available in the current environment.
|
|
7
|
+
* This allows the app to work in Expo Go where native modules aren't available.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
let _isMMKVAvailable = null;
|
|
11
|
+
let _MMKVClass = null;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check if MMKV is available in the current environment
|
|
15
|
+
*
|
|
16
|
+
* @returns True if react-native-mmkv is installed and available
|
|
17
|
+
*/
|
|
18
|
+
export function isMMKVAvailable() {
|
|
19
|
+
// Cache the result after first check
|
|
20
|
+
if (_isMMKVAvailable !== null) {
|
|
21
|
+
return _isMMKVAvailable;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
// Attempt to require MMKV
|
|
25
|
+
const MMKVModule = require('react-native-mmkv');
|
|
26
|
+
|
|
27
|
+
// v4 uses createMMKV factory function, v3 used MMKV class
|
|
28
|
+
// Try v4 API first (recommended), fall back to v3
|
|
29
|
+
_MMKVClass = MMKVModule.createMMKV || MMKVModule.MMKV;
|
|
30
|
+
_isMMKVAvailable = _MMKVClass !== undefined;
|
|
31
|
+
return _isMMKVAvailable;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
// MMKV is not available (e.g., Expo Go, not installed)
|
|
34
|
+
_isMMKVAvailable = false;
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get the MMKV class if available
|
|
41
|
+
*
|
|
42
|
+
* @returns MMKV class or null if not available
|
|
43
|
+
*/
|
|
44
|
+
export function getMMKVClass() {
|
|
45
|
+
if (!isMMKVAvailable()) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return _MMKVClass;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get a user-friendly error message when MMKV is not available
|
|
53
|
+
*/
|
|
54
|
+
export function getMMKVUnavailableMessage() {
|
|
55
|
+
return 'MMKV is not available in this environment. MMKV requires native modules and cannot run in Expo Go. Please use a development build or EAS Build.';
|
|
56
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MMKV Type Detection Utilities
|
|
5
|
+
*
|
|
6
|
+
* MMKV stores values with native types (string, number, boolean, ArrayBuffer).
|
|
7
|
+
* This module provides utilities to detect the type of a stored value by trying
|
|
8
|
+
* each getter sequentially.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Use 'any' type for MMKV to avoid hard dependency on react-native-mmkv
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Detect the type of a value stored in MMKV by trying each getter.
|
|
15
|
+
*
|
|
16
|
+
* Since MMKV doesn't provide a getType() method, we must try each getter
|
|
17
|
+
* (getString, getNumber, getBoolean, getBuffer) until one returns a value.
|
|
18
|
+
*
|
|
19
|
+
* Order matters: We try in order of most common types first.
|
|
20
|
+
*
|
|
21
|
+
* @param instance - MMKV instance to read from
|
|
22
|
+
* @param key - Storage key
|
|
23
|
+
* @returns Value and its detected type
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* const instance = createMMKV();
|
|
28
|
+
* instance.set('name', 'John');
|
|
29
|
+
* instance.set('age', 30);
|
|
30
|
+
* instance.set('active', true);
|
|
31
|
+
*
|
|
32
|
+
* detectMMKVType(instance, 'name'); // { value: 'John', type: 'string' }
|
|
33
|
+
* detectMMKVType(instance, 'age'); // { value: 30, type: 'number' }
|
|
34
|
+
* detectMMKVType(instance, 'active'); // { value: true, type: 'boolean' }
|
|
35
|
+
* detectMMKVType(instance, 'missing');// { value: undefined, type: 'unknown' }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export function detectMMKVType(instance, key) {
|
|
39
|
+
// Try string (most common)
|
|
40
|
+
const stringValue = instance.getString(key);
|
|
41
|
+
if (stringValue !== undefined) {
|
|
42
|
+
return {
|
|
43
|
+
value: stringValue,
|
|
44
|
+
type: 'string'
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Try number
|
|
49
|
+
const numberValue = instance.getNumber(key);
|
|
50
|
+
if (numberValue !== undefined) {
|
|
51
|
+
return {
|
|
52
|
+
value: numberValue,
|
|
53
|
+
type: 'number'
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Try boolean
|
|
58
|
+
const booleanValue = instance.getBoolean(key);
|
|
59
|
+
if (booleanValue !== undefined) {
|
|
60
|
+
return {
|
|
61
|
+
value: booleanValue,
|
|
62
|
+
type: 'boolean'
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Try buffer (least common)
|
|
67
|
+
const bufferValue = instance.getBuffer(key);
|
|
68
|
+
if (bufferValue !== undefined) {
|
|
69
|
+
// Format buffer for display (don't return raw ArrayBuffer)
|
|
70
|
+
return {
|
|
71
|
+
value: `<ArrayBuffer ${bufferValue.byteLength} bytes>`,
|
|
72
|
+
type: 'buffer'
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Key doesn't exist or unknown type
|
|
77
|
+
return {
|
|
78
|
+
value: undefined,
|
|
79
|
+
type: 'unknown'
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Format an MMKV value for display in the UI.
|
|
85
|
+
*
|
|
86
|
+
* @param value - The value to format
|
|
87
|
+
* @param type - The detected type
|
|
88
|
+
* @returns Formatted string representation
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```typescript
|
|
92
|
+
* formatMMKVValue('hello', 'string'); // '"hello"'
|
|
93
|
+
* formatMMKVValue(42, 'number'); // '42'
|
|
94
|
+
* formatMMKVValue(true, 'boolean'); // 'true'
|
|
95
|
+
* formatMMKVValue('<ArrayBuffer 1024 bytes>', 'buffer'); // '<ArrayBuffer 1024 bytes>'
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export function formatMMKVValue(value, type) {
|
|
99
|
+
if (value === undefined) return 'undefined';
|
|
100
|
+
if (value === null) return 'null';
|
|
101
|
+
switch (type) {
|
|
102
|
+
case 'string':
|
|
103
|
+
return `"${value}"`;
|
|
104
|
+
case 'number':
|
|
105
|
+
return String(value);
|
|
106
|
+
case 'boolean':
|
|
107
|
+
return value ? 'true' : 'false';
|
|
108
|
+
case 'buffer':
|
|
109
|
+
// Value is already formatted as "<ArrayBuffer X bytes>"
|
|
110
|
+
return value;
|
|
111
|
+
case 'unknown':
|
|
112
|
+
return 'unknown';
|
|
113
|
+
default:
|
|
114
|
+
return String(value);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check if a value type matches an expected type for validation.
|
|
120
|
+
*
|
|
121
|
+
* @param actualType - Detected type
|
|
122
|
+
* @param expectedType - Expected type from configuration
|
|
123
|
+
* @returns True if types match
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```typescript
|
|
127
|
+
* isTypeMatch('string', 'string'); // true
|
|
128
|
+
* isTypeMatch('number', 'string'); // false
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
export function isTypeMatch(actualType, expectedType) {
|
|
132
|
+
return actualType === expectedType;
|
|
133
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type guard to check if a value is a plain object (not array or null)
|
|
5
|
+
*
|
|
6
|
+
* @param obj - Value to check
|
|
7
|
+
* @returns True if the value is a plain object
|
|
8
|
+
*/
|
|
9
|
+
function isObject(obj) {
|
|
10
|
+
return obj !== null && typeof obj === "object" && !Array.isArray(obj);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Type guard to check if a value is an array
|
|
15
|
+
*
|
|
16
|
+
* @param obj - Value to check
|
|
17
|
+
* @returns True if the value is an array
|
|
18
|
+
*/
|
|
19
|
+
function isArray(obj) {
|
|
20
|
+
return Array.isArray(obj);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Recursively compare two values and collect differences
|
|
25
|
+
*
|
|
26
|
+
* This function handles deep comparison of objects, arrays, and primitive values,
|
|
27
|
+
* building a comprehensive diff that tracks the exact path of each change.
|
|
28
|
+
*
|
|
29
|
+
* @param oldVal - The original value to compare from
|
|
30
|
+
* @param newVal - The new value to compare to
|
|
31
|
+
* @param path - Current path in the object structure (for nested properties)
|
|
32
|
+
* @param diffs - Array to collect difference items
|
|
33
|
+
*
|
|
34
|
+
* @performance Uses recursive traversal with path tracking for memory efficiency
|
|
35
|
+
* @performance Handles large nested structures without stack overflow concerns for typical use cases
|
|
36
|
+
*/
|
|
37
|
+
function compareValues(oldVal, newVal, path, diffs) {
|
|
38
|
+
// Both are objects
|
|
39
|
+
if (isObject(oldVal) && isObject(newVal)) {
|
|
40
|
+
const allKeys = new Set([...Object.keys(oldVal), ...Object.keys(newVal)]);
|
|
41
|
+
for (const key of allKeys) {
|
|
42
|
+
const newPath = [...path, key];
|
|
43
|
+
if (!(key in oldVal)) {
|
|
44
|
+
// Key was added
|
|
45
|
+
diffs.push({
|
|
46
|
+
type: "CREATE",
|
|
47
|
+
path: newPath,
|
|
48
|
+
value: newVal[key]
|
|
49
|
+
});
|
|
50
|
+
} else if (!(key in newVal)) {
|
|
51
|
+
// Key was removed
|
|
52
|
+
diffs.push({
|
|
53
|
+
type: "REMOVE",
|
|
54
|
+
path: newPath,
|
|
55
|
+
oldValue: oldVal[key]
|
|
56
|
+
});
|
|
57
|
+
} else {
|
|
58
|
+
// Key exists in both, compare values
|
|
59
|
+
compareValues(oldVal[key], newVal[key], newPath, diffs);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Both are arrays
|
|
64
|
+
else if (isArray(oldVal) && isArray(newVal)) {
|
|
65
|
+
const maxLength = Math.max(oldVal.length, newVal.length);
|
|
66
|
+
for (let i = 0; i < maxLength; i++) {
|
|
67
|
+
const newPath = [...path, i];
|
|
68
|
+
if (i >= oldVal.length) {
|
|
69
|
+
// Item was added
|
|
70
|
+
diffs.push({
|
|
71
|
+
type: "CREATE",
|
|
72
|
+
path: newPath,
|
|
73
|
+
value: newVal[i]
|
|
74
|
+
});
|
|
75
|
+
} else if (i >= newVal.length) {
|
|
76
|
+
// Item was removed
|
|
77
|
+
diffs.push({
|
|
78
|
+
type: "REMOVE",
|
|
79
|
+
path: newPath,
|
|
80
|
+
oldValue: oldVal[i]
|
|
81
|
+
});
|
|
82
|
+
} else {
|
|
83
|
+
// Item exists in both, compare values
|
|
84
|
+
compareValues(oldVal[i], newVal[i], newPath, diffs);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Values are different types or primitives
|
|
89
|
+
else if (oldVal !== newVal) {
|
|
90
|
+
diffs.push({
|
|
91
|
+
type: "CHANGE",
|
|
92
|
+
path,
|
|
93
|
+
oldValue: oldVal,
|
|
94
|
+
value: newVal
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Generate a comprehensive diff between two objects or values
|
|
101
|
+
*
|
|
102
|
+
* This function performs a deep comparison between two values and returns
|
|
103
|
+
* a detailed list of all differences, including additions, removals, and changes.
|
|
104
|
+
* Each difference includes the exact path where the change occurred.
|
|
105
|
+
*
|
|
106
|
+
* @param oldObj - The original object/value to compare from
|
|
107
|
+
* @param newObj - The new object/value to compare to
|
|
108
|
+
* @returns Array of DiffItem objects describing all differences
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```typescript
|
|
112
|
+
* const oldData = { user: { name: "John", age: 30 }, items: [1, 2] };
|
|
113
|
+
* const newData = { user: { name: "Jane", age: 30 }, items: [1, 2, 3] };
|
|
114
|
+
*
|
|
115
|
+
* const diffs = objectDiff(oldData, newData);
|
|
116
|
+
* // Returns:
|
|
117
|
+
* // [
|
|
118
|
+
* // { type: "CHANGE", path: ["user", "name"], oldValue: "John", value: "Jane" },
|
|
119
|
+
* // { type: "CREATE", path: ["items", 2], value: 3 }
|
|
120
|
+
* // ]
|
|
121
|
+
* ```
|
|
122
|
+
*
|
|
123
|
+
* @performance Handles circular references and deep nesting efficiently
|
|
124
|
+
* @performance Uses Set for key deduplication to optimize comparison speed
|
|
125
|
+
*/
|
|
126
|
+
export function objectDiff(oldObj, newObj) {
|
|
127
|
+
const diffs = [];
|
|
128
|
+
|
|
129
|
+
// Handle null/undefined cases
|
|
130
|
+
if (oldObj === newObj) {
|
|
131
|
+
return diffs;
|
|
132
|
+
}
|
|
133
|
+
if (oldObj === null || oldObj === undefined) {
|
|
134
|
+
if (newObj !== null && newObj !== undefined) {
|
|
135
|
+
diffs.push({
|
|
136
|
+
type: "CREATE",
|
|
137
|
+
path: [],
|
|
138
|
+
value: newObj
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return diffs;
|
|
142
|
+
}
|
|
143
|
+
if (newObj === null || newObj === undefined) {
|
|
144
|
+
diffs.push({
|
|
145
|
+
type: "REMOVE",
|
|
146
|
+
path: [],
|
|
147
|
+
oldValue: oldObj
|
|
148
|
+
});
|
|
149
|
+
return diffs;
|
|
150
|
+
}
|
|
151
|
+
compareValues(oldObj, newObj, [], diffs);
|
|
152
|
+
return diffs;
|
|
153
|
+
}
|