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