j-templates 8.2.0 → 9.0.1
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/Node/vNode.js +2 -0
- package/Store/Diff/diffTree.d.ts +24 -49
- package/Store/Diff/diffTree.js +112 -511
- package/Store/Diff/index.d.ts +1 -0
- package/Store/Diff/index.js +1 -0
- package/Store/Diff/utils/diffTree.types.d.ts +6 -1
- package/Store/Diff/utils/homes.d.ts +46 -0
- package/Store/Diff/utils/homes.js +213 -0
- package/Store/Diff/utils/paths.d.ts +42 -0
- package/Store/Diff/utils/paths.js +87 -0
- package/Store/Diff/utils/projections.d.ts +55 -0
- package/Store/Diff/utils/projections.js +290 -0
- package/Store/Diff/utils/routes.d.ts +27 -0
- package/Store/Diff/utils/routes.js +168 -0
- package/Store/Diff/utils/state.d.ts +56 -0
- package/Store/Diff/utils/state.js +155 -0
- package/Store/Store/store.d.ts +8 -0
- package/Store/Store/store.js +13 -0
- package/Store/Store/storeAsync.js +7 -6
- package/Store/Store/storeSync.js +3 -3
- package/Store/Tree/observableNode.js +18 -15
- package/Store/Tree/observableScope.d.ts +1 -0
- package/Store/Tree/observableScope.js +4 -0
- package/Store/Tree/snapshot.d.ts +19 -21
- package/Store/Tree/snapshot.js +29 -23
- package/Utils/json.d.ts +166 -26
- package/Utils/json.js +510 -183
- package/package.json +1 -1
- package/Store/Diff/utils/diffTree.utils.d.ts +0 -79
- package/Store/Diff/utils/diffTree.utils.js +0 -398
package/Utils/json.js
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
/** How an inserted splice item gets its value */
|
|
2
|
+
export var SPLICE_ACTION;
|
|
3
|
+
(function (SPLICE_ACTION) {
|
|
4
|
+
/** The element at `from` in the array before the splice is applied */
|
|
5
|
+
SPLICE_ACTION[SPLICE_ACTION["MOVE"] = 1] = "MOVE";
|
|
6
|
+
/** A new value */
|
|
7
|
+
SPLICE_ACTION[SPLICE_ACTION["INSERT"] = 2] = "INSERT";
|
|
8
|
+
})(SPLICE_ACTION || (SPLICE_ACTION = {}));
|
|
9
|
+
/** The default strategy: every changed object and array is diffed into, arrays by position */
|
|
10
|
+
export const JsonDiffDefaultStrategy = () => "diff";
|
|
11
|
+
/** The default identity: an element is its own identity, so elements are compared by === */
|
|
12
|
+
export const JsonDiffDefaultIdentity = (value) => value;
|
|
2
13
|
const jsonProto = Object.getPrototypeOf({});
|
|
3
14
|
const arrayProto = Object.getPrototypeOf([]);
|
|
4
15
|
const strProto = Object.getPrototypeOf("");
|
|
@@ -96,262 +107,578 @@ export function JsonDeepClone(value, type = JsonType(value)) {
|
|
|
96
107
|
}
|
|
97
108
|
}
|
|
98
109
|
/**
|
|
99
|
-
* Applies a diff result to a root value in place
|
|
100
|
-
*
|
|
101
|
-
*
|
|
110
|
+
* Applies a diff result to a root value in place, one record at a time in order. A diff doesn't own
|
|
111
|
+
* its values (see JsonDiff), so each value written or inserted is deep-cloned first: the root never
|
|
112
|
+
* shares an object with the diff, the value it was computed from, or another record, unless
|
|
113
|
+
* `clone` says otherwise.
|
|
114
|
+
* - A write assigns its value at its path
|
|
115
|
+
* - A delete removes the property its path ends with
|
|
116
|
+
* - A splice record splices the array at its path in place (see JsonApplySplices)
|
|
117
|
+
* @param root - The value to update; must be an object or array
|
|
118
|
+
* @param diff - The diff result to apply
|
|
102
119
|
* @param assign - Assigns a written value (already cloned) to its parent
|
|
103
|
-
* @param
|
|
104
|
-
*
|
|
120
|
+
* @param remove - Removes a deleted property from its parent
|
|
121
|
+
* @param spliced - Called with each array a splice record changed, after it's changed
|
|
122
|
+
* @param clone - Copies each value before it's written or inserted. A caller that already owns the
|
|
123
|
+
* diff's values can pass one that keeps them, or parts of them, as they are.
|
|
124
|
+
* @throws For a write or delete with an empty path: the root can't be replaced or removed, only
|
|
125
|
+
* changed inside. JsonDiff records one only when the root's JSON type changes.
|
|
105
126
|
*/
|
|
106
|
-
export function JsonApplyDiff(root, diff,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
127
|
+
export function JsonApplyDiff(root, diff, assign = DefaultAssign, remove = DefaultRemove, spliced, clone = JsonDeepClone) {
|
|
128
|
+
// The objects along the last path walked, so records under the same parent walk it once:
|
|
129
|
+
// walked[y] is [path[y - 1], the object at path[0..y)]
|
|
130
|
+
const walked = [["", root]];
|
|
131
|
+
for (let x = 0; x < diff.length; x++) {
|
|
132
|
+
const record = diff[x];
|
|
133
|
+
const path = record.path;
|
|
134
|
+
if (path.length === 0 && record.type !== "splice")
|
|
135
|
+
throw new Error("Unable to replace the root of a diff: apply it to an object holding the root");
|
|
136
|
+
if (record.type !== "splice") {
|
|
137
|
+
const parent = Walk(walked, path, path.length - 1);
|
|
138
|
+
const prop = path[path.length - 1];
|
|
139
|
+
record.type === "write" ? assign(parent, prop, clone(record.value)) : remove(parent, prop);
|
|
140
|
+
// The value at the path was replaced or removed, so anything walked through it is gone
|
|
141
|
+
walked.length = Math.min(walked.length, path.length);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
const array = Walk(walked, path, path.length);
|
|
145
|
+
if (!Array.isArray(array))
|
|
146
|
+
throw new Error(`Unable to splice ${path.join(".")}: not an array`);
|
|
147
|
+
JsonApplySplices(array, record.splices, clone);
|
|
148
|
+
// The array is the same object, but its elements moved
|
|
149
|
+
walked.length = Math.min(walked.length, path.length + 1);
|
|
150
|
+
spliced?.(array);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
117
153
|
}
|
|
118
154
|
function DefaultAssign(parent, prop, value) {
|
|
119
155
|
parent[prop] = value;
|
|
120
156
|
}
|
|
157
|
+
function DefaultRemove(parent, prop) {
|
|
158
|
+
delete parent[prop];
|
|
159
|
+
}
|
|
160
|
+
/** The object at `path[0..depth)`, walking from the deepest object in `walked` still on the path */
|
|
161
|
+
function Walk(walked, path, depth) {
|
|
162
|
+
for (let y = 0; y < depth; y++) {
|
|
163
|
+
const property = path[y];
|
|
164
|
+
const index = y + 1;
|
|
165
|
+
if (index < walked.length && walked[index][0] === property)
|
|
166
|
+
continue;
|
|
167
|
+
const child = walked[y][1][property];
|
|
168
|
+
if (index < walked.length) {
|
|
169
|
+
walked[index] = [property, child];
|
|
170
|
+
// Deeper entries were under the old object; clearing the next one makes the walk refresh each
|
|
171
|
+
if (index + 1 < walked.length)
|
|
172
|
+
walked[index + 1][0] = null;
|
|
173
|
+
}
|
|
174
|
+
else
|
|
175
|
+
walked.push([property, child]);
|
|
176
|
+
}
|
|
177
|
+
return walked[depth][1];
|
|
178
|
+
}
|
|
179
|
+
/** Sets an array's elements to `items` in place. Unlike `splice(0, length, ...items)`, any length works. */
|
|
180
|
+
function ReplaceArrayContents(array, items) {
|
|
181
|
+
for (let x = 0; x < items.length; x++)
|
|
182
|
+
array[x] = items[x];
|
|
183
|
+
array.length = items.length;
|
|
184
|
+
}
|
|
121
185
|
/**
|
|
122
|
-
* Applies
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* @param
|
|
128
|
-
* @
|
|
129
|
-
* @param callback - Called with the object written to, the property written ("" when an array
|
|
130
|
-
* root is replaced), and whether that property is new to the object
|
|
131
|
-
* @throws If a root replacement would change the root's JSON type, or the root is a primitive
|
|
186
|
+
* Applies splices to an array in place, in one pass however many there are: the new contents are
|
|
187
|
+
* built from the old ones, then written back into the same array. A moved item is the old element
|
|
188
|
+
* at its `from`, as is; an inserted item's value is deep-cloned.
|
|
189
|
+
* @param splices - In ascending, non-overlapping order, as JsonDiffArraySplice returns them: each
|
|
190
|
+
* one's index is at or after where the one before it ended
|
|
191
|
+
* @param clone - Copies each inserted value; see JsonApplyDiff
|
|
192
|
+
* @throws When a splice starts before the one before it ended
|
|
132
193
|
*/
|
|
133
|
-
export function
|
|
134
|
-
|
|
135
|
-
if (diff.length === 1 && diff[0].path.length === 0) {
|
|
136
|
-
const diffRow = diff[0];
|
|
137
|
-
const diffPatch = JsonDeepClone(diffRow.value);
|
|
138
|
-
const type = JsonType(root);
|
|
139
|
-
const patchType = JsonType(diffPatch);
|
|
140
|
-
if (type !== patchType)
|
|
141
|
-
throw new Error("Unable to change type of root value");
|
|
142
|
-
switch (type) {
|
|
143
|
-
case "array": {
|
|
144
|
-
root.splice(0, root.length, ...diffPatch);
|
|
145
|
-
break;
|
|
146
|
-
}
|
|
147
|
-
case "object": {
|
|
148
|
-
const keys = Object.keys(root);
|
|
149
|
-
const patchKeys = Object.keys(diffPatch);
|
|
150
|
-
for (let x = 0; x < keys.length; x++)
|
|
151
|
-
if (!patchKeys.includes(keys[x]))
|
|
152
|
-
delete root[keys[x]];
|
|
153
|
-
Object.assign(root, diffPatch);
|
|
154
|
-
break;
|
|
155
|
-
}
|
|
156
|
-
case "value":
|
|
157
|
-
throw new Error("Unable to replace root value type: " + root);
|
|
158
|
-
}
|
|
194
|
+
export function JsonApplySplices(array, splices, clone = JsonDeepClone) {
|
|
195
|
+
if (splices.length === 0)
|
|
159
196
|
return;
|
|
197
|
+
// `array` isn't changed until the end, so it's read as the original throughout
|
|
198
|
+
const result = [];
|
|
199
|
+
// The next old element to copy. The array as the splices so far left it is `result`, then the old
|
|
200
|
+
// elements from `read` on.
|
|
201
|
+
let read = 0;
|
|
202
|
+
for (let x = 0; x < splices.length; x++) {
|
|
203
|
+
const splice = splices[x];
|
|
204
|
+
if (splice.index < result.length)
|
|
205
|
+
throw new Error(`Unable to apply splice at ${splice.index}: splices must be in ascending, non-overlapping order`);
|
|
206
|
+
// Copy the old elements up to the splice; an index past the end splices at the end, as
|
|
207
|
+
// Array.prototype.splice does
|
|
208
|
+
for (; result.length < splice.index && read < array.length; read++)
|
|
209
|
+
result.push(array[read]);
|
|
210
|
+
read = Math.min(read + splice.delete, array.length);
|
|
211
|
+
for (let y = 0; y < splice.insert.length; y++) {
|
|
212
|
+
const item = splice.insert[y];
|
|
213
|
+
result.push(item.type === SPLICE_ACTION.MOVE ? array[item.from] : clone(item.value));
|
|
214
|
+
}
|
|
160
215
|
}
|
|
161
|
-
for (
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
216
|
+
for (; read < array.length; read++)
|
|
217
|
+
result.push(array[read]);
|
|
218
|
+
ReplaceArrayContents(array, result);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Whether two JSON values differ, without recording how: it stops at the first difference and
|
|
222
|
+
* skips any subtree both values share, so it costs no more than walking what differs. An
|
|
223
|
+
* undefined property counts as absent, as in JSON. Arrays are compared by position.
|
|
224
|
+
* @param newValue - The new value
|
|
225
|
+
* @param oldValue - The old value to compare against
|
|
226
|
+
* @returns true when the values differ
|
|
227
|
+
*/
|
|
228
|
+
export function JsonHasChanges(newValue, oldValue) {
|
|
229
|
+
if (newValue === oldValue)
|
|
230
|
+
return false;
|
|
231
|
+
const type = JsonType(newValue);
|
|
232
|
+
if (type !== JsonType(oldValue))
|
|
233
|
+
return true;
|
|
234
|
+
switch (type) {
|
|
235
|
+
case "array": {
|
|
236
|
+
const newArray = newValue;
|
|
237
|
+
const oldArray = oldValue;
|
|
238
|
+
if (newArray.length !== oldArray.length)
|
|
239
|
+
return true;
|
|
240
|
+
for (let x = 0; x < newArray.length; x++)
|
|
241
|
+
if (JsonHasChanges(newArray[x], oldArray[x]))
|
|
242
|
+
return true;
|
|
243
|
+
return false;
|
|
179
244
|
}
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
|
|
245
|
+
case "object": {
|
|
246
|
+
const newObject = newValue;
|
|
247
|
+
const oldObject = oldValue;
|
|
248
|
+
const newKeys = Object.keys(newObject);
|
|
249
|
+
let defined = 0;
|
|
250
|
+
for (let x = 0; x < newKeys.length; x++) {
|
|
251
|
+
const value = newObject[newKeys[x]];
|
|
252
|
+
if (value === undefined)
|
|
253
|
+
continue;
|
|
254
|
+
defined++;
|
|
255
|
+
if (!Object.hasOwn(oldObject, newKeys[x]) || JsonHasChanges(value, oldObject[newKeys[x]]))
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
// Every defined key of the new value is in the old one, so they differ only when the old
|
|
259
|
+
// one defines more
|
|
260
|
+
const oldKeys = Object.keys(oldObject);
|
|
261
|
+
if (oldKeys.length === defined)
|
|
262
|
+
return false;
|
|
263
|
+
let oldDefined = 0;
|
|
264
|
+
for (let x = 0; x < oldKeys.length; x++)
|
|
265
|
+
if (oldObject[oldKeys[x]] !== undefined)
|
|
266
|
+
oldDefined++;
|
|
267
|
+
return oldDefined !== defined;
|
|
184
268
|
}
|
|
269
|
+
default:
|
|
270
|
+
return true;
|
|
185
271
|
}
|
|
186
272
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
273
|
+
/**
|
|
274
|
+
* `newValue`, with every part that has no changes from the same place in `oldValue` (as
|
|
275
|
+
* JsonHasChanges compares them) replaced by `oldValue`'s object, so unchanged parts keep their
|
|
276
|
+
* identity. Neither value is changed: a container of `newValue` is copied only when one of its
|
|
277
|
+
* children is replaced, and kept as it is otherwise. Arrays are compared by position.
|
|
278
|
+
*
|
|
279
|
+
* old: { a: { x: 1 }, b: { y: 1 } }
|
|
280
|
+
* new: { a: { x: 1 }, b: { y: 2 } }
|
|
281
|
+
* result: { a: old.a, b: new.b }, a new object
|
|
282
|
+
*
|
|
283
|
+
* @param newValue - The new value
|
|
284
|
+
* @param oldValue - The value whose objects are kept where nothing changed
|
|
285
|
+
* @returns `oldValue` itself when nothing changed, otherwise `newValue` or a copy of it holding
|
|
286
|
+
* `oldValue`'s unchanged parts
|
|
287
|
+
*/
|
|
288
|
+
export function JsonMaintainUnchanged(newValue, oldValue) {
|
|
289
|
+
if (newValue === oldValue)
|
|
290
|
+
return newValue;
|
|
291
|
+
const type = JsonType(newValue);
|
|
292
|
+
if (type !== JsonType(oldValue))
|
|
293
|
+
return newValue;
|
|
294
|
+
switch (type) {
|
|
295
|
+
case "array": {
|
|
296
|
+
const newArray = newValue;
|
|
297
|
+
const oldArray = oldValue;
|
|
298
|
+
let copy;
|
|
299
|
+
let unchanged = newArray.length === oldArray.length;
|
|
300
|
+
for (let x = 0; x < newArray.length; x++) {
|
|
301
|
+
const value = x < oldArray.length ? JsonMaintainUnchanged(newArray[x], oldArray[x]) : newArray[x];
|
|
302
|
+
unchanged &&= value === oldArray[x];
|
|
303
|
+
if (value !== newArray[x])
|
|
304
|
+
(copy ??= newArray.slice())[x] = value;
|
|
305
|
+
}
|
|
306
|
+
return unchanged ? oldValue : (copy ?? newValue);
|
|
206
307
|
}
|
|
207
|
-
|
|
208
|
-
|
|
308
|
+
case "object": {
|
|
309
|
+
const newObject = newValue;
|
|
310
|
+
const oldObject = oldValue;
|
|
311
|
+
const newKeys = Object.keys(newObject);
|
|
312
|
+
let copy;
|
|
313
|
+
let unchanged = true;
|
|
314
|
+
let defined = 0;
|
|
315
|
+
for (let x = 0; x < newKeys.length; x++) {
|
|
316
|
+
const key = newKeys[x];
|
|
317
|
+
const newProp = newObject[key];
|
|
318
|
+
if (newProp === undefined)
|
|
319
|
+
continue;
|
|
320
|
+
defined++;
|
|
321
|
+
if (!Object.hasOwn(oldObject, key)) {
|
|
322
|
+
unchanged = false;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const value = JsonMaintainUnchanged(newProp, oldObject[key]);
|
|
326
|
+
unchanged &&= value === oldObject[key];
|
|
327
|
+
if (value !== newProp)
|
|
328
|
+
SetOwnProperty(copy ??= { ...newObject }, key, value);
|
|
329
|
+
}
|
|
330
|
+
// Every defined key of the new value matched, so the old one is kept unless it defines more
|
|
331
|
+
if (unchanged) {
|
|
332
|
+
const oldKeys = Object.keys(oldObject);
|
|
333
|
+
let oldDefined = 0;
|
|
334
|
+
for (let x = 0; x < oldKeys.length && oldDefined <= defined; x++)
|
|
335
|
+
if (oldObject[oldKeys[x]] !== undefined)
|
|
336
|
+
oldDefined++;
|
|
337
|
+
if (oldDefined === defined)
|
|
338
|
+
return oldValue;
|
|
339
|
+
}
|
|
340
|
+
return (copy ?? newValue);
|
|
341
|
+
}
|
|
342
|
+
default:
|
|
343
|
+
return newValue;
|
|
209
344
|
}
|
|
210
345
|
}
|
|
346
|
+
/** Sets a key as an own property, even `__proto__`, which plain assignment would make the prototype */
|
|
347
|
+
export function SetOwnProperty(target, key, value) {
|
|
348
|
+
if (key === "__proto__")
|
|
349
|
+
Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
|
|
350
|
+
else
|
|
351
|
+
target[key] = value;
|
|
352
|
+
}
|
|
211
353
|
/**
|
|
212
354
|
* Computes the differences between two JSON values.
|
|
213
|
-
* Returns an array of paths and values that changed from oldValue to newValue. A
|
|
355
|
+
* Returns an array of paths and values that changed from oldValue to newValue. A value is only
|
|
356
|
+
* written whole where its JSON type changes (or a strategy says so); an object or array of the
|
|
357
|
+
* same type is always diffed into, however much of it changed. So the root itself is only ever
|
|
358
|
+
* written when its type changes, and a diff of values of the same type can always be applied
|
|
359
|
+
* in place. A record doesn't
|
|
214
360
|
* own its value: it references `newValue`'s objects, so two records can share one, and
|
|
215
361
|
* JsonApplyDiff clones each value as it's applied.
|
|
216
362
|
* @template T - The type of the JSON values being compared
|
|
217
363
|
* @param newValue - The new value
|
|
218
364
|
* @param oldValue - The old value to compare against
|
|
219
|
-
* @param rootPath - Optional dot-separated string for the base path
|
|
220
|
-
* @param
|
|
365
|
+
* @param rootPath - Optional dot-separated string for the base path
|
|
366
|
+
* @param strategy - Optional choice of how each changed object or array is diffed; without one,
|
|
367
|
+
* everything is diffed by walking into it, arrays by position
|
|
368
|
+
* @param identity - Optional identity a spliced array's elements are compared by; without one,
|
|
369
|
+
* elements are compared by ===. See JsonDiffIdentityFunc
|
|
221
370
|
* @returns Array of change records with path and new value
|
|
222
371
|
*/
|
|
223
|
-
export function JsonDiff(newValue, oldValue, rootPath,
|
|
224
|
-
const result =
|
|
225
|
-
|
|
226
|
-
JsonDiffRecursive(startPath, newValue, oldValue, result);
|
|
227
|
-
result.sort(function (a, b) {
|
|
228
|
-
const aNum = a.type === "write" ? 1 : 0;
|
|
229
|
-
const bNum = b.type === "write" ? 1 : 0;
|
|
230
|
-
return aNum - bNum;
|
|
231
|
-
});
|
|
372
|
+
export function JsonDiff(newValue, oldValue, rootPath, strategy, identity) {
|
|
373
|
+
const result = [];
|
|
374
|
+
JsonDiffInto(newValue, oldValue, rootPath ? rootPath.split(".") : [], result, strategy, identity);
|
|
232
375
|
return result;
|
|
233
376
|
}
|
|
234
377
|
/**
|
|
235
|
-
*
|
|
378
|
+
* Appends the differences between two JSON values to `result`, so several values can be diffed into
|
|
379
|
+
* one result.
|
|
380
|
+
* @param path - The path the values are at
|
|
381
|
+
* @param strategy - See JsonDiff
|
|
382
|
+
* @param identity - See JsonDiff
|
|
383
|
+
* @returns The result
|
|
384
|
+
*/
|
|
385
|
+
export function JsonDiffInto(newValue, oldValue, path, result, strategy = JsonDiffDefaultStrategy, identity = JsonDiffDefaultIdentity) {
|
|
386
|
+
JsonDiffRecursive(path, newValue, oldValue, result, { strategy, identity });
|
|
387
|
+
return result;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Recursive helper for JsonDiff that traverses and compares values. Values of the same JSON type
|
|
391
|
+
* are diffed into; anything else is written.
|
|
236
392
|
* @internal
|
|
237
393
|
* @param path - Current path being evaluated
|
|
238
394
|
* @param newValue - New value at this path
|
|
239
395
|
* @param oldValue - Old value at this path
|
|
240
396
|
* @param resp - Result array to populate with changes
|
|
241
|
-
* @
|
|
397
|
+
* @param options - The diff's strategy and identity, see JsonDiff
|
|
242
398
|
*/
|
|
243
|
-
function JsonDiffRecursive(path, newValue, oldValue, resp) {
|
|
399
|
+
function JsonDiffRecursive(path, newValue, oldValue, resp, options) {
|
|
244
400
|
if (newValue === oldValue)
|
|
245
|
-
return
|
|
401
|
+
return;
|
|
246
402
|
const newType = JsonType(newValue);
|
|
247
403
|
const oldType = JsonType(oldValue);
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
404
|
+
const choice = newType !== "value" ? options.strategy(newValue, oldValue, path) : undefined;
|
|
405
|
+
if (choice === "skip")
|
|
406
|
+
return;
|
|
407
|
+
if (newType === oldType && choice !== "write")
|
|
251
408
|
switch (newType) {
|
|
252
409
|
case "array": {
|
|
253
|
-
|
|
254
|
-
|
|
410
|
+
if (choice === "splice") {
|
|
411
|
+
const splices = JsonDiffArraySplice(newValue, oldValue, options.identity);
|
|
412
|
+
splices.length > 0 && resp.push({ type: "splice", path, splices });
|
|
413
|
+
}
|
|
414
|
+
else
|
|
415
|
+
JsonDiffArrays(path, newValue, oldValue, resp, options);
|
|
416
|
+
return;
|
|
255
417
|
}
|
|
256
418
|
case "object": {
|
|
257
|
-
|
|
258
|
-
|
|
419
|
+
JsonDiffObjects(path, newValue, oldValue, resp, options);
|
|
420
|
+
return;
|
|
259
421
|
}
|
|
260
422
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
423
|
+
resp.push({
|
|
424
|
+
type: "write",
|
|
425
|
+
path,
|
|
426
|
+
value: newValue,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* The splices that turn `oldValue` into `newValue`, matching elements by identity without walking
|
|
431
|
+
* into them: two elements are the same when their identities are ===. With the default identity,
|
|
432
|
+
* that's the elements themselves. A matched element is never diffed into, even when it isn't === to
|
|
433
|
+
* its match. An identity that repeats is matched in order, and a repeat with no old element left to
|
|
434
|
+
* match is inserted.
|
|
435
|
+
*
|
|
436
|
+
* It works in four stages, followed through this example in the comments below:
|
|
437
|
+
*
|
|
438
|
+
* old: [A, B, C, D, E, F]
|
|
439
|
+
* new: [A, E, B, X, D, F]
|
|
440
|
+
*
|
|
441
|
+
* 1. Trim the elements identical at the start and end (A and F): they can't need a change.
|
|
442
|
+
* 2. Match each remaining new element to the old index holding the same element, or -1.
|
|
443
|
+
* 3. Choose which matched elements stay in place: the longest run of them still in their old
|
|
444
|
+
* order (B, D). Keeping the longest run is what keeps the number of moves as small as possible.
|
|
445
|
+
* 4. Walk both arrays from kept element to kept element. Between two kept elements, delete the
|
|
446
|
+
* old elements that aren't kept (C, E) and insert the new ones that aren't (E as a move, X as
|
|
447
|
+
* a new value).
|
|
448
|
+
*
|
|
449
|
+
* @returns The splices, in the order to apply them; none when the arrays hold the same elements.
|
|
450
|
+
* Applied in order to [A, B, C, D, E, F], with each move read from the array before any splice:
|
|
451
|
+
*
|
|
452
|
+
* splice(1, 0, E) [A, E, B, C, D, E, F]
|
|
453
|
+
* splice(3, 1, X) [A, E, B, X, D, E, F]
|
|
454
|
+
* splice(5, 1) [A, E, B, X, D, F]
|
|
455
|
+
*
|
|
456
|
+
* @param identity - What elements are compared by; see JsonDiffIdentityFunc. Elements that are
|
|
457
|
+
* === are the same element without calling it, and it's called at most a few times for each
|
|
458
|
+
* other element, so it must give the same identity for the same value.
|
|
459
|
+
*/
|
|
460
|
+
export function JsonDiffArraySplice(newValue, oldValue, identity = JsonDiffDefaultIdentity) {
|
|
461
|
+
const newLength = newValue.length;
|
|
462
|
+
const oldLength = oldValue.length;
|
|
463
|
+
// Every comparison below is between identities. Inserted values are still read from newValue.
|
|
464
|
+
const same = (a, b) => a === b || identity(a) === identity(b);
|
|
465
|
+
// Stage 1: trim the elements with the same identity at the start and end. An append, a delete or a single
|
|
466
|
+
// replaced element leaves little or nothing in the middle, so the stages after this cost little.
|
|
467
|
+
//
|
|
468
|
+
// start: A === A, B !== E → start = 1
|
|
469
|
+
// end: F === F, E !== D → newEnd = oldEnd = 5
|
|
470
|
+
// middle old (indexes 1-4): [B, C, D, E]
|
|
471
|
+
// middle new (indexes 1-4): [E, B, X, D]
|
|
472
|
+
const shared = Math.min(newLength, oldLength);
|
|
473
|
+
let start = 0;
|
|
474
|
+
for (; start < shared && same(newValue[start], oldValue[start]); start++) { }
|
|
475
|
+
if (start === newLength && start === oldLength)
|
|
476
|
+
return [];
|
|
477
|
+
let newEnd = newLength;
|
|
478
|
+
let oldEnd = oldLength;
|
|
479
|
+
for (; newEnd > start && oldEnd > start && same(newValue[newEnd - 1], oldValue[oldEnd - 1]); newEnd--, oldEnd--) { }
|
|
480
|
+
// Stage 2: match each new element in the middle to an old index holding an element with the same
|
|
481
|
+
// identity.
|
|
482
|
+
//
|
|
483
|
+
// oldIndexes maps each old identity to every index it's at. The lists are filled from the end, so
|
|
484
|
+
// each holds its indexes highest first and pop() hands them out lowest first: the first copy of a
|
|
485
|
+
// repeated value in the new array matches its first copy in the old array, and so on. Once a
|
|
486
|
+
// value's indexes run out, any further copy has no match.
|
|
487
|
+
//
|
|
488
|
+
// oldIndexes: B → [1], C → [2], D → [3], E → [4]
|
|
489
|
+
const oldIndexes = new Map();
|
|
490
|
+
for (let x = oldEnd - 1; x >= start; x--) {
|
|
491
|
+
const id = identity(oldValue[x]);
|
|
492
|
+
const indexes = oldIndexes.get(id);
|
|
493
|
+
indexes ? indexes.push(x) : oldIndexes.set(id, [x]);
|
|
494
|
+
}
|
|
495
|
+
// matched[x] is the old index the new element at start + x matched, or -1 when it has none.
|
|
496
|
+
//
|
|
497
|
+
// new: E B X D
|
|
498
|
+
// matched: 4 1 -1 3
|
|
499
|
+
const middle = newEnd - start;
|
|
500
|
+
const matched = new Int32Array(middle);
|
|
501
|
+
for (let x = 0; x < middle; x++)
|
|
502
|
+
matched[x] = oldIndexes.get(identity(newValue[start + x]))?.pop() ?? -1;
|
|
503
|
+
// Stage 3: choose which matched elements stay in place. They're the longest run whose old
|
|
504
|
+
// indexes increase from left to right, because only those can keep their order without moving.
|
|
505
|
+
//
|
|
506
|
+
// matched: 4 1 -1 3
|
|
507
|
+
// longest increasing run: 1, 3 → B and D are kept
|
|
508
|
+
// E (4) is matched but out of order → it will be moved
|
|
509
|
+
// X (-1) has no match → it will be inserted
|
|
510
|
+
//
|
|
511
|
+
// keptNew is indexed by position in the new middle; keptOld marks the same kept elements by
|
|
512
|
+
// their position in the old middle.
|
|
513
|
+
//
|
|
514
|
+
// keptNew (E, B, X, D): 0 1 0 1
|
|
515
|
+
// keptOld (B, C, D, E): 1 0 1 0 → C and E aren't kept, so they will be deleted
|
|
516
|
+
const keptNew = KeptMatches(matched);
|
|
517
|
+
const keptOld = new Uint8Array(oldEnd - start);
|
|
518
|
+
for (let x = 0; x < middle; x++)
|
|
519
|
+
if (keptNew[x])
|
|
520
|
+
keptOld[matched[x] - start] = 1;
|
|
521
|
+
// Stage 4: walk both arrays at once. The kept elements appear in the same order in both, so after
|
|
522
|
+
// skipping the elements that aren't kept, both cursors stop at the same kept element (or both
|
|
523
|
+
// reach the end). Each stop emits one splice:
|
|
524
|
+
// - delete: the old elements skipped, which aren't kept
|
|
525
|
+
// - insert: the new elements skipped, as a move when they were matched, otherwise as a new value
|
|
526
|
+
//
|
|
527
|
+
// A splice's index is the new cursor's position when it starts. The splices before it have already
|
|
528
|
+
// made the array match the new array up to that position, so that's where this one goes.
|
|
529
|
+
//
|
|
530
|
+
// index 1: old skips nothing before B; new skips E (matched 4) → { 1, delete 0, [MOVE 4] }
|
|
531
|
+
// index 3: old skips C before D; new skips X (no match) → { 3, delete 1, [INSERT X] }
|
|
532
|
+
// index 5: old skips E to the end; new has nothing left → { 5, delete 1, [] }
|
|
533
|
+
//
|
|
534
|
+
// The last stop is what deletes old elements left over at the end of the array.
|
|
535
|
+
const splices = [];
|
|
536
|
+
let oldIndex = start;
|
|
537
|
+
let newIndex = start;
|
|
538
|
+
while (oldIndex < oldEnd || newIndex < newEnd) {
|
|
539
|
+
const index = newIndex;
|
|
540
|
+
let deleteCount = 0;
|
|
541
|
+
for (; oldIndex < oldEnd && !keptOld[oldIndex - start]; oldIndex++)
|
|
542
|
+
deleteCount++;
|
|
543
|
+
const insert = [];
|
|
544
|
+
for (; newIndex < newEnd && !keptNew[newIndex - start]; newIndex++) {
|
|
545
|
+
const from = matched[newIndex - start];
|
|
546
|
+
insert.push(from < 0 ? { type: SPLICE_ACTION.INSERT, value: newValue[newIndex] } : { type: SPLICE_ACTION.MOVE, from });
|
|
547
|
+
}
|
|
548
|
+
// Two kept elements next to each other in both arrays leave nothing to splice between them
|
|
549
|
+
if (deleteCount > 0 || insert.length > 0)
|
|
550
|
+
splices.push({ index, delete: deleteCount, insert });
|
|
551
|
+
// Step past the kept element both cursors stopped at. At the end, this takes both past the end
|
|
552
|
+
// and the loop stops.
|
|
553
|
+
oldIndex++;
|
|
554
|
+
newIndex++;
|
|
555
|
+
}
|
|
556
|
+
return splices;
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Which matched elements to keep in place: the longest run of them, from left to right, whose old
|
|
560
|
+
* indexes increase. That's the longest increasing subsequence of `matched`, ignoring the -1s, and
|
|
561
|
+
* every matched element left out of it has to move. O(n log n).
|
|
562
|
+
*
|
|
563
|
+
* For example, matched [4, 1, -1, 3] keeps the elements at 1 and 3 (old indexes 1 and 3), and a
|
|
564
|
+
* rotate, matched [4, 0, 1, 2, 3], keeps everything but the first, so only that one moves.
|
|
565
|
+
*
|
|
566
|
+
* @param matched - Each new element's matched old index, or -1 when it has none
|
|
567
|
+
* @returns 1 for each element kept, by position in `matched`
|
|
568
|
+
*/
|
|
569
|
+
function KeptMatches(matched) {
|
|
570
|
+
const length = matched.length;
|
|
571
|
+
// previous[x]: the element before x in the longest run found that ends at x, or -1. Following it
|
|
572
|
+
// back from the end of the longest run gives the whole run.
|
|
573
|
+
const previous = new Int32Array(length);
|
|
574
|
+
// tails[k]: of every increasing run of length k + 1 found so far, the element ending the one with
|
|
575
|
+
// the smallest old index. A smaller ending is better, since more elements can extend it. Their old
|
|
576
|
+
// indexes always increase along tails, which is what lets the search below be a binary search.
|
|
577
|
+
const tails = [];
|
|
578
|
+
for (let x = 0; x < length; x++) {
|
|
579
|
+
const value = matched[x];
|
|
580
|
+
// An unmatched element can't be kept
|
|
581
|
+
if (value < 0)
|
|
582
|
+
continue;
|
|
583
|
+
// Find the first run in tails whose ending old index isn't below this element's. This element
|
|
584
|
+
// can extend the run before it (every ending before it is lower), so it ends a run of length
|
|
585
|
+
// low + 1: either a better ending for that length, or, past the end of tails, a new longest run.
|
|
586
|
+
let low = 0;
|
|
587
|
+
let high = tails.length;
|
|
588
|
+
while (low < high) {
|
|
589
|
+
const mid = (low + high) >> 1;
|
|
590
|
+
if (matched[tails[mid]] < value)
|
|
591
|
+
low = mid + 1;
|
|
592
|
+
else
|
|
593
|
+
high = mid;
|
|
594
|
+
}
|
|
595
|
+
previous[x] = low > 0 ? tails[low - 1] : -1;
|
|
596
|
+
tails[low] = x;
|
|
269
597
|
}
|
|
270
|
-
|
|
598
|
+
// The last entry of tails ends a longest run; follow previous back from it to mark the whole run
|
|
599
|
+
//
|
|
600
|
+
// matched [4, 1, -1, 3]:
|
|
601
|
+
// x=0 (4): tails [0] (run: 4)
|
|
602
|
+
// x=1 (1): tails [1] (1 is a better ending than 4 for length 1)
|
|
603
|
+
// x=3 (3): tails [1, 3] (3 extends the run ending at 1; previous[3] = 1)
|
|
604
|
+
// back from 3 → 1 → done: kept = [0, 1, 0, 1]
|
|
605
|
+
const kept = new Uint8Array(length);
|
|
606
|
+
for (let x = tails.length > 0 ? tails[tails.length - 1] : -1; x >= 0; x = previous[x])
|
|
607
|
+
kept[x] = 1;
|
|
608
|
+
return kept;
|
|
271
609
|
}
|
|
272
|
-
const MOVE_SYMBOL = Symbol("MOVE_SYMBOL");
|
|
273
610
|
/**
|
|
274
|
-
* Compares two arrays and records differences
|
|
611
|
+
* Compares two arrays by position and records differences: each changed position, then the
|
|
612
|
+
* length when it changed. Elements identical at the start, and at the end when the lengths are the
|
|
613
|
+
* same, aren't diffed.
|
|
275
614
|
* @internal
|
|
276
615
|
* @param path - Current path being evaluated
|
|
277
616
|
* @param newValue - New array
|
|
278
617
|
* @param oldValue - Old array to compare against
|
|
279
618
|
* @param resp - Result array to populate with changes
|
|
280
|
-
* @
|
|
619
|
+
* @param options - The diff's strategy and identity, see JsonDiff
|
|
281
620
|
*/
|
|
282
|
-
function JsonDiffArrays(path, newValue, oldValue, resp) {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
for (; startIndex < newValue.length && startIndex < oldValue.length && newValue[startIndex] === oldValue[startIndex]; startIndex++) { }
|
|
621
|
+
function JsonDiffArrays(path, newValue, oldValue, resp, options) {
|
|
622
|
+
const sameLength = newValue.length === oldValue.length;
|
|
623
|
+
const shared = Math.min(newValue.length, oldValue.length);
|
|
624
|
+
let start = 0;
|
|
625
|
+
for (; start < shared && newValue[start] === oldValue[start]; start++) { }
|
|
288
626
|
// Arrays are equal, no diffing required
|
|
289
|
-
if (
|
|
290
|
-
return
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
resp.push({
|
|
299
|
-
type: "move",
|
|
300
|
-
path: path,
|
|
301
|
-
sourceProp: x,
|
|
302
|
-
targetProp: newIndex
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
for (let x = startIndex; x < newValue.length; x++) {
|
|
307
|
-
const arrayPath = path.concat(x);
|
|
308
|
-
if (newArraySlice[x] !== MOVE_SYMBOL)
|
|
309
|
-
JsonDiffRecursive(arrayPath, newArraySlice[x], oldArraySlice[x], resp);
|
|
310
|
-
}
|
|
311
|
-
if (oldValue.length !== newValue.length)
|
|
627
|
+
if (sameLength && start === newValue.length)
|
|
628
|
+
return;
|
|
629
|
+
let end = newValue.length;
|
|
630
|
+
if (sameLength)
|
|
631
|
+
for (; end > start && newValue[end - 1] === oldValue[end - 1]; end--) { }
|
|
632
|
+
for (let x = start; x < end; x++)
|
|
633
|
+
// Checked before the path is built: most elements of a diffed array are often unchanged
|
|
634
|
+
newValue[x] !== oldValue[x] && JsonDiffRecursive(path.concat(x), newValue[x], oldValue[x], resp, options);
|
|
635
|
+
if (!sameLength)
|
|
312
636
|
resp.push({
|
|
313
637
|
type: "write",
|
|
314
638
|
path: path.concat("length"),
|
|
315
639
|
value: newValue.length,
|
|
316
640
|
});
|
|
317
|
-
return false;
|
|
318
641
|
}
|
|
319
642
|
/**
|
|
320
|
-
* Compares two objects and records differences
|
|
321
|
-
*
|
|
643
|
+
* Compares two objects and records differences, walking their sorted keys together: a key in
|
|
644
|
+
* both is diffed, a key only the new value has is written, and a key only the old value has is
|
|
645
|
+
* deleted. A new key holding undefined counts as absent and isn't written.
|
|
322
646
|
* @internal
|
|
323
647
|
* @param path - Current path being evaluated
|
|
324
648
|
* @param newValue - New object
|
|
325
649
|
* @param oldValue - Old object to compare against
|
|
326
650
|
* @param resp - Result array to populate with changes
|
|
327
|
-
* @
|
|
651
|
+
* @param options - The diff's strategy and identity, see JsonDiff
|
|
328
652
|
*/
|
|
329
|
-
function JsonDiffObjects(path, newValue, oldValue, resp) {
|
|
653
|
+
function JsonDiffObjects(path, newValue, oldValue, resp, options) {
|
|
330
654
|
const newKeys = Object.keys(newValue).sort();
|
|
331
655
|
const oldKeys = Object.keys(oldValue).sort();
|
|
332
|
-
if (newKeys.length === 0 && oldKeys.length === 0)
|
|
333
|
-
return false;
|
|
334
|
-
if (newKeys.length < oldKeys.length)
|
|
335
|
-
return true;
|
|
336
656
|
let newKeyIndex = 0;
|
|
337
657
|
let oldKeyIndex = 0;
|
|
338
|
-
while (newKeyIndex < newKeys.length) {
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
658
|
+
while (newKeyIndex < newKeys.length || oldKeyIndex < oldKeys.length) {
|
|
659
|
+
const newKey = newKeys[newKeyIndex];
|
|
660
|
+
const oldKey = oldKeys[oldKeyIndex];
|
|
661
|
+
// Both lists are sorted the same way `<` compares strings
|
|
662
|
+
if (oldKeyIndex >= oldKeys.length || (newKeyIndex < newKeys.length && newKey < oldKey)) {
|
|
663
|
+
newValue[newKey] !== undefined && resp.push({
|
|
664
|
+
type: "write",
|
|
665
|
+
path: path.concat(newKey),
|
|
666
|
+
value: newValue[newKey],
|
|
667
|
+
});
|
|
668
|
+
newKeyIndex++;
|
|
344
669
|
}
|
|
345
|
-
else if (
|
|
670
|
+
else if (newKeyIndex >= newKeys.length || oldKey < newKey) {
|
|
346
671
|
resp.push({
|
|
347
|
-
type: "
|
|
348
|
-
path:
|
|
349
|
-
value: newValue[newKeys[newKeyIndex]],
|
|
672
|
+
type: "delete",
|
|
673
|
+
path: path.concat(oldKey),
|
|
350
674
|
});
|
|
675
|
+
oldKeyIndex++;
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
// Checked before the path is built: most properties of a diffed object are often unchanged
|
|
679
|
+
newValue[newKey] !== oldValue[oldKey] && JsonDiffRecursive(path.concat(newKey), newValue[newKey], oldValue[oldKey], resp, options);
|
|
680
|
+
newKeyIndex++;
|
|
681
|
+
oldKeyIndex++;
|
|
351
682
|
}
|
|
352
|
-
newKeyIndex++;
|
|
353
683
|
}
|
|
354
|
-
if (oldKeyIndex < oldKeys.length)
|
|
355
|
-
return true;
|
|
356
|
-
return false;
|
|
357
684
|
}
|