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