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