json-patch-to-crdt 0.2.0 → 0.3.0
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 +22 -4
- package/dist/{compact-BE9UsxEo.mjs → compact-BS7F604m.mjs} +1008 -163
- package/dist/{compact-DrmgKiVW.js → compact-BToZE6Q6.js} +1023 -160
- package/dist/{depth-Cd3nyHWy.d.mts → depth-BTHjgY18.d.mts} +24 -3
- package/dist/{depth-tcJ8L1dj.d.ts → depth-DSl2ghKu.d.ts} +24 -3
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/internals.d.mts +31 -4
- package/dist/internals.d.ts +31 -4
- package/dist/internals.js +4 -1
- package/dist/internals.mjs +2 -2
- package/package.json +4 -1
|
@@ -133,12 +133,31 @@ function dotToElemId(d) {
|
|
|
133
133
|
const HEAD = "HEAD";
|
|
134
134
|
const linearCache = /* @__PURE__ */ new WeakMap();
|
|
135
135
|
const seqVersion = /* @__PURE__ */ new WeakMap();
|
|
136
|
+
const maxSiblingInsDotByPrevCache = /* @__PURE__ */ new WeakMap();
|
|
136
137
|
function getVersion(seq) {
|
|
137
138
|
return seqVersion.get(seq) ?? 0;
|
|
138
139
|
}
|
|
139
140
|
function bumpVersion(seq) {
|
|
140
141
|
seqVersion.set(seq, getVersion(seq) + 1);
|
|
141
142
|
}
|
|
143
|
+
function buildMaxSiblingInsDotByPrevIndex(seq) {
|
|
144
|
+
const index = /* @__PURE__ */ new Map();
|
|
145
|
+
for (const elem of seq.elems.values()) {
|
|
146
|
+
const current = index.get(elem.prev);
|
|
147
|
+
if (!current || compareDot(elem.insDot, current) > 0) index.set(elem.prev, elem.insDot);
|
|
148
|
+
}
|
|
149
|
+
maxSiblingInsDotByPrevCache.set(seq, index);
|
|
150
|
+
return index;
|
|
151
|
+
}
|
|
152
|
+
function getMaxSiblingInsDotByPrevIndex(seq) {
|
|
153
|
+
return maxSiblingInsDotByPrevCache.get(seq) ?? buildMaxSiblingInsDotByPrevIndex(seq);
|
|
154
|
+
}
|
|
155
|
+
function trackInsertedSiblingDot(seq, prev, insDot) {
|
|
156
|
+
const index = maxSiblingInsDotByPrevCache.get(seq);
|
|
157
|
+
if (!index) return;
|
|
158
|
+
const current = index.get(prev);
|
|
159
|
+
if (!current || compareDot(insDot, current) > 0) index.set(prev, insDot);
|
|
160
|
+
}
|
|
142
161
|
function rgaChildrenIndex(seq) {
|
|
143
162
|
const idx = /* @__PURE__ */ new Map();
|
|
144
163
|
for (const e of seq.elems.values()) {
|
|
@@ -187,6 +206,30 @@ function rgaLinearizeIds(seq) {
|
|
|
187
206
|
});
|
|
188
207
|
return [...out];
|
|
189
208
|
}
|
|
209
|
+
function rgaCreateIndexedIdSnapshot(seq) {
|
|
210
|
+
const ids = rgaLinearizeIds(seq);
|
|
211
|
+
return {
|
|
212
|
+
length() {
|
|
213
|
+
return ids.length;
|
|
214
|
+
},
|
|
215
|
+
idAt(index) {
|
|
216
|
+
return ids[index];
|
|
217
|
+
},
|
|
218
|
+
prevForInsertAt(index) {
|
|
219
|
+
if (index <= 0) return HEAD;
|
|
220
|
+
return ids[index - 1] ?? (ids.length > 0 ? ids[ids.length - 1] : HEAD);
|
|
221
|
+
},
|
|
222
|
+
insertAt(index, id) {
|
|
223
|
+
const at = Math.max(0, Math.min(index, ids.length));
|
|
224
|
+
ids.splice(at, 0, id);
|
|
225
|
+
},
|
|
226
|
+
deleteAt(index) {
|
|
227
|
+
if (index < 0 || index >= ids.length) return;
|
|
228
|
+
const [removed] = ids.splice(index, 1);
|
|
229
|
+
return removed;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
190
233
|
function rgaInsertAfter(seq, prev, id, insDot, value) {
|
|
191
234
|
if (seq.elems.has(id)) return;
|
|
192
235
|
seq.elems.set(id, {
|
|
@@ -196,15 +239,110 @@ function rgaInsertAfter(seq, prev, id, insDot, value) {
|
|
|
196
239
|
value,
|
|
197
240
|
insDot
|
|
198
241
|
});
|
|
242
|
+
trackInsertedSiblingDot(seq, prev, insDot);
|
|
199
243
|
bumpVersion(seq);
|
|
200
244
|
}
|
|
201
|
-
function
|
|
245
|
+
function rgaInsertAfterChecked(seq, prev, id, insDot, value) {
|
|
246
|
+
if (seq.elems.has(id)) return;
|
|
247
|
+
if (prev !== HEAD && !seq.elems.has(prev)) throw new Error(`RGA predecessor '${prev}' does not exist`);
|
|
248
|
+
rgaInsertAfter(seq, prev, id, insDot, value);
|
|
249
|
+
}
|
|
250
|
+
function rgaDelete(seq, id, delDot) {
|
|
202
251
|
const e = seq.elems.get(id);
|
|
203
252
|
if (!e) return;
|
|
204
|
-
if (e.tombstone)
|
|
253
|
+
if (e.tombstone) {
|
|
254
|
+
if (delDot && (!e.delDot || compareDot(delDot, e.delDot) > 0)) {
|
|
255
|
+
e.delDot = {
|
|
256
|
+
actor: delDot.actor,
|
|
257
|
+
ctr: delDot.ctr
|
|
258
|
+
};
|
|
259
|
+
bumpVersion(seq);
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
205
263
|
e.tombstone = true;
|
|
264
|
+
if (delDot) e.delDot = {
|
|
265
|
+
actor: delDot.actor,
|
|
266
|
+
ctr: delDot.ctr
|
|
267
|
+
};
|
|
206
268
|
bumpVersion(seq);
|
|
207
269
|
}
|
|
270
|
+
function validateRgaSeq(seq) {
|
|
271
|
+
const issues = [];
|
|
272
|
+
for (const elem of seq.elems.values()) if (elem.prev !== HEAD && !seq.elems.has(elem.prev)) issues.push({
|
|
273
|
+
code: "MISSING_PREDECESSOR",
|
|
274
|
+
id: elem.id,
|
|
275
|
+
prev: elem.prev,
|
|
276
|
+
message: `RGA element '${elem.id}' references missing predecessor '${elem.prev}'`
|
|
277
|
+
});
|
|
278
|
+
const cycleIds = /* @__PURE__ */ new Set();
|
|
279
|
+
const visitState = /* @__PURE__ */ new Map();
|
|
280
|
+
const sortedIds = [...seq.elems.keys()].sort();
|
|
281
|
+
for (const startId of sortedIds) {
|
|
282
|
+
if (visitState.get(startId) === 2) continue;
|
|
283
|
+
const trail = [];
|
|
284
|
+
const trailIndex = /* @__PURE__ */ new Map();
|
|
285
|
+
let currentId = startId;
|
|
286
|
+
while (currentId !== void 0) {
|
|
287
|
+
const seenAt = trailIndex.get(currentId);
|
|
288
|
+
if (seenAt !== void 0) {
|
|
289
|
+
for (let i = seenAt; i < trail.length; i++) cycleIds.add(trail[i]);
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
if (visitState.get(currentId) === 2) break;
|
|
293
|
+
const elem = seq.elems.get(currentId);
|
|
294
|
+
if (!elem) break;
|
|
295
|
+
trailIndex.set(currentId, trail.length);
|
|
296
|
+
trail.push(currentId);
|
|
297
|
+
if (elem.prev === HEAD) break;
|
|
298
|
+
currentId = elem.prev;
|
|
299
|
+
}
|
|
300
|
+
for (const id of trail) visitState.set(id, 2);
|
|
301
|
+
}
|
|
302
|
+
for (const id of [...cycleIds].sort()) {
|
|
303
|
+
const elem = seq.elems.get(id);
|
|
304
|
+
issues.push({
|
|
305
|
+
code: "PREDECESSOR_CYCLE",
|
|
306
|
+
id,
|
|
307
|
+
prev: elem.prev,
|
|
308
|
+
message: `RGA predecessor cycle detected at '${id}'`
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
const children = rgaChildrenIndex(seq);
|
|
312
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
313
|
+
const stack = [...children.get(HEAD) ?? []];
|
|
314
|
+
while (stack.length > 0) {
|
|
315
|
+
const elem = stack.pop();
|
|
316
|
+
if (reachable.has(elem.id)) continue;
|
|
317
|
+
reachable.add(elem.id);
|
|
318
|
+
const descendants = children.get(elem.id);
|
|
319
|
+
if (descendants) stack.push(...descendants);
|
|
320
|
+
}
|
|
321
|
+
for (const id of sortedIds) {
|
|
322
|
+
if (reachable.has(id)) continue;
|
|
323
|
+
const elem = seq.elems.get(id);
|
|
324
|
+
issues.push({
|
|
325
|
+
code: "ORPHANED_ELEMENT",
|
|
326
|
+
id,
|
|
327
|
+
prev: elem.prev,
|
|
328
|
+
message: `RGA element '${id}' is unreachable from HEAD`
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
if (issues.length === 0) return {
|
|
332
|
+
ok: true,
|
|
333
|
+
issues: []
|
|
334
|
+
};
|
|
335
|
+
const issueOrder = {
|
|
336
|
+
MISSING_PREDECESSOR: 0,
|
|
337
|
+
PREDECESSOR_CYCLE: 1,
|
|
338
|
+
ORPHANED_ELEMENT: 2
|
|
339
|
+
};
|
|
340
|
+
issues.sort((a, b) => a.id.localeCompare(b.id) || issueOrder[a.code] - issueOrder[b.code] || a.prev.localeCompare(b.prev));
|
|
341
|
+
return {
|
|
342
|
+
ok: false,
|
|
343
|
+
issues
|
|
344
|
+
};
|
|
345
|
+
}
|
|
208
346
|
/**
|
|
209
347
|
* Prune tombstoned elements that are causally stable and have no live descendants
|
|
210
348
|
* depending on them for sequence traversal.
|
|
@@ -251,15 +389,19 @@ function rgaCompactTombstones(seq, isStable) {
|
|
|
251
389
|
continue;
|
|
252
390
|
}
|
|
253
391
|
const elem = seq.elems.get(frame.id);
|
|
254
|
-
if (!elem || !elem.tombstone || !isStable(elem.
|
|
392
|
+
if (!elem || !elem.tombstone || !elem.delDot || !isStable(elem.delDot)) continue;
|
|
255
393
|
const childIds = children.get(frame.id);
|
|
256
394
|
if (!childIds || childIds.every((childId) => removable.has(childId))) removable.add(frame.id);
|
|
257
395
|
}
|
|
258
396
|
if (removable.size === 0) return 0;
|
|
259
397
|
for (const id of removable) seq.elems.delete(id);
|
|
398
|
+
maxSiblingInsDotByPrevCache.delete(seq);
|
|
260
399
|
bumpVersion(seq);
|
|
261
400
|
return removable.size;
|
|
262
401
|
}
|
|
402
|
+
function rgaMaxInsertDotForPrev(seq, prev) {
|
|
403
|
+
return getMaxSiblingInsDotByPrevIndex(seq).get(prev) ?? null;
|
|
404
|
+
}
|
|
263
405
|
function rgaIdAtIndex(seq, index) {
|
|
264
406
|
return rgaLinearizeIds(seq)[index];
|
|
265
407
|
}
|
|
@@ -587,6 +729,7 @@ const ROOT_KEY = "@@crdt/root";
|
|
|
587
729
|
//#endregion
|
|
588
730
|
//#region src/patch.ts
|
|
589
731
|
const DEFAULT_LCS_MAX_CELLS = 25e4;
|
|
732
|
+
const LINEAR_LCS_MATRIX_BASE_CASE_MAX_CELLS = 4096;
|
|
590
733
|
/** Structured compile error used to map patch validation failures to typed reasons. */
|
|
591
734
|
var PatchCompileError = class extends Error {
|
|
592
735
|
reason;
|
|
@@ -677,22 +820,37 @@ function getAtJson(base, path) {
|
|
|
677
820
|
* @returns An array of `IntentOp` ready for `applyIntentsToCrdt`.
|
|
678
821
|
*/
|
|
679
822
|
function compileJsonPatchToIntent(baseJson, patch, options = {}) {
|
|
823
|
+
const internalOptions = options;
|
|
680
824
|
const semantics = options.semantics ?? "sequential";
|
|
825
|
+
const opIndexOffset = internalOptions.opIndexOffset ?? 0;
|
|
681
826
|
let workingBase = baseJson;
|
|
682
|
-
const pointerCache = /* @__PURE__ */ new Map();
|
|
827
|
+
const pointerCache = internalOptions.pointerCache ?? /* @__PURE__ */ new Map();
|
|
683
828
|
const intents = [];
|
|
684
829
|
for (let opIndex = 0; opIndex < patch.length; opIndex++) {
|
|
685
830
|
const op = patch[opIndex];
|
|
831
|
+
const absoluteOpIndex = opIndex + opIndexOffset;
|
|
686
832
|
const compileBase = semantics === "sequential" ? workingBase : baseJson;
|
|
687
|
-
intents.push(...compileSingleOp(compileBase, op,
|
|
688
|
-
if (semantics === "sequential") workingBase = applyPatchOpToJsonWithStructuralSharing(workingBase, op,
|
|
833
|
+
intents.push(...compileSingleOp(compileBase, op, absoluteOpIndex, semantics, pointerCache));
|
|
834
|
+
if (semantics === "sequential") workingBase = applyPatchOpToJsonWithStructuralSharing(workingBase, op, absoluteOpIndex, pointerCache);
|
|
689
835
|
}
|
|
690
836
|
return intents;
|
|
691
837
|
}
|
|
838
|
+
/** Compile a single JSON Patch operation into CRDT intents. */
|
|
839
|
+
function compileJsonPatchOpToIntent(baseJson, op, options = {}) {
|
|
840
|
+
const internalOptions = options;
|
|
841
|
+
const semantics = options.semantics ?? "sequential";
|
|
842
|
+
const pointerCache = internalOptions.pointerCache ?? /* @__PURE__ */ new Map();
|
|
843
|
+
return compileSingleOp(baseJson, op, internalOptions.opIndexOffset ?? 0, semantics, pointerCache);
|
|
844
|
+
}
|
|
692
845
|
/**
|
|
693
846
|
* Compute a JSON Patch delta between two JSON values.
|
|
694
847
|
* By default arrays use a deterministic LCS strategy.
|
|
695
848
|
* Pass `{ arrayStrategy: "atomic" }` for single-op array replacement.
|
|
849
|
+
* Pass `{ arrayStrategy: "lcs-linear" }` for a lower-memory LCS variant.
|
|
850
|
+
* Note that `lcs-linear` still runs in `O(n * m)` time and does not have an
|
|
851
|
+
* automatic fallback for very large unmatched windows.
|
|
852
|
+
* Pass `{ emitMoves: true }` or `{ emitCopies: true }` to opt into RFC 6902
|
|
853
|
+
* move/copy emission when a deterministic rewrite is available.
|
|
696
854
|
* @param base - The original JSON value.
|
|
697
855
|
* @param next - The target JSON value.
|
|
698
856
|
* @param options - Diff options.
|
|
@@ -709,14 +867,19 @@ function diffJsonPatch(base, next, options = {}) {
|
|
|
709
867
|
function diffValue(path, base, next, ops, options) {
|
|
710
868
|
if (jsonEquals(base, next)) return;
|
|
711
869
|
if (Array.isArray(base) || Array.isArray(next)) {
|
|
712
|
-
|
|
713
|
-
|
|
870
|
+
const arrayStrategy = options.arrayStrategy ?? "lcs";
|
|
871
|
+
if (arrayStrategy === "lcs" && Array.isArray(base) && Array.isArray(next)) {
|
|
872
|
+
if (!diffArrayWithLcsMatrix(path, base, next, ops, options)) ops.push({
|
|
714
873
|
op: "replace",
|
|
715
874
|
path: stringifyJsonPointer(path),
|
|
716
875
|
value: next
|
|
717
876
|
});
|
|
718
877
|
return;
|
|
719
878
|
}
|
|
879
|
+
if (arrayStrategy === "lcs-linear" && Array.isArray(base) && Array.isArray(next)) {
|
|
880
|
+
diffArrayWithLinearLcs(path, base, next, ops, options);
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
720
883
|
ops.push({
|
|
721
884
|
op: "replace",
|
|
722
885
|
path: stringifyJsonPointer(path),
|
|
@@ -732,150 +895,368 @@ function diffValue(path, base, next, ops, options) {
|
|
|
732
895
|
});
|
|
733
896
|
return;
|
|
734
897
|
}
|
|
898
|
+
diffObject(path, base, next, ops, options);
|
|
899
|
+
}
|
|
900
|
+
function diffObject(path, base, next, ops, options) {
|
|
735
901
|
const baseKeys = Object.keys(base).sort();
|
|
736
902
|
const nextKeys = Object.keys(next).sort();
|
|
903
|
+
const baseOnlyKeys = [];
|
|
904
|
+
const nextOnlyKeys = [];
|
|
905
|
+
const sharedKeys = [];
|
|
737
906
|
let baseIndex = 0;
|
|
738
907
|
let nextIndex = 0;
|
|
739
908
|
while (baseIndex < baseKeys.length && nextIndex < nextKeys.length) {
|
|
740
909
|
const baseKey = baseKeys[baseIndex];
|
|
741
910
|
const nextKey = nextKeys[nextIndex];
|
|
742
911
|
if (baseKey === nextKey) {
|
|
912
|
+
sharedKeys.push(baseKey);
|
|
743
913
|
baseIndex += 1;
|
|
744
914
|
nextIndex += 1;
|
|
745
915
|
continue;
|
|
746
916
|
}
|
|
747
917
|
if (baseKey < nextKey) {
|
|
918
|
+
baseOnlyKeys.push(baseKey);
|
|
919
|
+
baseIndex += 1;
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
nextOnlyKeys.push(nextKey);
|
|
923
|
+
nextIndex += 1;
|
|
924
|
+
}
|
|
925
|
+
while (baseIndex < baseKeys.length) {
|
|
926
|
+
baseOnlyKeys.push(baseKeys[baseIndex]);
|
|
927
|
+
baseIndex += 1;
|
|
928
|
+
}
|
|
929
|
+
while (nextIndex < nextKeys.length) {
|
|
930
|
+
nextOnlyKeys.push(nextKeys[nextIndex]);
|
|
931
|
+
nextIndex += 1;
|
|
932
|
+
}
|
|
933
|
+
emitObjectStructuralOps(path, base, next, sharedKeys, baseOnlyKeys, nextOnlyKeys, ops, options);
|
|
934
|
+
for (const key of sharedKeys) {
|
|
935
|
+
path.push(key);
|
|
936
|
+
diffValue(path, base[key], next[key], ops, options);
|
|
937
|
+
path.pop();
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
function emitObjectStructuralOps(path, base, next, sharedKeys, baseOnlyKeys, nextOnlyKeys, ops, options) {
|
|
941
|
+
if (!options.emitMoves && !options.emitCopies) {
|
|
942
|
+
for (const baseKey of baseOnlyKeys) {
|
|
748
943
|
path.push(baseKey);
|
|
749
944
|
ops.push({
|
|
750
945
|
op: "remove",
|
|
751
946
|
path: stringifyJsonPointer(path)
|
|
752
947
|
});
|
|
753
948
|
path.pop();
|
|
754
|
-
baseIndex += 1;
|
|
755
|
-
continue;
|
|
756
949
|
}
|
|
757
|
-
|
|
950
|
+
for (const nextKey of nextOnlyKeys) {
|
|
951
|
+
path.push(nextKey);
|
|
952
|
+
ops.push({
|
|
953
|
+
op: "add",
|
|
954
|
+
path: stringifyJsonPointer(path),
|
|
955
|
+
value: next[nextKey]
|
|
956
|
+
});
|
|
957
|
+
path.pop();
|
|
958
|
+
}
|
|
959
|
+
return;
|
|
758
960
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
961
|
+
const matchedMoveSources = /* @__PURE__ */ new Set();
|
|
962
|
+
const moveTargets = /* @__PURE__ */ new Map();
|
|
963
|
+
if (options.emitMoves) {
|
|
964
|
+
const moveSourceBuckets = /* @__PURE__ */ new Map();
|
|
965
|
+
for (const baseKey of baseOnlyKeys) {
|
|
966
|
+
const bucketKey = stableJsonValueKey(base[baseKey]);
|
|
967
|
+
const bucket = moveSourceBuckets.get(bucketKey);
|
|
968
|
+
if (bucket) bucket.push(baseKey);
|
|
969
|
+
else moveSourceBuckets.set(bucketKey, [baseKey]);
|
|
970
|
+
}
|
|
971
|
+
for (const nextKey of nextOnlyKeys) {
|
|
972
|
+
const bucket = moveSourceBuckets.get(stableJsonValueKey(next[nextKey]));
|
|
973
|
+
if (!bucket) continue;
|
|
974
|
+
if (bucket.length > 0) {
|
|
975
|
+
const candidate = bucket.shift();
|
|
976
|
+
matchedMoveSources.add(candidate);
|
|
977
|
+
moveTargets.set(nextKey, candidate);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
768
980
|
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
981
|
+
const availableSources = /* @__PURE__ */ new Map();
|
|
982
|
+
const availableSourceKeys = [];
|
|
983
|
+
for (const key of sharedKeys) {
|
|
984
|
+
if (!jsonEquals(base[key], next[key])) continue;
|
|
985
|
+
availableSources.set(key, base[key]);
|
|
986
|
+
availableSourceKeys.push(key);
|
|
987
|
+
}
|
|
988
|
+
for (const nextKey of nextOnlyKeys) {
|
|
989
|
+
path.push(nextKey);
|
|
990
|
+
const targetPath = stringifyJsonPointer(path);
|
|
991
|
+
path.pop();
|
|
992
|
+
const moveSource = moveTargets.get(nextKey);
|
|
993
|
+
if (moveSource !== void 0) {
|
|
994
|
+
path.push(moveSource);
|
|
995
|
+
const fromPath = stringifyJsonPointer(path);
|
|
996
|
+
path.pop();
|
|
997
|
+
ops.push({
|
|
998
|
+
op: "move",
|
|
999
|
+
from: fromPath,
|
|
1000
|
+
path: targetPath
|
|
1001
|
+
});
|
|
1002
|
+
availableSources.set(nextKey, next[nextKey]);
|
|
1003
|
+
insertSortedKey(availableSourceKeys, nextKey);
|
|
777
1004
|
continue;
|
|
778
1005
|
}
|
|
779
|
-
if (
|
|
780
|
-
|
|
781
|
-
|
|
1006
|
+
if (options.emitCopies) {
|
|
1007
|
+
const copySource = findObjectCopySource(availableSourceKeys, availableSources, next[nextKey]);
|
|
1008
|
+
if (copySource !== void 0) {
|
|
1009
|
+
path.push(copySource);
|
|
1010
|
+
const fromPath = stringifyJsonPointer(path);
|
|
1011
|
+
path.pop();
|
|
1012
|
+
ops.push({
|
|
1013
|
+
op: "copy",
|
|
1014
|
+
from: fromPath,
|
|
1015
|
+
path: targetPath
|
|
1016
|
+
});
|
|
1017
|
+
availableSources.set(nextKey, next[nextKey]);
|
|
1018
|
+
insertSortedKey(availableSourceKeys, nextKey);
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
782
1021
|
}
|
|
783
|
-
path.push(nextKey);
|
|
784
1022
|
ops.push({
|
|
785
1023
|
op: "add",
|
|
786
|
-
path:
|
|
1024
|
+
path: targetPath,
|
|
787
1025
|
value: next[nextKey]
|
|
788
1026
|
});
|
|
789
|
-
|
|
790
|
-
|
|
1027
|
+
availableSources.set(nextKey, next[nextKey]);
|
|
1028
|
+
insertSortedKey(availableSourceKeys, nextKey);
|
|
791
1029
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
path.push(
|
|
1030
|
+
for (const baseKey of baseOnlyKeys) {
|
|
1031
|
+
if (matchedMoveSources.has(baseKey)) continue;
|
|
1032
|
+
path.push(baseKey);
|
|
795
1033
|
ops.push({
|
|
796
|
-
op: "
|
|
797
|
-
path: stringifyJsonPointer(path)
|
|
798
|
-
value: next[nextKey]
|
|
1034
|
+
op: "remove",
|
|
1035
|
+
path: stringifyJsonPointer(path)
|
|
799
1036
|
});
|
|
800
1037
|
path.pop();
|
|
801
|
-
nextIndex += 1;
|
|
802
1038
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
1039
|
+
}
|
|
1040
|
+
function findObjectCopySource(sortedKeys, availableSources, target) {
|
|
1041
|
+
for (const key of sortedKeys) if (jsonEquals(availableSources.get(key), target)) return key;
|
|
1042
|
+
}
|
|
1043
|
+
function insertSortedKey(keys, key) {
|
|
1044
|
+
let low = 0;
|
|
1045
|
+
let high = keys.length;
|
|
1046
|
+
while (low < high) {
|
|
1047
|
+
const mid = Math.floor((low + high) / 2);
|
|
1048
|
+
if (keys[mid] < key) low = mid + 1;
|
|
1049
|
+
else high = mid;
|
|
1050
|
+
}
|
|
1051
|
+
keys.splice(low, 0, key);
|
|
1052
|
+
}
|
|
1053
|
+
function diffArrayWithLcsMatrix(path, base, next, ops, options) {
|
|
1054
|
+
const window = trimEqualArrayEdges(base, next);
|
|
1055
|
+
const baseStart = window.baseStart;
|
|
1056
|
+
const nextStart = window.nextStart;
|
|
1057
|
+
const n = window.unmatchedBaseLength;
|
|
1058
|
+
const m = window.unmatchedNextLength;
|
|
1059
|
+
if (!shouldUseLcsDiff(n, m, options.lcsMaxCells)) return false;
|
|
1060
|
+
if (n === 0 && m === 0) return true;
|
|
1061
|
+
const steps = [];
|
|
1062
|
+
buildArrayEditScriptWithMatrix(base, baseStart, baseStart + n, next, nextStart, nextStart + m, steps);
|
|
1063
|
+
pushArrayPatchOps(path, window.prefixLength, steps, ops, base, options);
|
|
1064
|
+
return true;
|
|
1065
|
+
}
|
|
1066
|
+
function diffArrayWithLinearLcs(path, base, next, ops, options) {
|
|
1067
|
+
const window = trimEqualArrayEdges(base, next);
|
|
1068
|
+
const steps = [];
|
|
1069
|
+
buildArrayEditScriptLinearSpace(base, window.baseStart, window.baseStart + window.unmatchedBaseLength, next, window.nextStart, window.nextStart + window.unmatchedNextLength, steps);
|
|
1070
|
+
pushArrayPatchOps(path, window.prefixLength, steps, ops, base, options);
|
|
1071
|
+
}
|
|
1072
|
+
function trimEqualArrayEdges(base, next) {
|
|
1073
|
+
const baseLength = base.length;
|
|
1074
|
+
const nextLength = next.length;
|
|
1075
|
+
let prefixLength = 0;
|
|
1076
|
+
while (prefixLength < baseLength && prefixLength < nextLength && jsonEquals(base[prefixLength], next[prefixLength])) prefixLength += 1;
|
|
1077
|
+
let suffixLength = 0;
|
|
1078
|
+
while (suffixLength < baseLength - prefixLength && suffixLength < nextLength - prefixLength && jsonEquals(base[baseLength - 1 - suffixLength], next[nextLength - 1 - suffixLength])) suffixLength += 1;
|
|
1079
|
+
return {
|
|
1080
|
+
baseStart: prefixLength,
|
|
1081
|
+
nextStart: prefixLength,
|
|
1082
|
+
prefixLength,
|
|
1083
|
+
unmatchedBaseLength: baseLength - prefixLength - suffixLength,
|
|
1084
|
+
unmatchedNextLength: nextLength - prefixLength - suffixLength
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
function buildArrayEditScriptLinearSpace(base, baseStart, baseEnd, next, nextStart, nextEnd, steps) {
|
|
1088
|
+
const unmatchedBaseLength = baseEnd - baseStart;
|
|
1089
|
+
const unmatchedNextLength = nextEnd - nextStart;
|
|
1090
|
+
if (unmatchedBaseLength === 0) {
|
|
1091
|
+
for (let nextIndex = nextStart; nextIndex < nextEnd; nextIndex++) steps.push({
|
|
1092
|
+
kind: "add",
|
|
1093
|
+
value: next[nextIndex]
|
|
1094
|
+
});
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
if (unmatchedNextLength === 0) {
|
|
1098
|
+
for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) steps.push({ kind: "remove" });
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
if (unmatchedBaseLength === 1) {
|
|
1102
|
+
pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, steps);
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
if (unmatchedNextLength === 1) {
|
|
1106
|
+
pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, steps);
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
if (shouldUseMatrixBaseCase(unmatchedBaseLength, unmatchedNextLength)) {
|
|
1110
|
+
buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStart, nextEnd, steps);
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
const baseMid = baseStart + Math.floor(unmatchedBaseLength / 2);
|
|
1114
|
+
const forwardScores = computeLcsPrefixLengths(base, baseStart, baseMid, next, nextStart, nextEnd);
|
|
1115
|
+
const reverseScores = computeLcsSuffixLengths(base, baseMid, baseEnd, next, nextStart, nextEnd);
|
|
1116
|
+
let bestOffset = 0;
|
|
1117
|
+
let bestScore = Number.NEGATIVE_INFINITY;
|
|
1118
|
+
for (let offset = 0; offset <= unmatchedNextLength; offset++) {
|
|
1119
|
+
const score = forwardScores[offset] + reverseScores[offset];
|
|
1120
|
+
if (score > bestScore) {
|
|
1121
|
+
bestScore = score;
|
|
1122
|
+
bestOffset = offset;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
const nextMid = nextStart + bestOffset;
|
|
1126
|
+
buildArrayEditScriptLinearSpace(base, baseStart, baseMid, next, nextStart, nextMid, steps);
|
|
1127
|
+
buildArrayEditScriptLinearSpace(base, baseMid, baseEnd, next, nextMid, nextEnd, steps);
|
|
1128
|
+
}
|
|
1129
|
+
function pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, steps) {
|
|
1130
|
+
const matchIndex = findFirstMatchingIndexInNext(base[baseStart], next, nextStart, nextEnd);
|
|
1131
|
+
if (matchIndex === -1) {
|
|
1132
|
+
steps.push({ kind: "remove" });
|
|
1133
|
+
for (let nextIndex = nextStart; nextIndex < nextEnd; nextIndex++) steps.push({
|
|
1134
|
+
kind: "add",
|
|
1135
|
+
value: next[nextIndex]
|
|
1136
|
+
});
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
for (let nextIndex = nextStart; nextIndex < matchIndex; nextIndex++) steps.push({
|
|
1140
|
+
kind: "add",
|
|
1141
|
+
value: next[nextIndex]
|
|
1142
|
+
});
|
|
1143
|
+
steps.push({ kind: "equal" });
|
|
1144
|
+
for (let nextIndex = matchIndex + 1; nextIndex < nextEnd; nextIndex++) steps.push({
|
|
1145
|
+
kind: "add",
|
|
1146
|
+
value: next[nextIndex]
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
function pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, steps) {
|
|
1150
|
+
const matchIndex = findFirstMatchingIndexInBase(next[nextStart], base, baseStart, baseEnd);
|
|
1151
|
+
if (matchIndex === -1) {
|
|
1152
|
+
for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) steps.push({ kind: "remove" });
|
|
1153
|
+
steps.push({
|
|
1154
|
+
kind: "add",
|
|
1155
|
+
value: next[nextStart]
|
|
1156
|
+
});
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
for (let baseIndex = baseStart; baseIndex < matchIndex; baseIndex++) steps.push({ kind: "remove" });
|
|
1160
|
+
steps.push({ kind: "equal" });
|
|
1161
|
+
for (let baseIndex = matchIndex + 1; baseIndex < baseEnd; baseIndex++) steps.push({ kind: "remove" });
|
|
1162
|
+
}
|
|
1163
|
+
function findFirstMatchingIndexInNext(target, next, nextStart, nextEnd) {
|
|
1164
|
+
for (let nextIndex = nextStart; nextIndex < nextEnd; nextIndex++) if (jsonEquals(target, next[nextIndex])) return nextIndex;
|
|
1165
|
+
return -1;
|
|
1166
|
+
}
|
|
1167
|
+
function findFirstMatchingIndexInBase(target, base, baseStart, baseEnd) {
|
|
1168
|
+
for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) if (jsonEquals(target, base[baseIndex])) return baseIndex;
|
|
1169
|
+
return -1;
|
|
1170
|
+
}
|
|
1171
|
+
function shouldUseMatrixBaseCase(baseLength, nextLength) {
|
|
1172
|
+
return (baseLength + 1) * (nextLength + 1) <= LINEAR_LCS_MATRIX_BASE_CASE_MAX_CELLS;
|
|
1173
|
+
}
|
|
1174
|
+
function buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStart, nextEnd, steps) {
|
|
1175
|
+
const unmatchedBaseLength = baseEnd - baseStart;
|
|
1176
|
+
const unmatchedNextLength = nextEnd - nextStart;
|
|
1177
|
+
const lcs = Array.from({ length: unmatchedBaseLength + 1 }, () => Array(unmatchedNextLength + 1).fill(0));
|
|
1178
|
+
for (let baseOffset = unmatchedBaseLength - 1; baseOffset >= 0; baseOffset--) for (let nextOffset = unmatchedNextLength - 1; nextOffset >= 0; nextOffset--) if (jsonEquals(base[baseStart + baseOffset], next[nextStart + nextOffset])) lcs[baseOffset][nextOffset] = 1 + lcs[baseOffset + 1][nextOffset + 1];
|
|
1179
|
+
else lcs[baseOffset][nextOffset] = Math.max(lcs[baseOffset + 1][nextOffset], lcs[baseOffset][nextOffset + 1]);
|
|
1180
|
+
let baseOffset = 0;
|
|
1181
|
+
let nextOffset = 0;
|
|
1182
|
+
while (baseOffset < unmatchedBaseLength || nextOffset < unmatchedNextLength) {
|
|
1183
|
+
if (baseOffset < unmatchedBaseLength && nextOffset < unmatchedNextLength && jsonEquals(base[baseStart + baseOffset], next[nextStart + nextOffset])) {
|
|
1184
|
+
steps.push({ kind: "equal" });
|
|
1185
|
+
baseOffset += 1;
|
|
1186
|
+
nextOffset += 1;
|
|
814
1187
|
continue;
|
|
815
1188
|
}
|
|
816
|
-
|
|
817
|
-
|
|
1189
|
+
const lcsDown = baseOffset < unmatchedBaseLength ? lcs[baseOffset + 1][nextOffset] : -1;
|
|
1190
|
+
const lcsRight = nextOffset < unmatchedNextLength ? lcs[baseOffset][nextOffset + 1] : -1;
|
|
1191
|
+
if (nextOffset < unmatchedNextLength && (baseOffset === unmatchedBaseLength || lcsRight > lcsDown)) {
|
|
1192
|
+
steps.push({
|
|
1193
|
+
kind: "add",
|
|
1194
|
+
value: next[nextStart + nextOffset]
|
|
1195
|
+
});
|
|
1196
|
+
nextOffset += 1;
|
|
818
1197
|
continue;
|
|
819
1198
|
}
|
|
820
|
-
|
|
1199
|
+
if (baseOffset < unmatchedBaseLength) {
|
|
1200
|
+
steps.push({ kind: "remove" });
|
|
1201
|
+
baseOffset += 1;
|
|
1202
|
+
}
|
|
821
1203
|
}
|
|
822
1204
|
}
|
|
823
|
-
function
|
|
824
|
-
const
|
|
825
|
-
|
|
826
|
-
let
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
1205
|
+
function computeLcsPrefixLengths(base, baseStart, baseEnd, next, nextStart, nextEnd) {
|
|
1206
|
+
const unmatchedNextLength = nextEnd - nextStart;
|
|
1207
|
+
let previousRow = new Int32Array(unmatchedNextLength + 1);
|
|
1208
|
+
let currentRow = new Int32Array(unmatchedNextLength + 1);
|
|
1209
|
+
for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) {
|
|
1210
|
+
for (let nextOffset = 0; nextOffset < unmatchedNextLength; nextOffset++) if (jsonEquals(base[baseIndex], next[nextStart + nextOffset])) currentRow[nextOffset + 1] = previousRow[nextOffset] + 1;
|
|
1211
|
+
else currentRow[nextOffset + 1] = Math.max(previousRow[nextOffset + 1], currentRow[nextOffset]);
|
|
1212
|
+
const nextPreviousRow = currentRow;
|
|
1213
|
+
currentRow = previousRow;
|
|
1214
|
+
previousRow = nextPreviousRow;
|
|
1215
|
+
currentRow.fill(0);
|
|
1216
|
+
}
|
|
1217
|
+
return previousRow;
|
|
1218
|
+
}
|
|
1219
|
+
function computeLcsSuffixLengths(base, baseStart, baseEnd, next, nextStart, nextEnd) {
|
|
1220
|
+
const unmatchedNextLength = nextEnd - nextStart;
|
|
1221
|
+
let previousRow = new Int32Array(unmatchedNextLength + 1);
|
|
1222
|
+
let currentRow = new Int32Array(unmatchedNextLength + 1);
|
|
1223
|
+
for (let baseIndex = baseEnd - 1; baseIndex >= baseStart; baseIndex--) {
|
|
1224
|
+
for (let nextOffset = unmatchedNextLength - 1; nextOffset >= 0; nextOffset--) if (jsonEquals(base[baseIndex], next[nextStart + nextOffset])) currentRow[nextOffset] = previousRow[nextOffset + 1] + 1;
|
|
1225
|
+
else currentRow[nextOffset] = Math.max(previousRow[nextOffset], currentRow[nextOffset + 1]);
|
|
1226
|
+
const nextPreviousRow = currentRow;
|
|
1227
|
+
currentRow = previousRow;
|
|
1228
|
+
previousRow = nextPreviousRow;
|
|
1229
|
+
currentRow.fill(0);
|
|
1230
|
+
}
|
|
1231
|
+
return previousRow;
|
|
1232
|
+
}
|
|
1233
|
+
function pushArrayPatchOps(path, startIndex, steps, ops, base, options) {
|
|
839
1234
|
const localOps = [];
|
|
840
|
-
let
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
while (i < n || j < m) {
|
|
844
|
-
if (i < n && j < m && jsonEquals(base[baseStart + i], next[nextStart + j])) {
|
|
845
|
-
i += 1;
|
|
846
|
-
j += 1;
|
|
1235
|
+
let index = startIndex;
|
|
1236
|
+
for (const step of steps) {
|
|
1237
|
+
if (step.kind === "equal") {
|
|
847
1238
|
index += 1;
|
|
848
1239
|
continue;
|
|
849
1240
|
}
|
|
850
|
-
const
|
|
851
|
-
|
|
852
|
-
if (
|
|
853
|
-
const indexSegment = String(index);
|
|
854
|
-
path.push(indexSegment);
|
|
1241
|
+
const indexSegment = String(index);
|
|
1242
|
+
path.push(indexSegment);
|
|
1243
|
+
if (step.kind === "add") {
|
|
855
1244
|
localOps.push({
|
|
856
1245
|
op: "add",
|
|
857
1246
|
path: stringifyJsonPointer(path),
|
|
858
|
-
value:
|
|
1247
|
+
value: step.value
|
|
859
1248
|
});
|
|
860
|
-
path.pop();
|
|
861
|
-
j += 1;
|
|
862
1249
|
index += 1;
|
|
863
|
-
continue;
|
|
864
|
-
}
|
|
865
|
-
if (i < n) {
|
|
866
|
-
const indexSegment = String(index);
|
|
867
|
-
path.push(indexSegment);
|
|
868
|
-
localOps.push({
|
|
869
|
-
op: "remove",
|
|
870
|
-
path: stringifyJsonPointer(path)
|
|
871
|
-
});
|
|
872
1250
|
path.pop();
|
|
873
|
-
i += 1;
|
|
874
1251
|
continue;
|
|
875
1252
|
}
|
|
1253
|
+
localOps.push({
|
|
1254
|
+
op: "remove",
|
|
1255
|
+
path: stringifyJsonPointer(path)
|
|
1256
|
+
});
|
|
1257
|
+
path.pop();
|
|
876
1258
|
}
|
|
877
|
-
ops.push(...
|
|
878
|
-
return true;
|
|
1259
|
+
ops.push(...finalizeArrayOps(path, base, localOps, options));
|
|
879
1260
|
}
|
|
880
1261
|
function shouldUseLcsDiff(baseLength, nextLength, lcsMaxCells) {
|
|
881
1262
|
if (lcsMaxCells === Number.POSITIVE_INFINITY) return true;
|
|
@@ -883,6 +1264,99 @@ function shouldUseLcsDiff(baseLength, nextLength, lcsMaxCells) {
|
|
|
883
1264
|
if (!Number.isFinite(cap) || cap < 1) return false;
|
|
884
1265
|
return (baseLength + 1) * (nextLength + 1) <= cap;
|
|
885
1266
|
}
|
|
1267
|
+
function finalizeArrayOps(arrayPath, base, ops, options) {
|
|
1268
|
+
if (ops.length === 0) return [];
|
|
1269
|
+
if (!options.emitMoves && !options.emitCopies) return compactArrayOps(ops);
|
|
1270
|
+
const out = [];
|
|
1271
|
+
const working = base.slice();
|
|
1272
|
+
for (let i = 0; i < ops.length; i++) {
|
|
1273
|
+
const op = ops[i];
|
|
1274
|
+
const next = ops[i + 1];
|
|
1275
|
+
if (op.op === "remove" && next && next.op === "add") {
|
|
1276
|
+
const removedValue = working[getArrayOpIndex(op.path, arrayPath)];
|
|
1277
|
+
const valuesMatch = jsonEquals(removedValue, next.value);
|
|
1278
|
+
if (op.path === next.path) {
|
|
1279
|
+
const replaceOp = {
|
|
1280
|
+
op: "replace",
|
|
1281
|
+
path: op.path,
|
|
1282
|
+
value: next.value
|
|
1283
|
+
};
|
|
1284
|
+
out.push(replaceOp);
|
|
1285
|
+
applyArrayOptimizationOp(working, replaceOp, arrayPath);
|
|
1286
|
+
i += 1;
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
if (options.emitMoves && valuesMatch) {
|
|
1290
|
+
const moveOp = {
|
|
1291
|
+
op: "move",
|
|
1292
|
+
from: op.path,
|
|
1293
|
+
path: next.path
|
|
1294
|
+
};
|
|
1295
|
+
out.push(moveOp);
|
|
1296
|
+
applyArrayOptimizationOp(working, moveOp, arrayPath);
|
|
1297
|
+
i += 1;
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
if (valuesMatch) {
|
|
1301
|
+
out.push(op);
|
|
1302
|
+
applyArrayOptimizationOp(working, op, arrayPath);
|
|
1303
|
+
out.push(next);
|
|
1304
|
+
applyArrayOptimizationOp(working, next, arrayPath);
|
|
1305
|
+
i += 1;
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
out.push(op);
|
|
1309
|
+
applyArrayOptimizationOp(working, op, arrayPath);
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
if (op.op === "add" && next && next.op === "remove") {
|
|
1313
|
+
const targetIndex = getArrayOpIndex(op.path, arrayPath);
|
|
1314
|
+
const removeIndex = getArrayOpIndex(next.path, arrayPath);
|
|
1315
|
+
const sourceIndex = removeIndex - (targetIndex <= removeIndex ? 1 : 0);
|
|
1316
|
+
const matchesPendingRemove = sourceIndex >= 0 && sourceIndex < working.length && jsonEquals(working[sourceIndex], op.value);
|
|
1317
|
+
if (options.emitMoves && matchesPendingRemove) {
|
|
1318
|
+
const moveOp = {
|
|
1319
|
+
op: "move",
|
|
1320
|
+
from: stringifyJsonPointer([...arrayPath, String(sourceIndex)]),
|
|
1321
|
+
path: op.path
|
|
1322
|
+
};
|
|
1323
|
+
out.push(moveOp);
|
|
1324
|
+
applyArrayOptimizationOp(working, moveOp, arrayPath);
|
|
1325
|
+
i += 1;
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
if (matchesPendingRemove) {
|
|
1329
|
+
out.push(op);
|
|
1330
|
+
applyArrayOptimizationOp(working, op, arrayPath);
|
|
1331
|
+
out.push(next);
|
|
1332
|
+
applyArrayOptimizationOp(working, next, arrayPath);
|
|
1333
|
+
i += 1;
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
if (op.op === "add" && options.emitCopies) {
|
|
1338
|
+
const copySourceIndex = findArrayCopySourceIndex(working, op.value);
|
|
1339
|
+
if (copySourceIndex !== -1) {
|
|
1340
|
+
const copyOp = {
|
|
1341
|
+
op: "copy",
|
|
1342
|
+
from: stringifyJsonPointer([...arrayPath, String(copySourceIndex)]),
|
|
1343
|
+
path: op.path
|
|
1344
|
+
};
|
|
1345
|
+
out.push(copyOp);
|
|
1346
|
+
applyArrayOptimizationOp(working, copyOp, arrayPath);
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
out.push(op);
|
|
1351
|
+
applyArrayOptimizationOp(working, op, arrayPath);
|
|
1352
|
+
}
|
|
1353
|
+
return out;
|
|
1354
|
+
}
|
|
1355
|
+
function stableJsonValueKey(value) {
|
|
1356
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
1357
|
+
if (Array.isArray(value)) return `[${value.map(stableJsonValueKey).join(",")}]`;
|
|
1358
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJsonValueKey(value[key])}`).join(",")}}`;
|
|
1359
|
+
}
|
|
886
1360
|
function compactArrayOps(ops) {
|
|
887
1361
|
const out = [];
|
|
888
1362
|
for (let i = 0; i < ops.length; i++) {
|
|
@@ -901,6 +1375,47 @@ function compactArrayOps(ops) {
|
|
|
901
1375
|
}
|
|
902
1376
|
return out;
|
|
903
1377
|
}
|
|
1378
|
+
function findArrayCopySourceIndex(working, value) {
|
|
1379
|
+
for (let index = 0; index < working.length; index++) if (jsonEquals(working[index], value)) return index;
|
|
1380
|
+
return -1;
|
|
1381
|
+
}
|
|
1382
|
+
function getArrayOpIndex(ptr, arrayPath) {
|
|
1383
|
+
const parsed = parseJsonPointer(ptr);
|
|
1384
|
+
if (parsed.length !== arrayPath.length + 1) throw new Error(`Expected array operation under ${stringifyJsonPointer(arrayPath)}: ${ptr}`);
|
|
1385
|
+
for (let index = 0; index < arrayPath.length; index++) if (parsed[index] !== arrayPath[index]) throw new Error(`Expected array operation under ${stringifyJsonPointer(arrayPath)}: ${ptr}`);
|
|
1386
|
+
const token = parsed[arrayPath.length];
|
|
1387
|
+
if (!ARRAY_INDEX_TOKEN_PATTERN.test(token)) throw new Error(`Expected numeric array index at ${ptr}`);
|
|
1388
|
+
return Number(token);
|
|
1389
|
+
}
|
|
1390
|
+
function applyArrayOptimizationOp(working, op, arrayPath) {
|
|
1391
|
+
if (op.op === "add") {
|
|
1392
|
+
working.splice(getArrayOpIndex(op.path, arrayPath), 0, structuredClone(op.value));
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
if (op.op === "remove") {
|
|
1396
|
+
working.splice(getArrayOpIndex(op.path, arrayPath), 1);
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
if (op.op === "replace") {
|
|
1400
|
+
working[getArrayOpIndex(op.path, arrayPath)] = structuredClone(op.value);
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
if (op.op === "copy") {
|
|
1404
|
+
const fromIndex = getArrayOpIndex(op.from, arrayPath);
|
|
1405
|
+
if (fromIndex < 0 || fromIndex >= working.length) throw new Error(`applyArrayOptimizationOp: copy from index ${fromIndex} is out of bounds (length ${working.length})`);
|
|
1406
|
+
const value = structuredClone(working[fromIndex]);
|
|
1407
|
+
working.splice(getArrayOpIndex(op.path, arrayPath), 0, value);
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
if (op.op === "move") {
|
|
1411
|
+
const fromIndex = getArrayOpIndex(op.from, arrayPath);
|
|
1412
|
+
if (fromIndex < 0 || fromIndex >= working.length) throw new Error(`applyArrayOptimizationOp: move from index ${fromIndex} is out of bounds (length ${working.length})`);
|
|
1413
|
+
const [value] = working.splice(fromIndex, 1);
|
|
1414
|
+
working.splice(getArrayOpIndex(op.path, arrayPath), 0, value);
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
throw new Error(`applyArrayOptimizationOp: unexpected op type "${op.op}"`);
|
|
1418
|
+
}
|
|
904
1419
|
function escapeJsonPointer(token) {
|
|
905
1420
|
return token.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
906
1421
|
}
|
|
@@ -1497,6 +2012,10 @@ function cloneNodeAtDepth(node, depth) {
|
|
|
1497
2012
|
id: e.id,
|
|
1498
2013
|
prev: e.prev,
|
|
1499
2014
|
tombstone: e.tombstone,
|
|
2015
|
+
delDot: e.delDot ? {
|
|
2016
|
+
actor: e.delDot.actor,
|
|
2017
|
+
ctr: e.delDot.ctr
|
|
2018
|
+
} : void 0,
|
|
1500
2019
|
value: cloneNodeAtDepth(e.value, depth + 1),
|
|
1501
2020
|
insDot: {
|
|
1502
2021
|
actor: e.insDot.actor,
|
|
@@ -1601,7 +2120,17 @@ function applyObjRemove(head, it, newDot) {
|
|
|
1601
2120
|
objRemove(parentObj, it.key, d);
|
|
1602
2121
|
return null;
|
|
1603
2122
|
}
|
|
1604
|
-
function
|
|
2123
|
+
function createArrayIndexLookupSession() {
|
|
2124
|
+
const bySeq = /* @__PURE__ */ new WeakMap();
|
|
2125
|
+
return { get(seq) {
|
|
2126
|
+
const cached = bySeq.get(seq);
|
|
2127
|
+
if (cached) return cached;
|
|
2128
|
+
const created = rgaCreateIndexedIdSnapshot(seq);
|
|
2129
|
+
bySeq.set(seq, created);
|
|
2130
|
+
return created;
|
|
2131
|
+
} };
|
|
2132
|
+
}
|
|
2133
|
+
function applyArrInsert(base, head, it, newDot, indexSession, bumpCounterAbove, strictParents = false) {
|
|
1605
2134
|
const pointer = `/${it.path.join("/")}`;
|
|
1606
2135
|
const baseSeq = getSeqAtPath(base, it.path);
|
|
1607
2136
|
if (!baseSeq) {
|
|
@@ -1633,8 +2162,9 @@ function applyArrInsert(base, head, it, newDot, bumpCounterAbove, strictParents
|
|
|
1633
2162
|
const headSeqRes = getHeadSeqForBaseArrayIntent(head, it.path);
|
|
1634
2163
|
if (!headSeqRes.ok) return headSeqRes;
|
|
1635
2164
|
const headSeq = headSeqRes.seq;
|
|
1636
|
-
const
|
|
1637
|
-
const baseLen =
|
|
2165
|
+
const baseIndex = indexSession.get(baseSeq);
|
|
2166
|
+
const baseLen = baseIndex.length();
|
|
2167
|
+
const idx = it.index === Number.POSITIVE_INFINITY ? baseLen : it.index;
|
|
1638
2168
|
if (idx < 0 || idx > baseLen) return {
|
|
1639
2169
|
ok: false,
|
|
1640
2170
|
code: 409,
|
|
@@ -1642,20 +2172,18 @@ function applyArrInsert(base, head, it, newDot, bumpCounterAbove, strictParents
|
|
|
1642
2172
|
message: `index out of bounds at /${it.path.join("/")}/${it.index}`,
|
|
1643
2173
|
path: `/${it.path.join("/")}/${it.index}`
|
|
1644
2174
|
};
|
|
1645
|
-
const prev =
|
|
2175
|
+
const prev = baseIndex.prevForInsertAt(idx);
|
|
1646
2176
|
const dotRes = nextInsertDotForPrev(headSeq, prev, newDot, pointer, bumpCounterAbove);
|
|
1647
2177
|
if (!dotRes.ok) return dotRes;
|
|
1648
2178
|
const d = dotRes.dot;
|
|
1649
|
-
|
|
2179
|
+
const id = dotToElemId(d);
|
|
2180
|
+
rgaInsertAfter(headSeq, prev, id, d, nodeFromJson(it.value, newDot));
|
|
2181
|
+
if (baseSeq === headSeq) baseIndex.insertAt(idx, id);
|
|
1650
2182
|
return null;
|
|
1651
2183
|
}
|
|
1652
2184
|
function nextInsertDotForPrev(seq, prev, newDot, path, bumpCounterAbove) {
|
|
1653
2185
|
const MAX_INSERT_DOT_ATTEMPTS = 1024;
|
|
1654
|
-
|
|
1655
|
-
for (const elem of seq.elems.values()) {
|
|
1656
|
-
if (elem.prev !== prev) continue;
|
|
1657
|
-
if (!maxSiblingDot || compareDot(elem.insDot, maxSiblingDot) > 0) maxSiblingDot = elem.insDot;
|
|
1658
|
-
}
|
|
2186
|
+
const maxSiblingDot = rgaMaxInsertDotForPrev(seq, prev);
|
|
1659
2187
|
if (maxSiblingDot) bumpCounterAbove?.(maxSiblingDot.ctr);
|
|
1660
2188
|
if (!maxSiblingDot) return {
|
|
1661
2189
|
ok: true,
|
|
@@ -1676,8 +2204,8 @@ function nextInsertDotForPrev(seq, prev, newDot, path, bumpCounterAbove) {
|
|
|
1676
2204
|
path
|
|
1677
2205
|
};
|
|
1678
2206
|
}
|
|
1679
|
-
function applyArrDelete(base, head, it, newDot) {
|
|
1680
|
-
newDot();
|
|
2207
|
+
function applyArrDelete(base, head, it, newDot, indexSession) {
|
|
2208
|
+
const _d = newDot();
|
|
1681
2209
|
const baseSeq = getSeqAtPath(base, it.path);
|
|
1682
2210
|
if (!baseSeq) return {
|
|
1683
2211
|
ok: false,
|
|
@@ -1689,7 +2217,8 @@ function applyArrDelete(base, head, it, newDot) {
|
|
|
1689
2217
|
const headSeqRes = getHeadSeqForBaseArrayIntent(head, it.path);
|
|
1690
2218
|
if (!headSeqRes.ok) return headSeqRes;
|
|
1691
2219
|
const headSeq = headSeqRes.seq;
|
|
1692
|
-
const
|
|
2220
|
+
const baseIndex = indexSession.get(baseSeq);
|
|
2221
|
+
const baseId = baseIndex.idAt(it.index);
|
|
1693
2222
|
if (!baseId) return {
|
|
1694
2223
|
ok: false,
|
|
1695
2224
|
code: 409,
|
|
@@ -1704,10 +2233,11 @@ function applyArrDelete(base, head, it, newDot) {
|
|
|
1704
2233
|
message: `element missing in head lineage at index ${it.index}`,
|
|
1705
2234
|
path: `/${it.path.join("/")}/${it.index}`
|
|
1706
2235
|
};
|
|
1707
|
-
rgaDelete(headSeq, baseId);
|
|
2236
|
+
rgaDelete(headSeq, baseId, _d);
|
|
2237
|
+
if (baseSeq === headSeq) baseIndex.deleteAt(it.index);
|
|
1708
2238
|
return null;
|
|
1709
2239
|
}
|
|
1710
|
-
function applyArrReplace(base, head, it, newDot) {
|
|
2240
|
+
function applyArrReplace(base, head, it, newDot, indexSession) {
|
|
1711
2241
|
newDot();
|
|
1712
2242
|
const baseSeq = getSeqAtPath(base, it.path);
|
|
1713
2243
|
if (!baseSeq) return {
|
|
@@ -1720,7 +2250,7 @@ function applyArrReplace(base, head, it, newDot) {
|
|
|
1720
2250
|
const headSeqRes = getHeadSeqForBaseArrayIntent(head, it.path);
|
|
1721
2251
|
if (!headSeqRes.ok) return headSeqRes;
|
|
1722
2252
|
const headSeq = headSeqRes.seq;
|
|
1723
|
-
const baseId =
|
|
2253
|
+
const baseId = indexSession.get(baseSeq).idAt(it.index);
|
|
1724
2254
|
if (!baseId) return {
|
|
1725
2255
|
ok: false,
|
|
1726
2256
|
code: 409,
|
|
@@ -1753,6 +2283,7 @@ function applyArrReplace(base, head, it, newDot) {
|
|
|
1753
2283
|
* @returns `{ ok: true }` on success, or `{ ok: false, code: 409, message }` on conflict.
|
|
1754
2284
|
*/
|
|
1755
2285
|
function applyIntentsToCrdt(base, head, intents, newDot, evalTestAgainst = "head", bumpCounterAbove, options = {}) {
|
|
2286
|
+
const arrayIndexSession = createArrayIndexLookupSession();
|
|
1756
2287
|
for (const it of intents) {
|
|
1757
2288
|
let fail = null;
|
|
1758
2289
|
switch (it.t) {
|
|
@@ -1766,13 +2297,13 @@ function applyIntentsToCrdt(base, head, intents, newDot, evalTestAgainst = "head
|
|
|
1766
2297
|
fail = applyObjRemove(head, it, newDot);
|
|
1767
2298
|
break;
|
|
1768
2299
|
case "ArrInsert":
|
|
1769
|
-
fail = applyArrInsert(base, head, it, newDot, bumpCounterAbove, options.strictParents ?? false);
|
|
2300
|
+
fail = applyArrInsert(base, head, it, newDot, arrayIndexSession, bumpCounterAbove, options.strictParents ?? false);
|
|
1770
2301
|
break;
|
|
1771
2302
|
case "ArrDelete":
|
|
1772
|
-
fail = applyArrDelete(base, head, it, newDot);
|
|
2303
|
+
fail = applyArrDelete(base, head, it, newDot, arrayIndexSession);
|
|
1773
2304
|
break;
|
|
1774
2305
|
case "ArrReplace":
|
|
1775
|
-
fail = applyArrReplace(base, head, it, newDot);
|
|
2306
|
+
fail = applyArrReplace(base, head, it, newDot, arrayIndexSession);
|
|
1776
2307
|
break;
|
|
1777
2308
|
default: assertNever(it, "Unhandled intent type");
|
|
1778
2309
|
}
|
|
@@ -1814,15 +2345,199 @@ function jsonPatchToCrdtSafe(baseOrOptions, head, patch, newDot, evalTestAgainst
|
|
|
1814
2345
|
}
|
|
1815
2346
|
/** Alias for codebases that prefer `try*` naming for non-throwing APIs. */
|
|
1816
2347
|
const tryJsonPatchToCrdt = jsonPatchToCrdtSafe;
|
|
2348
|
+
function nodeToJsonForPatch(node) {
|
|
2349
|
+
return node.kind === "lww" ? node.value : materialize(node);
|
|
2350
|
+
}
|
|
2351
|
+
function rebaseDiffOps(path, nestedOps, out) {
|
|
2352
|
+
const prefix = stringifyJsonPointer(path);
|
|
2353
|
+
for (const op of nestedOps) {
|
|
2354
|
+
const rebasedPath = prefix === "" ? op.path : op.path === "" ? prefix : `${prefix}${op.path}`;
|
|
2355
|
+
if (op.op === "remove") {
|
|
2356
|
+
out.push({
|
|
2357
|
+
op: "remove",
|
|
2358
|
+
path: rebasedPath
|
|
2359
|
+
});
|
|
2360
|
+
continue;
|
|
2361
|
+
}
|
|
2362
|
+
if (op.op === "add" || op.op === "replace") {
|
|
2363
|
+
out.push({
|
|
2364
|
+
op: op.op,
|
|
2365
|
+
path: rebasedPath,
|
|
2366
|
+
value: op.value
|
|
2367
|
+
});
|
|
2368
|
+
continue;
|
|
2369
|
+
}
|
|
2370
|
+
throw new Error(`Unexpected op '${op.op}' from diffJsonPatch`);
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
function nodesJsonEqual(baseNode, headNode, depth) {
|
|
2374
|
+
assertTraversalDepth(depth);
|
|
2375
|
+
if (baseNode === headNode) return true;
|
|
2376
|
+
if (baseNode.kind !== headNode.kind) return false;
|
|
2377
|
+
if (baseNode.kind === "lww") {
|
|
2378
|
+
const headLww = headNode;
|
|
2379
|
+
return jsonEquals(baseNode.value, headLww.value);
|
|
2380
|
+
}
|
|
2381
|
+
if (baseNode.kind === "obj") {
|
|
2382
|
+
const headObj = headNode;
|
|
2383
|
+
if (baseNode.entries.size !== headObj.entries.size) return false;
|
|
2384
|
+
for (const [key, baseEntry] of baseNode.entries.entries()) {
|
|
2385
|
+
const headEntry = headObj.entries.get(key);
|
|
2386
|
+
if (!headEntry) return false;
|
|
2387
|
+
if (!nodesJsonEqual(baseEntry.node, headEntry.node, depth + 1)) return false;
|
|
2388
|
+
}
|
|
2389
|
+
return true;
|
|
2390
|
+
}
|
|
2391
|
+
const headSeq = headNode;
|
|
2392
|
+
const baseCursor = rgaCreateLinearCursor(baseNode);
|
|
2393
|
+
const headCursor = rgaCreateLinearCursor(headSeq);
|
|
2394
|
+
while (true) {
|
|
2395
|
+
const baseElem = baseCursor.next();
|
|
2396
|
+
const headElem = headCursor.next();
|
|
2397
|
+
if (baseElem === void 0 || headElem === void 0) return baseElem === void 0 && headElem === void 0;
|
|
2398
|
+
if (!nodesJsonEqual(baseElem.value, headElem.value, depth + 1)) return false;
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
function diffObjectNodes(path, baseNode, headNode, options, ops, depth) {
|
|
2402
|
+
assertTraversalDepth(depth);
|
|
2403
|
+
const baseKeys = [...baseNode.entries.keys()].sort();
|
|
2404
|
+
const headKeys = [...headNode.entries.keys()].sort();
|
|
2405
|
+
let baseIndex = 0;
|
|
2406
|
+
let headIndex = 0;
|
|
2407
|
+
while (baseIndex < baseKeys.length && headIndex < headKeys.length) {
|
|
2408
|
+
const baseKey = baseKeys[baseIndex];
|
|
2409
|
+
const headKey = headKeys[headIndex];
|
|
2410
|
+
if (baseKey === headKey) {
|
|
2411
|
+
baseIndex += 1;
|
|
2412
|
+
headIndex += 1;
|
|
2413
|
+
continue;
|
|
2414
|
+
}
|
|
2415
|
+
if (baseKey < headKey) {
|
|
2416
|
+
path.push(baseKey);
|
|
2417
|
+
ops.push({
|
|
2418
|
+
op: "remove",
|
|
2419
|
+
path: stringifyJsonPointer(path)
|
|
2420
|
+
});
|
|
2421
|
+
path.pop();
|
|
2422
|
+
baseIndex += 1;
|
|
2423
|
+
continue;
|
|
2424
|
+
}
|
|
2425
|
+
headIndex += 1;
|
|
2426
|
+
}
|
|
2427
|
+
while (baseIndex < baseKeys.length) {
|
|
2428
|
+
const baseKey = baseKeys[baseIndex];
|
|
2429
|
+
path.push(baseKey);
|
|
2430
|
+
ops.push({
|
|
2431
|
+
op: "remove",
|
|
2432
|
+
path: stringifyJsonPointer(path)
|
|
2433
|
+
});
|
|
2434
|
+
path.pop();
|
|
2435
|
+
baseIndex += 1;
|
|
2436
|
+
}
|
|
2437
|
+
baseIndex = 0;
|
|
2438
|
+
headIndex = 0;
|
|
2439
|
+
while (baseIndex < baseKeys.length && headIndex < headKeys.length) {
|
|
2440
|
+
const baseKey = baseKeys[baseIndex];
|
|
2441
|
+
const headKey = headKeys[headIndex];
|
|
2442
|
+
if (baseKey === headKey) {
|
|
2443
|
+
baseIndex += 1;
|
|
2444
|
+
headIndex += 1;
|
|
2445
|
+
continue;
|
|
2446
|
+
}
|
|
2447
|
+
if (baseKey < headKey) {
|
|
2448
|
+
baseIndex += 1;
|
|
2449
|
+
continue;
|
|
2450
|
+
}
|
|
2451
|
+
const headEntry = headNode.entries.get(headKey);
|
|
2452
|
+
path.push(headKey);
|
|
2453
|
+
ops.push({
|
|
2454
|
+
op: "add",
|
|
2455
|
+
path: stringifyJsonPointer(path),
|
|
2456
|
+
value: nodeToJsonForPatch(headEntry.node)
|
|
2457
|
+
});
|
|
2458
|
+
path.pop();
|
|
2459
|
+
headIndex += 1;
|
|
2460
|
+
}
|
|
2461
|
+
while (headIndex < headKeys.length) {
|
|
2462
|
+
const headKey = headKeys[headIndex];
|
|
2463
|
+
const headEntry = headNode.entries.get(headKey);
|
|
2464
|
+
path.push(headKey);
|
|
2465
|
+
ops.push({
|
|
2466
|
+
op: "add",
|
|
2467
|
+
path: stringifyJsonPointer(path),
|
|
2468
|
+
value: nodeToJsonForPatch(headEntry.node)
|
|
2469
|
+
});
|
|
2470
|
+
path.pop();
|
|
2471
|
+
headIndex += 1;
|
|
2472
|
+
}
|
|
2473
|
+
baseIndex = 0;
|
|
2474
|
+
headIndex = 0;
|
|
2475
|
+
while (baseIndex < baseKeys.length && headIndex < headKeys.length) {
|
|
2476
|
+
const baseKey = baseKeys[baseIndex];
|
|
2477
|
+
const headKey = headKeys[headIndex];
|
|
2478
|
+
if (baseKey === headKey) {
|
|
2479
|
+
const baseEntry = baseNode.entries.get(baseKey);
|
|
2480
|
+
const headEntry = headNode.entries.get(headKey);
|
|
2481
|
+
if (!nodesJsonEqual(baseEntry.node, headEntry.node, depth + 1)) {
|
|
2482
|
+
path.push(baseKey);
|
|
2483
|
+
diffNodeToPatch(path, baseEntry.node, headEntry.node, options, ops, depth + 1);
|
|
2484
|
+
path.pop();
|
|
2485
|
+
}
|
|
2486
|
+
baseIndex += 1;
|
|
2487
|
+
headIndex += 1;
|
|
2488
|
+
continue;
|
|
2489
|
+
}
|
|
2490
|
+
if (baseKey < headKey) {
|
|
2491
|
+
baseIndex += 1;
|
|
2492
|
+
continue;
|
|
2493
|
+
}
|
|
2494
|
+
headIndex += 1;
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
function diffNodeToPatch(path, baseNode, headNode, options, ops, depth) {
|
|
2498
|
+
assertTraversalDepth(depth);
|
|
2499
|
+
if (baseNode === headNode) return;
|
|
2500
|
+
if (baseNode.kind !== headNode.kind) {
|
|
2501
|
+
ops.push({
|
|
2502
|
+
op: "replace",
|
|
2503
|
+
path: stringifyJsonPointer(path),
|
|
2504
|
+
value: nodeToJsonForPatch(headNode)
|
|
2505
|
+
});
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2508
|
+
if (baseNode.kind === "lww") {
|
|
2509
|
+
const headLww = headNode;
|
|
2510
|
+
if (jsonEquals(baseNode.value, headLww.value)) return;
|
|
2511
|
+
ops.push({
|
|
2512
|
+
op: "replace",
|
|
2513
|
+
path: stringifyJsonPointer(path),
|
|
2514
|
+
value: headLww.value
|
|
2515
|
+
});
|
|
2516
|
+
return;
|
|
2517
|
+
}
|
|
2518
|
+
if (baseNode.kind === "obj") {
|
|
2519
|
+
diffObjectNodes(path, baseNode, headNode, options, ops, depth);
|
|
2520
|
+
return;
|
|
2521
|
+
}
|
|
2522
|
+
const headSeq = headNode;
|
|
2523
|
+
rebaseDiffOps(path, diffJsonPatch(materialize(baseNode), materialize(headSeq), options), ops);
|
|
2524
|
+
}
|
|
1817
2525
|
/**
|
|
1818
2526
|
* Generate a JSON Patch delta between two CRDT documents.
|
|
1819
2527
|
* @param base - The base document snapshot.
|
|
1820
2528
|
* @param head - The current document state.
|
|
1821
|
-
* @param options - Diff options (e.g. `{ arrayStrategy: "lcs" }`).
|
|
2529
|
+
* @param options - Diff options (e.g. `{ arrayStrategy: "lcs" }` or `{ arrayStrategy: "lcs-linear" }`).
|
|
1822
2530
|
* @returns An array of JSON Patch operations that transform base into head.
|
|
1823
2531
|
*/
|
|
1824
2532
|
function crdtToJsonPatch(base, head, options) {
|
|
1825
|
-
return diffJsonPatch(materialize(base.root), materialize(head.root), options);
|
|
2533
|
+
if ((options?.jsonValidation ?? "none") !== "none") return diffJsonPatch(materialize(base.root), materialize(head.root), options);
|
|
2534
|
+
return crdtNodesToJsonPatch(base.root, head.root, options);
|
|
2535
|
+
}
|
|
2536
|
+
/** Internals-only helper for diffing CRDT nodes from an existing traversal depth. */
|
|
2537
|
+
function crdtNodesToJsonPatch(baseNode, headNode, options, depth = 0) {
|
|
2538
|
+
const ops = [];
|
|
2539
|
+
diffNodeToPatch([], baseNode, headNode, options ?? {}, ops, depth);
|
|
2540
|
+
return ops;
|
|
1826
2541
|
}
|
|
1827
2542
|
/**
|
|
1828
2543
|
* Emit a single root `replace` patch representing the full document state.
|
|
@@ -2125,9 +2840,12 @@ function toApplyPatchOptionsForActor(options) {
|
|
|
2125
2840
|
};
|
|
2126
2841
|
}
|
|
2127
2842
|
function applyPatchInternal(state, patch, options, execution) {
|
|
2843
|
+
const preparedPatch = preparePatchPayloadsSafe(patch, options.jsonValidation ?? "none");
|
|
2844
|
+
if (!preparedPatch.ok) return preparedPatch;
|
|
2845
|
+
const runtimePatch = preparedPatch.patch;
|
|
2128
2846
|
if ((options.semantics ?? "sequential") === "sequential") {
|
|
2129
2847
|
if (!options.base && execution === "batch") {
|
|
2130
|
-
const compiled =
|
|
2848
|
+
const compiled = compilePreparedIntents(materialize(state.doc.root), runtimePatch, "sequential");
|
|
2131
2849
|
if (!compiled.ok) return compiled;
|
|
2132
2850
|
return applyIntentsToCrdt(state.doc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
|
|
2133
2851
|
}
|
|
@@ -2135,10 +2853,15 @@ function applyPatchInternal(state, patch, options, execution) {
|
|
|
2135
2853
|
doc: cloneDoc(options.base.doc),
|
|
2136
2854
|
clock: createClock("__base__", 0)
|
|
2137
2855
|
} : null;
|
|
2856
|
+
const session = {
|
|
2857
|
+
pointerCache: /* @__PURE__ */ new Map(),
|
|
2858
|
+
baseShadowParentCache: /* @__PURE__ */ new Map(),
|
|
2859
|
+
headShadowParentCache: /* @__PURE__ */ new Map()
|
|
2860
|
+
};
|
|
2138
2861
|
let sequentialHeadJson = materialize(state.doc.root);
|
|
2139
2862
|
let sequentialBaseJson = explicitBaseState ? materialize(explicitBaseState.doc.root) : sequentialHeadJson;
|
|
2140
|
-
for (const [opIndex, op] of
|
|
2141
|
-
const step = applyPatchOpSequential(state, op, options, explicitBaseState ? explicitBaseState.doc : state.doc, sequentialBaseJson, sequentialHeadJson, explicitBaseState, opIndex);
|
|
2863
|
+
for (const [opIndex, op] of runtimePatch.entries()) {
|
|
2864
|
+
const step = applyPatchOpSequential(state, op, options, explicitBaseState ? explicitBaseState.doc : state.doc, sequentialBaseJson, sequentialHeadJson, explicitBaseState, opIndex, session);
|
|
2142
2865
|
if (!step.ok) return step;
|
|
2143
2866
|
sequentialBaseJson = step.baseJson;
|
|
2144
2867
|
sequentialHeadJson = step.headJson;
|
|
@@ -2146,29 +2869,29 @@ function applyPatchInternal(state, patch, options, execution) {
|
|
|
2146
2869
|
return { ok: true };
|
|
2147
2870
|
}
|
|
2148
2871
|
const baseDoc = options.base ? options.base.doc : cloneDoc(state.doc);
|
|
2149
|
-
const compiled =
|
|
2872
|
+
const compiled = compilePreparedIntents(materialize(baseDoc.root), runtimePatch, "base");
|
|
2150
2873
|
if (!compiled.ok) return compiled;
|
|
2151
2874
|
return applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
|
|
2152
2875
|
}
|
|
2153
|
-
function applyPatchOpSequential(state, op, options, baseDoc, baseJson, headJson, explicitBaseState, opIndex) {
|
|
2876
|
+
function applyPatchOpSequential(state, op, options, baseDoc, baseJson, headJson, explicitBaseState, opIndex, session) {
|
|
2154
2877
|
if (op.op === "move") {
|
|
2155
|
-
const fromResolved = resolveValueAtPointer(baseJson, op.from, opIndex);
|
|
2878
|
+
const fromResolved = resolveValueAtPointer(baseJson, op.from, opIndex, session.pointerCache);
|
|
2156
2879
|
if (!fromResolved.ok) return fromResolved;
|
|
2157
2880
|
const fromValue = structuredClone(fromResolved.value);
|
|
2158
2881
|
const removeRes = applySinglePatchOpSequentialStep(state, baseDoc, baseJson, headJson, {
|
|
2159
2882
|
op: "remove",
|
|
2160
2883
|
path: op.from
|
|
2161
|
-
}, options, explicitBaseState);
|
|
2884
|
+
}, options, explicitBaseState, opIndex, session);
|
|
2162
2885
|
if (!removeRes.ok) return removeRes;
|
|
2163
2886
|
const addOp = {
|
|
2164
2887
|
op: "add",
|
|
2165
2888
|
path: op.path,
|
|
2166
2889
|
value: fromValue
|
|
2167
2890
|
};
|
|
2168
|
-
if (!explicitBaseState) return applySinglePatchOpSequentialStep(state, state.doc, removeRes.baseJson, removeRes.headJson, addOp, options, null);
|
|
2169
|
-
const headAddRes = applySinglePatchOpSequentialStep(state, state.doc, removeRes.headJson, removeRes.headJson, addOp, options, null);
|
|
2891
|
+
if (!explicitBaseState) return applySinglePatchOpSequentialStep(state, state.doc, removeRes.baseJson, removeRes.headJson, addOp, options, null, opIndex, session);
|
|
2892
|
+
const headAddRes = applySinglePatchOpSequentialStep(state, state.doc, removeRes.headJson, removeRes.headJson, addOp, options, null, opIndex, session);
|
|
2170
2893
|
if (!headAddRes.ok) return headAddRes;
|
|
2171
|
-
const shadowAddRes = applySinglePatchOpExplicitShadowStep(explicitBaseState, removeRes.baseJson, addOp, options);
|
|
2894
|
+
const shadowAddRes = applySinglePatchOpExplicitShadowStep(explicitBaseState, removeRes.baseJson, addOp, options, opIndex, session);
|
|
2172
2895
|
if (!shadowAddRes.ok) return shadowAddRes;
|
|
2173
2896
|
return {
|
|
2174
2897
|
ok: true,
|
|
@@ -2177,18 +2900,18 @@ function applyPatchOpSequential(state, op, options, baseDoc, baseJson, headJson,
|
|
|
2177
2900
|
};
|
|
2178
2901
|
}
|
|
2179
2902
|
if (op.op === "copy") {
|
|
2180
|
-
const fromResolved = resolveValueAtPointer(baseJson, op.from, opIndex);
|
|
2903
|
+
const fromResolved = resolveValueAtPointer(baseJson, op.from, opIndex, session.pointerCache);
|
|
2181
2904
|
if (!fromResolved.ok) return fromResolved;
|
|
2182
2905
|
return applySinglePatchOpSequentialStep(state, baseDoc, baseJson, headJson, {
|
|
2183
2906
|
op: "add",
|
|
2184
2907
|
path: op.path,
|
|
2185
2908
|
value: structuredClone(fromResolved.value)
|
|
2186
|
-
}, options, explicitBaseState);
|
|
2909
|
+
}, options, explicitBaseState, opIndex, session);
|
|
2187
2910
|
}
|
|
2188
|
-
return applySinglePatchOpSequentialStep(state, baseDoc, baseJson, headJson, op, options, explicitBaseState);
|
|
2911
|
+
return applySinglePatchOpSequentialStep(state, baseDoc, baseJson, headJson, op, options, explicitBaseState, opIndex, session);
|
|
2189
2912
|
}
|
|
2190
|
-
function applySinglePatchOpSequentialStep(state, baseDoc, baseJson, headJson, op, options, explicitBaseState) {
|
|
2191
|
-
const compiled =
|
|
2913
|
+
function applySinglePatchOpSequentialStep(state, baseDoc, baseJson, headJson, op, options, explicitBaseState, opIndex, session) {
|
|
2914
|
+
const compiled = compilePreparedIntents(baseJson, [op], "base", session.pointerCache, opIndex);
|
|
2192
2915
|
if (!compiled.ok) return compiled;
|
|
2193
2916
|
const headStep = applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
|
|
2194
2917
|
if (!headStep.ok) return headStep;
|
|
@@ -2201,15 +2924,21 @@ function applySinglePatchOpSequentialStep(state, baseDoc, baseJson, headJson, op
|
|
|
2201
2924
|
baseJson,
|
|
2202
2925
|
headJson
|
|
2203
2926
|
};
|
|
2204
|
-
const nextBaseJson = applyJsonPatchOpToShadow(baseJson, op
|
|
2927
|
+
const nextBaseJson = applyJsonPatchOpToShadow(baseJson, op, explicitBaseState ? session.baseShadowParentCache : session.headShadowParentCache, {
|
|
2928
|
+
pointerCache: session.pointerCache,
|
|
2929
|
+
opIndex
|
|
2930
|
+
});
|
|
2205
2931
|
return {
|
|
2206
2932
|
ok: true,
|
|
2207
2933
|
baseJson: nextBaseJson,
|
|
2208
|
-
headJson: explicitBaseState ? applyJsonPatchOpToShadow(headJson, op
|
|
2934
|
+
headJson: explicitBaseState ? applyJsonPatchOpToShadow(headJson, op, session.headShadowParentCache, {
|
|
2935
|
+
pointerCache: session.pointerCache,
|
|
2936
|
+
opIndex
|
|
2937
|
+
}) : nextBaseJson
|
|
2209
2938
|
};
|
|
2210
2939
|
}
|
|
2211
|
-
function applySinglePatchOpExplicitShadowStep(explicitBaseState, baseJson, op, options) {
|
|
2212
|
-
const compiled =
|
|
2940
|
+
function applySinglePatchOpExplicitShadowStep(explicitBaseState, baseJson, op, options, opIndex, session) {
|
|
2941
|
+
const compiled = compilePreparedIntents(baseJson, [op], "base", session.pointerCache, opIndex);
|
|
2213
2942
|
if (!compiled.ok) return compiled;
|
|
2214
2943
|
const shadowStep = applyIntentsToCrdt(explicitBaseState.doc, explicitBaseState.doc, compiled.intents, () => explicitBaseState.clock.next(), "base", (ctr) => bumpClockCounter(explicitBaseState, ctr), { strictParents: options.strictParents });
|
|
2215
2944
|
if (!shadowStep.ok) return shadowStep;
|
|
@@ -2219,32 +2948,46 @@ function applySinglePatchOpExplicitShadowStep(explicitBaseState, baseJson, op, o
|
|
|
2219
2948
|
};
|
|
2220
2949
|
return {
|
|
2221
2950
|
ok: true,
|
|
2222
|
-
baseJson: applyJsonPatchOpToShadow(baseJson, op
|
|
2951
|
+
baseJson: applyJsonPatchOpToShadow(baseJson, op, session.baseShadowParentCache, {
|
|
2952
|
+
pointerCache: session.pointerCache,
|
|
2953
|
+
opIndex
|
|
2954
|
+
})
|
|
2223
2955
|
};
|
|
2224
2956
|
}
|
|
2225
|
-
function applyJsonPatchOpToShadow(baseJson, op) {
|
|
2226
|
-
|
|
2957
|
+
function applyJsonPatchOpToShadow(baseJson, op, parentCache, pointerContext) {
|
|
2958
|
+
let path;
|
|
2959
|
+
try {
|
|
2960
|
+
path = parsePointerWithCache(op.path, pointerContext.pointerCache);
|
|
2961
|
+
} catch (error) {
|
|
2962
|
+
throw toPointerParseCompileError(error, op.path, pointerContext.opIndex);
|
|
2963
|
+
}
|
|
2227
2964
|
if (path.length === 0) {
|
|
2965
|
+
parentCache.clear();
|
|
2228
2966
|
if (op.op === "test") return baseJson;
|
|
2229
2967
|
if (op.op === "remove") return null;
|
|
2230
2968
|
return structuredClone(op.value);
|
|
2231
2969
|
}
|
|
2970
|
+
const pathPointer = op.path;
|
|
2232
2971
|
const parentPath = path.slice(0, -1);
|
|
2972
|
+
const parentPointer = pointerParent(pathPointer);
|
|
2233
2973
|
const key = path[path.length - 1];
|
|
2234
|
-
const parent =
|
|
2974
|
+
const parent = resolveShadowParent(baseJson, parentPath, parentPointer, parentCache);
|
|
2235
2975
|
if (Array.isArray(parent)) {
|
|
2236
2976
|
const idx = key === "-" ? parent.length : Number(key);
|
|
2237
2977
|
if (!Number.isInteger(idx)) throw new Error(`Invalid array index ${key}`);
|
|
2238
2978
|
if (op.op === "add") {
|
|
2239
2979
|
parent.splice(idx, 0, structuredClone(op.value));
|
|
2980
|
+
invalidateArrayShadowParentCache(parentCache, parentPointer);
|
|
2240
2981
|
return baseJson;
|
|
2241
2982
|
}
|
|
2242
2983
|
if (op.op === "remove") {
|
|
2243
2984
|
parent.splice(idx, 1);
|
|
2985
|
+
invalidateArrayShadowParentCache(parentCache, parentPointer);
|
|
2244
2986
|
return baseJson;
|
|
2245
2987
|
}
|
|
2246
2988
|
if (op.op === "replace") {
|
|
2247
2989
|
parent[idx] = structuredClone(op.value);
|
|
2990
|
+
invalidateShadowPointerCache(parentCache, pathPointer);
|
|
2248
2991
|
return baseJson;
|
|
2249
2992
|
}
|
|
2250
2993
|
return baseJson;
|
|
@@ -2252,18 +2995,57 @@ function applyJsonPatchOpToShadow(baseJson, op) {
|
|
|
2252
2995
|
const obj = parent;
|
|
2253
2996
|
if (op.op === "add" || op.op === "replace") {
|
|
2254
2997
|
obj[key] = structuredClone(op.value);
|
|
2998
|
+
invalidateShadowPointerCache(parentCache, pathPointer);
|
|
2255
2999
|
return baseJson;
|
|
2256
3000
|
}
|
|
2257
3001
|
if (op.op === "remove") {
|
|
2258
3002
|
delete obj[key];
|
|
3003
|
+
invalidateShadowPointerCache(parentCache, pathPointer);
|
|
2259
3004
|
return baseJson;
|
|
2260
3005
|
}
|
|
2261
3006
|
return baseJson;
|
|
2262
3007
|
}
|
|
2263
|
-
function
|
|
3008
|
+
function resolveShadowParent(baseJson, parentPath, parentPointer, parentCache) {
|
|
3009
|
+
const cachedParent = parentCache.get(parentPointer);
|
|
3010
|
+
if (cachedParent !== void 0) return cachedParent;
|
|
3011
|
+
const parentValue = parentPath.length === 0 ? baseJson : getAtJson(baseJson, parentPath);
|
|
3012
|
+
if (!Array.isArray(parentValue) && !(parentValue && typeof parentValue === "object")) throw new Error(`Cannot mutate JSON shadow at non-container parent ${parentPointer || "<root>"}`);
|
|
3013
|
+
parentCache.set(parentPointer, parentValue);
|
|
3014
|
+
return parentValue;
|
|
3015
|
+
}
|
|
3016
|
+
function invalidateShadowPointerCache(parentCache, pointer) {
|
|
3017
|
+
if (pointer === "") {
|
|
3018
|
+
parentCache.clear();
|
|
3019
|
+
return;
|
|
3020
|
+
}
|
|
3021
|
+
const pointerPrefix = `${pointer}/`;
|
|
3022
|
+
for (const cachedPointer of parentCache.keys()) if (cachedPointer === pointer || cachedPointer.startsWith(pointerPrefix)) parentCache.delete(cachedPointer);
|
|
3023
|
+
}
|
|
3024
|
+
function invalidateArrayShadowParentCache(parentCache, parentPointer) {
|
|
3025
|
+
if (parentPointer === "") {
|
|
3026
|
+
for (const cachedPointer of parentCache.keys()) if (cachedPointer !== "") parentCache.delete(cachedPointer);
|
|
3027
|
+
return;
|
|
3028
|
+
}
|
|
3029
|
+
const pointerPrefix = `${parentPointer}/`;
|
|
3030
|
+
for (const cachedPointer of parentCache.keys()) if (cachedPointer.startsWith(pointerPrefix)) parentCache.delete(cachedPointer);
|
|
3031
|
+
}
|
|
3032
|
+
function pointerParent(pointer) {
|
|
3033
|
+
if (pointer === "") return "";
|
|
3034
|
+
const lastSlash = pointer.lastIndexOf("/");
|
|
3035
|
+
if (lastSlash <= 0) return "";
|
|
3036
|
+
return pointer.slice(0, lastSlash);
|
|
3037
|
+
}
|
|
3038
|
+
function parsePointerWithCache(pointer, pointerCache) {
|
|
3039
|
+
const cachedPath = pointerCache.get(pointer);
|
|
3040
|
+
if (cachedPath !== void 0) return cachedPath.slice();
|
|
3041
|
+
const parsedPath = parseJsonPointer(pointer);
|
|
3042
|
+
pointerCache.set(pointer, parsedPath);
|
|
3043
|
+
return parsedPath.slice();
|
|
3044
|
+
}
|
|
3045
|
+
function resolveValueAtPointer(baseJson, pointer, opIndex, pointerCache) {
|
|
2264
3046
|
let path;
|
|
2265
3047
|
try {
|
|
2266
|
-
path =
|
|
3048
|
+
path = parsePointerWithCache(pointer, pointerCache);
|
|
2267
3049
|
} catch (error) {
|
|
2268
3050
|
return toPointerParseApplyError(error, pointer, opIndex);
|
|
2269
3051
|
}
|
|
@@ -2279,11 +3061,33 @@ function resolveValueAtPointer(baseJson, pointer, opIndex) {
|
|
|
2279
3061
|
function bumpClockCounter(state, ctr) {
|
|
2280
3062
|
if (state.clock.ctr < ctr) state.clock.ctr = ctr;
|
|
2281
3063
|
}
|
|
2282
|
-
function
|
|
3064
|
+
function compilePreparedIntents(baseJson, patch, semantics = "sequential", pointerCache, opIndexOffset = 0) {
|
|
3065
|
+
try {
|
|
3066
|
+
const compileOptions = toCompilePatchOptions(semantics, pointerCache, opIndexOffset);
|
|
3067
|
+
if (patch.length === 1) return {
|
|
3068
|
+
ok: true,
|
|
3069
|
+
intents: compileJsonPatchOpToIntent(baseJson, patch[0], compileOptions)
|
|
3070
|
+
};
|
|
3071
|
+
return {
|
|
3072
|
+
ok: true,
|
|
3073
|
+
intents: compileJsonPatchToIntent(baseJson, patch, compileOptions)
|
|
3074
|
+
};
|
|
3075
|
+
} catch (error) {
|
|
3076
|
+
return toApplyError(error);
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
function toCompilePatchOptions(semantics, pointerCache, opIndexOffset = 0) {
|
|
3080
|
+
return {
|
|
3081
|
+
semantics,
|
|
3082
|
+
pointerCache,
|
|
3083
|
+
opIndexOffset
|
|
3084
|
+
};
|
|
3085
|
+
}
|
|
3086
|
+
function preparePatchPayloadsSafe(patch, mode) {
|
|
2283
3087
|
try {
|
|
2284
3088
|
return {
|
|
2285
3089
|
ok: true,
|
|
2286
|
-
|
|
3090
|
+
patch: preparePatchPayloads(patch, mode)
|
|
2287
3091
|
};
|
|
2288
3092
|
} catch (error) {
|
|
2289
3093
|
return toApplyError(error);
|
|
@@ -2384,6 +3188,9 @@ function toPointerParseApplyError(error, pointer, opIndex) {
|
|
|
2384
3188
|
opIndex
|
|
2385
3189
|
};
|
|
2386
3190
|
}
|
|
3191
|
+
function toPointerParseCompileError(error, pointer, opIndex) {
|
|
3192
|
+
return new PatchCompileError("INVALID_POINTER", error instanceof Error ? error.message : "invalid pointer", pointer, opIndex);
|
|
3193
|
+
}
|
|
2387
3194
|
function toPointerLookupApplyError(error, pointer, opIndex) {
|
|
2388
3195
|
const mapped = mapLookupErrorToPatchReason(error);
|
|
2389
3196
|
return {
|
|
@@ -2463,10 +3270,13 @@ function deserializeState(data) {
|
|
|
2463
3270
|
if (!("doc" in data)) fail("INVALID_SERIALIZED_SHAPE", "/doc", "serialized state is missing doc");
|
|
2464
3271
|
if (!("clock" in data)) fail("INVALID_SERIALIZED_SHAPE", "/clock", "serialized state is missing clock");
|
|
2465
3272
|
const clockRaw = asRecord(data.clock, "/clock");
|
|
2466
|
-
const
|
|
3273
|
+
const actor = readActor(clockRaw.actor, "/clock/actor");
|
|
3274
|
+
const ctr = readCounter(clockRaw.ctr, "/clock/ctr");
|
|
3275
|
+
const doc = deserializeDoc(data.doc);
|
|
3276
|
+
const observedCtr = maxObservedCounterForActorInNode(doc.root, actor);
|
|
2467
3277
|
return {
|
|
2468
|
-
doc
|
|
2469
|
-
clock
|
|
3278
|
+
doc,
|
|
3279
|
+
clock: createClock(actor, Math.max(ctr, observedCtr))
|
|
2470
3280
|
};
|
|
2471
3281
|
}
|
|
2472
3282
|
/** Non-throwing `deserializeState` variant with typed validation details. */
|
|
@@ -2515,16 +3325,23 @@ function serializeNode(node) {
|
|
|
2515
3325
|
};
|
|
2516
3326
|
}
|
|
2517
3327
|
const elems = createSerializedRecord();
|
|
2518
|
-
for (const [id, e] of node.elems.entries())
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
3328
|
+
for (const [id, e] of node.elems.entries()) {
|
|
3329
|
+
const serializedElem = {
|
|
3330
|
+
id: e.id,
|
|
3331
|
+
prev: e.prev,
|
|
3332
|
+
tombstone: e.tombstone,
|
|
3333
|
+
value: serializeNode(e.value),
|
|
3334
|
+
insDot: {
|
|
3335
|
+
actor: e.insDot.actor,
|
|
3336
|
+
ctr: e.insDot.ctr
|
|
3337
|
+
}
|
|
3338
|
+
};
|
|
3339
|
+
if (e.delDot) serializedElem.delDot = {
|
|
3340
|
+
actor: e.delDot.actor,
|
|
3341
|
+
ctr: e.delDot.ctr
|
|
3342
|
+
};
|
|
3343
|
+
setSerializedRecordValue(elems, id, serializedElem);
|
|
3344
|
+
}
|
|
2528
3345
|
return {
|
|
2529
3346
|
kind: "seq",
|
|
2530
3347
|
elems
|
|
@@ -2575,11 +3392,14 @@ function deserializeNode(node, path, depth) {
|
|
|
2575
3392
|
const tombstone = readBoolean(elem.tombstone, `${elemPath}/tombstone`);
|
|
2576
3393
|
const value = deserializeNode(elem.value, `${elemPath}/value`, depth + 1);
|
|
2577
3394
|
const insDot = readDot(elem.insDot, `${elemPath}/insDot`);
|
|
3395
|
+
const delDot = "delDot" in elem && elem.delDot !== void 0 ? readDot(elem.delDot, `${elemPath}/delDot`) : void 0;
|
|
2578
3396
|
if (dotToElemId(insDot) !== id) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/insDot`, "sequence element id must match its insertion dot");
|
|
3397
|
+
if (!tombstone && delDot) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/delDot`, "live sequence elements must not include delete metadata");
|
|
2579
3398
|
elems.set(id, {
|
|
2580
3399
|
id,
|
|
2581
3400
|
prev,
|
|
2582
3401
|
tombstone,
|
|
3402
|
+
delDot,
|
|
2583
3403
|
value,
|
|
2584
3404
|
insDot
|
|
2585
3405
|
});
|
|
@@ -2614,6 +3434,24 @@ function assertAcyclicRgaPredecessors(elems, path) {
|
|
|
2614
3434
|
for (const id of trail) visitState.set(id, 2);
|
|
2615
3435
|
}
|
|
2616
3436
|
}
|
|
3437
|
+
function maxObservedCounterForActorInNode(node, actor) {
|
|
3438
|
+
if (node.kind === "lww") return node.dot.actor === actor ? node.dot.ctr : 0;
|
|
3439
|
+
if (node.kind === "obj") {
|
|
3440
|
+
let maxCtr = 0;
|
|
3441
|
+
for (const entry of node.entries.values()) {
|
|
3442
|
+
if (entry.dot.actor === actor) maxCtr = Math.max(maxCtr, entry.dot.ctr);
|
|
3443
|
+
maxCtr = Math.max(maxCtr, maxObservedCounterForActorInNode(entry.node, actor));
|
|
3444
|
+
}
|
|
3445
|
+
for (const tombstoneDot of node.tombstone.values()) if (tombstoneDot.actor === actor) maxCtr = Math.max(maxCtr, tombstoneDot.ctr);
|
|
3446
|
+
return maxCtr;
|
|
3447
|
+
}
|
|
3448
|
+
let maxCtr = 0;
|
|
3449
|
+
for (const elem of node.elems.values()) {
|
|
3450
|
+
if (elem.insDot.actor === actor) maxCtr = Math.max(maxCtr, elem.insDot.ctr);
|
|
3451
|
+
maxCtr = Math.max(maxCtr, maxObservedCounterForActorInNode(elem.value, actor));
|
|
3452
|
+
}
|
|
3453
|
+
return maxCtr;
|
|
3454
|
+
}
|
|
2617
3455
|
function asRecord(value, path) {
|
|
2618
3456
|
if (!isRecord(value)) fail("INVALID_SERIALIZED_SHAPE", path, "expected object");
|
|
2619
3457
|
return value;
|
|
@@ -2958,6 +3796,7 @@ function mergeSeq(a, b, depth, path) {
|
|
|
2958
3796
|
id,
|
|
2959
3797
|
prev: ea.prev,
|
|
2960
3798
|
tombstone: ea.tombstone || eb.tombstone,
|
|
3799
|
+
delDot: mergeDeleteDot(ea.delDot, eb.delDot),
|
|
2961
3800
|
value: mergedValue,
|
|
2962
3801
|
insDot: { ...ea.insDot }
|
|
2963
3802
|
});
|
|
@@ -2982,10 +3821,16 @@ function cloneElem(e, depth) {
|
|
|
2982
3821
|
id: e.id,
|
|
2983
3822
|
prev: e.prev,
|
|
2984
3823
|
tombstone: e.tombstone,
|
|
3824
|
+
delDot: e.delDot ? { ...e.delDot } : void 0,
|
|
2985
3825
|
value: cloneNodeShallow(e.value, depth + 1),
|
|
2986
3826
|
insDot: { ...e.insDot }
|
|
2987
3827
|
};
|
|
2988
3828
|
}
|
|
3829
|
+
function mergeDeleteDot(a, b) {
|
|
3830
|
+
if (a && b) return compareDot(a, b) >= 0 ? { ...a } : { ...b };
|
|
3831
|
+
if (a) return { ...a };
|
|
3832
|
+
if (b) return { ...b };
|
|
3833
|
+
}
|
|
2989
3834
|
function cloneNodeShallow(node, depth) {
|
|
2990
3835
|
assertTraversalDepth(depth);
|
|
2991
3836
|
switch (node.kind) {
|
|
@@ -3219,6 +4064,12 @@ Object.defineProperty(exports, 'compileJsonPatchToIntent', {
|
|
|
3219
4064
|
return compileJsonPatchToIntent;
|
|
3220
4065
|
}
|
|
3221
4066
|
});
|
|
4067
|
+
Object.defineProperty(exports, 'crdtNodesToJsonPatch', {
|
|
4068
|
+
enumerable: true,
|
|
4069
|
+
get: function () {
|
|
4070
|
+
return crdtNodesToJsonPatch;
|
|
4071
|
+
}
|
|
4072
|
+
});
|
|
3222
4073
|
Object.defineProperty(exports, 'crdtToFullReplace', {
|
|
3223
4074
|
enumerable: true,
|
|
3224
4075
|
get: function () {
|
|
@@ -3411,6 +4262,12 @@ Object.defineProperty(exports, 'rgaInsertAfter', {
|
|
|
3411
4262
|
return rgaInsertAfter;
|
|
3412
4263
|
}
|
|
3413
4264
|
});
|
|
4265
|
+
Object.defineProperty(exports, 'rgaInsertAfterChecked', {
|
|
4266
|
+
enumerable: true,
|
|
4267
|
+
get: function () {
|
|
4268
|
+
return rgaInsertAfterChecked;
|
|
4269
|
+
}
|
|
4270
|
+
});
|
|
3414
4271
|
Object.defineProperty(exports, 'rgaLinearizeIds', {
|
|
3415
4272
|
enumerable: true,
|
|
3416
4273
|
get: function () {
|
|
@@ -3501,6 +4358,12 @@ Object.defineProperty(exports, 'validateJsonPatch', {
|
|
|
3501
4358
|
return validateJsonPatch;
|
|
3502
4359
|
}
|
|
3503
4360
|
});
|
|
4361
|
+
Object.defineProperty(exports, 'validateRgaSeq', {
|
|
4362
|
+
enumerable: true,
|
|
4363
|
+
get: function () {
|
|
4364
|
+
return validateRgaSeq;
|
|
4365
|
+
}
|
|
4366
|
+
});
|
|
3504
4367
|
Object.defineProperty(exports, 'vvHasDot', {
|
|
3505
4368
|
enumerable: true,
|
|
3506
4369
|
get: function () {
|