json-patch-to-crdt 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1435 @@
1
+ //#region src/clock.ts
2
+ /**
3
+ * Create a new clock for the given actor. Each call to `clock.next()` yields a fresh `Dot`.
4
+ * @param actor - Unique identifier for this peer.
5
+ * @param start - Initial counter value (defaults to 0).
6
+ */
7
+ function createClock(actor, start = 0) {
8
+ const clock = {
9
+ actor,
10
+ ctr: start,
11
+ next() {
12
+ clock.ctr += 1;
13
+ return {
14
+ actor: clock.actor,
15
+ ctr: clock.ctr
16
+ };
17
+ }
18
+ };
19
+ return clock;
20
+ }
21
+ /** Create an independent copy of a clock at the same counter position. */
22
+ function cloneClock(clock) {
23
+ return createClock(clock.actor, clock.ctr);
24
+ }
25
+ /**
26
+ * Generate the next per-actor dot from a mutable version vector.
27
+ * Useful when a server needs to mint dots for many actors.
28
+ */
29
+ function nextDotForActor(vv, actor) {
30
+ const ctr = (vv[actor] ?? 0) + 1;
31
+ vv[actor] = ctr;
32
+ return {
33
+ actor,
34
+ ctr
35
+ };
36
+ }
37
+ /** Record an observed dot in a version vector. */
38
+ function observeDot(vv, dot) {
39
+ if ((vv[dot.actor] ?? 0) < dot.ctr) vv[dot.actor] = dot.ctr;
40
+ }
41
+
42
+ //#endregion
43
+ //#region src/dot.ts
44
+ function compareDot(a, b) {
45
+ if (a.ctr !== b.ctr) return a.ctr - b.ctr;
46
+ return a.actor < b.actor ? -1 : a.actor > b.actor ? 1 : 0;
47
+ }
48
+ function vvHasDot(vv, d) {
49
+ return (vv[d.actor] ?? 0) >= d.ctr;
50
+ }
51
+ 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);
54
+ return out;
55
+ }
56
+ function dotToElemId(d) {
57
+ return `${d.actor}:${d.ctr}`;
58
+ }
59
+
60
+ //#endregion
61
+ //#region src/rga.ts
62
+ const HEAD = "HEAD";
63
+ const linearCache = /* @__PURE__ */ new WeakMap();
64
+ const seqVersion = /* @__PURE__ */ new WeakMap();
65
+ function getVersion(seq) {
66
+ return seqVersion.get(seq) ?? 0;
67
+ }
68
+ function bumpVersion(seq) {
69
+ seqVersion.set(seq, getVersion(seq) + 1);
70
+ }
71
+ function rgaChildrenIndex(seq) {
72
+ const idx = /* @__PURE__ */ new Map();
73
+ for (const e of seq.elems.values()) {
74
+ const arr = idx.get(e.prev) ?? [];
75
+ arr.push(e);
76
+ idx.set(e.prev, arr);
77
+ }
78
+ for (const arr of idx.values()) arr.sort((a, b) => compareDot(b.insDot, a.insDot));
79
+ return idx;
80
+ }
81
+ function rgaLinearizeIds(seq) {
82
+ const ver = getVersion(seq);
83
+ const cached = linearCache.get(seq);
84
+ if (cached && cached.version === ver) return cached.ids;
85
+ const idx = rgaChildrenIndex(seq);
86
+ 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);
93
+ }
94
+ }
95
+ walk(HEAD);
96
+ linearCache.set(seq, {
97
+ version: ver,
98
+ ids: out
99
+ });
100
+ return out;
101
+ }
102
+ function rgaInsertAfter(seq, prev, id, insDot, value) {
103
+ if (seq.elems.has(id)) return;
104
+ seq.elems.set(id, {
105
+ id,
106
+ prev,
107
+ tombstone: false,
108
+ value,
109
+ insDot
110
+ });
111
+ bumpVersion(seq);
112
+ }
113
+ function rgaDelete(seq, id) {
114
+ const e = seq.elems.get(id);
115
+ if (!e) return;
116
+ if (e.tombstone) return;
117
+ e.tombstone = true;
118
+ bumpVersion(seq);
119
+ }
120
+ function rgaIdAtIndex(seq, index) {
121
+ return rgaLinearizeIds(seq)[index];
122
+ }
123
+ function rgaPrevForInsertAtIndex(seq, index) {
124
+ if (index <= 0) return HEAD;
125
+ const ids = rgaLinearizeIds(seq);
126
+ return ids[index - 1] ?? (ids.length ? ids[ids.length - 1] : HEAD);
127
+ }
128
+
129
+ //#endregion
130
+ //#region src/materialize.ts
131
+ /** Recursively convert a CRDT node graph into a plain JSON value. */
132
+ 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;
139
+ }
140
+ case "seq": return rgaLinearizeIds(node).map((id) => materialize(node.elems.get(id).value));
141
+ }
142
+ }
143
+
144
+ //#endregion
145
+ //#region src/nodes.ts
146
+ function newObj() {
147
+ return {
148
+ kind: "obj",
149
+ entries: /* @__PURE__ */ new Map(),
150
+ tombstone: /* @__PURE__ */ new Map()
151
+ };
152
+ }
153
+ function newSeq() {
154
+ return {
155
+ kind: "seq",
156
+ elems: /* @__PURE__ */ new Map()
157
+ };
158
+ }
159
+ function newReg(value, dot) {
160
+ return {
161
+ kind: "lww",
162
+ value,
163
+ dot
164
+ };
165
+ }
166
+ function lwwSet(reg, value, dot) {
167
+ if (compareDot(reg.dot, dot) <= 0) {
168
+ reg.value = value;
169
+ reg.dot = dot;
170
+ }
171
+ }
172
+ function objSet(obj, key, node, dot) {
173
+ const delDot = obj.tombstone.get(key);
174
+ if (delDot && compareDot(delDot, dot) >= 0) return;
175
+ const cur = obj.entries.get(key);
176
+ if (!cur || compareDot(cur.dot, dot) <= 0) obj.entries.set(key, {
177
+ node,
178
+ dot
179
+ });
180
+ }
181
+ function objRemove(obj, key, dot) {
182
+ const curDel = obj.tombstone.get(key);
183
+ if (!curDel || compareDot(curDel, dot) <= 0) obj.tombstone.set(key, dot);
184
+ obj.entries.delete(key);
185
+ }
186
+
187
+ //#endregion
188
+ //#region src/types.ts
189
+ /**
190
+ * Internal sentinel key used in `IntentOp` to represent root-level operations.
191
+ * Namespaced to avoid collision with user data keys.
192
+ */
193
+ const ROOT_KEY = "@@crdt/root";
194
+
195
+ //#endregion
196
+ //#region src/patch.ts
197
+ /**
198
+ * Parse an RFC 6901 JSON Pointer into a path array, unescaping `~1` and `~0`.
199
+ * @param ptr - A JSON Pointer string (e.g. `"/a/b"` or `""`).
200
+ * @returns An array of path segments.
201
+ */
202
+ function parseJsonPointer(ptr) {
203
+ if (ptr === "") return [];
204
+ if (!ptr.startsWith("/")) throw new Error(`Invalid pointer: ${ptr}`);
205
+ return ptr.slice(1).split("/").map((s) => s.replace(/~1/g, "/").replace(/~0/g, "~"));
206
+ }
207
+ /** Convert a path array back to an RFC 6901 JSON Pointer string. */
208
+ function stringifyJsonPointer(path) {
209
+ if (path.length === 0) return "";
210
+ return `/${path.map(escapeJsonPointer).join("/")}`;
211
+ }
212
+ /**
213
+ * Navigate a JSON value by path and return the value at that location.
214
+ * Throws if the path is invalid, out of bounds, or traverses a non-container.
215
+ */
216
+ function getAtJson(base, path) {
217
+ let cur = base;
218
+ for (const seg of path) if (Array.isArray(cur)) {
219
+ const idx = seg === "-" ? cur.length : Number(seg);
220
+ if (!Number.isInteger(idx)) throw new Error(`Expected array index, got ${seg}`);
221
+ if (idx < 0 || idx >= cur.length) throw new Error(`Index out of bounds at ${seg}`);
222
+ cur = cur[idx];
223
+ } else if (cur && typeof cur === "object") {
224
+ if (!(seg in cur)) throw new Error(`Missing key ${seg}`);
225
+ cur = cur[seg];
226
+ } else throw new Error(`Cannot traverse into non-container at ${seg}`);
227
+ return cur;
228
+ }
229
+ /**
230
+ * Compile RFC 6902 JSON Patch operations into CRDT intent operations.
231
+ * `move`/`copy` are expanded to `add` + optional `remove`. Array indices
232
+ * and the `"-"` append token are resolved against the base JSON.
233
+ * @param baseJson - The base JSON value for resolving paths.
234
+ * @param patch - Array of JSON Patch operations.
235
+ * @returns An array of `IntentOp` ready for `applyIntentsToCrdt`.
236
+ */
237
+ function compileJsonPatchToIntent(baseJson, patch) {
238
+ const intents = [];
239
+ for (const op of patch) {
240
+ if (op.op === "test") {
241
+ intents.push({
242
+ t: "Test",
243
+ path: parseJsonPointer(op.path),
244
+ value: op.value
245
+ });
246
+ continue;
247
+ }
248
+ if (op.op === "copy" || op.op === "move") {
249
+ const val = getAtJson(baseJson, parseJsonPointer(op.from));
250
+ intents.push(...compileJsonPatchToIntent(baseJson, [{
251
+ op: "add",
252
+ path: op.path,
253
+ value: val
254
+ }]));
255
+ if (op.op === "move") intents.push(...compileJsonPatchToIntent(baseJson, [{
256
+ op: "remove",
257
+ path: op.from
258
+ }]));
259
+ continue;
260
+ }
261
+ const path = parseJsonPointer(op.path);
262
+ const parent = path.slice(0, -1);
263
+ const last = path[path.length - 1];
264
+ if (path.length === 0) {
265
+ if (op.op === "replace" || op.op === "add") intents.push({
266
+ t: "ObjSet",
267
+ path: [],
268
+ key: ROOT_KEY,
269
+ value: op.value
270
+ });
271
+ else if (op.op === "remove") intents.push({
272
+ t: "ObjSet",
273
+ path: [],
274
+ key: ROOT_KEY,
275
+ value: null
276
+ });
277
+ continue;
278
+ }
279
+ const isIndexLike = (s) => s === "-" || /^[0-9]+$/.test(s);
280
+ if (isIndexLike(last)) {
281
+ const index = last === "-" ? Number.POSITIVE_INFINITY : Number(last);
282
+ if (op.op === "add") intents.push({
283
+ t: "ArrInsert",
284
+ path: parent,
285
+ index,
286
+ value: op.value
287
+ });
288
+ else if (op.op === "remove") intents.push({
289
+ t: "ArrDelete",
290
+ path: parent,
291
+ index
292
+ });
293
+ else if (op.op === "replace") intents.push({
294
+ t: "ArrReplace",
295
+ path: parent,
296
+ index,
297
+ value: op.value
298
+ });
299
+ else assertNever$1(op, "Unsupported op at array index path");
300
+ } else {
301
+ const parentValue = pathValueAt(baseJson, parent);
302
+ if (!isPlainObject(parentValue)) throw new Error(`Expected object parent at ${stringifyJsonPointer(parent)}`);
303
+ if ((op.op === "replace" || op.op === "remove") && !hasOwn(parentValue, last)) throw new Error(`Missing key ${last} at ${stringifyJsonPointer(parent)}`);
304
+ if (op.op === "add") intents.push({
305
+ t: "ObjSet",
306
+ path: parent,
307
+ key: last,
308
+ value: op.value,
309
+ mode: "add"
310
+ });
311
+ else if (op.op === "replace") intents.push({
312
+ t: "ObjSet",
313
+ path: parent,
314
+ key: last,
315
+ value: op.value,
316
+ mode: "replace"
317
+ });
318
+ else if (op.op === "remove") intents.push({
319
+ t: "ObjRemove",
320
+ path: parent,
321
+ key: last
322
+ });
323
+ else assertNever$1(op, "Unsupported op");
324
+ }
325
+ }
326
+ return intents;
327
+ }
328
+ /**
329
+ * Compute a JSON Patch delta between two JSON values.
330
+ * By default arrays use a deterministic LCS strategy.
331
+ * Pass `{ arrayStrategy: "atomic" }` for single-op array replacement.
332
+ * @param base - The original JSON value.
333
+ * @param next - The target JSON value.
334
+ * @param options - Diff options.
335
+ * @returns An array of JSON Patch operations that transform `base` into `next`.
336
+ */
337
+ function diffJsonPatch(base, next, options = {}) {
338
+ const ops = [];
339
+ diffValue([], base, next, ops, options);
340
+ return ops;
341
+ }
342
+ function diffValue(path, base, next, ops, options) {
343
+ if (jsonEquals(base, next)) return;
344
+ if (Array.isArray(base) || Array.isArray(next)) {
345
+ if ((options.arrayStrategy ?? "lcs") === "lcs" && Array.isArray(base) && Array.isArray(next)) {
346
+ diffArray(path, base, next, ops, options);
347
+ return;
348
+ }
349
+ ops.push({
350
+ op: "replace",
351
+ path: stringifyJsonPointer(path),
352
+ value: next
353
+ });
354
+ return;
355
+ }
356
+ if (!isPlainObject(base) || !isPlainObject(next)) {
357
+ ops.push({
358
+ op: "replace",
359
+ path: stringifyJsonPointer(path),
360
+ value: next
361
+ });
362
+ return;
363
+ }
364
+ const baseKeys = Object.keys(base).sort();
365
+ const nextKeys = Object.keys(next).sort();
366
+ const baseSet = new Set(baseKeys);
367
+ const nextSet = new Set(nextKeys);
368
+ for (const key of baseKeys) if (!nextSet.has(key)) ops.push({
369
+ op: "remove",
370
+ path: stringifyJsonPointer([...path, key])
371
+ });
372
+ for (const key of nextKeys) if (!baseSet.has(key)) {
373
+ const nextValue = next[key];
374
+ ops.push({
375
+ op: "add",
376
+ path: stringifyJsonPointer([...path, key]),
377
+ value: nextValue
378
+ });
379
+ }
380
+ for (const key of baseKeys) if (nextSet.has(key)) diffValue([...path, key], base[key], next[key], ops, options);
381
+ }
382
+ function diffArray(path, base, next, ops, _options) {
383
+ const n = base.length;
384
+ const m = next.length;
385
+ const lcs = Array.from({ length: n + 1 }, () => Array(m + 1).fill(0));
386
+ for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--) if (jsonEquals(base[i], next[j])) lcs[i][j] = 1 + lcs[i + 1][j + 1];
387
+ else lcs[i][j] = Math.max(lcs[i + 1][j], lcs[i][j + 1]);
388
+ const localOps = [];
389
+ let i = 0;
390
+ let j = 0;
391
+ let index = 0;
392
+ while (i < n || j < m) {
393
+ if (i < n && j < m && jsonEquals(base[i], next[j])) {
394
+ i += 1;
395
+ j += 1;
396
+ index += 1;
397
+ continue;
398
+ }
399
+ const lcsDown = i < n ? lcs[i + 1][j] : -1;
400
+ const lcsRight = j < m ? lcs[i][j + 1] : -1;
401
+ if (j < m && (i === n || lcsRight > lcsDown)) {
402
+ localOps.push({
403
+ op: "add",
404
+ path: stringifyJsonPointer([...path, String(index)]),
405
+ value: next[j]
406
+ });
407
+ j += 1;
408
+ index += 1;
409
+ continue;
410
+ }
411
+ if (i < n) {
412
+ localOps.push({
413
+ op: "remove",
414
+ path: stringifyJsonPointer([...path, String(index)])
415
+ });
416
+ i += 1;
417
+ continue;
418
+ }
419
+ }
420
+ ops.push(...compactArrayOps(localOps));
421
+ }
422
+ function compactArrayOps(ops) {
423
+ const out = [];
424
+ for (let i = 0; i < ops.length; i++) {
425
+ const op = ops[i];
426
+ const next = ops[i + 1];
427
+ if (op.op === "remove" && next && next.op === "add" && op.path === next.path) {
428
+ out.push({
429
+ op: "replace",
430
+ path: op.path,
431
+ value: next.value
432
+ });
433
+ i += 1;
434
+ continue;
435
+ }
436
+ out.push(op);
437
+ }
438
+ return out;
439
+ }
440
+ function escapeJsonPointer(token) {
441
+ return token.replace(/~/g, "~0").replace(/\//g, "~1");
442
+ }
443
+ /** Deep equality check for JSON values (null-safe, handles arrays and objects). */
444
+ function jsonEquals(a, b) {
445
+ if (a === b) return true;
446
+ if (a === null || b === null) return false;
447
+ if (Array.isArray(a) || Array.isArray(b)) {
448
+ if (!Array.isArray(a) || !Array.isArray(b)) return false;
449
+ if (a.length !== b.length) return false;
450
+ for (let i = 0; i < a.length; i++) if (!jsonEquals(a[i], b[i])) return false;
451
+ return true;
452
+ }
453
+ if (!isPlainObject(a) || !isPlainObject(b)) return false;
454
+ const aKeys = Object.keys(a);
455
+ const bKeys = Object.keys(b);
456
+ if (aKeys.length !== bKeys.length) return false;
457
+ for (const key of aKeys) {
458
+ if (!(key in b)) return false;
459
+ if (!jsonEquals(a[key], b[key])) return false;
460
+ }
461
+ return true;
462
+ }
463
+ function isPlainObject(value) {
464
+ return typeof value === "object" && value !== null && !Array.isArray(value);
465
+ }
466
+ function hasOwn(value, key) {
467
+ return Object.prototype.hasOwnProperty.call(value, key);
468
+ }
469
+ function pathValueAt(base, path) {
470
+ if (path.length === 0) return base;
471
+ return getAtJson(base, path);
472
+ }
473
+ function assertNever$1(_value, message) {
474
+ throw new Error(message);
475
+ }
476
+
477
+ //#endregion
478
+ //#region src/doc.ts
479
+ /**
480
+ * Create a CRDT document from a JSON value, using fresh dots for each node.
481
+ * @param value - The JSON value to convert.
482
+ * @param nextDot - A function that generates a unique `Dot` on each call.
483
+ * @returns A new CRDT `Doc`.
484
+ */
485
+ function docFromJson(value, nextDot) {
486
+ return { root: nodeFromJson(value, nextDot) };
487
+ }
488
+ /**
489
+ * Legacy: create a doc using a single dot with counter offsets for array children.
490
+ * Prefer `docFromJson(value, nextDot)` to ensure unique dots per node.
491
+ */
492
+ function docFromJsonWithDot(value, dot) {
493
+ return { root: deepNodeFromJson(value, dot) };
494
+ }
495
+ function getSeqAtPath(doc, path) {
496
+ let cur = doc.root;
497
+ for (const seg of path) {
498
+ if (cur.kind !== "obj") return;
499
+ const ent = cur.entries.get(seg);
500
+ if (!ent) return;
501
+ cur = ent.node;
502
+ }
503
+ return cur.kind === "seq" ? cur : void 0;
504
+ }
505
+ function getObjAtPathStrict(doc, path) {
506
+ let cur = doc.root;
507
+ const seen = [];
508
+ if (path.length === 0) {
509
+ if (cur.kind !== "obj") return {
510
+ ok: false,
511
+ message: "expected object at /"
512
+ };
513
+ return {
514
+ ok: true,
515
+ obj: cur
516
+ };
517
+ }
518
+ for (const seg of path) {
519
+ if (cur.kind !== "obj") return {
520
+ ok: false,
521
+ message: `expected object at /${seen.join("/")}`
522
+ };
523
+ const entry = cur.entries.get(seg);
524
+ seen.push(seg);
525
+ if (!entry || entry.node.kind !== "obj") return {
526
+ ok: false,
527
+ message: `expected object at /${seen.join("/")}`
528
+ };
529
+ cur = entry.node;
530
+ }
531
+ return {
532
+ ok: true,
533
+ obj: cur
534
+ };
535
+ }
536
+ function ensureSeqAtPath(head, path, dotForCreate) {
537
+ let cur = head.root;
538
+ let parent = null;
539
+ let parentKey = null;
540
+ if (path.length === 0) {
541
+ if (head.root.kind !== "seq") head.root = newSeq();
542
+ return head.root;
543
+ }
544
+ for (let i = 0; i < path.length; i++) {
545
+ const seg = path[i];
546
+ if (cur.kind !== "obj") {
547
+ const replacement = newObj();
548
+ if (parent && parentKey !== null) objSet(parent, parentKey, replacement, dotForCreate);
549
+ else head.root = replacement;
550
+ cur = replacement;
551
+ }
552
+ const obj = cur;
553
+ const ent = obj.entries.get(seg);
554
+ if (i === path.length - 1) {
555
+ if (!ent || ent.node.kind !== "seq") {
556
+ const seq = newSeq();
557
+ objSet(obj, seg, seq, dotForCreate);
558
+ return seq;
559
+ }
560
+ return ent.node;
561
+ }
562
+ if (!ent || ent.node.kind !== "obj") {
563
+ const child = newObj();
564
+ objSet(obj, seg, child, dotForCreate);
565
+ parent = obj;
566
+ parentKey = seg;
567
+ cur = child;
568
+ } else {
569
+ parent = obj;
570
+ parentKey = seg;
571
+ cur = ent.node;
572
+ }
573
+ }
574
+ if (head.root.kind !== "seq") head.root = newSeq();
575
+ return head.root;
576
+ }
577
+ function deepNodeFromJson(value, dot) {
578
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return newReg(value, dot);
579
+ if (Array.isArray(value)) {
580
+ const seq = newSeq();
581
+ let prev = HEAD;
582
+ let ctr = dot.ctr;
583
+ for (const v of value) {
584
+ const childDot = {
585
+ actor: dot.actor,
586
+ ctr: ++ctr
587
+ };
588
+ const id = dotToElemId(childDot);
589
+ rgaInsertAfter(seq, prev, id, childDot, deepNodeFromJson(v, childDot));
590
+ prev = id;
591
+ }
592
+ return seq;
593
+ }
594
+ const obj = newObj();
595
+ for (const [k, v] of Object.entries(value)) objSet(obj, k, deepNodeFromJson(v, dot), dot);
596
+ return obj;
597
+ }
598
+ function nodeFromJson(value, nextDot) {
599
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return newReg(value, nextDot());
600
+ if (Array.isArray(value)) {
601
+ const seq = newSeq();
602
+ let prev = HEAD;
603
+ for (const v of value) {
604
+ const insDot = nextDot();
605
+ const id = dotToElemId(insDot);
606
+ rgaInsertAfter(seq, prev, id, insDot, nodeFromJson(v, nextDot));
607
+ prev = id;
608
+ }
609
+ return seq;
610
+ }
611
+ const obj = newObj();
612
+ for (const [k, v] of Object.entries(value)) {
613
+ const entryDot = nextDot();
614
+ objSet(obj, k, nodeFromJson(v, nextDot), entryDot);
615
+ }
616
+ return obj;
617
+ }
618
+ /** Deep-clone a CRDT document. The clone is fully independent of the original. */
619
+ function cloneDoc(doc) {
620
+ return { root: cloneNode(doc.root) };
621
+ }
622
+ function cloneNode(node) {
623
+ if (node.kind === "lww") return {
624
+ kind: "lww",
625
+ value: structuredClone(node.value),
626
+ dot: {
627
+ actor: node.dot.actor,
628
+ ctr: node.dot.ctr
629
+ }
630
+ };
631
+ if (node.kind === "obj") {
632
+ const entries = /* @__PURE__ */ new Map();
633
+ for (const [k, v] of node.entries.entries()) entries.set(k, {
634
+ node: cloneNode(v.node),
635
+ dot: {
636
+ actor: v.dot.actor,
637
+ ctr: v.dot.ctr
638
+ }
639
+ });
640
+ const tombstone = /* @__PURE__ */ new Map();
641
+ for (const [k, d] of node.tombstone.entries()) tombstone.set(k, {
642
+ actor: d.actor,
643
+ ctr: d.ctr
644
+ });
645
+ return {
646
+ kind: "obj",
647
+ entries,
648
+ tombstone
649
+ };
650
+ }
651
+ const elems = /* @__PURE__ */ new Map();
652
+ for (const [id, e] of node.elems.entries()) elems.set(id, {
653
+ id: e.id,
654
+ prev: e.prev,
655
+ tombstone: e.tombstone,
656
+ value: cloneNode(e.value),
657
+ insDot: {
658
+ actor: e.insDot.actor,
659
+ ctr: e.insDot.ctr
660
+ }
661
+ });
662
+ return {
663
+ kind: "seq",
664
+ elems
665
+ };
666
+ }
667
+ function applyTest(base, head, it, evalTestAgainst) {
668
+ const snapshot = evalTestAgainst === "head" ? materialize(head.root) : materialize(base.root);
669
+ let got;
670
+ try {
671
+ got = getAtJson(snapshot, it.path);
672
+ } catch {
673
+ return {
674
+ ok: false,
675
+ code: 409,
676
+ message: `test path missing at /${it.path.join("/")}`
677
+ };
678
+ }
679
+ if (!jsonEquals(got, it.value)) return {
680
+ ok: false,
681
+ code: 409,
682
+ message: `test failed at /${it.path.join("/")}`
683
+ };
684
+ return null;
685
+ }
686
+ function applyObjSet(head, it, newDot) {
687
+ if (it.path.length === 0 && it.key === ROOT_KEY) {
688
+ head.root = nodeFromJson(it.value, newDot);
689
+ return null;
690
+ }
691
+ const parentRes = getObjAtPathStrict(head, it.path);
692
+ if (!parentRes.ok) return {
693
+ ok: false,
694
+ code: 409,
695
+ message: parentRes.message
696
+ };
697
+ if (it.mode === "replace" && !parentRes.obj.entries.has(it.key)) return {
698
+ ok: false,
699
+ code: 409,
700
+ message: `no value at /${[...it.path, it.key].join("/")}`
701
+ };
702
+ const d = newDot();
703
+ const parentObj = parentRes.obj;
704
+ objSet(parentObj, it.key, nodeFromJson(it.value, newDot), d);
705
+ return null;
706
+ }
707
+ function applyObjRemove(head, it, newDot) {
708
+ const parentRes = getObjAtPathStrict(head, it.path);
709
+ if (!parentRes.ok) return {
710
+ ok: false,
711
+ code: 409,
712
+ message: parentRes.message
713
+ };
714
+ if (!parentRes.obj.entries.has(it.key)) return {
715
+ ok: false,
716
+ code: 409,
717
+ message: `no value at /${[...it.path, it.key].join("/")}`
718
+ };
719
+ const d = newDot();
720
+ const parentObj = parentRes.obj;
721
+ objRemove(parentObj, it.key, d);
722
+ return null;
723
+ }
724
+ function applyArrInsert(base, head, it, newDot, bumpCounterAbove) {
725
+ const baseSeq = getSeqAtPath(base, it.path);
726
+ if (!baseSeq) {
727
+ if (it.index === 0 || it.index === Number.POSITIVE_INFINITY) {
728
+ const headSeq = ensureSeqAtPath(head, it.path, newDot());
729
+ const prev = it.index === 0 ? HEAD : rgaPrevForInsertAtIndex(headSeq, Number.MAX_SAFE_INTEGER);
730
+ const d = nextInsertDotForPrev(headSeq, prev, newDot, bumpCounterAbove);
731
+ rgaInsertAfter(headSeq, prev, dotToElemId(d), d, nodeFromJson(it.value, newDot));
732
+ return null;
733
+ }
734
+ return {
735
+ ok: false,
736
+ code: 409,
737
+ message: `base array missing at /${it.path.join("/")}`
738
+ };
739
+ }
740
+ const headSeq = ensureSeqAtPath(head, it.path, newDot());
741
+ const idx = it.index === Number.POSITIVE_INFINITY ? rgaLinearizeIds(baseSeq).length : it.index;
742
+ const baseLen = rgaLinearizeIds(baseSeq).length;
743
+ if (idx < 0 || idx > baseLen) return {
744
+ ok: false,
745
+ code: 409,
746
+ message: `index out of bounds at /${it.path.join("/")}/${it.index}`
747
+ };
748
+ const prev = idx === 0 ? HEAD : rgaIdAtIndex(baseSeq, idx - 1) ?? HEAD;
749
+ const d = nextInsertDotForPrev(headSeq, prev, newDot, bumpCounterAbove);
750
+ rgaInsertAfter(headSeq, prev, dotToElemId(d), d, nodeFromJson(it.value, newDot));
751
+ return null;
752
+ }
753
+ function nextInsertDotForPrev(seq, prev, newDot, bumpCounterAbove) {
754
+ let maxSiblingDot = null;
755
+ for (const elem of seq.elems.values()) {
756
+ if (elem.prev !== prev) continue;
757
+ if (!maxSiblingDot || compareDot(elem.insDot, maxSiblingDot) > 0) maxSiblingDot = elem.insDot;
758
+ }
759
+ if (maxSiblingDot) bumpCounterAbove?.(maxSiblingDot.ctr);
760
+ let candidate = newDot();
761
+ while (maxSiblingDot && compareDot(candidate, maxSiblingDot) <= 0) candidate = newDot();
762
+ return candidate;
763
+ }
764
+ function applyArrDelete(base, head, it, newDot) {
765
+ const d = newDot();
766
+ const baseSeq = getSeqAtPath(base, it.path);
767
+ if (!baseSeq) return {
768
+ ok: false,
769
+ code: 409,
770
+ message: `base array missing at /${it.path.join("/")}`
771
+ };
772
+ const headSeq = ensureSeqAtPath(head, it.path, d);
773
+ const baseId = rgaIdAtIndex(baseSeq, it.index);
774
+ if (!baseId) return {
775
+ ok: false,
776
+ code: 409,
777
+ message: `no base element at index ${it.index}`
778
+ };
779
+ rgaDelete(headSeq, baseId);
780
+ return null;
781
+ }
782
+ function applyArrReplace(base, head, it, newDot) {
783
+ const d = newDot();
784
+ const baseSeq = getSeqAtPath(base, it.path);
785
+ if (!baseSeq) return {
786
+ ok: false,
787
+ code: 409,
788
+ message: `base array missing at /${it.path.join("/")}`
789
+ };
790
+ const headSeq = ensureSeqAtPath(head, it.path, d);
791
+ const baseId = rgaIdAtIndex(baseSeq, it.index);
792
+ if (!baseId) return {
793
+ ok: false,
794
+ code: 409,
795
+ message: `no base element at index ${it.index}`
796
+ };
797
+ const e = headSeq.elems.get(baseId);
798
+ if (!e || e.tombstone) return {
799
+ ok: false,
800
+ code: 409,
801
+ message: `element already deleted at index ${it.index}`
802
+ };
803
+ e.value = nodeFromJson(it.value, newDot);
804
+ return null;
805
+ }
806
+ /**
807
+ * Apply compiled intent operations to a CRDT document.
808
+ * Array indices are resolved against the base document.
809
+ * @param base - The base document snapshot used for index mapping and test evaluation.
810
+ * @param head - The target document to mutate.
811
+ * @param intents - Compiled intent operations from `compileJsonPatchToIntent`.
812
+ * @param newDot - A function that generates a unique `Dot` per mutation.
813
+ * @param evalTestAgainst - Whether `test` ops are evaluated against `"head"` or `"base"`.
814
+ * @param bumpCounterAbove - Optional hook that can fast-forward the underlying counter before inserts.
815
+ * @returns `{ ok: true }` on success, or `{ ok: false, code: 409, message }` on conflict.
816
+ */
817
+ function applyIntentsToCrdt(base, head, intents, newDot, evalTestAgainst = "head", bumpCounterAbove) {
818
+ for (const it of intents) {
819
+ let fail = null;
820
+ switch (it.t) {
821
+ case "Test":
822
+ fail = applyTest(base, head, it, evalTestAgainst);
823
+ break;
824
+ case "ObjSet":
825
+ fail = applyObjSet(head, it, newDot);
826
+ break;
827
+ case "ObjRemove":
828
+ fail = applyObjRemove(head, it, newDot);
829
+ break;
830
+ case "ArrInsert":
831
+ fail = applyArrInsert(base, head, it, newDot, bumpCounterAbove);
832
+ break;
833
+ case "ArrDelete":
834
+ fail = applyArrDelete(base, head, it, newDot);
835
+ break;
836
+ case "ArrReplace":
837
+ fail = applyArrReplace(base, head, it, newDot);
838
+ break;
839
+ default: assertNever(it, "Unhandled intent type");
840
+ }
841
+ if (fail) return fail;
842
+ }
843
+ return { ok: true };
844
+ }
845
+ /**
846
+ * Convenience wrapper: compile a JSON Patch and apply it to a CRDT document.
847
+ * @param base - The base document for index resolution.
848
+ * @param head - The target document to mutate.
849
+ * @param patch - Array of RFC 6902 JSON Patch operations.
850
+ * @param newDot - A function that generates a unique `Dot` per mutation.
851
+ * @param evalTestAgainst - Whether `test` ops evaluate against `"head"` or `"base"`.
852
+ * @param bumpCounterAbove - Optional hook that can fast-forward the underlying counter before inserts.
853
+ * @returns `{ ok: true }` on success, or `{ ok: false, code: 409, message }` on conflict.
854
+ */
855
+ function jsonPatchToCrdt(base, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove) {
856
+ return applyIntentsToCrdt(base, head, compileJsonPatchToIntent(materialize(base.root), patch), newDot, evalTestAgainst, bumpCounterAbove);
857
+ }
858
+ /**
859
+ * Safe wrapper around `jsonPatchToCrdt` that converts compile-time errors into `409` results.
860
+ * This function never throws for malformed/invalid patch paths.
861
+ */
862
+ function jsonPatchToCrdtSafe(base, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove) {
863
+ try {
864
+ return jsonPatchToCrdt(base, head, patch, newDot, evalTestAgainst, bumpCounterAbove);
865
+ } catch (error) {
866
+ return {
867
+ ok: false,
868
+ code: 409,
869
+ message: error instanceof Error ? error.message : "failed to compile patch"
870
+ };
871
+ }
872
+ }
873
+ /**
874
+ * Generate a JSON Patch delta between two CRDT documents.
875
+ * @param base - The base document snapshot.
876
+ * @param head - The current document state.
877
+ * @param options - Diff options (e.g. `{ arrayStrategy: "lcs" }`).
878
+ * @returns An array of JSON Patch operations that transform base into head.
879
+ */
880
+ function crdtToJsonPatch(base, head, options) {
881
+ return diffJsonPatch(materialize(base.root), materialize(head.root), options);
882
+ }
883
+ /**
884
+ * Emit a single root `replace` patch representing the full document state.
885
+ * Use `crdtToJsonPatch(base, head)` for delta patches instead.
886
+ */
887
+ function crdtToFullReplace(doc) {
888
+ return [{
889
+ op: "replace",
890
+ path: "",
891
+ value: materialize(doc.root)
892
+ }];
893
+ }
894
+ function assertNever(_value, message) {
895
+ throw new Error(message);
896
+ }
897
+
898
+ //#endregion
899
+ //#region src/state.ts
900
+ /** Error thrown when a JSON Patch cannot be applied. Includes a numeric `.code` (409 for conflicts). */
901
+ var PatchError = class extends Error {
902
+ code;
903
+ constructor(message, code = 409) {
904
+ super(message);
905
+ this.name = "PatchError";
906
+ this.code = code;
907
+ }
908
+ };
909
+ /**
910
+ * Create a new CRDT state from an initial JSON value.
911
+ * @param initial - The initial JSON document.
912
+ * @param options - Actor ID and optional starting counter.
913
+ * @returns A new `CrdtState` containing the document and clock.
914
+ */
915
+ function createState(initial, options) {
916
+ const clock = createClock(options.actor, options.start ?? 0);
917
+ return {
918
+ doc: docFromJson(initial, clock.next),
919
+ clock
920
+ };
921
+ }
922
+ /**
923
+ * Materialize a CRDT document or state back to a plain JSON value.
924
+ * @param target - A `Doc` or `CrdtState` to materialize.
925
+ * @returns The JSON representation of the current document.
926
+ */
927
+ function toJson(target) {
928
+ if ("doc" in target) return materialize(target.doc.root);
929
+ return materialize(target.root);
930
+ }
931
+ /**
932
+ * Apply a JSON Patch to the state, returning a new immutable state.
933
+ * Throws `PatchError` on conflict (e.g. out-of-bounds index, failed test op).
934
+ * @param state - The current CRDT state.
935
+ * @param patch - Array of RFC 6902 JSON Patch operations.
936
+ * @param options - Optional base document and test evaluation mode.
937
+ * @returns A new `CrdtState` with the patch applied.
938
+ */
939
+ function applyPatch(state, patch, options = {}) {
940
+ const nextState = {
941
+ doc: cloneDoc(state.doc),
942
+ clock: cloneClock(state.clock)
943
+ };
944
+ const result = applyPatchInternal(nextState, patch, options);
945
+ if (!result.ok) throw new PatchError(result.message, result.code);
946
+ return nextState;
947
+ }
948
+ /**
949
+ * Apply a JSON Patch to the state in place, mutating the existing state.
950
+ * Throws `PatchError` on conflict.
951
+ * @param state - The CRDT state to mutate.
952
+ * @param patch - Array of RFC 6902 JSON Patch operations.
953
+ * @param options - Optional base document and test evaluation mode.
954
+ */
955
+ function applyPatchInPlace(state, patch, options = {}) {
956
+ if (options.atomic ?? true) {
957
+ const next = applyPatch(state, patch, options);
958
+ state.doc = next.doc;
959
+ state.clock = next.clock;
960
+ return;
961
+ }
962
+ const result = applyPatchInternal(state, patch, options);
963
+ if (!result.ok) throw new PatchError(result.message, result.code);
964
+ }
965
+ /**
966
+ * Apply a JSON Patch as a specific actor while maintaining an external version vector.
967
+ * Returns the updated state and a new version vector snapshot.
968
+ */
969
+ function applyPatchAsActor(doc, vv, actor, patch, options = {}) {
970
+ const observedCtr = maxCtrInNodeForActor$1(doc.root, actor);
971
+ const state = applyPatch({
972
+ doc,
973
+ clock: createClock(actor, Math.max(vv[actor] ?? 0, observedCtr))
974
+ }, patch, options);
975
+ return {
976
+ state,
977
+ vv: {
978
+ ...vv,
979
+ [actor]: Math.max(vv[actor] ?? 0, state.clock.ctr)
980
+ }
981
+ };
982
+ }
983
+ function applyPatchInternal(state, patch, options) {
984
+ if ((options.semantics ?? "base") === "sequential") {
985
+ const explicitBaseState = options.base ? {
986
+ doc: cloneDoc(options.base),
987
+ clock: createClock("__base__", 0)
988
+ } : null;
989
+ for (const op of patch) {
990
+ const step = applyPatchOpSequential(state, op, options, explicitBaseState ? explicitBaseState.doc : cloneDoc(state.doc));
991
+ if (!step.ok) return step;
992
+ if (explicitBaseState) {
993
+ const baseStep = applyPatchInternal(explicitBaseState, [op], {
994
+ semantics: "sequential",
995
+ testAgainst: options.testAgainst
996
+ });
997
+ if (!baseStep.ok) return baseStep;
998
+ }
999
+ }
1000
+ return { ok: true };
1001
+ }
1002
+ const baseDoc = options.base ? options.base : cloneDoc(state.doc);
1003
+ const compiled = compileIntents(materialize(baseDoc.root), patch);
1004
+ if (!compiled.ok) return compiled;
1005
+ return applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr));
1006
+ }
1007
+ function applyPatchOpSequential(state, op, options, baseDoc) {
1008
+ const baseJson = materialize(baseDoc.root);
1009
+ if (op.op === "move") {
1010
+ const fromValue = getAtJson(baseJson, parseJsonPointer(op.from));
1011
+ const removeRes = applySinglePatchOp(state, baseDoc, {
1012
+ op: "remove",
1013
+ path: op.from
1014
+ }, options);
1015
+ if (!removeRes.ok) return removeRes;
1016
+ return applySinglePatchOp(state, cloneDoc(state.doc), {
1017
+ op: "add",
1018
+ path: op.path,
1019
+ value: fromValue
1020
+ }, options);
1021
+ }
1022
+ if (op.op === "copy") {
1023
+ const fromValue = getAtJson(baseJson, parseJsonPointer(op.from));
1024
+ return applySinglePatchOp(state, baseDoc, {
1025
+ op: "add",
1026
+ path: op.path,
1027
+ value: fromValue
1028
+ }, options);
1029
+ }
1030
+ return applySinglePatchOp(state, baseDoc, op, options);
1031
+ }
1032
+ function applySinglePatchOp(state, baseDoc, op, options) {
1033
+ const compiled = compileIntents(materialize(baseDoc.root), [op]);
1034
+ if (!compiled.ok) return compiled;
1035
+ return applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr));
1036
+ }
1037
+ function bumpClockCounter(state, ctr) {
1038
+ if (state.clock.ctr < ctr) state.clock.ctr = ctr;
1039
+ }
1040
+ function compileIntents(baseJson, patch) {
1041
+ try {
1042
+ return {
1043
+ ok: true,
1044
+ intents: compileJsonPatchToIntent(baseJson, patch)
1045
+ };
1046
+ } catch (error) {
1047
+ return {
1048
+ ok: false,
1049
+ code: 409,
1050
+ message: error instanceof Error ? error.message : "failed to compile patch"
1051
+ };
1052
+ }
1053
+ }
1054
+ function maxCtrInNodeForActor$1(node, actor) {
1055
+ switch (node.kind) {
1056
+ case "lww": return node.dot.actor === actor ? node.dot.ctr : 0;
1057
+ case "obj": {
1058
+ let best = 0;
1059
+ for (const entry of node.entries.values()) {
1060
+ if (entry.dot.actor === actor && entry.dot.ctr > best) best = entry.dot.ctr;
1061
+ const childBest = maxCtrInNodeForActor$1(entry.node, actor);
1062
+ if (childBest > best) best = childBest;
1063
+ }
1064
+ for (const tomb of node.tombstone.values()) if (tomb.actor === actor && tomb.ctr > best) best = tomb.ctr;
1065
+ return best;
1066
+ }
1067
+ case "seq": {
1068
+ let best = 0;
1069
+ for (const elem of node.elems.values()) {
1070
+ if (elem.insDot.actor === actor && elem.insDot.ctr > best) best = elem.insDot.ctr;
1071
+ const childBest = maxCtrInNodeForActor$1(elem.value, actor);
1072
+ if (childBest > best) best = childBest;
1073
+ }
1074
+ return best;
1075
+ }
1076
+ }
1077
+ }
1078
+
1079
+ //#endregion
1080
+ //#region src/serialize.ts
1081
+ /** Serialize a CRDT document to a JSON-safe representation (Maps become plain objects). */
1082
+ function serializeDoc(doc) {
1083
+ return { root: serializeNode(doc.root) };
1084
+ }
1085
+ /** Reconstruct a CRDT document from its serialized form. */
1086
+ function deserializeDoc(data) {
1087
+ return { root: deserializeNode(data.root) };
1088
+ }
1089
+ /** Serialize a full CRDT state (document + clock) to a JSON-safe representation. */
1090
+ function serializeState(state) {
1091
+ return {
1092
+ doc: serializeDoc(state.doc),
1093
+ clock: {
1094
+ actor: state.clock.actor,
1095
+ ctr: state.clock.ctr
1096
+ }
1097
+ };
1098
+ }
1099
+ /** Reconstruct a full CRDT state from its serialized form, restoring the clock. */
1100
+ function deserializeState(data) {
1101
+ const clock = createClock(data.clock.actor, data.clock.ctr);
1102
+ return {
1103
+ doc: deserializeDoc(data.doc),
1104
+ clock
1105
+ };
1106
+ }
1107
+ function serializeNode(node) {
1108
+ if (node.kind === "lww") return {
1109
+ kind: "lww",
1110
+ value: structuredClone(node.value),
1111
+ dot: {
1112
+ actor: node.dot.actor,
1113
+ ctr: node.dot.ctr
1114
+ }
1115
+ };
1116
+ if (node.kind === "obj") {
1117
+ const entries = {};
1118
+ for (const [k, v] of node.entries.entries()) entries[k] = {
1119
+ node: serializeNode(v.node),
1120
+ dot: {
1121
+ actor: v.dot.actor,
1122
+ ctr: v.dot.ctr
1123
+ }
1124
+ };
1125
+ const tombstone = {};
1126
+ for (const [k, d] of node.tombstone.entries()) tombstone[k] = {
1127
+ actor: d.actor,
1128
+ ctr: d.ctr
1129
+ };
1130
+ return {
1131
+ kind: "obj",
1132
+ entries,
1133
+ tombstone
1134
+ };
1135
+ }
1136
+ const elems = {};
1137
+ for (const [id, e] of node.elems.entries()) elems[id] = {
1138
+ id: e.id,
1139
+ prev: e.prev,
1140
+ tombstone: e.tombstone,
1141
+ value: serializeNode(e.value),
1142
+ insDot: {
1143
+ actor: e.insDot.actor,
1144
+ ctr: e.insDot.ctr
1145
+ }
1146
+ };
1147
+ return {
1148
+ kind: "seq",
1149
+ elems
1150
+ };
1151
+ }
1152
+ function deserializeNode(node) {
1153
+ if (node.kind === "lww") return {
1154
+ kind: "lww",
1155
+ value: structuredClone(node.value),
1156
+ dot: {
1157
+ actor: node.dot.actor,
1158
+ ctr: node.dot.ctr
1159
+ }
1160
+ };
1161
+ if (node.kind === "obj") {
1162
+ const entries = /* @__PURE__ */ new Map();
1163
+ for (const [k, v] of Object.entries(node.entries)) entries.set(k, {
1164
+ node: deserializeNode(v.node),
1165
+ dot: {
1166
+ actor: v.dot.actor,
1167
+ ctr: v.dot.ctr
1168
+ }
1169
+ });
1170
+ const tombstone = /* @__PURE__ */ new Map();
1171
+ for (const [k, d] of Object.entries(node.tombstone)) tombstone.set(k, {
1172
+ actor: d.actor,
1173
+ ctr: d.ctr
1174
+ });
1175
+ return {
1176
+ kind: "obj",
1177
+ entries,
1178
+ tombstone
1179
+ };
1180
+ }
1181
+ const elems = /* @__PURE__ */ new Map();
1182
+ for (const [id, e] of Object.entries(node.elems)) elems.set(id, {
1183
+ id: e.id,
1184
+ prev: e.prev,
1185
+ tombstone: e.tombstone,
1186
+ value: deserializeNode(e.value),
1187
+ insDot: {
1188
+ actor: e.insDot.actor,
1189
+ ctr: e.insDot.ctr
1190
+ }
1191
+ });
1192
+ return {
1193
+ kind: "seq",
1194
+ elems
1195
+ };
1196
+ }
1197
+
1198
+ //#endregion
1199
+ //#region src/merge.ts
1200
+ /**
1201
+ * Merge two CRDT documents from different peers into one.
1202
+ * By default this requires shared array lineage for non-empty sequences.
1203
+ *
1204
+ * Resolution rules:
1205
+ * - **LwwReg**: the register with the higher dot wins (total order by counter then actor).
1206
+ * - **ObjNode**: entries are merged key-by-key; tombstones use max-dot-per-key.
1207
+ * If both sides have a live entry for the same key, the entry nodes are merged recursively.
1208
+ * Delete-wins: if a tombstone dot >= an entry dot, the entry is removed.
1209
+ * - **RgaSeq**: elements from both sides are unioned by element ID.
1210
+ * If both sides have the same element, tombstone wins (delete bias) and values are merged recursively.
1211
+ * - **Kind mismatch**: the node with the higher "representative dot" wins and replaces the other entirely.
1212
+ */
1213
+ function mergeDoc(a, b, options = {}) {
1214
+ const mismatchPath = options.requireSharedOrigin ?? true ? findSeqLineageMismatch(a.root, b.root, []) : null;
1215
+ if (mismatchPath) throw new Error(`merge requires shared array origin at ${mismatchPath}`);
1216
+ return { root: mergeNode(a.root, b.root) };
1217
+ }
1218
+ /**
1219
+ * Merge two CRDT states.
1220
+ *
1221
+ * The merged clock keeps a stable actor identity:
1222
+ * - defaults to the actor from the first argument (`a`)
1223
+ * - can be overridden via `options.actor`
1224
+ * - optional `options.requireSharedOrigin` controls merge lineage checks
1225
+ *
1226
+ * The merged counter is lifted to the highest counter already observed for
1227
+ * that actor across both input clocks and the merged document dots.
1228
+ */
1229
+ function mergeState(a, b, options = {}) {
1230
+ const doc = mergeDoc(a.doc, b.doc, { requireSharedOrigin: options.requireSharedOrigin });
1231
+ const actor = options.actor ?? a.clock.actor;
1232
+ return {
1233
+ doc,
1234
+ clock: createClock(actor, maxObservedCtrForActor(doc, actor, a, b))
1235
+ };
1236
+ }
1237
+ function findSeqLineageMismatch(a, b, path) {
1238
+ if (a.kind === "seq" && b.kind === "seq") {
1239
+ const hasElemsA = a.elems.size > 0;
1240
+ const hasElemsB = b.elems.size > 0;
1241
+ if (hasElemsA && hasElemsB) {
1242
+ let shared = false;
1243
+ for (const id of a.elems.keys()) if (b.elems.has(id)) {
1244
+ shared = true;
1245
+ break;
1246
+ }
1247
+ if (!shared) return `/${path.join("/")}`;
1248
+ }
1249
+ }
1250
+ if (a.kind === "obj" && b.kind === "obj") {
1251
+ const sharedKeys = new Set([...a.entries.keys()].filter((key) => b.entries.has(key)));
1252
+ for (const key of sharedKeys) {
1253
+ const nextA = a.entries.get(key).node;
1254
+ const nextB = b.entries.get(key).node;
1255
+ const mismatch = findSeqLineageMismatch(nextA, nextB, [...path, key]);
1256
+ if (mismatch) return mismatch;
1257
+ }
1258
+ }
1259
+ return null;
1260
+ }
1261
+ function maxObservedCtrForActor(doc, actor, a, b) {
1262
+ let best = maxCtrInNodeForActor(doc.root, actor);
1263
+ if (a.clock.actor === actor && a.clock.ctr > best) best = a.clock.ctr;
1264
+ if (b.clock.actor === actor && b.clock.ctr > best) best = b.clock.ctr;
1265
+ return best;
1266
+ }
1267
+ function maxCtrInNodeForActor(node, actor) {
1268
+ switch (node.kind) {
1269
+ case "lww": return node.dot.actor === actor ? node.dot.ctr : 0;
1270
+ case "obj": {
1271
+ let best = 0;
1272
+ for (const entry of node.entries.values()) {
1273
+ if (entry.dot.actor === actor && entry.dot.ctr > best) best = entry.dot.ctr;
1274
+ const childBest = maxCtrInNodeForActor(entry.node, actor);
1275
+ if (childBest > best) best = childBest;
1276
+ }
1277
+ for (const tomb of node.tombstone.values()) if (tomb.actor === actor && tomb.ctr > best) best = tomb.ctr;
1278
+ return best;
1279
+ }
1280
+ case "seq": {
1281
+ let best = 0;
1282
+ for (const elem of node.elems.values()) {
1283
+ if (elem.insDot.actor === actor && elem.insDot.ctr > best) best = elem.insDot.ctr;
1284
+ const childBest = maxCtrInNodeForActor(elem.value, actor);
1285
+ if (childBest > best) best = childBest;
1286
+ }
1287
+ return best;
1288
+ }
1289
+ }
1290
+ }
1291
+ function repDot(node) {
1292
+ switch (node.kind) {
1293
+ case "lww": return node.dot;
1294
+ case "obj": {
1295
+ let best = {
1296
+ actor: "",
1297
+ ctr: 0
1298
+ };
1299
+ for (const entry of node.entries.values()) if (compareDot(entry.dot, best) > 0) best = entry.dot;
1300
+ for (const d of node.tombstone.values()) if (compareDot(d, best) > 0) best = d;
1301
+ return best;
1302
+ }
1303
+ case "seq": {
1304
+ let best = {
1305
+ actor: "",
1306
+ ctr: 0
1307
+ };
1308
+ for (const e of node.elems.values()) if (compareDot(e.insDot, best) > 0) best = e.insDot;
1309
+ return best;
1310
+ }
1311
+ }
1312
+ }
1313
+ function mergeNode(a, b) {
1314
+ if (a.kind === "lww" && b.kind === "lww") return mergeLww(a, b);
1315
+ if (a.kind === "obj" && b.kind === "obj") return mergeObj(a, b);
1316
+ if (a.kind === "seq" && b.kind === "seq") return mergeSeq(a, b);
1317
+ if (compareDot(repDot(a), repDot(b)) >= 0) return cloneNodeShallow(a);
1318
+ return cloneNodeShallow(b);
1319
+ }
1320
+ function mergeLww(a, b) {
1321
+ if (compareDot(a.dot, b.dot) >= 0) return {
1322
+ kind: "lww",
1323
+ value: structuredClone(a.value),
1324
+ dot: { ...a.dot }
1325
+ };
1326
+ return {
1327
+ kind: "lww",
1328
+ value: structuredClone(b.value),
1329
+ dot: { ...b.dot }
1330
+ };
1331
+ }
1332
+ function mergeObj(a, b) {
1333
+ const entries = /* @__PURE__ */ new Map();
1334
+ const tombstone = /* @__PURE__ */ new Map();
1335
+ const allTombKeys = new Set([...a.tombstone.keys(), ...b.tombstone.keys()]);
1336
+ for (const key of allTombKeys) {
1337
+ const da = a.tombstone.get(key);
1338
+ const db = b.tombstone.get(key);
1339
+ if (da && db) tombstone.set(key, compareDot(da, db) >= 0 ? { ...da } : { ...db });
1340
+ else if (da) tombstone.set(key, { ...da });
1341
+ else tombstone.set(key, { ...db });
1342
+ }
1343
+ const allKeys = new Set([...a.entries.keys(), ...b.entries.keys()]);
1344
+ for (const key of allKeys) {
1345
+ const ea = a.entries.get(key);
1346
+ const eb = b.entries.get(key);
1347
+ let merged;
1348
+ if (ea && eb) merged = {
1349
+ node: mergeNode(ea.node, eb.node),
1350
+ dot: compareDot(ea.dot, eb.dot) >= 0 ? { ...ea.dot } : { ...eb.dot }
1351
+ };
1352
+ else if (ea) merged = {
1353
+ node: cloneNodeShallow(ea.node),
1354
+ dot: { ...ea.dot }
1355
+ };
1356
+ else merged = {
1357
+ node: cloneNodeShallow(eb.node),
1358
+ dot: { ...eb.dot }
1359
+ };
1360
+ const td = tombstone.get(key);
1361
+ if (td && compareDot(td, merged.dot) >= 0) continue;
1362
+ entries.set(key, merged);
1363
+ }
1364
+ return {
1365
+ kind: "obj",
1366
+ entries,
1367
+ tombstone
1368
+ };
1369
+ }
1370
+ function mergeSeq(a, b) {
1371
+ const elems = /* @__PURE__ */ new Map();
1372
+ const allIds = new Set([...a.elems.keys(), ...b.elems.keys()]);
1373
+ for (const id of allIds) {
1374
+ const ea = a.elems.get(id);
1375
+ const eb = b.elems.get(id);
1376
+ if (ea && eb) {
1377
+ const mergedValue = mergeNode(ea.value, eb.value);
1378
+ elems.set(id, {
1379
+ id,
1380
+ prev: ea.prev,
1381
+ tombstone: ea.tombstone || eb.tombstone,
1382
+ value: mergedValue,
1383
+ insDot: { ...ea.insDot }
1384
+ });
1385
+ } else if (ea) elems.set(id, cloneElem(ea));
1386
+ else elems.set(id, cloneElem(eb));
1387
+ }
1388
+ return {
1389
+ kind: "seq",
1390
+ elems
1391
+ };
1392
+ }
1393
+ function cloneElem(e) {
1394
+ return {
1395
+ id: e.id,
1396
+ prev: e.prev,
1397
+ tombstone: e.tombstone,
1398
+ value: cloneNodeShallow(e.value),
1399
+ insDot: { ...e.insDot }
1400
+ };
1401
+ }
1402
+ function cloneNodeShallow(node) {
1403
+ switch (node.kind) {
1404
+ case "lww": return {
1405
+ kind: "lww",
1406
+ value: structuredClone(node.value),
1407
+ dot: { ...node.dot }
1408
+ };
1409
+ case "obj": {
1410
+ const entries = /* @__PURE__ */ new Map();
1411
+ for (const [k, v] of node.entries) entries.set(k, {
1412
+ node: cloneNodeShallow(v.node),
1413
+ dot: { ...v.dot }
1414
+ });
1415
+ const tombstone = /* @__PURE__ */ new Map();
1416
+ for (const [k, d] of node.tombstone) tombstone.set(k, { ...d });
1417
+ return {
1418
+ kind: "obj",
1419
+ entries,
1420
+ tombstone
1421
+ };
1422
+ }
1423
+ case "seq": {
1424
+ const elems = /* @__PURE__ */ new Map();
1425
+ for (const [id, e] of node.elems) elems.set(id, cloneElem(e));
1426
+ return {
1427
+ kind: "seq",
1428
+ elems
1429
+ };
1430
+ }
1431
+ }
1432
+ }
1433
+
1434
+ //#endregion
1435
+ export { newReg as A, rgaPrevForInsertAtIndex as B, getAtJson as C, ROOT_KEY as D, stringifyJsonPointer as E, HEAD as F, cloneClock as G, dotToElemId as H, rgaDelete as I, observeDot as J, createClock as K, rgaIdAtIndex as L, objRemove as M, objSet as N, lwwSet as O, materialize as P, rgaInsertAfter as R, diffJsonPatch as S, parseJsonPointer as T, vvHasDot as U, compareDot as V, vvMerge as W, docFromJson as _, serializeDoc as a, jsonPatchToCrdtSafe as b, applyPatch as c, createState as d, toJson as f, crdtToJsonPatch as g, crdtToFullReplace as h, deserializeState as i, newSeq as j, newObj as k, applyPatchAsActor as l, cloneDoc as m, mergeState as n, serializeState as o, applyIntentsToCrdt as p, nextDotForActor as q, deserializeDoc as r, PatchError as s, mergeDoc as t, applyPatchInPlace as u, docFromJsonWithDot as v, jsonEquals as w, compileJsonPatchToIntent as x, jsonPatchToCrdt as y, rgaLinearizeIds as z };