json-patch-to-crdt 0.1.1 → 0.1.3

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,10 +1,33 @@
1
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;
8
+ }
9
+ };
10
+ function readVvCounter$1(vv, actor) {
11
+ if (!Object.prototype.hasOwnProperty.call(vv, actor)) return 0;
12
+ const counter = vv[actor];
13
+ return typeof counter === "number" ? counter : 0;
14
+ }
15
+ function writeVvCounter$1(vv, actor, counter) {
16
+ Object.defineProperty(vv, actor, {
17
+ configurable: true,
18
+ enumerable: true,
19
+ value: counter,
20
+ writable: true
21
+ });
22
+ }
2
23
  /**
3
24
  * Create a new clock for the given actor. Each call to `clock.next()` yields a fresh `Dot`.
4
25
  * @param actor - Unique identifier for this peer.
5
26
  * @param start - Initial counter value (defaults to 0).
6
27
  */
7
28
  function createClock(actor, start = 0) {
29
+ assertActorId(actor);
30
+ assertCounter(start);
8
31
  const clock = {
9
32
  actor,
10
33
  ctr: start,
@@ -18,6 +41,12 @@ function createClock(actor, start = 0) {
18
41
  };
19
42
  return clock;
20
43
  }
44
+ function assertActorId(actor) {
45
+ if (actor.length === 0) throw new ClockValidationError("INVALID_ACTOR", "actor must not be empty");
46
+ }
47
+ function assertCounter(counter) {
48
+ if (!Number.isSafeInteger(counter) || counter < 0) throw new ClockValidationError("INVALID_COUNTER", "counter must be a non-negative safe integer");
49
+ }
21
50
  /** Create an independent copy of a clock at the same counter position. */
22
51
  function cloneClock(clock) {
23
52
  return createClock(clock.actor, clock.ctr);
@@ -27,8 +56,8 @@ function cloneClock(clock) {
27
56
  * Useful when a server needs to mint dots for many actors.
28
57
  */
29
58
  function nextDotForActor(vv, actor) {
30
- const ctr = (vv[actor] ?? 0) + 1;
31
- vv[actor] = ctr;
59
+ const ctr = readVvCounter$1(vv, actor) + 1;
60
+ writeVvCounter$1(vv, actor, ctr);
32
61
  return {
33
62
  actor,
34
63
  ctr
@@ -36,21 +65,62 @@ function nextDotForActor(vv, actor) {
36
65
  }
37
66
  /** Record an observed dot in a version vector. */
38
67
  function observeDot(vv, dot) {
39
- if ((vv[dot.actor] ?? 0) < dot.ctr) vv[dot.actor] = dot.ctr;
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
+ };
40
96
  }
41
97
 
42
98
  //#endregion
43
99
  //#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
+ }
44
113
  function compareDot(a, b) {
45
114
  if (a.ctr !== b.ctr) return a.ctr - b.ctr;
46
115
  return a.actor < b.actor ? -1 : a.actor > b.actor ? 1 : 0;
47
116
  }
48
117
  function vvHasDot(vv, d) {
49
- return (vv[d.actor] ?? 0) >= d.ctr;
118
+ return (readVvCounter(vv, d.actor) ?? 0) >= d.ctr;
50
119
  }
51
120
  function vvMerge(a, b) {
52
- const out = { ...a };
53
- for (const [actor, ctr] of Object.entries(b)) out[actor] = Math.max(out[actor] ?? 0, ctr);
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));
54
124
  return out;
55
125
  }
56
126
  function dotToElemId(d) {
@@ -84,15 +154,26 @@ function rgaLinearizeIds(seq) {
84
154
  if (cached && cached.version === ver) return cached.ids;
85
155
  const idx = rgaChildrenIndex(seq);
86
156
  const out = [];
87
- function walk(prev) {
88
- const children = idx.get(prev);
89
- if (!children) return;
90
- for (const c of children) {
91
- if (!c.tombstone) out.push(c.id);
92
- walk(c.id);
157
+ const stack = [];
158
+ const rootChildren = idx.get(HEAD);
159
+ if (rootChildren) stack.push({
160
+ children: rootChildren,
161
+ index: 0
162
+ });
163
+ while (stack.length > 0) {
164
+ const frame = stack[stack.length - 1];
165
+ if (frame.index >= frame.children.length) {
166
+ stack.pop();
167
+ continue;
93
168
  }
169
+ const child = frame.children[frame.index++];
170
+ if (!child.tombstone) out.push(child.id);
171
+ const grandchildren = idx.get(child.id);
172
+ if (grandchildren) stack.push({
173
+ children: grandchildren,
174
+ index: 0
175
+ });
94
176
  }
95
- walk(HEAD);
96
177
  linearCache.set(seq, {
97
178
  version: ver,
98
179
  ids: out
@@ -117,6 +198,61 @@ function rgaDelete(seq, id) {
117
198
  e.tombstone = true;
118
199
  bumpVersion(seq);
119
200
  }
201
+ /**
202
+ * Prune tombstoned elements that are causally stable and have no live descendants
203
+ * depending on them for sequence traversal.
204
+ *
205
+ * Returns the number of removed elements.
206
+ */
207
+ function rgaCompactTombstones(seq, isStable) {
208
+ if (seq.elems.size === 0) return 0;
209
+ const children = /* @__PURE__ */ new Map();
210
+ const roots = [];
211
+ for (const elem of seq.elems.values()) {
212
+ const byPrev = children.get(elem.prev);
213
+ if (byPrev) byPrev.push(elem.id);
214
+ else children.set(elem.prev, [elem.id]);
215
+ if (elem.prev === HEAD || !seq.elems.has(elem.prev)) roots.push(elem.id);
216
+ }
217
+ const removable = /* @__PURE__ */ new Set();
218
+ const visited = /* @__PURE__ */ new Set();
219
+ const stack = [];
220
+ const pushRoot = (id) => {
221
+ if (!visited.has(id)) stack.push({
222
+ id,
223
+ expanded: false
224
+ });
225
+ };
226
+ for (const id of roots) pushRoot(id);
227
+ for (const id of seq.elems.keys()) pushRoot(id);
228
+ while (stack.length > 0) {
229
+ const frame = stack.pop();
230
+ if (!frame.expanded) {
231
+ if (visited.has(frame.id)) continue;
232
+ visited.add(frame.id);
233
+ stack.push({
234
+ id: frame.id,
235
+ expanded: true
236
+ });
237
+ const childIds = children.get(frame.id);
238
+ if (childIds) {
239
+ for (const childId of childIds) if (!visited.has(childId)) stack.push({
240
+ id: childId,
241
+ expanded: false
242
+ });
243
+ }
244
+ continue;
245
+ }
246
+ const elem = seq.elems.get(frame.id);
247
+ if (!elem || !elem.tombstone || !isStable(elem.insDot)) continue;
248
+ const childIds = children.get(frame.id);
249
+ if (!childIds || childIds.every((childId) => removable.has(childId))) removable.add(frame.id);
250
+ }
251
+ if (removable.size === 0) return 0;
252
+ for (const id of removable) seq.elems.delete(id);
253
+ bumpVersion(seq);
254
+ return removable.size;
255
+ }
120
256
  function rgaIdAtIndex(seq, index) {
121
257
  return rgaLinearizeIds(seq)[index];
122
258
  }
@@ -128,17 +264,111 @@ function rgaPrevForInsertAtIndex(seq, index) {
128
264
 
129
265
  //#endregion
130
266
  //#region src/materialize.ts
131
- /** Recursively convert a CRDT node graph into a plain JSON value. */
267
+ function createMaterializedObject() {
268
+ return Object.create(null);
269
+ }
270
+ function setMaterializedProperty(out, key, value) {
271
+ Object.defineProperty(out, key, {
272
+ configurable: true,
273
+ enumerable: true,
274
+ value,
275
+ writable: true
276
+ });
277
+ }
278
+ /** Convert a CRDT node graph into a plain JSON value using an explicit stack. */
132
279
  function materialize(node) {
133
- switch (node.kind) {
134
- case "lww": return node.value;
135
- case "obj": {
136
- const out = {};
137
- for (const [k, { node: child }] of node.entries.entries()) out[k] = materialize(child);
138
- return out;
280
+ if (node.kind === "lww") return node.value;
281
+ const root = node.kind === "obj" ? createMaterializedObject() : [];
282
+ const stack = [];
283
+ if (node.kind === "obj") stack.push({
284
+ kind: "obj",
285
+ depth: 0,
286
+ entries: Array.from(node.entries.entries(), ([key, value]) => [key, value.node]),
287
+ index: 0,
288
+ out: root
289
+ });
290
+ else stack.push({
291
+ kind: "seq",
292
+ depth: 0,
293
+ ids: rgaLinearizeIds(node),
294
+ index: 0,
295
+ seq: node,
296
+ out: root
297
+ });
298
+ while (stack.length > 0) {
299
+ const frame = stack[stack.length - 1];
300
+ if (frame.kind === "obj") {
301
+ if (frame.index >= frame.entries.length) {
302
+ stack.pop();
303
+ continue;
304
+ }
305
+ const [key, child] = frame.entries[frame.index++];
306
+ const childDepth = frame.depth + 1;
307
+ assertTraversalDepth(childDepth);
308
+ if (child.kind === "lww") {
309
+ setMaterializedProperty(frame.out, key, child.value);
310
+ continue;
311
+ }
312
+ if (child.kind === "obj") {
313
+ const outObj = createMaterializedObject();
314
+ setMaterializedProperty(frame.out, key, outObj);
315
+ stack.push({
316
+ kind: "obj",
317
+ depth: childDepth,
318
+ entries: Array.from(child.entries.entries(), ([childKey, value]) => [childKey, value.node]),
319
+ index: 0,
320
+ out: outObj
321
+ });
322
+ continue;
323
+ }
324
+ const outArr = [];
325
+ setMaterializedProperty(frame.out, key, outArr);
326
+ stack.push({
327
+ kind: "seq",
328
+ depth: childDepth,
329
+ ids: rgaLinearizeIds(child),
330
+ index: 0,
331
+ seq: child,
332
+ out: outArr
333
+ });
334
+ continue;
139
335
  }
140
- case "seq": return rgaLinearizeIds(node).map((id) => materialize(node.elems.get(id).value));
336
+ if (frame.index >= frame.ids.length) {
337
+ stack.pop();
338
+ continue;
339
+ }
340
+ const id = frame.ids[frame.index++];
341
+ const child = frame.seq.elems.get(id).value;
342
+ const childDepth = frame.depth + 1;
343
+ assertTraversalDepth(childDepth);
344
+ if (child.kind === "lww") {
345
+ frame.out.push(child.value);
346
+ continue;
347
+ }
348
+ if (child.kind === "obj") {
349
+ const outObj = createMaterializedObject();
350
+ frame.out.push(outObj);
351
+ stack.push({
352
+ kind: "obj",
353
+ depth: childDepth,
354
+ entries: Array.from(child.entries.entries(), ([key, value]) => [key, value.node]),
355
+ index: 0,
356
+ out: outObj
357
+ });
358
+ continue;
359
+ }
360
+ const outArr = [];
361
+ frame.out.push(outArr);
362
+ stack.push({
363
+ kind: "seq",
364
+ depth: childDepth,
365
+ ids: rgaLinearizeIds(child),
366
+ index: 0,
367
+ seq: child,
368
+ out: outArr
369
+ });
141
370
  }
371
+ return root;
142
372
  }
143
373
 
144
374
  //#endregion
@@ -183,6 +413,168 @@ function objRemove(obj, key, dot) {
183
413
  if (!curDel || compareDot(curDel, dot) <= 0) obj.tombstone.set(key, dot);
184
414
  obj.entries.delete(key);
185
415
  }
416
+ /**
417
+ * Prune object tombstones that satisfy a caller-provided stability predicate.
418
+ * Returns the number of removed tombstone records.
419
+ */
420
+ function objCompactTombstones(obj, isStable) {
421
+ let removed = 0;
422
+ for (const [key, dot] of obj.tombstone.entries()) {
423
+ if (!isStable(dot)) continue;
424
+ obj.tombstone.delete(key);
425
+ removed += 1;
426
+ }
427
+ return removed;
428
+ }
429
+
430
+ //#endregion
431
+ //#region src/json-value.ts
432
+ /**
433
+ * Runtime validation error for values that are not JSON-compatible.
434
+ * `path` is an RFC 6901 pointer relative to the validated root.
435
+ */
436
+ var JsonValueValidationError = class extends TypeError {
437
+ path;
438
+ detail;
439
+ constructor(path, detail) {
440
+ super(`invalid JSON value at ${path === "" ? "<root>" : path}: ${detail}`);
441
+ this.name = "JsonValueValidationError";
442
+ this.path = path;
443
+ this.detail = detail;
444
+ }
445
+ };
446
+ /** Assert that a runtime value is JSON-compatible (including finite numbers only). */
447
+ function assertRuntimeJsonValue(value) {
448
+ const stack = [{
449
+ value,
450
+ path: "",
451
+ depth: 0
452
+ }];
453
+ while (stack.length > 0) {
454
+ const frame = stack.pop();
455
+ assertTraversalDepth(frame.depth);
456
+ if (isJsonPrimitive$1(frame.value)) continue;
457
+ if (Array.isArray(frame.value)) {
458
+ for (const [index, child] of frame.value.entries()) stack.push({
459
+ value: child,
460
+ path: appendPointerSegment(frame.path, String(index)),
461
+ depth: frame.depth + 1
462
+ });
463
+ continue;
464
+ }
465
+ if (isJsonObject(frame.value)) {
466
+ for (const [key, child] of Object.entries(frame.value)) stack.push({
467
+ value: child,
468
+ path: appendPointerSegment(frame.path, key),
469
+ depth: frame.depth + 1
470
+ });
471
+ continue;
472
+ }
473
+ throw new JsonValueValidationError(frame.path, describeInvalidValue(frame.value));
474
+ }
475
+ }
476
+ /**
477
+ * Normalize a runtime value to JSON-compatible data.
478
+ * - non-finite numbers -> null
479
+ * - invalid object-property values -> key omitted
480
+ * - invalid root / array values -> null
481
+ */
482
+ function normalizeRuntimeJsonValue(value) {
483
+ const rootHolder = {};
484
+ const stack = [{
485
+ value,
486
+ path: "",
487
+ depth: 0,
488
+ slot: { kind: "root" }
489
+ }];
490
+ while (stack.length > 0) {
491
+ const frame = stack.pop();
492
+ assertTraversalDepth(frame.depth);
493
+ if (isJsonPrimitive$1(frame.value)) {
494
+ assignSlot(frame.slot, frame.value, rootHolder);
495
+ continue;
496
+ }
497
+ if (Array.isArray(frame.value)) {
498
+ const out = [];
499
+ assignSlot(frame.slot, out, rootHolder);
500
+ for (const [index, child] of frame.value.entries()) stack.push({
501
+ value: child,
502
+ path: appendPointerSegment(frame.path, String(index)),
503
+ depth: frame.depth + 1,
504
+ slot: {
505
+ kind: "array",
506
+ out,
507
+ index
508
+ }
509
+ });
510
+ continue;
511
+ }
512
+ if (isJsonObject(frame.value)) {
513
+ const out = Object.create(null);
514
+ assignSlot(frame.slot, out, rootHolder);
515
+ for (const [key, child] of Object.entries(frame.value)) stack.push({
516
+ value: child,
517
+ path: appendPointerSegment(frame.path, key),
518
+ depth: frame.depth + 1,
519
+ slot: {
520
+ kind: "object",
521
+ out,
522
+ key
523
+ }
524
+ });
525
+ continue;
526
+ }
527
+ if (isNonFiniteNumber(frame.value)) {
528
+ assignSlot(frame.slot, null, rootHolder);
529
+ continue;
530
+ }
531
+ if (frame.slot.kind !== "object") assignSlot(frame.slot, null, rootHolder);
532
+ }
533
+ return rootHolder.value ?? null;
534
+ }
535
+ /** Runtime JSON guardrail helper shared by create/apply/diff paths. */
536
+ function coerceRuntimeJsonValue(value, mode) {
537
+ if (mode === "none") return value;
538
+ if (mode === "strict") {
539
+ assertRuntimeJsonValue(value);
540
+ return value;
541
+ }
542
+ return normalizeRuntimeJsonValue(value);
543
+ }
544
+ function assignSlot(slot, value, rootHolder) {
545
+ if (slot.kind === "root") {
546
+ rootHolder.value = value;
547
+ return;
548
+ }
549
+ if (slot.kind === "array") {
550
+ slot.out[slot.index] = value;
551
+ return;
552
+ }
553
+ slot.out[slot.key] = value;
554
+ }
555
+ function appendPointerSegment(path, segment) {
556
+ const escaped = segment.replaceAll("~", "~0").replaceAll("/", "~1");
557
+ if (path === "") return `/${escaped}`;
558
+ return `${path}/${escaped}`;
559
+ }
560
+ function isJsonPrimitive$1(value) {
561
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
562
+ return typeof value === "number" && Number.isFinite(value);
563
+ }
564
+ function isJsonObject(value) {
565
+ return typeof value === "object" && value !== null && !Array.isArray(value);
566
+ }
567
+ function isNonFiniteNumber(value) {
568
+ return typeof value === "number" && !Number.isFinite(value);
569
+ }
570
+ function describeInvalidValue(value) {
571
+ if (typeof value === "number") return `non-finite number (${String(value)})`;
572
+ if (value === void 0) return "undefined is not valid JSON";
573
+ if (typeof value === "bigint") return "bigint is not valid JSON";
574
+ if (typeof value === "symbol") return "symbol is not valid JSON";
575
+ if (typeof value === "function") return "function is not valid JSON";
576
+ return `unsupported value type (${typeof value})`;
577
+ }
186
578
 
187
579
  //#endregion
188
580
  //#region src/types.ts
@@ -194,6 +586,7 @@ const ROOT_KEY = "@@crdt/root";
194
586
 
195
587
  //#endregion
196
588
  //#region src/patch.ts
589
+ const DEFAULT_LCS_MAX_CELLS = 25e4;
197
590
  /** Structured compile error used to map patch validation failures to typed reasons. */
198
591
  var PatchCompileError = class extends Error {
199
592
  reason;
@@ -207,6 +600,17 @@ var PatchCompileError = class extends Error {
207
600
  this.opIndex = opIndex;
208
601
  }
209
602
  };
603
+ /** Structured lookup error thrown by `getAtJson`. */
604
+ var JsonLookupError = class extends Error {
605
+ code;
606
+ segment;
607
+ constructor(code, segment, message) {
608
+ super(message);
609
+ this.name = "JsonLookupError";
610
+ this.code = code;
611
+ this.segment = segment;
612
+ }
613
+ };
210
614
  /**
211
615
  * Parse an RFC 6901 JSON Pointer into a path array, unescaping `~1` and `~0`.
212
616
  * @param ptr - A JSON Pointer string (e.g. `"/a/b"` or `""`).
@@ -253,14 +657,15 @@ function unescapeJsonPointerToken(token) {
253
657
  function getAtJson(base, path) {
254
658
  let cur = base;
255
659
  for (const seg of path) if (Array.isArray(cur)) {
256
- if (!ARRAY_INDEX_TOKEN_PATTERN.test(seg)) throw new Error(`Expected array index, got ${seg}`);
660
+ if (!ARRAY_INDEX_TOKEN_PATTERN.test(seg)) throw new JsonLookupError("EXPECTED_ARRAY_INDEX", seg, `Expected array index, got '${seg}'`);
257
661
  const idx = Number(seg);
258
- if (idx < 0 || idx >= cur.length) throw new Error(`Index out of bounds at ${seg}`);
662
+ if (idx < 0 || idx >= cur.length) throw new JsonLookupError("INDEX_OUT_OF_BOUNDS", seg, `Index out of bounds at '${seg}'`);
259
663
  cur = cur[idx];
260
664
  } else if (cur && typeof cur === "object") {
261
- if (!(seg in cur)) throw new Error(`Missing key ${seg}`);
262
- cur = cur[seg];
263
- } else throw new Error(`Cannot traverse into non-container at ${seg}`);
665
+ const obj = cur;
666
+ if (!hasOwn(obj, seg)) throw new JsonLookupError("MISSING_KEY", seg, `Missing key '${seg}'`);
667
+ cur = obj[seg];
668
+ } else throw new JsonLookupError("NON_CONTAINER", seg, `Cannot traverse into non-container at '${seg}'`);
264
669
  return cur;
265
670
  }
266
671
  /**
@@ -274,12 +679,13 @@ function getAtJson(base, path) {
274
679
  function compileJsonPatchToIntent(baseJson, patch, options = {}) {
275
680
  const semantics = options.semantics ?? "sequential";
276
681
  let workingBase = semantics === "sequential" ? structuredClone(baseJson) : baseJson;
682
+ const pointerCache = /* @__PURE__ */ new Map();
277
683
  const intents = [];
278
684
  for (let opIndex = 0; opIndex < patch.length; opIndex++) {
279
685
  const op = patch[opIndex];
280
686
  const compileBase = semantics === "sequential" ? workingBase : baseJson;
281
- intents.push(...compileSingleOp(compileBase, op, opIndex, semantics));
282
- if (semantics === "sequential") workingBase = applyPatchOpToJson(workingBase, op, opIndex);
687
+ intents.push(...compileSingleOp(compileBase, op, opIndex, semantics, pointerCache));
688
+ if (semantics === "sequential") workingBase = applyPatchOpToJsonInPlace(workingBase, op, opIndex, pointerCache);
283
689
  }
284
690
  return intents;
285
691
  }
@@ -293,15 +699,26 @@ function compileJsonPatchToIntent(baseJson, patch, options = {}) {
293
699
  * @returns An array of JSON Patch operations that transform `base` into `next`.
294
700
  */
295
701
  function diffJsonPatch(base, next, options = {}) {
702
+ const runtimeMode = options.jsonValidation ?? "none";
703
+ const runtimeBase = coerceRuntimeJsonValue(base, runtimeMode);
704
+ const runtimeNext = coerceRuntimeJsonValue(next, runtimeMode);
296
705
  const ops = [];
297
- diffValue([], base, next, ops, options);
706
+ diffValue([], runtimeBase, runtimeNext, ops, options);
298
707
  return ops;
299
708
  }
300
709
  function diffValue(path, base, next, ops, options) {
301
710
  if (jsonEquals(base, next)) return;
302
711
  if (Array.isArray(base) || Array.isArray(next)) {
303
712
  if ((options.arrayStrategy ?? "lcs") === "lcs" && Array.isArray(base) && Array.isArray(next)) {
304
- diffArray(path, base, next, ops, options);
713
+ if (!shouldUseLcsDiff(base.length, next.length, options.lcsMaxCells)) {
714
+ ops.push({
715
+ op: "replace",
716
+ path: stringifyJsonPointer(path),
717
+ value: next
718
+ });
719
+ return;
720
+ }
721
+ diffArray(path, base, next, ops);
305
722
  return;
306
723
  }
307
724
  ops.push({
@@ -337,7 +754,7 @@ function diffValue(path, base, next, ops, options) {
337
754
  }
338
755
  for (const key of baseKeys) if (nextSet.has(key)) diffValue([...path, key], base[key], next[key], ops, options);
339
756
  }
340
- function diffArray(path, base, next, ops, _options) {
757
+ function diffArray(path, base, next, ops) {
341
758
  const n = base.length;
342
759
  const m = next.length;
343
760
  const lcs = Array.from({ length: n + 1 }, () => Array(m + 1).fill(0));
@@ -377,6 +794,12 @@ function diffArray(path, base, next, ops, _options) {
377
794
  }
378
795
  ops.push(...compactArrayOps(localOps));
379
796
  }
797
+ function shouldUseLcsDiff(baseLength, nextLength, lcsMaxCells) {
798
+ if (lcsMaxCells === Number.POSITIVE_INFINITY) return true;
799
+ const cap = lcsMaxCells ?? DEFAULT_LCS_MAX_CELLS;
800
+ if (!Number.isFinite(cap) || cap < 1) return false;
801
+ return (baseLength + 1) * (nextLength + 1) <= cap;
802
+ }
380
803
  function compactArrayOps(ops) {
381
804
  const out = [];
382
805
  for (let i = 0; i < ops.length; i++) {
@@ -413,7 +836,7 @@ function jsonEquals(a, b) {
413
836
  const bKeys = Object.keys(b);
414
837
  if (aKeys.length !== bKeys.length) return false;
415
838
  for (const key of aKeys) {
416
- if (!(key in b)) return false;
839
+ if (!hasOwn(b, key)) return false;
417
840
  if (!jsonEquals(a[key], b[key])) return false;
418
841
  }
419
842
  return true;
@@ -425,6 +848,9 @@ const ARRAY_INDEX_TOKEN_PATTERN = /^(0|[1-9][0-9]*)$/;
425
848
  function hasOwn(value, key) {
426
849
  return Object.prototype.hasOwnProperty.call(value, key);
427
850
  }
851
+ function isUnsafeObjectKey(key) {
852
+ return key === "__proto__";
853
+ }
428
854
  function pathValueAt(base, path) {
429
855
  if (path.length === 0) return base;
430
856
  return getAtJson(base, path);
@@ -432,17 +858,17 @@ function pathValueAt(base, path) {
432
858
  function assertNever$1(_value, message) {
433
859
  throw new Error(message);
434
860
  }
435
- function compileSingleOp(baseJson, op, opIndex, semantics) {
861
+ function compileSingleOp(baseJson, op, opIndex, semantics, pointerCache) {
436
862
  if (op.op === "test") return [{
437
863
  t: "Test",
438
- path: parsePointerOrThrow(op.path, op.path, opIndex),
864
+ path: parsePointerOrThrow(op.path, op.path, opIndex, pointerCache),
439
865
  value: op.value
440
866
  }];
441
867
  if (op.op === "copy" || op.op === "move") {
442
- const fromPath = parsePointerOrThrow(op.from, op.from, opIndex);
443
- const toPath = parsePointerOrThrow(op.path, op.path, opIndex);
868
+ const fromPath = parsePointerOrThrow(op.from, op.from, opIndex, pointerCache);
869
+ const toPath = parsePointerOrThrow(op.path, op.path, opIndex, pointerCache);
444
870
  if (op.op === "move" && isStrictDescendantPath(fromPath, toPath)) throw compileError("INVALID_MOVE", `cannot move a value into one of its descendants at ${op.path}`, op.path, opIndex);
445
- const val = lookupValueOrThrow(baseJson, fromPath, op.from, opIndex);
871
+ const val = structuredClone(lookupValueOrThrow(baseJson, fromPath, op.from, opIndex));
446
872
  if (op.op === "move" && isSamePath(fromPath, toPath)) return [];
447
873
  if (op.op === "move" && semantics === "sequential") {
448
874
  const removeOp = {
@@ -454,21 +880,21 @@ function compileSingleOp(baseJson, op, opIndex, semantics) {
454
880
  path: op.path,
455
881
  value: val
456
882
  };
457
- const baseAfterRemove = applyPatchOpToJson(baseJson, removeOp, opIndex);
458
- return [...compileSingleOp(baseJson, removeOp, opIndex, semantics), ...compileSingleOp(baseAfterRemove, addOp, opIndex, semantics)];
883
+ const baseAfterRemove = applyPatchOpToJson(baseJson, removeOp, opIndex, pointerCache);
884
+ return [...compileSingleOp(baseJson, removeOp, opIndex, semantics, pointerCache), ...compileSingleOp(baseAfterRemove, addOp, opIndex, semantics, pointerCache)];
459
885
  }
460
886
  const out = compileSingleOp(baseJson, {
461
887
  op: "add",
462
888
  path: op.path,
463
889
  value: val
464
- }, opIndex, semantics);
890
+ }, opIndex, semantics, pointerCache);
465
891
  if (op.op === "move") out.push(...compileSingleOp(baseJson, {
466
892
  op: "remove",
467
893
  path: op.from
468
- }, opIndex, semantics));
894
+ }, opIndex, semantics, pointerCache));
469
895
  return out;
470
896
  }
471
- const path = parsePointerOrThrow(op.path, op.path, opIndex);
897
+ const path = parsePointerOrThrow(op.path, op.path, opIndex, pointerCache);
472
898
  if (path.length === 0) {
473
899
  if (op.op === "replace" || op.op === "add") return [{
474
900
  t: "ObjSet",
@@ -504,6 +930,7 @@ function compileSingleOp(baseJson, op, opIndex, semantics) {
504
930
  return assertNever$1(op, "Unsupported op at array path");
505
931
  }
506
932
  if (!isPlainObject(parentValue)) throw compileError("INVALID_TARGET", `expected object or array parent at ${parentPath}`, parentPath, opIndex);
933
+ if (isUnsafeObjectKey(token)) throw compileError("INVALID_POINTER", `unsafe object key at ${op.path}`, op.path, opIndex);
507
934
  if ((op.op === "replace" || op.op === "remove") && !hasOwn(parentValue, token)) throw compileError("MISSING_TARGET", `missing key ${token} at ${parentPath}`, op.path, opIndex);
508
935
  if (op.op === "add") return [{
509
936
  t: "ObjSet",
@@ -526,32 +953,31 @@ function compileSingleOp(baseJson, op, opIndex, semantics) {
526
953
  }];
527
954
  return assertNever$1(op, "Unsupported op");
528
955
  }
529
- function applyPatchOpToJson(baseJson, op, opIndex) {
530
- let doc = structuredClone(baseJson);
956
+ function applyPatchOpToJson(baseJson, op, opIndex, pointerCache) {
957
+ return applyPatchOpToJsonInPlace(structuredClone(baseJson), op, opIndex, pointerCache);
958
+ }
959
+ function applyPatchOpToJsonInPlace(doc, op, opIndex, pointerCache) {
531
960
  if (op.op === "test") return doc;
532
961
  if (op.op === "copy" || op.op === "move") {
533
- const fromPath = parsePointerOrThrow(op.from, op.from, opIndex);
962
+ const fromPath = parsePointerOrThrow(op.from, op.from, opIndex, pointerCache);
534
963
  const value = structuredClone(lookupValueOrThrow(doc, fromPath, op.from, opIndex));
535
- if (op.op === "move") doc = applyPatchOpToJson(doc, {
964
+ return applyPatchOpToJsonInPlace(op.op === "move" ? applyPatchOpToJsonInPlace(doc, {
536
965
  op: "remove",
537
966
  path: op.from
538
- }, opIndex);
539
- return applyPatchOpToJson(doc, {
967
+ }, opIndex, pointerCache) : doc, {
540
968
  op: "add",
541
969
  path: op.path,
542
970
  value
543
- }, opIndex);
971
+ }, opIndex, pointerCache);
544
972
  }
545
- const path = parsePointerOrThrow(op.path, op.path, opIndex);
973
+ const path = parsePointerOrThrow(op.path, op.path, opIndex, pointerCache);
546
974
  if (path.length === 0) {
547
975
  if (op.op === "add" || op.op === "replace") return structuredClone(op.value);
548
976
  throw compileError("INVALID_TARGET", "remove at root path is not supported in RFC-compliant mode", op.path, opIndex);
549
977
  }
550
978
  const parentPath = path.slice(0, -1);
551
979
  const token = path[path.length - 1];
552
- let parent;
553
- if (parentPath.length === 0) parent = doc;
554
- else parent = lookupValueOrThrow(doc, parentPath, op.path, opIndex);
980
+ const parent = parentPath.length === 0 ? doc : lookupValueOrThrow(doc, parentPath, op.path, opIndex);
555
981
  if (Array.isArray(parent)) {
556
982
  const index = parseArrayIndexToken(token, op.op, parent.length, op.path, opIndex);
557
983
  if (op.op === "add") {
@@ -567,6 +993,7 @@ function applyPatchOpToJson(baseJson, op, opIndex) {
567
993
  return doc;
568
994
  }
569
995
  if (!isPlainObject(parent)) throw compileError("INVALID_TARGET", `expected object or array parent at ${stringifyJsonPointer(parentPath)}`, op.path, opIndex);
996
+ if (isUnsafeObjectKey(token)) throw compileError("INVALID_POINTER", `unsafe object key at ${op.path}`, op.path, opIndex);
570
997
  if (op.op === "add" || op.op === "replace") {
571
998
  parent[token] = structuredClone(op.value);
572
999
  return doc;
@@ -574,9 +1001,13 @@ function applyPatchOpToJson(baseJson, op, opIndex) {
574
1001
  delete parent[token];
575
1002
  return doc;
576
1003
  }
577
- function parsePointerOrThrow(ptr, path, opIndex) {
1004
+ function parsePointerOrThrow(ptr, path, opIndex, pointerCache) {
1005
+ const cached = pointerCache.get(ptr);
1006
+ if (cached) return cached.slice();
578
1007
  try {
579
- return parseJsonPointer(ptr);
1008
+ const parsed = parseJsonPointer(ptr);
1009
+ pointerCache.set(ptr, parsed);
1010
+ return parsed.slice();
580
1011
  } catch (error) {
581
1012
  throw compileError("INVALID_POINTER", error instanceof Error ? error.message : "invalid pointer", path, opIndex);
582
1013
  }
@@ -614,26 +1045,31 @@ function compileErrorFromLookup(error, path, opIndex) {
614
1045
  return compileError(mapped.reason, mapped.message, path, opIndex);
615
1046
  }
616
1047
  function mapLookupErrorToPatchReason(error) {
617
- const message = error instanceof Error ? error.message : "invalid path";
618
- if (message.includes("Expected array index")) return {
619
- reason: "INVALID_POINTER",
620
- message
621
- };
622
- if (message.includes("Index out of bounds")) return {
623
- reason: "OUT_OF_BOUNDS",
624
- message
625
- };
626
- if (message.includes("Missing key")) return {
627
- reason: "MISSING_PARENT",
628
- message
629
- };
630
- if (message.includes("Cannot traverse into non-container")) return {
631
- reason: "INVALID_TARGET",
632
- message
633
- };
1048
+ if (error instanceof JsonLookupError) switch (error.code) {
1049
+ case "EXPECTED_ARRAY_INDEX": return {
1050
+ reason: "INVALID_POINTER",
1051
+ message: error.message
1052
+ };
1053
+ case "INDEX_OUT_OF_BOUNDS": return {
1054
+ reason: "OUT_OF_BOUNDS",
1055
+ message: error.message
1056
+ };
1057
+ case "MISSING_KEY": return {
1058
+ reason: "MISSING_PARENT",
1059
+ message: error.message
1060
+ };
1061
+ case "NON_CONTAINER": return {
1062
+ reason: "INVALID_TARGET",
1063
+ message: error.message
1064
+ };
1065
+ default: return {
1066
+ reason: "INVALID_PATCH",
1067
+ message: error.message
1068
+ };
1069
+ }
634
1070
  return {
635
1071
  reason: "INVALID_PATCH",
636
- message
1072
+ message: error instanceof Error ? error.message : "invalid path"
637
1073
  };
638
1074
  }
639
1075
  function compileError(reason, message, path, opIndex) {
@@ -750,7 +1186,43 @@ function ensureSeqAtPath(head, path, dotForCreate) {
750
1186
  if (head.root.kind !== "seq") head.root = newSeq();
751
1187
  return head.root;
752
1188
  }
1189
+ function getNodeAtPath(doc, path) {
1190
+ let cur = doc.root;
1191
+ for (const seg of path) {
1192
+ if (cur.kind !== "obj") return;
1193
+ const ent = cur.entries.get(seg);
1194
+ if (!ent) return;
1195
+ cur = ent.node;
1196
+ }
1197
+ return cur;
1198
+ }
1199
+ function getHeadSeqForBaseArrayIntent(head, path) {
1200
+ const pointer = `/${path.join("/")}`;
1201
+ const headNode = getNodeAtPath(head, path);
1202
+ if (!headNode) return {
1203
+ ok: false,
1204
+ code: 409,
1205
+ reason: "MISSING_PARENT",
1206
+ message: `head array missing at ${pointer}`,
1207
+ path: pointer
1208
+ };
1209
+ if (headNode.kind !== "seq") return {
1210
+ ok: false,
1211
+ code: 409,
1212
+ reason: "INVALID_TARGET",
1213
+ message: `expected array at ${pointer}`,
1214
+ path: pointer
1215
+ };
1216
+ return {
1217
+ ok: true,
1218
+ seq: headNode
1219
+ };
1220
+ }
753
1221
  function deepNodeFromJson(value, dot) {
1222
+ return deepNodeFromJsonWithDepth(value, dot, 0);
1223
+ }
1224
+ function deepNodeFromJsonWithDepth(value, dot, depth) {
1225
+ assertTraversalDepth(depth);
754
1226
  if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return newReg(value, dot);
755
1227
  if (Array.isArray(value)) {
756
1228
  const seq = newSeq();
@@ -762,40 +1234,123 @@ function deepNodeFromJson(value, dot) {
762
1234
  ctr: ++ctr
763
1235
  };
764
1236
  const id = dotToElemId(childDot);
765
- rgaInsertAfter(seq, prev, id, childDot, deepNodeFromJson(v, childDot));
1237
+ rgaInsertAfter(seq, prev, id, childDot, deepNodeFromJsonWithDepth(v, childDot, depth + 1));
766
1238
  prev = id;
767
1239
  }
768
1240
  return seq;
769
1241
  }
770
1242
  const obj = newObj();
771
- for (const [k, v] of Object.entries(value)) objSet(obj, k, deepNodeFromJson(v, dot), dot);
1243
+ for (const [k, v] of Object.entries(value)) objSet(obj, k, deepNodeFromJsonWithDepth(v, dot, depth + 1), dot);
772
1244
  return obj;
773
1245
  }
774
1246
  function nodeFromJson(value, nextDot) {
775
- if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return newReg(value, nextDot());
776
- if (Array.isArray(value)) {
777
- const seq = newSeq();
778
- let prev = HEAD;
779
- for (const v of value) {
780
- const insDot = nextDot();
781
- const id = dotToElemId(insDot);
782
- rgaInsertAfter(seq, prev, id, insDot, nodeFromJson(v, nextDot));
783
- prev = id;
1247
+ if (isJsonPrimitive(value)) return newReg(value, nextDot());
1248
+ const root = Array.isArray(value) ? newSeq() : newObj();
1249
+ const stack = [];
1250
+ if (Array.isArray(value)) stack.push({
1251
+ kind: "seq",
1252
+ depth: 0,
1253
+ values: value,
1254
+ index: 0,
1255
+ prev: HEAD,
1256
+ target: root
1257
+ });
1258
+ else stack.push({
1259
+ kind: "obj",
1260
+ depth: 0,
1261
+ entries: Object.entries(value),
1262
+ index: 0,
1263
+ target: root
1264
+ });
1265
+ while (stack.length > 0) {
1266
+ const frame = stack[stack.length - 1];
1267
+ if (frame.kind === "obj") {
1268
+ if (frame.index >= frame.entries.length) {
1269
+ stack.pop();
1270
+ continue;
1271
+ }
1272
+ const [key, childValue] = frame.entries[frame.index++];
1273
+ const childDepth = frame.depth + 1;
1274
+ assertTraversalDepth(childDepth);
1275
+ const entryDot = nextDot();
1276
+ if (isJsonPrimitive(childValue)) {
1277
+ objSet(frame.target, key, newReg(childValue, nextDot()), entryDot);
1278
+ continue;
1279
+ }
1280
+ if (Array.isArray(childValue)) {
1281
+ const childSeq = newSeq();
1282
+ objSet(frame.target, key, childSeq, entryDot);
1283
+ stack.push({
1284
+ kind: "seq",
1285
+ depth: childDepth,
1286
+ values: childValue,
1287
+ index: 0,
1288
+ prev: HEAD,
1289
+ target: childSeq
1290
+ });
1291
+ continue;
1292
+ }
1293
+ const childObj = newObj();
1294
+ objSet(frame.target, key, childObj, entryDot);
1295
+ stack.push({
1296
+ kind: "obj",
1297
+ depth: childDepth,
1298
+ entries: Object.entries(childValue),
1299
+ index: 0,
1300
+ target: childObj
1301
+ });
1302
+ continue;
784
1303
  }
785
- return seq;
786
- }
787
- const obj = newObj();
788
- for (const [k, v] of Object.entries(value)) {
789
- const entryDot = nextDot();
790
- objSet(obj, k, nodeFromJson(v, nextDot), entryDot);
1304
+ if (frame.index >= frame.values.length) {
1305
+ stack.pop();
1306
+ continue;
1307
+ }
1308
+ const childValue = frame.values[frame.index++];
1309
+ const childDepth = frame.depth + 1;
1310
+ assertTraversalDepth(childDepth);
1311
+ const insDot = nextDot();
1312
+ const id = dotToElemId(insDot);
1313
+ if (isJsonPrimitive(childValue)) {
1314
+ rgaInsertAfter(frame.target, frame.prev, id, insDot, newReg(childValue, nextDot()));
1315
+ frame.prev = id;
1316
+ continue;
1317
+ }
1318
+ if (Array.isArray(childValue)) {
1319
+ const childSeq = newSeq();
1320
+ rgaInsertAfter(frame.target, frame.prev, id, insDot, childSeq);
1321
+ frame.prev = id;
1322
+ stack.push({
1323
+ kind: "seq",
1324
+ depth: childDepth,
1325
+ values: childValue,
1326
+ index: 0,
1327
+ prev: HEAD,
1328
+ target: childSeq
1329
+ });
1330
+ continue;
1331
+ }
1332
+ const childObj = newObj();
1333
+ rgaInsertAfter(frame.target, frame.prev, id, insDot, childObj);
1334
+ frame.prev = id;
1335
+ stack.push({
1336
+ kind: "obj",
1337
+ depth: childDepth,
1338
+ entries: Object.entries(childValue),
1339
+ index: 0,
1340
+ target: childObj
1341
+ });
791
1342
  }
792
- return obj;
1343
+ return root;
793
1344
  }
794
1345
  /** Deep-clone a CRDT document. The clone is fully independent of the original. */
795
1346
  function cloneDoc(doc) {
796
1347
  return { root: cloneNode(doc.root) };
797
1348
  }
798
1349
  function cloneNode(node) {
1350
+ return cloneNodeAtDepth(node, 0);
1351
+ }
1352
+ function cloneNodeAtDepth(node, depth) {
1353
+ assertTraversalDepth(depth);
799
1354
  if (node.kind === "lww") return {
800
1355
  kind: "lww",
801
1356
  value: structuredClone(node.value),
@@ -807,7 +1362,7 @@ function cloneNode(node) {
807
1362
  if (node.kind === "obj") {
808
1363
  const entries = /* @__PURE__ */ new Map();
809
1364
  for (const [k, v] of node.entries.entries()) entries.set(k, {
810
- node: cloneNode(v.node),
1365
+ node: cloneNodeAtDepth(v.node, depth + 1),
811
1366
  dot: {
812
1367
  actor: v.dot.actor,
813
1368
  ctr: v.dot.ctr
@@ -829,7 +1384,7 @@ function cloneNode(node) {
829
1384
  id: e.id,
830
1385
  prev: e.prev,
831
1386
  tombstone: e.tombstone,
832
- value: cloneNode(e.value),
1387
+ value: cloneNodeAtDepth(e.value, depth + 1),
833
1388
  insDot: {
834
1389
  actor: e.insDot.actor,
835
1390
  ctr: e.insDot.ctr
@@ -840,6 +1395,9 @@ function cloneNode(node) {
840
1395
  elems
841
1396
  };
842
1397
  }
1398
+ function isJsonPrimitive(value) {
1399
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
1400
+ }
843
1401
  function applyTest(base, head, it, evalTestAgainst) {
844
1402
  const snapshot = evalTestAgainst === "head" ? materialize(head.root) : materialize(base.root);
845
1403
  let got;
@@ -909,13 +1467,23 @@ function applyObjRemove(head, it, newDot) {
909
1467
  objRemove(parentObj, it.key, d);
910
1468
  return null;
911
1469
  }
912
- function applyArrInsert(base, head, it, newDot, bumpCounterAbove) {
1470
+ function applyArrInsert(base, head, it, newDot, bumpCounterAbove, strictParents = false) {
1471
+ const pointer = `/${it.path.join("/")}`;
913
1472
  const baseSeq = getSeqAtPath(base, it.path);
914
1473
  if (!baseSeq) {
1474
+ if (strictParents) return {
1475
+ ok: false,
1476
+ code: 409,
1477
+ reason: "MISSING_PARENT",
1478
+ message: `base array missing at /${it.path.join("/")}`,
1479
+ path: pointer
1480
+ };
915
1481
  if (it.index === 0 || it.index === Number.POSITIVE_INFINITY) {
916
1482
  const headSeq = ensureSeqAtPath(head, it.path, newDot());
917
1483
  const prev = it.index === 0 ? HEAD : rgaPrevForInsertAtIndex(headSeq, Number.MAX_SAFE_INTEGER);
918
- const d = nextInsertDotForPrev(headSeq, prev, newDot, bumpCounterAbove);
1484
+ const dotRes = nextInsertDotForPrev(headSeq, prev, newDot, pointer, bumpCounterAbove);
1485
+ if (!dotRes.ok) return dotRes;
1486
+ const d = dotRes.dot;
919
1487
  rgaInsertAfter(headSeq, prev, dotToElemId(d), d, nodeFromJson(it.value, newDot));
920
1488
  return null;
921
1489
  }
@@ -924,10 +1492,13 @@ function applyArrInsert(base, head, it, newDot, bumpCounterAbove) {
924
1492
  code: 409,
925
1493
  reason: "MISSING_PARENT",
926
1494
  message: `base array missing at /${it.path.join("/")}`,
927
- path: `/${it.path.join("/")}`
1495
+ path: pointer
928
1496
  };
929
1497
  }
930
- const headSeq = ensureSeqAtPath(head, it.path, newDot());
1498
+ newDot();
1499
+ const headSeqRes = getHeadSeqForBaseArrayIntent(head, it.path);
1500
+ if (!headSeqRes.ok) return headSeqRes;
1501
+ const headSeq = headSeqRes.seq;
931
1502
  const idx = it.index === Number.POSITIVE_INFINITY ? rgaLinearizeIds(baseSeq).length : it.index;
932
1503
  const baseLen = rgaLinearizeIds(baseSeq).length;
933
1504
  if (idx < 0 || idx > baseLen) return {
@@ -938,23 +1509,41 @@ function applyArrInsert(base, head, it, newDot, bumpCounterAbove) {
938
1509
  path: `/${it.path.join("/")}/${it.index}`
939
1510
  };
940
1511
  const prev = idx === 0 ? HEAD : rgaIdAtIndex(baseSeq, idx - 1) ?? HEAD;
941
- const d = nextInsertDotForPrev(headSeq, prev, newDot, bumpCounterAbove);
1512
+ const dotRes = nextInsertDotForPrev(headSeq, prev, newDot, pointer, bumpCounterAbove);
1513
+ if (!dotRes.ok) return dotRes;
1514
+ const d = dotRes.dot;
942
1515
  rgaInsertAfter(headSeq, prev, dotToElemId(d), d, nodeFromJson(it.value, newDot));
943
1516
  return null;
944
1517
  }
945
- function nextInsertDotForPrev(seq, prev, newDot, bumpCounterAbove) {
1518
+ function nextInsertDotForPrev(seq, prev, newDot, path, bumpCounterAbove) {
1519
+ const MAX_INSERT_DOT_ATTEMPTS = 1024;
946
1520
  let maxSiblingDot = null;
947
1521
  for (const elem of seq.elems.values()) {
948
1522
  if (elem.prev !== prev) continue;
949
1523
  if (!maxSiblingDot || compareDot(elem.insDot, maxSiblingDot) > 0) maxSiblingDot = elem.insDot;
950
1524
  }
951
1525
  if (maxSiblingDot) bumpCounterAbove?.(maxSiblingDot.ctr);
952
- let candidate = newDot();
953
- while (maxSiblingDot && compareDot(candidate, maxSiblingDot) <= 0) candidate = newDot();
954
- return candidate;
1526
+ if (!maxSiblingDot) return {
1527
+ ok: true,
1528
+ dot: newDot()
1529
+ };
1530
+ for (let attempts = 0; attempts < MAX_INSERT_DOT_ATTEMPTS; attempts++) {
1531
+ const candidate = newDot();
1532
+ if (compareDot(candidate, maxSiblingDot) > 0) return {
1533
+ ok: true,
1534
+ dot: candidate
1535
+ };
1536
+ }
1537
+ return {
1538
+ ok: false,
1539
+ code: 409,
1540
+ reason: "DOT_GENERATION_EXHAUSTED",
1541
+ message: `failed to generate insert dot within ${MAX_INSERT_DOT_ATTEMPTS} attempts`,
1542
+ path
1543
+ };
955
1544
  }
956
1545
  function applyArrDelete(base, head, it, newDot) {
957
- const d = newDot();
1546
+ newDot();
958
1547
  const baseSeq = getSeqAtPath(base, it.path);
959
1548
  if (!baseSeq) return {
960
1549
  ok: false,
@@ -963,7 +1552,9 @@ function applyArrDelete(base, head, it, newDot) {
963
1552
  message: `base array missing at /${it.path.join("/")}`,
964
1553
  path: `/${it.path.join("/")}`
965
1554
  };
966
- const headSeq = ensureSeqAtPath(head, it.path, d);
1555
+ const headSeqRes = getHeadSeqForBaseArrayIntent(head, it.path);
1556
+ if (!headSeqRes.ok) return headSeqRes;
1557
+ const headSeq = headSeqRes.seq;
967
1558
  const baseId = rgaIdAtIndex(baseSeq, it.index);
968
1559
  if (!baseId) return {
969
1560
  ok: false,
@@ -972,11 +1563,18 @@ function applyArrDelete(base, head, it, newDot) {
972
1563
  message: `no base element at index ${it.index}`,
973
1564
  path: `/${it.path.join("/")}/${it.index}`
974
1565
  };
1566
+ if (!headSeq.elems.get(baseId)) return {
1567
+ ok: false,
1568
+ code: 409,
1569
+ reason: "MISSING_TARGET",
1570
+ message: `element missing in head lineage at index ${it.index}`,
1571
+ path: `/${it.path.join("/")}/${it.index}`
1572
+ };
975
1573
  rgaDelete(headSeq, baseId);
976
1574
  return null;
977
1575
  }
978
1576
  function applyArrReplace(base, head, it, newDot) {
979
- const d = newDot();
1577
+ newDot();
980
1578
  const baseSeq = getSeqAtPath(base, it.path);
981
1579
  if (!baseSeq) return {
982
1580
  ok: false,
@@ -985,7 +1583,9 @@ function applyArrReplace(base, head, it, newDot) {
985
1583
  message: `base array missing at /${it.path.join("/")}`,
986
1584
  path: `/${it.path.join("/")}`
987
1585
  };
988
- const headSeq = ensureSeqAtPath(head, it.path, d);
1586
+ const headSeqRes = getHeadSeqForBaseArrayIntent(head, it.path);
1587
+ if (!headSeqRes.ok) return headSeqRes;
1588
+ const headSeq = headSeqRes.seq;
989
1589
  const baseId = rgaIdAtIndex(baseSeq, it.index);
990
1590
  if (!baseId) return {
991
1591
  ok: false,
@@ -1014,9 +1614,11 @@ function applyArrReplace(base, head, it, newDot) {
1014
1614
  * @param newDot - A function that generates a unique `Dot` per mutation.
1015
1615
  * @param evalTestAgainst - Whether `test` ops are evaluated against `"head"` or `"base"`.
1016
1616
  * @param bumpCounterAbove - Optional hook that can fast-forward the underlying counter before inserts.
1617
+ * @param options - Optional behavior toggles.
1618
+ * @param options.strictParents - When `true`, reject array inserts whose base parent path is missing.
1017
1619
  * @returns `{ ok: true }` on success, or `{ ok: false, code: 409, message }` on conflict.
1018
1620
  */
1019
- function applyIntentsToCrdt(base, head, intents, newDot, evalTestAgainst = "head", bumpCounterAbove) {
1621
+ function applyIntentsToCrdt(base, head, intents, newDot, evalTestAgainst = "head", bumpCounterAbove, options = {}) {
1020
1622
  for (const it of intents) {
1021
1623
  let fail = null;
1022
1624
  switch (it.t) {
@@ -1030,7 +1632,7 @@ function applyIntentsToCrdt(base, head, intents, newDot, evalTestAgainst = "head
1030
1632
  fail = applyObjRemove(head, it, newDot);
1031
1633
  break;
1032
1634
  case "ArrInsert":
1033
- fail = applyArrInsert(base, head, it, newDot, bumpCounterAbove);
1635
+ fail = applyArrInsert(base, head, it, newDot, bumpCounterAbove, options.strictParents ?? false);
1034
1636
  break;
1035
1637
  case "ArrDelete":
1036
1638
  fail = applyArrDelete(base, head, it, newDot);
@@ -1044,7 +1646,7 @@ function applyIntentsToCrdt(base, head, intents, newDot, evalTestAgainst = "head
1044
1646
  }
1045
1647
  return { ok: true };
1046
1648
  }
1047
- function jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove) {
1649
+ function jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove, strictParents = false) {
1048
1650
  if (isJsonPatchToCrdtOptions(baseOrOptions)) return jsonPatchToCrdtInternal(baseOrOptions);
1049
1651
  if (!head || !patch || !newDot) return {
1050
1652
  ok: false,
@@ -1058,10 +1660,11 @@ function jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst = "
1058
1660
  patch,
1059
1661
  newDot,
1060
1662
  evalTestAgainst,
1061
- bumpCounterAbove
1663
+ bumpCounterAbove,
1664
+ strictParents
1062
1665
  });
1063
1666
  }
1064
- function jsonPatchToCrdtSafe(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove) {
1667
+ function jsonPatchToCrdtSafe(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove, strictParents = false) {
1065
1668
  try {
1066
1669
  if (isJsonPatchToCrdtOptions(baseOrOptions)) return jsonPatchToCrdt(baseOrOptions);
1067
1670
  if (!head || !patch || !newDot) return {
@@ -1070,7 +1673,7 @@ function jsonPatchToCrdtSafe(baseOrOptions, head, patch, newDot, evalTestAgainst
1070
1673
  reason: "INVALID_PATCH",
1071
1674
  message: "invalid jsonPatchToCrdtSafe call signature"
1072
1675
  };
1073
- return jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst, bumpCounterAbove);
1676
+ return jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst, bumpCounterAbove, strictParents);
1074
1677
  } catch (error) {
1075
1678
  return toApplyError$1(error);
1076
1679
  }
@@ -1108,7 +1711,7 @@ function jsonPatchToCrdtInternal(options) {
1108
1711
  } catch (error) {
1109
1712
  return toApplyError$1(error);
1110
1713
  }
1111
- return applyIntentsToCrdt(options.base, options.head, intents, options.newDot, evalTestAgainst, options.bumpCounterAbove);
1714
+ return applyIntentsToCrdt(options.base, options.head, intents, options.newDot, evalTestAgainst, options.bumpCounterAbove, { strictParents: options.strictParents });
1112
1715
  }
1113
1716
  let shadowBase = cloneDoc(evalTestAgainst === "base" ? options.base : options.head);
1114
1717
  let shadowCtr = 0;
@@ -1127,10 +1730,10 @@ function jsonPatchToCrdtInternal(options) {
1127
1730
  } catch (error) {
1128
1731
  return withOpIndex(toApplyError$1(error), opIndex);
1129
1732
  }
1130
- const headStep = applyIntentsToCrdt(shadowBase, options.head, intents, options.newDot, evalTestAgainst, options.bumpCounterAbove);
1733
+ const headStep = applyIntentsToCrdt(shadowBase, options.head, intents, options.newDot, evalTestAgainst, options.bumpCounterAbove, { strictParents: options.strictParents });
1131
1734
  if (!headStep.ok) return withOpIndex(headStep, opIndex);
1132
1735
  if (evalTestAgainst === "base") {
1133
- const shadowStep = applyIntentsToCrdt(shadowBase, shadowBase, intents, shadowDot, "base", shadowBump);
1736
+ const shadowStep = applyIntentsToCrdt(shadowBase, shadowBase, intents, shadowDot, "base", shadowBump, { strictParents: options.strictParents });
1134
1737
  if (!shadowStep.ok) return withOpIndex(shadowStep, opIndex);
1135
1738
  } else shadowBase = cloneDoc(options.head);
1136
1739
  return { ok: true };
@@ -1183,6 +1786,7 @@ function isJsonPatchToCrdtOptions(value) {
1183
1786
  return typeof value === "object" && value !== null && "base" in value && "head" in value && "patch" in value && "newDot" in value;
1184
1787
  }
1185
1788
  function toApplyError$1(error) {
1789
+ if (error instanceof TraversalDepthError) return toDepthApplyError(error);
1186
1790
  if (error instanceof PatchCompileError) return {
1187
1791
  ok: false,
1188
1792
  code: 409,
@@ -1233,7 +1837,7 @@ var PatchError = class extends Error {
1233
1837
  function createState(initial, options) {
1234
1838
  const clock = createClock(options.actor, options.start ?? 0);
1235
1839
  return {
1236
- doc: docFromJson(initial, clock.next),
1840
+ doc: docFromJson(coerceRuntimeJsonValue(initial, options.jsonValidation ?? "none"), clock.next),
1237
1841
  clock
1238
1842
  };
1239
1843
  }
@@ -1288,11 +1892,18 @@ function tryApplyPatch(state, patch, options = {}) {
1288
1892
  doc: cloneDoc(state.doc),
1289
1893
  clock: cloneClock(state.clock)
1290
1894
  };
1291
- const result = applyPatchInternal(nextState, patch, options);
1292
- if (!result.ok) return {
1293
- ok: false,
1294
- error: result
1295
- };
1895
+ try {
1896
+ const result = applyPatchInternal(nextState, patch, options, "batch");
1897
+ if (!result.ok) return {
1898
+ ok: false,
1899
+ error: result
1900
+ };
1901
+ } catch (error) {
1902
+ return {
1903
+ ok: false,
1904
+ error: toApplyError(error)
1905
+ };
1906
+ }
1296
1907
  return {
1297
1908
  ok: true,
1298
1909
  state: nextState
@@ -1308,11 +1919,18 @@ function tryApplyPatchInPlace(state, patch, options = {}) {
1308
1919
  state.clock = next.state.clock;
1309
1920
  return { ok: true };
1310
1921
  }
1311
- const result = applyPatchInternal(state, patch, applyOptions);
1312
- if (!result.ok) return {
1313
- ok: false,
1314
- error: result
1315
- };
1922
+ try {
1923
+ const result = applyPatchInternal(state, patch, applyOptions, "step");
1924
+ if (!result.ok) return {
1925
+ ok: false,
1926
+ error: result
1927
+ };
1928
+ } catch (error) {
1929
+ return {
1930
+ ok: false,
1931
+ error: toApplyError(error)
1932
+ };
1933
+ }
1316
1934
  return { ok: true };
1317
1935
  }
1318
1936
  /**
@@ -1320,7 +1938,10 @@ function tryApplyPatchInPlace(state, patch, options = {}) {
1320
1938
  * Does not mutate caller-provided values.
1321
1939
  */
1322
1940
  function validateJsonPatch(base, patch, options = {}) {
1323
- const result = tryApplyPatch(createState(base, { actor: "__validate__" }), patch, options);
1941
+ const result = tryApplyPatch(createState(base, {
1942
+ actor: "__validate__",
1943
+ jsonValidation: options.jsonValidation
1944
+ }), patch, options);
1324
1945
  if (!result.ok) return {
1325
1946
  ok: false,
1326
1947
  error: result.error
@@ -1349,35 +1970,43 @@ function toApplyPatchOptionsForActor(options) {
1349
1970
  return {
1350
1971
  semantics: options.semantics,
1351
1972
  testAgainst: options.testAgainst,
1973
+ strictParents: options.strictParents,
1974
+ jsonValidation: options.jsonValidation,
1352
1975
  base: options.base ? {
1353
1976
  doc: options.base,
1354
1977
  clock: createClock("__base__", 0)
1355
1978
  } : void 0
1356
1979
  };
1357
1980
  }
1358
- function applyPatchInternal(state, patch, options) {
1981
+ function applyPatchInternal(state, patch, options, execution) {
1359
1982
  if ((options.semantics ?? "sequential") === "sequential") {
1983
+ if (!options.base && execution === "batch") {
1984
+ const compiled = compileIntents(materialize(state.doc.root), patch, "sequential", options.jsonValidation ?? "none");
1985
+ if (!compiled.ok) return compiled;
1986
+ return applyIntentsToCrdt(state.doc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
1987
+ }
1360
1988
  const explicitBaseState = options.base ? {
1361
1989
  doc: cloneDoc(options.base.doc),
1362
1990
  clock: createClock("__base__", 0)
1363
1991
  } : null;
1364
1992
  for (const [opIndex, op] of patch.entries()) {
1365
- const step = applyPatchOpSequential(state, op, options, explicitBaseState ? explicitBaseState.doc : cloneDoc(state.doc), opIndex);
1993
+ const step = applyPatchOpSequential(state, op, options, explicitBaseState ? explicitBaseState.doc : state.doc, opIndex);
1366
1994
  if (!step.ok) return step;
1367
1995
  if (explicitBaseState && op.op !== "test") {
1368
1996
  const baseStep = applyPatchInternal(explicitBaseState, [op], {
1369
1997
  semantics: "sequential",
1370
- testAgainst: "base"
1371
- });
1998
+ testAgainst: "base",
1999
+ strictParents: options.strictParents
2000
+ }, "step");
1372
2001
  if (!baseStep.ok) return baseStep;
1373
2002
  }
1374
2003
  }
1375
2004
  return { ok: true };
1376
2005
  }
1377
2006
  const baseDoc = options.base ? options.base.doc : cloneDoc(state.doc);
1378
- const compiled = compileIntents(materialize(baseDoc.root), patch, "base");
2007
+ const compiled = compileIntents(materialize(baseDoc.root), patch, "base", options.jsonValidation ?? "none");
1379
2008
  if (!compiled.ok) return compiled;
1380
- return applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr));
2009
+ return applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
1381
2010
  }
1382
2011
  function applyPatchOpSequential(state, op, options, baseDoc, opIndex) {
1383
2012
  const baseJson = materialize(baseDoc.root);
@@ -1385,12 +2014,13 @@ function applyPatchOpSequential(state, op, options, baseDoc, opIndex) {
1385
2014
  const fromResolved = resolveValueAtPointer(baseJson, op.from, opIndex);
1386
2015
  if (!fromResolved.ok) return fromResolved;
1387
2016
  const fromValue = fromResolved.value;
1388
- const removeRes = applySinglePatchOp(state, baseDoc, {
2017
+ const removeRes = applySinglePatchOp(state, baseDoc, baseJson, {
1389
2018
  op: "remove",
1390
2019
  path: op.from
1391
2020
  }, options);
1392
2021
  if (!removeRes.ok) return removeRes;
1393
- return applySinglePatchOp(state, cloneDoc(state.doc), {
2022
+ const addBase = state.doc;
2023
+ return applySinglePatchOp(state, addBase, materialize(addBase.root), {
1394
2024
  op: "add",
1395
2025
  path: op.path,
1396
2026
  value: fromValue
@@ -1400,13 +2030,13 @@ function applyPatchOpSequential(state, op, options, baseDoc, opIndex) {
1400
2030
  const fromResolved = resolveValueAtPointer(baseJson, op.from, opIndex);
1401
2031
  if (!fromResolved.ok) return fromResolved;
1402
2032
  const fromValue = fromResolved.value;
1403
- return applySinglePatchOp(state, baseDoc, {
2033
+ return applySinglePatchOp(state, baseDoc, baseJson, {
1404
2034
  op: "add",
1405
2035
  path: op.path,
1406
2036
  value: fromValue
1407
2037
  }, options);
1408
2038
  }
1409
- return applySinglePatchOp(state, baseDoc, op, options);
2039
+ return applySinglePatchOp(state, baseDoc, baseJson, op, options);
1410
2040
  }
1411
2041
  function resolveValueAtPointer(baseJson, pointer, opIndex) {
1412
2042
  let path;
@@ -1424,49 +2054,94 @@ function resolveValueAtPointer(baseJson, pointer, opIndex) {
1424
2054
  return toPointerLookupApplyError(error, pointer, opIndex);
1425
2055
  }
1426
2056
  }
1427
- function applySinglePatchOp(state, baseDoc, op, options) {
1428
- const compiled = compileIntents(materialize(baseDoc.root), [op], "sequential");
2057
+ function applySinglePatchOp(state, baseDoc, baseJson, op, options) {
2058
+ const compiled = compileIntents(baseJson, [op], "sequential", options.jsonValidation ?? "none");
1429
2059
  if (!compiled.ok) return compiled;
1430
- return applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr));
2060
+ return applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
1431
2061
  }
1432
2062
  function bumpClockCounter(state, ctr) {
1433
2063
  if (state.clock.ctr < ctr) state.clock.ctr = ctr;
1434
2064
  }
1435
- function compileIntents(baseJson, patch, semantics = "sequential") {
2065
+ function compileIntents(baseJson, patch, semantics = "sequential", jsonValidation = "none") {
1436
2066
  try {
1437
2067
  return {
1438
2068
  ok: true,
1439
- intents: compileJsonPatchToIntent(baseJson, patch, { semantics })
2069
+ intents: compileJsonPatchToIntent(baseJson, preparePatchPayloads(patch, jsonValidation), { semantics })
1440
2070
  };
1441
2071
  } catch (error) {
1442
2072
  return toApplyError(error);
1443
2073
  }
1444
2074
  }
2075
+ function preparePatchPayloads(patch, mode) {
2076
+ if (mode === "none") return patch;
2077
+ const out = [];
2078
+ for (const [opIndex, op] of patch.entries()) {
2079
+ if (op.op === "move" || op.op === "copy" || op.op === "remove") {
2080
+ out.push(op);
2081
+ continue;
2082
+ }
2083
+ if (mode === "strict") {
2084
+ try {
2085
+ assertRuntimeJsonValue(op.value);
2086
+ } catch (error) {
2087
+ if (error instanceof JsonValueValidationError) throw patchPayloadCompileError(op, opIndex, error);
2088
+ throw error;
2089
+ }
2090
+ out.push(op);
2091
+ continue;
2092
+ }
2093
+ out.push({
2094
+ ...op,
2095
+ value: coerceRuntimeJsonValue(op.value, mode)
2096
+ });
2097
+ }
2098
+ return out;
2099
+ }
2100
+ function patchPayloadCompileError(op, opIndex, error) {
2101
+ const path = mergePointerPaths(op.path, error.path);
2102
+ return new PatchCompileError("INVALID_PATCH", `invalid JSON value for '${op.op}' at ${path === "" ? "<root>" : path}: ${error.detail}`, path, opIndex);
2103
+ }
2104
+ function mergePointerPaths(basePointer, nestedPointer) {
2105
+ if (nestedPointer === "") return basePointer;
2106
+ if (basePointer === "") return nestedPointer;
2107
+ return `${basePointer}${nestedPointer}`;
2108
+ }
1445
2109
  function maxCtrInNodeForActor$1(node, actor) {
1446
- switch (node.kind) {
1447
- case "lww": return node.dot.actor === actor ? node.dot.ctr : 0;
1448
- case "obj": {
1449
- let best = 0;
1450
- for (const entry of node.entries.values()) {
2110
+ let best = 0;
2111
+ const stack = [{
2112
+ node,
2113
+ depth: 0
2114
+ }];
2115
+ while (stack.length > 0) {
2116
+ const frame = stack.pop();
2117
+ assertTraversalDepth(frame.depth);
2118
+ if (frame.node.kind === "lww") {
2119
+ if (frame.node.dot.actor === actor && frame.node.dot.ctr > best) best = frame.node.dot.ctr;
2120
+ continue;
2121
+ }
2122
+ if (frame.node.kind === "obj") {
2123
+ for (const entry of frame.node.entries.values()) {
1451
2124
  if (entry.dot.actor === actor && entry.dot.ctr > best) best = entry.dot.ctr;
1452
- const childBest = maxCtrInNodeForActor$1(entry.node, actor);
1453
- if (childBest > best) best = childBest;
2125
+ stack.push({
2126
+ node: entry.node,
2127
+ depth: frame.depth + 1
2128
+ });
1454
2129
  }
1455
- for (const tomb of node.tombstone.values()) if (tomb.actor === actor && tomb.ctr > best) best = tomb.ctr;
1456
- return best;
2130
+ for (const tomb of frame.node.tombstone.values()) if (tomb.actor === actor && tomb.ctr > best) best = tomb.ctr;
2131
+ continue;
1457
2132
  }
1458
- case "seq": {
1459
- let best = 0;
1460
- for (const elem of node.elems.values()) {
1461
- if (elem.insDot.actor === actor && elem.insDot.ctr > best) best = elem.insDot.ctr;
1462
- const childBest = maxCtrInNodeForActor$1(elem.value, actor);
1463
- if (childBest > best) best = childBest;
1464
- }
1465
- return best;
2133
+ for (const elem of frame.node.elems.values()) {
2134
+ if (elem.insDot.actor === actor && elem.insDot.ctr > best) best = elem.insDot.ctr;
2135
+ stack.push({
2136
+ node: elem.value,
2137
+ depth: frame.depth + 1
2138
+ });
1466
2139
  }
1467
2140
  }
2141
+ return best;
1468
2142
  }
1469
2143
  function toApplyError(error) {
2144
+ if (error instanceof TraversalDepthError) return toDepthApplyError(error);
1470
2145
  if (error instanceof PatchCompileError) return {
1471
2146
  ok: false,
1472
2147
  code: 409,
@@ -1506,13 +2181,38 @@ function toPointerLookupApplyError(error, pointer, opIndex) {
1506
2181
 
1507
2182
  //#endregion
1508
2183
  //#region src/serialize.ts
2184
+ const HEAD_ELEM_ID = "HEAD";
2185
+ function createSerializedRecord() {
2186
+ return Object.create(null);
2187
+ }
2188
+ function setSerializedRecordValue(out, key, value) {
2189
+ Object.defineProperty(out, key, {
2190
+ configurable: true,
2191
+ enumerable: true,
2192
+ value,
2193
+ writable: true
2194
+ });
2195
+ }
2196
+ var DeserializeError = class extends Error {
2197
+ code = 409;
2198
+ reason;
2199
+ path;
2200
+ constructor(reason, path, message) {
2201
+ super(message);
2202
+ this.name = "DeserializeError";
2203
+ this.reason = reason;
2204
+ this.path = path;
2205
+ }
2206
+ };
1509
2207
  /** Serialize a CRDT document to a JSON-safe representation (Maps become plain objects). */
1510
2208
  function serializeDoc(doc) {
1511
2209
  return { root: serializeNode(doc.root) };
1512
2210
  }
1513
2211
  /** Reconstruct a CRDT document from its serialized form. */
1514
2212
  function deserializeDoc(data) {
1515
- return { root: deserializeNode(data.root) };
2213
+ if (!isRecord(data)) fail("INVALID_SERIALIZED_SHAPE", "/", "serialized doc must be an object");
2214
+ if (!("root" in data)) fail("INVALID_SERIALIZED_SHAPE", "/root", "serialized doc is missing root");
2215
+ return { root: deserializeNode(data.root, "/root", 0) };
1516
2216
  }
1517
2217
  /** Serialize a full CRDT state (document + clock) to a JSON-safe representation. */
1518
2218
  function serializeState(state) {
@@ -1526,7 +2226,11 @@ function serializeState(state) {
1526
2226
  }
1527
2227
  /** Reconstruct a full CRDT state from its serialized form, restoring the clock. */
1528
2228
  function deserializeState(data) {
1529
- const clock = createClock(data.clock.actor, data.clock.ctr);
2229
+ if (!isRecord(data)) fail("INVALID_SERIALIZED_SHAPE", "/", "serialized state must be an object");
2230
+ if (!("doc" in data)) fail("INVALID_SERIALIZED_SHAPE", "/doc", "serialized state is missing doc");
2231
+ if (!("clock" in data)) fail("INVALID_SERIALIZED_SHAPE", "/clock", "serialized state is missing clock");
2232
+ const clockRaw = asRecord(data.clock, "/clock");
2233
+ const clock = createClock(readActor(clockRaw.actor, "/clock/actor"), readCounter(clockRaw.ctr, "/clock/ctr"));
1530
2234
  return {
1531
2235
  doc: deserializeDoc(data.doc),
1532
2236
  clock
@@ -1542,27 +2246,27 @@ function serializeNode(node) {
1542
2246
  }
1543
2247
  };
1544
2248
  if (node.kind === "obj") {
1545
- const entries = {};
1546
- for (const [k, v] of node.entries.entries()) entries[k] = {
2249
+ const entries = createSerializedRecord();
2250
+ for (const [k, v] of node.entries.entries()) setSerializedRecordValue(entries, k, {
1547
2251
  node: serializeNode(v.node),
1548
2252
  dot: {
1549
2253
  actor: v.dot.actor,
1550
2254
  ctr: v.dot.ctr
1551
2255
  }
1552
- };
1553
- const tombstone = {};
1554
- for (const [k, d] of node.tombstone.entries()) tombstone[k] = {
2256
+ });
2257
+ const tombstone = createSerializedRecord();
2258
+ for (const [k, d] of node.tombstone.entries()) setSerializedRecordValue(tombstone, k, {
1555
2259
  actor: d.actor,
1556
2260
  ctr: d.ctr
1557
- };
2261
+ });
1558
2262
  return {
1559
2263
  kind: "obj",
1560
2264
  entries,
1561
2265
  tombstone
1562
2266
  };
1563
2267
  }
1564
- const elems = {};
1565
- for (const [id, e] of node.elems.entries()) elems[id] = {
2268
+ const elems = createSerializedRecord();
2269
+ for (const [id, e] of node.elems.entries()) setSerializedRecordValue(elems, id, {
1566
2270
  id: e.id,
1567
2271
  prev: e.prev,
1568
2272
  tombstone: e.tombstone,
@@ -1571,60 +2275,138 @@ function serializeNode(node) {
1571
2275
  actor: e.insDot.actor,
1572
2276
  ctr: e.insDot.ctr
1573
2277
  }
1574
- };
2278
+ });
1575
2279
  return {
1576
2280
  kind: "seq",
1577
2281
  elems
1578
2282
  };
1579
2283
  }
1580
- function deserializeNode(node) {
1581
- if (node.kind === "lww") return {
1582
- kind: "lww",
1583
- value: structuredClone(node.value),
1584
- dot: {
1585
- actor: node.dot.actor,
1586
- ctr: node.dot.ctr
1587
- }
1588
- };
1589
- if (node.kind === "obj") {
2284
+ function deserializeNode(node, path, depth) {
2285
+ assertTraversalDepth(depth);
2286
+ const raw = asRecord(node, path);
2287
+ const kind = readString(raw.kind, `${path}/kind`);
2288
+ if (kind === "lww") {
2289
+ if (!("value" in raw)) fail("INVALID_SERIALIZED_SHAPE", `${path}/value`, "lww node is missing value");
2290
+ if (!("dot" in raw)) fail("INVALID_SERIALIZED_SHAPE", `${path}/dot`, "lww node is missing dot");
2291
+ return {
2292
+ kind: "lww",
2293
+ value: structuredClone(readJsonValue(raw.value, `${path}/value`, depth + 1)),
2294
+ dot: readDot(raw.dot, `${path}/dot`)
2295
+ };
2296
+ }
2297
+ if (kind === "obj") {
2298
+ const entriesRaw = asRecord(raw.entries, `${path}/entries`);
2299
+ const tombstoneRaw = asRecord(raw.tombstone, `${path}/tombstone`);
1590
2300
  const entries = /* @__PURE__ */ new Map();
1591
- for (const [k, v] of Object.entries(node.entries)) entries.set(k, {
1592
- node: deserializeNode(v.node),
1593
- dot: {
1594
- actor: v.dot.actor,
1595
- ctr: v.dot.ctr
1596
- }
1597
- });
2301
+ for (const [k, v] of Object.entries(entriesRaw)) {
2302
+ const entryPath = `${path}/entries/${k}`;
2303
+ const entryRaw = asRecord(v, entryPath);
2304
+ entries.set(k, {
2305
+ node: deserializeNode(entryRaw.node, `${entryPath}/node`, depth + 1),
2306
+ dot: readDot(entryRaw.dot, `${entryPath}/dot`)
2307
+ });
2308
+ }
1598
2309
  const tombstone = /* @__PURE__ */ new Map();
1599
- for (const [k, d] of Object.entries(node.tombstone)) tombstone.set(k, {
1600
- actor: d.actor,
1601
- ctr: d.ctr
1602
- });
2310
+ for (const [k, d] of Object.entries(tombstoneRaw)) tombstone.set(k, readDot(d, `${path}/tombstone/${k}`));
1603
2311
  return {
1604
2312
  kind: "obj",
1605
2313
  entries,
1606
2314
  tombstone
1607
2315
  };
1608
2316
  }
2317
+ if (kind !== "seq") fail("INVALID_SERIALIZED_SHAPE", `${path}/kind`, `unsupported node kind '${kind}'`);
2318
+ const elemsRaw = asRecord(raw.elems, `${path}/elems`);
1609
2319
  const elems = /* @__PURE__ */ new Map();
1610
- for (const [id, e] of Object.entries(node.elems)) elems.set(id, {
1611
- id: e.id,
1612
- prev: e.prev,
1613
- tombstone: e.tombstone,
1614
- value: deserializeNode(e.value),
1615
- insDot: {
1616
- actor: e.insDot.actor,
1617
- ctr: e.insDot.ctr
1618
- }
1619
- });
2320
+ for (const [id, rawElem] of Object.entries(elemsRaw)) {
2321
+ const elemPath = `${path}/elems/${id}`;
2322
+ const elem = asRecord(rawElem, elemPath);
2323
+ const elemId = readString(elem.id, `${elemPath}/id`);
2324
+ if (elemId !== id) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/id`, `sequence element id '${elemId}' does not match key '${id}'`);
2325
+ const prev = readString(elem.prev, `${elemPath}/prev`);
2326
+ const tombstone = readBoolean(elem.tombstone, `${elemPath}/tombstone`);
2327
+ const value = deserializeNode(elem.value, `${elemPath}/value`, depth + 1);
2328
+ const insDot = readDot(elem.insDot, `${elemPath}/insDot`);
2329
+ if (dotToElemId(insDot) !== id) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/insDot`, "sequence element id must match its insertion dot");
2330
+ elems.set(id, {
2331
+ id,
2332
+ prev,
2333
+ tombstone,
2334
+ value,
2335
+ insDot
2336
+ });
2337
+ }
2338
+ for (const elem of elems.values()) {
2339
+ if (elem.prev === elem.id) fail("INVALID_SERIALIZED_INVARIANT", `${path}/elems/${elem.id}/prev`, "sequence element cannot reference itself as predecessor");
2340
+ if (elem.prev !== HEAD_ELEM_ID && !elems.has(elem.prev)) fail("INVALID_SERIALIZED_INVARIANT", `${path}/elems/${elem.id}/prev`, `sequence predecessor '${elem.prev}' does not exist`);
2341
+ }
1620
2342
  return {
1621
2343
  kind: "seq",
1622
2344
  elems
1623
2345
  };
1624
2346
  }
2347
+ function asRecord(value, path) {
2348
+ if (!isRecord(value)) fail("INVALID_SERIALIZED_SHAPE", path, "expected object");
2349
+ return value;
2350
+ }
2351
+ function readDot(value, path) {
2352
+ const raw = asRecord(value, path);
2353
+ return {
2354
+ actor: readActor(raw.actor, `${path}/actor`),
2355
+ ctr: readCounter(raw.ctr, `${path}/ctr`)
2356
+ };
2357
+ }
2358
+ function readActor(value, path) {
2359
+ const actor = readString(value, path);
2360
+ if (actor.length === 0) fail("INVALID_SERIALIZED_SHAPE", path, "actor must not be empty");
2361
+ return actor;
2362
+ }
2363
+ function readCounter(value, path) {
2364
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) fail("INVALID_SERIALIZED_SHAPE", path, "counter must be a non-negative safe integer");
2365
+ return value;
2366
+ }
2367
+ function readString(value, path) {
2368
+ if (typeof value !== "string") fail("INVALID_SERIALIZED_SHAPE", path, "expected string");
2369
+ return value;
2370
+ }
2371
+ function readBoolean(value, path) {
2372
+ if (typeof value !== "boolean") fail("INVALID_SERIALIZED_SHAPE", path, "expected boolean");
2373
+ return value;
2374
+ }
2375
+ function readJsonValue(value, path, depth) {
2376
+ assertJsonValue(value, path, depth);
2377
+ return value;
2378
+ }
2379
+ function assertJsonValue(value, path, depth) {
2380
+ assertTraversalDepth(depth);
2381
+ if (value === null || typeof value === "string" || typeof value === "boolean") return;
2382
+ if (typeof value === "number") {
2383
+ if (!Number.isFinite(value)) fail("INVALID_SERIALIZED_SHAPE", path, "json number must be finite");
2384
+ return;
2385
+ }
2386
+ if (Array.isArray(value)) {
2387
+ for (const [index, item] of value.entries()) assertJsonValue(item, `${path}/${index}`, depth + 1);
2388
+ return;
2389
+ }
2390
+ if (!isRecord(value)) fail("INVALID_SERIALIZED_SHAPE", path, "expected JSON value");
2391
+ for (const [key, child] of Object.entries(value)) assertJsonValue(child, `${path}/${key}`, depth + 1);
2392
+ }
2393
+ function fail(reason, path, message) {
2394
+ throw new DeserializeError(reason, path, message);
2395
+ }
2396
+ function isRecord(value) {
2397
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2398
+ }
1625
2399
 
1626
2400
  //#endregion
1627
2401
  //#region src/merge.ts
2402
+ var SharedElementMetadataMismatchError = class extends Error {
2403
+ path;
2404
+ constructor(path, id, field) {
2405
+ super(`shared RGA element '${id}' has conflicting ${field} metadata`);
2406
+ this.name = "SharedElementMetadataMismatchError";
2407
+ this.path = path;
2408
+ }
2409
+ };
1628
2410
  /** Error thrown by throwing merge helpers (`mergeDoc` / `mergeState`). */
1629
2411
  var MergeError = class extends Error {
1630
2412
  code;
@@ -1634,7 +2416,7 @@ var MergeError = class extends Error {
1634
2416
  super(error.message);
1635
2417
  this.name = "MergeError";
1636
2418
  this.code = error.code;
1637
- this.reason = "LINEAGE_MISMATCH";
2419
+ this.reason = error.reason;
1638
2420
  this.path = error.path;
1639
2421
  }
1640
2422
  };
@@ -1658,21 +2440,39 @@ function mergeDoc(a, b, options = {}) {
1658
2440
  }
1659
2441
  /** Non-throwing `mergeDoc` variant with structured conflict details. */
1660
2442
  function tryMergeDoc(a, b, options = {}) {
1661
- const mismatchPath = options.requireSharedOrigin ?? true ? findSeqLineageMismatch(a.root, b.root, []) : null;
1662
- if (mismatchPath) return {
1663
- ok: false,
1664
- error: {
2443
+ try {
2444
+ const mismatchPath = options.requireSharedOrigin ?? true ? findSeqLineageMismatch(a.root, b.root, []) : null;
2445
+ if (mismatchPath) return {
1665
2446
  ok: false,
1666
- code: 409,
1667
- reason: "LINEAGE_MISMATCH",
1668
- message: `merge requires shared array origin at ${mismatchPath}`,
1669
- path: mismatchPath
1670
- }
1671
- };
1672
- return {
1673
- ok: true,
1674
- doc: { root: mergeNode(a.root, b.root) }
1675
- };
2447
+ error: {
2448
+ ok: false,
2449
+ code: 409,
2450
+ reason: "LINEAGE_MISMATCH",
2451
+ message: `merge requires shared array origin at ${mismatchPath}`,
2452
+ path: mismatchPath
2453
+ }
2454
+ };
2455
+ return {
2456
+ ok: true,
2457
+ doc: { root: mergeNode(a.root, b.root) }
2458
+ };
2459
+ } catch (error) {
2460
+ if (error instanceof SharedElementMetadataMismatchError) return {
2461
+ ok: false,
2462
+ error: {
2463
+ ok: false,
2464
+ code: 409,
2465
+ reason: "LINEAGE_MISMATCH",
2466
+ message: error.message,
2467
+ path: error.path
2468
+ }
2469
+ };
2470
+ if (error instanceof TraversalDepthError) return {
2471
+ ok: false,
2472
+ error: toDepthApplyError(error)
2473
+ };
2474
+ throw error;
2475
+ }
1676
2476
  }
1677
2477
  /**
1678
2478
  * Merge two CRDT states.
@@ -1705,25 +2505,42 @@ function tryMergeState(a, b, options = {}) {
1705
2505
  };
1706
2506
  }
1707
2507
  function findSeqLineageMismatch(a, b, path) {
1708
- if (a.kind === "seq" && b.kind === "seq") {
1709
- const hasElemsA = a.elems.size > 0;
1710
- const hasElemsB = b.elems.size > 0;
1711
- if (hasElemsA && hasElemsB) {
1712
- let shared = false;
1713
- for (const id of a.elems.keys()) if (b.elems.has(id)) {
1714
- shared = true;
1715
- break;
2508
+ const stack = [{
2509
+ a,
2510
+ b,
2511
+ path,
2512
+ depth: path.length
2513
+ }];
2514
+ while (stack.length > 0) {
2515
+ const frame = stack.pop();
2516
+ assertTraversalDepth(frame.depth);
2517
+ if (frame.a.kind === "seq" && frame.b.kind === "seq") {
2518
+ const hasElemsA = frame.a.elems.size > 0;
2519
+ const hasElemsB = frame.b.elems.size > 0;
2520
+ if (hasElemsA && hasElemsB) {
2521
+ let shared = false;
2522
+ for (const id of frame.a.elems.keys()) if (frame.b.elems.has(id)) {
2523
+ shared = true;
2524
+ break;
2525
+ }
2526
+ if (!shared) return `/${frame.path.join("/")}`;
1716
2527
  }
1717
- if (!shared) return `/${path.join("/")}`;
1718
2528
  }
1719
- }
1720
- if (a.kind === "obj" && b.kind === "obj") {
1721
- const sharedKeys = new Set([...a.entries.keys()].filter((key) => b.entries.has(key)));
1722
- for (const key of sharedKeys) {
1723
- const nextA = a.entries.get(key).node;
1724
- const nextB = b.entries.get(key).node;
1725
- const mismatch = findSeqLineageMismatch(nextA, nextB, [...path, key]);
1726
- if (mismatch) return mismatch;
2529
+ if (frame.a.kind === "obj" && frame.b.kind === "obj") {
2530
+ const left = frame.a;
2531
+ const right = frame.b;
2532
+ const sharedKeys = [...left.entries.keys()].filter((key) => right.entries.has(key));
2533
+ for (let i = sharedKeys.length - 1; i >= 0; i--) {
2534
+ const key = sharedKeys[i];
2535
+ const nextA = left.entries.get(key).node;
2536
+ const nextB = right.entries.get(key).node;
2537
+ stack.push({
2538
+ a: nextA,
2539
+ b: nextB,
2540
+ path: [...frame.path, key],
2541
+ depth: frame.depth + 1
2542
+ });
2543
+ }
1727
2544
  }
1728
2545
  }
1729
2546
  return null;
@@ -1735,28 +2552,38 @@ function maxObservedCtrForActor(doc, actor, a, b) {
1735
2552
  return best;
1736
2553
  }
1737
2554
  function maxCtrInNodeForActor(node, actor) {
1738
- switch (node.kind) {
1739
- case "lww": return node.dot.actor === actor ? node.dot.ctr : 0;
1740
- case "obj": {
1741
- let best = 0;
1742
- for (const entry of node.entries.values()) {
2555
+ let best = 0;
2556
+ const stack = [{
2557
+ node,
2558
+ depth: 0
2559
+ }];
2560
+ while (stack.length > 0) {
2561
+ const frame = stack.pop();
2562
+ assertTraversalDepth(frame.depth);
2563
+ if (frame.node.kind === "lww") {
2564
+ if (frame.node.dot.actor === actor && frame.node.dot.ctr > best) best = frame.node.dot.ctr;
2565
+ continue;
2566
+ }
2567
+ if (frame.node.kind === "obj") {
2568
+ for (const entry of frame.node.entries.values()) {
1743
2569
  if (entry.dot.actor === actor && entry.dot.ctr > best) best = entry.dot.ctr;
1744
- const childBest = maxCtrInNodeForActor(entry.node, actor);
1745
- if (childBest > best) best = childBest;
2570
+ stack.push({
2571
+ node: entry.node,
2572
+ depth: frame.depth + 1
2573
+ });
1746
2574
  }
1747
- for (const tomb of node.tombstone.values()) if (tomb.actor === actor && tomb.ctr > best) best = tomb.ctr;
1748
- return best;
2575
+ for (const tomb of frame.node.tombstone.values()) if (tomb.actor === actor && tomb.ctr > best) best = tomb.ctr;
2576
+ continue;
1749
2577
  }
1750
- case "seq": {
1751
- let best = 0;
1752
- for (const elem of node.elems.values()) {
1753
- if (elem.insDot.actor === actor && elem.insDot.ctr > best) best = elem.insDot.ctr;
1754
- const childBest = maxCtrInNodeForActor(elem.value, actor);
1755
- if (childBest > best) best = childBest;
1756
- }
1757
- return best;
2578
+ for (const elem of frame.node.elems.values()) {
2579
+ if (elem.insDot.actor === actor && elem.insDot.ctr > best) best = elem.insDot.ctr;
2580
+ stack.push({
2581
+ node: elem.value,
2582
+ depth: frame.depth + 1
2583
+ });
1758
2584
  }
1759
2585
  }
2586
+ return best;
1760
2587
  }
1761
2588
  function repDot(node) {
1762
2589
  switch (node.kind) {
@@ -1781,11 +2608,15 @@ function repDot(node) {
1781
2608
  }
1782
2609
  }
1783
2610
  function mergeNode(a, b) {
2611
+ return mergeNodeAtDepth(a, b, 0, []);
2612
+ }
2613
+ function mergeNodeAtDepth(a, b, depth, path) {
2614
+ assertTraversalDepth(depth);
1784
2615
  if (a.kind === "lww" && b.kind === "lww") return mergeLww(a, b);
1785
- if (a.kind === "obj" && b.kind === "obj") return mergeObj(a, b);
1786
- if (a.kind === "seq" && b.kind === "seq") return mergeSeq(a, b);
1787
- if (compareDot(repDot(a), repDot(b)) >= 0) return cloneNodeShallow(a);
1788
- return cloneNodeShallow(b);
2616
+ if (a.kind === "obj" && b.kind === "obj") return mergeObj(a, b, depth + 1, path);
2617
+ if (a.kind === "seq" && b.kind === "seq") return mergeSeq(a, b, depth + 1, path);
2618
+ if (compareDot(repDot(a), repDot(b)) >= 0) return cloneNodeShallow(a, depth + 1);
2619
+ return cloneNodeShallow(b, depth + 1);
1789
2620
  }
1790
2621
  function mergeLww(a, b) {
1791
2622
  if (compareDot(a.dot, b.dot) >= 0) return {
@@ -1799,7 +2630,8 @@ function mergeLww(a, b) {
1799
2630
  dot: { ...b.dot }
1800
2631
  };
1801
2632
  }
1802
- function mergeObj(a, b) {
2633
+ function mergeObj(a, b, depth, path) {
2634
+ assertTraversalDepth(depth);
1803
2635
  const entries = /* @__PURE__ */ new Map();
1804
2636
  const tombstone = /* @__PURE__ */ new Map();
1805
2637
  const allTombKeys = new Set([...a.tombstone.keys(), ...b.tombstone.keys()]);
@@ -1816,15 +2648,15 @@ function mergeObj(a, b) {
1816
2648
  const eb = b.entries.get(key);
1817
2649
  let merged;
1818
2650
  if (ea && eb) merged = {
1819
- node: mergeNode(ea.node, eb.node),
2651
+ node: mergeNodeAtDepth(ea.node, eb.node, depth + 1, [...path, key]),
1820
2652
  dot: compareDot(ea.dot, eb.dot) >= 0 ? { ...ea.dot } : { ...eb.dot }
1821
2653
  };
1822
2654
  else if (ea) merged = {
1823
- node: cloneNodeShallow(ea.node),
2655
+ node: cloneNodeShallow(ea.node, depth + 1),
1824
2656
  dot: { ...ea.dot }
1825
2657
  };
1826
2658
  else merged = {
1827
- node: cloneNodeShallow(eb.node),
2659
+ node: cloneNodeShallow(eb.node, depth + 1),
1828
2660
  dot: { ...eb.dot }
1829
2661
  };
1830
2662
  const td = tombstone.get(key);
@@ -1837,14 +2669,17 @@ function mergeObj(a, b) {
1837
2669
  tombstone
1838
2670
  };
1839
2671
  }
1840
- function mergeSeq(a, b) {
2672
+ function mergeSeq(a, b, depth, path) {
2673
+ assertTraversalDepth(depth);
1841
2674
  const elems = /* @__PURE__ */ new Map();
1842
2675
  const allIds = new Set([...a.elems.keys(), ...b.elems.keys()]);
1843
2676
  for (const id of allIds) {
1844
2677
  const ea = a.elems.get(id);
1845
2678
  const eb = b.elems.get(id);
1846
2679
  if (ea && eb) {
1847
- const mergedValue = mergeNode(ea.value, eb.value);
2680
+ if (ea.prev !== eb.prev) throw new SharedElementMetadataMismatchError(toPointer(path), id, "prev");
2681
+ if (!sameDot(ea.insDot, eb.insDot)) throw new SharedElementMetadataMismatchError(toPointer(path), id, "insDot");
2682
+ const mergedValue = mergeNodeAtDepth(ea.value, eb.value, depth + 1, [...path, id]);
1848
2683
  elems.set(id, {
1849
2684
  id,
1850
2685
  prev: ea.prev,
@@ -1852,24 +2687,33 @@ function mergeSeq(a, b) {
1852
2687
  value: mergedValue,
1853
2688
  insDot: { ...ea.insDot }
1854
2689
  });
1855
- } else if (ea) elems.set(id, cloneElem(ea));
1856
- else elems.set(id, cloneElem(eb));
2690
+ } else if (ea) elems.set(id, cloneElem(ea, depth + 1));
2691
+ else elems.set(id, cloneElem(eb, depth + 1));
1857
2692
  }
1858
2693
  return {
1859
2694
  kind: "seq",
1860
2695
  elems
1861
2696
  };
1862
2697
  }
1863
- function cloneElem(e) {
2698
+ function sameDot(a, b) {
2699
+ return a.actor === b.actor && a.ctr === b.ctr;
2700
+ }
2701
+ function toPointer(path) {
2702
+ if (path.length === 0) return "/";
2703
+ return `/${path.join("/")}`;
2704
+ }
2705
+ function cloneElem(e, depth) {
2706
+ assertTraversalDepth(depth);
1864
2707
  return {
1865
2708
  id: e.id,
1866
2709
  prev: e.prev,
1867
2710
  tombstone: e.tombstone,
1868
- value: cloneNodeShallow(e.value),
2711
+ value: cloneNodeShallow(e.value, depth + 1),
1869
2712
  insDot: { ...e.insDot }
1870
2713
  };
1871
2714
  }
1872
- function cloneNodeShallow(node) {
2715
+ function cloneNodeShallow(node, depth) {
2716
+ assertTraversalDepth(depth);
1873
2717
  switch (node.kind) {
1874
2718
  case "lww": return {
1875
2719
  kind: "lww",
@@ -1879,7 +2723,7 @@ function cloneNodeShallow(node) {
1879
2723
  case "obj": {
1880
2724
  const entries = /* @__PURE__ */ new Map();
1881
2725
  for (const [k, v] of node.entries) entries.set(k, {
1882
- node: cloneNodeShallow(v.node),
2726
+ node: cloneNodeShallow(v.node, depth + 1),
1883
2727
  dot: { ...v.dot }
1884
2728
  });
1885
2729
  const tombstone = /* @__PURE__ */ new Map();
@@ -1892,7 +2736,7 @@ function cloneNodeShallow(node) {
1892
2736
  }
1893
2737
  case "seq": {
1894
2738
  const elems = /* @__PURE__ */ new Map();
1895
- for (const [id, e] of node.elems) elems.set(id, cloneElem(e));
2739
+ for (const [id, e] of node.elems) elems.set(id, cloneElem(e, depth + 1));
1896
2740
  return {
1897
2741
  kind: "seq",
1898
2742
  elems
@@ -1902,4 +2746,83 @@ function cloneNodeShallow(node) {
1902
2746
  }
1903
2747
 
1904
2748
  //#endregion
1905
- export { vvMerge as $, compileJsonPatchToIntent as A, newSeq as B, crdtToJsonPatch as C, jsonPatchToCrdtSafe as D, jsonPatchToCrdt as E, stringifyJsonPointer as F, rgaDelete as G, objSet as H, ROOT_KEY as I, rgaLinearizeIds as J, rgaIdAtIndex as K, lwwSet as L, getAtJson as M, jsonEquals as N, tryJsonPatchToCrdt as O, parseJsonPointer as P, vvHasDot as Q, newObj as R, crdtToFullReplace as S, docFromJsonWithDot as T, materialize as U, objRemove as V, HEAD as W, compareDot as X, rgaPrevForInsertAtIndex as Y, dotToElemId as Z, tryApplyPatch as _, tryMergeState as a, applyIntentsToCrdt as b, serializeDoc as c, applyPatch as d, cloneClock as et, applyPatchAsActor as f, toJson as g, forkState as h, tryMergeDoc as i, diffJsonPatch as j, PatchCompileError as k, serializeState as l, createState as m, mergeDoc as n, nextDotForActor as nt, deserializeDoc as o, applyPatchInPlace as p, rgaInsertAfter as q, mergeState as r, observeDot as rt, deserializeState as s, MergeError as t, createClock as tt, PatchError as u, tryApplyPatchInPlace as v, docFromJson as w, cloneDoc as x, validateJsonPatch as y, newReg as z };
2749
+ //#region src/compact.ts
2750
+ function isDotStable(stable, dot) {
2751
+ return (stable[dot.actor] ?? 0) >= dot.ctr;
2752
+ }
2753
+ /**
2754
+ * Compact causally-stable tombstones in a document.
2755
+ *
2756
+ * Safety note:
2757
+ * - Only compact at checkpoints that are causally stable across all peers you
2758
+ * may still merge with.
2759
+ * - Do not merge this compacted document with replicas that might be behind
2760
+ * the provided checkpoint.
2761
+ */
2762
+ function compactDocTombstones(doc, options) {
2763
+ const targetDoc = options.mutate ? doc : cloneDoc(doc);
2764
+ const stats = {
2765
+ objectTombstonesRemoved: 0,
2766
+ sequenceTombstonesRemoved: 0
2767
+ };
2768
+ const stable = options.stable;
2769
+ const stack = [{
2770
+ node: targetDoc.root,
2771
+ depth: 0
2772
+ }];
2773
+ while (stack.length > 0) {
2774
+ const frame = stack.pop();
2775
+ assertTraversalDepth(frame.depth);
2776
+ if (frame.node.kind === "obj") {
2777
+ stats.objectTombstonesRemoved += objCompactTombstones(frame.node, (dot) => isDotStable(stable, dot));
2778
+ for (const entry of frame.node.entries.values()) stack.push({
2779
+ node: entry.node,
2780
+ depth: frame.depth + 1
2781
+ });
2782
+ continue;
2783
+ }
2784
+ if (frame.node.kind === "seq") {
2785
+ stats.sequenceTombstonesRemoved += rgaCompactTombstones(frame.node, (dot) => isDotStable(stable, dot));
2786
+ for (const elem of frame.node.elems.values()) stack.push({
2787
+ node: elem.value,
2788
+ depth: frame.depth + 1
2789
+ });
2790
+ }
2791
+ }
2792
+ return {
2793
+ doc: targetDoc,
2794
+ stats
2795
+ };
2796
+ }
2797
+ /**
2798
+ * Compact causally-stable tombstones in a state document.
2799
+ *
2800
+ * Safety note:
2801
+ * - Only compact at checkpoints that are causally stable across all peers you
2802
+ * may still merge with.
2803
+ * - Do not merge this compacted state with replicas that might be behind the
2804
+ * provided checkpoint.
2805
+ */
2806
+ function compactStateTombstones(state, options) {
2807
+ if (options.mutate) return {
2808
+ state,
2809
+ stats: compactDocTombstones(state.doc, {
2810
+ stable: options.stable,
2811
+ mutate: true
2812
+ }).stats
2813
+ };
2814
+ const nextState = {
2815
+ doc: cloneDoc(state.doc),
2816
+ clock: cloneClock(state.clock)
2817
+ };
2818
+ return {
2819
+ state: nextState,
2820
+ stats: compactDocTombstones(nextState.doc, {
2821
+ stable: options.stable,
2822
+ mutate: true
2823
+ }).stats
2824
+ };
2825
+ }
2826
+
2827
+ //#endregion
2828
+ export { rgaInsertAfter as $, jsonPatchToCrdtSafe as A, JsonValueValidationError as B, applyIntentsToCrdt as C, docFromJson as D, crdtToJsonPatch as E, getAtJson as F, objCompactTombstones as G, newObj as H, jsonEquals as I, materialize as J, objRemove as K, parseJsonPointer as L, PatchCompileError as M, compileJsonPatchToIntent as N, docFromJsonWithDot as O, diffJsonPatch as P, rgaIdAtIndex as Q, stringifyJsonPointer as R, validateJsonPatch as S, crdtToFullReplace as T, newReg as U, lwwSet as V, newSeq as W, rgaCompactTombstones as X, HEAD as Y, rgaDelete as Z, createState as _, mergeState as a, vvMerge as at, tryApplyPatch as b, DeserializeError as c, ClockValidationError as ct, serializeDoc as d, nextDotForActor as dt, rgaLinearizeIds as et, serializeState as f, observeDot as ft, applyPatchInPlace as g, applyPatchAsActor as h, mergeDoc as i, vvHasDot as it, tryJsonPatchToCrdt as j, jsonPatchToCrdt as k, deserializeDoc as l, cloneClock as lt, applyPatch as m, compactStateTombstones as n, compareDot as nt, tryMergeDoc as o, MAX_TRAVERSAL_DEPTH as ot, PatchError as p, objSet as q, MergeError as r, dotToElemId as rt, tryMergeState as s, TraversalDepthError as st, compactDocTombstones as t, rgaPrevForInsertAtIndex as tt, deserializeState as u, createClock as ut, forkState as v, cloneDoc as w, tryApplyPatchInPlace as x, toJson as y, ROOT_KEY as z };