json-patch-to-crdt 0.3.0 → 0.4.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 +79 -1
- package/dist/{compact-BS7F604m.mjs → compact-BcwxBNx_.mjs} +810 -409
- package/dist/{compact-BToZE6Q6.js → compact-CXfvMNCT.js} +839 -408
- package/dist/{depth-BTHjgY18.d.mts → depth-CpJSyZE5.d.mts} +72 -14
- package/dist/{depth-DSl2ghKu.d.ts → depth-D88VeWb-.d.ts} +72 -14
- 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 +2 -2
- package/dist/internals.d.ts +2 -2
- package/dist/internals.js +6 -1
- package/dist/internals.mjs +2 -2
- package/package.json +1 -1
|
@@ -1,18 +1,37 @@
|
|
|
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
|
+
function readVersionVectorCounter(vv, actor) {
|
|
30
|
+
if (!Object.prototype.hasOwnProperty.call(vv, actor)) return;
|
|
12
31
|
const counter = vv[actor];
|
|
13
|
-
return typeof counter === "number" ? counter : 0;
|
|
32
|
+
return typeof counter === "number" ? counter : void 0;
|
|
14
33
|
}
|
|
15
|
-
function
|
|
34
|
+
function writeVersionVectorCounter(vv, actor, counter) {
|
|
16
35
|
Object.defineProperty(vv, actor, {
|
|
17
36
|
configurable: true,
|
|
18
37
|
enumerable: true,
|
|
@@ -20,6 +39,104 @@ function writeVvCounter$1(vv, actor, counter) {
|
|
|
20
39
|
writable: true
|
|
21
40
|
});
|
|
22
41
|
}
|
|
42
|
+
function observeVersionVectorDot(vv, dot) {
|
|
43
|
+
if ((readVersionVectorCounter(vv, dot.actor) ?? 0) < dot.ctr) writeVersionVectorCounter(vv, dot.actor, dot.ctr);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Inspect a document or state and return the highest observed counter per actor.
|
|
47
|
+
*
|
|
48
|
+
* When a `CrdtState` is provided, the returned vector is also seeded from the
|
|
49
|
+
* state's local clock so callers do not lose counters that have advanced ahead
|
|
50
|
+
* of the currently materialized document tree.
|
|
51
|
+
*/
|
|
52
|
+
function observedVersionVector(target) {
|
|
53
|
+
const doc = "doc" in target ? target.doc : target;
|
|
54
|
+
const vv = Object.create(null);
|
|
55
|
+
if ("clock" in target) observeVersionVectorDot(vv, {
|
|
56
|
+
actor: target.clock.actor,
|
|
57
|
+
ctr: target.clock.ctr
|
|
58
|
+
});
|
|
59
|
+
const stack = [{
|
|
60
|
+
node: doc.root,
|
|
61
|
+
depth: 0
|
|
62
|
+
}];
|
|
63
|
+
while (stack.length > 0) {
|
|
64
|
+
const frame = stack.pop();
|
|
65
|
+
assertTraversalDepth(frame.depth);
|
|
66
|
+
if (frame.node.kind === "lww") {
|
|
67
|
+
observeVersionVectorDot(vv, frame.node.dot);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (frame.node.kind === "obj") {
|
|
71
|
+
for (const entry of frame.node.entries.values()) {
|
|
72
|
+
observeVersionVectorDot(vv, entry.dot);
|
|
73
|
+
stack.push({
|
|
74
|
+
node: entry.node,
|
|
75
|
+
depth: frame.depth + 1
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
for (const tombstone of frame.node.tombstone.values()) observeVersionVectorDot(vv, tombstone);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
for (const elem of frame.node.elems.values()) {
|
|
82
|
+
observeVersionVectorDot(vv, elem.insDot);
|
|
83
|
+
if (elem.delDot) observeVersionVectorDot(vv, elem.delDot);
|
|
84
|
+
stack.push({
|
|
85
|
+
node: elem.value,
|
|
86
|
+
depth: frame.depth + 1
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return vv;
|
|
91
|
+
}
|
|
92
|
+
/** Combine version vectors using per-actor maxima. */
|
|
93
|
+
function mergeVersionVectors(...vectors) {
|
|
94
|
+
const merged = Object.create(null);
|
|
95
|
+
for (const vv of vectors) for (const actor of Object.keys(vv)) {
|
|
96
|
+
const counter = readVersionVectorCounter(vv, actor);
|
|
97
|
+
if (counter === void 0) continue;
|
|
98
|
+
writeVersionVectorCounter(merged, actor, Math.max(readVersionVectorCounter(merged, actor) ?? 0, counter));
|
|
99
|
+
}
|
|
100
|
+
return merged;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Derive a causally-stable checkpoint by taking the per-actor minimum.
|
|
104
|
+
*
|
|
105
|
+
* When called with a single vector the result equals that vector. In practice,
|
|
106
|
+
* a meaningful shared-stability checkpoint usually needs acknowledgements from
|
|
107
|
+
* at least two peers or from an explicit quorum.
|
|
108
|
+
*/
|
|
109
|
+
function intersectVersionVectors(...vectors) {
|
|
110
|
+
if (vectors.length === 0) return Object.create(null);
|
|
111
|
+
const actors = /* @__PURE__ */ new Set();
|
|
112
|
+
for (const vv of vectors) for (const actor of Object.keys(vv)) actors.add(actor);
|
|
113
|
+
const intersection = Object.create(null);
|
|
114
|
+
for (const actor of actors) {
|
|
115
|
+
const counters = vectors.map((vv) => readVersionVectorCounter(vv, actor) ?? 0);
|
|
116
|
+
const counter = Math.min(...counters);
|
|
117
|
+
if (counter > 0) writeVersionVectorCounter(intersection, actor, counter);
|
|
118
|
+
}
|
|
119
|
+
return intersection;
|
|
120
|
+
}
|
|
121
|
+
/** Check whether one version vector has observed every counter in another. */
|
|
122
|
+
function versionVectorCovers(observed, required) {
|
|
123
|
+
for (const actor of Object.keys(required)) {
|
|
124
|
+
const requiredCounter = readVersionVectorCounter(required, actor) ?? 0;
|
|
125
|
+
if ((readVersionVectorCounter(observed, actor) ?? 0) < requiredCounter) return false;
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region src/clock.ts
|
|
132
|
+
var ClockValidationError = class extends TypeError {
|
|
133
|
+
reason;
|
|
134
|
+
constructor(reason, message) {
|
|
135
|
+
super(message);
|
|
136
|
+
this.name = "ClockValidationError";
|
|
137
|
+
this.reason = reason;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
23
140
|
/**
|
|
24
141
|
* Create a new clock for the given actor. Each call to `clock.next()` yields a fresh `Dot`.
|
|
25
142
|
* @param actor - Unique identifier for this peer.
|
|
@@ -56,8 +173,8 @@ function cloneClock(clock) {
|
|
|
56
173
|
* Useful when a server needs to mint dots for many actors.
|
|
57
174
|
*/
|
|
58
175
|
function nextDotForActor(vv, actor) {
|
|
59
|
-
const ctr =
|
|
60
|
-
|
|
176
|
+
const ctr = (readVersionVectorCounter(vv, actor) ?? 0) + 1;
|
|
177
|
+
writeVersionVectorCounter(vv, actor, ctr);
|
|
61
178
|
return {
|
|
62
179
|
actor,
|
|
63
180
|
ctr
|
|
@@ -65,63 +182,20 @@ function nextDotForActor(vv, actor) {
|
|
|
65
182
|
}
|
|
66
183
|
/** Record an observed dot in a version vector. */
|
|
67
184
|
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
|
-
};
|
|
185
|
+
observeVersionVectorDot(vv, dot);
|
|
96
186
|
}
|
|
97
187
|
|
|
98
188
|
//#endregion
|
|
99
189
|
//#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
190
|
function compareDot(a, b) {
|
|
114
191
|
if (a.ctr !== b.ctr) return a.ctr - b.ctr;
|
|
115
192
|
return a.actor < b.actor ? -1 : a.actor > b.actor ? 1 : 0;
|
|
116
193
|
}
|
|
117
194
|
function vvHasDot(vv, d) {
|
|
118
|
-
return (
|
|
195
|
+
return (readVersionVectorCounter(vv, d.actor) ?? 0) >= d.ctr;
|
|
119
196
|
}
|
|
120
197
|
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;
|
|
198
|
+
return mergeVersionVectors(a, b);
|
|
125
199
|
}
|
|
126
200
|
function dotToElemId(d) {
|
|
127
201
|
return `${d.actor}:${d.ctr}`;
|
|
@@ -205,6 +279,12 @@ function rgaLinearizeIds(seq) {
|
|
|
205
279
|
});
|
|
206
280
|
return [...out];
|
|
207
281
|
}
|
|
282
|
+
function rgaLength(seq) {
|
|
283
|
+
const ver = getVersion(seq);
|
|
284
|
+
const cached = linearCache.get(seq);
|
|
285
|
+
if (cached && cached.version === ver) return cached.ids.length;
|
|
286
|
+
return rgaLinearizeIds(seq).length;
|
|
287
|
+
}
|
|
208
288
|
function rgaCreateIndexedIdSnapshot(seq) {
|
|
209
289
|
const ids = rgaLinearizeIds(seq);
|
|
210
290
|
return {
|
|
@@ -412,6 +492,7 @@ function rgaPrevForInsertAtIndex(seq, index) {
|
|
|
412
492
|
|
|
413
493
|
//#endregion
|
|
414
494
|
//#region src/materialize.ts
|
|
495
|
+
let materializeObserver = null;
|
|
415
496
|
function createMaterializedObject() {
|
|
416
497
|
return Object.create(null);
|
|
417
498
|
}
|
|
@@ -425,6 +506,8 @@ function setMaterializedProperty(out, key, value) {
|
|
|
425
506
|
}
|
|
426
507
|
/** Convert a CRDT node graph into a plain JSON value using an explicit stack. */
|
|
427
508
|
function materialize(node) {
|
|
509
|
+
const observer = materializeObserver;
|
|
510
|
+
observer?.([], node);
|
|
428
511
|
if (node.kind === "lww") return node.value;
|
|
429
512
|
const root = node.kind === "obj" ? createMaterializedObject() : [];
|
|
430
513
|
const stack = [];
|
|
@@ -432,13 +515,16 @@ function materialize(node) {
|
|
|
432
515
|
kind: "obj",
|
|
433
516
|
depth: 0,
|
|
434
517
|
entries: node.entries.entries(),
|
|
435
|
-
out: root
|
|
518
|
+
out: root,
|
|
519
|
+
path: []
|
|
436
520
|
});
|
|
437
521
|
else stack.push({
|
|
438
522
|
kind: "seq",
|
|
439
523
|
depth: 0,
|
|
440
524
|
cursor: rgaCreateLinearCursor(node),
|
|
441
|
-
out: root
|
|
525
|
+
out: root,
|
|
526
|
+
path: [],
|
|
527
|
+
nextIndex: 0
|
|
442
528
|
});
|
|
443
529
|
while (stack.length > 0) {
|
|
444
530
|
const frame = stack[stack.length - 1];
|
|
@@ -452,6 +538,8 @@ function materialize(node) {
|
|
|
452
538
|
const child = entry.node;
|
|
453
539
|
const childDepth = frame.depth + 1;
|
|
454
540
|
assertTraversalDepth(childDepth);
|
|
541
|
+
const childPath = [...frame.path, key];
|
|
542
|
+
observer?.(childPath, child);
|
|
455
543
|
if (child.kind === "lww") {
|
|
456
544
|
setMaterializedProperty(frame.out, key, child.value);
|
|
457
545
|
continue;
|
|
@@ -463,7 +551,8 @@ function materialize(node) {
|
|
|
463
551
|
kind: "obj",
|
|
464
552
|
depth: childDepth,
|
|
465
553
|
entries: child.entries.entries(),
|
|
466
|
-
out: outObj
|
|
554
|
+
out: outObj,
|
|
555
|
+
path: childPath
|
|
467
556
|
});
|
|
468
557
|
continue;
|
|
469
558
|
}
|
|
@@ -473,7 +562,9 @@ function materialize(node) {
|
|
|
473
562
|
kind: "seq",
|
|
474
563
|
depth: childDepth,
|
|
475
564
|
cursor: rgaCreateLinearCursor(child),
|
|
476
|
-
out: outArr
|
|
565
|
+
out: outArr,
|
|
566
|
+
path: childPath,
|
|
567
|
+
nextIndex: 0
|
|
477
568
|
});
|
|
478
569
|
continue;
|
|
479
570
|
}
|
|
@@ -485,6 +576,9 @@ function materialize(node) {
|
|
|
485
576
|
const child = elem.value;
|
|
486
577
|
const childDepth = frame.depth + 1;
|
|
487
578
|
assertTraversalDepth(childDepth);
|
|
579
|
+
const childPath = [...frame.path, String(frame.nextIndex)];
|
|
580
|
+
frame.nextIndex += 1;
|
|
581
|
+
observer?.(childPath, child);
|
|
488
582
|
if (child.kind === "lww") {
|
|
489
583
|
frame.out.push(child.value);
|
|
490
584
|
continue;
|
|
@@ -496,7 +590,8 @@ function materialize(node) {
|
|
|
496
590
|
kind: "obj",
|
|
497
591
|
depth: childDepth,
|
|
498
592
|
entries: child.entries.entries(),
|
|
499
|
-
out: outObj
|
|
593
|
+
out: outObj,
|
|
594
|
+
path: childPath
|
|
500
595
|
});
|
|
501
596
|
continue;
|
|
502
597
|
}
|
|
@@ -506,7 +601,9 @@ function materialize(node) {
|
|
|
506
601
|
kind: "seq",
|
|
507
602
|
depth: childDepth,
|
|
508
603
|
cursor: rgaCreateLinearCursor(child),
|
|
509
|
-
out: outArr
|
|
604
|
+
out: outArr,
|
|
605
|
+
path: childPath,
|
|
606
|
+
nextIndex: 0
|
|
510
607
|
});
|
|
511
608
|
}
|
|
512
609
|
return root;
|
|
@@ -617,6 +714,7 @@ function assertRuntimeJsonValue(value) {
|
|
|
617
714
|
/**
|
|
618
715
|
* Normalize a runtime value to JSON-compatible data.
|
|
619
716
|
* - non-finite numbers -> null
|
|
717
|
+
* - non-plain objects -> null at the root / in arrays, omitted from object properties
|
|
620
718
|
* - invalid object-property values -> key omitted
|
|
621
719
|
* - invalid root / array values -> null
|
|
622
720
|
*/
|
|
@@ -703,7 +801,10 @@ function isJsonPrimitive$1(value) {
|
|
|
703
801
|
return typeof value === "number" && Number.isFinite(value);
|
|
704
802
|
}
|
|
705
803
|
function isJsonObject(value) {
|
|
706
|
-
|
|
804
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
805
|
+
if (Object.prototype.toString.call(value) !== "[object Object]") return false;
|
|
806
|
+
const prototype = Object.getPrototypeOf(value);
|
|
807
|
+
return prototype === null || Object.getPrototypeOf(prototype) === null;
|
|
707
808
|
}
|
|
708
809
|
function isNonFiniteNumber(value) {
|
|
709
810
|
return typeof value === "number" && !Number.isFinite(value);
|
|
@@ -714,8 +815,16 @@ function describeInvalidValue(value) {
|
|
|
714
815
|
if (typeof value === "bigint") return "bigint is not valid JSON";
|
|
715
816
|
if (typeof value === "symbol") return "symbol is not valid JSON";
|
|
716
817
|
if (typeof value === "function") return "function is not valid JSON";
|
|
818
|
+
if (typeof value === "object" && value !== null) return `non-plain object (${describeObjectKind(value)}) is not valid JSON`;
|
|
717
819
|
return `unsupported value type (${typeof value})`;
|
|
718
820
|
}
|
|
821
|
+
function describeObjectKind(value) {
|
|
822
|
+
const tag = Object.prototype.toString.call(value).slice(8, -1);
|
|
823
|
+
if (tag !== "Object") return tag;
|
|
824
|
+
const constructor = value.constructor;
|
|
825
|
+
if (typeof constructor === "function" && constructor.name !== "" && constructor.name !== "Object") return constructor.name;
|
|
826
|
+
return "Object";
|
|
827
|
+
}
|
|
719
828
|
|
|
720
829
|
//#endregion
|
|
721
830
|
//#region src/types.ts
|
|
@@ -846,8 +955,8 @@ function compileJsonPatchOpToIntent(baseJson, op, options = {}) {
|
|
|
846
955
|
* By default arrays use a deterministic LCS strategy.
|
|
847
956
|
* Pass `{ arrayStrategy: "atomic" }` for single-op array replacement.
|
|
848
957
|
* Pass `{ arrayStrategy: "lcs-linear" }` for a lower-memory LCS variant.
|
|
849
|
-
*
|
|
850
|
-
*
|
|
958
|
+
* Use `lcsLinearMaxCells` to optionally cap worst-case `lcs-linear` work and
|
|
959
|
+
* fall back to an atomic array replacement for very large unmatched windows.
|
|
851
960
|
* Pass `{ emitMoves: true }` or `{ emitCopies: true }` to opt into RFC 6902
|
|
852
961
|
* move/copy emission when a deterministic rewrite is available.
|
|
853
962
|
* @param base - The original JSON value.
|
|
@@ -864,39 +973,97 @@ function diffJsonPatch(base, next, options = {}) {
|
|
|
864
973
|
return ops;
|
|
865
974
|
}
|
|
866
975
|
function diffValue(path, base, next, ops, options) {
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
976
|
+
const stack = [{
|
|
977
|
+
kind: "value",
|
|
978
|
+
base,
|
|
979
|
+
next
|
|
980
|
+
}];
|
|
981
|
+
while (stack.length > 0) {
|
|
982
|
+
const frame = stack.pop();
|
|
983
|
+
if (frame.kind === "path-pop") {
|
|
984
|
+
path.pop();
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
if (frame.kind === "object") {
|
|
988
|
+
if (frame.index >= frame.sharedKeys.length) continue;
|
|
989
|
+
const key = frame.sharedKeys[frame.index];
|
|
990
|
+
stack.push({
|
|
991
|
+
kind: "object",
|
|
992
|
+
base: frame.base,
|
|
993
|
+
next: frame.next,
|
|
994
|
+
sharedKeys: frame.sharedKeys,
|
|
995
|
+
index: frame.index + 1
|
|
996
|
+
});
|
|
997
|
+
path.push(key);
|
|
998
|
+
stack.push({ kind: "path-pop" });
|
|
999
|
+
stack.push({
|
|
1000
|
+
kind: "value",
|
|
1001
|
+
base: frame.base[key],
|
|
1002
|
+
next: frame.next[key]
|
|
1003
|
+
});
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
assertTraversalDepth(path.length);
|
|
1007
|
+
if (frame.base === frame.next) continue;
|
|
1008
|
+
const baseIsArray = Array.isArray(frame.base);
|
|
1009
|
+
const nextIsArray = Array.isArray(frame.next);
|
|
1010
|
+
if (baseIsArray || nextIsArray) {
|
|
1011
|
+
if (!baseIsArray || !nextIsArray) {
|
|
1012
|
+
ops.push({
|
|
1013
|
+
op: "replace",
|
|
1014
|
+
path: stringifyJsonPointer(path),
|
|
1015
|
+
value: frame.next
|
|
1016
|
+
});
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
if (jsonEquals(frame.base, frame.next)) continue;
|
|
1020
|
+
const arrayStrategy = options.arrayStrategy ?? "lcs";
|
|
1021
|
+
if (arrayStrategy === "lcs") {
|
|
1022
|
+
if (!diffArrayWithLcsMatrix(path, frame.base, frame.next, ops, options)) ops.push({
|
|
1023
|
+
op: "replace",
|
|
1024
|
+
path: stringifyJsonPointer(path),
|
|
1025
|
+
value: frame.next
|
|
1026
|
+
});
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
if (arrayStrategy === "lcs-linear") {
|
|
1030
|
+
if (!diffArrayWithLinearLcs(path, frame.base, frame.next, ops, options)) ops.push({
|
|
1031
|
+
op: "replace",
|
|
1032
|
+
path: stringifyJsonPointer(path),
|
|
1033
|
+
value: frame.next
|
|
1034
|
+
});
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
ops.push({
|
|
872
1038
|
op: "replace",
|
|
873
1039
|
path: stringifyJsonPointer(path),
|
|
874
|
-
value: next
|
|
1040
|
+
value: frame.next
|
|
875
1041
|
});
|
|
876
|
-
|
|
1042
|
+
continue;
|
|
877
1043
|
}
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
1044
|
+
const baseIsObject = isPlainObject(frame.base);
|
|
1045
|
+
const nextIsObject = isPlainObject(frame.next);
|
|
1046
|
+
if (!baseIsObject || !nextIsObject) {
|
|
1047
|
+
ops.push({
|
|
1048
|
+
op: "replace",
|
|
1049
|
+
path: stringifyJsonPointer(path),
|
|
1050
|
+
value: frame.next
|
|
1051
|
+
});
|
|
1052
|
+
continue;
|
|
881
1053
|
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1054
|
+
const { sharedKeys, baseOnlyKeys, nextOnlyKeys } = collectObjectKeys(frame.base, frame.next);
|
|
1055
|
+
if (!(baseOnlyKeys.length > 0 || nextOnlyKeys.length > 0) && (path.length === 0 || sharedKeys.length > 1) && jsonEquals(frame.base, frame.next)) continue;
|
|
1056
|
+
emitObjectStructuralOps(path, frame.base, frame.next, sharedKeys, baseOnlyKeys, nextOnlyKeys, ops, options);
|
|
1057
|
+
if (sharedKeys.length > 0) stack.push({
|
|
1058
|
+
kind: "object",
|
|
1059
|
+
base: frame.base,
|
|
1060
|
+
next: frame.next,
|
|
1061
|
+
sharedKeys,
|
|
1062
|
+
index: 0
|
|
886
1063
|
});
|
|
887
|
-
return;
|
|
888
1064
|
}
|
|
889
|
-
if (!isPlainObject(base) || !isPlainObject(next)) {
|
|
890
|
-
ops.push({
|
|
891
|
-
op: "replace",
|
|
892
|
-
path: stringifyJsonPointer(path),
|
|
893
|
-
value: next
|
|
894
|
-
});
|
|
895
|
-
return;
|
|
896
|
-
}
|
|
897
|
-
diffObject(path, base, next, ops, options);
|
|
898
1065
|
}
|
|
899
|
-
function
|
|
1066
|
+
function collectObjectKeys(base, next) {
|
|
900
1067
|
const baseKeys = Object.keys(base).sort();
|
|
901
1068
|
const nextKeys = Object.keys(next).sort();
|
|
902
1069
|
const baseOnlyKeys = [];
|
|
@@ -929,12 +1096,11 @@ function diffObject(path, base, next, ops, options) {
|
|
|
929
1096
|
nextOnlyKeys.push(nextKeys[nextIndex]);
|
|
930
1097
|
nextIndex += 1;
|
|
931
1098
|
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
}
|
|
1099
|
+
return {
|
|
1100
|
+
sharedKeys,
|
|
1101
|
+
baseOnlyKeys,
|
|
1102
|
+
nextOnlyKeys
|
|
1103
|
+
};
|
|
938
1104
|
}
|
|
939
1105
|
function emitObjectStructuralOps(path, base, next, sharedKeys, baseOnlyKeys, nextOnlyKeys, ops, options) {
|
|
940
1106
|
if (!options.emitMoves && !options.emitCopies) {
|
|
@@ -957,18 +1123,14 @@ function emitObjectStructuralOps(path, base, next, sharedKeys, baseOnlyKeys, nex
|
|
|
957
1123
|
}
|
|
958
1124
|
return;
|
|
959
1125
|
}
|
|
1126
|
+
const structuralKeyCache = /* @__PURE__ */ new WeakMap();
|
|
960
1127
|
const matchedMoveSources = /* @__PURE__ */ new Set();
|
|
961
1128
|
const moveTargets = /* @__PURE__ */ new Map();
|
|
962
1129
|
if (options.emitMoves) {
|
|
963
1130
|
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
|
-
}
|
|
1131
|
+
for (const baseKey of baseOnlyKeys) insertObjectSourceBucket(moveSourceBuckets, baseKey, base[baseKey], structuralKeyCache);
|
|
970
1132
|
for (const nextKey of nextOnlyKeys) {
|
|
971
|
-
const bucket = moveSourceBuckets.get(stableJsonValueKey(next[nextKey]));
|
|
1133
|
+
const bucket = moveSourceBuckets.get(stableJsonValueKey(next[nextKey], structuralKeyCache));
|
|
972
1134
|
if (!bucket) continue;
|
|
973
1135
|
if (bucket.length > 0) {
|
|
974
1136
|
const candidate = bucket.shift();
|
|
@@ -977,12 +1139,10 @@ function emitObjectStructuralOps(path, base, next, sharedKeys, baseOnlyKeys, nex
|
|
|
977
1139
|
}
|
|
978
1140
|
}
|
|
979
1141
|
}
|
|
980
|
-
const
|
|
981
|
-
const availableSourceKeys = [];
|
|
1142
|
+
const copySourceBuckets = /* @__PURE__ */ new Map();
|
|
982
1143
|
for (const key of sharedKeys) {
|
|
983
1144
|
if (!jsonEquals(base[key], next[key])) continue;
|
|
984
|
-
|
|
985
|
-
availableSourceKeys.push(key);
|
|
1145
|
+
insertObjectSourceBucket(copySourceBuckets, key, base[key], structuralKeyCache);
|
|
986
1146
|
}
|
|
987
1147
|
for (const nextKey of nextOnlyKeys) {
|
|
988
1148
|
path.push(nextKey);
|
|
@@ -998,12 +1158,11 @@ function emitObjectStructuralOps(path, base, next, sharedKeys, baseOnlyKeys, nex
|
|
|
998
1158
|
from: fromPath,
|
|
999
1159
|
path: targetPath
|
|
1000
1160
|
});
|
|
1001
|
-
|
|
1002
|
-
insertSortedKey(availableSourceKeys, nextKey);
|
|
1161
|
+
insertObjectSourceBucket(copySourceBuckets, nextKey, next[nextKey], structuralKeyCache);
|
|
1003
1162
|
continue;
|
|
1004
1163
|
}
|
|
1005
1164
|
if (options.emitCopies) {
|
|
1006
|
-
const copySource = findObjectCopySource(
|
|
1165
|
+
const copySource = findObjectCopySource(copySourceBuckets, next[nextKey], structuralKeyCache);
|
|
1007
1166
|
if (copySource !== void 0) {
|
|
1008
1167
|
path.push(copySource);
|
|
1009
1168
|
const fromPath = stringifyJsonPointer(path);
|
|
@@ -1013,8 +1172,7 @@ function emitObjectStructuralOps(path, base, next, sharedKeys, baseOnlyKeys, nex
|
|
|
1013
1172
|
from: fromPath,
|
|
1014
1173
|
path: targetPath
|
|
1015
1174
|
});
|
|
1016
|
-
|
|
1017
|
-
insertSortedKey(availableSourceKeys, nextKey);
|
|
1175
|
+
insertObjectSourceBucket(copySourceBuckets, nextKey, next[nextKey], structuralKeyCache);
|
|
1018
1176
|
continue;
|
|
1019
1177
|
}
|
|
1020
1178
|
}
|
|
@@ -1023,8 +1181,7 @@ function emitObjectStructuralOps(path, base, next, sharedKeys, baseOnlyKeys, nex
|
|
|
1023
1181
|
path: targetPath,
|
|
1024
1182
|
value: next[nextKey]
|
|
1025
1183
|
});
|
|
1026
|
-
|
|
1027
|
-
insertSortedKey(availableSourceKeys, nextKey);
|
|
1184
|
+
insertObjectSourceBucket(copySourceBuckets, nextKey, next[nextKey], structuralKeyCache);
|
|
1028
1185
|
}
|
|
1029
1186
|
for (const baseKey of baseOnlyKeys) {
|
|
1030
1187
|
if (matchedMoveSources.has(baseKey)) continue;
|
|
@@ -1036,8 +1193,17 @@ function emitObjectStructuralOps(path, base, next, sharedKeys, baseOnlyKeys, nex
|
|
|
1036
1193
|
path.pop();
|
|
1037
1194
|
}
|
|
1038
1195
|
}
|
|
1039
|
-
function
|
|
1040
|
-
|
|
1196
|
+
function insertObjectSourceBucket(buckets, key, value, structuralKeyCache) {
|
|
1197
|
+
const bucketKey = stableJsonValueKey(value, structuralKeyCache);
|
|
1198
|
+
let bucket = buckets.get(bucketKey);
|
|
1199
|
+
if (!bucket) {
|
|
1200
|
+
bucket = [];
|
|
1201
|
+
buckets.set(bucketKey, bucket);
|
|
1202
|
+
}
|
|
1203
|
+
insertSortedKey(bucket, key);
|
|
1204
|
+
}
|
|
1205
|
+
function findObjectCopySource(copySourceBuckets, target, structuralKeyCache) {
|
|
1206
|
+
return copySourceBuckets.get(stableJsonValueKey(target, structuralKeyCache))?.[0];
|
|
1041
1207
|
}
|
|
1042
1208
|
function insertSortedKey(keys, key) {
|
|
1043
1209
|
let low = 0;
|
|
@@ -1064,9 +1230,11 @@ function diffArrayWithLcsMatrix(path, base, next, ops, options) {
|
|
|
1064
1230
|
}
|
|
1065
1231
|
function diffArrayWithLinearLcs(path, base, next, ops, options) {
|
|
1066
1232
|
const window = trimEqualArrayEdges(base, next);
|
|
1233
|
+
if (!shouldUseLinearLcsDiff(window.unmatchedBaseLength, window.unmatchedNextLength, options)) return false;
|
|
1067
1234
|
const steps = [];
|
|
1068
1235
|
buildArrayEditScriptLinearSpace(base, window.baseStart, window.baseStart + window.unmatchedBaseLength, next, window.nextStart, window.nextStart + window.unmatchedNextLength, steps);
|
|
1069
1236
|
pushArrayPatchOps(path, window.prefixLength, steps, ops, base, options);
|
|
1237
|
+
return true;
|
|
1070
1238
|
}
|
|
1071
1239
|
function trimEqualArrayEdges(base, next) {
|
|
1072
1240
|
const baseLength = base.length;
|
|
@@ -1263,17 +1431,22 @@ function shouldUseLcsDiff(baseLength, nextLength, lcsMaxCells) {
|
|
|
1263
1431
|
if (!Number.isFinite(cap) || cap < 1) return false;
|
|
1264
1432
|
return (baseLength + 1) * (nextLength + 1) <= cap;
|
|
1265
1433
|
}
|
|
1434
|
+
function shouldUseLinearLcsDiff(baseLength, nextLength, options) {
|
|
1435
|
+
const cap = options.lcsLinearMaxCells;
|
|
1436
|
+
if (cap === void 0 || cap === Number.POSITIVE_INFINITY) return true;
|
|
1437
|
+
if (!Number.isFinite(cap) || cap < 1) return false;
|
|
1438
|
+
return (baseLength + 1) * (nextLength + 1) <= cap;
|
|
1439
|
+
}
|
|
1266
1440
|
function finalizeArrayOps(arrayPath, base, ops, options) {
|
|
1267
1441
|
if (ops.length === 0) return [];
|
|
1268
1442
|
if (!options.emitMoves && !options.emitCopies) return compactArrayOps(ops);
|
|
1269
1443
|
const out = [];
|
|
1270
|
-
const working = base
|
|
1444
|
+
const working = createArrayRewriteState(base);
|
|
1271
1445
|
for (let i = 0; i < ops.length; i++) {
|
|
1272
1446
|
const op = ops[i];
|
|
1273
1447
|
const next = ops[i + 1];
|
|
1274
1448
|
if (op.op === "remove" && next && next.op === "add") {
|
|
1275
|
-
const
|
|
1276
|
-
const valuesMatch = jsonEquals(removedValue, next.value);
|
|
1449
|
+
const valuesMatch = working.entries[getArrayOpIndex(op.path, arrayPath)].key === getArrayRewriteValueKey(working, next.value);
|
|
1277
1450
|
if (op.path === next.path) {
|
|
1278
1451
|
const replaceOp = {
|
|
1279
1452
|
op: "replace",
|
|
@@ -1312,7 +1485,7 @@ function finalizeArrayOps(arrayPath, base, ops, options) {
|
|
|
1312
1485
|
const targetIndex = getArrayOpIndex(op.path, arrayPath);
|
|
1313
1486
|
const removeIndex = getArrayOpIndex(next.path, arrayPath);
|
|
1314
1487
|
const sourceIndex = removeIndex - (targetIndex <= removeIndex ? 1 : 0);
|
|
1315
|
-
const matchesPendingRemove = sourceIndex >= 0 && sourceIndex < working.length &&
|
|
1488
|
+
const matchesPendingRemove = sourceIndex >= 0 && sourceIndex < working.entries.length && working.entries[sourceIndex].key === getArrayRewriteValueKey(working, op.value);
|
|
1316
1489
|
if (options.emitMoves && matchesPendingRemove) {
|
|
1317
1490
|
const moveOp = {
|
|
1318
1491
|
op: "move",
|
|
@@ -1351,10 +1524,75 @@ function finalizeArrayOps(arrayPath, base, ops, options) {
|
|
|
1351
1524
|
}
|
|
1352
1525
|
return out;
|
|
1353
1526
|
}
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
if (
|
|
1357
|
-
|
|
1527
|
+
/** @internal Stable structural fingerprint used for deterministic diff rewrites. */
|
|
1528
|
+
function stableJsonValueKey(value, structuralKeyCache) {
|
|
1529
|
+
if (value !== null && typeof value === "object") {
|
|
1530
|
+
const cachedValue = structuralKeyCache?.get(value);
|
|
1531
|
+
if (cachedValue !== void 0) return cachedValue;
|
|
1532
|
+
}
|
|
1533
|
+
const stack = [{
|
|
1534
|
+
kind: "value",
|
|
1535
|
+
value,
|
|
1536
|
+
depth: 0
|
|
1537
|
+
}];
|
|
1538
|
+
const results = [];
|
|
1539
|
+
while (stack.length > 0) {
|
|
1540
|
+
const frame = stack.pop();
|
|
1541
|
+
if (frame.kind === "array") {
|
|
1542
|
+
const stableKey = `[${results.splice(frame.startIndex).join(",")}]`;
|
|
1543
|
+
structuralKeyCache?.set(frame.value, stableKey);
|
|
1544
|
+
results.push(stableKey);
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
if (frame.kind === "object") {
|
|
1548
|
+
const childParts = results.splice(frame.startIndex);
|
|
1549
|
+
const stableKey = `{${frame.keys.map((key, index) => `${JSON.stringify(key)}:${childParts[index]}`).join(",")}}`;
|
|
1550
|
+
structuralKeyCache?.set(frame.value, stableKey);
|
|
1551
|
+
results.push(stableKey);
|
|
1552
|
+
continue;
|
|
1553
|
+
}
|
|
1554
|
+
assertTraversalDepth(frame.depth);
|
|
1555
|
+
if (frame.value === null || typeof frame.value !== "object") {
|
|
1556
|
+
results.push(JSON.stringify(frame.value));
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
const cachedValue = structuralKeyCache?.get(frame.value);
|
|
1560
|
+
if (cachedValue !== void 0) {
|
|
1561
|
+
results.push(cachedValue);
|
|
1562
|
+
continue;
|
|
1563
|
+
}
|
|
1564
|
+
if (Array.isArray(frame.value)) {
|
|
1565
|
+
const startIndex = results.length;
|
|
1566
|
+
stack.push({
|
|
1567
|
+
kind: "array",
|
|
1568
|
+
value: frame.value,
|
|
1569
|
+
startIndex
|
|
1570
|
+
});
|
|
1571
|
+
for (let index = frame.value.length - 1; index >= 0; index--) stack.push({
|
|
1572
|
+
kind: "value",
|
|
1573
|
+
value: frame.value[index],
|
|
1574
|
+
depth: frame.depth + 1
|
|
1575
|
+
});
|
|
1576
|
+
continue;
|
|
1577
|
+
}
|
|
1578
|
+
const keys = Object.keys(frame.value).sort();
|
|
1579
|
+
const startIndex = results.length;
|
|
1580
|
+
stack.push({
|
|
1581
|
+
kind: "object",
|
|
1582
|
+
value: frame.value,
|
|
1583
|
+
keys,
|
|
1584
|
+
startIndex
|
|
1585
|
+
});
|
|
1586
|
+
for (let index = keys.length - 1; index >= 0; index--) {
|
|
1587
|
+
const key = keys[index];
|
|
1588
|
+
stack.push({
|
|
1589
|
+
kind: "value",
|
|
1590
|
+
value: frame.value[key],
|
|
1591
|
+
depth: frame.depth + 1
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
return results[0];
|
|
1358
1596
|
}
|
|
1359
1597
|
function compactArrayOps(ops) {
|
|
1360
1598
|
const out = [];
|
|
@@ -1374,9 +1612,65 @@ function compactArrayOps(ops) {
|
|
|
1374
1612
|
}
|
|
1375
1613
|
return out;
|
|
1376
1614
|
}
|
|
1377
|
-
function
|
|
1378
|
-
|
|
1379
|
-
|
|
1615
|
+
function createArrayRewriteState(base) {
|
|
1616
|
+
const structuralKeyCache = /* @__PURE__ */ new WeakMap();
|
|
1617
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
1618
|
+
return {
|
|
1619
|
+
entries: base.map((value, currentIndex) => {
|
|
1620
|
+
const entry = {
|
|
1621
|
+
value,
|
|
1622
|
+
key: stableJsonValueKey(value, structuralKeyCache),
|
|
1623
|
+
currentIndex,
|
|
1624
|
+
bucketIndex: -1
|
|
1625
|
+
};
|
|
1626
|
+
insertArrayRewriteBucketEntry(buckets, entry);
|
|
1627
|
+
return entry;
|
|
1628
|
+
}),
|
|
1629
|
+
buckets,
|
|
1630
|
+
structuralKeyCache
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
function getArrayRewriteValueKey(state, value) {
|
|
1634
|
+
return stableJsonValueKey(value, state.structuralKeyCache);
|
|
1635
|
+
}
|
|
1636
|
+
function findArrayCopySourceIndex(state, value) {
|
|
1637
|
+
return state.buckets.get(getArrayRewriteValueKey(state, value))?.[0]?.currentIndex ?? -1;
|
|
1638
|
+
}
|
|
1639
|
+
function insertArrayRewriteBucketEntry(buckets, entry) {
|
|
1640
|
+
let bucket = buckets.get(entry.key);
|
|
1641
|
+
if (!bucket) {
|
|
1642
|
+
bucket = [];
|
|
1643
|
+
buckets.set(entry.key, bucket);
|
|
1644
|
+
}
|
|
1645
|
+
let low = 0;
|
|
1646
|
+
let high = bucket.length;
|
|
1647
|
+
while (low < high) {
|
|
1648
|
+
const mid = Math.floor((low + high) / 2);
|
|
1649
|
+
if (bucket[mid].currentIndex < entry.currentIndex) low = mid + 1;
|
|
1650
|
+
else high = mid;
|
|
1651
|
+
}
|
|
1652
|
+
bucket.splice(low, 0, entry);
|
|
1653
|
+
reindexArrayRewriteBucketPositions(bucket, low);
|
|
1654
|
+
}
|
|
1655
|
+
function removeArrayRewriteBucketEntry(buckets, entry) {
|
|
1656
|
+
const bucket = buckets.get(entry.key);
|
|
1657
|
+
if (!bucket) return;
|
|
1658
|
+
const bucketIndex = entry.bucketIndex;
|
|
1659
|
+
if (bucketIndex < 0 || bucketIndex >= bucket.length || bucket[bucketIndex] !== entry) return;
|
|
1660
|
+
bucket.splice(bucketIndex, 1);
|
|
1661
|
+
if (bucket.length === 0) {
|
|
1662
|
+
buckets.delete(entry.key);
|
|
1663
|
+
entry.bucketIndex = -1;
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
entry.bucketIndex = -1;
|
|
1667
|
+
reindexArrayRewriteBucketPositions(bucket, bucketIndex);
|
|
1668
|
+
}
|
|
1669
|
+
function reindexArrayRewriteBucketPositions(bucket, startIndex) {
|
|
1670
|
+
for (let index = startIndex; index < bucket.length; index++) bucket[index].bucketIndex = index;
|
|
1671
|
+
}
|
|
1672
|
+
function reindexArrayRewriteEntries(entries, startIndex) {
|
|
1673
|
+
for (let index = startIndex; index < entries.length; index++) entries[index].currentIndex = index;
|
|
1380
1674
|
}
|
|
1381
1675
|
function getArrayOpIndex(ptr, arrayPath) {
|
|
1382
1676
|
const parsed = parseJsonPointer(ptr);
|
|
@@ -1388,29 +1682,60 @@ function getArrayOpIndex(ptr, arrayPath) {
|
|
|
1388
1682
|
}
|
|
1389
1683
|
function applyArrayOptimizationOp(working, op, arrayPath) {
|
|
1390
1684
|
if (op.op === "add") {
|
|
1391
|
-
|
|
1685
|
+
const index = getArrayOpIndex(op.path, arrayPath);
|
|
1686
|
+
const entry = {
|
|
1687
|
+
value: structuredClone(op.value),
|
|
1688
|
+
key: getArrayRewriteValueKey(working, op.value),
|
|
1689
|
+
currentIndex: index,
|
|
1690
|
+
bucketIndex: -1
|
|
1691
|
+
};
|
|
1692
|
+
working.entries.splice(index, 0, entry);
|
|
1693
|
+
reindexArrayRewriteEntries(working.entries, index + 1);
|
|
1694
|
+
insertArrayRewriteBucketEntry(working.buckets, entry);
|
|
1392
1695
|
return;
|
|
1393
1696
|
}
|
|
1394
1697
|
if (op.op === "remove") {
|
|
1395
|
-
|
|
1698
|
+
const index = getArrayOpIndex(op.path, arrayPath);
|
|
1699
|
+
const [removedEntry] = working.entries.splice(index, 1);
|
|
1700
|
+
if (removedEntry) removeArrayRewriteBucketEntry(working.buckets, removedEntry);
|
|
1701
|
+
reindexArrayRewriteEntries(working.entries, index);
|
|
1396
1702
|
return;
|
|
1397
1703
|
}
|
|
1398
1704
|
if (op.op === "replace") {
|
|
1399
|
-
|
|
1705
|
+
const index = getArrayOpIndex(op.path, arrayPath);
|
|
1706
|
+
const entry = working.entries[index];
|
|
1707
|
+
removeArrayRewriteBucketEntry(working.buckets, entry);
|
|
1708
|
+
entry.value = structuredClone(op.value);
|
|
1709
|
+
entry.key = getArrayRewriteValueKey(working, op.value);
|
|
1710
|
+
insertArrayRewriteBucketEntry(working.buckets, entry);
|
|
1400
1711
|
return;
|
|
1401
1712
|
}
|
|
1402
1713
|
if (op.op === "copy") {
|
|
1403
1714
|
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
|
-
|
|
1715
|
+
if (fromIndex < 0 || fromIndex >= working.entries.length) throw new Error(`applyArrayOptimizationOp: copy from index ${fromIndex} is out of bounds (length ${working.entries.length})`);
|
|
1716
|
+
const index = getArrayOpIndex(op.path, arrayPath);
|
|
1717
|
+
const source = working.entries[fromIndex];
|
|
1718
|
+
const entry = {
|
|
1719
|
+
value: structuredClone(source.value),
|
|
1720
|
+
key: source.key,
|
|
1721
|
+
currentIndex: index,
|
|
1722
|
+
bucketIndex: -1
|
|
1723
|
+
};
|
|
1724
|
+
working.entries.splice(index, 0, entry);
|
|
1725
|
+
reindexArrayRewriteEntries(working.entries, index + 1);
|
|
1726
|
+
insertArrayRewriteBucketEntry(working.buckets, entry);
|
|
1407
1727
|
return;
|
|
1408
1728
|
}
|
|
1409
1729
|
if (op.op === "move") {
|
|
1410
1730
|
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
|
-
|
|
1731
|
+
if (fromIndex < 0 || fromIndex >= working.entries.length) throw new Error(`applyArrayOptimizationOp: move from index ${fromIndex} is out of bounds (length ${working.entries.length})`);
|
|
1732
|
+
const [entry] = working.entries.splice(fromIndex, 1);
|
|
1733
|
+
if (!entry) throw new Error(`applyArrayOptimizationOp: move from index ${fromIndex} did not resolve`);
|
|
1734
|
+
removeArrayRewriteBucketEntry(working.buckets, entry);
|
|
1735
|
+
const index = getArrayOpIndex(op.path, arrayPath);
|
|
1736
|
+
working.entries.splice(index, 0, entry);
|
|
1737
|
+
reindexArrayRewriteEntries(working.entries, Math.min(fromIndex, index));
|
|
1738
|
+
insertArrayRewriteBucketEntry(working.buckets, entry);
|
|
1414
1739
|
return;
|
|
1415
1740
|
}
|
|
1416
1741
|
throw new Error(`applyArrayOptimizationOp: unexpected op type "${op.op}"`);
|
|
@@ -1420,21 +1745,39 @@ function escapeJsonPointer(token) {
|
|
|
1420
1745
|
}
|
|
1421
1746
|
/** Deep equality check for JSON values (null-safe, handles arrays and objects). */
|
|
1422
1747
|
function jsonEquals(a, b) {
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1748
|
+
const stack = [{
|
|
1749
|
+
left: a,
|
|
1750
|
+
right: b,
|
|
1751
|
+
depth: 0
|
|
1752
|
+
}];
|
|
1753
|
+
while (stack.length > 0) {
|
|
1754
|
+
const frame = stack.pop();
|
|
1755
|
+
assertTraversalDepth(frame.depth);
|
|
1756
|
+
if (frame.left === frame.right) continue;
|
|
1757
|
+
if (frame.left === null || frame.right === null) return false;
|
|
1758
|
+
if (Array.isArray(frame.left) || Array.isArray(frame.right)) {
|
|
1759
|
+
if (!Array.isArray(frame.left) || !Array.isArray(frame.right)) return false;
|
|
1760
|
+
if (frame.left.length !== frame.right.length) return false;
|
|
1761
|
+
for (let index = frame.left.length - 1; index >= 0; index--) stack.push({
|
|
1762
|
+
left: frame.left[index],
|
|
1763
|
+
right: frame.right[index],
|
|
1764
|
+
depth: frame.depth + 1
|
|
1765
|
+
});
|
|
1766
|
+
continue;
|
|
1767
|
+
}
|
|
1768
|
+
if (!isPlainObject(frame.left) || !isPlainObject(frame.right)) return false;
|
|
1769
|
+
const leftKeys = Object.keys(frame.left);
|
|
1770
|
+
const rightKeys = Object.keys(frame.right);
|
|
1771
|
+
if (leftKeys.length !== rightKeys.length) return false;
|
|
1772
|
+
for (let index = leftKeys.length - 1; index >= 0; index--) {
|
|
1773
|
+
const key = leftKeys[index];
|
|
1774
|
+
if (!hasOwn(frame.right, key)) return false;
|
|
1775
|
+
stack.push({
|
|
1776
|
+
left: frame.left[key],
|
|
1777
|
+
right: frame.right[key],
|
|
1778
|
+
depth: frame.depth + 1
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1438
1781
|
}
|
|
1439
1782
|
return true;
|
|
1440
1783
|
}
|
|
@@ -2810,7 +3153,7 @@ function applyPatchAsActor(doc, vv, actor, patch, options = {}) {
|
|
|
2810
3153
|
}
|
|
2811
3154
|
/** Non-throwing `applyPatchAsActor` variant for internals sync flows. */
|
|
2812
3155
|
function tryApplyPatchAsActor(doc, vv, actor, patch, options = {}) {
|
|
2813
|
-
const observedCtr =
|
|
3156
|
+
const observedCtr = observedVersionVector(doc)[actor] ?? 0;
|
|
2814
3157
|
const applied = tryApplyPatch({
|
|
2815
3158
|
doc,
|
|
2816
3159
|
clock: createClock(actor, Math.max(vv[actor] ?? 0, observedCtr))
|
|
@@ -2852,18 +3195,10 @@ function applyPatchInternal(state, patch, options, execution) {
|
|
|
2852
3195
|
doc: cloneDoc(options.base.doc),
|
|
2853
3196
|
clock: createClock("__base__", 0)
|
|
2854
3197
|
} : 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;
|
|
3198
|
+
const session = { pointerCache: /* @__PURE__ */ new Map() };
|
|
2862
3199
|
for (const [opIndex, op] of runtimePatch.entries()) {
|
|
2863
|
-
const step = applyPatchOpSequential(state, op, options, explicitBaseState ? explicitBaseState.doc : state.doc,
|
|
3200
|
+
const step = applyPatchOpSequential(state, op, options, explicitBaseState ? explicitBaseState.doc : state.doc, explicitBaseState, opIndex, session);
|
|
2864
3201
|
if (!step.ok) return step;
|
|
2865
|
-
sequentialBaseJson = step.baseJson;
|
|
2866
|
-
sequentialHeadJson = step.headJson;
|
|
2867
3202
|
}
|
|
2868
3203
|
return { ok: true };
|
|
2869
3204
|
}
|
|
@@ -2872,12 +3207,12 @@ function applyPatchInternal(state, patch, options, execution) {
|
|
|
2872
3207
|
if (!compiled.ok) return compiled;
|
|
2873
3208
|
return applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
|
|
2874
3209
|
}
|
|
2875
|
-
function applyPatchOpSequential(state, op, options, baseDoc,
|
|
3210
|
+
function applyPatchOpSequential(state, op, options, baseDoc, explicitBaseState, opIndex, session) {
|
|
2876
3211
|
if (op.op === "move") {
|
|
2877
|
-
const fromResolved =
|
|
3212
|
+
const fromResolved = resolveValueAtPointerInDoc(baseDoc, op.from, opIndex, session.pointerCache);
|
|
2878
3213
|
if (!fromResolved.ok) return fromResolved;
|
|
2879
3214
|
const fromValue = structuredClone(fromResolved.value);
|
|
2880
|
-
const removeRes = applySinglePatchOpSequentialStep(state, baseDoc,
|
|
3215
|
+
const removeRes = applySinglePatchOpSequentialStep(state, baseDoc, {
|
|
2881
3216
|
op: "remove",
|
|
2882
3217
|
path: op.from
|
|
2883
3218
|
}, options, explicitBaseState, opIndex, session);
|
|
@@ -2887,30 +3222,26 @@ function applyPatchOpSequential(state, op, options, baseDoc, baseJson, headJson,
|
|
|
2887
3222
|
path: op.path,
|
|
2888
3223
|
value: fromValue
|
|
2889
3224
|
};
|
|
2890
|
-
if (!explicitBaseState) return applySinglePatchOpSequentialStep(state, state.doc,
|
|
2891
|
-
const headAddRes = applySinglePatchOpSequentialStep(state, state.doc,
|
|
3225
|
+
if (!explicitBaseState) return applySinglePatchOpSequentialStep(state, state.doc, addOp, options, null, opIndex, session);
|
|
3226
|
+
const headAddRes = applySinglePatchOpSequentialStep(state, state.doc, addOp, options, null, opIndex, session);
|
|
2892
3227
|
if (!headAddRes.ok) return headAddRes;
|
|
2893
|
-
const shadowAddRes = applySinglePatchOpExplicitShadowStep(explicitBaseState,
|
|
3228
|
+
const shadowAddRes = applySinglePatchOpExplicitShadowStep(explicitBaseState, addOp, options, opIndex, session);
|
|
2894
3229
|
if (!shadowAddRes.ok) return shadowAddRes;
|
|
2895
|
-
return {
|
|
2896
|
-
ok: true,
|
|
2897
|
-
baseJson: shadowAddRes.baseJson,
|
|
2898
|
-
headJson: headAddRes.headJson
|
|
2899
|
-
};
|
|
3230
|
+
return { ok: true };
|
|
2900
3231
|
}
|
|
2901
3232
|
if (op.op === "copy") {
|
|
2902
|
-
const fromResolved =
|
|
3233
|
+
const fromResolved = resolveValueAtPointerInDoc(baseDoc, op.from, opIndex, session.pointerCache);
|
|
2903
3234
|
if (!fromResolved.ok) return fromResolved;
|
|
2904
|
-
return applySinglePatchOpSequentialStep(state, baseDoc,
|
|
3235
|
+
return applySinglePatchOpSequentialStep(state, baseDoc, {
|
|
2905
3236
|
op: "add",
|
|
2906
3237
|
path: op.path,
|
|
2907
3238
|
value: structuredClone(fromResolved.value)
|
|
2908
3239
|
}, options, explicitBaseState, opIndex, session);
|
|
2909
3240
|
}
|
|
2910
|
-
return applySinglePatchOpSequentialStep(state, baseDoc,
|
|
3241
|
+
return applySinglePatchOpSequentialStep(state, baseDoc, op, options, explicitBaseState, opIndex, session);
|
|
2911
3242
|
}
|
|
2912
|
-
function applySinglePatchOpSequentialStep(state, baseDoc,
|
|
2913
|
-
const compiled =
|
|
3243
|
+
function applySinglePatchOpSequentialStep(state, baseDoc, op, options, explicitBaseState, opIndex, session) {
|
|
3244
|
+
const compiled = compilePreparedSingleIntentFromDoc(baseDoc, op, session.pointerCache, opIndex);
|
|
2914
3245
|
if (!compiled.ok) return compiled;
|
|
2915
3246
|
const headStep = applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
|
|
2916
3247
|
if (!headStep.ok) return headStep;
|
|
@@ -2918,121 +3249,157 @@ function applySinglePatchOpSequentialStep(state, baseDoc, baseJson, headJson, op
|
|
|
2918
3249
|
const shadowStep = applyIntentsToCrdt(explicitBaseState.doc, explicitBaseState.doc, compiled.intents, () => explicitBaseState.clock.next(), "base", (ctr) => bumpClockCounter(explicitBaseState, ctr), { strictParents: options.strictParents });
|
|
2919
3250
|
if (!shadowStep.ok) return shadowStep;
|
|
2920
3251
|
}
|
|
2921
|
-
|
|
2922
|
-
ok: true,
|
|
2923
|
-
baseJson,
|
|
2924
|
-
headJson
|
|
2925
|
-
};
|
|
2926
|
-
const nextBaseJson = applyJsonPatchOpToShadow(baseJson, op, explicitBaseState ? session.baseShadowParentCache : session.headShadowParentCache, {
|
|
2927
|
-
pointerCache: session.pointerCache,
|
|
2928
|
-
opIndex
|
|
2929
|
-
});
|
|
2930
|
-
return {
|
|
2931
|
-
ok: true,
|
|
2932
|
-
baseJson: nextBaseJson,
|
|
2933
|
-
headJson: explicitBaseState ? applyJsonPatchOpToShadow(headJson, op, session.headShadowParentCache, {
|
|
2934
|
-
pointerCache: session.pointerCache,
|
|
2935
|
-
opIndex
|
|
2936
|
-
}) : nextBaseJson
|
|
2937
|
-
};
|
|
3252
|
+
return { ok: true };
|
|
2938
3253
|
}
|
|
2939
|
-
function applySinglePatchOpExplicitShadowStep(explicitBaseState,
|
|
2940
|
-
const compiled =
|
|
3254
|
+
function applySinglePatchOpExplicitShadowStep(explicitBaseState, op, options, opIndex, session) {
|
|
3255
|
+
const compiled = compilePreparedSingleIntentFromDoc(explicitBaseState.doc, op, session.pointerCache, opIndex);
|
|
2941
3256
|
if (!compiled.ok) return compiled;
|
|
2942
3257
|
const shadowStep = applyIntentsToCrdt(explicitBaseState.doc, explicitBaseState.doc, compiled.intents, () => explicitBaseState.clock.next(), "base", (ctr) => bumpClockCounter(explicitBaseState, ctr), { strictParents: options.strictParents });
|
|
2943
3258
|
if (!shadowStep.ok) return shadowStep;
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
3259
|
+
return { ok: true };
|
|
3260
|
+
}
|
|
3261
|
+
function resolveValueAtPointerInDoc(doc, pointer, opIndex, pointerCache) {
|
|
3262
|
+
let path;
|
|
3263
|
+
try {
|
|
3264
|
+
path = parsePointerWithCache(pointer, pointerCache);
|
|
3265
|
+
} catch (error) {
|
|
3266
|
+
return toPointerParseApplyError(error, pointer, opIndex);
|
|
3267
|
+
}
|
|
3268
|
+
const resolved = resolveNodeAtPath(doc.root, path);
|
|
3269
|
+
if (!resolved.ok) return {
|
|
3270
|
+
ok: false,
|
|
3271
|
+
...resolved.error,
|
|
3272
|
+
path: pointer,
|
|
3273
|
+
opIndex
|
|
2947
3274
|
};
|
|
2948
3275
|
return {
|
|
2949
3276
|
ok: true,
|
|
2950
|
-
|
|
2951
|
-
pointerCache: session.pointerCache,
|
|
2952
|
-
opIndex
|
|
2953
|
-
})
|
|
3277
|
+
value: materialize(resolved.node)
|
|
2954
3278
|
};
|
|
2955
3279
|
}
|
|
2956
|
-
function
|
|
3280
|
+
function compilePreparedSingleIntentFromDoc(baseDoc, op, pointerCache, opIndex) {
|
|
2957
3281
|
let path;
|
|
2958
3282
|
try {
|
|
2959
|
-
path = parsePointerWithCache(op.path,
|
|
3283
|
+
path = parsePointerWithCache(op.path, pointerCache);
|
|
2960
3284
|
} catch (error) {
|
|
2961
|
-
|
|
3285
|
+
return toPointerParseApplyError(error, op.path, opIndex);
|
|
2962
3286
|
}
|
|
3287
|
+
if (op.op === "test") return {
|
|
3288
|
+
ok: true,
|
|
3289
|
+
intents: [{
|
|
3290
|
+
t: "Test",
|
|
3291
|
+
path,
|
|
3292
|
+
value: op.value
|
|
3293
|
+
}]
|
|
3294
|
+
};
|
|
2963
3295
|
if (path.length === 0) {
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
3296
|
+
if (op.op === "remove") return {
|
|
3297
|
+
ok: false,
|
|
3298
|
+
code: 409,
|
|
3299
|
+
reason: "INVALID_TARGET",
|
|
3300
|
+
message: "remove at root path is not supported in RFC-compliant mode",
|
|
3301
|
+
path: op.path,
|
|
3302
|
+
opIndex
|
|
3303
|
+
};
|
|
3304
|
+
return {
|
|
3305
|
+
ok: true,
|
|
3306
|
+
intents: [{
|
|
3307
|
+
t: "ObjSet",
|
|
3308
|
+
path: [],
|
|
3309
|
+
key: ROOT_KEY,
|
|
3310
|
+
value: op.value
|
|
3311
|
+
}]
|
|
3312
|
+
};
|
|
2968
3313
|
}
|
|
2969
|
-
const pathPointer = op.path;
|
|
2970
3314
|
const parentPath = path.slice(0, -1);
|
|
2971
|
-
const parentPointer =
|
|
3315
|
+
const parentPointer = stringifyJsonPointer(parentPath);
|
|
2972
3316
|
const key = path[path.length - 1];
|
|
2973
|
-
const
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
return
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
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;
|
|
3317
|
+
const resolvedParent = parentPath.length === 0 ? {
|
|
3318
|
+
ok: true,
|
|
3319
|
+
node: baseDoc.root
|
|
3320
|
+
} : resolveNodeAtPath(baseDoc.root, parentPath);
|
|
3321
|
+
if (!resolvedParent.ok) return {
|
|
3322
|
+
ok: false,
|
|
3323
|
+
...resolvedParent.error,
|
|
3324
|
+
path: parentPointer,
|
|
3325
|
+
opIndex
|
|
3326
|
+
};
|
|
3327
|
+
const parentNode = resolvedParent.node;
|
|
3328
|
+
if (parentNode.kind === "seq") {
|
|
3329
|
+
const parsedIndex = parseArrayIndexTokenForDoc(key, op.op, op.path, opIndex);
|
|
3330
|
+
if (!parsedIndex.ok) return parsedIndex;
|
|
3331
|
+
const boundedIndex = validateArrayIndexBounds(parsedIndex.index, op.op, rgaLength(parentNode), op.path, opIndex);
|
|
3332
|
+
if (!boundedIndex.ok) return boundedIndex;
|
|
3333
|
+
if (op.op === "add") return {
|
|
3334
|
+
ok: true,
|
|
3335
|
+
intents: [{
|
|
3336
|
+
t: "ArrInsert",
|
|
3337
|
+
path: parentPath,
|
|
3338
|
+
index: boundedIndex.index,
|
|
3339
|
+
value: op.value
|
|
3340
|
+
}]
|
|
3341
|
+
};
|
|
3342
|
+
if (op.op === "remove") return {
|
|
3343
|
+
ok: true,
|
|
3344
|
+
intents: [{
|
|
3345
|
+
t: "ArrDelete",
|
|
3346
|
+
path: parentPath,
|
|
3347
|
+
index: boundedIndex.index
|
|
3348
|
+
}]
|
|
3349
|
+
};
|
|
3350
|
+
return {
|
|
3351
|
+
ok: true,
|
|
3352
|
+
intents: [{
|
|
3353
|
+
t: "ArrReplace",
|
|
3354
|
+
path: parentPath,
|
|
3355
|
+
index: boundedIndex.index,
|
|
3356
|
+
value: op.value
|
|
3357
|
+
}]
|
|
3358
|
+
};
|
|
3027
3359
|
}
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3360
|
+
if (parentNode.kind !== "obj") return {
|
|
3361
|
+
ok: false,
|
|
3362
|
+
code: 409,
|
|
3363
|
+
reason: "INVALID_TARGET",
|
|
3364
|
+
message: `expected object or array parent at ${parentPointer}`,
|
|
3365
|
+
path: parentPointer,
|
|
3366
|
+
opIndex
|
|
3367
|
+
};
|
|
3368
|
+
if (key === "__proto__") return {
|
|
3369
|
+
ok: false,
|
|
3370
|
+
code: 409,
|
|
3371
|
+
reason: "INVALID_POINTER",
|
|
3372
|
+
message: `unsafe object key at ${op.path}`,
|
|
3373
|
+
path: op.path,
|
|
3374
|
+
opIndex
|
|
3375
|
+
};
|
|
3376
|
+
const entry = parentNode.entries.get(key);
|
|
3377
|
+
if ((op.op === "replace" || op.op === "remove") && !entry) return {
|
|
3378
|
+
ok: false,
|
|
3379
|
+
code: 409,
|
|
3380
|
+
reason: "MISSING_TARGET",
|
|
3381
|
+
message: `missing key ${key} at ${parentPointer}`,
|
|
3382
|
+
path: op.path,
|
|
3383
|
+
opIndex
|
|
3384
|
+
};
|
|
3385
|
+
if (op.op === "remove") return {
|
|
3386
|
+
ok: true,
|
|
3387
|
+
intents: [{
|
|
3388
|
+
t: "ObjRemove",
|
|
3389
|
+
path: parentPath,
|
|
3390
|
+
key
|
|
3391
|
+
}]
|
|
3392
|
+
};
|
|
3393
|
+
return {
|
|
3394
|
+
ok: true,
|
|
3395
|
+
intents: [{
|
|
3396
|
+
t: "ObjSet",
|
|
3397
|
+
path: parentPath,
|
|
3398
|
+
key,
|
|
3399
|
+
value: op.value,
|
|
3400
|
+
mode: op.op
|
|
3401
|
+
}]
|
|
3402
|
+
};
|
|
3036
3403
|
}
|
|
3037
3404
|
function parsePointerWithCache(pointer, pointerCache) {
|
|
3038
3405
|
const cachedPath = pointerCache.get(pointer);
|
|
@@ -3041,21 +3408,129 @@ function parsePointerWithCache(pointer, pointerCache) {
|
|
|
3041
3408
|
pointerCache.set(pointer, parsedPath);
|
|
3042
3409
|
return parsedPath.slice();
|
|
3043
3410
|
}
|
|
3044
|
-
function
|
|
3045
|
-
let
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3411
|
+
function resolveNodeAtPath(root, path) {
|
|
3412
|
+
let current = root;
|
|
3413
|
+
for (const segment of path) {
|
|
3414
|
+
if (current.kind === "obj") {
|
|
3415
|
+
const entry = current.entries.get(segment);
|
|
3416
|
+
if (!entry) return {
|
|
3417
|
+
ok: false,
|
|
3418
|
+
error: {
|
|
3419
|
+
code: 409,
|
|
3420
|
+
reason: "MISSING_PARENT",
|
|
3421
|
+
message: `Missing key '${segment}'`
|
|
3422
|
+
}
|
|
3423
|
+
};
|
|
3424
|
+
current = entry.node;
|
|
3425
|
+
continue;
|
|
3426
|
+
}
|
|
3427
|
+
if (current.kind === "seq") {
|
|
3428
|
+
if (!ARRAY_INDEX_TOKEN_PATTERN.test(segment)) return {
|
|
3429
|
+
ok: false,
|
|
3430
|
+
error: {
|
|
3431
|
+
code: 409,
|
|
3432
|
+
reason: "INVALID_POINTER",
|
|
3433
|
+
message: `Expected array index, got '${segment}'`
|
|
3434
|
+
}
|
|
3435
|
+
};
|
|
3436
|
+
const index = Number(segment);
|
|
3437
|
+
if (!Number.isSafeInteger(index)) return {
|
|
3438
|
+
ok: false,
|
|
3439
|
+
error: {
|
|
3440
|
+
code: 409,
|
|
3441
|
+
reason: "OUT_OF_BOUNDS",
|
|
3442
|
+
message: `Index out of bounds at '${segment}'`
|
|
3443
|
+
}
|
|
3444
|
+
};
|
|
3445
|
+
const elemId = rgaIdAtIndex(current, index);
|
|
3446
|
+
if (elemId === void 0) return {
|
|
3447
|
+
ok: false,
|
|
3448
|
+
error: {
|
|
3449
|
+
code: 409,
|
|
3450
|
+
reason: "OUT_OF_BOUNDS",
|
|
3451
|
+
message: `Index out of bounds at '${segment}'`
|
|
3452
|
+
}
|
|
3453
|
+
};
|
|
3454
|
+
current = current.elems.get(elemId).value;
|
|
3455
|
+
continue;
|
|
3456
|
+
}
|
|
3457
|
+
return {
|
|
3458
|
+
ok: false,
|
|
3459
|
+
error: {
|
|
3460
|
+
code: 409,
|
|
3461
|
+
reason: "INVALID_TARGET",
|
|
3462
|
+
message: `Cannot traverse into non-container at '${segment}'`
|
|
3463
|
+
}
|
|
3464
|
+
};
|
|
3050
3465
|
}
|
|
3051
|
-
|
|
3466
|
+
return {
|
|
3467
|
+
ok: true,
|
|
3468
|
+
node: current
|
|
3469
|
+
};
|
|
3470
|
+
}
|
|
3471
|
+
function parseArrayIndexTokenForDoc(token, op, path, opIndex) {
|
|
3472
|
+
if (token === "-") {
|
|
3473
|
+
if (op !== "add") return {
|
|
3474
|
+
ok: false,
|
|
3475
|
+
code: 409,
|
|
3476
|
+
reason: "INVALID_POINTER",
|
|
3477
|
+
message: `'-' index is only valid for add at ${path}`,
|
|
3478
|
+
path,
|
|
3479
|
+
opIndex
|
|
3480
|
+
};
|
|
3052
3481
|
return {
|
|
3053
3482
|
ok: true,
|
|
3054
|
-
|
|
3483
|
+
index: Number.POSITIVE_INFINITY
|
|
3055
3484
|
};
|
|
3056
|
-
} catch (error) {
|
|
3057
|
-
return toPointerLookupApplyError(error, pointer, opIndex);
|
|
3058
3485
|
}
|
|
3486
|
+
if (!ARRAY_INDEX_TOKEN_PATTERN.test(token)) return {
|
|
3487
|
+
ok: false,
|
|
3488
|
+
code: 409,
|
|
3489
|
+
reason: "INVALID_POINTER",
|
|
3490
|
+
message: `expected array index at ${path}`,
|
|
3491
|
+
path,
|
|
3492
|
+
opIndex
|
|
3493
|
+
};
|
|
3494
|
+
const index = Number(token);
|
|
3495
|
+
if (!Number.isSafeInteger(index)) return {
|
|
3496
|
+
ok: false,
|
|
3497
|
+
code: 409,
|
|
3498
|
+
reason: "OUT_OF_BOUNDS",
|
|
3499
|
+
message: `array index is too large at ${path}`,
|
|
3500
|
+
path,
|
|
3501
|
+
opIndex
|
|
3502
|
+
};
|
|
3503
|
+
return {
|
|
3504
|
+
ok: true,
|
|
3505
|
+
index
|
|
3506
|
+
};
|
|
3507
|
+
}
|
|
3508
|
+
function validateArrayIndexBounds(index, op, arrLength, path, opIndex) {
|
|
3509
|
+
if (op === "add") {
|
|
3510
|
+
if (index === Number.POSITIVE_INFINITY) return {
|
|
3511
|
+
ok: true,
|
|
3512
|
+
index
|
|
3513
|
+
};
|
|
3514
|
+
if (index > arrLength) return {
|
|
3515
|
+
ok: false,
|
|
3516
|
+
code: 409,
|
|
3517
|
+
reason: "OUT_OF_BOUNDS",
|
|
3518
|
+
message: `index out of bounds at ${path}; expected 0..${arrLength}`,
|
|
3519
|
+
path,
|
|
3520
|
+
opIndex
|
|
3521
|
+
};
|
|
3522
|
+
} else if (index >= arrLength) return {
|
|
3523
|
+
ok: false,
|
|
3524
|
+
code: 409,
|
|
3525
|
+
reason: "OUT_OF_BOUNDS",
|
|
3526
|
+
message: `index out of bounds at ${path}; expected 0..${Math.max(arrLength - 1, 0)}`,
|
|
3527
|
+
path,
|
|
3528
|
+
opIndex
|
|
3529
|
+
};
|
|
3530
|
+
return {
|
|
3531
|
+
ok: true,
|
|
3532
|
+
index
|
|
3533
|
+
};
|
|
3059
3534
|
}
|
|
3060
3535
|
function bumpClockCounter(state, ctr) {
|
|
3061
3536
|
if (state.clock.ctr < ctr) state.clock.ctr = ctr;
|
|
@@ -3126,40 +3601,6 @@ function mergePointerPaths(basePointer, nestedPointer) {
|
|
|
3126
3601
|
if (basePointer === "") return nestedPointer;
|
|
3127
3602
|
return `${basePointer}${nestedPointer}`;
|
|
3128
3603
|
}
|
|
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
3604
|
function toApplyError(error) {
|
|
3164
3605
|
if (error instanceof TraversalDepthError) return toDepthApplyError(error);
|
|
3165
3606
|
if (error instanceof PatchCompileError) return {
|
|
@@ -3187,24 +3628,12 @@ function toPointerParseApplyError(error, pointer, opIndex) {
|
|
|
3187
3628
|
opIndex
|
|
3188
3629
|
};
|
|
3189
3630
|
}
|
|
3190
|
-
function toPointerParseCompileError(error, pointer, opIndex) {
|
|
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);
|
|
3195
|
-
return {
|
|
3196
|
-
ok: false,
|
|
3197
|
-
code: 409,
|
|
3198
|
-
reason: mapped.reason,
|
|
3199
|
-
message: mapped.message,
|
|
3200
|
-
path: pointer,
|
|
3201
|
-
opIndex
|
|
3202
|
-
};
|
|
3203
|
-
}
|
|
3204
3631
|
|
|
3205
3632
|
//#endregion
|
|
3206
3633
|
//#region src/serialize.ts
|
|
3207
3634
|
const HEAD_ELEM_ID = "HEAD";
|
|
3635
|
+
const SERIALIZED_DOC_VERSION = 1;
|
|
3636
|
+
const SERIALIZED_STATE_VERSION = 1;
|
|
3208
3637
|
function createSerializedRecord() {
|
|
3209
3638
|
return Object.create(null);
|
|
3210
3639
|
}
|
|
@@ -3229,13 +3658,16 @@ var DeserializeError = class extends Error {
|
|
|
3229
3658
|
};
|
|
3230
3659
|
/** Serialize a CRDT document to a JSON-safe representation (Maps become plain objects). */
|
|
3231
3660
|
function serializeDoc(doc) {
|
|
3232
|
-
return {
|
|
3661
|
+
return {
|
|
3662
|
+
version: SERIALIZED_DOC_VERSION,
|
|
3663
|
+
root: serializeNode(doc.root)
|
|
3664
|
+
};
|
|
3233
3665
|
}
|
|
3234
3666
|
/** Reconstruct a CRDT document from its serialized form. */
|
|
3235
3667
|
function deserializeDoc(data) {
|
|
3236
|
-
|
|
3237
|
-
if (!("root" in
|
|
3238
|
-
return { root: deserializeNode(
|
|
3668
|
+
const raw = readSerializedDocEnvelope(data);
|
|
3669
|
+
if (!("root" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/root", "serialized doc is missing root");
|
|
3670
|
+
return { root: deserializeNode(raw.root, "/root", 0) };
|
|
3239
3671
|
}
|
|
3240
3672
|
/** Non-throwing `deserializeDoc` variant with typed validation details. */
|
|
3241
3673
|
function tryDeserializeDoc(data) {
|
|
@@ -3256,6 +3688,7 @@ function tryDeserializeDoc(data) {
|
|
|
3256
3688
|
/** Serialize a full CRDT state (document + clock) to a JSON-safe representation. */
|
|
3257
3689
|
function serializeState(state) {
|
|
3258
3690
|
return {
|
|
3691
|
+
version: SERIALIZED_STATE_VERSION,
|
|
3259
3692
|
doc: serializeDoc(state.doc),
|
|
3260
3693
|
clock: {
|
|
3261
3694
|
actor: state.clock.actor,
|
|
@@ -3263,16 +3696,21 @@ function serializeState(state) {
|
|
|
3263
3696
|
}
|
|
3264
3697
|
};
|
|
3265
3698
|
}
|
|
3266
|
-
/**
|
|
3699
|
+
/**
|
|
3700
|
+
* Reconstruct a full CRDT state from its serialized form, restoring the clock.
|
|
3701
|
+
*
|
|
3702
|
+
* May throw `TraversalDepthError` when the payload exceeds the maximum
|
|
3703
|
+
* supported nesting depth.
|
|
3704
|
+
*/
|
|
3267
3705
|
function deserializeState(data) {
|
|
3268
|
-
|
|
3269
|
-
if (!("doc" in
|
|
3270
|
-
if (!("clock" in
|
|
3271
|
-
const clockRaw = asRecord(
|
|
3706
|
+
const raw = readSerializedStateEnvelope(data);
|
|
3707
|
+
if (!("doc" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/doc", "serialized state is missing doc");
|
|
3708
|
+
if (!("clock" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/clock", "serialized state is missing clock");
|
|
3709
|
+
const clockRaw = asRecord(raw.clock, "/clock");
|
|
3272
3710
|
const actor = readActor(clockRaw.actor, "/clock/actor");
|
|
3273
3711
|
const ctr = readCounter(clockRaw.ctr, "/clock/ctr");
|
|
3274
|
-
const doc = deserializeDoc(
|
|
3275
|
-
const observedCtr =
|
|
3712
|
+
const doc = deserializeDoc(raw.doc);
|
|
3713
|
+
const observedCtr = observedVersionVector(doc)[actor] ?? 0;
|
|
3276
3714
|
return {
|
|
3277
3715
|
doc,
|
|
3278
3716
|
clock: createClock(actor, Math.max(ctr, observedCtr))
|
|
@@ -3346,6 +3784,16 @@ function serializeNode(node) {
|
|
|
3346
3784
|
elems
|
|
3347
3785
|
};
|
|
3348
3786
|
}
|
|
3787
|
+
function readSerializedDocEnvelope(data) {
|
|
3788
|
+
const raw = asRecord(data, "/");
|
|
3789
|
+
assertSerializedEnvelopeVersion(raw, "/version", SERIALIZED_DOC_VERSION, "doc");
|
|
3790
|
+
return raw;
|
|
3791
|
+
}
|
|
3792
|
+
function readSerializedStateEnvelope(data) {
|
|
3793
|
+
const raw = asRecord(data, "/");
|
|
3794
|
+
assertSerializedEnvelopeVersion(raw, "/version", SERIALIZED_STATE_VERSION, "state");
|
|
3795
|
+
return raw;
|
|
3796
|
+
}
|
|
3349
3797
|
function deserializeNode(node, path, depth) {
|
|
3350
3798
|
assertTraversalDepth(depth);
|
|
3351
3799
|
const raw = asRecord(node, path);
|
|
@@ -3433,28 +3881,19 @@ function assertAcyclicRgaPredecessors(elems, path) {
|
|
|
3433
3881
|
for (const id of trail) visitState.set(id, 2);
|
|
3434
3882
|
}
|
|
3435
3883
|
}
|
|
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
3884
|
function asRecord(value, path) {
|
|
3455
3885
|
if (!isRecord(value)) fail("INVALID_SERIALIZED_SHAPE", path, "expected object");
|
|
3456
3886
|
return value;
|
|
3457
3887
|
}
|
|
3888
|
+
function assertSerializedEnvelopeVersion(raw, path, expectedVersion, label) {
|
|
3889
|
+
if (!("version" in raw)) return;
|
|
3890
|
+
const version = readVersion(raw.version, path);
|
|
3891
|
+
if (version !== expectedVersion) fail("INVALID_SERIALIZED_SHAPE", path, `unsupported serialized ${label} version '${version}'`);
|
|
3892
|
+
}
|
|
3893
|
+
function readVersion(value, path) {
|
|
3894
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) fail("INVALID_SERIALIZED_SHAPE", path, "envelope version must be a non-negative safe integer");
|
|
3895
|
+
return value;
|
|
3896
|
+
}
|
|
3458
3897
|
function readDot(value, path) {
|
|
3459
3898
|
const raw = asRecord(value, path);
|
|
3460
3899
|
return {
|
|
@@ -3553,7 +3992,7 @@ function mergeDoc(a, b, options = {}) {
|
|
|
3553
3992
|
function tryMergeDoc(a, b, options = {}) {
|
|
3554
3993
|
try {
|
|
3555
3994
|
const mismatchPath = options.requireSharedOrigin ?? true ? findSeqLineageMismatch(a.root, b.root, []) : null;
|
|
3556
|
-
if (mismatchPath) return {
|
|
3995
|
+
if (mismatchPath !== null) return {
|
|
3557
3996
|
ok: false,
|
|
3558
3997
|
error: {
|
|
3559
3998
|
ok: false,
|
|
@@ -3634,7 +4073,7 @@ function findSeqLineageMismatch(a, b, path) {
|
|
|
3634
4073
|
shared = true;
|
|
3635
4074
|
break;
|
|
3636
4075
|
}
|
|
3637
|
-
if (!shared) return
|
|
4076
|
+
if (!shared) return stringifyJsonPointer(frame.path);
|
|
3638
4077
|
}
|
|
3639
4078
|
}
|
|
3640
4079
|
if (frame.a.kind === "obj" && frame.b.kind === "obj") {
|
|
@@ -3657,45 +4096,11 @@ function findSeqLineageMismatch(a, b, path) {
|
|
|
3657
4096
|
return null;
|
|
3658
4097
|
}
|
|
3659
4098
|
function maxObservedCtrForActor(doc, actor, a, b) {
|
|
3660
|
-
let best =
|
|
4099
|
+
let best = observedVersionVector(doc)[actor] ?? 0;
|
|
3661
4100
|
if (a.clock.actor === actor && a.clock.ctr > best) best = a.clock.ctr;
|
|
3662
4101
|
if (b.clock.actor === actor && b.clock.ctr > best) best = b.clock.ctr;
|
|
3663
4102
|
return best;
|
|
3664
4103
|
}
|
|
3665
|
-
function maxCtrInNodeForActor(node, actor) {
|
|
3666
|
-
let best = 0;
|
|
3667
|
-
const stack = [{
|
|
3668
|
-
node,
|
|
3669
|
-
depth: 0
|
|
3670
|
-
}];
|
|
3671
|
-
while (stack.length > 0) {
|
|
3672
|
-
const frame = stack.pop();
|
|
3673
|
-
assertTraversalDepth(frame.depth);
|
|
3674
|
-
if (frame.node.kind === "lww") {
|
|
3675
|
-
if (frame.node.dot.actor === actor && frame.node.dot.ctr > best) best = frame.node.dot.ctr;
|
|
3676
|
-
continue;
|
|
3677
|
-
}
|
|
3678
|
-
if (frame.node.kind === "obj") {
|
|
3679
|
-
for (const entry of frame.node.entries.values()) {
|
|
3680
|
-
if (entry.dot.actor === actor && entry.dot.ctr > best) best = entry.dot.ctr;
|
|
3681
|
-
stack.push({
|
|
3682
|
-
node: entry.node,
|
|
3683
|
-
depth: frame.depth + 1
|
|
3684
|
-
});
|
|
3685
|
-
}
|
|
3686
|
-
for (const tomb of frame.node.tombstone.values()) if (tomb.actor === actor && tomb.ctr > best) best = tomb.ctr;
|
|
3687
|
-
continue;
|
|
3688
|
-
}
|
|
3689
|
-
for (const elem of frame.node.elems.values()) {
|
|
3690
|
-
if (elem.insDot.actor === actor && elem.insDot.ctr > best) best = elem.insDot.ctr;
|
|
3691
|
-
stack.push({
|
|
3692
|
-
node: elem.value,
|
|
3693
|
-
depth: frame.depth + 1
|
|
3694
|
-
});
|
|
3695
|
-
}
|
|
3696
|
-
}
|
|
3697
|
-
return best;
|
|
3698
|
-
}
|
|
3699
4104
|
function repDot(node) {
|
|
3700
4105
|
switch (node.kind) {
|
|
3701
4106
|
case "lww": return node.dot;
|
|
@@ -3788,8 +4193,8 @@ function mergeSeq(a, b, depth, path) {
|
|
|
3788
4193
|
const ea = a.elems.get(id);
|
|
3789
4194
|
const eb = b.elems.get(id);
|
|
3790
4195
|
if (ea && eb) {
|
|
3791
|
-
if (ea.prev !== eb.prev) throw new SharedElementMetadataMismatchError(
|
|
3792
|
-
if (!sameDot(ea.insDot, eb.insDot)) throw new SharedElementMetadataMismatchError(
|
|
4196
|
+
if (ea.prev !== eb.prev) throw new SharedElementMetadataMismatchError(stringifyJsonPointer(path), id, "prev");
|
|
4197
|
+
if (!sameDot(ea.insDot, eb.insDot)) throw new SharedElementMetadataMismatchError(stringifyJsonPointer(path), id, "insDot");
|
|
3793
4198
|
const mergedValue = mergeNodeAtDepth(ea.value, eb.value, depth + 1, [...path, id]);
|
|
3794
4199
|
elems.set(id, {
|
|
3795
4200
|
id,
|
|
@@ -3810,10 +4215,6 @@ function mergeSeq(a, b, depth, path) {
|
|
|
3810
4215
|
function sameDot(a, b) {
|
|
3811
4216
|
return a.actor === b.actor && a.ctr === b.ctr;
|
|
3812
4217
|
}
|
|
3813
|
-
function toPointer(path) {
|
|
3814
|
-
if (path.length === 0) return "/";
|
|
3815
|
-
return `/${path.join("/")}`;
|
|
3816
|
-
}
|
|
3817
4218
|
function cloneElem(e, depth) {
|
|
3818
4219
|
assertTraversalDepth(depth);
|
|
3819
4220
|
return {
|
|
@@ -3943,4 +4344,4 @@ function compactStateTombstones(state, options) {
|
|
|
3943
4344
|
}
|
|
3944
4345
|
|
|
3945
4346
|
//#endregion
|
|
3946
|
-
export {
|
|
4347
|
+
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 };
|