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