json-patch-to-crdt 0.3.0 → 0.5.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 +84 -1
- package/dist/{compact-BToZE6Q6.js → compact-CDvajUfn.js} +1602 -621
- package/dist/{compact-BS7F604m.mjs → compact-Dj0BYeY5.mjs} +1715 -764
- package/dist/{depth-BTHjgY18.d.mts → depth-CM1kCxhm.d.mts} +101 -19
- package/dist/{depth-DSl2ghKu.d.ts → depth-NbZ6Giq9.d.ts} +101 -19
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +6 -2
- package/dist/index.mjs +2 -2
- package/dist/internals.d.mts +11 -4
- package/dist/internals.d.ts +11 -4
- package/dist/internals.js +6 -1
- package/dist/internals.mjs +2 -2
- package/package.json +1 -1
|
@@ -1,19 +1,39 @@
|
|
|
1
1
|
|
|
2
|
-
//#region src/
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
//#region src/depth.ts
|
|
3
|
+
const MAX_TRAVERSAL_DEPTH = 16384;
|
|
4
|
+
var TraversalDepthError = class extends Error {
|
|
5
|
+
code = 409;
|
|
6
|
+
reason = "MAX_DEPTH_EXCEEDED";
|
|
7
|
+
depth;
|
|
8
|
+
maxDepth;
|
|
9
|
+
constructor(depth, maxDepth = MAX_TRAVERSAL_DEPTH) {
|
|
10
|
+
super(`maximum nesting depth ${maxDepth} exceeded at depth ${depth}`);
|
|
11
|
+
this.name = "TraversalDepthError";
|
|
12
|
+
this.depth = depth;
|
|
13
|
+
this.maxDepth = maxDepth;
|
|
9
14
|
}
|
|
10
15
|
};
|
|
11
|
-
function
|
|
12
|
-
if (
|
|
16
|
+
function assertTraversalDepth(depth, maxDepth = MAX_TRAVERSAL_DEPTH) {
|
|
17
|
+
if (depth > maxDepth) throw new TraversalDepthError(depth, maxDepth);
|
|
18
|
+
}
|
|
19
|
+
function toDepthApplyError(error) {
|
|
20
|
+
return {
|
|
21
|
+
ok: false,
|
|
22
|
+
code: error.code,
|
|
23
|
+
reason: error.reason,
|
|
24
|
+
message: error.message
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/version-vector.ts
|
|
30
|
+
let observedVersionVectorObserverForTests = null;
|
|
31
|
+
function readVersionVectorCounter(vv, actor) {
|
|
32
|
+
if (!Object.prototype.hasOwnProperty.call(vv, actor)) return;
|
|
13
33
|
const counter = vv[actor];
|
|
14
|
-
return typeof counter === "number" ? counter : 0;
|
|
34
|
+
return typeof counter === "number" ? counter : void 0;
|
|
15
35
|
}
|
|
16
|
-
function
|
|
36
|
+
function writeVersionVectorCounter(vv, actor, counter) {
|
|
17
37
|
Object.defineProperty(vv, actor, {
|
|
18
38
|
configurable: true,
|
|
19
39
|
enumerable: true,
|
|
@@ -21,6 +41,105 @@ function writeVvCounter$1(vv, actor, counter) {
|
|
|
21
41
|
writable: true
|
|
22
42
|
});
|
|
23
43
|
}
|
|
44
|
+
function observeVersionVectorDot(vv, dot) {
|
|
45
|
+
if ((readVersionVectorCounter(vv, dot.actor) ?? 0) < dot.ctr) writeVersionVectorCounter(vv, dot.actor, dot.ctr);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Inspect a document or state and return the highest observed counter per actor.
|
|
49
|
+
*
|
|
50
|
+
* When a `CrdtState` is provided, the returned vector is also seeded from the
|
|
51
|
+
* state's local clock so callers do not lose counters that have advanced ahead
|
|
52
|
+
* of the currently materialized document tree.
|
|
53
|
+
*/
|
|
54
|
+
function observedVersionVector(target) {
|
|
55
|
+
observedVersionVectorObserverForTests?.(target);
|
|
56
|
+
const doc = "doc" in target ? target.doc : target;
|
|
57
|
+
const vv = Object.create(null);
|
|
58
|
+
if ("clock" in target) observeVersionVectorDot(vv, {
|
|
59
|
+
actor: target.clock.actor,
|
|
60
|
+
ctr: target.clock.ctr
|
|
61
|
+
});
|
|
62
|
+
const stack = [{
|
|
63
|
+
node: doc.root,
|
|
64
|
+
depth: 0
|
|
65
|
+
}];
|
|
66
|
+
while (stack.length > 0) {
|
|
67
|
+
const frame = stack.pop();
|
|
68
|
+
assertTraversalDepth(frame.depth);
|
|
69
|
+
if (frame.node.kind === "lww") {
|
|
70
|
+
observeVersionVectorDot(vv, frame.node.dot);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (frame.node.kind === "obj") {
|
|
74
|
+
for (const entry of frame.node.entries.values()) {
|
|
75
|
+
observeVersionVectorDot(vv, entry.dot);
|
|
76
|
+
stack.push({
|
|
77
|
+
node: entry.node,
|
|
78
|
+
depth: frame.depth + 1
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
for (const tombstone of frame.node.tombstone.values()) observeVersionVectorDot(vv, tombstone);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
for (const elem of frame.node.elems.values()) {
|
|
85
|
+
observeVersionVectorDot(vv, elem.insDot);
|
|
86
|
+
if (elem.delDot) observeVersionVectorDot(vv, elem.delDot);
|
|
87
|
+
stack.push({
|
|
88
|
+
node: elem.value,
|
|
89
|
+
depth: frame.depth + 1
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return vv;
|
|
94
|
+
}
|
|
95
|
+
/** Combine version vectors using per-actor maxima. */
|
|
96
|
+
function mergeVersionVectors(...vectors) {
|
|
97
|
+
const merged = Object.create(null);
|
|
98
|
+
for (const vv of vectors) for (const actor of Object.keys(vv)) {
|
|
99
|
+
const counter = readVersionVectorCounter(vv, actor);
|
|
100
|
+
if (counter === void 0) continue;
|
|
101
|
+
writeVersionVectorCounter(merged, actor, Math.max(readVersionVectorCounter(merged, actor) ?? 0, counter));
|
|
102
|
+
}
|
|
103
|
+
return merged;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Derive a causally-stable checkpoint by taking the per-actor minimum.
|
|
107
|
+
*
|
|
108
|
+
* When called with a single vector the result equals that vector. In practice,
|
|
109
|
+
* a meaningful shared-stability checkpoint usually needs acknowledgements from
|
|
110
|
+
* at least two peers or from an explicit quorum.
|
|
111
|
+
*/
|
|
112
|
+
function intersectVersionVectors(...vectors) {
|
|
113
|
+
if (vectors.length === 0) return Object.create(null);
|
|
114
|
+
const actors = /* @__PURE__ */ new Set();
|
|
115
|
+
for (const vv of vectors) for (const actor of Object.keys(vv)) actors.add(actor);
|
|
116
|
+
const intersection = Object.create(null);
|
|
117
|
+
for (const actor of actors) {
|
|
118
|
+
const counters = vectors.map((vv) => readVersionVectorCounter(vv, actor) ?? 0);
|
|
119
|
+
const counter = Math.min(...counters);
|
|
120
|
+
if (counter > 0) writeVersionVectorCounter(intersection, actor, counter);
|
|
121
|
+
}
|
|
122
|
+
return intersection;
|
|
123
|
+
}
|
|
124
|
+
/** Check whether one version vector has observed every counter in another. */
|
|
125
|
+
function versionVectorCovers(observed, required) {
|
|
126
|
+
for (const actor of Object.keys(required)) {
|
|
127
|
+
const requiredCounter = readVersionVectorCounter(required, actor) ?? 0;
|
|
128
|
+
if ((readVersionVectorCounter(observed, actor) ?? 0) < requiredCounter) return false;
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/clock.ts
|
|
135
|
+
var ClockValidationError = class extends TypeError {
|
|
136
|
+
reason;
|
|
137
|
+
constructor(reason, message) {
|
|
138
|
+
super(message);
|
|
139
|
+
this.name = "ClockValidationError";
|
|
140
|
+
this.reason = reason;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
24
143
|
/**
|
|
25
144
|
* Create a new clock for the given actor. Each call to `clock.next()` yields a fresh `Dot`.
|
|
26
145
|
* @param actor - Unique identifier for this peer.
|
|
@@ -57,8 +176,8 @@ function cloneClock(clock) {
|
|
|
57
176
|
* Useful when a server needs to mint dots for many actors.
|
|
58
177
|
*/
|
|
59
178
|
function nextDotForActor(vv, actor) {
|
|
60
|
-
const ctr =
|
|
61
|
-
|
|
179
|
+
const ctr = (readVersionVectorCounter(vv, actor) ?? 0) + 1;
|
|
180
|
+
writeVersionVectorCounter(vv, actor, ctr);
|
|
62
181
|
return {
|
|
63
182
|
actor,
|
|
64
183
|
ctr
|
|
@@ -66,63 +185,20 @@ function nextDotForActor(vv, actor) {
|
|
|
66
185
|
}
|
|
67
186
|
/** Record an observed dot in a version vector. */
|
|
68
187
|
function observeDot(vv, dot) {
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
//#endregion
|
|
73
|
-
//#region src/depth.ts
|
|
74
|
-
const MAX_TRAVERSAL_DEPTH = 16384;
|
|
75
|
-
var TraversalDepthError = class extends Error {
|
|
76
|
-
code = 409;
|
|
77
|
-
reason = "MAX_DEPTH_EXCEEDED";
|
|
78
|
-
depth;
|
|
79
|
-
maxDepth;
|
|
80
|
-
constructor(depth, maxDepth = MAX_TRAVERSAL_DEPTH) {
|
|
81
|
-
super(`maximum nesting depth ${maxDepth} exceeded at depth ${depth}`);
|
|
82
|
-
this.name = "TraversalDepthError";
|
|
83
|
-
this.depth = depth;
|
|
84
|
-
this.maxDepth = maxDepth;
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
function assertTraversalDepth(depth, maxDepth = MAX_TRAVERSAL_DEPTH) {
|
|
88
|
-
if (depth > maxDepth) throw new TraversalDepthError(depth, maxDepth);
|
|
89
|
-
}
|
|
90
|
-
function toDepthApplyError(error) {
|
|
91
|
-
return {
|
|
92
|
-
ok: false,
|
|
93
|
-
code: error.code,
|
|
94
|
-
reason: error.reason,
|
|
95
|
-
message: error.message
|
|
96
|
-
};
|
|
188
|
+
observeVersionVectorDot(vv, dot);
|
|
97
189
|
}
|
|
98
190
|
|
|
99
191
|
//#endregion
|
|
100
192
|
//#region src/dot.ts
|
|
101
|
-
function readVvCounter(vv, actor) {
|
|
102
|
-
if (!Object.prototype.hasOwnProperty.call(vv, actor)) return;
|
|
103
|
-
const counter = vv[actor];
|
|
104
|
-
return typeof counter === "number" ? counter : void 0;
|
|
105
|
-
}
|
|
106
|
-
function writeVvCounter(vv, actor, counter) {
|
|
107
|
-
Object.defineProperty(vv, actor, {
|
|
108
|
-
configurable: true,
|
|
109
|
-
enumerable: true,
|
|
110
|
-
value: counter,
|
|
111
|
-
writable: true
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
193
|
function compareDot(a, b) {
|
|
115
194
|
if (a.ctr !== b.ctr) return a.ctr - b.ctr;
|
|
116
195
|
return a.actor < b.actor ? -1 : a.actor > b.actor ? 1 : 0;
|
|
117
196
|
}
|
|
118
197
|
function vvHasDot(vv, d) {
|
|
119
|
-
return (
|
|
198
|
+
return (readVersionVectorCounter(vv, d.actor) ?? 0) >= d.ctr;
|
|
120
199
|
}
|
|
121
200
|
function vvMerge(a, b) {
|
|
122
|
-
|
|
123
|
-
for (const [actor, ctr] of Object.entries(a)) writeVvCounter(out, actor, ctr);
|
|
124
|
-
for (const [actor, ctr] of Object.entries(b)) writeVvCounter(out, actor, Math.max(readVvCounter(out, actor) ?? 0, ctr));
|
|
125
|
-
return out;
|
|
201
|
+
return mergeVersionVectors(a, b);
|
|
126
202
|
}
|
|
127
203
|
function dotToElemId(d) {
|
|
128
204
|
return `${d.actor}:${d.ctr}`;
|
|
@@ -206,6 +282,12 @@ function rgaLinearizeIds(seq) {
|
|
|
206
282
|
});
|
|
207
283
|
return [...out];
|
|
208
284
|
}
|
|
285
|
+
function rgaLength(seq) {
|
|
286
|
+
const ver = getVersion(seq);
|
|
287
|
+
const cached = linearCache.get(seq);
|
|
288
|
+
if (cached && cached.version === ver) return cached.ids.length;
|
|
289
|
+
return rgaLinearizeIds(seq).length;
|
|
290
|
+
}
|
|
209
291
|
function rgaCreateIndexedIdSnapshot(seq) {
|
|
210
292
|
const ids = rgaLinearizeIds(seq);
|
|
211
293
|
return {
|
|
@@ -413,6 +495,8 @@ function rgaPrevForInsertAtIndex(seq, index) {
|
|
|
413
495
|
|
|
414
496
|
//#endregion
|
|
415
497
|
//#region src/materialize.ts
|
|
498
|
+
let materializeObserver = null;
|
|
499
|
+
const EMPTY_PATH = [];
|
|
416
500
|
function createMaterializedObject() {
|
|
417
501
|
return Object.create(null);
|
|
418
502
|
}
|
|
@@ -426,6 +510,8 @@ function setMaterializedProperty(out, key, value) {
|
|
|
426
510
|
}
|
|
427
511
|
/** Convert a CRDT node graph into a plain JSON value using an explicit stack. */
|
|
428
512
|
function materialize(node) {
|
|
513
|
+
const observer = materializeObserver;
|
|
514
|
+
observer?.(EMPTY_PATH, node);
|
|
429
515
|
if (node.kind === "lww") return node.value;
|
|
430
516
|
const root = node.kind === "obj" ? createMaterializedObject() : [];
|
|
431
517
|
const stack = [];
|
|
@@ -433,13 +519,16 @@ function materialize(node) {
|
|
|
433
519
|
kind: "obj",
|
|
434
520
|
depth: 0,
|
|
435
521
|
entries: node.entries.entries(),
|
|
436
|
-
out: root
|
|
522
|
+
out: root,
|
|
523
|
+
path: []
|
|
437
524
|
});
|
|
438
525
|
else stack.push({
|
|
439
526
|
kind: "seq",
|
|
440
527
|
depth: 0,
|
|
441
528
|
cursor: rgaCreateLinearCursor(node),
|
|
442
|
-
out: root
|
|
529
|
+
out: root,
|
|
530
|
+
path: [],
|
|
531
|
+
nextIndex: 0
|
|
443
532
|
});
|
|
444
533
|
while (stack.length > 0) {
|
|
445
534
|
const frame = stack[stack.length - 1];
|
|
@@ -453,6 +542,8 @@ function materialize(node) {
|
|
|
453
542
|
const child = entry.node;
|
|
454
543
|
const childDepth = frame.depth + 1;
|
|
455
544
|
assertTraversalDepth(childDepth);
|
|
545
|
+
const childPath = observer ? [...frame.path, key] : EMPTY_PATH;
|
|
546
|
+
observer?.(childPath, child);
|
|
456
547
|
if (child.kind === "lww") {
|
|
457
548
|
setMaterializedProperty(frame.out, key, child.value);
|
|
458
549
|
continue;
|
|
@@ -464,7 +555,8 @@ function materialize(node) {
|
|
|
464
555
|
kind: "obj",
|
|
465
556
|
depth: childDepth,
|
|
466
557
|
entries: child.entries.entries(),
|
|
467
|
-
out: outObj
|
|
558
|
+
out: outObj,
|
|
559
|
+
path: childPath
|
|
468
560
|
});
|
|
469
561
|
continue;
|
|
470
562
|
}
|
|
@@ -474,7 +566,9 @@ function materialize(node) {
|
|
|
474
566
|
kind: "seq",
|
|
475
567
|
depth: childDepth,
|
|
476
568
|
cursor: rgaCreateLinearCursor(child),
|
|
477
|
-
out: outArr
|
|
569
|
+
out: outArr,
|
|
570
|
+
path: childPath,
|
|
571
|
+
nextIndex: 0
|
|
478
572
|
});
|
|
479
573
|
continue;
|
|
480
574
|
}
|
|
@@ -486,6 +580,9 @@ function materialize(node) {
|
|
|
486
580
|
const child = elem.value;
|
|
487
581
|
const childDepth = frame.depth + 1;
|
|
488
582
|
assertTraversalDepth(childDepth);
|
|
583
|
+
const childPath = observer ? [...frame.path, String(frame.nextIndex)] : EMPTY_PATH;
|
|
584
|
+
frame.nextIndex += 1;
|
|
585
|
+
observer?.(childPath, child);
|
|
489
586
|
if (child.kind === "lww") {
|
|
490
587
|
frame.out.push(child.value);
|
|
491
588
|
continue;
|
|
@@ -497,7 +594,8 @@ function materialize(node) {
|
|
|
497
594
|
kind: "obj",
|
|
498
595
|
depth: childDepth,
|
|
499
596
|
entries: child.entries.entries(),
|
|
500
|
-
out: outObj
|
|
597
|
+
out: outObj,
|
|
598
|
+
path: childPath
|
|
501
599
|
});
|
|
502
600
|
continue;
|
|
503
601
|
}
|
|
@@ -507,7 +605,9 @@ function materialize(node) {
|
|
|
507
605
|
kind: "seq",
|
|
508
606
|
depth: childDepth,
|
|
509
607
|
cursor: rgaCreateLinearCursor(child),
|
|
510
|
-
out: outArr
|
|
608
|
+
out: outArr,
|
|
609
|
+
path: childPath,
|
|
610
|
+
nextIndex: 0
|
|
511
611
|
});
|
|
512
612
|
}
|
|
513
613
|
return root;
|
|
@@ -618,6 +718,7 @@ function assertRuntimeJsonValue(value) {
|
|
|
618
718
|
/**
|
|
619
719
|
* Normalize a runtime value to JSON-compatible data.
|
|
620
720
|
* - non-finite numbers -> null
|
|
721
|
+
* - non-plain objects -> null at the root / in arrays, omitted from object properties
|
|
621
722
|
* - invalid object-property values -> key omitted
|
|
622
723
|
* - invalid root / array values -> null
|
|
623
724
|
*/
|
|
@@ -704,7 +805,10 @@ function isJsonPrimitive$1(value) {
|
|
|
704
805
|
return typeof value === "number" && Number.isFinite(value);
|
|
705
806
|
}
|
|
706
807
|
function isJsonObject(value) {
|
|
707
|
-
|
|
808
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
809
|
+
if (Object.prototype.toString.call(value) !== "[object Object]") return false;
|
|
810
|
+
const prototype = Object.getPrototypeOf(value);
|
|
811
|
+
return prototype === null || Object.getPrototypeOf(prototype) === null;
|
|
708
812
|
}
|
|
709
813
|
function isNonFiniteNumber(value) {
|
|
710
814
|
return typeof value === "number" && !Number.isFinite(value);
|
|
@@ -715,8 +819,16 @@ function describeInvalidValue(value) {
|
|
|
715
819
|
if (typeof value === "bigint") return "bigint is not valid JSON";
|
|
716
820
|
if (typeof value === "symbol") return "symbol is not valid JSON";
|
|
717
821
|
if (typeof value === "function") return "function is not valid JSON";
|
|
822
|
+
if (typeof value === "object" && value !== null) return `non-plain object (${describeObjectKind(value)}) is not valid JSON`;
|
|
718
823
|
return `unsupported value type (${typeof value})`;
|
|
719
824
|
}
|
|
825
|
+
function describeObjectKind(value) {
|
|
826
|
+
const tag = Object.prototype.toString.call(value).slice(8, -1);
|
|
827
|
+
if (tag !== "Object") return tag;
|
|
828
|
+
const constructor = value.constructor;
|
|
829
|
+
if (typeof constructor === "function" && constructor.name !== "" && constructor.name !== "Object") return constructor.name;
|
|
830
|
+
return "Object";
|
|
831
|
+
}
|
|
720
832
|
|
|
721
833
|
//#endregion
|
|
722
834
|
//#region src/types.ts
|
|
@@ -847,8 +959,8 @@ function compileJsonPatchOpToIntent(baseJson, op, options = {}) {
|
|
|
847
959
|
* By default arrays use a deterministic LCS strategy.
|
|
848
960
|
* Pass `{ arrayStrategy: "atomic" }` for single-op array replacement.
|
|
849
961
|
* Pass `{ arrayStrategy: "lcs-linear" }` for a lower-memory LCS variant.
|
|
850
|
-
*
|
|
851
|
-
*
|
|
962
|
+
* Use `lcsLinearMaxCells` to optionally cap worst-case `lcs-linear` work and
|
|
963
|
+
* fall back to an atomic array replacement for very large unmatched windows.
|
|
852
964
|
* Pass `{ emitMoves: true }` or `{ emitCopies: true }` to opt into RFC 6902
|
|
853
965
|
* move/copy emission when a deterministic rewrite is available.
|
|
854
966
|
* @param base - The original JSON value.
|
|
@@ -865,39 +977,97 @@ function diffJsonPatch(base, next, options = {}) {
|
|
|
865
977
|
return ops;
|
|
866
978
|
}
|
|
867
979
|
function diffValue(path, base, next, ops, options) {
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
980
|
+
const stack = [{
|
|
981
|
+
kind: "value",
|
|
982
|
+
base,
|
|
983
|
+
next
|
|
984
|
+
}];
|
|
985
|
+
while (stack.length > 0) {
|
|
986
|
+
const frame = stack.pop();
|
|
987
|
+
if (frame.kind === "path-pop") {
|
|
988
|
+
path.pop();
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
if (frame.kind === "object") {
|
|
992
|
+
if (frame.index >= frame.sharedKeys.length) continue;
|
|
993
|
+
const key = frame.sharedKeys[frame.index];
|
|
994
|
+
stack.push({
|
|
995
|
+
kind: "object",
|
|
996
|
+
base: frame.base,
|
|
997
|
+
next: frame.next,
|
|
998
|
+
sharedKeys: frame.sharedKeys,
|
|
999
|
+
index: frame.index + 1
|
|
1000
|
+
});
|
|
1001
|
+
path.push(key);
|
|
1002
|
+
stack.push({ kind: "path-pop" });
|
|
1003
|
+
stack.push({
|
|
1004
|
+
kind: "value",
|
|
1005
|
+
base: frame.base[key],
|
|
1006
|
+
next: frame.next[key]
|
|
1007
|
+
});
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
assertTraversalDepth(path.length);
|
|
1011
|
+
if (frame.base === frame.next) continue;
|
|
1012
|
+
const baseIsArray = Array.isArray(frame.base);
|
|
1013
|
+
const nextIsArray = Array.isArray(frame.next);
|
|
1014
|
+
if (baseIsArray || nextIsArray) {
|
|
1015
|
+
if (!baseIsArray || !nextIsArray) {
|
|
1016
|
+
ops.push({
|
|
1017
|
+
op: "replace",
|
|
1018
|
+
path: stringifyJsonPointer(path),
|
|
1019
|
+
value: frame.next
|
|
1020
|
+
});
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
if (jsonEquals(frame.base, frame.next)) continue;
|
|
1024
|
+
const arrayStrategy = options.arrayStrategy ?? "lcs";
|
|
1025
|
+
if (arrayStrategy === "lcs") {
|
|
1026
|
+
if (!diffArrayWithLcsMatrix(path, frame.base, frame.next, ops, options)) ops.push({
|
|
1027
|
+
op: "replace",
|
|
1028
|
+
path: stringifyJsonPointer(path),
|
|
1029
|
+
value: frame.next
|
|
1030
|
+
});
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
if (arrayStrategy === "lcs-linear") {
|
|
1034
|
+
if (!diffArrayWithLinearLcs(path, frame.base, frame.next, ops, options)) ops.push({
|
|
1035
|
+
op: "replace",
|
|
1036
|
+
path: stringifyJsonPointer(path),
|
|
1037
|
+
value: frame.next
|
|
1038
|
+
});
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
ops.push({
|
|
873
1042
|
op: "replace",
|
|
874
1043
|
path: stringifyJsonPointer(path),
|
|
875
|
-
value: next
|
|
1044
|
+
value: frame.next
|
|
876
1045
|
});
|
|
877
|
-
|
|
1046
|
+
continue;
|
|
878
1047
|
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1048
|
+
const baseIsObject = isPlainObject(frame.base);
|
|
1049
|
+
const nextIsObject = isPlainObject(frame.next);
|
|
1050
|
+
if (!baseIsObject || !nextIsObject) {
|
|
1051
|
+
ops.push({
|
|
1052
|
+
op: "replace",
|
|
1053
|
+
path: stringifyJsonPointer(path),
|
|
1054
|
+
value: frame.next
|
|
1055
|
+
});
|
|
1056
|
+
continue;
|
|
882
1057
|
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
op: "replace",
|
|
893
|
-
path: stringifyJsonPointer(path),
|
|
894
|
-
value: next
|
|
1058
|
+
const { sharedKeys, baseOnlyKeys, nextOnlyKeys } = collectObjectKeys(frame.base, frame.next);
|
|
1059
|
+
if (!(baseOnlyKeys.length > 0 || nextOnlyKeys.length > 0) && (path.length === 0 || sharedKeys.length > 1) && jsonEquals(frame.base, frame.next)) continue;
|
|
1060
|
+
emitObjectStructuralOps(path, frame.base, frame.next, sharedKeys, baseOnlyKeys, nextOnlyKeys, ops, options);
|
|
1061
|
+
if (sharedKeys.length > 0) stack.push({
|
|
1062
|
+
kind: "object",
|
|
1063
|
+
base: frame.base,
|
|
1064
|
+
next: frame.next,
|
|
1065
|
+
sharedKeys,
|
|
1066
|
+
index: 0
|
|
895
1067
|
});
|
|
896
|
-
return;
|
|
897
1068
|
}
|
|
898
|
-
diffObject(path, base, next, ops, options);
|
|
899
1069
|
}
|
|
900
|
-
function
|
|
1070
|
+
function collectObjectKeys(base, next) {
|
|
901
1071
|
const baseKeys = Object.keys(base).sort();
|
|
902
1072
|
const nextKeys = Object.keys(next).sort();
|
|
903
1073
|
const baseOnlyKeys = [];
|
|
@@ -930,12 +1100,11 @@ function diffObject(path, base, next, ops, options) {
|
|
|
930
1100
|
nextOnlyKeys.push(nextKeys[nextIndex]);
|
|
931
1101
|
nextIndex += 1;
|
|
932
1102
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
}
|
|
1103
|
+
return {
|
|
1104
|
+
sharedKeys,
|
|
1105
|
+
baseOnlyKeys,
|
|
1106
|
+
nextOnlyKeys
|
|
1107
|
+
};
|
|
939
1108
|
}
|
|
940
1109
|
function emitObjectStructuralOps(path, base, next, sharedKeys, baseOnlyKeys, nextOnlyKeys, ops, options) {
|
|
941
1110
|
if (!options.emitMoves && !options.emitCopies) {
|
|
@@ -958,18 +1127,14 @@ function emitObjectStructuralOps(path, base, next, sharedKeys, baseOnlyKeys, nex
|
|
|
958
1127
|
}
|
|
959
1128
|
return;
|
|
960
1129
|
}
|
|
1130
|
+
const structuralKeyCache = /* @__PURE__ */ new WeakMap();
|
|
961
1131
|
const matchedMoveSources = /* @__PURE__ */ new Set();
|
|
962
1132
|
const moveTargets = /* @__PURE__ */ new Map();
|
|
963
1133
|
if (options.emitMoves) {
|
|
964
1134
|
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
|
-
}
|
|
1135
|
+
for (const baseKey of baseOnlyKeys) insertObjectSourceBucket(moveSourceBuckets, baseKey, base[baseKey], structuralKeyCache);
|
|
971
1136
|
for (const nextKey of nextOnlyKeys) {
|
|
972
|
-
const bucket = moveSourceBuckets.get(stableJsonValueKey(next[nextKey]));
|
|
1137
|
+
const bucket = moveSourceBuckets.get(stableJsonValueKey(next[nextKey], structuralKeyCache));
|
|
973
1138
|
if (!bucket) continue;
|
|
974
1139
|
if (bucket.length > 0) {
|
|
975
1140
|
const candidate = bucket.shift();
|
|
@@ -978,12 +1143,10 @@ function emitObjectStructuralOps(path, base, next, sharedKeys, baseOnlyKeys, nex
|
|
|
978
1143
|
}
|
|
979
1144
|
}
|
|
980
1145
|
}
|
|
981
|
-
const
|
|
982
|
-
const availableSourceKeys = [];
|
|
1146
|
+
const copySourceBuckets = /* @__PURE__ */ new Map();
|
|
983
1147
|
for (const key of sharedKeys) {
|
|
984
1148
|
if (!jsonEquals(base[key], next[key])) continue;
|
|
985
|
-
|
|
986
|
-
availableSourceKeys.push(key);
|
|
1149
|
+
insertObjectSourceBucket(copySourceBuckets, key, base[key], structuralKeyCache);
|
|
987
1150
|
}
|
|
988
1151
|
for (const nextKey of nextOnlyKeys) {
|
|
989
1152
|
path.push(nextKey);
|
|
@@ -999,12 +1162,11 @@ function emitObjectStructuralOps(path, base, next, sharedKeys, baseOnlyKeys, nex
|
|
|
999
1162
|
from: fromPath,
|
|
1000
1163
|
path: targetPath
|
|
1001
1164
|
});
|
|
1002
|
-
|
|
1003
|
-
insertSortedKey(availableSourceKeys, nextKey);
|
|
1165
|
+
insertObjectSourceBucket(copySourceBuckets, nextKey, next[nextKey], structuralKeyCache);
|
|
1004
1166
|
continue;
|
|
1005
1167
|
}
|
|
1006
1168
|
if (options.emitCopies) {
|
|
1007
|
-
const copySource = findObjectCopySource(
|
|
1169
|
+
const copySource = findObjectCopySource(copySourceBuckets, next[nextKey], structuralKeyCache);
|
|
1008
1170
|
if (copySource !== void 0) {
|
|
1009
1171
|
path.push(copySource);
|
|
1010
1172
|
const fromPath = stringifyJsonPointer(path);
|
|
@@ -1014,8 +1176,7 @@ function emitObjectStructuralOps(path, base, next, sharedKeys, baseOnlyKeys, nex
|
|
|
1014
1176
|
from: fromPath,
|
|
1015
1177
|
path: targetPath
|
|
1016
1178
|
});
|
|
1017
|
-
|
|
1018
|
-
insertSortedKey(availableSourceKeys, nextKey);
|
|
1179
|
+
insertObjectSourceBucket(copySourceBuckets, nextKey, next[nextKey], structuralKeyCache);
|
|
1019
1180
|
continue;
|
|
1020
1181
|
}
|
|
1021
1182
|
}
|
|
@@ -1024,8 +1185,7 @@ function emitObjectStructuralOps(path, base, next, sharedKeys, baseOnlyKeys, nex
|
|
|
1024
1185
|
path: targetPath,
|
|
1025
1186
|
value: next[nextKey]
|
|
1026
1187
|
});
|
|
1027
|
-
|
|
1028
|
-
insertSortedKey(availableSourceKeys, nextKey);
|
|
1188
|
+
insertObjectSourceBucket(copySourceBuckets, nextKey, next[nextKey], structuralKeyCache);
|
|
1029
1189
|
}
|
|
1030
1190
|
for (const baseKey of baseOnlyKeys) {
|
|
1031
1191
|
if (matchedMoveSources.has(baseKey)) continue;
|
|
@@ -1037,8 +1197,17 @@ function emitObjectStructuralOps(path, base, next, sharedKeys, baseOnlyKeys, nex
|
|
|
1037
1197
|
path.pop();
|
|
1038
1198
|
}
|
|
1039
1199
|
}
|
|
1040
|
-
function
|
|
1041
|
-
|
|
1200
|
+
function insertObjectSourceBucket(buckets, key, value, structuralKeyCache) {
|
|
1201
|
+
const bucketKey = stableJsonValueKey(value, structuralKeyCache);
|
|
1202
|
+
let bucket = buckets.get(bucketKey);
|
|
1203
|
+
if (!bucket) {
|
|
1204
|
+
bucket = [];
|
|
1205
|
+
buckets.set(bucketKey, bucket);
|
|
1206
|
+
}
|
|
1207
|
+
insertSortedKey(bucket, key);
|
|
1208
|
+
}
|
|
1209
|
+
function findObjectCopySource(copySourceBuckets, target, structuralKeyCache) {
|
|
1210
|
+
return copySourceBuckets.get(stableJsonValueKey(target, structuralKeyCache))?.[0];
|
|
1042
1211
|
}
|
|
1043
1212
|
function insertSortedKey(keys, key) {
|
|
1044
1213
|
let low = 0;
|
|
@@ -1065,9 +1234,11 @@ function diffArrayWithLcsMatrix(path, base, next, ops, options) {
|
|
|
1065
1234
|
}
|
|
1066
1235
|
function diffArrayWithLinearLcs(path, base, next, ops, options) {
|
|
1067
1236
|
const window = trimEqualArrayEdges(base, next);
|
|
1237
|
+
if (!shouldUseLinearLcsDiff(window.unmatchedBaseLength, window.unmatchedNextLength, options)) return false;
|
|
1068
1238
|
const steps = [];
|
|
1069
1239
|
buildArrayEditScriptLinearSpace(base, window.baseStart, window.baseStart + window.unmatchedBaseLength, next, window.nextStart, window.nextStart + window.unmatchedNextLength, steps);
|
|
1070
1240
|
pushArrayPatchOps(path, window.prefixLength, steps, ops, base, options);
|
|
1241
|
+
return true;
|
|
1071
1242
|
}
|
|
1072
1243
|
function trimEqualArrayEdges(base, next) {
|
|
1073
1244
|
const baseLength = base.length;
|
|
@@ -1264,17 +1435,22 @@ function shouldUseLcsDiff(baseLength, nextLength, lcsMaxCells) {
|
|
|
1264
1435
|
if (!Number.isFinite(cap) || cap < 1) return false;
|
|
1265
1436
|
return (baseLength + 1) * (nextLength + 1) <= cap;
|
|
1266
1437
|
}
|
|
1438
|
+
function shouldUseLinearLcsDiff(baseLength, nextLength, options) {
|
|
1439
|
+
const cap = options.lcsLinearMaxCells;
|
|
1440
|
+
if (cap === void 0 || cap === Number.POSITIVE_INFINITY) return true;
|
|
1441
|
+
if (!Number.isFinite(cap) || cap < 1) return false;
|
|
1442
|
+
return (baseLength + 1) * (nextLength + 1) <= cap;
|
|
1443
|
+
}
|
|
1267
1444
|
function finalizeArrayOps(arrayPath, base, ops, options) {
|
|
1268
1445
|
if (ops.length === 0) return [];
|
|
1269
1446
|
if (!options.emitMoves && !options.emitCopies) return compactArrayOps(ops);
|
|
1270
1447
|
const out = [];
|
|
1271
|
-
const working = base
|
|
1448
|
+
const working = createArrayRewriteState(base);
|
|
1272
1449
|
for (let i = 0; i < ops.length; i++) {
|
|
1273
1450
|
const op = ops[i];
|
|
1274
1451
|
const next = ops[i + 1];
|
|
1275
1452
|
if (op.op === "remove" && next && next.op === "add") {
|
|
1276
|
-
const
|
|
1277
|
-
const valuesMatch = jsonEquals(removedValue, next.value);
|
|
1453
|
+
const valuesMatch = working.entries[getArrayOpIndex(op.path, arrayPath)].key === getArrayRewriteValueKey(working, next.value);
|
|
1278
1454
|
if (op.path === next.path) {
|
|
1279
1455
|
const replaceOp = {
|
|
1280
1456
|
op: "replace",
|
|
@@ -1313,7 +1489,7 @@ function finalizeArrayOps(arrayPath, base, ops, options) {
|
|
|
1313
1489
|
const targetIndex = getArrayOpIndex(op.path, arrayPath);
|
|
1314
1490
|
const removeIndex = getArrayOpIndex(next.path, arrayPath);
|
|
1315
1491
|
const sourceIndex = removeIndex - (targetIndex <= removeIndex ? 1 : 0);
|
|
1316
|
-
const matchesPendingRemove = sourceIndex >= 0 && sourceIndex < working.length &&
|
|
1492
|
+
const matchesPendingRemove = sourceIndex >= 0 && sourceIndex < working.entries.length && working.entries[sourceIndex].key === getArrayRewriteValueKey(working, op.value);
|
|
1317
1493
|
if (options.emitMoves && matchesPendingRemove) {
|
|
1318
1494
|
const moveOp = {
|
|
1319
1495
|
op: "move",
|
|
@@ -1352,10 +1528,75 @@ function finalizeArrayOps(arrayPath, base, ops, options) {
|
|
|
1352
1528
|
}
|
|
1353
1529
|
return out;
|
|
1354
1530
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
if (
|
|
1358
|
-
|
|
1531
|
+
/** @internal Stable structural fingerprint used for deterministic diff rewrites. */
|
|
1532
|
+
function stableJsonValueKey(value, structuralKeyCache) {
|
|
1533
|
+
if (value !== null && typeof value === "object") {
|
|
1534
|
+
const cachedValue = structuralKeyCache?.get(value);
|
|
1535
|
+
if (cachedValue !== void 0) return cachedValue;
|
|
1536
|
+
}
|
|
1537
|
+
const stack = [{
|
|
1538
|
+
kind: "value",
|
|
1539
|
+
value,
|
|
1540
|
+
depth: 0
|
|
1541
|
+
}];
|
|
1542
|
+
const results = [];
|
|
1543
|
+
while (stack.length > 0) {
|
|
1544
|
+
const frame = stack.pop();
|
|
1545
|
+
if (frame.kind === "array") {
|
|
1546
|
+
const stableKey = `[${results.splice(frame.startIndex).join(",")}]`;
|
|
1547
|
+
structuralKeyCache?.set(frame.value, stableKey);
|
|
1548
|
+
results.push(stableKey);
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
if (frame.kind === "object") {
|
|
1552
|
+
const childParts = results.splice(frame.startIndex);
|
|
1553
|
+
const stableKey = `{${frame.keys.map((key, index) => `${JSON.stringify(key)}:${childParts[index]}`).join(",")}}`;
|
|
1554
|
+
structuralKeyCache?.set(frame.value, stableKey);
|
|
1555
|
+
results.push(stableKey);
|
|
1556
|
+
continue;
|
|
1557
|
+
}
|
|
1558
|
+
assertTraversalDepth(frame.depth);
|
|
1559
|
+
if (frame.value === null || typeof frame.value !== "object") {
|
|
1560
|
+
results.push(JSON.stringify(frame.value));
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
const cachedValue = structuralKeyCache?.get(frame.value);
|
|
1564
|
+
if (cachedValue !== void 0) {
|
|
1565
|
+
results.push(cachedValue);
|
|
1566
|
+
continue;
|
|
1567
|
+
}
|
|
1568
|
+
if (Array.isArray(frame.value)) {
|
|
1569
|
+
const startIndex = results.length;
|
|
1570
|
+
stack.push({
|
|
1571
|
+
kind: "array",
|
|
1572
|
+
value: frame.value,
|
|
1573
|
+
startIndex
|
|
1574
|
+
});
|
|
1575
|
+
for (let index = frame.value.length - 1; index >= 0; index--) stack.push({
|
|
1576
|
+
kind: "value",
|
|
1577
|
+
value: frame.value[index],
|
|
1578
|
+
depth: frame.depth + 1
|
|
1579
|
+
});
|
|
1580
|
+
continue;
|
|
1581
|
+
}
|
|
1582
|
+
const keys = Object.keys(frame.value).sort();
|
|
1583
|
+
const startIndex = results.length;
|
|
1584
|
+
stack.push({
|
|
1585
|
+
kind: "object",
|
|
1586
|
+
value: frame.value,
|
|
1587
|
+
keys,
|
|
1588
|
+
startIndex
|
|
1589
|
+
});
|
|
1590
|
+
for (let index = keys.length - 1; index >= 0; index--) {
|
|
1591
|
+
const key = keys[index];
|
|
1592
|
+
stack.push({
|
|
1593
|
+
kind: "value",
|
|
1594
|
+
value: frame.value[key],
|
|
1595
|
+
depth: frame.depth + 1
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
return results[0];
|
|
1359
1600
|
}
|
|
1360
1601
|
function compactArrayOps(ops) {
|
|
1361
1602
|
const out = [];
|
|
@@ -1375,9 +1616,65 @@ function compactArrayOps(ops) {
|
|
|
1375
1616
|
}
|
|
1376
1617
|
return out;
|
|
1377
1618
|
}
|
|
1378
|
-
function
|
|
1379
|
-
|
|
1380
|
-
|
|
1619
|
+
function createArrayRewriteState(base) {
|
|
1620
|
+
const structuralKeyCache = /* @__PURE__ */ new WeakMap();
|
|
1621
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
1622
|
+
return {
|
|
1623
|
+
entries: base.map((value, currentIndex) => {
|
|
1624
|
+
const entry = {
|
|
1625
|
+
value,
|
|
1626
|
+
key: stableJsonValueKey(value, structuralKeyCache),
|
|
1627
|
+
currentIndex,
|
|
1628
|
+
bucketIndex: -1
|
|
1629
|
+
};
|
|
1630
|
+
insertArrayRewriteBucketEntry(buckets, entry);
|
|
1631
|
+
return entry;
|
|
1632
|
+
}),
|
|
1633
|
+
buckets,
|
|
1634
|
+
structuralKeyCache
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
function getArrayRewriteValueKey(state, value) {
|
|
1638
|
+
return stableJsonValueKey(value, state.structuralKeyCache);
|
|
1639
|
+
}
|
|
1640
|
+
function findArrayCopySourceIndex(state, value) {
|
|
1641
|
+
return state.buckets.get(getArrayRewriteValueKey(state, value))?.[0]?.currentIndex ?? -1;
|
|
1642
|
+
}
|
|
1643
|
+
function insertArrayRewriteBucketEntry(buckets, entry) {
|
|
1644
|
+
let bucket = buckets.get(entry.key);
|
|
1645
|
+
if (!bucket) {
|
|
1646
|
+
bucket = [];
|
|
1647
|
+
buckets.set(entry.key, bucket);
|
|
1648
|
+
}
|
|
1649
|
+
let low = 0;
|
|
1650
|
+
let high = bucket.length;
|
|
1651
|
+
while (low < high) {
|
|
1652
|
+
const mid = Math.floor((low + high) / 2);
|
|
1653
|
+
if (bucket[mid].currentIndex < entry.currentIndex) low = mid + 1;
|
|
1654
|
+
else high = mid;
|
|
1655
|
+
}
|
|
1656
|
+
bucket.splice(low, 0, entry);
|
|
1657
|
+
reindexArrayRewriteBucketPositions(bucket, low);
|
|
1658
|
+
}
|
|
1659
|
+
function removeArrayRewriteBucketEntry(buckets, entry) {
|
|
1660
|
+
const bucket = buckets.get(entry.key);
|
|
1661
|
+
if (!bucket) return;
|
|
1662
|
+
const bucketIndex = entry.bucketIndex;
|
|
1663
|
+
if (bucketIndex < 0 || bucketIndex >= bucket.length || bucket[bucketIndex] !== entry) return;
|
|
1664
|
+
bucket.splice(bucketIndex, 1);
|
|
1665
|
+
if (bucket.length === 0) {
|
|
1666
|
+
buckets.delete(entry.key);
|
|
1667
|
+
entry.bucketIndex = -1;
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
entry.bucketIndex = -1;
|
|
1671
|
+
reindexArrayRewriteBucketPositions(bucket, bucketIndex);
|
|
1672
|
+
}
|
|
1673
|
+
function reindexArrayRewriteBucketPositions(bucket, startIndex) {
|
|
1674
|
+
for (let index = startIndex; index < bucket.length; index++) bucket[index].bucketIndex = index;
|
|
1675
|
+
}
|
|
1676
|
+
function reindexArrayRewriteEntries(entries, startIndex) {
|
|
1677
|
+
for (let index = startIndex; index < entries.length; index++) entries[index].currentIndex = index;
|
|
1381
1678
|
}
|
|
1382
1679
|
function getArrayOpIndex(ptr, arrayPath) {
|
|
1383
1680
|
const parsed = parseJsonPointer(ptr);
|
|
@@ -1389,29 +1686,60 @@ function getArrayOpIndex(ptr, arrayPath) {
|
|
|
1389
1686
|
}
|
|
1390
1687
|
function applyArrayOptimizationOp(working, op, arrayPath) {
|
|
1391
1688
|
if (op.op === "add") {
|
|
1392
|
-
|
|
1689
|
+
const index = getArrayOpIndex(op.path, arrayPath);
|
|
1690
|
+
const entry = {
|
|
1691
|
+
value: structuredClone(op.value),
|
|
1692
|
+
key: getArrayRewriteValueKey(working, op.value),
|
|
1693
|
+
currentIndex: index,
|
|
1694
|
+
bucketIndex: -1
|
|
1695
|
+
};
|
|
1696
|
+
working.entries.splice(index, 0, entry);
|
|
1697
|
+
reindexArrayRewriteEntries(working.entries, index + 1);
|
|
1698
|
+
insertArrayRewriteBucketEntry(working.buckets, entry);
|
|
1393
1699
|
return;
|
|
1394
1700
|
}
|
|
1395
1701
|
if (op.op === "remove") {
|
|
1396
|
-
|
|
1702
|
+
const index = getArrayOpIndex(op.path, arrayPath);
|
|
1703
|
+
const [removedEntry] = working.entries.splice(index, 1);
|
|
1704
|
+
if (removedEntry) removeArrayRewriteBucketEntry(working.buckets, removedEntry);
|
|
1705
|
+
reindexArrayRewriteEntries(working.entries, index);
|
|
1397
1706
|
return;
|
|
1398
1707
|
}
|
|
1399
1708
|
if (op.op === "replace") {
|
|
1400
|
-
|
|
1709
|
+
const index = getArrayOpIndex(op.path, arrayPath);
|
|
1710
|
+
const entry = working.entries[index];
|
|
1711
|
+
removeArrayRewriteBucketEntry(working.buckets, entry);
|
|
1712
|
+
entry.value = structuredClone(op.value);
|
|
1713
|
+
entry.key = getArrayRewriteValueKey(working, op.value);
|
|
1714
|
+
insertArrayRewriteBucketEntry(working.buckets, entry);
|
|
1401
1715
|
return;
|
|
1402
1716
|
}
|
|
1403
1717
|
if (op.op === "copy") {
|
|
1404
1718
|
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
|
|
1407
|
-
|
|
1719
|
+
if (fromIndex < 0 || fromIndex >= working.entries.length) throw new Error(`applyArrayOptimizationOp: copy from index ${fromIndex} is out of bounds (length ${working.entries.length})`);
|
|
1720
|
+
const index = getArrayOpIndex(op.path, arrayPath);
|
|
1721
|
+
const source = working.entries[fromIndex];
|
|
1722
|
+
const entry = {
|
|
1723
|
+
value: structuredClone(source.value),
|
|
1724
|
+
key: source.key,
|
|
1725
|
+
currentIndex: index,
|
|
1726
|
+
bucketIndex: -1
|
|
1727
|
+
};
|
|
1728
|
+
working.entries.splice(index, 0, entry);
|
|
1729
|
+
reindexArrayRewriteEntries(working.entries, index + 1);
|
|
1730
|
+
insertArrayRewriteBucketEntry(working.buckets, entry);
|
|
1408
1731
|
return;
|
|
1409
1732
|
}
|
|
1410
1733
|
if (op.op === "move") {
|
|
1411
1734
|
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 [
|
|
1414
|
-
|
|
1735
|
+
if (fromIndex < 0 || fromIndex >= working.entries.length) throw new Error(`applyArrayOptimizationOp: move from index ${fromIndex} is out of bounds (length ${working.entries.length})`);
|
|
1736
|
+
const [entry] = working.entries.splice(fromIndex, 1);
|
|
1737
|
+
if (!entry) throw new Error(`applyArrayOptimizationOp: move from index ${fromIndex} did not resolve`);
|
|
1738
|
+
removeArrayRewriteBucketEntry(working.buckets, entry);
|
|
1739
|
+
const index = getArrayOpIndex(op.path, arrayPath);
|
|
1740
|
+
working.entries.splice(index, 0, entry);
|
|
1741
|
+
reindexArrayRewriteEntries(working.entries, Math.min(fromIndex, index));
|
|
1742
|
+
insertArrayRewriteBucketEntry(working.buckets, entry);
|
|
1415
1743
|
return;
|
|
1416
1744
|
}
|
|
1417
1745
|
throw new Error(`applyArrayOptimizationOp: unexpected op type "${op.op}"`);
|
|
@@ -1421,21 +1749,39 @@ function escapeJsonPointer(token) {
|
|
|
1421
1749
|
}
|
|
1422
1750
|
/** Deep equality check for JSON values (null-safe, handles arrays and objects). */
|
|
1423
1751
|
function jsonEquals(a, b) {
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1752
|
+
const stack = [{
|
|
1753
|
+
left: a,
|
|
1754
|
+
right: b,
|
|
1755
|
+
depth: 0
|
|
1756
|
+
}];
|
|
1757
|
+
while (stack.length > 0) {
|
|
1758
|
+
const frame = stack.pop();
|
|
1759
|
+
assertTraversalDepth(frame.depth);
|
|
1760
|
+
if (frame.left === frame.right) continue;
|
|
1761
|
+
if (frame.left === null || frame.right === null) return false;
|
|
1762
|
+
if (Array.isArray(frame.left) || Array.isArray(frame.right)) {
|
|
1763
|
+
if (!Array.isArray(frame.left) || !Array.isArray(frame.right)) return false;
|
|
1764
|
+
if (frame.left.length !== frame.right.length) return false;
|
|
1765
|
+
for (let index = frame.left.length - 1; index >= 0; index--) stack.push({
|
|
1766
|
+
left: frame.left[index],
|
|
1767
|
+
right: frame.right[index],
|
|
1768
|
+
depth: frame.depth + 1
|
|
1769
|
+
});
|
|
1770
|
+
continue;
|
|
1771
|
+
}
|
|
1772
|
+
if (!isPlainObject(frame.left) || !isPlainObject(frame.right)) return false;
|
|
1773
|
+
const leftKeys = Object.keys(frame.left);
|
|
1774
|
+
const rightKeys = Object.keys(frame.right);
|
|
1775
|
+
if (leftKeys.length !== rightKeys.length) return false;
|
|
1776
|
+
for (let index = leftKeys.length - 1; index >= 0; index--) {
|
|
1777
|
+
const key = leftKeys[index];
|
|
1778
|
+
if (!hasOwn(frame.right, key)) return false;
|
|
1779
|
+
stack.push({
|
|
1780
|
+
left: frame.left[key],
|
|
1781
|
+
right: frame.right[key],
|
|
1782
|
+
depth: frame.depth + 1
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1439
1785
|
}
|
|
1440
1786
|
return true;
|
|
1441
1787
|
}
|
|
@@ -1726,51 +2072,35 @@ function docFromJson(value, nextDot) {
|
|
|
1726
2072
|
return { root: nodeFromJson(value, nextDot) };
|
|
1727
2073
|
}
|
|
1728
2074
|
/**
|
|
1729
|
-
* Legacy
|
|
1730
|
-
*
|
|
2075
|
+
* Legacy helper for tests and fixtures that seeds an entire document from one dot.
|
|
2076
|
+
*
|
|
2077
|
+
* It reuses that dot for object entries and synthesizes array child counters from the
|
|
2078
|
+
* same seed, which can produce low-quality causal metadata and unrealistic sequence
|
|
2079
|
+
* identities in production CRDT state.
|
|
2080
|
+
*
|
|
2081
|
+
* Prefer `docFromJson(value, nextDot)` so every node receives a fresh unique dot.
|
|
2082
|
+
*
|
|
2083
|
+
* @deprecated Use `docFromJson(value, nextDot)` for production documents.
|
|
1731
2084
|
*/
|
|
1732
2085
|
function docFromJsonWithDot(value, dot) {
|
|
1733
2086
|
return { root: deepNodeFromJson(value, dot) };
|
|
1734
2087
|
}
|
|
1735
2088
|
function getSeqAtPath(doc, path) {
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
if (cur.kind !== "obj") return;
|
|
1739
|
-
const ent = cur.entries.get(seg);
|
|
1740
|
-
if (!ent) return;
|
|
1741
|
-
cur = ent.node;
|
|
1742
|
-
}
|
|
1743
|
-
return cur.kind === "seq" ? cur : void 0;
|
|
2089
|
+
const node = getNodeAtPath(doc, path);
|
|
2090
|
+
return node?.kind === "seq" ? node : void 0;
|
|
1744
2091
|
}
|
|
1745
2092
|
function getObjAtPathStrict(doc, path) {
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
if (cur.kind !== "obj") return {
|
|
1750
|
-
ok: false,
|
|
1751
|
-
message: "expected object at /"
|
|
1752
|
-
};
|
|
2093
|
+
const node = getNodeAtPath(doc, path);
|
|
2094
|
+
if (!node || node.kind !== "obj") {
|
|
2095
|
+
const pointer = stringifyJsonPointer(path);
|
|
1753
2096
|
return {
|
|
1754
|
-
ok: true,
|
|
1755
|
-
obj: cur
|
|
1756
|
-
};
|
|
1757
|
-
}
|
|
1758
|
-
for (const seg of path) {
|
|
1759
|
-
if (cur.kind !== "obj") return {
|
|
1760
2097
|
ok: false,
|
|
1761
|
-
message: `expected object at
|
|
2098
|
+
message: `expected object at ${pointer === "" ? "/" : pointer}`
|
|
1762
2099
|
};
|
|
1763
|
-
const entry = cur.entries.get(seg);
|
|
1764
|
-
seen.push(seg);
|
|
1765
|
-
if (!entry || entry.node.kind !== "obj") return {
|
|
1766
|
-
ok: false,
|
|
1767
|
-
message: `expected object at /${seen.join("/")}`
|
|
1768
|
-
};
|
|
1769
|
-
cur = entry.node;
|
|
1770
2100
|
}
|
|
1771
2101
|
return {
|
|
1772
2102
|
ok: true,
|
|
1773
|
-
obj:
|
|
2103
|
+
obj: node
|
|
1774
2104
|
};
|
|
1775
2105
|
}
|
|
1776
2106
|
function ensureSeqAtPath(head, path, dotForCreate) {
|
|
@@ -1817,10 +2147,24 @@ function ensureSeqAtPath(head, path, dotForCreate) {
|
|
|
1817
2147
|
function getNodeAtPath(doc, path) {
|
|
1818
2148
|
let cur = doc.root;
|
|
1819
2149
|
for (const seg of path) {
|
|
1820
|
-
if (cur.kind
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
2150
|
+
if (cur.kind === "obj") {
|
|
2151
|
+
const ent = cur.entries.get(seg);
|
|
2152
|
+
if (!ent) return;
|
|
2153
|
+
cur = ent.node;
|
|
2154
|
+
continue;
|
|
2155
|
+
}
|
|
2156
|
+
if (cur.kind === "seq") {
|
|
2157
|
+
if (!ARRAY_INDEX_TOKEN_PATTERN.test(seg)) return;
|
|
2158
|
+
const index = Number(seg);
|
|
2159
|
+
if (!Number.isSafeInteger(index)) return;
|
|
2160
|
+
const elemId = rgaIdAtIndex(cur, index);
|
|
2161
|
+
if (elemId === void 0) return;
|
|
2162
|
+
const elem = cur.elems.get(elemId);
|
|
2163
|
+
if (!elem) return;
|
|
2164
|
+
cur = elem.value;
|
|
2165
|
+
continue;
|
|
2166
|
+
}
|
|
2167
|
+
if (cur.kind === "lww") return;
|
|
1824
2168
|
}
|
|
1825
2169
|
return cur;
|
|
1826
2170
|
}
|
|
@@ -2034,38 +2378,88 @@ function getJsonAtDocPathForTest(doc, path) {
|
|
|
2034
2378
|
let cur = doc.root;
|
|
2035
2379
|
for (let i = 0; i < path.length; i++) {
|
|
2036
2380
|
const seg = path[i];
|
|
2037
|
-
|
|
2381
|
+
try {
|
|
2382
|
+
assertTraversalDepth(i + 1);
|
|
2383
|
+
} catch (error) {
|
|
2384
|
+
return {
|
|
2385
|
+
ok: false,
|
|
2386
|
+
error: error instanceof TraversalDepthError ? toDepthApplyError(error) : {
|
|
2387
|
+
ok: false,
|
|
2388
|
+
code: 409,
|
|
2389
|
+
reason: "INVALID_PATCH",
|
|
2390
|
+
message: error instanceof Error ? error.message : "invalid test path"
|
|
2391
|
+
}
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2038
2394
|
if (cur.kind === "obj") {
|
|
2039
2395
|
const ent = cur.entries.get(seg);
|
|
2040
|
-
if (!ent)
|
|
2396
|
+
if (!ent) return {
|
|
2397
|
+
ok: false,
|
|
2398
|
+
error: {
|
|
2399
|
+
ok: false,
|
|
2400
|
+
code: 409,
|
|
2401
|
+
reason: "MISSING_TARGET",
|
|
2402
|
+
message: `Missing key '${seg}'`
|
|
2403
|
+
}
|
|
2404
|
+
};
|
|
2041
2405
|
cur = ent.node;
|
|
2042
2406
|
continue;
|
|
2043
2407
|
}
|
|
2044
2408
|
if (cur.kind === "seq") {
|
|
2045
|
-
if (!ARRAY_INDEX_TOKEN_PATTERN.test(seg))
|
|
2046
|
-
|
|
2047
|
-
|
|
2409
|
+
if (!ARRAY_INDEX_TOKEN_PATTERN.test(seg)) return {
|
|
2410
|
+
ok: false,
|
|
2411
|
+
error: {
|
|
2412
|
+
ok: false,
|
|
2413
|
+
code: 409,
|
|
2414
|
+
reason: "INVALID_POINTER",
|
|
2415
|
+
message: `Expected array index, got '${seg}'`
|
|
2416
|
+
}
|
|
2417
|
+
};
|
|
2418
|
+
const idx = Number(seg);
|
|
2419
|
+
if (!Number.isSafeInteger(idx)) return {
|
|
2420
|
+
ok: false,
|
|
2421
|
+
error: {
|
|
2422
|
+
ok: false,
|
|
2423
|
+
code: 409,
|
|
2424
|
+
reason: "OUT_OF_BOUNDS",
|
|
2425
|
+
message: `Index out of bounds at '${seg}'`
|
|
2426
|
+
}
|
|
2427
|
+
};
|
|
2428
|
+
const id = rgaIdAtIndex(cur, idx);
|
|
2429
|
+
if (id === void 0) return {
|
|
2430
|
+
ok: false,
|
|
2431
|
+
error: {
|
|
2432
|
+
ok: false,
|
|
2433
|
+
code: 409,
|
|
2434
|
+
reason: "OUT_OF_BOUNDS",
|
|
2435
|
+
message: `Index out of bounds at '${seg}'`
|
|
2436
|
+
}
|
|
2437
|
+
};
|
|
2048
2438
|
cur = cur.elems.get(id).value;
|
|
2049
2439
|
continue;
|
|
2050
2440
|
}
|
|
2051
|
-
throw new Error(`Cannot traverse into non-container at '${seg}'`);
|
|
2052
|
-
}
|
|
2053
|
-
return cur.kind === "lww" ? cur.value : materialize(cur);
|
|
2054
|
-
}
|
|
2055
|
-
function applyTest(base, head, it, evalTestAgainst) {
|
|
2056
|
-
let got;
|
|
2057
|
-
try {
|
|
2058
|
-
got = getJsonAtDocPathForTest(evalTestAgainst === "head" ? head : base, it.path);
|
|
2059
|
-
} catch {
|
|
2060
2441
|
return {
|
|
2061
2442
|
ok: false,
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2443
|
+
error: {
|
|
2444
|
+
ok: false,
|
|
2445
|
+
code: 409,
|
|
2446
|
+
reason: "INVALID_TARGET",
|
|
2447
|
+
message: `Cannot traverse into non-container at '${seg}'`
|
|
2448
|
+
}
|
|
2066
2449
|
};
|
|
2067
2450
|
}
|
|
2068
|
-
|
|
2451
|
+
return {
|
|
2452
|
+
ok: true,
|
|
2453
|
+
value: cur.kind === "lww" ? cur.value : materialize(cur)
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
function applyTest(base, head, it, evalTestAgainst) {
|
|
2457
|
+
const got = getJsonAtDocPathForTest(evalTestAgainst === "head" ? head : base, it.path);
|
|
2458
|
+
if (!got.ok) return {
|
|
2459
|
+
...got.error,
|
|
2460
|
+
path: `/${it.path.join("/")}`
|
|
2461
|
+
};
|
|
2462
|
+
if (!jsonEquals(got.value, it.value)) return {
|
|
2069
2463
|
ok: false,
|
|
2070
2464
|
code: 409,
|
|
2071
2465
|
reason: "TEST_FAILED",
|
|
@@ -2370,6 +2764,46 @@ function rebaseDiffOps(path, nestedOps, out) {
|
|
|
2370
2764
|
throw new Error(`Unexpected op '${op.op}' from diffJsonPatch`);
|
|
2371
2765
|
}
|
|
2372
2766
|
}
|
|
2767
|
+
function collectLiveSequenceElements(seq) {
|
|
2768
|
+
const elems = [];
|
|
2769
|
+
const cursor = rgaCreateLinearCursor(seq);
|
|
2770
|
+
for (let elem = cursor.next(); elem; elem = cursor.next()) elems.push(elem);
|
|
2771
|
+
return elems;
|
|
2772
|
+
}
|
|
2773
|
+
function materializeSequenceWindow(elems, start, end) {
|
|
2774
|
+
const out = [];
|
|
2775
|
+
for (let i = start; i < end; i++) out.push(nodeToJsonForPatch(elems[i].value));
|
|
2776
|
+
return out;
|
|
2777
|
+
}
|
|
2778
|
+
function rebaseSequenceWindowDiffOps(path, indexOffset, nestedOps, out) {
|
|
2779
|
+
const pending = [];
|
|
2780
|
+
for (const op of nestedOps) {
|
|
2781
|
+
if (op.path === "") return false;
|
|
2782
|
+
const rebasedSegments = parseJsonPointer(op.path);
|
|
2783
|
+
const indexToken = rebasedSegments[0];
|
|
2784
|
+
if (!indexToken || !ARRAY_INDEX_TOKEN_PATTERN.test(indexToken)) return false;
|
|
2785
|
+
rebasedSegments[0] = String(Number(indexToken) + indexOffset);
|
|
2786
|
+
const rebasedPath = stringifyJsonPointer([...path, ...rebasedSegments]);
|
|
2787
|
+
if (op.op === "remove") {
|
|
2788
|
+
pending.push({
|
|
2789
|
+
op: "remove",
|
|
2790
|
+
path: rebasedPath
|
|
2791
|
+
});
|
|
2792
|
+
continue;
|
|
2793
|
+
}
|
|
2794
|
+
if (op.op === "add" || op.op === "replace") {
|
|
2795
|
+
pending.push({
|
|
2796
|
+
op: op.op,
|
|
2797
|
+
path: rebasedPath,
|
|
2798
|
+
value: op.value
|
|
2799
|
+
});
|
|
2800
|
+
continue;
|
|
2801
|
+
}
|
|
2802
|
+
return false;
|
|
2803
|
+
}
|
|
2804
|
+
out.push(...pending);
|
|
2805
|
+
return true;
|
|
2806
|
+
}
|
|
2373
2807
|
function nodesJsonEqual(baseNode, headNode, depth) {
|
|
2374
2808
|
assertTraversalDepth(depth);
|
|
2375
2809
|
if (baseNode === headNode) return true;
|
|
@@ -2494,6 +2928,35 @@ function diffObjectNodes(path, baseNode, headNode, options, ops, depth) {
|
|
|
2494
2928
|
headIndex += 1;
|
|
2495
2929
|
}
|
|
2496
2930
|
}
|
|
2931
|
+
function diffSequenceNodes(path, baseNode, headSeq, options, ops, depth) {
|
|
2932
|
+
if ((options.arrayStrategy ?? "lcs") === "atomic") {
|
|
2933
|
+
rebaseDiffOps(path, diffJsonPatch(materialize(baseNode), materialize(headSeq), options), ops);
|
|
2934
|
+
return;
|
|
2935
|
+
}
|
|
2936
|
+
const baseElems = collectLiveSequenceElements(baseNode);
|
|
2937
|
+
const headElems = collectLiveSequenceElements(headSeq);
|
|
2938
|
+
const sharedLength = Math.min(baseElems.length, headElems.length);
|
|
2939
|
+
let prefixLength = 0;
|
|
2940
|
+
while (prefixLength < sharedLength && nodesJsonEqual(baseElems[prefixLength].value, headElems[prefixLength].value, depth + 1)) prefixLength += 1;
|
|
2941
|
+
if (prefixLength === baseElems.length && prefixLength === headElems.length) return;
|
|
2942
|
+
let baseEnd = baseElems.length;
|
|
2943
|
+
let headEnd = headElems.length;
|
|
2944
|
+
while (baseEnd > prefixLength && headEnd > prefixLength && nodesJsonEqual(baseElems[baseEnd - 1].value, headElems[headEnd - 1].value, depth + 1)) {
|
|
2945
|
+
baseEnd -= 1;
|
|
2946
|
+
headEnd -= 1;
|
|
2947
|
+
}
|
|
2948
|
+
const unmatchedBaseLength = baseEnd - prefixLength;
|
|
2949
|
+
const unmatchedHeadLength = headEnd - prefixLength;
|
|
2950
|
+
if (unmatchedBaseLength === 1 && unmatchedHeadLength === 1) {
|
|
2951
|
+
path.push(String(prefixLength));
|
|
2952
|
+
diffNodeToPatch(path, baseElems[prefixLength].value, headElems[prefixLength].value, options, ops, depth + 1);
|
|
2953
|
+
path.pop();
|
|
2954
|
+
return;
|
|
2955
|
+
}
|
|
2956
|
+
const seqOps = diffJsonPatch(materializeSequenceWindow(baseElems, prefixLength, baseEnd), materializeSequenceWindow(headElems, prefixLength, headEnd), options);
|
|
2957
|
+
if (rebaseSequenceWindowDiffOps(path, prefixLength, seqOps, ops)) return;
|
|
2958
|
+
rebaseDiffOps(path, diffJsonPatch(materialize(baseNode), materialize(headSeq), options), ops);
|
|
2959
|
+
}
|
|
2497
2960
|
function diffNodeToPatch(path, baseNode, headNode, options, ops, depth) {
|
|
2498
2961
|
assertTraversalDepth(depth);
|
|
2499
2962
|
if (baseNode === headNode) return;
|
|
@@ -2519,8 +2982,7 @@ function diffNodeToPatch(path, baseNode, headNode, options, ops, depth) {
|
|
|
2519
2982
|
diffObjectNodes(path, baseNode, headNode, options, ops, depth);
|
|
2520
2983
|
return;
|
|
2521
2984
|
}
|
|
2522
|
-
|
|
2523
|
-
rebaseDiffOps(path, diffJsonPatch(materialize(baseNode), materialize(headSeq), options), ops);
|
|
2985
|
+
diffSequenceNodes(path, baseNode, headNode, options, ops, depth);
|
|
2524
2986
|
}
|
|
2525
2987
|
/**
|
|
2526
2988
|
* Generate a JSON Patch delta between two CRDT documents.
|
|
@@ -2562,7 +3024,7 @@ function jsonPatchToCrdtInternal(options) {
|
|
|
2562
3024
|
}
|
|
2563
3025
|
return applyIntentsToCrdt(options.base, options.head, intents, options.newDot, evalTestAgainst, options.bumpCounterAbove, { strictParents: options.strictParents });
|
|
2564
3026
|
}
|
|
2565
|
-
|
|
3027
|
+
const shadowBase = evalTestAgainst === "base" ? cloneDoc(options.base) : null;
|
|
2566
3028
|
let shadowCtr = 0;
|
|
2567
3029
|
const shadowDot = () => ({
|
|
2568
3030
|
actor: "__shadow__",
|
|
@@ -2571,60 +3033,340 @@ function jsonPatchToCrdtInternal(options) {
|
|
|
2571
3033
|
const shadowBump = (ctr) => {
|
|
2572
3034
|
if (shadowCtr < ctr) shadowCtr = ctr;
|
|
2573
3035
|
};
|
|
2574
|
-
const
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
3036
|
+
const session = { pointerCache: /* @__PURE__ */ new Map() };
|
|
3037
|
+
for (const [opIndex, op] of options.patch.entries()) {
|
|
3038
|
+
const step = applySequentialPatchOp(options, evalTestAgainst === "base" ? shadowBase : options.head, op, opIndex, evalTestAgainst, shadowDot, shadowBump, session);
|
|
3039
|
+
if (!step.ok) return step;
|
|
3040
|
+
}
|
|
3041
|
+
return { ok: true };
|
|
3042
|
+
}
|
|
3043
|
+
function applySequentialPatchOp(options, compileBase, op, opIndex, evalTestAgainst, shadowDot, shadowBump, session) {
|
|
3044
|
+
if (op.op === "move") {
|
|
3045
|
+
if (op.from === op.path) {
|
|
3046
|
+
const pathCheck = resolveValueAtPointerInDoc$1(compileBase, op.from, opIndex, session.pointerCache);
|
|
3047
|
+
if (!pathCheck.ok) return pathCheck;
|
|
3048
|
+
return { ok: true };
|
|
2581
3049
|
}
|
|
2582
|
-
const
|
|
2583
|
-
if (!
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
}
|
|
2588
|
-
|
|
3050
|
+
const fromResolved = resolveValueAtPointerInDoc$1(compileBase, op.from, opIndex, session.pointerCache);
|
|
3051
|
+
if (!fromResolved.ok) return fromResolved;
|
|
3052
|
+
const removeStep = applySingleSequentialPatchStep(options, compileBase, {
|
|
3053
|
+
op: "remove",
|
|
3054
|
+
path: op.from
|
|
3055
|
+
}, opIndex, evalTestAgainst, shadowDot, shadowBump, session);
|
|
3056
|
+
if (!removeStep.ok) return removeStep;
|
|
3057
|
+
return applySingleSequentialPatchStep(options, compileBase, {
|
|
3058
|
+
op: "add",
|
|
3059
|
+
path: op.path,
|
|
3060
|
+
value: structuredClone(fromResolved.value)
|
|
3061
|
+
}, opIndex, evalTestAgainst, shadowDot, shadowBump, session);
|
|
3062
|
+
}
|
|
3063
|
+
if (op.op === "copy") {
|
|
3064
|
+
const fromResolved = resolveValueAtPointerInDoc$1(compileBase, op.from, opIndex, session.pointerCache);
|
|
3065
|
+
if (!fromResolved.ok) return fromResolved;
|
|
3066
|
+
return applySingleSequentialPatchStep(options, compileBase, {
|
|
3067
|
+
op: "add",
|
|
3068
|
+
path: op.path,
|
|
3069
|
+
value: structuredClone(fromResolved.value)
|
|
3070
|
+
}, opIndex, evalTestAgainst, shadowDot, shadowBump, session);
|
|
3071
|
+
}
|
|
3072
|
+
return applySingleSequentialPatchStep(options, compileBase, op, opIndex, evalTestAgainst, shadowDot, shadowBump, session);
|
|
3073
|
+
}
|
|
3074
|
+
function applySingleSequentialPatchStep(options, compileBase, op, opIndex, evalTestAgainst, shadowDot, shadowBump, session) {
|
|
3075
|
+
const compiled = compilePreparedSingleIntentFromDoc$1(compileBase, op, session.pointerCache, opIndex);
|
|
3076
|
+
if (!compiled.ok) return compiled;
|
|
3077
|
+
const headStep = applyIntentsToCrdt(compileBase, options.head, compiled.intents, options.newDot, evalTestAgainst, options.bumpCounterAbove, { strictParents: options.strictParents });
|
|
3078
|
+
if (!headStep.ok) return withOpIndex$1(headStep, opIndex);
|
|
3079
|
+
if (op.op === "test") return { ok: true };
|
|
3080
|
+
if (evalTestAgainst === "head") return { ok: true };
|
|
3081
|
+
const shadowStep = applyIntentsToCrdt(compileBase, compileBase, compiled.intents, shadowDot, "base", shadowBump, { strictParents: options.strictParents });
|
|
3082
|
+
if (!shadowStep.ok) return withOpIndex$1(shadowStep, opIndex);
|
|
3083
|
+
return { ok: true };
|
|
3084
|
+
}
|
|
3085
|
+
function resolveValueAtPointerInDoc$1(doc, pointer, opIndex, pointerCache) {
|
|
3086
|
+
const parsedPath = parsePointerWithCache$1(pointer, pointerCache, opIndex);
|
|
3087
|
+
if (!parsedPath.ok) return parsedPath;
|
|
3088
|
+
const resolved = resolveNodeAtPath$1(doc.root, parsedPath.path);
|
|
3089
|
+
if (!resolved.ok) return {
|
|
3090
|
+
ok: false,
|
|
3091
|
+
...resolved.error,
|
|
3092
|
+
path: pointer,
|
|
3093
|
+
opIndex
|
|
2589
3094
|
};
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
3095
|
+
return {
|
|
3096
|
+
ok: true,
|
|
3097
|
+
value: nodeToJsonForPatch(resolved.node)
|
|
3098
|
+
};
|
|
3099
|
+
}
|
|
3100
|
+
function compilePreparedSingleIntentFromDoc$1(baseDoc, op, pointerCache, opIndex) {
|
|
3101
|
+
const parsedPath = parsePointerWithCache$1(op.path, pointerCache, opIndex);
|
|
3102
|
+
if (!parsedPath.ok) return parsedPath;
|
|
3103
|
+
const path = parsedPath.path;
|
|
3104
|
+
if (op.op === "test") return {
|
|
3105
|
+
ok: true,
|
|
3106
|
+
intents: [{
|
|
3107
|
+
t: "Test",
|
|
3108
|
+
path,
|
|
3109
|
+
value: op.value
|
|
3110
|
+
}]
|
|
3111
|
+
};
|
|
3112
|
+
if (path.length === 0) {
|
|
3113
|
+
if (op.op === "remove") return {
|
|
3114
|
+
ok: false,
|
|
3115
|
+
code: 409,
|
|
3116
|
+
reason: "INVALID_TARGET",
|
|
3117
|
+
message: "remove at root path is not supported in RFC-compliant mode",
|
|
3118
|
+
path: op.path,
|
|
3119
|
+
opIndex
|
|
3120
|
+
};
|
|
3121
|
+
return {
|
|
3122
|
+
ok: true,
|
|
3123
|
+
intents: [{
|
|
3124
|
+
t: "ObjSet",
|
|
3125
|
+
path: [],
|
|
3126
|
+
key: ROOT_KEY,
|
|
3127
|
+
value: op.value
|
|
3128
|
+
}]
|
|
3129
|
+
};
|
|
3130
|
+
}
|
|
3131
|
+
const parentPath = path.slice(0, -1);
|
|
3132
|
+
const parentPointer = stringifyJsonPointer(parentPath);
|
|
3133
|
+
const key = path[path.length - 1];
|
|
3134
|
+
const resolvedParent = parentPath.length === 0 ? {
|
|
3135
|
+
ok: true,
|
|
3136
|
+
node: baseDoc.root
|
|
3137
|
+
} : resolveNodeAtPath$1(baseDoc.root, parentPath);
|
|
3138
|
+
if (!resolvedParent.ok) return {
|
|
3139
|
+
ok: false,
|
|
3140
|
+
...resolvedParent.error,
|
|
3141
|
+
path: parentPointer,
|
|
3142
|
+
opIndex
|
|
3143
|
+
};
|
|
3144
|
+
const parentNode = resolvedParent.node;
|
|
3145
|
+
if (parentNode.kind === "seq") {
|
|
3146
|
+
const parsedIndex = parseArrayIndexTokenForDoc$1(key, op.op, op.path, opIndex);
|
|
3147
|
+
if (!parsedIndex.ok) return parsedIndex;
|
|
3148
|
+
const boundedIndex = validateArrayIndexBounds$1(parsedIndex.index, op.op, rgaLength(parentNode), op.path, opIndex);
|
|
3149
|
+
if (!boundedIndex.ok) return boundedIndex;
|
|
3150
|
+
if (op.op === "add") return {
|
|
3151
|
+
ok: true,
|
|
3152
|
+
intents: [{
|
|
3153
|
+
t: "ArrInsert",
|
|
3154
|
+
path: parentPath,
|
|
3155
|
+
index: boundedIndex.index,
|
|
3156
|
+
value: op.value
|
|
3157
|
+
}]
|
|
3158
|
+
};
|
|
3159
|
+
if (op.op === "remove") return {
|
|
3160
|
+
ok: true,
|
|
3161
|
+
intents: [{
|
|
3162
|
+
t: "ArrDelete",
|
|
3163
|
+
path: parentPath,
|
|
3164
|
+
index: boundedIndex.index
|
|
3165
|
+
}]
|
|
3166
|
+
};
|
|
3167
|
+
return {
|
|
3168
|
+
ok: true,
|
|
3169
|
+
intents: [{
|
|
3170
|
+
t: "ArrReplace",
|
|
3171
|
+
path: parentPath,
|
|
3172
|
+
index: boundedIndex.index,
|
|
3173
|
+
value: op.value
|
|
3174
|
+
}]
|
|
3175
|
+
};
|
|
3176
|
+
}
|
|
3177
|
+
if (parentNode.kind !== "obj") return {
|
|
3178
|
+
ok: false,
|
|
3179
|
+
code: 409,
|
|
3180
|
+
reason: "INVALID_TARGET",
|
|
3181
|
+
message: `expected object or array parent at ${parentPointer}`,
|
|
3182
|
+
path: parentPointer,
|
|
3183
|
+
opIndex
|
|
3184
|
+
};
|
|
3185
|
+
if (key === "__proto__") return {
|
|
3186
|
+
ok: false,
|
|
3187
|
+
code: 409,
|
|
3188
|
+
reason: "INVALID_POINTER",
|
|
3189
|
+
message: `unsafe object key at ${op.path}`,
|
|
3190
|
+
path: op.path,
|
|
3191
|
+
opIndex
|
|
3192
|
+
};
|
|
3193
|
+
const entry = parentNode.entries.get(key);
|
|
3194
|
+
if ((op.op === "replace" || op.op === "remove") && !entry) return {
|
|
3195
|
+
ok: false,
|
|
3196
|
+
code: 409,
|
|
3197
|
+
reason: "MISSING_TARGET",
|
|
3198
|
+
message: `missing key ${key} at ${parentPointer}`,
|
|
3199
|
+
path: op.path,
|
|
3200
|
+
opIndex
|
|
3201
|
+
};
|
|
3202
|
+
if (op.op === "remove") return {
|
|
3203
|
+
ok: true,
|
|
3204
|
+
intents: [{
|
|
3205
|
+
t: "ObjRemove",
|
|
3206
|
+
path: parentPath,
|
|
3207
|
+
key
|
|
3208
|
+
}]
|
|
3209
|
+
};
|
|
3210
|
+
return {
|
|
3211
|
+
ok: true,
|
|
3212
|
+
intents: [{
|
|
3213
|
+
t: "ObjSet",
|
|
3214
|
+
path: parentPath,
|
|
3215
|
+
key,
|
|
3216
|
+
value: op.value,
|
|
3217
|
+
mode: op.op
|
|
3218
|
+
}]
|
|
3219
|
+
};
|
|
3220
|
+
}
|
|
3221
|
+
function parsePointerWithCache$1(pointer, pointerCache, opIndex) {
|
|
3222
|
+
const cachedPath = pointerCache.get(pointer);
|
|
3223
|
+
if (cachedPath !== void 0) return {
|
|
3224
|
+
ok: true,
|
|
3225
|
+
path: cachedPath.slice()
|
|
3226
|
+
};
|
|
3227
|
+
try {
|
|
3228
|
+
const parsedPath = parseJsonPointer(pointer);
|
|
3229
|
+
pointerCache.set(pointer, parsedPath);
|
|
3230
|
+
return {
|
|
3231
|
+
ok: true,
|
|
3232
|
+
path: parsedPath.slice()
|
|
3233
|
+
};
|
|
3234
|
+
} catch (error) {
|
|
3235
|
+
return {
|
|
3236
|
+
ok: false,
|
|
3237
|
+
code: 409,
|
|
3238
|
+
reason: "INVALID_POINTER",
|
|
3239
|
+
message: error instanceof Error ? error.message : "invalid pointer",
|
|
3240
|
+
path: pointer,
|
|
3241
|
+
opIndex
|
|
3242
|
+
};
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
function resolveNodeAtPath$1(root, path) {
|
|
3246
|
+
let current = root;
|
|
3247
|
+
for (const segment of path) {
|
|
3248
|
+
if (current.kind === "obj") {
|
|
3249
|
+
const entry = current.entries.get(segment);
|
|
3250
|
+
if (!entry) return {
|
|
3251
|
+
ok: false,
|
|
3252
|
+
error: {
|
|
3253
|
+
code: 409,
|
|
3254
|
+
reason: "MISSING_PARENT",
|
|
3255
|
+
message: `Missing key '${segment}'`
|
|
2605
3256
|
}
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
if (op.from === op.path) continue;
|
|
2609
|
-
const removeStep = applySequentialOp({
|
|
2610
|
-
op: "remove",
|
|
2611
|
-
path: op.from
|
|
2612
|
-
}, opIndex);
|
|
2613
|
-
if (!removeStep.ok) return removeStep;
|
|
2614
|
-
const addStep = applySequentialOp({
|
|
2615
|
-
op: "add",
|
|
2616
|
-
path: op.path,
|
|
2617
|
-
value: fromValue
|
|
2618
|
-
}, opIndex);
|
|
2619
|
-
if (!addStep.ok) return addStep;
|
|
3257
|
+
};
|
|
3258
|
+
current = entry.node;
|
|
2620
3259
|
continue;
|
|
2621
3260
|
}
|
|
2622
|
-
|
|
2623
|
-
|
|
3261
|
+
if (current.kind === "seq") {
|
|
3262
|
+
if (!ARRAY_INDEX_TOKEN_PATTERN.test(segment)) return {
|
|
3263
|
+
ok: false,
|
|
3264
|
+
error: {
|
|
3265
|
+
code: 409,
|
|
3266
|
+
reason: "INVALID_POINTER",
|
|
3267
|
+
message: `Expected array index, got '${segment}'`
|
|
3268
|
+
}
|
|
3269
|
+
};
|
|
3270
|
+
const index = Number(segment);
|
|
3271
|
+
if (!Number.isSafeInteger(index)) return {
|
|
3272
|
+
ok: false,
|
|
3273
|
+
error: {
|
|
3274
|
+
code: 409,
|
|
3275
|
+
reason: "OUT_OF_BOUNDS",
|
|
3276
|
+
message: `Index out of bounds at '${segment}'`
|
|
3277
|
+
}
|
|
3278
|
+
};
|
|
3279
|
+
const elemId = rgaIdAtIndex(current, index);
|
|
3280
|
+
if (elemId === void 0) return {
|
|
3281
|
+
ok: false,
|
|
3282
|
+
error: {
|
|
3283
|
+
code: 409,
|
|
3284
|
+
reason: "OUT_OF_BOUNDS",
|
|
3285
|
+
message: `Index out of bounds at '${segment}'`
|
|
3286
|
+
}
|
|
3287
|
+
};
|
|
3288
|
+
current = current.elems.get(elemId).value;
|
|
3289
|
+
continue;
|
|
3290
|
+
}
|
|
3291
|
+
return {
|
|
3292
|
+
ok: false,
|
|
3293
|
+
error: {
|
|
3294
|
+
code: 409,
|
|
3295
|
+
reason: "INVALID_TARGET",
|
|
3296
|
+
message: `Cannot traverse into non-container at '${segment}'`
|
|
3297
|
+
}
|
|
3298
|
+
};
|
|
2624
3299
|
}
|
|
2625
|
-
return {
|
|
3300
|
+
return {
|
|
3301
|
+
ok: true,
|
|
3302
|
+
node: current
|
|
3303
|
+
};
|
|
2626
3304
|
}
|
|
2627
|
-
function
|
|
3305
|
+
function parseArrayIndexTokenForDoc$1(token, op, path, opIndex) {
|
|
3306
|
+
if (token === "-") {
|
|
3307
|
+
if (op !== "add") return {
|
|
3308
|
+
ok: false,
|
|
3309
|
+
code: 409,
|
|
3310
|
+
reason: "INVALID_POINTER",
|
|
3311
|
+
message: `'-' index is only valid for add at ${path}`,
|
|
3312
|
+
path,
|
|
3313
|
+
opIndex
|
|
3314
|
+
};
|
|
3315
|
+
return {
|
|
3316
|
+
ok: true,
|
|
3317
|
+
index: Number.POSITIVE_INFINITY
|
|
3318
|
+
};
|
|
3319
|
+
}
|
|
3320
|
+
if (!ARRAY_INDEX_TOKEN_PATTERN.test(token)) return {
|
|
3321
|
+
ok: false,
|
|
3322
|
+
code: 409,
|
|
3323
|
+
reason: "INVALID_POINTER",
|
|
3324
|
+
message: `expected array index at ${path}`,
|
|
3325
|
+
path,
|
|
3326
|
+
opIndex
|
|
3327
|
+
};
|
|
3328
|
+
const index = Number(token);
|
|
3329
|
+
if (!Number.isSafeInteger(index)) return {
|
|
3330
|
+
ok: false,
|
|
3331
|
+
code: 409,
|
|
3332
|
+
reason: "OUT_OF_BOUNDS",
|
|
3333
|
+
message: `array index is too large at ${path}`,
|
|
3334
|
+
path,
|
|
3335
|
+
opIndex
|
|
3336
|
+
};
|
|
3337
|
+
return {
|
|
3338
|
+
ok: true,
|
|
3339
|
+
index
|
|
3340
|
+
};
|
|
3341
|
+
}
|
|
3342
|
+
function validateArrayIndexBounds$1(index, op, arrLength, path, opIndex) {
|
|
3343
|
+
if (op === "add") {
|
|
3344
|
+
if (index === Number.POSITIVE_INFINITY) return {
|
|
3345
|
+
ok: true,
|
|
3346
|
+
index
|
|
3347
|
+
};
|
|
3348
|
+
if (index > arrLength) return {
|
|
3349
|
+
ok: false,
|
|
3350
|
+
code: 409,
|
|
3351
|
+
reason: "OUT_OF_BOUNDS",
|
|
3352
|
+
message: `index out of bounds at ${path}; expected 0..${arrLength}`,
|
|
3353
|
+
path,
|
|
3354
|
+
opIndex
|
|
3355
|
+
};
|
|
3356
|
+
} else if (index >= arrLength) return {
|
|
3357
|
+
ok: false,
|
|
3358
|
+
code: 409,
|
|
3359
|
+
reason: "OUT_OF_BOUNDS",
|
|
3360
|
+
message: `index out of bounds at ${path}; expected 0..${Math.max(arrLength - 1, 0)}`,
|
|
3361
|
+
path,
|
|
3362
|
+
opIndex
|
|
3363
|
+
};
|
|
3364
|
+
return {
|
|
3365
|
+
ok: true,
|
|
3366
|
+
index
|
|
3367
|
+
};
|
|
3368
|
+
}
|
|
3369
|
+
function withOpIndex$1(error, opIndex) {
|
|
2628
3370
|
if (error.opIndex !== void 0) return error;
|
|
2629
3371
|
return {
|
|
2630
3372
|
...error,
|
|
@@ -2787,10 +3529,13 @@ function tryApplyPatchInPlace(state, patch, options = {}) {
|
|
|
2787
3529
|
* Does not mutate caller-provided values.
|
|
2788
3530
|
*/
|
|
2789
3531
|
function validateJsonPatch(base, patch, options = {}) {
|
|
2790
|
-
const result =
|
|
3532
|
+
const result = tryApplyPatchInPlace(createState(base, {
|
|
2791
3533
|
actor: "__validate__",
|
|
2792
3534
|
jsonValidation: options.jsonValidation
|
|
2793
|
-
}), patch,
|
|
3535
|
+
}), patch, {
|
|
3536
|
+
...options,
|
|
3537
|
+
atomic: false
|
|
3538
|
+
});
|
|
2794
3539
|
if (!result.ok) return {
|
|
2795
3540
|
ok: false,
|
|
2796
3541
|
error: result.error
|
|
@@ -2811,7 +3556,7 @@ function applyPatchAsActor(doc, vv, actor, patch, options = {}) {
|
|
|
2811
3556
|
}
|
|
2812
3557
|
/** Non-throwing `applyPatchAsActor` variant for internals sync flows. */
|
|
2813
3558
|
function tryApplyPatchAsActor(doc, vv, actor, patch, options = {}) {
|
|
2814
|
-
const observedCtr =
|
|
3559
|
+
const observedCtr = observedVersionVector(doc)[actor] ?? 0;
|
|
2815
3560
|
const applied = tryApplyPatch({
|
|
2816
3561
|
doc,
|
|
2817
3562
|
clock: createClock(actor, Math.max(vv[actor] ?? 0, observedCtr))
|
|
@@ -2839,32 +3584,19 @@ function toApplyPatchOptionsForActor(options) {
|
|
|
2839
3584
|
} : void 0
|
|
2840
3585
|
};
|
|
2841
3586
|
}
|
|
2842
|
-
function applyPatchInternal(state, patch, options,
|
|
3587
|
+
function applyPatchInternal(state, patch, options, _execution) {
|
|
2843
3588
|
const preparedPatch = preparePatchPayloadsSafe(patch, options.jsonValidation ?? "none");
|
|
2844
3589
|
if (!preparedPatch.ok) return preparedPatch;
|
|
2845
3590
|
const runtimePatch = preparedPatch.patch;
|
|
2846
3591
|
if ((options.semantics ?? "sequential") === "sequential") {
|
|
2847
|
-
if (!options.base && execution === "batch") {
|
|
2848
|
-
const compiled = compilePreparedIntents(materialize(state.doc.root), runtimePatch, "sequential");
|
|
2849
|
-
if (!compiled.ok) return compiled;
|
|
2850
|
-
return applyIntentsToCrdt(state.doc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
|
|
2851
|
-
}
|
|
2852
3592
|
const explicitBaseState = options.base ? {
|
|
2853
3593
|
doc: cloneDoc(options.base.doc),
|
|
2854
3594
|
clock: createClock("__base__", 0)
|
|
2855
3595
|
} : null;
|
|
2856
|
-
const session = {
|
|
2857
|
-
pointerCache: /* @__PURE__ */ new Map(),
|
|
2858
|
-
baseShadowParentCache: /* @__PURE__ */ new Map(),
|
|
2859
|
-
headShadowParentCache: /* @__PURE__ */ new Map()
|
|
2860
|
-
};
|
|
2861
|
-
let sequentialHeadJson = materialize(state.doc.root);
|
|
2862
|
-
let sequentialBaseJson = explicitBaseState ? materialize(explicitBaseState.doc.root) : sequentialHeadJson;
|
|
3596
|
+
const session = { pointerCache: /* @__PURE__ */ new Map() };
|
|
2863
3597
|
for (const [opIndex, op] of runtimePatch.entries()) {
|
|
2864
|
-
const step = applyPatchOpSequential(state, op, options, explicitBaseState ? explicitBaseState.doc : state.doc,
|
|
3598
|
+
const step = applyPatchOpSequential(state, op, options, explicitBaseState ? explicitBaseState.doc : state.doc, explicitBaseState, opIndex, session);
|
|
2865
3599
|
if (!step.ok) return step;
|
|
2866
|
-
sequentialBaseJson = step.baseJson;
|
|
2867
|
-
sequentialHeadJson = step.headJson;
|
|
2868
3600
|
}
|
|
2869
3601
|
return { ok: true };
|
|
2870
3602
|
}
|
|
@@ -2873,12 +3605,12 @@ function applyPatchInternal(state, patch, options, execution) {
|
|
|
2873
3605
|
if (!compiled.ok) return compiled;
|
|
2874
3606
|
return applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
|
|
2875
3607
|
}
|
|
2876
|
-
function applyPatchOpSequential(state, op, options, baseDoc,
|
|
3608
|
+
function applyPatchOpSequential(state, op, options, baseDoc, explicitBaseState, opIndex, session) {
|
|
2877
3609
|
if (op.op === "move") {
|
|
2878
|
-
const fromResolved =
|
|
3610
|
+
const fromResolved = resolveValueAtPointerInDoc(baseDoc, op.from, opIndex, session.pointerCache);
|
|
2879
3611
|
if (!fromResolved.ok) return fromResolved;
|
|
2880
3612
|
const fromValue = structuredClone(fromResolved.value);
|
|
2881
|
-
const removeRes = applySinglePatchOpSequentialStep(state, baseDoc,
|
|
3613
|
+
const removeRes = applySinglePatchOpSequentialStep(state, baseDoc, {
|
|
2882
3614
|
op: "remove",
|
|
2883
3615
|
path: op.from
|
|
2884
3616
|
}, options, explicitBaseState, opIndex, session);
|
|
@@ -2888,152 +3620,184 @@ function applyPatchOpSequential(state, op, options, baseDoc, baseJson, headJson,
|
|
|
2888
3620
|
path: op.path,
|
|
2889
3621
|
value: fromValue
|
|
2890
3622
|
};
|
|
2891
|
-
if (!explicitBaseState) return applySinglePatchOpSequentialStep(state, state.doc,
|
|
2892
|
-
const headAddRes = applySinglePatchOpSequentialStep(state, state.doc,
|
|
3623
|
+
if (!explicitBaseState) return applySinglePatchOpSequentialStep(state, state.doc, addOp, options, null, opIndex, session);
|
|
3624
|
+
const headAddRes = applySinglePatchOpSequentialStep(state, state.doc, addOp, options, null, opIndex, session);
|
|
2893
3625
|
if (!headAddRes.ok) return headAddRes;
|
|
2894
|
-
const shadowAddRes = applySinglePatchOpExplicitShadowStep(explicitBaseState,
|
|
3626
|
+
const shadowAddRes = applySinglePatchOpExplicitShadowStep(explicitBaseState, addOp, options, opIndex, session);
|
|
2895
3627
|
if (!shadowAddRes.ok) return shadowAddRes;
|
|
2896
|
-
return {
|
|
2897
|
-
ok: true,
|
|
2898
|
-
baseJson: shadowAddRes.baseJson,
|
|
2899
|
-
headJson: headAddRes.headJson
|
|
2900
|
-
};
|
|
3628
|
+
return { ok: true };
|
|
2901
3629
|
}
|
|
2902
3630
|
if (op.op === "copy") {
|
|
2903
|
-
const fromResolved =
|
|
3631
|
+
const fromResolved = resolveValueAtPointerInDoc(baseDoc, op.from, opIndex, session.pointerCache);
|
|
2904
3632
|
if (!fromResolved.ok) return fromResolved;
|
|
2905
|
-
return applySinglePatchOpSequentialStep(state, baseDoc,
|
|
3633
|
+
return applySinglePatchOpSequentialStep(state, baseDoc, {
|
|
2906
3634
|
op: "add",
|
|
2907
3635
|
path: op.path,
|
|
2908
3636
|
value: structuredClone(fromResolved.value)
|
|
2909
3637
|
}, options, explicitBaseState, opIndex, session);
|
|
2910
3638
|
}
|
|
2911
|
-
return applySinglePatchOpSequentialStep(state, baseDoc,
|
|
3639
|
+
return applySinglePatchOpSequentialStep(state, baseDoc, op, options, explicitBaseState, opIndex, session);
|
|
2912
3640
|
}
|
|
2913
|
-
function applySinglePatchOpSequentialStep(state, baseDoc,
|
|
2914
|
-
const compiled =
|
|
3641
|
+
function applySinglePatchOpSequentialStep(state, baseDoc, op, options, explicitBaseState, opIndex, session) {
|
|
3642
|
+
const compiled = compilePreparedSingleIntentFromDoc(baseDoc, op, session.pointerCache, opIndex);
|
|
2915
3643
|
if (!compiled.ok) return compiled;
|
|
2916
3644
|
const headStep = applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
|
|
2917
|
-
if (!headStep.ok) return headStep;
|
|
3645
|
+
if (!headStep.ok) return withOpIndex(headStep, opIndex);
|
|
2918
3646
|
if (explicitBaseState && op.op !== "test") {
|
|
2919
3647
|
const shadowStep = applyIntentsToCrdt(explicitBaseState.doc, explicitBaseState.doc, compiled.intents, () => explicitBaseState.clock.next(), "base", (ctr) => bumpClockCounter(explicitBaseState, ctr), { strictParents: options.strictParents });
|
|
2920
|
-
if (!shadowStep.ok) return shadowStep;
|
|
3648
|
+
if (!shadowStep.ok) return withOpIndex(shadowStep, opIndex);
|
|
2921
3649
|
}
|
|
2922
|
-
|
|
2923
|
-
ok: true,
|
|
2924
|
-
baseJson,
|
|
2925
|
-
headJson
|
|
2926
|
-
};
|
|
2927
|
-
const nextBaseJson = applyJsonPatchOpToShadow(baseJson, op, explicitBaseState ? session.baseShadowParentCache : session.headShadowParentCache, {
|
|
2928
|
-
pointerCache: session.pointerCache,
|
|
2929
|
-
opIndex
|
|
2930
|
-
});
|
|
2931
|
-
return {
|
|
2932
|
-
ok: true,
|
|
2933
|
-
baseJson: nextBaseJson,
|
|
2934
|
-
headJson: explicitBaseState ? applyJsonPatchOpToShadow(headJson, op, session.headShadowParentCache, {
|
|
2935
|
-
pointerCache: session.pointerCache,
|
|
2936
|
-
opIndex
|
|
2937
|
-
}) : nextBaseJson
|
|
2938
|
-
};
|
|
3650
|
+
return { ok: true };
|
|
2939
3651
|
}
|
|
2940
|
-
function applySinglePatchOpExplicitShadowStep(explicitBaseState,
|
|
2941
|
-
const compiled =
|
|
3652
|
+
function applySinglePatchOpExplicitShadowStep(explicitBaseState, op, options, opIndex, session) {
|
|
3653
|
+
const compiled = compilePreparedSingleIntentFromDoc(explicitBaseState.doc, op, session.pointerCache, opIndex);
|
|
2942
3654
|
if (!compiled.ok) return compiled;
|
|
2943
3655
|
const shadowStep = applyIntentsToCrdt(explicitBaseState.doc, explicitBaseState.doc, compiled.intents, () => explicitBaseState.clock.next(), "base", (ctr) => bumpClockCounter(explicitBaseState, ctr), { strictParents: options.strictParents });
|
|
2944
|
-
if (!shadowStep.ok) return shadowStep;
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
3656
|
+
if (!shadowStep.ok) return withOpIndex(shadowStep, opIndex);
|
|
3657
|
+
return { ok: true };
|
|
3658
|
+
}
|
|
3659
|
+
function resolveValueAtPointerInDoc(doc, pointer, opIndex, pointerCache) {
|
|
3660
|
+
let path;
|
|
3661
|
+
try {
|
|
3662
|
+
path = parsePointerWithCache(pointer, pointerCache);
|
|
3663
|
+
} catch (error) {
|
|
3664
|
+
return toPointerParseApplyError(error, pointer, opIndex);
|
|
3665
|
+
}
|
|
3666
|
+
const resolved = resolveNodeAtPath(doc.root, path);
|
|
3667
|
+
if (!resolved.ok) return {
|
|
3668
|
+
ok: false,
|
|
3669
|
+
...resolved.error,
|
|
3670
|
+
path: pointer,
|
|
3671
|
+
opIndex
|
|
2948
3672
|
};
|
|
2949
3673
|
return {
|
|
2950
3674
|
ok: true,
|
|
2951
|
-
|
|
2952
|
-
pointerCache: session.pointerCache,
|
|
2953
|
-
opIndex
|
|
2954
|
-
})
|
|
3675
|
+
value: materialize(resolved.node)
|
|
2955
3676
|
};
|
|
2956
3677
|
}
|
|
2957
|
-
function
|
|
3678
|
+
function compilePreparedSingleIntentFromDoc(baseDoc, op, pointerCache, opIndex) {
|
|
2958
3679
|
let path;
|
|
2959
3680
|
try {
|
|
2960
|
-
path = parsePointerWithCache(op.path,
|
|
3681
|
+
path = parsePointerWithCache(op.path, pointerCache);
|
|
2961
3682
|
} catch (error) {
|
|
2962
|
-
|
|
3683
|
+
return toPointerParseApplyError(error, op.path, opIndex);
|
|
2963
3684
|
}
|
|
3685
|
+
if (op.op === "test") return {
|
|
3686
|
+
ok: true,
|
|
3687
|
+
intents: [{
|
|
3688
|
+
t: "Test",
|
|
3689
|
+
path,
|
|
3690
|
+
value: op.value
|
|
3691
|
+
}]
|
|
3692
|
+
};
|
|
2964
3693
|
if (path.length === 0) {
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
3694
|
+
if (op.op === "remove") return {
|
|
3695
|
+
ok: false,
|
|
3696
|
+
code: 409,
|
|
3697
|
+
reason: "INVALID_TARGET",
|
|
3698
|
+
message: "remove at root path is not supported in RFC-compliant mode",
|
|
3699
|
+
path: op.path,
|
|
3700
|
+
opIndex
|
|
3701
|
+
};
|
|
3702
|
+
return {
|
|
3703
|
+
ok: true,
|
|
3704
|
+
intents: [{
|
|
3705
|
+
t: "ObjSet",
|
|
3706
|
+
path: [],
|
|
3707
|
+
key: ROOT_KEY,
|
|
3708
|
+
value: op.value
|
|
3709
|
+
}]
|
|
3710
|
+
};
|
|
2969
3711
|
}
|
|
2970
|
-
const pathPointer = op.path;
|
|
2971
3712
|
const parentPath = path.slice(0, -1);
|
|
2972
|
-
const parentPointer =
|
|
3713
|
+
const parentPointer = stringifyJsonPointer(parentPath);
|
|
2973
3714
|
const key = path[path.length - 1];
|
|
2974
|
-
const
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
return
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
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;
|
|
3715
|
+
const resolvedParent = parentPath.length === 0 ? {
|
|
3716
|
+
ok: true,
|
|
3717
|
+
node: baseDoc.root
|
|
3718
|
+
} : resolveNodeAtPath(baseDoc.root, parentPath);
|
|
3719
|
+
if (!resolvedParent.ok) return {
|
|
3720
|
+
ok: false,
|
|
3721
|
+
...resolvedParent.error,
|
|
3722
|
+
path: parentPointer,
|
|
3723
|
+
opIndex
|
|
3724
|
+
};
|
|
3725
|
+
const parentNode = resolvedParent.node;
|
|
3726
|
+
if (parentNode.kind === "seq") {
|
|
3727
|
+
const parsedIndex = parseArrayIndexTokenForDoc(key, op.op, op.path, opIndex);
|
|
3728
|
+
if (!parsedIndex.ok) return parsedIndex;
|
|
3729
|
+
const boundedIndex = validateArrayIndexBounds(parsedIndex.index, op.op, rgaLength(parentNode), op.path, opIndex);
|
|
3730
|
+
if (!boundedIndex.ok) return boundedIndex;
|
|
3731
|
+
if (op.op === "add") return {
|
|
3732
|
+
ok: true,
|
|
3733
|
+
intents: [{
|
|
3734
|
+
t: "ArrInsert",
|
|
3735
|
+
path: parentPath,
|
|
3736
|
+
index: boundedIndex.index,
|
|
3737
|
+
value: op.value
|
|
3738
|
+
}]
|
|
3739
|
+
};
|
|
3740
|
+
if (op.op === "remove") return {
|
|
3741
|
+
ok: true,
|
|
3742
|
+
intents: [{
|
|
3743
|
+
t: "ArrDelete",
|
|
3744
|
+
path: parentPath,
|
|
3745
|
+
index: boundedIndex.index
|
|
3746
|
+
}]
|
|
3747
|
+
};
|
|
3748
|
+
return {
|
|
3749
|
+
ok: true,
|
|
3750
|
+
intents: [{
|
|
3751
|
+
t: "ArrReplace",
|
|
3752
|
+
path: parentPath,
|
|
3753
|
+
index: boundedIndex.index,
|
|
3754
|
+
value: op.value
|
|
3755
|
+
}]
|
|
3756
|
+
};
|
|
3028
3757
|
}
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3758
|
+
if (parentNode.kind !== "obj") return {
|
|
3759
|
+
ok: false,
|
|
3760
|
+
code: 409,
|
|
3761
|
+
reason: "INVALID_TARGET",
|
|
3762
|
+
message: `expected object or array parent at ${parentPointer}`,
|
|
3763
|
+
path: parentPointer,
|
|
3764
|
+
opIndex
|
|
3765
|
+
};
|
|
3766
|
+
if (key === "__proto__") return {
|
|
3767
|
+
ok: false,
|
|
3768
|
+
code: 409,
|
|
3769
|
+
reason: "INVALID_POINTER",
|
|
3770
|
+
message: `unsafe object key at ${op.path}`,
|
|
3771
|
+
path: op.path,
|
|
3772
|
+
opIndex
|
|
3773
|
+
};
|
|
3774
|
+
const entry = parentNode.entries.get(key);
|
|
3775
|
+
if ((op.op === "replace" || op.op === "remove") && !entry) return {
|
|
3776
|
+
ok: false,
|
|
3777
|
+
code: 409,
|
|
3778
|
+
reason: "MISSING_TARGET",
|
|
3779
|
+
message: `missing key ${key} at ${parentPointer}`,
|
|
3780
|
+
path: op.path,
|
|
3781
|
+
opIndex
|
|
3782
|
+
};
|
|
3783
|
+
if (op.op === "remove") return {
|
|
3784
|
+
ok: true,
|
|
3785
|
+
intents: [{
|
|
3786
|
+
t: "ObjRemove",
|
|
3787
|
+
path: parentPath,
|
|
3788
|
+
key
|
|
3789
|
+
}]
|
|
3790
|
+
};
|
|
3791
|
+
return {
|
|
3792
|
+
ok: true,
|
|
3793
|
+
intents: [{
|
|
3794
|
+
t: "ObjSet",
|
|
3795
|
+
path: parentPath,
|
|
3796
|
+
key,
|
|
3797
|
+
value: op.value,
|
|
3798
|
+
mode: op.op
|
|
3799
|
+
}]
|
|
3800
|
+
};
|
|
3037
3801
|
}
|
|
3038
3802
|
function parsePointerWithCache(pointer, pointerCache) {
|
|
3039
3803
|
const cachedPath = pointerCache.get(pointer);
|
|
@@ -3042,21 +3806,129 @@ function parsePointerWithCache(pointer, pointerCache) {
|
|
|
3042
3806
|
pointerCache.set(pointer, parsedPath);
|
|
3043
3807
|
return parsedPath.slice();
|
|
3044
3808
|
}
|
|
3045
|
-
function
|
|
3046
|
-
let
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3809
|
+
function resolveNodeAtPath(root, path) {
|
|
3810
|
+
let current = root;
|
|
3811
|
+
for (const segment of path) {
|
|
3812
|
+
if (current.kind === "obj") {
|
|
3813
|
+
const entry = current.entries.get(segment);
|
|
3814
|
+
if (!entry) return {
|
|
3815
|
+
ok: false,
|
|
3816
|
+
error: {
|
|
3817
|
+
code: 409,
|
|
3818
|
+
reason: "MISSING_PARENT",
|
|
3819
|
+
message: `Missing key '${segment}'`
|
|
3820
|
+
}
|
|
3821
|
+
};
|
|
3822
|
+
current = entry.node;
|
|
3823
|
+
continue;
|
|
3824
|
+
}
|
|
3825
|
+
if (current.kind === "seq") {
|
|
3826
|
+
if (!ARRAY_INDEX_TOKEN_PATTERN.test(segment)) return {
|
|
3827
|
+
ok: false,
|
|
3828
|
+
error: {
|
|
3829
|
+
code: 409,
|
|
3830
|
+
reason: "INVALID_POINTER",
|
|
3831
|
+
message: `Expected array index, got '${segment}'`
|
|
3832
|
+
}
|
|
3833
|
+
};
|
|
3834
|
+
const index = Number(segment);
|
|
3835
|
+
if (!Number.isSafeInteger(index)) return {
|
|
3836
|
+
ok: false,
|
|
3837
|
+
error: {
|
|
3838
|
+
code: 409,
|
|
3839
|
+
reason: "OUT_OF_BOUNDS",
|
|
3840
|
+
message: `Index out of bounds at '${segment}'`
|
|
3841
|
+
}
|
|
3842
|
+
};
|
|
3843
|
+
const elemId = rgaIdAtIndex(current, index);
|
|
3844
|
+
if (elemId === void 0) return {
|
|
3845
|
+
ok: false,
|
|
3846
|
+
error: {
|
|
3847
|
+
code: 409,
|
|
3848
|
+
reason: "OUT_OF_BOUNDS",
|
|
3849
|
+
message: `Index out of bounds at '${segment}'`
|
|
3850
|
+
}
|
|
3851
|
+
};
|
|
3852
|
+
current = current.elems.get(elemId).value;
|
|
3853
|
+
continue;
|
|
3854
|
+
}
|
|
3855
|
+
return {
|
|
3856
|
+
ok: false,
|
|
3857
|
+
error: {
|
|
3858
|
+
code: 409,
|
|
3859
|
+
reason: "INVALID_TARGET",
|
|
3860
|
+
message: `Cannot traverse into non-container at '${segment}'`
|
|
3861
|
+
}
|
|
3862
|
+
};
|
|
3051
3863
|
}
|
|
3052
|
-
|
|
3864
|
+
return {
|
|
3865
|
+
ok: true,
|
|
3866
|
+
node: current
|
|
3867
|
+
};
|
|
3868
|
+
}
|
|
3869
|
+
function parseArrayIndexTokenForDoc(token, op, path, opIndex) {
|
|
3870
|
+
if (token === "-") {
|
|
3871
|
+
if (op !== "add") return {
|
|
3872
|
+
ok: false,
|
|
3873
|
+
code: 409,
|
|
3874
|
+
reason: "INVALID_POINTER",
|
|
3875
|
+
message: `'-' index is only valid for add at ${path}`,
|
|
3876
|
+
path,
|
|
3877
|
+
opIndex
|
|
3878
|
+
};
|
|
3053
3879
|
return {
|
|
3054
3880
|
ok: true,
|
|
3055
|
-
|
|
3881
|
+
index: Number.POSITIVE_INFINITY
|
|
3056
3882
|
};
|
|
3057
|
-
} catch (error) {
|
|
3058
|
-
return toPointerLookupApplyError(error, pointer, opIndex);
|
|
3059
3883
|
}
|
|
3884
|
+
if (!ARRAY_INDEX_TOKEN_PATTERN.test(token)) return {
|
|
3885
|
+
ok: false,
|
|
3886
|
+
code: 409,
|
|
3887
|
+
reason: "INVALID_POINTER",
|
|
3888
|
+
message: `expected array index at ${path}`,
|
|
3889
|
+
path,
|
|
3890
|
+
opIndex
|
|
3891
|
+
};
|
|
3892
|
+
const index = Number(token);
|
|
3893
|
+
if (!Number.isSafeInteger(index)) return {
|
|
3894
|
+
ok: false,
|
|
3895
|
+
code: 409,
|
|
3896
|
+
reason: "OUT_OF_BOUNDS",
|
|
3897
|
+
message: `array index is too large at ${path}`,
|
|
3898
|
+
path,
|
|
3899
|
+
opIndex
|
|
3900
|
+
};
|
|
3901
|
+
return {
|
|
3902
|
+
ok: true,
|
|
3903
|
+
index
|
|
3904
|
+
};
|
|
3905
|
+
}
|
|
3906
|
+
function validateArrayIndexBounds(index, op, arrLength, path, opIndex) {
|
|
3907
|
+
if (op === "add") {
|
|
3908
|
+
if (index === Number.POSITIVE_INFINITY) return {
|
|
3909
|
+
ok: true,
|
|
3910
|
+
index
|
|
3911
|
+
};
|
|
3912
|
+
if (index > arrLength) return {
|
|
3913
|
+
ok: false,
|
|
3914
|
+
code: 409,
|
|
3915
|
+
reason: "OUT_OF_BOUNDS",
|
|
3916
|
+
message: `index out of bounds at ${path}; expected 0..${arrLength}`,
|
|
3917
|
+
path,
|
|
3918
|
+
opIndex
|
|
3919
|
+
};
|
|
3920
|
+
} else if (index >= arrLength) return {
|
|
3921
|
+
ok: false,
|
|
3922
|
+
code: 409,
|
|
3923
|
+
reason: "OUT_OF_BOUNDS",
|
|
3924
|
+
message: `index out of bounds at ${path}; expected 0..${Math.max(arrLength - 1, 0)}`,
|
|
3925
|
+
path,
|
|
3926
|
+
opIndex
|
|
3927
|
+
};
|
|
3928
|
+
return {
|
|
3929
|
+
ok: true,
|
|
3930
|
+
index
|
|
3931
|
+
};
|
|
3060
3932
|
}
|
|
3061
3933
|
function bumpClockCounter(state, ctr) {
|
|
3062
3934
|
if (state.clock.ctr < ctr) state.clock.ctr = ctr;
|
|
@@ -3127,40 +3999,6 @@ function mergePointerPaths(basePointer, nestedPointer) {
|
|
|
3127
3999
|
if (basePointer === "") return nestedPointer;
|
|
3128
4000
|
return `${basePointer}${nestedPointer}`;
|
|
3129
4001
|
}
|
|
3130
|
-
function maxCtrInNodeForActor$1(node, actor) {
|
|
3131
|
-
let best = 0;
|
|
3132
|
-
const stack = [{
|
|
3133
|
-
node,
|
|
3134
|
-
depth: 0
|
|
3135
|
-
}];
|
|
3136
|
-
while (stack.length > 0) {
|
|
3137
|
-
const frame = stack.pop();
|
|
3138
|
-
assertTraversalDepth(frame.depth);
|
|
3139
|
-
if (frame.node.kind === "lww") {
|
|
3140
|
-
if (frame.node.dot.actor === actor && frame.node.dot.ctr > best) best = frame.node.dot.ctr;
|
|
3141
|
-
continue;
|
|
3142
|
-
}
|
|
3143
|
-
if (frame.node.kind === "obj") {
|
|
3144
|
-
for (const entry of frame.node.entries.values()) {
|
|
3145
|
-
if (entry.dot.actor === actor && entry.dot.ctr > best) best = entry.dot.ctr;
|
|
3146
|
-
stack.push({
|
|
3147
|
-
node: entry.node,
|
|
3148
|
-
depth: frame.depth + 1
|
|
3149
|
-
});
|
|
3150
|
-
}
|
|
3151
|
-
for (const tomb of frame.node.tombstone.values()) if (tomb.actor === actor && tomb.ctr > best) best = tomb.ctr;
|
|
3152
|
-
continue;
|
|
3153
|
-
}
|
|
3154
|
-
for (const elem of frame.node.elems.values()) {
|
|
3155
|
-
if (elem.insDot.actor === actor && elem.insDot.ctr > best) best = elem.insDot.ctr;
|
|
3156
|
-
stack.push({
|
|
3157
|
-
node: elem.value,
|
|
3158
|
-
depth: frame.depth + 1
|
|
3159
|
-
});
|
|
3160
|
-
}
|
|
3161
|
-
}
|
|
3162
|
-
return best;
|
|
3163
|
-
}
|
|
3164
4002
|
function toApplyError(error) {
|
|
3165
4003
|
if (error instanceof TraversalDepthError) return toDepthApplyError(error);
|
|
3166
4004
|
if (error instanceof PatchCompileError) return {
|
|
@@ -3178,26 +4016,19 @@ function toApplyError(error) {
|
|
|
3178
4016
|
message: error instanceof Error ? error.message : "failed to compile patch"
|
|
3179
4017
|
};
|
|
3180
4018
|
}
|
|
3181
|
-
function
|
|
4019
|
+
function withOpIndex(error, opIndex) {
|
|
4020
|
+
if (error.opIndex !== void 0) return error;
|
|
3182
4021
|
return {
|
|
3183
|
-
|
|
3184
|
-
code: 409,
|
|
3185
|
-
reason: "INVALID_POINTER",
|
|
3186
|
-
message: error instanceof Error ? error.message : "invalid pointer",
|
|
3187
|
-
path: pointer,
|
|
4022
|
+
...error,
|
|
3188
4023
|
opIndex
|
|
3189
4024
|
};
|
|
3190
4025
|
}
|
|
3191
|
-
function
|
|
3192
|
-
return new PatchCompileError("INVALID_POINTER", error instanceof Error ? error.message : "invalid pointer", pointer, opIndex);
|
|
3193
|
-
}
|
|
3194
|
-
function toPointerLookupApplyError(error, pointer, opIndex) {
|
|
3195
|
-
const mapped = mapLookupErrorToPatchReason(error);
|
|
4026
|
+
function toPointerParseApplyError(error, pointer, opIndex) {
|
|
3196
4027
|
return {
|
|
3197
4028
|
ok: false,
|
|
3198
4029
|
code: 409,
|
|
3199
|
-
reason:
|
|
3200
|
-
message:
|
|
4030
|
+
reason: "INVALID_POINTER",
|
|
4031
|
+
message: error instanceof Error ? error.message : "invalid pointer",
|
|
3201
4032
|
path: pointer,
|
|
3202
4033
|
opIndex
|
|
3203
4034
|
};
|
|
@@ -3206,6 +4037,8 @@ function toPointerLookupApplyError(error, pointer, opIndex) {
|
|
|
3206
4037
|
//#endregion
|
|
3207
4038
|
//#region src/serialize.ts
|
|
3208
4039
|
const HEAD_ELEM_ID = "HEAD";
|
|
4040
|
+
const SERIALIZED_DOC_VERSION = 1;
|
|
4041
|
+
const SERIALIZED_STATE_VERSION = 1;
|
|
3209
4042
|
function createSerializedRecord() {
|
|
3210
4043
|
return Object.create(null);
|
|
3211
4044
|
}
|
|
@@ -3230,13 +4063,16 @@ var DeserializeError = class extends Error {
|
|
|
3230
4063
|
};
|
|
3231
4064
|
/** Serialize a CRDT document to a JSON-safe representation (Maps become plain objects). */
|
|
3232
4065
|
function serializeDoc(doc) {
|
|
3233
|
-
return {
|
|
4066
|
+
return {
|
|
4067
|
+
version: SERIALIZED_DOC_VERSION,
|
|
4068
|
+
root: serializeNode(doc.root)
|
|
4069
|
+
};
|
|
3234
4070
|
}
|
|
3235
4071
|
/** Reconstruct a CRDT document from its serialized form. */
|
|
3236
4072
|
function deserializeDoc(data) {
|
|
3237
|
-
|
|
3238
|
-
if (!("root" in
|
|
3239
|
-
return { root: deserializeNode(
|
|
4073
|
+
const raw = readSerializedDocEnvelope(data);
|
|
4074
|
+
if (!("root" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/root", "serialized doc is missing root");
|
|
4075
|
+
return { root: deserializeNode(raw.root, "/root", 0) };
|
|
3240
4076
|
}
|
|
3241
4077
|
/** Non-throwing `deserializeDoc` variant with typed validation details. */
|
|
3242
4078
|
function tryDeserializeDoc(data) {
|
|
@@ -3257,6 +4093,7 @@ function tryDeserializeDoc(data) {
|
|
|
3257
4093
|
/** Serialize a full CRDT state (document + clock) to a JSON-safe representation. */
|
|
3258
4094
|
function serializeState(state) {
|
|
3259
4095
|
return {
|
|
4096
|
+
version: SERIALIZED_STATE_VERSION,
|
|
3260
4097
|
doc: serializeDoc(state.doc),
|
|
3261
4098
|
clock: {
|
|
3262
4099
|
actor: state.clock.actor,
|
|
@@ -3264,16 +4101,21 @@ function serializeState(state) {
|
|
|
3264
4101
|
}
|
|
3265
4102
|
};
|
|
3266
4103
|
}
|
|
3267
|
-
/**
|
|
4104
|
+
/**
|
|
4105
|
+
* Reconstruct a full CRDT state from its serialized form, restoring the clock.
|
|
4106
|
+
*
|
|
4107
|
+
* May throw `TraversalDepthError` when the payload exceeds the maximum
|
|
4108
|
+
* supported nesting depth.
|
|
4109
|
+
*/
|
|
3268
4110
|
function deserializeState(data) {
|
|
3269
|
-
|
|
3270
|
-
if (!("doc" in
|
|
3271
|
-
if (!("clock" in
|
|
3272
|
-
const clockRaw = asRecord(
|
|
4111
|
+
const raw = readSerializedStateEnvelope(data);
|
|
4112
|
+
if (!("doc" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/doc", "serialized state is missing doc");
|
|
4113
|
+
if (!("clock" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/clock", "serialized state is missing clock");
|
|
4114
|
+
const clockRaw = asRecord(raw.clock, "/clock");
|
|
3273
4115
|
const actor = readActor(clockRaw.actor, "/clock/actor");
|
|
3274
4116
|
const ctr = readCounter(clockRaw.ctr, "/clock/ctr");
|
|
3275
|
-
const doc = deserializeDoc(
|
|
3276
|
-
const observedCtr =
|
|
4117
|
+
const doc = deserializeDoc(raw.doc);
|
|
4118
|
+
const observedCtr = observedVersionVector(doc)[actor] ?? 0;
|
|
3277
4119
|
return {
|
|
3278
4120
|
doc,
|
|
3279
4121
|
clock: createClock(actor, Math.max(ctr, observedCtr))
|
|
@@ -3347,6 +4189,16 @@ function serializeNode(node) {
|
|
|
3347
4189
|
elems
|
|
3348
4190
|
};
|
|
3349
4191
|
}
|
|
4192
|
+
function readSerializedDocEnvelope(data) {
|
|
4193
|
+
const raw = asRecord(data, "/");
|
|
4194
|
+
assertSerializedEnvelopeVersion(raw, "/version", SERIALIZED_DOC_VERSION, "doc");
|
|
4195
|
+
return raw;
|
|
4196
|
+
}
|
|
4197
|
+
function readSerializedStateEnvelope(data) {
|
|
4198
|
+
const raw = asRecord(data, "/");
|
|
4199
|
+
assertSerializedEnvelopeVersion(raw, "/version", SERIALIZED_STATE_VERSION, "state");
|
|
4200
|
+
return raw;
|
|
4201
|
+
}
|
|
3350
4202
|
function deserializeNode(node, path, depth) {
|
|
3351
4203
|
assertTraversalDepth(depth);
|
|
3352
4204
|
const raw = asRecord(node, path);
|
|
@@ -3434,28 +4286,19 @@ function assertAcyclicRgaPredecessors(elems, path) {
|
|
|
3434
4286
|
for (const id of trail) visitState.set(id, 2);
|
|
3435
4287
|
}
|
|
3436
4288
|
}
|
|
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
|
-
}
|
|
3455
4289
|
function asRecord(value, path) {
|
|
3456
4290
|
if (!isRecord(value)) fail("INVALID_SERIALIZED_SHAPE", path, "expected object");
|
|
3457
4291
|
return value;
|
|
3458
4292
|
}
|
|
4293
|
+
function assertSerializedEnvelopeVersion(raw, path, expectedVersion, label) {
|
|
4294
|
+
if (!("version" in raw)) return;
|
|
4295
|
+
const version = readVersion(raw.version, path);
|
|
4296
|
+
if (version !== expectedVersion) fail("INVALID_SERIALIZED_SHAPE", path, `unsupported serialized ${label} version '${version}'`);
|
|
4297
|
+
}
|
|
4298
|
+
function readVersion(value, path) {
|
|
4299
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) fail("INVALID_SERIALIZED_SHAPE", path, "envelope version must be a non-negative safe integer");
|
|
4300
|
+
return value;
|
|
4301
|
+
}
|
|
3459
4302
|
function readDot(value, path) {
|
|
3460
4303
|
const raw = asRecord(value, path);
|
|
3461
4304
|
return {
|
|
@@ -3553,20 +4396,23 @@ function mergeDoc(a, b, options = {}) {
|
|
|
3553
4396
|
/** Non-throwing `mergeDoc` variant with structured conflict details. */
|
|
3554
4397
|
function tryMergeDoc(a, b, options = {}) {
|
|
3555
4398
|
try {
|
|
3556
|
-
const
|
|
3557
|
-
if (
|
|
3558
|
-
|
|
3559
|
-
|
|
4399
|
+
const config = { unrelatedArrays: resolveUnrelatedArraysStrategy(options) };
|
|
4400
|
+
if (config.unrelatedArrays === "reject") {
|
|
4401
|
+
const mismatchPath = findSeqLineageMismatch(a.root, b.root, []);
|
|
4402
|
+
if (mismatchPath !== null) return {
|
|
3560
4403
|
ok: false,
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
4404
|
+
error: {
|
|
4405
|
+
ok: false,
|
|
4406
|
+
code: 409,
|
|
4407
|
+
reason: "LINEAGE_MISMATCH",
|
|
4408
|
+
message: `merge requires shared array origin at ${mismatchPath}`,
|
|
4409
|
+
path: mismatchPath
|
|
4410
|
+
}
|
|
4411
|
+
};
|
|
4412
|
+
}
|
|
3567
4413
|
return {
|
|
3568
4414
|
ok: true,
|
|
3569
|
-
doc:
|
|
4415
|
+
doc: mergeDocRoot(a.root, b.root, config).doc
|
|
3570
4416
|
};
|
|
3571
4417
|
} catch (error) {
|
|
3572
4418
|
if (error instanceof SharedElementMetadataMismatchError) return {
|
|
@@ -3592,7 +4438,7 @@ function tryMergeDoc(a, b, options = {}) {
|
|
|
3592
4438
|
* The merged clock keeps a stable actor identity:
|
|
3593
4439
|
* - defaults to the actor from the first argument (`a`)
|
|
3594
4440
|
* - can be overridden via `options.actor`
|
|
3595
|
-
* - optional `options.
|
|
4441
|
+
* - optional `options.unrelatedArrays` controls the merge strategy for non-overlapping sequences
|
|
3596
4442
|
*
|
|
3597
4443
|
* The merged counter is lifted to the highest counter already observed for
|
|
3598
4444
|
* that actor across both input clocks and the merged document dots.
|
|
@@ -3604,17 +4450,51 @@ function mergeState(a, b, options = {}) {
|
|
|
3604
4450
|
}
|
|
3605
4451
|
/** Non-throwing `mergeState` variant with structured conflict details. */
|
|
3606
4452
|
function tryMergeState(a, b, options = {}) {
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
doc,
|
|
3615
|
-
|
|
4453
|
+
try {
|
|
4454
|
+
const actor = options.actor ?? a.clock.actor;
|
|
4455
|
+
const config = {
|
|
4456
|
+
actor,
|
|
4457
|
+
unrelatedArrays: resolveUnrelatedArraysStrategy(options)
|
|
4458
|
+
};
|
|
4459
|
+
if (config.unrelatedArrays === "reject") {
|
|
4460
|
+
const mismatchPath = findSeqLineageMismatch(a.doc.root, b.doc.root, []);
|
|
4461
|
+
if (mismatchPath !== null) return {
|
|
4462
|
+
ok: false,
|
|
4463
|
+
error: {
|
|
4464
|
+
ok: false,
|
|
4465
|
+
code: 409,
|
|
4466
|
+
reason: "LINEAGE_MISMATCH",
|
|
4467
|
+
message: `merge requires shared array origin at ${mismatchPath}`,
|
|
4468
|
+
path: mismatchPath
|
|
4469
|
+
}
|
|
4470
|
+
};
|
|
3616
4471
|
}
|
|
3617
|
-
|
|
4472
|
+
const merged = mergeDocRoot(a.doc.root, b.doc.root, config);
|
|
4473
|
+
const ctr = maxObservedCtrForActor(merged.maxObservedCtr, actor, a, b);
|
|
4474
|
+
return {
|
|
4475
|
+
ok: true,
|
|
4476
|
+
state: {
|
|
4477
|
+
doc: merged.doc,
|
|
4478
|
+
clock: createClock(actor, ctr)
|
|
4479
|
+
}
|
|
4480
|
+
};
|
|
4481
|
+
} catch (error) {
|
|
4482
|
+
if (error instanceof SharedElementMetadataMismatchError) return {
|
|
4483
|
+
ok: false,
|
|
4484
|
+
error: {
|
|
4485
|
+
ok: false,
|
|
4486
|
+
code: 409,
|
|
4487
|
+
reason: "LINEAGE_MISMATCH",
|
|
4488
|
+
message: error.message,
|
|
4489
|
+
path: error.path
|
|
4490
|
+
}
|
|
4491
|
+
};
|
|
4492
|
+
if (error instanceof TraversalDepthError) return {
|
|
4493
|
+
ok: false,
|
|
4494
|
+
error: toDepthApplyError(error)
|
|
4495
|
+
};
|
|
4496
|
+
throw error;
|
|
4497
|
+
}
|
|
3618
4498
|
}
|
|
3619
4499
|
function findSeqLineageMismatch(a, b, path) {
|
|
3620
4500
|
const stack = [{
|
|
@@ -3635,7 +4515,7 @@ function findSeqLineageMismatch(a, b, path) {
|
|
|
3635
4515
|
shared = true;
|
|
3636
4516
|
break;
|
|
3637
4517
|
}
|
|
3638
|
-
if (!shared) return
|
|
4518
|
+
if (!shared) return stringifyJsonPointer(frame.path);
|
|
3639
4519
|
}
|
|
3640
4520
|
}
|
|
3641
4521
|
if (frame.a.kind === "obj" && frame.b.kind === "obj") {
|
|
@@ -3657,14 +4537,29 @@ function findSeqLineageMismatch(a, b, path) {
|
|
|
3657
4537
|
}
|
|
3658
4538
|
return null;
|
|
3659
4539
|
}
|
|
3660
|
-
function
|
|
3661
|
-
|
|
4540
|
+
function mergeDocRoot(a, b, config) {
|
|
4541
|
+
const merged = mergeNodeAtDepth(a, b, 0, [], config);
|
|
4542
|
+
return {
|
|
4543
|
+
doc: { root: merged.node },
|
|
4544
|
+
maxObservedCtr: merged.maxObservedCtr
|
|
4545
|
+
};
|
|
4546
|
+
}
|
|
4547
|
+
function resolveUnrelatedArraysStrategy(options) {
|
|
4548
|
+
if (options.unrelatedArrays !== void 0) return options.unrelatedArrays;
|
|
4549
|
+
if (options.requireSharedOrigin === false) return "unsafe-union";
|
|
4550
|
+
return "reject";
|
|
4551
|
+
}
|
|
4552
|
+
function maxObservedCtrForActor(docObservedCtr, actor, a, b) {
|
|
4553
|
+
let best = docObservedCtr;
|
|
3662
4554
|
if (a.clock.actor === actor && a.clock.ctr > best) best = a.clock.ctr;
|
|
3663
4555
|
if (b.clock.actor === actor && b.clock.ctr > best) best = b.clock.ctr;
|
|
3664
4556
|
return best;
|
|
3665
4557
|
}
|
|
3666
|
-
function
|
|
3667
|
-
let best =
|
|
4558
|
+
function repDot(node) {
|
|
4559
|
+
let best = {
|
|
4560
|
+
actor: "",
|
|
4561
|
+
ctr: 0
|
|
4562
|
+
};
|
|
3668
4563
|
const stack = [{
|
|
3669
4564
|
node,
|
|
3670
4565
|
depth: 0
|
|
@@ -3672,158 +4567,188 @@ function maxCtrInNodeForActor(node, actor) {
|
|
|
3672
4567
|
while (stack.length > 0) {
|
|
3673
4568
|
const frame = stack.pop();
|
|
3674
4569
|
assertTraversalDepth(frame.depth);
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
4570
|
+
switch (frame.node.kind) {
|
|
4571
|
+
case "lww":
|
|
4572
|
+
if (compareDot(frame.node.dot, best) > 0) best = frame.node.dot;
|
|
4573
|
+
break;
|
|
4574
|
+
case "obj":
|
|
4575
|
+
for (const entry of frame.node.entries.values()) {
|
|
4576
|
+
if (compareDot(entry.dot, best) > 0) best = entry.dot;
|
|
4577
|
+
stack.push({
|
|
4578
|
+
node: entry.node,
|
|
4579
|
+
depth: frame.depth + 1
|
|
4580
|
+
});
|
|
4581
|
+
}
|
|
4582
|
+
for (const tombstone of frame.node.tombstone.values()) if (compareDot(tombstone, best) > 0) best = tombstone;
|
|
4583
|
+
break;
|
|
4584
|
+
case "seq":
|
|
4585
|
+
for (const elem of frame.node.elems.values()) {
|
|
4586
|
+
if (compareDot(elem.insDot, best) > 0) best = elem.insDot;
|
|
4587
|
+
if (elem.delDot && compareDot(elem.delDot, best) > 0) best = elem.delDot;
|
|
4588
|
+
stack.push({
|
|
4589
|
+
node: elem.value,
|
|
4590
|
+
depth: frame.depth + 1
|
|
4591
|
+
});
|
|
4592
|
+
}
|
|
4593
|
+
break;
|
|
3696
4594
|
}
|
|
3697
4595
|
}
|
|
3698
4596
|
return best;
|
|
3699
4597
|
}
|
|
3700
|
-
function
|
|
3701
|
-
switch (node.kind) {
|
|
3702
|
-
case "lww": return node.dot;
|
|
3703
|
-
case "obj": {
|
|
3704
|
-
let best = {
|
|
3705
|
-
actor: "",
|
|
3706
|
-
ctr: 0
|
|
3707
|
-
};
|
|
3708
|
-
for (const entry of node.entries.values()) if (compareDot(entry.dot, best) > 0) best = entry.dot;
|
|
3709
|
-
for (const d of node.tombstone.values()) if (compareDot(d, best) > 0) best = d;
|
|
3710
|
-
return best;
|
|
3711
|
-
}
|
|
3712
|
-
case "seq": {
|
|
3713
|
-
let best = {
|
|
3714
|
-
actor: "",
|
|
3715
|
-
ctr: 0
|
|
3716
|
-
};
|
|
3717
|
-
for (const e of node.elems.values()) if (compareDot(e.insDot, best) > 0) best = e.insDot;
|
|
3718
|
-
return best;
|
|
3719
|
-
}
|
|
3720
|
-
}
|
|
3721
|
-
}
|
|
3722
|
-
function mergeNode(a, b) {
|
|
3723
|
-
return mergeNodeAtDepth(a, b, 0, []);
|
|
3724
|
-
}
|
|
3725
|
-
function mergeNodeAtDepth(a, b, depth, path) {
|
|
4598
|
+
function mergeNodeAtDepth(a, b, depth, path, config) {
|
|
3726
4599
|
assertTraversalDepth(depth);
|
|
3727
|
-
if (a.kind === "lww" && b.kind === "lww") return mergeLww(a, b);
|
|
3728
|
-
if (a.kind === "obj" && b.kind === "obj") return mergeObj(a, b, depth + 1, path);
|
|
3729
|
-
if (a.kind === "seq" && b.kind === "seq") return mergeSeq(a, b, depth + 1, path);
|
|
3730
|
-
if (compareDot(repDot(a), repDot(b)) >= 0) return cloneNodeShallow(a, depth + 1);
|
|
3731
|
-
return cloneNodeShallow(b, depth + 1);
|
|
4600
|
+
if (a.kind === "lww" && b.kind === "lww") return mergeLww(a, b, config.actor);
|
|
4601
|
+
if (a.kind === "obj" && b.kind === "obj") return mergeObj(a, b, depth + 1, path, config);
|
|
4602
|
+
if (a.kind === "seq" && b.kind === "seq") return mergeSeq(a, b, depth + 1, path, config);
|
|
4603
|
+
if (compareDot(repDot(a), repDot(b)) >= 0) return cloneNodeShallow(a, depth + 1, config.actor);
|
|
4604
|
+
return cloneNodeShallow(b, depth + 1, config.actor);
|
|
3732
4605
|
}
|
|
3733
|
-
function mergeLww(a, b) {
|
|
4606
|
+
function mergeLww(a, b, actor) {
|
|
3734
4607
|
if (compareDot(a.dot, b.dot) >= 0) return {
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
4608
|
+
node: {
|
|
4609
|
+
kind: "lww",
|
|
4610
|
+
value: structuredClone(a.value),
|
|
4611
|
+
dot: { ...a.dot }
|
|
4612
|
+
},
|
|
4613
|
+
maxObservedCtr: maxObservedCtrForDot(a.dot, actor)
|
|
3738
4614
|
};
|
|
3739
4615
|
return {
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
4616
|
+
node: {
|
|
4617
|
+
kind: "lww",
|
|
4618
|
+
value: structuredClone(b.value),
|
|
4619
|
+
dot: { ...b.dot }
|
|
4620
|
+
},
|
|
4621
|
+
maxObservedCtr: maxObservedCtrForDot(b.dot, actor)
|
|
3743
4622
|
};
|
|
3744
4623
|
}
|
|
3745
|
-
function mergeObj(a, b, depth, path) {
|
|
4624
|
+
function mergeObj(a, b, depth, path, config) {
|
|
3746
4625
|
assertTraversalDepth(depth);
|
|
3747
4626
|
const entries = /* @__PURE__ */ new Map();
|
|
3748
4627
|
const tombstone = /* @__PURE__ */ new Map();
|
|
4628
|
+
let maxObservedCtr = 0;
|
|
3749
4629
|
const allTombKeys = new Set([...a.tombstone.keys(), ...b.tombstone.keys()]);
|
|
3750
4630
|
for (const key of allTombKeys) {
|
|
3751
4631
|
const da = a.tombstone.get(key);
|
|
3752
4632
|
const db = b.tombstone.get(key);
|
|
3753
|
-
if (da && db)
|
|
3754
|
-
|
|
3755
|
-
|
|
4633
|
+
if (da && db) {
|
|
4634
|
+
const mergedDot = compareDot(da, db) >= 0 ? { ...da } : { ...db };
|
|
4635
|
+
tombstone.set(key, mergedDot);
|
|
4636
|
+
maxObservedCtr = Math.max(maxObservedCtr, maxObservedCtrForDot(mergedDot, config.actor));
|
|
4637
|
+
} else if (da) {
|
|
4638
|
+
tombstone.set(key, { ...da });
|
|
4639
|
+
maxObservedCtr = Math.max(maxObservedCtr, maxObservedCtrForDot(da, config.actor));
|
|
4640
|
+
} else {
|
|
4641
|
+
tombstone.set(key, { ...db });
|
|
4642
|
+
maxObservedCtr = Math.max(maxObservedCtr, maxObservedCtrForDot(db, config.actor));
|
|
4643
|
+
}
|
|
3756
4644
|
}
|
|
3757
4645
|
const allKeys = new Set([...a.entries.keys(), ...b.entries.keys()]);
|
|
3758
4646
|
for (const key of allKeys) {
|
|
3759
4647
|
const ea = a.entries.get(key);
|
|
3760
4648
|
const eb = b.entries.get(key);
|
|
3761
4649
|
let merged;
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
4650
|
+
let mergedNodeMaxObservedCtr = 0;
|
|
4651
|
+
if (ea && eb) {
|
|
4652
|
+
const mergedNode = mergeNodeAtDepth(ea.node, eb.node, depth + 1, [...path, key], config);
|
|
4653
|
+
const dot = compareDot(ea.dot, eb.dot) >= 0 ? { ...ea.dot } : { ...eb.dot };
|
|
4654
|
+
merged = {
|
|
4655
|
+
node: mergedNode.node,
|
|
4656
|
+
dot
|
|
4657
|
+
};
|
|
4658
|
+
mergedNodeMaxObservedCtr = mergedNode.maxObservedCtr;
|
|
4659
|
+
} else if (ea) {
|
|
4660
|
+
const cloned = cloneNodeShallow(ea.node, depth + 1, config.actor);
|
|
4661
|
+
merged = {
|
|
4662
|
+
node: cloned.node,
|
|
4663
|
+
dot: { ...ea.dot }
|
|
4664
|
+
};
|
|
4665
|
+
mergedNodeMaxObservedCtr = cloned.maxObservedCtr;
|
|
4666
|
+
} else {
|
|
4667
|
+
const cloned = cloneNodeShallow(eb.node, depth + 1, config.actor);
|
|
4668
|
+
merged = {
|
|
4669
|
+
node: cloned.node,
|
|
4670
|
+
dot: { ...eb.dot }
|
|
4671
|
+
};
|
|
4672
|
+
mergedNodeMaxObservedCtr = cloned.maxObservedCtr;
|
|
4673
|
+
}
|
|
3774
4674
|
const td = tombstone.get(key);
|
|
3775
4675
|
if (td && compareDot(td, merged.dot) >= 0) continue;
|
|
3776
4676
|
entries.set(key, merged);
|
|
4677
|
+
maxObservedCtr = Math.max(maxObservedCtr, mergedNodeMaxObservedCtr, maxObservedCtrForDot(merged.dot, config.actor));
|
|
3777
4678
|
}
|
|
3778
4679
|
return {
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
4680
|
+
node: {
|
|
4681
|
+
kind: "obj",
|
|
4682
|
+
entries,
|
|
4683
|
+
tombstone
|
|
4684
|
+
},
|
|
4685
|
+
maxObservedCtr
|
|
3782
4686
|
};
|
|
3783
4687
|
}
|
|
3784
|
-
function mergeSeq(a, b, depth, path) {
|
|
4688
|
+
function mergeSeq(a, b, depth, path, config) {
|
|
3785
4689
|
assertTraversalDepth(depth);
|
|
4690
|
+
if (config.unrelatedArrays === "atomic-replace" && a.elems.size > 0 && b.elems.size > 0) {
|
|
4691
|
+
let shared = false;
|
|
4692
|
+
for (const id of a.elems.keys()) if (b.elems.has(id)) {
|
|
4693
|
+
shared = true;
|
|
4694
|
+
break;
|
|
4695
|
+
}
|
|
4696
|
+
if (!shared) return cloneNodeShallow(compareDot(repDot(a), repDot(b)) >= 0 ? a : b, depth, config.actor);
|
|
4697
|
+
}
|
|
3786
4698
|
const elems = /* @__PURE__ */ new Map();
|
|
4699
|
+
let maxObservedCtr = 0;
|
|
3787
4700
|
const allIds = new Set([...a.elems.keys(), ...b.elems.keys()]);
|
|
3788
4701
|
for (const id of allIds) {
|
|
3789
4702
|
const ea = a.elems.get(id);
|
|
3790
4703
|
const eb = b.elems.get(id);
|
|
3791
4704
|
if (ea && eb) {
|
|
3792
|
-
if (ea.prev !== eb.prev) throw new SharedElementMetadataMismatchError(
|
|
3793
|
-
if (!sameDot(ea.insDot, eb.insDot)) throw new SharedElementMetadataMismatchError(
|
|
3794
|
-
const mergedValue = mergeNodeAtDepth(ea.value, eb.value, depth + 1, [...path, id]);
|
|
4705
|
+
if (ea.prev !== eb.prev) throw new SharedElementMetadataMismatchError(stringifyJsonPointer(path), id, "prev");
|
|
4706
|
+
if (!sameDot(ea.insDot, eb.insDot)) throw new SharedElementMetadataMismatchError(stringifyJsonPointer(path), id, "insDot");
|
|
4707
|
+
const mergedValue = mergeNodeAtDepth(ea.value, eb.value, depth + 1, [...path, id], config);
|
|
4708
|
+
const mergedDeleteDot = mergeDeleteDot(ea.delDot, eb.delDot);
|
|
3795
4709
|
elems.set(id, {
|
|
3796
4710
|
id,
|
|
3797
4711
|
prev: ea.prev,
|
|
3798
4712
|
tombstone: ea.tombstone || eb.tombstone,
|
|
3799
|
-
delDot:
|
|
3800
|
-
value: mergedValue,
|
|
4713
|
+
delDot: mergedDeleteDot,
|
|
4714
|
+
value: mergedValue.node,
|
|
3801
4715
|
insDot: { ...ea.insDot }
|
|
3802
4716
|
});
|
|
3803
|
-
|
|
3804
|
-
else
|
|
4717
|
+
maxObservedCtr = Math.max(maxObservedCtr, mergedValue.maxObservedCtr, maxObservedCtrForDot(ea.insDot, config.actor), maxObservedCtrForDot(mergedDeleteDot, config.actor));
|
|
4718
|
+
} else if (ea) {
|
|
4719
|
+
const cloned = cloneElem(ea, depth + 1, config.actor);
|
|
4720
|
+
elems.set(id, cloned.elem);
|
|
4721
|
+
maxObservedCtr = Math.max(maxObservedCtr, cloned.maxObservedCtr);
|
|
4722
|
+
} else {
|
|
4723
|
+
const cloned = cloneElem(eb, depth + 1, config.actor);
|
|
4724
|
+
elems.set(id, cloned.elem);
|
|
4725
|
+
maxObservedCtr = Math.max(maxObservedCtr, cloned.maxObservedCtr);
|
|
4726
|
+
}
|
|
3805
4727
|
}
|
|
3806
4728
|
return {
|
|
3807
|
-
|
|
3808
|
-
|
|
4729
|
+
node: {
|
|
4730
|
+
kind: "seq",
|
|
4731
|
+
elems
|
|
4732
|
+
},
|
|
4733
|
+
maxObservedCtr
|
|
3809
4734
|
};
|
|
3810
4735
|
}
|
|
3811
4736
|
function sameDot(a, b) {
|
|
3812
4737
|
return a.actor === b.actor && a.ctr === b.ctr;
|
|
3813
4738
|
}
|
|
3814
|
-
function
|
|
3815
|
-
if (path.length === 0) return "/";
|
|
3816
|
-
return `/${path.join("/")}`;
|
|
3817
|
-
}
|
|
3818
|
-
function cloneElem(e, depth) {
|
|
4739
|
+
function cloneElem(e, depth, actor) {
|
|
3819
4740
|
assertTraversalDepth(depth);
|
|
4741
|
+
const value = cloneNodeShallow(e.value, depth + 1, actor);
|
|
3820
4742
|
return {
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
4743
|
+
elem: {
|
|
4744
|
+
id: e.id,
|
|
4745
|
+
prev: e.prev,
|
|
4746
|
+
tombstone: e.tombstone,
|
|
4747
|
+
delDot: e.delDot ? { ...e.delDot } : void 0,
|
|
4748
|
+
value: value.node,
|
|
4749
|
+
insDot: { ...e.insDot }
|
|
4750
|
+
},
|
|
4751
|
+
maxObservedCtr: Math.max(value.maxObservedCtr, maxObservedCtrForDot(e.insDot, actor), maxObservedCtrForDot(e.delDot, actor))
|
|
3827
4752
|
};
|
|
3828
4753
|
}
|
|
3829
4754
|
function mergeDeleteDot(a, b) {
|
|
@@ -3831,38 +4756,64 @@ function mergeDeleteDot(a, b) {
|
|
|
3831
4756
|
if (a) return { ...a };
|
|
3832
4757
|
if (b) return { ...b };
|
|
3833
4758
|
}
|
|
3834
|
-
function cloneNodeShallow(node, depth) {
|
|
4759
|
+
function cloneNodeShallow(node, depth, actor) {
|
|
3835
4760
|
assertTraversalDepth(depth);
|
|
3836
4761
|
switch (node.kind) {
|
|
3837
4762
|
case "lww": return {
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
4763
|
+
node: {
|
|
4764
|
+
kind: "lww",
|
|
4765
|
+
value: structuredClone(node.value),
|
|
4766
|
+
dot: { ...node.dot }
|
|
4767
|
+
},
|
|
4768
|
+
maxObservedCtr: maxObservedCtrForDot(node.dot, actor)
|
|
3841
4769
|
};
|
|
3842
4770
|
case "obj": {
|
|
3843
4771
|
const entries = /* @__PURE__ */ new Map();
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
4772
|
+
let maxObservedCtr = 0;
|
|
4773
|
+
for (const [k, v] of node.entries) {
|
|
4774
|
+
const cloned = cloneNodeShallow(v.node, depth + 1, actor);
|
|
4775
|
+
entries.set(k, {
|
|
4776
|
+
node: cloned.node,
|
|
4777
|
+
dot: { ...v.dot }
|
|
4778
|
+
});
|
|
4779
|
+
maxObservedCtr = Math.max(maxObservedCtr, cloned.maxObservedCtr, maxObservedCtrForDot(v.dot, actor));
|
|
4780
|
+
}
|
|
3848
4781
|
const tombstone = /* @__PURE__ */ new Map();
|
|
3849
|
-
for (const [k, d] of node.tombstone)
|
|
4782
|
+
for (const [k, d] of node.tombstone) {
|
|
4783
|
+
tombstone.set(k, { ...d });
|
|
4784
|
+
maxObservedCtr = Math.max(maxObservedCtr, maxObservedCtrForDot(d, actor));
|
|
4785
|
+
}
|
|
3850
4786
|
return {
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
4787
|
+
node: {
|
|
4788
|
+
kind: "obj",
|
|
4789
|
+
entries,
|
|
4790
|
+
tombstone
|
|
4791
|
+
},
|
|
4792
|
+
maxObservedCtr
|
|
3854
4793
|
};
|
|
3855
4794
|
}
|
|
3856
4795
|
case "seq": {
|
|
3857
4796
|
const elems = /* @__PURE__ */ new Map();
|
|
3858
|
-
|
|
4797
|
+
let maxObservedCtr = 0;
|
|
4798
|
+
for (const [id, e] of node.elems) {
|
|
4799
|
+
const cloned = cloneElem(e, depth + 1, actor);
|
|
4800
|
+
elems.set(id, cloned.elem);
|
|
4801
|
+
maxObservedCtr = Math.max(maxObservedCtr, cloned.maxObservedCtr);
|
|
4802
|
+
}
|
|
3859
4803
|
return {
|
|
3860
|
-
|
|
3861
|
-
|
|
4804
|
+
node: {
|
|
4805
|
+
kind: "seq",
|
|
4806
|
+
elems
|
|
4807
|
+
},
|
|
4808
|
+
maxObservedCtr
|
|
3862
4809
|
};
|
|
3863
4810
|
}
|
|
3864
4811
|
}
|
|
3865
4812
|
}
|
|
4813
|
+
function maxObservedCtrForDot(dot, actor) {
|
|
4814
|
+
if (!dot || !actor || dot.actor !== actor) return 0;
|
|
4815
|
+
return dot.ctr;
|
|
4816
|
+
}
|
|
3866
4817
|
|
|
3867
4818
|
//#endregion
|
|
3868
4819
|
//#region src/compact.ts
|
|
@@ -4142,6 +5093,12 @@ Object.defineProperty(exports, 'getAtJson', {
|
|
|
4142
5093
|
return getAtJson;
|
|
4143
5094
|
}
|
|
4144
5095
|
});
|
|
5096
|
+
Object.defineProperty(exports, 'intersectVersionVectors', {
|
|
5097
|
+
enumerable: true,
|
|
5098
|
+
get: function () {
|
|
5099
|
+
return intersectVersionVectors;
|
|
5100
|
+
}
|
|
5101
|
+
});
|
|
4145
5102
|
Object.defineProperty(exports, 'jsonEquals', {
|
|
4146
5103
|
enumerable: true,
|
|
4147
5104
|
get: function () {
|
|
@@ -4184,6 +5141,12 @@ Object.defineProperty(exports, 'mergeState', {
|
|
|
4184
5141
|
return mergeState;
|
|
4185
5142
|
}
|
|
4186
5143
|
});
|
|
5144
|
+
Object.defineProperty(exports, 'mergeVersionVectors', {
|
|
5145
|
+
enumerable: true,
|
|
5146
|
+
get: function () {
|
|
5147
|
+
return mergeVersionVectors;
|
|
5148
|
+
}
|
|
5149
|
+
});
|
|
4187
5150
|
Object.defineProperty(exports, 'newObj', {
|
|
4188
5151
|
enumerable: true,
|
|
4189
5152
|
get: function () {
|
|
@@ -4232,6 +5195,12 @@ Object.defineProperty(exports, 'observeDot', {
|
|
|
4232
5195
|
return observeDot;
|
|
4233
5196
|
}
|
|
4234
5197
|
});
|
|
5198
|
+
Object.defineProperty(exports, 'observedVersionVector', {
|
|
5199
|
+
enumerable: true,
|
|
5200
|
+
get: function () {
|
|
5201
|
+
return observedVersionVector;
|
|
5202
|
+
}
|
|
5203
|
+
});
|
|
4235
5204
|
Object.defineProperty(exports, 'parseJsonPointer', {
|
|
4236
5205
|
enumerable: true,
|
|
4237
5206
|
get: function () {
|
|
@@ -4292,6 +5261,12 @@ Object.defineProperty(exports, 'serializeState', {
|
|
|
4292
5261
|
return serializeState;
|
|
4293
5262
|
}
|
|
4294
5263
|
});
|
|
5264
|
+
Object.defineProperty(exports, 'stableJsonValueKey', {
|
|
5265
|
+
enumerable: true,
|
|
5266
|
+
get: function () {
|
|
5267
|
+
return stableJsonValueKey;
|
|
5268
|
+
}
|
|
5269
|
+
});
|
|
4295
5270
|
Object.defineProperty(exports, 'stringifyJsonPointer', {
|
|
4296
5271
|
enumerable: true,
|
|
4297
5272
|
get: function () {
|
|
@@ -4364,6 +5339,12 @@ Object.defineProperty(exports, 'validateRgaSeq', {
|
|
|
4364
5339
|
return validateRgaSeq;
|
|
4365
5340
|
}
|
|
4366
5341
|
});
|
|
5342
|
+
Object.defineProperty(exports, 'versionVectorCovers', {
|
|
5343
|
+
enumerable: true,
|
|
5344
|
+
get: function () {
|
|
5345
|
+
return versionVectorCovers;
|
|
5346
|
+
}
|
|
5347
|
+
});
|
|
4367
5348
|
Object.defineProperty(exports, 'vvHasDot', {
|
|
4368
5349
|
enumerable: true,
|
|
4369
5350
|
get: function () {
|