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