json-patch-to-crdt 0.3.0 → 0.4.0

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