json-patch-to-crdt 0.2.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,19 +1,38 @@
1
1
 
2
- //#region src/clock.ts
3
- var ClockValidationError = class extends TypeError {
4
- reason;
5
- constructor(reason, message) {
6
- super(message);
7
- this.name = "ClockValidationError";
8
- this.reason = reason;
2
+ //#region src/depth.ts
3
+ const MAX_TRAVERSAL_DEPTH = 16384;
4
+ var TraversalDepthError = class extends Error {
5
+ code = 409;
6
+ reason = "MAX_DEPTH_EXCEEDED";
7
+ depth;
8
+ maxDepth;
9
+ constructor(depth, maxDepth = MAX_TRAVERSAL_DEPTH) {
10
+ super(`maximum nesting depth ${maxDepth} exceeded at depth ${depth}`);
11
+ this.name = "TraversalDepthError";
12
+ this.depth = depth;
13
+ this.maxDepth = maxDepth;
9
14
  }
10
15
  };
11
- function readVvCounter$1(vv, actor) {
12
- if (!Object.prototype.hasOwnProperty.call(vv, actor)) return 0;
16
+ function assertTraversalDepth(depth, maxDepth = MAX_TRAVERSAL_DEPTH) {
17
+ if (depth > maxDepth) throw new TraversalDepthError(depth, maxDepth);
18
+ }
19
+ function toDepthApplyError(error) {
20
+ return {
21
+ ok: false,
22
+ code: error.code,
23
+ reason: error.reason,
24
+ message: error.message
25
+ };
26
+ }
27
+
28
+ //#endregion
29
+ //#region src/version-vector.ts
30
+ function readVersionVectorCounter(vv, actor) {
31
+ if (!Object.prototype.hasOwnProperty.call(vv, actor)) return;
13
32
  const counter = vv[actor];
14
- return typeof counter === "number" ? counter : 0;
33
+ return typeof counter === "number" ? counter : void 0;
15
34
  }
16
- function writeVvCounter$1(vv, actor, counter) {
35
+ function writeVersionVectorCounter(vv, actor, counter) {
17
36
  Object.defineProperty(vv, actor, {
18
37
  configurable: true,
19
38
  enumerable: true,
@@ -21,6 +40,104 @@ function writeVvCounter$1(vv, actor, counter) {
21
40
  writable: true
22
41
  });
23
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
+ const doc = "doc" in target ? target.doc : target;
55
+ const vv = Object.create(null);
56
+ if ("clock" in target) observeVersionVectorDot(vv, {
57
+ actor: target.clock.actor,
58
+ ctr: target.clock.ctr
59
+ });
60
+ const stack = [{
61
+ node: doc.root,
62
+ depth: 0
63
+ }];
64
+ while (stack.length > 0) {
65
+ const frame = stack.pop();
66
+ assertTraversalDepth(frame.depth);
67
+ if (frame.node.kind === "lww") {
68
+ observeVersionVectorDot(vv, frame.node.dot);
69
+ continue;
70
+ }
71
+ if (frame.node.kind === "obj") {
72
+ for (const entry of frame.node.entries.values()) {
73
+ observeVersionVectorDot(vv, entry.dot);
74
+ stack.push({
75
+ node: entry.node,
76
+ depth: frame.depth + 1
77
+ });
78
+ }
79
+ for (const tombstone of frame.node.tombstone.values()) observeVersionVectorDot(vv, tombstone);
80
+ continue;
81
+ }
82
+ for (const elem of frame.node.elems.values()) {
83
+ observeVersionVectorDot(vv, elem.insDot);
84
+ if (elem.delDot) observeVersionVectorDot(vv, elem.delDot);
85
+ stack.push({
86
+ node: elem.value,
87
+ depth: frame.depth + 1
88
+ });
89
+ }
90
+ }
91
+ return vv;
92
+ }
93
+ /** Combine version vectors using per-actor maxima. */
94
+ function mergeVersionVectors(...vectors) {
95
+ const merged = Object.create(null);
96
+ for (const vv of vectors) for (const actor of Object.keys(vv)) {
97
+ const counter = readVersionVectorCounter(vv, actor);
98
+ if (counter === void 0) continue;
99
+ writeVersionVectorCounter(merged, actor, Math.max(readVersionVectorCounter(merged, actor) ?? 0, counter));
100
+ }
101
+ return merged;
102
+ }
103
+ /**
104
+ * Derive a causally-stable checkpoint by taking the per-actor minimum.
105
+ *
106
+ * When called with a single vector the result equals that vector. In practice,
107
+ * a meaningful shared-stability checkpoint usually needs acknowledgements from
108
+ * at least two peers or from an explicit quorum.
109
+ */
110
+ function intersectVersionVectors(...vectors) {
111
+ if (vectors.length === 0) return Object.create(null);
112
+ const actors = /* @__PURE__ */ new Set();
113
+ for (const vv of vectors) for (const actor of Object.keys(vv)) actors.add(actor);
114
+ const intersection = Object.create(null);
115
+ for (const actor of actors) {
116
+ const counters = vectors.map((vv) => readVersionVectorCounter(vv, actor) ?? 0);
117
+ const counter = Math.min(...counters);
118
+ if (counter > 0) writeVersionVectorCounter(intersection, actor, counter);
119
+ }
120
+ return intersection;
121
+ }
122
+ /** Check whether one version vector has observed every counter in another. */
123
+ function versionVectorCovers(observed, required) {
124
+ for (const actor of Object.keys(required)) {
125
+ const requiredCounter = readVersionVectorCounter(required, actor) ?? 0;
126
+ if ((readVersionVectorCounter(observed, actor) ?? 0) < requiredCounter) return false;
127
+ }
128
+ return true;
129
+ }
130
+
131
+ //#endregion
132
+ //#region src/clock.ts
133
+ var ClockValidationError = class extends TypeError {
134
+ reason;
135
+ constructor(reason, message) {
136
+ super(message);
137
+ this.name = "ClockValidationError";
138
+ this.reason = reason;
139
+ }
140
+ };
24
141
  /**
25
142
  * Create a new clock for the given actor. Each call to `clock.next()` yields a fresh `Dot`.
26
143
  * @param actor - Unique identifier for this peer.
@@ -57,8 +174,8 @@ function cloneClock(clock) {
57
174
  * Useful when a server needs to mint dots for many actors.
58
175
  */
59
176
  function nextDotForActor(vv, actor) {
60
- const ctr = readVvCounter$1(vv, actor) + 1;
61
- writeVvCounter$1(vv, actor, ctr);
177
+ const ctr = (readVersionVectorCounter(vv, actor) ?? 0) + 1;
178
+ writeVersionVectorCounter(vv, actor, ctr);
62
179
  return {
63
180
  actor,
64
181
  ctr
@@ -66,63 +183,20 @@ function nextDotForActor(vv, actor) {
66
183
  }
67
184
  /** Record an observed dot in a version vector. */
68
185
  function observeDot(vv, dot) {
69
- if (readVvCounter$1(vv, dot.actor) < dot.ctr) writeVvCounter$1(vv, dot.actor, dot.ctr);
70
- }
71
-
72
- //#endregion
73
- //#region src/depth.ts
74
- const MAX_TRAVERSAL_DEPTH = 16384;
75
- var TraversalDepthError = class extends Error {
76
- code = 409;
77
- reason = "MAX_DEPTH_EXCEEDED";
78
- depth;
79
- maxDepth;
80
- constructor(depth, maxDepth = MAX_TRAVERSAL_DEPTH) {
81
- super(`maximum nesting depth ${maxDepth} exceeded at depth ${depth}`);
82
- this.name = "TraversalDepthError";
83
- this.depth = depth;
84
- this.maxDepth = maxDepth;
85
- }
86
- };
87
- function assertTraversalDepth(depth, maxDepth = MAX_TRAVERSAL_DEPTH) {
88
- if (depth > maxDepth) throw new TraversalDepthError(depth, maxDepth);
89
- }
90
- function toDepthApplyError(error) {
91
- return {
92
- ok: false,
93
- code: error.code,
94
- reason: error.reason,
95
- message: error.message
96
- };
186
+ observeVersionVectorDot(vv, dot);
97
187
  }
98
188
 
99
189
  //#endregion
100
190
  //#region src/dot.ts
101
- function readVvCounter(vv, actor) {
102
- if (!Object.prototype.hasOwnProperty.call(vv, actor)) return;
103
- const counter = vv[actor];
104
- return typeof counter === "number" ? counter : void 0;
105
- }
106
- function writeVvCounter(vv, actor, counter) {
107
- Object.defineProperty(vv, actor, {
108
- configurable: true,
109
- enumerable: true,
110
- value: counter,
111
- writable: true
112
- });
113
- }
114
191
  function compareDot(a, b) {
115
192
  if (a.ctr !== b.ctr) return a.ctr - b.ctr;
116
193
  return a.actor < b.actor ? -1 : a.actor > b.actor ? 1 : 0;
117
194
  }
118
195
  function vvHasDot(vv, d) {
119
- return (readVvCounter(vv, d.actor) ?? 0) >= d.ctr;
196
+ return (readVersionVectorCounter(vv, d.actor) ?? 0) >= d.ctr;
120
197
  }
121
198
  function vvMerge(a, b) {
122
- const out = Object.create(null);
123
- for (const [actor, ctr] of Object.entries(a)) writeVvCounter(out, actor, ctr);
124
- for (const [actor, ctr] of Object.entries(b)) writeVvCounter(out, actor, Math.max(readVvCounter(out, actor) ?? 0, ctr));
125
- return out;
199
+ return mergeVersionVectors(a, b);
126
200
  }
127
201
  function dotToElemId(d) {
128
202
  return `${d.actor}:${d.ctr}`;
@@ -133,12 +207,31 @@ function dotToElemId(d) {
133
207
  const HEAD = "HEAD";
134
208
  const linearCache = /* @__PURE__ */ new WeakMap();
135
209
  const seqVersion = /* @__PURE__ */ new WeakMap();
210
+ const maxSiblingInsDotByPrevCache = /* @__PURE__ */ new WeakMap();
136
211
  function getVersion(seq) {
137
212
  return seqVersion.get(seq) ?? 0;
138
213
  }
139
214
  function bumpVersion(seq) {
140
215
  seqVersion.set(seq, getVersion(seq) + 1);
141
216
  }
217
+ function buildMaxSiblingInsDotByPrevIndex(seq) {
218
+ const index = /* @__PURE__ */ new Map();
219
+ for (const elem of seq.elems.values()) {
220
+ const current = index.get(elem.prev);
221
+ if (!current || compareDot(elem.insDot, current) > 0) index.set(elem.prev, elem.insDot);
222
+ }
223
+ maxSiblingInsDotByPrevCache.set(seq, index);
224
+ return index;
225
+ }
226
+ function getMaxSiblingInsDotByPrevIndex(seq) {
227
+ return maxSiblingInsDotByPrevCache.get(seq) ?? buildMaxSiblingInsDotByPrevIndex(seq);
228
+ }
229
+ function trackInsertedSiblingDot(seq, prev, insDot) {
230
+ const index = maxSiblingInsDotByPrevCache.get(seq);
231
+ if (!index) return;
232
+ const current = index.get(prev);
233
+ if (!current || compareDot(insDot, current) > 0) index.set(prev, insDot);
234
+ }
142
235
  function rgaChildrenIndex(seq) {
143
236
  const idx = /* @__PURE__ */ new Map();
144
237
  for (const e of seq.elems.values()) {
@@ -187,6 +280,36 @@ function rgaLinearizeIds(seq) {
187
280
  });
188
281
  return [...out];
189
282
  }
283
+ function rgaLength(seq) {
284
+ const ver = getVersion(seq);
285
+ const cached = linearCache.get(seq);
286
+ if (cached && cached.version === ver) return cached.ids.length;
287
+ return rgaLinearizeIds(seq).length;
288
+ }
289
+ function rgaCreateIndexedIdSnapshot(seq) {
290
+ const ids = rgaLinearizeIds(seq);
291
+ return {
292
+ length() {
293
+ return ids.length;
294
+ },
295
+ idAt(index) {
296
+ return ids[index];
297
+ },
298
+ prevForInsertAt(index) {
299
+ if (index <= 0) return HEAD;
300
+ return ids[index - 1] ?? (ids.length > 0 ? ids[ids.length - 1] : HEAD);
301
+ },
302
+ insertAt(index, id) {
303
+ const at = Math.max(0, Math.min(index, ids.length));
304
+ ids.splice(at, 0, id);
305
+ },
306
+ deleteAt(index) {
307
+ if (index < 0 || index >= ids.length) return;
308
+ const [removed] = ids.splice(index, 1);
309
+ return removed;
310
+ }
311
+ };
312
+ }
190
313
  function rgaInsertAfter(seq, prev, id, insDot, value) {
191
314
  if (seq.elems.has(id)) return;
192
315
  seq.elems.set(id, {
@@ -196,15 +319,110 @@ function rgaInsertAfter(seq, prev, id, insDot, value) {
196
319
  value,
197
320
  insDot
198
321
  });
322
+ trackInsertedSiblingDot(seq, prev, insDot);
199
323
  bumpVersion(seq);
200
324
  }
201
- function rgaDelete(seq, id) {
325
+ function rgaInsertAfterChecked(seq, prev, id, insDot, value) {
326
+ if (seq.elems.has(id)) return;
327
+ if (prev !== HEAD && !seq.elems.has(prev)) throw new Error(`RGA predecessor '${prev}' does not exist`);
328
+ rgaInsertAfter(seq, prev, id, insDot, value);
329
+ }
330
+ function rgaDelete(seq, id, delDot) {
202
331
  const e = seq.elems.get(id);
203
332
  if (!e) return;
204
- if (e.tombstone) return;
333
+ if (e.tombstone) {
334
+ if (delDot && (!e.delDot || compareDot(delDot, e.delDot) > 0)) {
335
+ e.delDot = {
336
+ actor: delDot.actor,
337
+ ctr: delDot.ctr
338
+ };
339
+ bumpVersion(seq);
340
+ }
341
+ return;
342
+ }
205
343
  e.tombstone = true;
344
+ if (delDot) e.delDot = {
345
+ actor: delDot.actor,
346
+ ctr: delDot.ctr
347
+ };
206
348
  bumpVersion(seq);
207
349
  }
350
+ function validateRgaSeq(seq) {
351
+ const issues = [];
352
+ for (const elem of seq.elems.values()) if (elem.prev !== HEAD && !seq.elems.has(elem.prev)) issues.push({
353
+ code: "MISSING_PREDECESSOR",
354
+ id: elem.id,
355
+ prev: elem.prev,
356
+ message: `RGA element '${elem.id}' references missing predecessor '${elem.prev}'`
357
+ });
358
+ const cycleIds = /* @__PURE__ */ new Set();
359
+ const visitState = /* @__PURE__ */ new Map();
360
+ const sortedIds = [...seq.elems.keys()].sort();
361
+ for (const startId of sortedIds) {
362
+ if (visitState.get(startId) === 2) continue;
363
+ const trail = [];
364
+ const trailIndex = /* @__PURE__ */ new Map();
365
+ let currentId = startId;
366
+ while (currentId !== void 0) {
367
+ const seenAt = trailIndex.get(currentId);
368
+ if (seenAt !== void 0) {
369
+ for (let i = seenAt; i < trail.length; i++) cycleIds.add(trail[i]);
370
+ break;
371
+ }
372
+ if (visitState.get(currentId) === 2) break;
373
+ const elem = seq.elems.get(currentId);
374
+ if (!elem) break;
375
+ trailIndex.set(currentId, trail.length);
376
+ trail.push(currentId);
377
+ if (elem.prev === HEAD) break;
378
+ currentId = elem.prev;
379
+ }
380
+ for (const id of trail) visitState.set(id, 2);
381
+ }
382
+ for (const id of [...cycleIds].sort()) {
383
+ const elem = seq.elems.get(id);
384
+ issues.push({
385
+ code: "PREDECESSOR_CYCLE",
386
+ id,
387
+ prev: elem.prev,
388
+ message: `RGA predecessor cycle detected at '${id}'`
389
+ });
390
+ }
391
+ const children = rgaChildrenIndex(seq);
392
+ const reachable = /* @__PURE__ */ new Set();
393
+ const stack = [...children.get(HEAD) ?? []];
394
+ while (stack.length > 0) {
395
+ const elem = stack.pop();
396
+ if (reachable.has(elem.id)) continue;
397
+ reachable.add(elem.id);
398
+ const descendants = children.get(elem.id);
399
+ if (descendants) stack.push(...descendants);
400
+ }
401
+ for (const id of sortedIds) {
402
+ if (reachable.has(id)) continue;
403
+ const elem = seq.elems.get(id);
404
+ issues.push({
405
+ code: "ORPHANED_ELEMENT",
406
+ id,
407
+ prev: elem.prev,
408
+ message: `RGA element '${id}' is unreachable from HEAD`
409
+ });
410
+ }
411
+ if (issues.length === 0) return {
412
+ ok: true,
413
+ issues: []
414
+ };
415
+ const issueOrder = {
416
+ MISSING_PREDECESSOR: 0,
417
+ PREDECESSOR_CYCLE: 1,
418
+ ORPHANED_ELEMENT: 2
419
+ };
420
+ issues.sort((a, b) => a.id.localeCompare(b.id) || issueOrder[a.code] - issueOrder[b.code] || a.prev.localeCompare(b.prev));
421
+ return {
422
+ ok: false,
423
+ issues
424
+ };
425
+ }
208
426
  /**
209
427
  * Prune tombstoned elements that are causally stable and have no live descendants
210
428
  * depending on them for sequence traversal.
@@ -251,15 +469,19 @@ function rgaCompactTombstones(seq, isStable) {
251
469
  continue;
252
470
  }
253
471
  const elem = seq.elems.get(frame.id);
254
- if (!elem || !elem.tombstone || !isStable(elem.insDot)) continue;
472
+ if (!elem || !elem.tombstone || !elem.delDot || !isStable(elem.delDot)) continue;
255
473
  const childIds = children.get(frame.id);
256
474
  if (!childIds || childIds.every((childId) => removable.has(childId))) removable.add(frame.id);
257
475
  }
258
476
  if (removable.size === 0) return 0;
259
477
  for (const id of removable) seq.elems.delete(id);
478
+ maxSiblingInsDotByPrevCache.delete(seq);
260
479
  bumpVersion(seq);
261
480
  return removable.size;
262
481
  }
482
+ function rgaMaxInsertDotForPrev(seq, prev) {
483
+ return getMaxSiblingInsDotByPrevIndex(seq).get(prev) ?? null;
484
+ }
263
485
  function rgaIdAtIndex(seq, index) {
264
486
  return rgaLinearizeIds(seq)[index];
265
487
  }
@@ -271,6 +493,7 @@ function rgaPrevForInsertAtIndex(seq, index) {
271
493
 
272
494
  //#endregion
273
495
  //#region src/materialize.ts
496
+ let materializeObserver = null;
274
497
  function createMaterializedObject() {
275
498
  return Object.create(null);
276
499
  }
@@ -284,6 +507,8 @@ function setMaterializedProperty(out, key, value) {
284
507
  }
285
508
  /** Convert a CRDT node graph into a plain JSON value using an explicit stack. */
286
509
  function materialize(node) {
510
+ const observer = materializeObserver;
511
+ observer?.([], node);
287
512
  if (node.kind === "lww") return node.value;
288
513
  const root = node.kind === "obj" ? createMaterializedObject() : [];
289
514
  const stack = [];
@@ -291,13 +516,16 @@ function materialize(node) {
291
516
  kind: "obj",
292
517
  depth: 0,
293
518
  entries: node.entries.entries(),
294
- out: root
519
+ out: root,
520
+ path: []
295
521
  });
296
522
  else stack.push({
297
523
  kind: "seq",
298
524
  depth: 0,
299
525
  cursor: rgaCreateLinearCursor(node),
300
- out: root
526
+ out: root,
527
+ path: [],
528
+ nextIndex: 0
301
529
  });
302
530
  while (stack.length > 0) {
303
531
  const frame = stack[stack.length - 1];
@@ -311,6 +539,8 @@ function materialize(node) {
311
539
  const child = entry.node;
312
540
  const childDepth = frame.depth + 1;
313
541
  assertTraversalDepth(childDepth);
542
+ const childPath = [...frame.path, key];
543
+ observer?.(childPath, child);
314
544
  if (child.kind === "lww") {
315
545
  setMaterializedProperty(frame.out, key, child.value);
316
546
  continue;
@@ -322,7 +552,8 @@ function materialize(node) {
322
552
  kind: "obj",
323
553
  depth: childDepth,
324
554
  entries: child.entries.entries(),
325
- out: outObj
555
+ out: outObj,
556
+ path: childPath
326
557
  });
327
558
  continue;
328
559
  }
@@ -332,7 +563,9 @@ function materialize(node) {
332
563
  kind: "seq",
333
564
  depth: childDepth,
334
565
  cursor: rgaCreateLinearCursor(child),
335
- out: outArr
566
+ out: outArr,
567
+ path: childPath,
568
+ nextIndex: 0
336
569
  });
337
570
  continue;
338
571
  }
@@ -344,6 +577,9 @@ function materialize(node) {
344
577
  const child = elem.value;
345
578
  const childDepth = frame.depth + 1;
346
579
  assertTraversalDepth(childDepth);
580
+ const childPath = [...frame.path, String(frame.nextIndex)];
581
+ frame.nextIndex += 1;
582
+ observer?.(childPath, child);
347
583
  if (child.kind === "lww") {
348
584
  frame.out.push(child.value);
349
585
  continue;
@@ -355,7 +591,8 @@ function materialize(node) {
355
591
  kind: "obj",
356
592
  depth: childDepth,
357
593
  entries: child.entries.entries(),
358
- out: outObj
594
+ out: outObj,
595
+ path: childPath
359
596
  });
360
597
  continue;
361
598
  }
@@ -365,7 +602,9 @@ function materialize(node) {
365
602
  kind: "seq",
366
603
  depth: childDepth,
367
604
  cursor: rgaCreateLinearCursor(child),
368
- out: outArr
605
+ out: outArr,
606
+ path: childPath,
607
+ nextIndex: 0
369
608
  });
370
609
  }
371
610
  return root;
@@ -476,6 +715,7 @@ function assertRuntimeJsonValue(value) {
476
715
  /**
477
716
  * Normalize a runtime value to JSON-compatible data.
478
717
  * - non-finite numbers -> null
718
+ * - non-plain objects -> null at the root / in arrays, omitted from object properties
479
719
  * - invalid object-property values -> key omitted
480
720
  * - invalid root / array values -> null
481
721
  */
@@ -562,7 +802,10 @@ function isJsonPrimitive$1(value) {
562
802
  return typeof value === "number" && Number.isFinite(value);
563
803
  }
564
804
  function isJsonObject(value) {
565
- return typeof value === "object" && value !== null && !Array.isArray(value);
805
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
806
+ if (Object.prototype.toString.call(value) !== "[object Object]") return false;
807
+ const prototype = Object.getPrototypeOf(value);
808
+ return prototype === null || Object.getPrototypeOf(prototype) === null;
566
809
  }
567
810
  function isNonFiniteNumber(value) {
568
811
  return typeof value === "number" && !Number.isFinite(value);
@@ -573,8 +816,16 @@ function describeInvalidValue(value) {
573
816
  if (typeof value === "bigint") return "bigint is not valid JSON";
574
817
  if (typeof value === "symbol") return "symbol is not valid JSON";
575
818
  if (typeof value === "function") return "function is not valid JSON";
819
+ if (typeof value === "object" && value !== null) return `non-plain object (${describeObjectKind(value)}) is not valid JSON`;
576
820
  return `unsupported value type (${typeof value})`;
577
821
  }
822
+ function describeObjectKind(value) {
823
+ const tag = Object.prototype.toString.call(value).slice(8, -1);
824
+ if (tag !== "Object") return tag;
825
+ const constructor = value.constructor;
826
+ if (typeof constructor === "function" && constructor.name !== "" && constructor.name !== "Object") return constructor.name;
827
+ return "Object";
828
+ }
578
829
 
579
830
  //#endregion
580
831
  //#region src/types.ts
@@ -587,6 +838,7 @@ const ROOT_KEY = "@@crdt/root";
587
838
  //#endregion
588
839
  //#region src/patch.ts
589
840
  const DEFAULT_LCS_MAX_CELLS = 25e4;
841
+ const LINEAR_LCS_MATRIX_BASE_CASE_MAX_CELLS = 4096;
590
842
  /** Structured compile error used to map patch validation failures to typed reasons. */
591
843
  var PatchCompileError = class extends Error {
592
844
  reason;
@@ -677,22 +929,37 @@ function getAtJson(base, path) {
677
929
  * @returns An array of `IntentOp` ready for `applyIntentsToCrdt`.
678
930
  */
679
931
  function compileJsonPatchToIntent(baseJson, patch, options = {}) {
932
+ const internalOptions = options;
680
933
  const semantics = options.semantics ?? "sequential";
934
+ const opIndexOffset = internalOptions.opIndexOffset ?? 0;
681
935
  let workingBase = baseJson;
682
- const pointerCache = /* @__PURE__ */ new Map();
936
+ const pointerCache = internalOptions.pointerCache ?? /* @__PURE__ */ new Map();
683
937
  const intents = [];
684
938
  for (let opIndex = 0; opIndex < patch.length; opIndex++) {
685
939
  const op = patch[opIndex];
940
+ const absoluteOpIndex = opIndex + opIndexOffset;
686
941
  const compileBase = semantics === "sequential" ? workingBase : baseJson;
687
- intents.push(...compileSingleOp(compileBase, op, opIndex, semantics, pointerCache));
688
- if (semantics === "sequential") workingBase = applyPatchOpToJsonWithStructuralSharing(workingBase, op, opIndex, pointerCache);
942
+ intents.push(...compileSingleOp(compileBase, op, absoluteOpIndex, semantics, pointerCache));
943
+ if (semantics === "sequential") workingBase = applyPatchOpToJsonWithStructuralSharing(workingBase, op, absoluteOpIndex, pointerCache);
689
944
  }
690
945
  return intents;
691
946
  }
947
+ /** Compile a single JSON Patch operation into CRDT intents. */
948
+ function compileJsonPatchOpToIntent(baseJson, op, options = {}) {
949
+ const internalOptions = options;
950
+ const semantics = options.semantics ?? "sequential";
951
+ const pointerCache = internalOptions.pointerCache ?? /* @__PURE__ */ new Map();
952
+ return compileSingleOp(baseJson, op, internalOptions.opIndexOffset ?? 0, semantics, pointerCache);
953
+ }
692
954
  /**
693
955
  * Compute a JSON Patch delta between two JSON values.
694
956
  * By default arrays use a deterministic LCS strategy.
695
957
  * Pass `{ arrayStrategy: "atomic" }` for single-op array replacement.
958
+ * Pass `{ arrayStrategy: "lcs-linear" }` for a lower-memory LCS variant.
959
+ * Use `lcsLinearMaxCells` to optionally cap worst-case `lcs-linear` work and
960
+ * fall back to an atomic array replacement for very large unmatched windows.
961
+ * Pass `{ emitMoves: true }` or `{ emitCopies: true }` to opt into RFC 6902
962
+ * move/copy emission when a deterministic rewrite is available.
696
963
  * @param base - The original JSON value.
697
964
  * @param next - The target JSON value.
698
965
  * @param options - Diff options.
@@ -707,175 +974,457 @@ function diffJsonPatch(base, next, options = {}) {
707
974
  return ops;
708
975
  }
709
976
  function diffValue(path, base, next, ops, options) {
710
- if (jsonEquals(base, next)) return;
711
- if (Array.isArray(base) || Array.isArray(next)) {
712
- if ((options.arrayStrategy ?? "lcs") === "lcs" && Array.isArray(base) && Array.isArray(next)) {
713
- if (!diffArray(path, base, next, ops, options.lcsMaxCells)) ops.push({
977
+ const stack = [{
978
+ kind: "value",
979
+ base,
980
+ next
981
+ }];
982
+ while (stack.length > 0) {
983
+ const frame = stack.pop();
984
+ if (frame.kind === "path-pop") {
985
+ path.pop();
986
+ continue;
987
+ }
988
+ if (frame.kind === "object") {
989
+ if (frame.index >= frame.sharedKeys.length) continue;
990
+ const key = frame.sharedKeys[frame.index];
991
+ stack.push({
992
+ kind: "object",
993
+ base: frame.base,
994
+ next: frame.next,
995
+ sharedKeys: frame.sharedKeys,
996
+ index: frame.index + 1
997
+ });
998
+ path.push(key);
999
+ stack.push({ kind: "path-pop" });
1000
+ stack.push({
1001
+ kind: "value",
1002
+ base: frame.base[key],
1003
+ next: frame.next[key]
1004
+ });
1005
+ continue;
1006
+ }
1007
+ assertTraversalDepth(path.length);
1008
+ if (frame.base === frame.next) continue;
1009
+ const baseIsArray = Array.isArray(frame.base);
1010
+ const nextIsArray = Array.isArray(frame.next);
1011
+ if (baseIsArray || nextIsArray) {
1012
+ if (!baseIsArray || !nextIsArray) {
1013
+ ops.push({
1014
+ op: "replace",
1015
+ path: stringifyJsonPointer(path),
1016
+ value: frame.next
1017
+ });
1018
+ continue;
1019
+ }
1020
+ if (jsonEquals(frame.base, frame.next)) continue;
1021
+ const arrayStrategy = options.arrayStrategy ?? "lcs";
1022
+ if (arrayStrategy === "lcs") {
1023
+ if (!diffArrayWithLcsMatrix(path, frame.base, frame.next, ops, options)) ops.push({
1024
+ op: "replace",
1025
+ path: stringifyJsonPointer(path),
1026
+ value: frame.next
1027
+ });
1028
+ continue;
1029
+ }
1030
+ if (arrayStrategy === "lcs-linear") {
1031
+ if (!diffArrayWithLinearLcs(path, frame.base, frame.next, ops, options)) ops.push({
1032
+ op: "replace",
1033
+ path: stringifyJsonPointer(path),
1034
+ value: frame.next
1035
+ });
1036
+ continue;
1037
+ }
1038
+ ops.push({
714
1039
  op: "replace",
715
1040
  path: stringifyJsonPointer(path),
716
- value: next
1041
+ value: frame.next
717
1042
  });
718
- return;
1043
+ continue;
719
1044
  }
720
- ops.push({
721
- op: "replace",
722
- path: stringifyJsonPointer(path),
723
- value: next
724
- });
725
- return;
726
- }
727
- if (!isPlainObject(base) || !isPlainObject(next)) {
728
- ops.push({
729
- op: "replace",
730
- path: stringifyJsonPointer(path),
731
- value: next
1045
+ const baseIsObject = isPlainObject(frame.base);
1046
+ const nextIsObject = isPlainObject(frame.next);
1047
+ if (!baseIsObject || !nextIsObject) {
1048
+ ops.push({
1049
+ op: "replace",
1050
+ path: stringifyJsonPointer(path),
1051
+ value: frame.next
1052
+ });
1053
+ continue;
1054
+ }
1055
+ const { sharedKeys, baseOnlyKeys, nextOnlyKeys } = collectObjectKeys(frame.base, frame.next);
1056
+ if (!(baseOnlyKeys.length > 0 || nextOnlyKeys.length > 0) && (path.length === 0 || sharedKeys.length > 1) && jsonEquals(frame.base, frame.next)) continue;
1057
+ emitObjectStructuralOps(path, frame.base, frame.next, sharedKeys, baseOnlyKeys, nextOnlyKeys, ops, options);
1058
+ if (sharedKeys.length > 0) stack.push({
1059
+ kind: "object",
1060
+ base: frame.base,
1061
+ next: frame.next,
1062
+ sharedKeys,
1063
+ index: 0
732
1064
  });
733
- return;
734
1065
  }
1066
+ }
1067
+ function collectObjectKeys(base, next) {
735
1068
  const baseKeys = Object.keys(base).sort();
736
1069
  const nextKeys = Object.keys(next).sort();
1070
+ const baseOnlyKeys = [];
1071
+ const nextOnlyKeys = [];
1072
+ const sharedKeys = [];
737
1073
  let baseIndex = 0;
738
1074
  let nextIndex = 0;
739
1075
  while (baseIndex < baseKeys.length && nextIndex < nextKeys.length) {
740
1076
  const baseKey = baseKeys[baseIndex];
741
1077
  const nextKey = nextKeys[nextIndex];
742
1078
  if (baseKey === nextKey) {
1079
+ sharedKeys.push(baseKey);
743
1080
  baseIndex += 1;
744
1081
  nextIndex += 1;
745
1082
  continue;
746
1083
  }
747
1084
  if (baseKey < nextKey) {
1085
+ baseOnlyKeys.push(baseKey);
1086
+ baseIndex += 1;
1087
+ continue;
1088
+ }
1089
+ nextOnlyKeys.push(nextKey);
1090
+ nextIndex += 1;
1091
+ }
1092
+ while (baseIndex < baseKeys.length) {
1093
+ baseOnlyKeys.push(baseKeys[baseIndex]);
1094
+ baseIndex += 1;
1095
+ }
1096
+ while (nextIndex < nextKeys.length) {
1097
+ nextOnlyKeys.push(nextKeys[nextIndex]);
1098
+ nextIndex += 1;
1099
+ }
1100
+ return {
1101
+ sharedKeys,
1102
+ baseOnlyKeys,
1103
+ nextOnlyKeys
1104
+ };
1105
+ }
1106
+ function emitObjectStructuralOps(path, base, next, sharedKeys, baseOnlyKeys, nextOnlyKeys, ops, options) {
1107
+ if (!options.emitMoves && !options.emitCopies) {
1108
+ for (const baseKey of baseOnlyKeys) {
748
1109
  path.push(baseKey);
749
1110
  ops.push({
750
1111
  op: "remove",
751
1112
  path: stringifyJsonPointer(path)
752
1113
  });
753
1114
  path.pop();
754
- baseIndex += 1;
755
- continue;
756
1115
  }
757
- nextIndex += 1;
1116
+ for (const nextKey of nextOnlyKeys) {
1117
+ path.push(nextKey);
1118
+ ops.push({
1119
+ op: "add",
1120
+ path: stringifyJsonPointer(path),
1121
+ value: next[nextKey]
1122
+ });
1123
+ path.pop();
1124
+ }
1125
+ return;
758
1126
  }
759
- while (baseIndex < baseKeys.length) {
760
- const baseKey = baseKeys[baseIndex];
761
- path.push(baseKey);
762
- ops.push({
763
- op: "remove",
764
- path: stringifyJsonPointer(path)
765
- });
766
- path.pop();
767
- baseIndex += 1;
1127
+ const structuralKeyCache = /* @__PURE__ */ new WeakMap();
1128
+ const matchedMoveSources = /* @__PURE__ */ new Set();
1129
+ const moveTargets = /* @__PURE__ */ new Map();
1130
+ if (options.emitMoves) {
1131
+ const moveSourceBuckets = /* @__PURE__ */ new Map();
1132
+ for (const baseKey of baseOnlyKeys) insertObjectSourceBucket(moveSourceBuckets, baseKey, base[baseKey], structuralKeyCache);
1133
+ for (const nextKey of nextOnlyKeys) {
1134
+ const bucket = moveSourceBuckets.get(stableJsonValueKey(next[nextKey], structuralKeyCache));
1135
+ if (!bucket) continue;
1136
+ if (bucket.length > 0) {
1137
+ const candidate = bucket.shift();
1138
+ matchedMoveSources.add(candidate);
1139
+ moveTargets.set(nextKey, candidate);
1140
+ }
1141
+ }
768
1142
  }
769
- baseIndex = 0;
770
- nextIndex = 0;
771
- while (baseIndex < baseKeys.length && nextIndex < nextKeys.length) {
772
- const baseKey = baseKeys[baseIndex];
773
- const nextKey = nextKeys[nextIndex];
774
- if (baseKey === nextKey) {
775
- baseIndex += 1;
776
- nextIndex += 1;
1143
+ const copySourceBuckets = /* @__PURE__ */ new Map();
1144
+ for (const key of sharedKeys) {
1145
+ if (!jsonEquals(base[key], next[key])) continue;
1146
+ insertObjectSourceBucket(copySourceBuckets, key, base[key], structuralKeyCache);
1147
+ }
1148
+ for (const nextKey of nextOnlyKeys) {
1149
+ path.push(nextKey);
1150
+ const targetPath = stringifyJsonPointer(path);
1151
+ path.pop();
1152
+ const moveSource = moveTargets.get(nextKey);
1153
+ if (moveSource !== void 0) {
1154
+ path.push(moveSource);
1155
+ const fromPath = stringifyJsonPointer(path);
1156
+ path.pop();
1157
+ ops.push({
1158
+ op: "move",
1159
+ from: fromPath,
1160
+ path: targetPath
1161
+ });
1162
+ insertObjectSourceBucket(copySourceBuckets, nextKey, next[nextKey], structuralKeyCache);
777
1163
  continue;
778
1164
  }
779
- if (baseKey < nextKey) {
780
- baseIndex += 1;
781
- continue;
1165
+ if (options.emitCopies) {
1166
+ const copySource = findObjectCopySource(copySourceBuckets, next[nextKey], structuralKeyCache);
1167
+ if (copySource !== void 0) {
1168
+ path.push(copySource);
1169
+ const fromPath = stringifyJsonPointer(path);
1170
+ path.pop();
1171
+ ops.push({
1172
+ op: "copy",
1173
+ from: fromPath,
1174
+ path: targetPath
1175
+ });
1176
+ insertObjectSourceBucket(copySourceBuckets, nextKey, next[nextKey], structuralKeyCache);
1177
+ continue;
1178
+ }
782
1179
  }
783
- path.push(nextKey);
784
1180
  ops.push({
785
1181
  op: "add",
786
- path: stringifyJsonPointer(path),
1182
+ path: targetPath,
787
1183
  value: next[nextKey]
788
1184
  });
789
- path.pop();
790
- nextIndex += 1;
1185
+ insertObjectSourceBucket(copySourceBuckets, nextKey, next[nextKey], structuralKeyCache);
791
1186
  }
792
- while (nextIndex < nextKeys.length) {
793
- const nextKey = nextKeys[nextIndex];
794
- path.push(nextKey);
1187
+ for (const baseKey of baseOnlyKeys) {
1188
+ if (matchedMoveSources.has(baseKey)) continue;
1189
+ path.push(baseKey);
795
1190
  ops.push({
796
- op: "add",
797
- path: stringifyJsonPointer(path),
798
- value: next[nextKey]
1191
+ op: "remove",
1192
+ path: stringifyJsonPointer(path)
799
1193
  });
800
1194
  path.pop();
801
- nextIndex += 1;
802
1195
  }
803
- baseIndex = 0;
804
- nextIndex = 0;
805
- while (baseIndex < baseKeys.length && nextIndex < nextKeys.length) {
806
- const baseKey = baseKeys[baseIndex];
807
- const nextKey = nextKeys[nextIndex];
808
- if (baseKey === nextKey) {
809
- path.push(baseKey);
810
- diffValue(path, base[baseKey], next[nextKey], ops, options);
811
- path.pop();
812
- baseIndex += 1;
813
- nextIndex += 1;
1196
+ }
1197
+ function insertObjectSourceBucket(buckets, key, value, structuralKeyCache) {
1198
+ const bucketKey = stableJsonValueKey(value, structuralKeyCache);
1199
+ let bucket = buckets.get(bucketKey);
1200
+ if (!bucket) {
1201
+ bucket = [];
1202
+ buckets.set(bucketKey, bucket);
1203
+ }
1204
+ insertSortedKey(bucket, key);
1205
+ }
1206
+ function findObjectCopySource(copySourceBuckets, target, structuralKeyCache) {
1207
+ return copySourceBuckets.get(stableJsonValueKey(target, structuralKeyCache))?.[0];
1208
+ }
1209
+ function insertSortedKey(keys, key) {
1210
+ let low = 0;
1211
+ let high = keys.length;
1212
+ while (low < high) {
1213
+ const mid = Math.floor((low + high) / 2);
1214
+ if (keys[mid] < key) low = mid + 1;
1215
+ else high = mid;
1216
+ }
1217
+ keys.splice(low, 0, key);
1218
+ }
1219
+ function diffArrayWithLcsMatrix(path, base, next, ops, options) {
1220
+ const window = trimEqualArrayEdges(base, next);
1221
+ const baseStart = window.baseStart;
1222
+ const nextStart = window.nextStart;
1223
+ const n = window.unmatchedBaseLength;
1224
+ const m = window.unmatchedNextLength;
1225
+ if (!shouldUseLcsDiff(n, m, options.lcsMaxCells)) return false;
1226
+ if (n === 0 && m === 0) return true;
1227
+ const steps = [];
1228
+ buildArrayEditScriptWithMatrix(base, baseStart, baseStart + n, next, nextStart, nextStart + m, steps);
1229
+ pushArrayPatchOps(path, window.prefixLength, steps, ops, base, options);
1230
+ return true;
1231
+ }
1232
+ function diffArrayWithLinearLcs(path, base, next, ops, options) {
1233
+ const window = trimEqualArrayEdges(base, next);
1234
+ if (!shouldUseLinearLcsDiff(window.unmatchedBaseLength, window.unmatchedNextLength, options)) return false;
1235
+ const steps = [];
1236
+ buildArrayEditScriptLinearSpace(base, window.baseStart, window.baseStart + window.unmatchedBaseLength, next, window.nextStart, window.nextStart + window.unmatchedNextLength, steps);
1237
+ pushArrayPatchOps(path, window.prefixLength, steps, ops, base, options);
1238
+ return true;
1239
+ }
1240
+ function trimEqualArrayEdges(base, next) {
1241
+ const baseLength = base.length;
1242
+ const nextLength = next.length;
1243
+ let prefixLength = 0;
1244
+ while (prefixLength < baseLength && prefixLength < nextLength && jsonEquals(base[prefixLength], next[prefixLength])) prefixLength += 1;
1245
+ let suffixLength = 0;
1246
+ while (suffixLength < baseLength - prefixLength && suffixLength < nextLength - prefixLength && jsonEquals(base[baseLength - 1 - suffixLength], next[nextLength - 1 - suffixLength])) suffixLength += 1;
1247
+ return {
1248
+ baseStart: prefixLength,
1249
+ nextStart: prefixLength,
1250
+ prefixLength,
1251
+ unmatchedBaseLength: baseLength - prefixLength - suffixLength,
1252
+ unmatchedNextLength: nextLength - prefixLength - suffixLength
1253
+ };
1254
+ }
1255
+ function buildArrayEditScriptLinearSpace(base, baseStart, baseEnd, next, nextStart, nextEnd, steps) {
1256
+ const unmatchedBaseLength = baseEnd - baseStart;
1257
+ const unmatchedNextLength = nextEnd - nextStart;
1258
+ if (unmatchedBaseLength === 0) {
1259
+ for (let nextIndex = nextStart; nextIndex < nextEnd; nextIndex++) steps.push({
1260
+ kind: "add",
1261
+ value: next[nextIndex]
1262
+ });
1263
+ return;
1264
+ }
1265
+ if (unmatchedNextLength === 0) {
1266
+ for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) steps.push({ kind: "remove" });
1267
+ return;
1268
+ }
1269
+ if (unmatchedBaseLength === 1) {
1270
+ pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, steps);
1271
+ return;
1272
+ }
1273
+ if (unmatchedNextLength === 1) {
1274
+ pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, steps);
1275
+ return;
1276
+ }
1277
+ if (shouldUseMatrixBaseCase(unmatchedBaseLength, unmatchedNextLength)) {
1278
+ buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStart, nextEnd, steps);
1279
+ return;
1280
+ }
1281
+ const baseMid = baseStart + Math.floor(unmatchedBaseLength / 2);
1282
+ const forwardScores = computeLcsPrefixLengths(base, baseStart, baseMid, next, nextStart, nextEnd);
1283
+ const reverseScores = computeLcsSuffixLengths(base, baseMid, baseEnd, next, nextStart, nextEnd);
1284
+ let bestOffset = 0;
1285
+ let bestScore = Number.NEGATIVE_INFINITY;
1286
+ for (let offset = 0; offset <= unmatchedNextLength; offset++) {
1287
+ const score = forwardScores[offset] + reverseScores[offset];
1288
+ if (score > bestScore) {
1289
+ bestScore = score;
1290
+ bestOffset = offset;
1291
+ }
1292
+ }
1293
+ const nextMid = nextStart + bestOffset;
1294
+ buildArrayEditScriptLinearSpace(base, baseStart, baseMid, next, nextStart, nextMid, steps);
1295
+ buildArrayEditScriptLinearSpace(base, baseMid, baseEnd, next, nextMid, nextEnd, steps);
1296
+ }
1297
+ function pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, steps) {
1298
+ const matchIndex = findFirstMatchingIndexInNext(base[baseStart], next, nextStart, nextEnd);
1299
+ if (matchIndex === -1) {
1300
+ steps.push({ kind: "remove" });
1301
+ for (let nextIndex = nextStart; nextIndex < nextEnd; nextIndex++) steps.push({
1302
+ kind: "add",
1303
+ value: next[nextIndex]
1304
+ });
1305
+ return;
1306
+ }
1307
+ for (let nextIndex = nextStart; nextIndex < matchIndex; nextIndex++) steps.push({
1308
+ kind: "add",
1309
+ value: next[nextIndex]
1310
+ });
1311
+ steps.push({ kind: "equal" });
1312
+ for (let nextIndex = matchIndex + 1; nextIndex < nextEnd; nextIndex++) steps.push({
1313
+ kind: "add",
1314
+ value: next[nextIndex]
1315
+ });
1316
+ }
1317
+ function pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, steps) {
1318
+ const matchIndex = findFirstMatchingIndexInBase(next[nextStart], base, baseStart, baseEnd);
1319
+ if (matchIndex === -1) {
1320
+ for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) steps.push({ kind: "remove" });
1321
+ steps.push({
1322
+ kind: "add",
1323
+ value: next[nextStart]
1324
+ });
1325
+ return;
1326
+ }
1327
+ for (let baseIndex = baseStart; baseIndex < matchIndex; baseIndex++) steps.push({ kind: "remove" });
1328
+ steps.push({ kind: "equal" });
1329
+ for (let baseIndex = matchIndex + 1; baseIndex < baseEnd; baseIndex++) steps.push({ kind: "remove" });
1330
+ }
1331
+ function findFirstMatchingIndexInNext(target, next, nextStart, nextEnd) {
1332
+ for (let nextIndex = nextStart; nextIndex < nextEnd; nextIndex++) if (jsonEquals(target, next[nextIndex])) return nextIndex;
1333
+ return -1;
1334
+ }
1335
+ function findFirstMatchingIndexInBase(target, base, baseStart, baseEnd) {
1336
+ for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) if (jsonEquals(target, base[baseIndex])) return baseIndex;
1337
+ return -1;
1338
+ }
1339
+ function shouldUseMatrixBaseCase(baseLength, nextLength) {
1340
+ return (baseLength + 1) * (nextLength + 1) <= LINEAR_LCS_MATRIX_BASE_CASE_MAX_CELLS;
1341
+ }
1342
+ function buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStart, nextEnd, steps) {
1343
+ const unmatchedBaseLength = baseEnd - baseStart;
1344
+ const unmatchedNextLength = nextEnd - nextStart;
1345
+ const lcs = Array.from({ length: unmatchedBaseLength + 1 }, () => Array(unmatchedNextLength + 1).fill(0));
1346
+ for (let baseOffset = unmatchedBaseLength - 1; baseOffset >= 0; baseOffset--) for (let nextOffset = unmatchedNextLength - 1; nextOffset >= 0; nextOffset--) if (jsonEquals(base[baseStart + baseOffset], next[nextStart + nextOffset])) lcs[baseOffset][nextOffset] = 1 + lcs[baseOffset + 1][nextOffset + 1];
1347
+ else lcs[baseOffset][nextOffset] = Math.max(lcs[baseOffset + 1][nextOffset], lcs[baseOffset][nextOffset + 1]);
1348
+ let baseOffset = 0;
1349
+ let nextOffset = 0;
1350
+ while (baseOffset < unmatchedBaseLength || nextOffset < unmatchedNextLength) {
1351
+ if (baseOffset < unmatchedBaseLength && nextOffset < unmatchedNextLength && jsonEquals(base[baseStart + baseOffset], next[nextStart + nextOffset])) {
1352
+ steps.push({ kind: "equal" });
1353
+ baseOffset += 1;
1354
+ nextOffset += 1;
814
1355
  continue;
815
1356
  }
816
- if (baseKey < nextKey) {
817
- baseIndex += 1;
1357
+ const lcsDown = baseOffset < unmatchedBaseLength ? lcs[baseOffset + 1][nextOffset] : -1;
1358
+ const lcsRight = nextOffset < unmatchedNextLength ? lcs[baseOffset][nextOffset + 1] : -1;
1359
+ if (nextOffset < unmatchedNextLength && (baseOffset === unmatchedBaseLength || lcsRight > lcsDown)) {
1360
+ steps.push({
1361
+ kind: "add",
1362
+ value: next[nextStart + nextOffset]
1363
+ });
1364
+ nextOffset += 1;
818
1365
  continue;
819
1366
  }
820
- nextIndex += 1;
1367
+ if (baseOffset < unmatchedBaseLength) {
1368
+ steps.push({ kind: "remove" });
1369
+ baseOffset += 1;
1370
+ }
821
1371
  }
822
1372
  }
823
- function diffArray(path, base, next, ops, lcsMaxCells) {
824
- const baseLength = base.length;
825
- const nextLength = next.length;
826
- let prefix = 0;
827
- while (prefix < baseLength && prefix < nextLength && jsonEquals(base[prefix], next[prefix])) prefix += 1;
828
- let suffix = 0;
829
- while (suffix < baseLength - prefix && suffix < nextLength - prefix && jsonEquals(base[baseLength - 1 - suffix], next[nextLength - 1 - suffix])) suffix += 1;
830
- const baseStart = prefix;
831
- const nextStart = prefix;
832
- const n = baseLength - prefix - suffix;
833
- const m = nextLength - prefix - suffix;
834
- if (!shouldUseLcsDiff(n, m, lcsMaxCells)) return false;
835
- if (n === 0 && m === 0) return true;
836
- const lcs = Array.from({ length: n + 1 }, () => Array(m + 1).fill(0));
837
- for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--) if (jsonEquals(base[baseStart + i], next[nextStart + j])) lcs[i][j] = 1 + lcs[i + 1][j + 1];
838
- else lcs[i][j] = Math.max(lcs[i + 1][j], lcs[i][j + 1]);
1373
+ function computeLcsPrefixLengths(base, baseStart, baseEnd, next, nextStart, nextEnd) {
1374
+ const unmatchedNextLength = nextEnd - nextStart;
1375
+ let previousRow = new Int32Array(unmatchedNextLength + 1);
1376
+ let currentRow = new Int32Array(unmatchedNextLength + 1);
1377
+ for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) {
1378
+ for (let nextOffset = 0; nextOffset < unmatchedNextLength; nextOffset++) if (jsonEquals(base[baseIndex], next[nextStart + nextOffset])) currentRow[nextOffset + 1] = previousRow[nextOffset] + 1;
1379
+ else currentRow[nextOffset + 1] = Math.max(previousRow[nextOffset + 1], currentRow[nextOffset]);
1380
+ const nextPreviousRow = currentRow;
1381
+ currentRow = previousRow;
1382
+ previousRow = nextPreviousRow;
1383
+ currentRow.fill(0);
1384
+ }
1385
+ return previousRow;
1386
+ }
1387
+ function computeLcsSuffixLengths(base, baseStart, baseEnd, next, nextStart, nextEnd) {
1388
+ const unmatchedNextLength = nextEnd - nextStart;
1389
+ let previousRow = new Int32Array(unmatchedNextLength + 1);
1390
+ let currentRow = new Int32Array(unmatchedNextLength + 1);
1391
+ for (let baseIndex = baseEnd - 1; baseIndex >= baseStart; baseIndex--) {
1392
+ for (let nextOffset = unmatchedNextLength - 1; nextOffset >= 0; nextOffset--) if (jsonEquals(base[baseIndex], next[nextStart + nextOffset])) currentRow[nextOffset] = previousRow[nextOffset + 1] + 1;
1393
+ else currentRow[nextOffset] = Math.max(previousRow[nextOffset], currentRow[nextOffset + 1]);
1394
+ const nextPreviousRow = currentRow;
1395
+ currentRow = previousRow;
1396
+ previousRow = nextPreviousRow;
1397
+ currentRow.fill(0);
1398
+ }
1399
+ return previousRow;
1400
+ }
1401
+ function pushArrayPatchOps(path, startIndex, steps, ops, base, options) {
839
1402
  const localOps = [];
840
- let i = 0;
841
- let j = 0;
842
- let index = prefix;
843
- while (i < n || j < m) {
844
- if (i < n && j < m && jsonEquals(base[baseStart + i], next[nextStart + j])) {
845
- i += 1;
846
- j += 1;
1403
+ let index = startIndex;
1404
+ for (const step of steps) {
1405
+ if (step.kind === "equal") {
847
1406
  index += 1;
848
1407
  continue;
849
1408
  }
850
- const lcsDown = i < n ? lcs[i + 1][j] : -1;
851
- const lcsRight = j < m ? lcs[i][j + 1] : -1;
852
- if (j < m && (i === n || lcsRight > lcsDown)) {
853
- const indexSegment = String(index);
854
- path.push(indexSegment);
1409
+ const indexSegment = String(index);
1410
+ path.push(indexSegment);
1411
+ if (step.kind === "add") {
855
1412
  localOps.push({
856
1413
  op: "add",
857
1414
  path: stringifyJsonPointer(path),
858
- value: next[nextStart + j]
1415
+ value: step.value
859
1416
  });
860
- path.pop();
861
- j += 1;
862
1417
  index += 1;
863
- continue;
864
- }
865
- if (i < n) {
866
- const indexSegment = String(index);
867
- path.push(indexSegment);
868
- localOps.push({
869
- op: "remove",
870
- path: stringifyJsonPointer(path)
871
- });
872
1418
  path.pop();
873
- i += 1;
874
1419
  continue;
875
1420
  }
1421
+ localOps.push({
1422
+ op: "remove",
1423
+ path: stringifyJsonPointer(path)
1424
+ });
1425
+ path.pop();
876
1426
  }
877
- ops.push(...compactArrayOps(localOps));
878
- return true;
1427
+ ops.push(...finalizeArrayOps(path, base, localOps, options));
879
1428
  }
880
1429
  function shouldUseLcsDiff(baseLength, nextLength, lcsMaxCells) {
881
1430
  if (lcsMaxCells === Number.POSITIVE_INFINITY) return true;
@@ -883,6 +1432,169 @@ function shouldUseLcsDiff(baseLength, nextLength, lcsMaxCells) {
883
1432
  if (!Number.isFinite(cap) || cap < 1) return false;
884
1433
  return (baseLength + 1) * (nextLength + 1) <= cap;
885
1434
  }
1435
+ function shouldUseLinearLcsDiff(baseLength, nextLength, options) {
1436
+ const cap = options.lcsLinearMaxCells;
1437
+ if (cap === void 0 || cap === Number.POSITIVE_INFINITY) return true;
1438
+ if (!Number.isFinite(cap) || cap < 1) return false;
1439
+ return (baseLength + 1) * (nextLength + 1) <= cap;
1440
+ }
1441
+ function finalizeArrayOps(arrayPath, base, ops, options) {
1442
+ if (ops.length === 0) return [];
1443
+ if (!options.emitMoves && !options.emitCopies) return compactArrayOps(ops);
1444
+ const out = [];
1445
+ const working = createArrayRewriteState(base);
1446
+ for (let i = 0; i < ops.length; i++) {
1447
+ const op = ops[i];
1448
+ const next = ops[i + 1];
1449
+ if (op.op === "remove" && next && next.op === "add") {
1450
+ const valuesMatch = working.entries[getArrayOpIndex(op.path, arrayPath)].key === getArrayRewriteValueKey(working, next.value);
1451
+ if (op.path === next.path) {
1452
+ const replaceOp = {
1453
+ op: "replace",
1454
+ path: op.path,
1455
+ value: next.value
1456
+ };
1457
+ out.push(replaceOp);
1458
+ applyArrayOptimizationOp(working, replaceOp, arrayPath);
1459
+ i += 1;
1460
+ continue;
1461
+ }
1462
+ if (options.emitMoves && valuesMatch) {
1463
+ const moveOp = {
1464
+ op: "move",
1465
+ from: op.path,
1466
+ path: next.path
1467
+ };
1468
+ out.push(moveOp);
1469
+ applyArrayOptimizationOp(working, moveOp, arrayPath);
1470
+ i += 1;
1471
+ continue;
1472
+ }
1473
+ if (valuesMatch) {
1474
+ out.push(op);
1475
+ applyArrayOptimizationOp(working, op, arrayPath);
1476
+ out.push(next);
1477
+ applyArrayOptimizationOp(working, next, arrayPath);
1478
+ i += 1;
1479
+ continue;
1480
+ }
1481
+ out.push(op);
1482
+ applyArrayOptimizationOp(working, op, arrayPath);
1483
+ continue;
1484
+ }
1485
+ if (op.op === "add" && next && next.op === "remove") {
1486
+ const targetIndex = getArrayOpIndex(op.path, arrayPath);
1487
+ const removeIndex = getArrayOpIndex(next.path, arrayPath);
1488
+ const sourceIndex = removeIndex - (targetIndex <= removeIndex ? 1 : 0);
1489
+ const matchesPendingRemove = sourceIndex >= 0 && sourceIndex < working.entries.length && working.entries[sourceIndex].key === getArrayRewriteValueKey(working, op.value);
1490
+ if (options.emitMoves && matchesPendingRemove) {
1491
+ const moveOp = {
1492
+ op: "move",
1493
+ from: stringifyJsonPointer([...arrayPath, String(sourceIndex)]),
1494
+ path: op.path
1495
+ };
1496
+ out.push(moveOp);
1497
+ applyArrayOptimizationOp(working, moveOp, arrayPath);
1498
+ i += 1;
1499
+ continue;
1500
+ }
1501
+ if (matchesPendingRemove) {
1502
+ out.push(op);
1503
+ applyArrayOptimizationOp(working, op, arrayPath);
1504
+ out.push(next);
1505
+ applyArrayOptimizationOp(working, next, arrayPath);
1506
+ i += 1;
1507
+ continue;
1508
+ }
1509
+ }
1510
+ if (op.op === "add" && options.emitCopies) {
1511
+ const copySourceIndex = findArrayCopySourceIndex(working, op.value);
1512
+ if (copySourceIndex !== -1) {
1513
+ const copyOp = {
1514
+ op: "copy",
1515
+ from: stringifyJsonPointer([...arrayPath, String(copySourceIndex)]),
1516
+ path: op.path
1517
+ };
1518
+ out.push(copyOp);
1519
+ applyArrayOptimizationOp(working, copyOp, arrayPath);
1520
+ continue;
1521
+ }
1522
+ }
1523
+ out.push(op);
1524
+ applyArrayOptimizationOp(working, op, arrayPath);
1525
+ }
1526
+ return out;
1527
+ }
1528
+ /** @internal Stable structural fingerprint used for deterministic diff rewrites. */
1529
+ function stableJsonValueKey(value, structuralKeyCache) {
1530
+ if (value !== null && typeof value === "object") {
1531
+ const cachedValue = structuralKeyCache?.get(value);
1532
+ if (cachedValue !== void 0) return cachedValue;
1533
+ }
1534
+ const stack = [{
1535
+ kind: "value",
1536
+ value,
1537
+ depth: 0
1538
+ }];
1539
+ const results = [];
1540
+ while (stack.length > 0) {
1541
+ const frame = stack.pop();
1542
+ if (frame.kind === "array") {
1543
+ const stableKey = `[${results.splice(frame.startIndex).join(",")}]`;
1544
+ structuralKeyCache?.set(frame.value, stableKey);
1545
+ results.push(stableKey);
1546
+ continue;
1547
+ }
1548
+ if (frame.kind === "object") {
1549
+ const childParts = results.splice(frame.startIndex);
1550
+ const stableKey = `{${frame.keys.map((key, index) => `${JSON.stringify(key)}:${childParts[index]}`).join(",")}}`;
1551
+ structuralKeyCache?.set(frame.value, stableKey);
1552
+ results.push(stableKey);
1553
+ continue;
1554
+ }
1555
+ assertTraversalDepth(frame.depth);
1556
+ if (frame.value === null || typeof frame.value !== "object") {
1557
+ results.push(JSON.stringify(frame.value));
1558
+ continue;
1559
+ }
1560
+ const cachedValue = structuralKeyCache?.get(frame.value);
1561
+ if (cachedValue !== void 0) {
1562
+ results.push(cachedValue);
1563
+ continue;
1564
+ }
1565
+ if (Array.isArray(frame.value)) {
1566
+ const startIndex = results.length;
1567
+ stack.push({
1568
+ kind: "array",
1569
+ value: frame.value,
1570
+ startIndex
1571
+ });
1572
+ for (let index = frame.value.length - 1; index >= 0; index--) stack.push({
1573
+ kind: "value",
1574
+ value: frame.value[index],
1575
+ depth: frame.depth + 1
1576
+ });
1577
+ continue;
1578
+ }
1579
+ const keys = Object.keys(frame.value).sort();
1580
+ const startIndex = results.length;
1581
+ stack.push({
1582
+ kind: "object",
1583
+ value: frame.value,
1584
+ keys,
1585
+ startIndex
1586
+ });
1587
+ for (let index = keys.length - 1; index >= 0; index--) {
1588
+ const key = keys[index];
1589
+ stack.push({
1590
+ kind: "value",
1591
+ value: frame.value[key],
1592
+ depth: frame.depth + 1
1593
+ });
1594
+ }
1595
+ }
1596
+ return results[0];
1597
+ }
886
1598
  function compactArrayOps(ops) {
887
1599
  const out = [];
888
1600
  for (let i = 0; i < ops.length; i++) {
@@ -899,28 +1611,174 @@ function compactArrayOps(ops) {
899
1611
  }
900
1612
  out.push(op);
901
1613
  }
902
- return out;
1614
+ return out;
1615
+ }
1616
+ function createArrayRewriteState(base) {
1617
+ const structuralKeyCache = /* @__PURE__ */ new WeakMap();
1618
+ const buckets = /* @__PURE__ */ new Map();
1619
+ return {
1620
+ entries: base.map((value, currentIndex) => {
1621
+ const entry = {
1622
+ value,
1623
+ key: stableJsonValueKey(value, structuralKeyCache),
1624
+ currentIndex,
1625
+ bucketIndex: -1
1626
+ };
1627
+ insertArrayRewriteBucketEntry(buckets, entry);
1628
+ return entry;
1629
+ }),
1630
+ buckets,
1631
+ structuralKeyCache
1632
+ };
1633
+ }
1634
+ function getArrayRewriteValueKey(state, value) {
1635
+ return stableJsonValueKey(value, state.structuralKeyCache);
1636
+ }
1637
+ function findArrayCopySourceIndex(state, value) {
1638
+ return state.buckets.get(getArrayRewriteValueKey(state, value))?.[0]?.currentIndex ?? -1;
1639
+ }
1640
+ function insertArrayRewriteBucketEntry(buckets, entry) {
1641
+ let bucket = buckets.get(entry.key);
1642
+ if (!bucket) {
1643
+ bucket = [];
1644
+ buckets.set(entry.key, bucket);
1645
+ }
1646
+ let low = 0;
1647
+ let high = bucket.length;
1648
+ while (low < high) {
1649
+ const mid = Math.floor((low + high) / 2);
1650
+ if (bucket[mid].currentIndex < entry.currentIndex) low = mid + 1;
1651
+ else high = mid;
1652
+ }
1653
+ bucket.splice(low, 0, entry);
1654
+ reindexArrayRewriteBucketPositions(bucket, low);
1655
+ }
1656
+ function removeArrayRewriteBucketEntry(buckets, entry) {
1657
+ const bucket = buckets.get(entry.key);
1658
+ if (!bucket) return;
1659
+ const bucketIndex = entry.bucketIndex;
1660
+ if (bucketIndex < 0 || bucketIndex >= bucket.length || bucket[bucketIndex] !== entry) return;
1661
+ bucket.splice(bucketIndex, 1);
1662
+ if (bucket.length === 0) {
1663
+ buckets.delete(entry.key);
1664
+ entry.bucketIndex = -1;
1665
+ return;
1666
+ }
1667
+ entry.bucketIndex = -1;
1668
+ reindexArrayRewriteBucketPositions(bucket, bucketIndex);
1669
+ }
1670
+ function reindexArrayRewriteBucketPositions(bucket, startIndex) {
1671
+ for (let index = startIndex; index < bucket.length; index++) bucket[index].bucketIndex = index;
1672
+ }
1673
+ function reindexArrayRewriteEntries(entries, startIndex) {
1674
+ for (let index = startIndex; index < entries.length; index++) entries[index].currentIndex = index;
1675
+ }
1676
+ function getArrayOpIndex(ptr, arrayPath) {
1677
+ const parsed = parseJsonPointer(ptr);
1678
+ if (parsed.length !== arrayPath.length + 1) throw new Error(`Expected array operation under ${stringifyJsonPointer(arrayPath)}: ${ptr}`);
1679
+ for (let index = 0; index < arrayPath.length; index++) if (parsed[index] !== arrayPath[index]) throw new Error(`Expected array operation under ${stringifyJsonPointer(arrayPath)}: ${ptr}`);
1680
+ const token = parsed[arrayPath.length];
1681
+ if (!ARRAY_INDEX_TOKEN_PATTERN.test(token)) throw new Error(`Expected numeric array index at ${ptr}`);
1682
+ return Number(token);
1683
+ }
1684
+ function applyArrayOptimizationOp(working, op, arrayPath) {
1685
+ if (op.op === "add") {
1686
+ const index = getArrayOpIndex(op.path, arrayPath);
1687
+ const entry = {
1688
+ value: structuredClone(op.value),
1689
+ key: getArrayRewriteValueKey(working, op.value),
1690
+ currentIndex: index,
1691
+ bucketIndex: -1
1692
+ };
1693
+ working.entries.splice(index, 0, entry);
1694
+ reindexArrayRewriteEntries(working.entries, index + 1);
1695
+ insertArrayRewriteBucketEntry(working.buckets, entry);
1696
+ return;
1697
+ }
1698
+ if (op.op === "remove") {
1699
+ const index = getArrayOpIndex(op.path, arrayPath);
1700
+ const [removedEntry] = working.entries.splice(index, 1);
1701
+ if (removedEntry) removeArrayRewriteBucketEntry(working.buckets, removedEntry);
1702
+ reindexArrayRewriteEntries(working.entries, index);
1703
+ return;
1704
+ }
1705
+ if (op.op === "replace") {
1706
+ const index = getArrayOpIndex(op.path, arrayPath);
1707
+ const entry = working.entries[index];
1708
+ removeArrayRewriteBucketEntry(working.buckets, entry);
1709
+ entry.value = structuredClone(op.value);
1710
+ entry.key = getArrayRewriteValueKey(working, op.value);
1711
+ insertArrayRewriteBucketEntry(working.buckets, entry);
1712
+ return;
1713
+ }
1714
+ if (op.op === "copy") {
1715
+ const fromIndex = getArrayOpIndex(op.from, arrayPath);
1716
+ if (fromIndex < 0 || fromIndex >= working.entries.length) throw new Error(`applyArrayOptimizationOp: copy from index ${fromIndex} is out of bounds (length ${working.entries.length})`);
1717
+ const index = getArrayOpIndex(op.path, arrayPath);
1718
+ const source = working.entries[fromIndex];
1719
+ const entry = {
1720
+ value: structuredClone(source.value),
1721
+ key: source.key,
1722
+ currentIndex: index,
1723
+ bucketIndex: -1
1724
+ };
1725
+ working.entries.splice(index, 0, entry);
1726
+ reindexArrayRewriteEntries(working.entries, index + 1);
1727
+ insertArrayRewriteBucketEntry(working.buckets, entry);
1728
+ return;
1729
+ }
1730
+ if (op.op === "move") {
1731
+ const fromIndex = getArrayOpIndex(op.from, arrayPath);
1732
+ if (fromIndex < 0 || fromIndex >= working.entries.length) throw new Error(`applyArrayOptimizationOp: move from index ${fromIndex} is out of bounds (length ${working.entries.length})`);
1733
+ const [entry] = working.entries.splice(fromIndex, 1);
1734
+ if (!entry) throw new Error(`applyArrayOptimizationOp: move from index ${fromIndex} did not resolve`);
1735
+ removeArrayRewriteBucketEntry(working.buckets, entry);
1736
+ const index = getArrayOpIndex(op.path, arrayPath);
1737
+ working.entries.splice(index, 0, entry);
1738
+ reindexArrayRewriteEntries(working.entries, Math.min(fromIndex, index));
1739
+ insertArrayRewriteBucketEntry(working.buckets, entry);
1740
+ return;
1741
+ }
1742
+ throw new Error(`applyArrayOptimizationOp: unexpected op type "${op.op}"`);
903
1743
  }
904
1744
  function escapeJsonPointer(token) {
905
1745
  return token.replace(/~/g, "~0").replace(/\//g, "~1");
906
1746
  }
907
1747
  /** Deep equality check for JSON values (null-safe, handles arrays and objects). */
908
1748
  function jsonEquals(a, b) {
909
- if (a === b) return true;
910
- if (a === null || b === null) return false;
911
- if (Array.isArray(a) || Array.isArray(b)) {
912
- if (!Array.isArray(a) || !Array.isArray(b)) return false;
913
- if (a.length !== b.length) return false;
914
- for (let i = 0; i < a.length; i++) if (!jsonEquals(a[i], b[i])) return false;
915
- return true;
916
- }
917
- if (!isPlainObject(a) || !isPlainObject(b)) return false;
918
- const aKeys = Object.keys(a);
919
- const bKeys = Object.keys(b);
920
- if (aKeys.length !== bKeys.length) return false;
921
- for (const key of aKeys) {
922
- if (!hasOwn(b, key)) return false;
923
- if (!jsonEquals(a[key], b[key])) return false;
1749
+ const stack = [{
1750
+ left: a,
1751
+ right: b,
1752
+ depth: 0
1753
+ }];
1754
+ while (stack.length > 0) {
1755
+ const frame = stack.pop();
1756
+ assertTraversalDepth(frame.depth);
1757
+ if (frame.left === frame.right) continue;
1758
+ if (frame.left === null || frame.right === null) return false;
1759
+ if (Array.isArray(frame.left) || Array.isArray(frame.right)) {
1760
+ if (!Array.isArray(frame.left) || !Array.isArray(frame.right)) return false;
1761
+ if (frame.left.length !== frame.right.length) return false;
1762
+ for (let index = frame.left.length - 1; index >= 0; index--) stack.push({
1763
+ left: frame.left[index],
1764
+ right: frame.right[index],
1765
+ depth: frame.depth + 1
1766
+ });
1767
+ continue;
1768
+ }
1769
+ if (!isPlainObject(frame.left) || !isPlainObject(frame.right)) return false;
1770
+ const leftKeys = Object.keys(frame.left);
1771
+ const rightKeys = Object.keys(frame.right);
1772
+ if (leftKeys.length !== rightKeys.length) return false;
1773
+ for (let index = leftKeys.length - 1; index >= 0; index--) {
1774
+ const key = leftKeys[index];
1775
+ if (!hasOwn(frame.right, key)) return false;
1776
+ stack.push({
1777
+ left: frame.left[key],
1778
+ right: frame.right[key],
1779
+ depth: frame.depth + 1
1780
+ });
1781
+ }
924
1782
  }
925
1783
  return true;
926
1784
  }
@@ -1497,6 +2355,10 @@ function cloneNodeAtDepth(node, depth) {
1497
2355
  id: e.id,
1498
2356
  prev: e.prev,
1499
2357
  tombstone: e.tombstone,
2358
+ delDot: e.delDot ? {
2359
+ actor: e.delDot.actor,
2360
+ ctr: e.delDot.ctr
2361
+ } : void 0,
1500
2362
  value: cloneNodeAtDepth(e.value, depth + 1),
1501
2363
  insDot: {
1502
2364
  actor: e.insDot.actor,
@@ -1601,7 +2463,17 @@ function applyObjRemove(head, it, newDot) {
1601
2463
  objRemove(parentObj, it.key, d);
1602
2464
  return null;
1603
2465
  }
1604
- function applyArrInsert(base, head, it, newDot, bumpCounterAbove, strictParents = false) {
2466
+ function createArrayIndexLookupSession() {
2467
+ const bySeq = /* @__PURE__ */ new WeakMap();
2468
+ return { get(seq) {
2469
+ const cached = bySeq.get(seq);
2470
+ if (cached) return cached;
2471
+ const created = rgaCreateIndexedIdSnapshot(seq);
2472
+ bySeq.set(seq, created);
2473
+ return created;
2474
+ } };
2475
+ }
2476
+ function applyArrInsert(base, head, it, newDot, indexSession, bumpCounterAbove, strictParents = false) {
1605
2477
  const pointer = `/${it.path.join("/")}`;
1606
2478
  const baseSeq = getSeqAtPath(base, it.path);
1607
2479
  if (!baseSeq) {
@@ -1633,8 +2505,9 @@ function applyArrInsert(base, head, it, newDot, bumpCounterAbove, strictParents
1633
2505
  const headSeqRes = getHeadSeqForBaseArrayIntent(head, it.path);
1634
2506
  if (!headSeqRes.ok) return headSeqRes;
1635
2507
  const headSeq = headSeqRes.seq;
1636
- const idx = it.index === Number.POSITIVE_INFINITY ? rgaLinearizeIds(baseSeq).length : it.index;
1637
- const baseLen = rgaLinearizeIds(baseSeq).length;
2508
+ const baseIndex = indexSession.get(baseSeq);
2509
+ const baseLen = baseIndex.length();
2510
+ const idx = it.index === Number.POSITIVE_INFINITY ? baseLen : it.index;
1638
2511
  if (idx < 0 || idx > baseLen) return {
1639
2512
  ok: false,
1640
2513
  code: 409,
@@ -1642,20 +2515,18 @@ function applyArrInsert(base, head, it, newDot, bumpCounterAbove, strictParents
1642
2515
  message: `index out of bounds at /${it.path.join("/")}/${it.index}`,
1643
2516
  path: `/${it.path.join("/")}/${it.index}`
1644
2517
  };
1645
- const prev = idx === 0 ? HEAD : rgaIdAtIndex(baseSeq, idx - 1) ?? HEAD;
2518
+ const prev = baseIndex.prevForInsertAt(idx);
1646
2519
  const dotRes = nextInsertDotForPrev(headSeq, prev, newDot, pointer, bumpCounterAbove);
1647
2520
  if (!dotRes.ok) return dotRes;
1648
2521
  const d = dotRes.dot;
1649
- rgaInsertAfter(headSeq, prev, dotToElemId(d), d, nodeFromJson(it.value, newDot));
2522
+ const id = dotToElemId(d);
2523
+ rgaInsertAfter(headSeq, prev, id, d, nodeFromJson(it.value, newDot));
2524
+ if (baseSeq === headSeq) baseIndex.insertAt(idx, id);
1650
2525
  return null;
1651
2526
  }
1652
2527
  function nextInsertDotForPrev(seq, prev, newDot, path, bumpCounterAbove) {
1653
2528
  const MAX_INSERT_DOT_ATTEMPTS = 1024;
1654
- let maxSiblingDot = null;
1655
- for (const elem of seq.elems.values()) {
1656
- if (elem.prev !== prev) continue;
1657
- if (!maxSiblingDot || compareDot(elem.insDot, maxSiblingDot) > 0) maxSiblingDot = elem.insDot;
1658
- }
2529
+ const maxSiblingDot = rgaMaxInsertDotForPrev(seq, prev);
1659
2530
  if (maxSiblingDot) bumpCounterAbove?.(maxSiblingDot.ctr);
1660
2531
  if (!maxSiblingDot) return {
1661
2532
  ok: true,
@@ -1676,8 +2547,8 @@ function nextInsertDotForPrev(seq, prev, newDot, path, bumpCounterAbove) {
1676
2547
  path
1677
2548
  };
1678
2549
  }
1679
- function applyArrDelete(base, head, it, newDot) {
1680
- newDot();
2550
+ function applyArrDelete(base, head, it, newDot, indexSession) {
2551
+ const _d = newDot();
1681
2552
  const baseSeq = getSeqAtPath(base, it.path);
1682
2553
  if (!baseSeq) return {
1683
2554
  ok: false,
@@ -1689,7 +2560,8 @@ function applyArrDelete(base, head, it, newDot) {
1689
2560
  const headSeqRes = getHeadSeqForBaseArrayIntent(head, it.path);
1690
2561
  if (!headSeqRes.ok) return headSeqRes;
1691
2562
  const headSeq = headSeqRes.seq;
1692
- const baseId = rgaIdAtIndex(baseSeq, it.index);
2563
+ const baseIndex = indexSession.get(baseSeq);
2564
+ const baseId = baseIndex.idAt(it.index);
1693
2565
  if (!baseId) return {
1694
2566
  ok: false,
1695
2567
  code: 409,
@@ -1704,10 +2576,11 @@ function applyArrDelete(base, head, it, newDot) {
1704
2576
  message: `element missing in head lineage at index ${it.index}`,
1705
2577
  path: `/${it.path.join("/")}/${it.index}`
1706
2578
  };
1707
- rgaDelete(headSeq, baseId);
2579
+ rgaDelete(headSeq, baseId, _d);
2580
+ if (baseSeq === headSeq) baseIndex.deleteAt(it.index);
1708
2581
  return null;
1709
2582
  }
1710
- function applyArrReplace(base, head, it, newDot) {
2583
+ function applyArrReplace(base, head, it, newDot, indexSession) {
1711
2584
  newDot();
1712
2585
  const baseSeq = getSeqAtPath(base, it.path);
1713
2586
  if (!baseSeq) return {
@@ -1720,7 +2593,7 @@ function applyArrReplace(base, head, it, newDot) {
1720
2593
  const headSeqRes = getHeadSeqForBaseArrayIntent(head, it.path);
1721
2594
  if (!headSeqRes.ok) return headSeqRes;
1722
2595
  const headSeq = headSeqRes.seq;
1723
- const baseId = rgaIdAtIndex(baseSeq, it.index);
2596
+ const baseId = indexSession.get(baseSeq).idAt(it.index);
1724
2597
  if (!baseId) return {
1725
2598
  ok: false,
1726
2599
  code: 409,
@@ -1753,6 +2626,7 @@ function applyArrReplace(base, head, it, newDot) {
1753
2626
  * @returns `{ ok: true }` on success, or `{ ok: false, code: 409, message }` on conflict.
1754
2627
  */
1755
2628
  function applyIntentsToCrdt(base, head, intents, newDot, evalTestAgainst = "head", bumpCounterAbove, options = {}) {
2629
+ const arrayIndexSession = createArrayIndexLookupSession();
1756
2630
  for (const it of intents) {
1757
2631
  let fail = null;
1758
2632
  switch (it.t) {
@@ -1766,13 +2640,13 @@ function applyIntentsToCrdt(base, head, intents, newDot, evalTestAgainst = "head
1766
2640
  fail = applyObjRemove(head, it, newDot);
1767
2641
  break;
1768
2642
  case "ArrInsert":
1769
- fail = applyArrInsert(base, head, it, newDot, bumpCounterAbove, options.strictParents ?? false);
2643
+ fail = applyArrInsert(base, head, it, newDot, arrayIndexSession, bumpCounterAbove, options.strictParents ?? false);
1770
2644
  break;
1771
2645
  case "ArrDelete":
1772
- fail = applyArrDelete(base, head, it, newDot);
2646
+ fail = applyArrDelete(base, head, it, newDot, arrayIndexSession);
1773
2647
  break;
1774
2648
  case "ArrReplace":
1775
- fail = applyArrReplace(base, head, it, newDot);
2649
+ fail = applyArrReplace(base, head, it, newDot, arrayIndexSession);
1776
2650
  break;
1777
2651
  default: assertNever(it, "Unhandled intent type");
1778
2652
  }
@@ -1814,15 +2688,199 @@ function jsonPatchToCrdtSafe(baseOrOptions, head, patch, newDot, evalTestAgainst
1814
2688
  }
1815
2689
  /** Alias for codebases that prefer `try*` naming for non-throwing APIs. */
1816
2690
  const tryJsonPatchToCrdt = jsonPatchToCrdtSafe;
2691
+ function nodeToJsonForPatch(node) {
2692
+ return node.kind === "lww" ? node.value : materialize(node);
2693
+ }
2694
+ function rebaseDiffOps(path, nestedOps, out) {
2695
+ const prefix = stringifyJsonPointer(path);
2696
+ for (const op of nestedOps) {
2697
+ const rebasedPath = prefix === "" ? op.path : op.path === "" ? prefix : `${prefix}${op.path}`;
2698
+ if (op.op === "remove") {
2699
+ out.push({
2700
+ op: "remove",
2701
+ path: rebasedPath
2702
+ });
2703
+ continue;
2704
+ }
2705
+ if (op.op === "add" || op.op === "replace") {
2706
+ out.push({
2707
+ op: op.op,
2708
+ path: rebasedPath,
2709
+ value: op.value
2710
+ });
2711
+ continue;
2712
+ }
2713
+ throw new Error(`Unexpected op '${op.op}' from diffJsonPatch`);
2714
+ }
2715
+ }
2716
+ function nodesJsonEqual(baseNode, headNode, depth) {
2717
+ assertTraversalDepth(depth);
2718
+ if (baseNode === headNode) return true;
2719
+ if (baseNode.kind !== headNode.kind) return false;
2720
+ if (baseNode.kind === "lww") {
2721
+ const headLww = headNode;
2722
+ return jsonEquals(baseNode.value, headLww.value);
2723
+ }
2724
+ if (baseNode.kind === "obj") {
2725
+ const headObj = headNode;
2726
+ if (baseNode.entries.size !== headObj.entries.size) return false;
2727
+ for (const [key, baseEntry] of baseNode.entries.entries()) {
2728
+ const headEntry = headObj.entries.get(key);
2729
+ if (!headEntry) return false;
2730
+ if (!nodesJsonEqual(baseEntry.node, headEntry.node, depth + 1)) return false;
2731
+ }
2732
+ return true;
2733
+ }
2734
+ const headSeq = headNode;
2735
+ const baseCursor = rgaCreateLinearCursor(baseNode);
2736
+ const headCursor = rgaCreateLinearCursor(headSeq);
2737
+ while (true) {
2738
+ const baseElem = baseCursor.next();
2739
+ const headElem = headCursor.next();
2740
+ if (baseElem === void 0 || headElem === void 0) return baseElem === void 0 && headElem === void 0;
2741
+ if (!nodesJsonEqual(baseElem.value, headElem.value, depth + 1)) return false;
2742
+ }
2743
+ }
2744
+ function diffObjectNodes(path, baseNode, headNode, options, ops, depth) {
2745
+ assertTraversalDepth(depth);
2746
+ const baseKeys = [...baseNode.entries.keys()].sort();
2747
+ const headKeys = [...headNode.entries.keys()].sort();
2748
+ let baseIndex = 0;
2749
+ let headIndex = 0;
2750
+ while (baseIndex < baseKeys.length && headIndex < headKeys.length) {
2751
+ const baseKey = baseKeys[baseIndex];
2752
+ const headKey = headKeys[headIndex];
2753
+ if (baseKey === headKey) {
2754
+ baseIndex += 1;
2755
+ headIndex += 1;
2756
+ continue;
2757
+ }
2758
+ if (baseKey < headKey) {
2759
+ path.push(baseKey);
2760
+ ops.push({
2761
+ op: "remove",
2762
+ path: stringifyJsonPointer(path)
2763
+ });
2764
+ path.pop();
2765
+ baseIndex += 1;
2766
+ continue;
2767
+ }
2768
+ headIndex += 1;
2769
+ }
2770
+ while (baseIndex < baseKeys.length) {
2771
+ const baseKey = baseKeys[baseIndex];
2772
+ path.push(baseKey);
2773
+ ops.push({
2774
+ op: "remove",
2775
+ path: stringifyJsonPointer(path)
2776
+ });
2777
+ path.pop();
2778
+ baseIndex += 1;
2779
+ }
2780
+ baseIndex = 0;
2781
+ headIndex = 0;
2782
+ while (baseIndex < baseKeys.length && headIndex < headKeys.length) {
2783
+ const baseKey = baseKeys[baseIndex];
2784
+ const headKey = headKeys[headIndex];
2785
+ if (baseKey === headKey) {
2786
+ baseIndex += 1;
2787
+ headIndex += 1;
2788
+ continue;
2789
+ }
2790
+ if (baseKey < headKey) {
2791
+ baseIndex += 1;
2792
+ continue;
2793
+ }
2794
+ const headEntry = headNode.entries.get(headKey);
2795
+ path.push(headKey);
2796
+ ops.push({
2797
+ op: "add",
2798
+ path: stringifyJsonPointer(path),
2799
+ value: nodeToJsonForPatch(headEntry.node)
2800
+ });
2801
+ path.pop();
2802
+ headIndex += 1;
2803
+ }
2804
+ while (headIndex < headKeys.length) {
2805
+ const headKey = headKeys[headIndex];
2806
+ const headEntry = headNode.entries.get(headKey);
2807
+ path.push(headKey);
2808
+ ops.push({
2809
+ op: "add",
2810
+ path: stringifyJsonPointer(path),
2811
+ value: nodeToJsonForPatch(headEntry.node)
2812
+ });
2813
+ path.pop();
2814
+ headIndex += 1;
2815
+ }
2816
+ baseIndex = 0;
2817
+ headIndex = 0;
2818
+ while (baseIndex < baseKeys.length && headIndex < headKeys.length) {
2819
+ const baseKey = baseKeys[baseIndex];
2820
+ const headKey = headKeys[headIndex];
2821
+ if (baseKey === headKey) {
2822
+ const baseEntry = baseNode.entries.get(baseKey);
2823
+ const headEntry = headNode.entries.get(headKey);
2824
+ if (!nodesJsonEqual(baseEntry.node, headEntry.node, depth + 1)) {
2825
+ path.push(baseKey);
2826
+ diffNodeToPatch(path, baseEntry.node, headEntry.node, options, ops, depth + 1);
2827
+ path.pop();
2828
+ }
2829
+ baseIndex += 1;
2830
+ headIndex += 1;
2831
+ continue;
2832
+ }
2833
+ if (baseKey < headKey) {
2834
+ baseIndex += 1;
2835
+ continue;
2836
+ }
2837
+ headIndex += 1;
2838
+ }
2839
+ }
2840
+ function diffNodeToPatch(path, baseNode, headNode, options, ops, depth) {
2841
+ assertTraversalDepth(depth);
2842
+ if (baseNode === headNode) return;
2843
+ if (baseNode.kind !== headNode.kind) {
2844
+ ops.push({
2845
+ op: "replace",
2846
+ path: stringifyJsonPointer(path),
2847
+ value: nodeToJsonForPatch(headNode)
2848
+ });
2849
+ return;
2850
+ }
2851
+ if (baseNode.kind === "lww") {
2852
+ const headLww = headNode;
2853
+ if (jsonEquals(baseNode.value, headLww.value)) return;
2854
+ ops.push({
2855
+ op: "replace",
2856
+ path: stringifyJsonPointer(path),
2857
+ value: headLww.value
2858
+ });
2859
+ return;
2860
+ }
2861
+ if (baseNode.kind === "obj") {
2862
+ diffObjectNodes(path, baseNode, headNode, options, ops, depth);
2863
+ return;
2864
+ }
2865
+ const headSeq = headNode;
2866
+ rebaseDiffOps(path, diffJsonPatch(materialize(baseNode), materialize(headSeq), options), ops);
2867
+ }
1817
2868
  /**
1818
2869
  * Generate a JSON Patch delta between two CRDT documents.
1819
2870
  * @param base - The base document snapshot.
1820
2871
  * @param head - The current document state.
1821
- * @param options - Diff options (e.g. `{ arrayStrategy: "lcs" }`).
2872
+ * @param options - Diff options (e.g. `{ arrayStrategy: "lcs" }` or `{ arrayStrategy: "lcs-linear" }`).
1822
2873
  * @returns An array of JSON Patch operations that transform base into head.
1823
2874
  */
1824
2875
  function crdtToJsonPatch(base, head, options) {
1825
- return diffJsonPatch(materialize(base.root), materialize(head.root), options);
2876
+ if ((options?.jsonValidation ?? "none") !== "none") return diffJsonPatch(materialize(base.root), materialize(head.root), options);
2877
+ return crdtNodesToJsonPatch(base.root, head.root, options);
2878
+ }
2879
+ /** Internals-only helper for diffing CRDT nodes from an existing traversal depth. */
2880
+ function crdtNodesToJsonPatch(baseNode, headNode, options, depth = 0) {
2881
+ const ops = [];
2882
+ diffNodeToPatch([], baseNode, headNode, options ?? {}, ops, depth);
2883
+ return ops;
1826
2884
  }
1827
2885
  /**
1828
2886
  * Emit a single root `replace` patch representing the full document state.
@@ -2096,7 +3154,7 @@ function applyPatchAsActor(doc, vv, actor, patch, options = {}) {
2096
3154
  }
2097
3155
  /** Non-throwing `applyPatchAsActor` variant for internals sync flows. */
2098
3156
  function tryApplyPatchAsActor(doc, vv, actor, patch, options = {}) {
2099
- const observedCtr = maxCtrInNodeForActor$1(doc.root, actor);
3157
+ const observedCtr = observedVersionVector(doc)[actor] ?? 0;
2100
3158
  const applied = tryApplyPatch({
2101
3159
  doc,
2102
3160
  clock: createClock(actor, Math.max(vv[actor] ?? 0, observedCtr))
@@ -2124,166 +3182,387 @@ function toApplyPatchOptionsForActor(options) {
2124
3182
  } : void 0
2125
3183
  };
2126
3184
  }
2127
- function applyPatchInternal(state, patch, options, execution) {
2128
- if ((options.semantics ?? "sequential") === "sequential") {
2129
- if (!options.base && execution === "batch") {
2130
- const compiled = compileIntents(materialize(state.doc.root), patch, "sequential", options.jsonValidation ?? "none");
2131
- if (!compiled.ok) return compiled;
2132
- return applyIntentsToCrdt(state.doc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
3185
+ function applyPatchInternal(state, patch, options, execution) {
3186
+ const preparedPatch = preparePatchPayloadsSafe(patch, options.jsonValidation ?? "none");
3187
+ if (!preparedPatch.ok) return preparedPatch;
3188
+ const runtimePatch = preparedPatch.patch;
3189
+ if ((options.semantics ?? "sequential") === "sequential") {
3190
+ if (!options.base && execution === "batch") {
3191
+ const compiled = compilePreparedIntents(materialize(state.doc.root), runtimePatch, "sequential");
3192
+ if (!compiled.ok) return compiled;
3193
+ return applyIntentsToCrdt(state.doc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
3194
+ }
3195
+ const explicitBaseState = options.base ? {
3196
+ doc: cloneDoc(options.base.doc),
3197
+ clock: createClock("__base__", 0)
3198
+ } : null;
3199
+ const session = { pointerCache: /* @__PURE__ */ new Map() };
3200
+ for (const [opIndex, op] of runtimePatch.entries()) {
3201
+ const step = applyPatchOpSequential(state, op, options, explicitBaseState ? explicitBaseState.doc : state.doc, explicitBaseState, opIndex, session);
3202
+ if (!step.ok) return step;
3203
+ }
3204
+ return { ok: true };
3205
+ }
3206
+ const baseDoc = options.base ? options.base.doc : cloneDoc(state.doc);
3207
+ const compiled = compilePreparedIntents(materialize(baseDoc.root), runtimePatch, "base");
3208
+ if (!compiled.ok) return compiled;
3209
+ return applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
3210
+ }
3211
+ function applyPatchOpSequential(state, op, options, baseDoc, explicitBaseState, opIndex, session) {
3212
+ if (op.op === "move") {
3213
+ const fromResolved = resolveValueAtPointerInDoc(baseDoc, op.from, opIndex, session.pointerCache);
3214
+ if (!fromResolved.ok) return fromResolved;
3215
+ const fromValue = structuredClone(fromResolved.value);
3216
+ const removeRes = applySinglePatchOpSequentialStep(state, baseDoc, {
3217
+ op: "remove",
3218
+ path: op.from
3219
+ }, options, explicitBaseState, opIndex, session);
3220
+ if (!removeRes.ok) return removeRes;
3221
+ const addOp = {
3222
+ op: "add",
3223
+ path: op.path,
3224
+ value: fromValue
3225
+ };
3226
+ if (!explicitBaseState) return applySinglePatchOpSequentialStep(state, state.doc, addOp, options, null, opIndex, session);
3227
+ const headAddRes = applySinglePatchOpSequentialStep(state, state.doc, addOp, options, null, opIndex, session);
3228
+ if (!headAddRes.ok) return headAddRes;
3229
+ const shadowAddRes = applySinglePatchOpExplicitShadowStep(explicitBaseState, addOp, options, opIndex, session);
3230
+ if (!shadowAddRes.ok) return shadowAddRes;
3231
+ return { ok: true };
3232
+ }
3233
+ if (op.op === "copy") {
3234
+ const fromResolved = resolveValueAtPointerInDoc(baseDoc, op.from, opIndex, session.pointerCache);
3235
+ if (!fromResolved.ok) return fromResolved;
3236
+ return applySinglePatchOpSequentialStep(state, baseDoc, {
3237
+ op: "add",
3238
+ path: op.path,
3239
+ value: structuredClone(fromResolved.value)
3240
+ }, options, explicitBaseState, opIndex, session);
3241
+ }
3242
+ return applySinglePatchOpSequentialStep(state, baseDoc, op, options, explicitBaseState, opIndex, session);
3243
+ }
3244
+ function applySinglePatchOpSequentialStep(state, baseDoc, op, options, explicitBaseState, opIndex, session) {
3245
+ const compiled = compilePreparedSingleIntentFromDoc(baseDoc, op, session.pointerCache, opIndex);
3246
+ if (!compiled.ok) return compiled;
3247
+ const headStep = applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
3248
+ if (!headStep.ok) return headStep;
3249
+ if (explicitBaseState && op.op !== "test") {
3250
+ const shadowStep = applyIntentsToCrdt(explicitBaseState.doc, explicitBaseState.doc, compiled.intents, () => explicitBaseState.clock.next(), "base", (ctr) => bumpClockCounter(explicitBaseState, ctr), { strictParents: options.strictParents });
3251
+ if (!shadowStep.ok) return shadowStep;
3252
+ }
3253
+ return { ok: true };
3254
+ }
3255
+ function applySinglePatchOpExplicitShadowStep(explicitBaseState, op, options, opIndex, session) {
3256
+ const compiled = compilePreparedSingleIntentFromDoc(explicitBaseState.doc, op, session.pointerCache, opIndex);
3257
+ if (!compiled.ok) return compiled;
3258
+ const shadowStep = applyIntentsToCrdt(explicitBaseState.doc, explicitBaseState.doc, compiled.intents, () => explicitBaseState.clock.next(), "base", (ctr) => bumpClockCounter(explicitBaseState, ctr), { strictParents: options.strictParents });
3259
+ if (!shadowStep.ok) return shadowStep;
3260
+ return { ok: true };
3261
+ }
3262
+ function resolveValueAtPointerInDoc(doc, pointer, opIndex, pointerCache) {
3263
+ let path;
3264
+ try {
3265
+ path = parsePointerWithCache(pointer, pointerCache);
3266
+ } catch (error) {
3267
+ return toPointerParseApplyError(error, pointer, opIndex);
3268
+ }
3269
+ const resolved = resolveNodeAtPath(doc.root, path);
3270
+ if (!resolved.ok) return {
3271
+ ok: false,
3272
+ ...resolved.error,
3273
+ path: pointer,
3274
+ opIndex
3275
+ };
3276
+ return {
3277
+ ok: true,
3278
+ value: materialize(resolved.node)
3279
+ };
3280
+ }
3281
+ function compilePreparedSingleIntentFromDoc(baseDoc, op, pointerCache, opIndex) {
3282
+ let path;
3283
+ try {
3284
+ path = parsePointerWithCache(op.path, pointerCache);
3285
+ } catch (error) {
3286
+ return toPointerParseApplyError(error, op.path, opIndex);
3287
+ }
3288
+ if (op.op === "test") return {
3289
+ ok: true,
3290
+ intents: [{
3291
+ t: "Test",
3292
+ path,
3293
+ value: op.value
3294
+ }]
3295
+ };
3296
+ if (path.length === 0) {
3297
+ if (op.op === "remove") return {
3298
+ ok: false,
3299
+ code: 409,
3300
+ reason: "INVALID_TARGET",
3301
+ message: "remove at root path is not supported in RFC-compliant mode",
3302
+ path: op.path,
3303
+ opIndex
3304
+ };
3305
+ return {
3306
+ ok: true,
3307
+ intents: [{
3308
+ t: "ObjSet",
3309
+ path: [],
3310
+ key: ROOT_KEY,
3311
+ value: op.value
3312
+ }]
3313
+ };
3314
+ }
3315
+ const parentPath = path.slice(0, -1);
3316
+ const parentPointer = stringifyJsonPointer(parentPath);
3317
+ const key = path[path.length - 1];
3318
+ const resolvedParent = parentPath.length === 0 ? {
3319
+ ok: true,
3320
+ node: baseDoc.root
3321
+ } : resolveNodeAtPath(baseDoc.root, parentPath);
3322
+ if (!resolvedParent.ok) return {
3323
+ ok: false,
3324
+ ...resolvedParent.error,
3325
+ path: parentPointer,
3326
+ opIndex
3327
+ };
3328
+ const parentNode = resolvedParent.node;
3329
+ if (parentNode.kind === "seq") {
3330
+ const parsedIndex = parseArrayIndexTokenForDoc(key, op.op, op.path, opIndex);
3331
+ if (!parsedIndex.ok) return parsedIndex;
3332
+ const boundedIndex = validateArrayIndexBounds(parsedIndex.index, op.op, rgaLength(parentNode), op.path, opIndex);
3333
+ if (!boundedIndex.ok) return boundedIndex;
3334
+ if (op.op === "add") return {
3335
+ ok: true,
3336
+ intents: [{
3337
+ t: "ArrInsert",
3338
+ path: parentPath,
3339
+ index: boundedIndex.index,
3340
+ value: op.value
3341
+ }]
3342
+ };
3343
+ if (op.op === "remove") return {
3344
+ ok: true,
3345
+ intents: [{
3346
+ t: "ArrDelete",
3347
+ path: parentPath,
3348
+ index: boundedIndex.index
3349
+ }]
3350
+ };
3351
+ return {
3352
+ ok: true,
3353
+ intents: [{
3354
+ t: "ArrReplace",
3355
+ path: parentPath,
3356
+ index: boundedIndex.index,
3357
+ value: op.value
3358
+ }]
3359
+ };
3360
+ }
3361
+ if (parentNode.kind !== "obj") return {
3362
+ ok: false,
3363
+ code: 409,
3364
+ reason: "INVALID_TARGET",
3365
+ message: `expected object or array parent at ${parentPointer}`,
3366
+ path: parentPointer,
3367
+ opIndex
3368
+ };
3369
+ if (key === "__proto__") return {
3370
+ ok: false,
3371
+ code: 409,
3372
+ reason: "INVALID_POINTER",
3373
+ message: `unsafe object key at ${op.path}`,
3374
+ path: op.path,
3375
+ opIndex
3376
+ };
3377
+ const entry = parentNode.entries.get(key);
3378
+ if ((op.op === "replace" || op.op === "remove") && !entry) return {
3379
+ ok: false,
3380
+ code: 409,
3381
+ reason: "MISSING_TARGET",
3382
+ message: `missing key ${key} at ${parentPointer}`,
3383
+ path: op.path,
3384
+ opIndex
3385
+ };
3386
+ if (op.op === "remove") return {
3387
+ ok: true,
3388
+ intents: [{
3389
+ t: "ObjRemove",
3390
+ path: parentPath,
3391
+ key
3392
+ }]
3393
+ };
3394
+ return {
3395
+ ok: true,
3396
+ intents: [{
3397
+ t: "ObjSet",
3398
+ path: parentPath,
3399
+ key,
3400
+ value: op.value,
3401
+ mode: op.op
3402
+ }]
3403
+ };
3404
+ }
3405
+ function parsePointerWithCache(pointer, pointerCache) {
3406
+ const cachedPath = pointerCache.get(pointer);
3407
+ if (cachedPath !== void 0) return cachedPath.slice();
3408
+ const parsedPath = parseJsonPointer(pointer);
3409
+ pointerCache.set(pointer, parsedPath);
3410
+ return parsedPath.slice();
3411
+ }
3412
+ function resolveNodeAtPath(root, path) {
3413
+ let current = root;
3414
+ for (const segment of path) {
3415
+ if (current.kind === "obj") {
3416
+ const entry = current.entries.get(segment);
3417
+ if (!entry) return {
3418
+ ok: false,
3419
+ error: {
3420
+ code: 409,
3421
+ reason: "MISSING_PARENT",
3422
+ message: `Missing key '${segment}'`
3423
+ }
3424
+ };
3425
+ current = entry.node;
3426
+ continue;
2133
3427
  }
2134
- const explicitBaseState = options.base ? {
2135
- doc: cloneDoc(options.base.doc),
2136
- clock: createClock("__base__", 0)
2137
- } : null;
2138
- let sequentialHeadJson = materialize(state.doc.root);
2139
- let sequentialBaseJson = explicitBaseState ? materialize(explicitBaseState.doc.root) : sequentialHeadJson;
2140
- for (const [opIndex, op] of patch.entries()) {
2141
- const step = applyPatchOpSequential(state, op, options, explicitBaseState ? explicitBaseState.doc : state.doc, sequentialBaseJson, sequentialHeadJson, explicitBaseState, opIndex);
2142
- if (!step.ok) return step;
2143
- sequentialBaseJson = step.baseJson;
2144
- sequentialHeadJson = step.headJson;
3428
+ if (current.kind === "seq") {
3429
+ if (!ARRAY_INDEX_TOKEN_PATTERN.test(segment)) return {
3430
+ ok: false,
3431
+ error: {
3432
+ code: 409,
3433
+ reason: "INVALID_POINTER",
3434
+ message: `Expected array index, got '${segment}'`
3435
+ }
3436
+ };
3437
+ const index = Number(segment);
3438
+ if (!Number.isSafeInteger(index)) return {
3439
+ ok: false,
3440
+ error: {
3441
+ code: 409,
3442
+ reason: "OUT_OF_BOUNDS",
3443
+ message: `Index out of bounds at '${segment}'`
3444
+ }
3445
+ };
3446
+ const elemId = rgaIdAtIndex(current, index);
3447
+ if (elemId === void 0) return {
3448
+ ok: false,
3449
+ error: {
3450
+ code: 409,
3451
+ reason: "OUT_OF_BOUNDS",
3452
+ message: `Index out of bounds at '${segment}'`
3453
+ }
3454
+ };
3455
+ current = current.elems.get(elemId).value;
3456
+ continue;
2145
3457
  }
2146
- return { ok: true };
3458
+ return {
3459
+ ok: false,
3460
+ error: {
3461
+ code: 409,
3462
+ reason: "INVALID_TARGET",
3463
+ message: `Cannot traverse into non-container at '${segment}'`
3464
+ }
3465
+ };
2147
3466
  }
2148
- const baseDoc = options.base ? options.base.doc : cloneDoc(state.doc);
2149
- const compiled = compileIntents(materialize(baseDoc.root), patch, "base", options.jsonValidation ?? "none");
2150
- if (!compiled.ok) return compiled;
2151
- return applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
3467
+ return {
3468
+ ok: true,
3469
+ node: current
3470
+ };
2152
3471
  }
2153
- function applyPatchOpSequential(state, op, options, baseDoc, baseJson, headJson, explicitBaseState, opIndex) {
2154
- if (op.op === "move") {
2155
- const fromResolved = resolveValueAtPointer(baseJson, op.from, opIndex);
2156
- if (!fromResolved.ok) return fromResolved;
2157
- const fromValue = structuredClone(fromResolved.value);
2158
- const removeRes = applySinglePatchOpSequentialStep(state, baseDoc, baseJson, headJson, {
2159
- op: "remove",
2160
- path: op.from
2161
- }, options, explicitBaseState);
2162
- if (!removeRes.ok) return removeRes;
2163
- const addOp = {
2164
- op: "add",
2165
- path: op.path,
2166
- value: fromValue
3472
+ function parseArrayIndexTokenForDoc(token, op, path, opIndex) {
3473
+ if (token === "-") {
3474
+ if (op !== "add") return {
3475
+ ok: false,
3476
+ code: 409,
3477
+ reason: "INVALID_POINTER",
3478
+ message: `'-' index is only valid for add at ${path}`,
3479
+ path,
3480
+ opIndex
2167
3481
  };
2168
- if (!explicitBaseState) return applySinglePatchOpSequentialStep(state, state.doc, removeRes.baseJson, removeRes.headJson, addOp, options, null);
2169
- const headAddRes = applySinglePatchOpSequentialStep(state, state.doc, removeRes.headJson, removeRes.headJson, addOp, options, null);
2170
- if (!headAddRes.ok) return headAddRes;
2171
- const shadowAddRes = applySinglePatchOpExplicitShadowStep(explicitBaseState, removeRes.baseJson, addOp, options);
2172
- if (!shadowAddRes.ok) return shadowAddRes;
2173
3482
  return {
2174
3483
  ok: true,
2175
- baseJson: shadowAddRes.baseJson,
2176
- headJson: headAddRes.headJson
3484
+ index: Number.POSITIVE_INFINITY
2177
3485
  };
2178
3486
  }
2179
- if (op.op === "copy") {
2180
- const fromResolved = resolveValueAtPointer(baseJson, op.from, opIndex);
2181
- if (!fromResolved.ok) return fromResolved;
2182
- return applySinglePatchOpSequentialStep(state, baseDoc, baseJson, headJson, {
2183
- op: "add",
2184
- path: op.path,
2185
- value: structuredClone(fromResolved.value)
2186
- }, options, explicitBaseState);
2187
- }
2188
- return applySinglePatchOpSequentialStep(state, baseDoc, baseJson, headJson, op, options, explicitBaseState);
2189
- }
2190
- function applySinglePatchOpSequentialStep(state, baseDoc, baseJson, headJson, op, options, explicitBaseState) {
2191
- const compiled = compileIntents(baseJson, [op], "sequential", options.jsonValidation ?? "none");
2192
- if (!compiled.ok) return compiled;
2193
- const headStep = applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
2194
- if (!headStep.ok) return headStep;
2195
- if (explicitBaseState && op.op !== "test") {
2196
- const shadowStep = applyIntentsToCrdt(explicitBaseState.doc, explicitBaseState.doc, compiled.intents, () => explicitBaseState.clock.next(), "base", (ctr) => bumpClockCounter(explicitBaseState, ctr), { strictParents: options.strictParents });
2197
- if (!shadowStep.ok) return shadowStep;
2198
- }
2199
- if (op.op === "test") return {
2200
- ok: true,
2201
- baseJson,
2202
- headJson
3487
+ if (!ARRAY_INDEX_TOKEN_PATTERN.test(token)) return {
3488
+ ok: false,
3489
+ code: 409,
3490
+ reason: "INVALID_POINTER",
3491
+ message: `expected array index at ${path}`,
3492
+ path,
3493
+ opIndex
3494
+ };
3495
+ const index = Number(token);
3496
+ if (!Number.isSafeInteger(index)) return {
3497
+ ok: false,
3498
+ code: 409,
3499
+ reason: "OUT_OF_BOUNDS",
3500
+ message: `array index is too large at ${path}`,
3501
+ path,
3502
+ opIndex
2203
3503
  };
2204
- const nextBaseJson = applyJsonPatchOpToShadow(baseJson, op);
2205
3504
  return {
2206
3505
  ok: true,
2207
- baseJson: nextBaseJson,
2208
- headJson: explicitBaseState ? applyJsonPatchOpToShadow(headJson, op) : nextBaseJson
3506
+ index
2209
3507
  };
2210
3508
  }
2211
- function applySinglePatchOpExplicitShadowStep(explicitBaseState, baseJson, op, options) {
2212
- const compiled = compileIntents(baseJson, [op], "sequential", options.jsonValidation ?? "none");
2213
- if (!compiled.ok) return compiled;
2214
- const shadowStep = applyIntentsToCrdt(explicitBaseState.doc, explicitBaseState.doc, compiled.intents, () => explicitBaseState.clock.next(), "base", (ctr) => bumpClockCounter(explicitBaseState, ctr), { strictParents: options.strictParents });
2215
- if (!shadowStep.ok) return shadowStep;
2216
- if (op.op === "test") return {
2217
- ok: true,
2218
- baseJson
3509
+ function validateArrayIndexBounds(index, op, arrLength, path, opIndex) {
3510
+ if (op === "add") {
3511
+ if (index === Number.POSITIVE_INFINITY) return {
3512
+ ok: true,
3513
+ index
3514
+ };
3515
+ if (index > arrLength) return {
3516
+ ok: false,
3517
+ code: 409,
3518
+ reason: "OUT_OF_BOUNDS",
3519
+ message: `index out of bounds at ${path}; expected 0..${arrLength}`,
3520
+ path,
3521
+ opIndex
3522
+ };
3523
+ } else if (index >= arrLength) return {
3524
+ ok: false,
3525
+ code: 409,
3526
+ reason: "OUT_OF_BOUNDS",
3527
+ message: `index out of bounds at ${path}; expected 0..${Math.max(arrLength - 1, 0)}`,
3528
+ path,
3529
+ opIndex
2219
3530
  };
2220
3531
  return {
2221
3532
  ok: true,
2222
- baseJson: applyJsonPatchOpToShadow(baseJson, op)
3533
+ index
2223
3534
  };
2224
3535
  }
2225
- function applyJsonPatchOpToShadow(baseJson, op) {
2226
- const path = parseJsonPointer(op.path);
2227
- if (path.length === 0) {
2228
- if (op.op === "test") return baseJson;
2229
- if (op.op === "remove") return null;
2230
- return structuredClone(op.value);
2231
- }
2232
- const parentPath = path.slice(0, -1);
2233
- const key = path[path.length - 1];
2234
- const parent = getAtJson(baseJson, parentPath);
2235
- if (Array.isArray(parent)) {
2236
- const idx = key === "-" ? parent.length : Number(key);
2237
- if (!Number.isInteger(idx)) throw new Error(`Invalid array index ${key}`);
2238
- if (op.op === "add") {
2239
- parent.splice(idx, 0, structuredClone(op.value));
2240
- return baseJson;
2241
- }
2242
- if (op.op === "remove") {
2243
- parent.splice(idx, 1);
2244
- return baseJson;
2245
- }
2246
- if (op.op === "replace") {
2247
- parent[idx] = structuredClone(op.value);
2248
- return baseJson;
2249
- }
2250
- return baseJson;
2251
- }
2252
- const obj = parent;
2253
- if (op.op === "add" || op.op === "replace") {
2254
- obj[key] = structuredClone(op.value);
2255
- return baseJson;
2256
- }
2257
- if (op.op === "remove") {
2258
- delete obj[key];
2259
- return baseJson;
2260
- }
2261
- return baseJson;
3536
+ function bumpClockCounter(state, ctr) {
3537
+ if (state.clock.ctr < ctr) state.clock.ctr = ctr;
2262
3538
  }
2263
- function resolveValueAtPointer(baseJson, pointer, opIndex) {
2264
- let path;
2265
- try {
2266
- path = parseJsonPointer(pointer);
2267
- } catch (error) {
2268
- return toPointerParseApplyError(error, pointer, opIndex);
2269
- }
3539
+ function compilePreparedIntents(baseJson, patch, semantics = "sequential", pointerCache, opIndexOffset = 0) {
2270
3540
  try {
3541
+ const compileOptions = toCompilePatchOptions(semantics, pointerCache, opIndexOffset);
3542
+ if (patch.length === 1) return {
3543
+ ok: true,
3544
+ intents: compileJsonPatchOpToIntent(baseJson, patch[0], compileOptions)
3545
+ };
2271
3546
  return {
2272
3547
  ok: true,
2273
- value: getAtJson(baseJson, path)
3548
+ intents: compileJsonPatchToIntent(baseJson, patch, compileOptions)
2274
3549
  };
2275
3550
  } catch (error) {
2276
- return toPointerLookupApplyError(error, pointer, opIndex);
3551
+ return toApplyError(error);
2277
3552
  }
2278
3553
  }
2279
- function bumpClockCounter(state, ctr) {
2280
- if (state.clock.ctr < ctr) state.clock.ctr = ctr;
3554
+ function toCompilePatchOptions(semantics, pointerCache, opIndexOffset = 0) {
3555
+ return {
3556
+ semantics,
3557
+ pointerCache,
3558
+ opIndexOffset
3559
+ };
2281
3560
  }
2282
- function compileIntents(baseJson, patch, semantics = "sequential", jsonValidation = "none") {
3561
+ function preparePatchPayloadsSafe(patch, mode) {
2283
3562
  try {
2284
3563
  return {
2285
3564
  ok: true,
2286
- intents: compileJsonPatchToIntent(baseJson, preparePatchPayloads(patch, jsonValidation), { semantics })
3565
+ patch: preparePatchPayloads(patch, mode)
2287
3566
  };
2288
3567
  } catch (error) {
2289
3568
  return toApplyError(error);
@@ -2323,40 +3602,6 @@ function mergePointerPaths(basePointer, nestedPointer) {
2323
3602
  if (basePointer === "") return nestedPointer;
2324
3603
  return `${basePointer}${nestedPointer}`;
2325
3604
  }
2326
- function maxCtrInNodeForActor$1(node, actor) {
2327
- let best = 0;
2328
- const stack = [{
2329
- node,
2330
- depth: 0
2331
- }];
2332
- while (stack.length > 0) {
2333
- const frame = stack.pop();
2334
- assertTraversalDepth(frame.depth);
2335
- if (frame.node.kind === "lww") {
2336
- if (frame.node.dot.actor === actor && frame.node.dot.ctr > best) best = frame.node.dot.ctr;
2337
- continue;
2338
- }
2339
- if (frame.node.kind === "obj") {
2340
- for (const entry of frame.node.entries.values()) {
2341
- if (entry.dot.actor === actor && entry.dot.ctr > best) best = entry.dot.ctr;
2342
- stack.push({
2343
- node: entry.node,
2344
- depth: frame.depth + 1
2345
- });
2346
- }
2347
- for (const tomb of frame.node.tombstone.values()) if (tomb.actor === actor && tomb.ctr > best) best = tomb.ctr;
2348
- continue;
2349
- }
2350
- for (const elem of frame.node.elems.values()) {
2351
- if (elem.insDot.actor === actor && elem.insDot.ctr > best) best = elem.insDot.ctr;
2352
- stack.push({
2353
- node: elem.value,
2354
- depth: frame.depth + 1
2355
- });
2356
- }
2357
- }
2358
- return best;
2359
- }
2360
3605
  function toApplyError(error) {
2361
3606
  if (error instanceof TraversalDepthError) return toDepthApplyError(error);
2362
3607
  if (error instanceof PatchCompileError) return {
@@ -2384,21 +3629,12 @@ function toPointerParseApplyError(error, pointer, opIndex) {
2384
3629
  opIndex
2385
3630
  };
2386
3631
  }
2387
- function toPointerLookupApplyError(error, pointer, opIndex) {
2388
- const mapped = mapLookupErrorToPatchReason(error);
2389
- return {
2390
- ok: false,
2391
- code: 409,
2392
- reason: mapped.reason,
2393
- message: mapped.message,
2394
- path: pointer,
2395
- opIndex
2396
- };
2397
- }
2398
3632
 
2399
3633
  //#endregion
2400
3634
  //#region src/serialize.ts
2401
3635
  const HEAD_ELEM_ID = "HEAD";
3636
+ const SERIALIZED_DOC_VERSION = 1;
3637
+ const SERIALIZED_STATE_VERSION = 1;
2402
3638
  function createSerializedRecord() {
2403
3639
  return Object.create(null);
2404
3640
  }
@@ -2423,13 +3659,16 @@ var DeserializeError = class extends Error {
2423
3659
  };
2424
3660
  /** Serialize a CRDT document to a JSON-safe representation (Maps become plain objects). */
2425
3661
  function serializeDoc(doc) {
2426
- return { root: serializeNode(doc.root) };
3662
+ return {
3663
+ version: SERIALIZED_DOC_VERSION,
3664
+ root: serializeNode(doc.root)
3665
+ };
2427
3666
  }
2428
3667
  /** Reconstruct a CRDT document from its serialized form. */
2429
3668
  function deserializeDoc(data) {
2430
- if (!isRecord(data)) fail("INVALID_SERIALIZED_SHAPE", "/", "serialized doc must be an object");
2431
- if (!("root" in data)) fail("INVALID_SERIALIZED_SHAPE", "/root", "serialized doc is missing root");
2432
- return { root: deserializeNode(data.root, "/root", 0) };
3669
+ const raw = readSerializedDocEnvelope(data);
3670
+ if (!("root" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/root", "serialized doc is missing root");
3671
+ return { root: deserializeNode(raw.root, "/root", 0) };
2433
3672
  }
2434
3673
  /** Non-throwing `deserializeDoc` variant with typed validation details. */
2435
3674
  function tryDeserializeDoc(data) {
@@ -2450,6 +3689,7 @@ function tryDeserializeDoc(data) {
2450
3689
  /** Serialize a full CRDT state (document + clock) to a JSON-safe representation. */
2451
3690
  function serializeState(state) {
2452
3691
  return {
3692
+ version: SERIALIZED_STATE_VERSION,
2453
3693
  doc: serializeDoc(state.doc),
2454
3694
  clock: {
2455
3695
  actor: state.clock.actor,
@@ -2457,16 +3697,24 @@ function serializeState(state) {
2457
3697
  }
2458
3698
  };
2459
3699
  }
2460
- /** Reconstruct a full CRDT state from its serialized form, restoring the clock. */
3700
+ /**
3701
+ * Reconstruct a full CRDT state from its serialized form, restoring the clock.
3702
+ *
3703
+ * May throw `TraversalDepthError` when the payload exceeds the maximum
3704
+ * supported nesting depth.
3705
+ */
2461
3706
  function deserializeState(data) {
2462
- if (!isRecord(data)) fail("INVALID_SERIALIZED_SHAPE", "/", "serialized state must be an object");
2463
- if (!("doc" in data)) fail("INVALID_SERIALIZED_SHAPE", "/doc", "serialized state is missing doc");
2464
- if (!("clock" in data)) fail("INVALID_SERIALIZED_SHAPE", "/clock", "serialized state is missing clock");
2465
- const clockRaw = asRecord(data.clock, "/clock");
2466
- const clock = createClock(readActor(clockRaw.actor, "/clock/actor"), readCounter(clockRaw.ctr, "/clock/ctr"));
3707
+ const raw = readSerializedStateEnvelope(data);
3708
+ if (!("doc" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/doc", "serialized state is missing doc");
3709
+ if (!("clock" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/clock", "serialized state is missing clock");
3710
+ const clockRaw = asRecord(raw.clock, "/clock");
3711
+ const actor = readActor(clockRaw.actor, "/clock/actor");
3712
+ const ctr = readCounter(clockRaw.ctr, "/clock/ctr");
3713
+ const doc = deserializeDoc(raw.doc);
3714
+ const observedCtr = observedVersionVector(doc)[actor] ?? 0;
2467
3715
  return {
2468
- doc: deserializeDoc(data.doc),
2469
- clock
3716
+ doc,
3717
+ clock: createClock(actor, Math.max(ctr, observedCtr))
2470
3718
  };
2471
3719
  }
2472
3720
  /** Non-throwing `deserializeState` variant with typed validation details. */
@@ -2515,21 +3763,38 @@ function serializeNode(node) {
2515
3763
  };
2516
3764
  }
2517
3765
  const elems = createSerializedRecord();
2518
- for (const [id, e] of node.elems.entries()) setSerializedRecordValue(elems, id, {
2519
- id: e.id,
2520
- prev: e.prev,
2521
- tombstone: e.tombstone,
2522
- value: serializeNode(e.value),
2523
- insDot: {
2524
- actor: e.insDot.actor,
2525
- ctr: e.insDot.ctr
2526
- }
2527
- });
3766
+ for (const [id, e] of node.elems.entries()) {
3767
+ const serializedElem = {
3768
+ id: e.id,
3769
+ prev: e.prev,
3770
+ tombstone: e.tombstone,
3771
+ value: serializeNode(e.value),
3772
+ insDot: {
3773
+ actor: e.insDot.actor,
3774
+ ctr: e.insDot.ctr
3775
+ }
3776
+ };
3777
+ if (e.delDot) serializedElem.delDot = {
3778
+ actor: e.delDot.actor,
3779
+ ctr: e.delDot.ctr
3780
+ };
3781
+ setSerializedRecordValue(elems, id, serializedElem);
3782
+ }
2528
3783
  return {
2529
3784
  kind: "seq",
2530
3785
  elems
2531
3786
  };
2532
3787
  }
3788
+ function readSerializedDocEnvelope(data) {
3789
+ const raw = asRecord(data, "/");
3790
+ assertSerializedEnvelopeVersion(raw, "/version", SERIALIZED_DOC_VERSION, "doc");
3791
+ return raw;
3792
+ }
3793
+ function readSerializedStateEnvelope(data) {
3794
+ const raw = asRecord(data, "/");
3795
+ assertSerializedEnvelopeVersion(raw, "/version", SERIALIZED_STATE_VERSION, "state");
3796
+ return raw;
3797
+ }
2533
3798
  function deserializeNode(node, path, depth) {
2534
3799
  assertTraversalDepth(depth);
2535
3800
  const raw = asRecord(node, path);
@@ -2575,11 +3840,14 @@ function deserializeNode(node, path, depth) {
2575
3840
  const tombstone = readBoolean(elem.tombstone, `${elemPath}/tombstone`);
2576
3841
  const value = deserializeNode(elem.value, `${elemPath}/value`, depth + 1);
2577
3842
  const insDot = readDot(elem.insDot, `${elemPath}/insDot`);
3843
+ const delDot = "delDot" in elem && elem.delDot !== void 0 ? readDot(elem.delDot, `${elemPath}/delDot`) : void 0;
2578
3844
  if (dotToElemId(insDot) !== id) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/insDot`, "sequence element id must match its insertion dot");
3845
+ if (!tombstone && delDot) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/delDot`, "live sequence elements must not include delete metadata");
2579
3846
  elems.set(id, {
2580
3847
  id,
2581
3848
  prev,
2582
3849
  tombstone,
3850
+ delDot,
2583
3851
  value,
2584
3852
  insDot
2585
3853
  });
@@ -2618,6 +3886,15 @@ function asRecord(value, path) {
2618
3886
  if (!isRecord(value)) fail("INVALID_SERIALIZED_SHAPE", path, "expected object");
2619
3887
  return value;
2620
3888
  }
3889
+ function assertSerializedEnvelopeVersion(raw, path, expectedVersion, label) {
3890
+ if (!("version" in raw)) return;
3891
+ const version = readVersion(raw.version, path);
3892
+ if (version !== expectedVersion) fail("INVALID_SERIALIZED_SHAPE", path, `unsupported serialized ${label} version '${version}'`);
3893
+ }
3894
+ function readVersion(value, path) {
3895
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) fail("INVALID_SERIALIZED_SHAPE", path, "envelope version must be a non-negative safe integer");
3896
+ return value;
3897
+ }
2621
3898
  function readDot(value, path) {
2622
3899
  const raw = asRecord(value, path);
2623
3900
  return {
@@ -2716,7 +3993,7 @@ function mergeDoc(a, b, options = {}) {
2716
3993
  function tryMergeDoc(a, b, options = {}) {
2717
3994
  try {
2718
3995
  const mismatchPath = options.requireSharedOrigin ?? true ? findSeqLineageMismatch(a.root, b.root, []) : null;
2719
- if (mismatchPath) return {
3996
+ if (mismatchPath !== null) return {
2720
3997
  ok: false,
2721
3998
  error: {
2722
3999
  ok: false,
@@ -2797,7 +4074,7 @@ function findSeqLineageMismatch(a, b, path) {
2797
4074
  shared = true;
2798
4075
  break;
2799
4076
  }
2800
- if (!shared) return `/${frame.path.join("/")}`;
4077
+ if (!shared) return stringifyJsonPointer(frame.path);
2801
4078
  }
2802
4079
  }
2803
4080
  if (frame.a.kind === "obj" && frame.b.kind === "obj") {
@@ -2820,45 +4097,11 @@ function findSeqLineageMismatch(a, b, path) {
2820
4097
  return null;
2821
4098
  }
2822
4099
  function maxObservedCtrForActor(doc, actor, a, b) {
2823
- let best = maxCtrInNodeForActor(doc.root, actor);
4100
+ let best = observedVersionVector(doc)[actor] ?? 0;
2824
4101
  if (a.clock.actor === actor && a.clock.ctr > best) best = a.clock.ctr;
2825
4102
  if (b.clock.actor === actor && b.clock.ctr > best) best = b.clock.ctr;
2826
4103
  return best;
2827
4104
  }
2828
- function maxCtrInNodeForActor(node, actor) {
2829
- let best = 0;
2830
- const stack = [{
2831
- node,
2832
- depth: 0
2833
- }];
2834
- while (stack.length > 0) {
2835
- const frame = stack.pop();
2836
- assertTraversalDepth(frame.depth);
2837
- if (frame.node.kind === "lww") {
2838
- if (frame.node.dot.actor === actor && frame.node.dot.ctr > best) best = frame.node.dot.ctr;
2839
- continue;
2840
- }
2841
- if (frame.node.kind === "obj") {
2842
- for (const entry of frame.node.entries.values()) {
2843
- if (entry.dot.actor === actor && entry.dot.ctr > best) best = entry.dot.ctr;
2844
- stack.push({
2845
- node: entry.node,
2846
- depth: frame.depth + 1
2847
- });
2848
- }
2849
- for (const tomb of frame.node.tombstone.values()) if (tomb.actor === actor && tomb.ctr > best) best = tomb.ctr;
2850
- continue;
2851
- }
2852
- for (const elem of frame.node.elems.values()) {
2853
- if (elem.insDot.actor === actor && elem.insDot.ctr > best) best = elem.insDot.ctr;
2854
- stack.push({
2855
- node: elem.value,
2856
- depth: frame.depth + 1
2857
- });
2858
- }
2859
- }
2860
- return best;
2861
- }
2862
4105
  function repDot(node) {
2863
4106
  switch (node.kind) {
2864
4107
  case "lww": return node.dot;
@@ -2951,13 +4194,14 @@ function mergeSeq(a, b, depth, path) {
2951
4194
  const ea = a.elems.get(id);
2952
4195
  const eb = b.elems.get(id);
2953
4196
  if (ea && eb) {
2954
- if (ea.prev !== eb.prev) throw new SharedElementMetadataMismatchError(toPointer(path), id, "prev");
2955
- if (!sameDot(ea.insDot, eb.insDot)) throw new SharedElementMetadataMismatchError(toPointer(path), id, "insDot");
4197
+ if (ea.prev !== eb.prev) throw new SharedElementMetadataMismatchError(stringifyJsonPointer(path), id, "prev");
4198
+ if (!sameDot(ea.insDot, eb.insDot)) throw new SharedElementMetadataMismatchError(stringifyJsonPointer(path), id, "insDot");
2956
4199
  const mergedValue = mergeNodeAtDepth(ea.value, eb.value, depth + 1, [...path, id]);
2957
4200
  elems.set(id, {
2958
4201
  id,
2959
4202
  prev: ea.prev,
2960
4203
  tombstone: ea.tombstone || eb.tombstone,
4204
+ delDot: mergeDeleteDot(ea.delDot, eb.delDot),
2961
4205
  value: mergedValue,
2962
4206
  insDot: { ...ea.insDot }
2963
4207
  });
@@ -2972,20 +4216,22 @@ function mergeSeq(a, b, depth, path) {
2972
4216
  function sameDot(a, b) {
2973
4217
  return a.actor === b.actor && a.ctr === b.ctr;
2974
4218
  }
2975
- function toPointer(path) {
2976
- if (path.length === 0) return "/";
2977
- return `/${path.join("/")}`;
2978
- }
2979
4219
  function cloneElem(e, depth) {
2980
4220
  assertTraversalDepth(depth);
2981
4221
  return {
2982
4222
  id: e.id,
2983
4223
  prev: e.prev,
2984
4224
  tombstone: e.tombstone,
4225
+ delDot: e.delDot ? { ...e.delDot } : void 0,
2985
4226
  value: cloneNodeShallow(e.value, depth + 1),
2986
4227
  insDot: { ...e.insDot }
2987
4228
  };
2988
4229
  }
4230
+ function mergeDeleteDot(a, b) {
4231
+ if (a && b) return compareDot(a, b) >= 0 ? { ...a } : { ...b };
4232
+ if (a) return { ...a };
4233
+ if (b) return { ...b };
4234
+ }
2989
4235
  function cloneNodeShallow(node, depth) {
2990
4236
  assertTraversalDepth(depth);
2991
4237
  switch (node.kind) {
@@ -3219,6 +4465,12 @@ Object.defineProperty(exports, 'compileJsonPatchToIntent', {
3219
4465
  return compileJsonPatchToIntent;
3220
4466
  }
3221
4467
  });
4468
+ Object.defineProperty(exports, 'crdtNodesToJsonPatch', {
4469
+ enumerable: true,
4470
+ get: function () {
4471
+ return crdtNodesToJsonPatch;
4472
+ }
4473
+ });
3222
4474
  Object.defineProperty(exports, 'crdtToFullReplace', {
3223
4475
  enumerable: true,
3224
4476
  get: function () {
@@ -3291,6 +4543,12 @@ Object.defineProperty(exports, 'getAtJson', {
3291
4543
  return getAtJson;
3292
4544
  }
3293
4545
  });
4546
+ Object.defineProperty(exports, 'intersectVersionVectors', {
4547
+ enumerable: true,
4548
+ get: function () {
4549
+ return intersectVersionVectors;
4550
+ }
4551
+ });
3294
4552
  Object.defineProperty(exports, 'jsonEquals', {
3295
4553
  enumerable: true,
3296
4554
  get: function () {
@@ -3333,6 +4591,12 @@ Object.defineProperty(exports, 'mergeState', {
3333
4591
  return mergeState;
3334
4592
  }
3335
4593
  });
4594
+ Object.defineProperty(exports, 'mergeVersionVectors', {
4595
+ enumerable: true,
4596
+ get: function () {
4597
+ return mergeVersionVectors;
4598
+ }
4599
+ });
3336
4600
  Object.defineProperty(exports, 'newObj', {
3337
4601
  enumerable: true,
3338
4602
  get: function () {
@@ -3381,6 +4645,12 @@ Object.defineProperty(exports, 'observeDot', {
3381
4645
  return observeDot;
3382
4646
  }
3383
4647
  });
4648
+ Object.defineProperty(exports, 'observedVersionVector', {
4649
+ enumerable: true,
4650
+ get: function () {
4651
+ return observedVersionVector;
4652
+ }
4653
+ });
3384
4654
  Object.defineProperty(exports, 'parseJsonPointer', {
3385
4655
  enumerable: true,
3386
4656
  get: function () {
@@ -3411,6 +4681,12 @@ Object.defineProperty(exports, 'rgaInsertAfter', {
3411
4681
  return rgaInsertAfter;
3412
4682
  }
3413
4683
  });
4684
+ Object.defineProperty(exports, 'rgaInsertAfterChecked', {
4685
+ enumerable: true,
4686
+ get: function () {
4687
+ return rgaInsertAfterChecked;
4688
+ }
4689
+ });
3414
4690
  Object.defineProperty(exports, 'rgaLinearizeIds', {
3415
4691
  enumerable: true,
3416
4692
  get: function () {
@@ -3435,6 +4711,12 @@ Object.defineProperty(exports, 'serializeState', {
3435
4711
  return serializeState;
3436
4712
  }
3437
4713
  });
4714
+ Object.defineProperty(exports, 'stableJsonValueKey', {
4715
+ enumerable: true,
4716
+ get: function () {
4717
+ return stableJsonValueKey;
4718
+ }
4719
+ });
3438
4720
  Object.defineProperty(exports, 'stringifyJsonPointer', {
3439
4721
  enumerable: true,
3440
4722
  get: function () {
@@ -3501,6 +4783,18 @@ Object.defineProperty(exports, 'validateJsonPatch', {
3501
4783
  return validateJsonPatch;
3502
4784
  }
3503
4785
  });
4786
+ Object.defineProperty(exports, 'validateRgaSeq', {
4787
+ enumerable: true,
4788
+ get: function () {
4789
+ return validateRgaSeq;
4790
+ }
4791
+ });
4792
+ Object.defineProperty(exports, 'versionVectorCovers', {
4793
+ enumerable: true,
4794
+ get: function () {
4795
+ return versionVectorCovers;
4796
+ }
4797
+ });
3504
4798
  Object.defineProperty(exports, 'vvHasDot', {
3505
4799
  enumerable: true,
3506
4800
  get: function () {