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