json-patch-to-crdt 0.0.0 → 0.1.1

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.
@@ -195,6 +195,19 @@ const ROOT_KEY = "@@crdt/root";
195
195
 
196
196
  //#endregion
197
197
  //#region src/patch.ts
198
+ /** Structured compile error used to map patch validation failures to typed reasons. */
199
+ var PatchCompileError = class extends Error {
200
+ reason;
201
+ path;
202
+ opIndex;
203
+ constructor(reason, message, path, opIndex) {
204
+ super(message);
205
+ this.name = "PatchCompileError";
206
+ this.reason = reason;
207
+ this.path = path;
208
+ this.opIndex = opIndex;
209
+ }
210
+ };
198
211
  /**
199
212
  * Parse an RFC 6901 JSON Pointer into a path array, unescaping `~1` and `~0`.
200
213
  * @param ptr - A JSON Pointer string (e.g. `"/a/b"` or `""`).
@@ -203,13 +216,37 @@ const ROOT_KEY = "@@crdt/root";
203
216
  function parseJsonPointer(ptr) {
204
217
  if (ptr === "") return [];
205
218
  if (!ptr.startsWith("/")) throw new Error(`Invalid pointer: ${ptr}`);
206
- return ptr.slice(1).split("/").map((s) => s.replace(/~1/g, "/").replace(/~0/g, "~"));
219
+ return ptr.slice(1).split("/").map(unescapeJsonPointerToken);
207
220
  }
208
221
  /** Convert a path array back to an RFC 6901 JSON Pointer string. */
209
222
  function stringifyJsonPointer(path) {
210
223
  if (path.length === 0) return "";
211
224
  return `/${path.map(escapeJsonPointer).join("/")}`;
212
225
  }
226
+ function unescapeJsonPointerToken(token) {
227
+ let out = "";
228
+ for (let i = 0; i < token.length; i++) {
229
+ const ch = token[i];
230
+ if (ch !== "~") {
231
+ out += ch;
232
+ continue;
233
+ }
234
+ const esc = token[i + 1];
235
+ if (esc === "0") {
236
+ out += "~";
237
+ i += 1;
238
+ continue;
239
+ }
240
+ if (esc === "1") {
241
+ out += "/";
242
+ i += 1;
243
+ continue;
244
+ }
245
+ const sequence = esc === void 0 ? "~" : `~${esc}`;
246
+ throw new Error(`Invalid pointer escape sequence '${sequence}'`);
247
+ }
248
+ return out;
249
+ }
213
250
  /**
214
251
  * Navigate a JSON value by path and return the value at that location.
215
252
  * Throws if the path is invalid, out of bounds, or traverses a non-container.
@@ -217,8 +254,8 @@ function stringifyJsonPointer(path) {
217
254
  function getAtJson(base, path) {
218
255
  let cur = base;
219
256
  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}`);
257
+ if (!ARRAY_INDEX_TOKEN_PATTERN.test(seg)) throw new Error(`Expected array index, got ${seg}`);
258
+ const idx = Number(seg);
222
259
  if (idx < 0 || idx >= cur.length) throw new Error(`Index out of bounds at ${seg}`);
223
260
  cur = cur[idx];
224
261
  } else if (cur && typeof cur === "object") {
@@ -235,94 +272,15 @@ function getAtJson(base, path) {
235
272
  * @param patch - Array of JSON Patch operations.
236
273
  * @returns An array of `IntentOp` ready for `applyIntentsToCrdt`.
237
274
  */
238
- function compileJsonPatchToIntent(baseJson, patch) {
275
+ function compileJsonPatchToIntent(baseJson, patch, options = {}) {
276
+ const semantics = options.semantics ?? "sequential";
277
+ let workingBase = semantics === "sequential" ? structuredClone(baseJson) : baseJson;
239
278
  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
- }
279
+ for (let opIndex = 0; opIndex < patch.length; opIndex++) {
280
+ const op = patch[opIndex];
281
+ const compileBase = semantics === "sequential" ? workingBase : baseJson;
282
+ intents.push(...compileSingleOp(compileBase, op, opIndex, semantics));
283
+ if (semantics === "sequential") workingBase = applyPatchOpToJson(workingBase, op, opIndex);
326
284
  }
327
285
  return intents;
328
286
  }
@@ -464,6 +422,7 @@ function jsonEquals(a, b) {
464
422
  function isPlainObject(value) {
465
423
  return typeof value === "object" && value !== null && !Array.isArray(value);
466
424
  }
425
+ const ARRAY_INDEX_TOKEN_PATTERN = /^(0|[1-9][0-9]*)$/;
467
426
  function hasOwn(value, key) {
468
427
  return Object.prototype.hasOwnProperty.call(value, key);
469
428
  }
@@ -474,6 +433,223 @@ function pathValueAt(base, path) {
474
433
  function assertNever$1(_value, message) {
475
434
  throw new Error(message);
476
435
  }
436
+ function compileSingleOp(baseJson, op, opIndex, semantics) {
437
+ if (op.op === "test") return [{
438
+ t: "Test",
439
+ path: parsePointerOrThrow(op.path, op.path, opIndex),
440
+ value: op.value
441
+ }];
442
+ if (op.op === "copy" || op.op === "move") {
443
+ const fromPath = parsePointerOrThrow(op.from, op.from, opIndex);
444
+ const toPath = parsePointerOrThrow(op.path, op.path, opIndex);
445
+ if (op.op === "move" && isStrictDescendantPath(fromPath, toPath)) throw compileError("INVALID_MOVE", `cannot move a value into one of its descendants at ${op.path}`, op.path, opIndex);
446
+ const val = lookupValueOrThrow(baseJson, fromPath, op.from, opIndex);
447
+ if (op.op === "move" && isSamePath(fromPath, toPath)) return [];
448
+ if (op.op === "move" && semantics === "sequential") {
449
+ const removeOp = {
450
+ op: "remove",
451
+ path: op.from
452
+ };
453
+ const addOp = {
454
+ op: "add",
455
+ path: op.path,
456
+ value: val
457
+ };
458
+ const baseAfterRemove = applyPatchOpToJson(baseJson, removeOp, opIndex);
459
+ return [...compileSingleOp(baseJson, removeOp, opIndex, semantics), ...compileSingleOp(baseAfterRemove, addOp, opIndex, semantics)];
460
+ }
461
+ const out = compileSingleOp(baseJson, {
462
+ op: "add",
463
+ path: op.path,
464
+ value: val
465
+ }, opIndex, semantics);
466
+ if (op.op === "move") out.push(...compileSingleOp(baseJson, {
467
+ op: "remove",
468
+ path: op.from
469
+ }, opIndex, semantics));
470
+ return out;
471
+ }
472
+ const path = parsePointerOrThrow(op.path, op.path, opIndex);
473
+ if (path.length === 0) {
474
+ if (op.op === "replace" || op.op === "add") return [{
475
+ t: "ObjSet",
476
+ path: [],
477
+ key: ROOT_KEY,
478
+ value: op.value
479
+ }];
480
+ throw compileError("INVALID_TARGET", "remove at root path is not supported in RFC-compliant mode", op.path, opIndex);
481
+ }
482
+ const parent = path.slice(0, -1);
483
+ const token = path[path.length - 1];
484
+ const parentPath = stringifyJsonPointer(parent);
485
+ const parentValue = getParentValue(baseJson, parent, opIndex);
486
+ if (Array.isArray(parentValue)) {
487
+ const index = parseArrayIndexToken(token, op.op, parentValue.length, op.path, opIndex);
488
+ if (op.op === "add") return [{
489
+ t: "ArrInsert",
490
+ path: parent,
491
+ index,
492
+ value: op.value
493
+ }];
494
+ if (op.op === "remove") return [{
495
+ t: "ArrDelete",
496
+ path: parent,
497
+ index
498
+ }];
499
+ if (op.op === "replace") return [{
500
+ t: "ArrReplace",
501
+ path: parent,
502
+ index,
503
+ value: op.value
504
+ }];
505
+ return assertNever$1(op, "Unsupported op at array path");
506
+ }
507
+ if (!isPlainObject(parentValue)) throw compileError("INVALID_TARGET", `expected object or array parent at ${parentPath}`, parentPath, opIndex);
508
+ if ((op.op === "replace" || op.op === "remove") && !hasOwn(parentValue, token)) throw compileError("MISSING_TARGET", `missing key ${token} at ${parentPath}`, op.path, opIndex);
509
+ if (op.op === "add") return [{
510
+ t: "ObjSet",
511
+ path: parent,
512
+ key: token,
513
+ value: op.value,
514
+ mode: "add"
515
+ }];
516
+ if (op.op === "replace") return [{
517
+ t: "ObjSet",
518
+ path: parent,
519
+ key: token,
520
+ value: op.value,
521
+ mode: "replace"
522
+ }];
523
+ if (op.op === "remove") return [{
524
+ t: "ObjRemove",
525
+ path: parent,
526
+ key: token
527
+ }];
528
+ return assertNever$1(op, "Unsupported op");
529
+ }
530
+ function applyPatchOpToJson(baseJson, op, opIndex) {
531
+ let doc = structuredClone(baseJson);
532
+ if (op.op === "test") return doc;
533
+ if (op.op === "copy" || op.op === "move") {
534
+ const fromPath = parsePointerOrThrow(op.from, op.from, opIndex);
535
+ const value = structuredClone(lookupValueOrThrow(doc, fromPath, op.from, opIndex));
536
+ if (op.op === "move") doc = applyPatchOpToJson(doc, {
537
+ op: "remove",
538
+ path: op.from
539
+ }, opIndex);
540
+ return applyPatchOpToJson(doc, {
541
+ op: "add",
542
+ path: op.path,
543
+ value
544
+ }, opIndex);
545
+ }
546
+ const path = parsePointerOrThrow(op.path, op.path, opIndex);
547
+ if (path.length === 0) {
548
+ if (op.op === "add" || op.op === "replace") return structuredClone(op.value);
549
+ throw compileError("INVALID_TARGET", "remove at root path is not supported in RFC-compliant mode", op.path, opIndex);
550
+ }
551
+ const parentPath = path.slice(0, -1);
552
+ const token = path[path.length - 1];
553
+ let parent;
554
+ if (parentPath.length === 0) parent = doc;
555
+ else parent = lookupValueOrThrow(doc, parentPath, op.path, opIndex);
556
+ if (Array.isArray(parent)) {
557
+ const index = parseArrayIndexToken(token, op.op, parent.length, op.path, opIndex);
558
+ if (op.op === "add") {
559
+ const insertAt = index === Number.POSITIVE_INFINITY ? parent.length : index;
560
+ parent.splice(insertAt, 0, structuredClone(op.value));
561
+ return doc;
562
+ }
563
+ if (op.op === "replace") {
564
+ parent[index] = structuredClone(op.value);
565
+ return doc;
566
+ }
567
+ parent.splice(index, 1);
568
+ return doc;
569
+ }
570
+ if (!isPlainObject(parent)) throw compileError("INVALID_TARGET", `expected object or array parent at ${stringifyJsonPointer(parentPath)}`, op.path, opIndex);
571
+ if (op.op === "add" || op.op === "replace") {
572
+ parent[token] = structuredClone(op.value);
573
+ return doc;
574
+ }
575
+ delete parent[token];
576
+ return doc;
577
+ }
578
+ function parsePointerOrThrow(ptr, path, opIndex) {
579
+ try {
580
+ return parseJsonPointer(ptr);
581
+ } catch (error) {
582
+ throw compileError("INVALID_POINTER", error instanceof Error ? error.message : "invalid pointer", path, opIndex);
583
+ }
584
+ }
585
+ function lookupValueOrThrow(baseJson, path, pointer, opIndex) {
586
+ try {
587
+ return getAtJson(baseJson, path);
588
+ } catch (error) {
589
+ throw compileErrorFromLookup(error, pointer, opIndex);
590
+ }
591
+ }
592
+ function getParentValue(baseJson, parent, opIndex) {
593
+ if (parent.length === 0) return baseJson;
594
+ try {
595
+ return pathValueAt(baseJson, parent);
596
+ } catch (error) {
597
+ throw compileErrorFromLookup(error, stringifyJsonPointer(parent), opIndex);
598
+ }
599
+ }
600
+ function parseArrayIndexToken(token, op, arrLength, path, opIndex) {
601
+ if (token === "-") {
602
+ if (op !== "add") throw compileError("INVALID_POINTER", `'-' index is only valid for add at ${path}`, path, opIndex);
603
+ return Number.POSITIVE_INFINITY;
604
+ }
605
+ if (!ARRAY_INDEX_TOKEN_PATTERN.test(token)) throw compileError("INVALID_POINTER", `expected array index at ${path}`, path, opIndex);
606
+ const index = Number(token);
607
+ if (!Number.isSafeInteger(index)) throw compileError("OUT_OF_BOUNDS", `array index is too large at ${path}`, path, opIndex);
608
+ if (op === "add") {
609
+ if (index > arrLength) throw compileError("OUT_OF_BOUNDS", `index out of bounds at ${path}; expected 0..${arrLength}`, path, opIndex);
610
+ } else if (index >= arrLength) throw compileError("OUT_OF_BOUNDS", `index out of bounds at ${path}; expected 0..${Math.max(arrLength - 1, 0)}`, path, opIndex);
611
+ return index;
612
+ }
613
+ function compileErrorFromLookup(error, path, opIndex) {
614
+ const mapped = mapLookupErrorToPatchReason(error);
615
+ return compileError(mapped.reason, mapped.message, path, opIndex);
616
+ }
617
+ function mapLookupErrorToPatchReason(error) {
618
+ const message = error instanceof Error ? error.message : "invalid path";
619
+ if (message.includes("Expected array index")) return {
620
+ reason: "INVALID_POINTER",
621
+ message
622
+ };
623
+ if (message.includes("Index out of bounds")) return {
624
+ reason: "OUT_OF_BOUNDS",
625
+ message
626
+ };
627
+ if (message.includes("Missing key")) return {
628
+ reason: "MISSING_PARENT",
629
+ message
630
+ };
631
+ if (message.includes("Cannot traverse into non-container")) return {
632
+ reason: "INVALID_TARGET",
633
+ message
634
+ };
635
+ return {
636
+ reason: "INVALID_PATCH",
637
+ message
638
+ };
639
+ }
640
+ function compileError(reason, message, path, opIndex) {
641
+ return new PatchCompileError(reason, message, path, opIndex);
642
+ }
643
+ function isStrictDescendantPath(from, to) {
644
+ if (to.length <= from.length) return false;
645
+ for (let i = 0; i < from.length; i++) if (from[i] !== to[i]) return false;
646
+ return true;
647
+ }
648
+ function isSamePath(a, b) {
649
+ if (a.length !== b.length) return false;
650
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
651
+ return true;
652
+ }
477
653
 
478
654
  //#endregion
479
655
  //#region src/doc.ts
@@ -674,13 +850,17 @@ function applyTest(base, head, it, evalTestAgainst) {
674
850
  return {
675
851
  ok: false,
676
852
  code: 409,
677
- message: `test path missing at /${it.path.join("/")}`
853
+ reason: "MISSING_TARGET",
854
+ message: `test path missing at /${it.path.join("/")}`,
855
+ path: `/${it.path.join("/")}`
678
856
  };
679
857
  }
680
858
  if (!jsonEquals(got, it.value)) return {
681
859
  ok: false,
682
860
  code: 409,
683
- message: `test failed at /${it.path.join("/")}`
861
+ reason: "TEST_FAILED",
862
+ message: `test failed at /${it.path.join("/")}`,
863
+ path: `/${it.path.join("/")}`
684
864
  };
685
865
  return null;
686
866
  }
@@ -693,12 +873,16 @@ function applyObjSet(head, it, newDot) {
693
873
  if (!parentRes.ok) return {
694
874
  ok: false,
695
875
  code: 409,
696
- message: parentRes.message
876
+ reason: "MISSING_PARENT",
877
+ message: parentRes.message,
878
+ path: `/${it.path.join("/")}`
697
879
  };
698
880
  if (it.mode === "replace" && !parentRes.obj.entries.has(it.key)) return {
699
881
  ok: false,
700
882
  code: 409,
701
- message: `no value at /${[...it.path, it.key].join("/")}`
883
+ reason: "MISSING_TARGET",
884
+ message: `no value at /${[...it.path, it.key].join("/")}`,
885
+ path: `/${[...it.path, it.key].join("/")}`
702
886
  };
703
887
  const d = newDot();
704
888
  const parentObj = parentRes.obj;
@@ -710,12 +894,16 @@ function applyObjRemove(head, it, newDot) {
710
894
  if (!parentRes.ok) return {
711
895
  ok: false,
712
896
  code: 409,
713
- message: parentRes.message
897
+ reason: "MISSING_PARENT",
898
+ message: parentRes.message,
899
+ path: `/${it.path.join("/")}`
714
900
  };
715
901
  if (!parentRes.obj.entries.has(it.key)) return {
716
902
  ok: false,
717
903
  code: 409,
718
- message: `no value at /${[...it.path, it.key].join("/")}`
904
+ reason: "MISSING_TARGET",
905
+ message: `no value at /${[...it.path, it.key].join("/")}`,
906
+ path: `/${[...it.path, it.key].join("/")}`
719
907
  };
720
908
  const d = newDot();
721
909
  const parentObj = parentRes.obj;
@@ -735,7 +923,9 @@ function applyArrInsert(base, head, it, newDot, bumpCounterAbove) {
735
923
  return {
736
924
  ok: false,
737
925
  code: 409,
738
- message: `base array missing at /${it.path.join("/")}`
926
+ reason: "MISSING_PARENT",
927
+ message: `base array missing at /${it.path.join("/")}`,
928
+ path: `/${it.path.join("/")}`
739
929
  };
740
930
  }
741
931
  const headSeq = ensureSeqAtPath(head, it.path, newDot());
@@ -744,7 +934,9 @@ function applyArrInsert(base, head, it, newDot, bumpCounterAbove) {
744
934
  if (idx < 0 || idx > baseLen) return {
745
935
  ok: false,
746
936
  code: 409,
747
- message: `index out of bounds at /${it.path.join("/")}/${it.index}`
937
+ reason: "OUT_OF_BOUNDS",
938
+ message: `index out of bounds at /${it.path.join("/")}/${it.index}`,
939
+ path: `/${it.path.join("/")}/${it.index}`
748
940
  };
749
941
  const prev = idx === 0 ? HEAD : rgaIdAtIndex(baseSeq, idx - 1) ?? HEAD;
750
942
  const d = nextInsertDotForPrev(headSeq, prev, newDot, bumpCounterAbove);
@@ -768,14 +960,18 @@ function applyArrDelete(base, head, it, newDot) {
768
960
  if (!baseSeq) return {
769
961
  ok: false,
770
962
  code: 409,
771
- message: `base array missing at /${it.path.join("/")}`
963
+ reason: "MISSING_PARENT",
964
+ message: `base array missing at /${it.path.join("/")}`,
965
+ path: `/${it.path.join("/")}`
772
966
  };
773
967
  const headSeq = ensureSeqAtPath(head, it.path, d);
774
968
  const baseId = rgaIdAtIndex(baseSeq, it.index);
775
969
  if (!baseId) return {
776
970
  ok: false,
777
971
  code: 409,
778
- message: `no base element at index ${it.index}`
972
+ reason: "MISSING_TARGET",
973
+ message: `no base element at index ${it.index}`,
974
+ path: `/${it.path.join("/")}/${it.index}`
779
975
  };
780
976
  rgaDelete(headSeq, baseId);
781
977
  return null;
@@ -786,20 +982,26 @@ function applyArrReplace(base, head, it, newDot) {
786
982
  if (!baseSeq) return {
787
983
  ok: false,
788
984
  code: 409,
789
- message: `base array missing at /${it.path.join("/")}`
985
+ reason: "MISSING_PARENT",
986
+ message: `base array missing at /${it.path.join("/")}`,
987
+ path: `/${it.path.join("/")}`
790
988
  };
791
989
  const headSeq = ensureSeqAtPath(head, it.path, d);
792
990
  const baseId = rgaIdAtIndex(baseSeq, it.index);
793
991
  if (!baseId) return {
794
992
  ok: false,
795
993
  code: 409,
796
- message: `no base element at index ${it.index}`
994
+ reason: "MISSING_TARGET",
995
+ message: `no base element at index ${it.index}`,
996
+ path: `/${it.path.join("/")}/${it.index}`
797
997
  };
798
998
  const e = headSeq.elems.get(baseId);
799
999
  if (!e || e.tombstone) return {
800
1000
  ok: false,
801
1001
  code: 409,
802
- message: `element already deleted at index ${it.index}`
1002
+ reason: "MISSING_TARGET",
1003
+ message: `element already deleted at index ${it.index}`,
1004
+ path: `/${it.path.join("/")}/${it.index}`
803
1005
  };
804
1006
  e.value = nodeFromJson(it.value, newDot);
805
1007
  return null;
@@ -843,34 +1045,39 @@ function applyIntentsToCrdt(base, head, intents, newDot, evalTestAgainst = "head
843
1045
  }
844
1046
  return { ok: true };
845
1047
  }
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);
1048
+ function jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove) {
1049
+ if (isJsonPatchToCrdtOptions(baseOrOptions)) return jsonPatchToCrdtInternal(baseOrOptions);
1050
+ if (!head || !patch || !newDot) return {
1051
+ ok: false,
1052
+ code: 409,
1053
+ reason: "INVALID_PATCH",
1054
+ message: "invalid jsonPatchToCrdt call signature"
1055
+ };
1056
+ return jsonPatchToCrdtInternal({
1057
+ base: baseOrOptions,
1058
+ head,
1059
+ patch,
1060
+ newDot,
1061
+ evalTestAgainst,
1062
+ bumpCounterAbove
1063
+ });
858
1064
  }
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) {
1065
+ function jsonPatchToCrdtSafe(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove) {
864
1066
  try {
865
- return jsonPatchToCrdt(base, head, patch, newDot, evalTestAgainst, bumpCounterAbove);
866
- } catch (error) {
867
- return {
1067
+ if (isJsonPatchToCrdtOptions(baseOrOptions)) return jsonPatchToCrdt(baseOrOptions);
1068
+ if (!head || !patch || !newDot) return {
868
1069
  ok: false,
869
1070
  code: 409,
870
- message: error instanceof Error ? error.message : "failed to compile patch"
1071
+ reason: "INVALID_PATCH",
1072
+ message: "invalid jsonPatchToCrdtSafe call signature"
871
1073
  };
1074
+ return jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst, bumpCounterAbove);
1075
+ } catch (error) {
1076
+ return toApplyError$1(error);
872
1077
  }
873
1078
  }
1079
+ /** Alias for codebases that prefer `try*` naming for non-throwing APIs. */
1080
+ const tryJsonPatchToCrdt = jsonPatchToCrdtSafe;
874
1081
  /**
875
1082
  * Generate a JSON Patch delta between two CRDT documents.
876
1083
  * @param base - The base document snapshot.
@@ -892,19 +1099,130 @@ function crdtToFullReplace(doc) {
892
1099
  value: materialize(doc.root)
893
1100
  }];
894
1101
  }
1102
+ function jsonPatchToCrdtInternal(options) {
1103
+ const evalTestAgainst = options.evalTestAgainst ?? "head";
1104
+ if ((options.semantics ?? "sequential") === "base") {
1105
+ const baseJson = materialize(options.base.root);
1106
+ let intents;
1107
+ try {
1108
+ intents = compileJsonPatchToIntent(baseJson, options.patch, { semantics: "base" });
1109
+ } catch (error) {
1110
+ return toApplyError$1(error);
1111
+ }
1112
+ return applyIntentsToCrdt(options.base, options.head, intents, options.newDot, evalTestAgainst, options.bumpCounterAbove);
1113
+ }
1114
+ let shadowBase = cloneDoc(evalTestAgainst === "base" ? options.base : options.head);
1115
+ let shadowCtr = 0;
1116
+ const shadowDot = () => ({
1117
+ actor: "__shadow__",
1118
+ ctr: ++shadowCtr
1119
+ });
1120
+ const shadowBump = (ctr) => {
1121
+ if (shadowCtr < ctr) shadowCtr = ctr;
1122
+ };
1123
+ const applySequentialOp = (op, opIndex) => {
1124
+ const baseJson = materialize(shadowBase.root);
1125
+ let intents;
1126
+ try {
1127
+ intents = compileJsonPatchToIntent(baseJson, [op], { semantics: "sequential" });
1128
+ } catch (error) {
1129
+ return withOpIndex(toApplyError$1(error), opIndex);
1130
+ }
1131
+ const headStep = applyIntentsToCrdt(shadowBase, options.head, intents, options.newDot, evalTestAgainst, options.bumpCounterAbove);
1132
+ if (!headStep.ok) return withOpIndex(headStep, opIndex);
1133
+ if (evalTestAgainst === "base") {
1134
+ const shadowStep = applyIntentsToCrdt(shadowBase, shadowBase, intents, shadowDot, "base", shadowBump);
1135
+ if (!shadowStep.ok) return withOpIndex(shadowStep, opIndex);
1136
+ } else shadowBase = cloneDoc(options.head);
1137
+ return { ok: true };
1138
+ };
1139
+ for (let opIndex = 0; opIndex < options.patch.length; opIndex++) {
1140
+ const op = options.patch[opIndex];
1141
+ if (op.op === "move") {
1142
+ const baseJson = materialize(shadowBase.root);
1143
+ let fromValue;
1144
+ try {
1145
+ fromValue = structuredClone(getAtJson(baseJson, parseJsonPointer(op.from)));
1146
+ } catch {
1147
+ try {
1148
+ compileJsonPatchToIntent(baseJson, [{
1149
+ op: "remove",
1150
+ path: op.from
1151
+ }], { semantics: "sequential" });
1152
+ } catch (error) {
1153
+ return withOpIndex(toApplyError$1(error), opIndex);
1154
+ }
1155
+ return withOpIndex(toApplyError$1(/* @__PURE__ */ new Error(`failed to resolve move source at ${op.from}`)), opIndex);
1156
+ }
1157
+ if (op.from === op.path) continue;
1158
+ const removeStep = applySequentialOp({
1159
+ op: "remove",
1160
+ path: op.from
1161
+ }, opIndex);
1162
+ if (!removeStep.ok) return removeStep;
1163
+ const addStep = applySequentialOp({
1164
+ op: "add",
1165
+ path: op.path,
1166
+ value: fromValue
1167
+ }, opIndex);
1168
+ if (!addStep.ok) return addStep;
1169
+ continue;
1170
+ }
1171
+ const step = applySequentialOp(op, opIndex);
1172
+ if (!step.ok) return step;
1173
+ }
1174
+ return { ok: true };
1175
+ }
1176
+ function withOpIndex(error, opIndex) {
1177
+ if (error.opIndex !== void 0) return error;
1178
+ return {
1179
+ ...error,
1180
+ opIndex
1181
+ };
1182
+ }
1183
+ function isJsonPatchToCrdtOptions(value) {
1184
+ return typeof value === "object" && value !== null && "base" in value && "head" in value && "patch" in value && "newDot" in value;
1185
+ }
1186
+ function toApplyError$1(error) {
1187
+ if (error instanceof PatchCompileError) return {
1188
+ ok: false,
1189
+ code: 409,
1190
+ reason: error.reason,
1191
+ message: error.message,
1192
+ path: error.path,
1193
+ opIndex: error.opIndex
1194
+ };
1195
+ return {
1196
+ ok: false,
1197
+ code: 409,
1198
+ reason: "INVALID_PATCH",
1199
+ message: error instanceof Error ? error.message : "failed to compile/apply patch"
1200
+ };
1201
+ }
895
1202
  function assertNever(_value, message) {
896
1203
  throw new Error(message);
897
1204
  }
898
1205
 
899
1206
  //#endregion
900
1207
  //#region src/state.ts
901
- /** Error thrown when a JSON Patch cannot be applied. Includes a numeric `.code` (409 for conflicts). */
1208
+ /** Error thrown when a JSON Patch cannot be applied. Includes structured conflict metadata. */
902
1209
  var PatchError = class extends Error {
903
1210
  code;
904
- constructor(message, code = 409) {
905
- super(message);
1211
+ reason;
1212
+ path;
1213
+ opIndex;
1214
+ constructor(errorOrMessage, code = 409, reason = "INVALID_PATCH") {
1215
+ super(typeof errorOrMessage === "string" ? errorOrMessage : errorOrMessage.message);
906
1216
  this.name = "PatchError";
907
- this.code = code;
1217
+ if (typeof errorOrMessage === "string") {
1218
+ this.code = code;
1219
+ this.reason = reason;
1220
+ return;
1221
+ }
1222
+ this.code = errorOrMessage.code;
1223
+ this.reason = errorOrMessage.reason;
1224
+ this.path = errorOrMessage.path;
1225
+ this.opIndex = errorOrMessage.opIndex;
908
1226
  }
909
1227
  };
910
1228
  /**
@@ -921,6 +1239,18 @@ function createState(initial, options) {
921
1239
  };
922
1240
  }
923
1241
  /**
1242
+ * Fork a replica from a shared origin state while assigning a new local actor ID.
1243
+ * The forked state has an independent document clone and clock.
1244
+ * By default this rejects actor reuse to prevent duplicate-dot collisions across peers.
1245
+ */
1246
+ function forkState(origin, actor, options = {}) {
1247
+ if (actor === origin.clock.actor && !options.allowActorReuse) throw new Error(`forkState actor must be unique; refusing to reuse origin actor '${actor}'`);
1248
+ return {
1249
+ doc: cloneDoc(origin.doc),
1250
+ clock: createClock(actor, origin.clock.ctr)
1251
+ };
1252
+ }
1253
+ /**
924
1254
  * Materialize a CRDT document or state back to a plain JSON value.
925
1255
  * @param target - A `Doc` or `CrdtState` to materialize.
926
1256
  * @returns The JSON representation of the current document.
@@ -934,34 +1264,69 @@ function toJson(target) {
934
1264
  * Throws `PatchError` on conflict (e.g. out-of-bounds index, failed test op).
935
1265
  * @param state - The current CRDT state.
936
1266
  * @param patch - Array of RFC 6902 JSON Patch operations.
937
- * @param options - Optional base document and test evaluation mode.
1267
+ * @param options - Optional base state snapshot and patch semantics.
938
1268
  * @returns A new `CrdtState` with the patch applied.
939
1269
  */
940
1270
  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;
1271
+ const result = tryApplyPatch(state, patch, options);
1272
+ if (!result.ok) throw new PatchError(result.error);
1273
+ return result.state;
948
1274
  }
949
1275
  /**
950
1276
  * Apply a JSON Patch to the state in place, mutating the existing state.
951
1277
  * Throws `PatchError` on conflict.
952
1278
  * @param state - The CRDT state to mutate.
953
1279
  * @param patch - Array of RFC 6902 JSON Patch operations.
954
- * @param options - Optional base document and test evaluation mode.
1280
+ * @param options - Optional base state snapshot, patch semantics, and atomicity.
955
1281
  */
956
1282
  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;
1283
+ const result = tryApplyPatchInPlace(state, patch, options);
1284
+ if (!result.ok) throw new PatchError(result.error);
1285
+ }
1286
+ /** Non-throwing immutable patch application variant. */
1287
+ function tryApplyPatch(state, patch, options = {}) {
1288
+ const nextState = {
1289
+ doc: cloneDoc(state.doc),
1290
+ clock: cloneClock(state.clock)
1291
+ };
1292
+ const result = applyPatchInternal(nextState, patch, options);
1293
+ if (!result.ok) return {
1294
+ ok: false,
1295
+ error: result
1296
+ };
1297
+ return {
1298
+ ok: true,
1299
+ state: nextState
1300
+ };
1301
+ }
1302
+ /** Non-throwing in-place patch application variant. */
1303
+ function tryApplyPatchInPlace(state, patch, options = {}) {
1304
+ const { atomic = true, ...applyOptions } = options;
1305
+ if (atomic) {
1306
+ const next = tryApplyPatch(state, patch, applyOptions);
1307
+ if (!next.ok) return next;
1308
+ state.doc = next.state.doc;
1309
+ state.clock = next.state.clock;
1310
+ return { ok: true };
962
1311
  }
963
- const result = applyPatchInternal(state, patch, options);
964
- if (!result.ok) throw new PatchError(result.message, result.code);
1312
+ const result = applyPatchInternal(state, patch, applyOptions);
1313
+ if (!result.ok) return {
1314
+ ok: false,
1315
+ error: result
1316
+ };
1317
+ return { ok: true };
1318
+ }
1319
+ /**
1320
+ * Validate whether a patch is applicable against a JSON base value under the chosen options.
1321
+ * Does not mutate caller-provided values.
1322
+ */
1323
+ function validateJsonPatch(base, patch, options = {}) {
1324
+ const result = tryApplyPatch(createState(base, { actor: "__validate__" }), patch, options);
1325
+ if (!result.ok) return {
1326
+ ok: false,
1327
+ error: result.error
1328
+ };
1329
+ return { ok: true };
965
1330
  }
966
1331
  /**
967
1332
  * Apply a JSON Patch as a specific actor while maintaining an external version vector.
@@ -972,7 +1337,7 @@ function applyPatchAsActor(doc, vv, actor, patch, options = {}) {
972
1337
  const state = applyPatch({
973
1338
  doc,
974
1339
  clock: createClock(actor, Math.max(vv[actor] ?? 0, observedCtr))
975
- }, patch, options);
1340
+ }, patch, toApplyPatchOptionsForActor(options));
976
1341
  return {
977
1342
  state,
978
1343
  vv: {
@@ -981,34 +1346,46 @@ function applyPatchAsActor(doc, vv, actor, patch, options = {}) {
981
1346
  }
982
1347
  };
983
1348
  }
1349
+ function toApplyPatchOptionsForActor(options) {
1350
+ return {
1351
+ semantics: options.semantics,
1352
+ testAgainst: options.testAgainst,
1353
+ base: options.base ? {
1354
+ doc: options.base,
1355
+ clock: createClock("__base__", 0)
1356
+ } : void 0
1357
+ };
1358
+ }
984
1359
  function applyPatchInternal(state, patch, options) {
985
- if ((options.semantics ?? "base") === "sequential") {
1360
+ if ((options.semantics ?? "sequential") === "sequential") {
986
1361
  const explicitBaseState = options.base ? {
987
- doc: cloneDoc(options.base),
1362
+ doc: cloneDoc(options.base.doc),
988
1363
  clock: createClock("__base__", 0)
989
1364
  } : null;
990
- for (const op of patch) {
991
- const step = applyPatchOpSequential(state, op, options, explicitBaseState ? explicitBaseState.doc : cloneDoc(state.doc));
1365
+ for (const [opIndex, op] of patch.entries()) {
1366
+ const step = applyPatchOpSequential(state, op, options, explicitBaseState ? explicitBaseState.doc : cloneDoc(state.doc), opIndex);
992
1367
  if (!step.ok) return step;
993
- if (explicitBaseState) {
1368
+ if (explicitBaseState && op.op !== "test") {
994
1369
  const baseStep = applyPatchInternal(explicitBaseState, [op], {
995
1370
  semantics: "sequential",
996
- testAgainst: options.testAgainst
1371
+ testAgainst: "base"
997
1372
  });
998
1373
  if (!baseStep.ok) return baseStep;
999
1374
  }
1000
1375
  }
1001
1376
  return { ok: true };
1002
1377
  }
1003
- const baseDoc = options.base ? options.base : cloneDoc(state.doc);
1004
- const compiled = compileIntents(materialize(baseDoc.root), patch);
1378
+ const baseDoc = options.base ? options.base.doc : cloneDoc(state.doc);
1379
+ const compiled = compileIntents(materialize(baseDoc.root), patch, "base");
1005
1380
  if (!compiled.ok) return compiled;
1006
1381
  return applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr));
1007
1382
  }
1008
- function applyPatchOpSequential(state, op, options, baseDoc) {
1383
+ function applyPatchOpSequential(state, op, options, baseDoc, opIndex) {
1009
1384
  const baseJson = materialize(baseDoc.root);
1010
1385
  if (op.op === "move") {
1011
- const fromValue = getAtJson(baseJson, parseJsonPointer(op.from));
1386
+ const fromResolved = resolveValueAtPointer(baseJson, op.from, opIndex);
1387
+ if (!fromResolved.ok) return fromResolved;
1388
+ const fromValue = fromResolved.value;
1012
1389
  const removeRes = applySinglePatchOp(state, baseDoc, {
1013
1390
  op: "remove",
1014
1391
  path: op.from
@@ -1021,7 +1398,9 @@ function applyPatchOpSequential(state, op, options, baseDoc) {
1021
1398
  }, options);
1022
1399
  }
1023
1400
  if (op.op === "copy") {
1024
- const fromValue = getAtJson(baseJson, parseJsonPointer(op.from));
1401
+ const fromResolved = resolveValueAtPointer(baseJson, op.from, opIndex);
1402
+ if (!fromResolved.ok) return fromResolved;
1403
+ const fromValue = fromResolved.value;
1025
1404
  return applySinglePatchOp(state, baseDoc, {
1026
1405
  op: "add",
1027
1406
  path: op.path,
@@ -1030,26 +1409,38 @@ function applyPatchOpSequential(state, op, options, baseDoc) {
1030
1409
  }
1031
1410
  return applySinglePatchOp(state, baseDoc, op, options);
1032
1411
  }
1412
+ function resolveValueAtPointer(baseJson, pointer, opIndex) {
1413
+ let path;
1414
+ try {
1415
+ path = parseJsonPointer(pointer);
1416
+ } catch (error) {
1417
+ return toPointerParseApplyError(error, pointer, opIndex);
1418
+ }
1419
+ try {
1420
+ return {
1421
+ ok: true,
1422
+ value: getAtJson(baseJson, path)
1423
+ };
1424
+ } catch (error) {
1425
+ return toPointerLookupApplyError(error, pointer, opIndex);
1426
+ }
1427
+ }
1033
1428
  function applySinglePatchOp(state, baseDoc, op, options) {
1034
- const compiled = compileIntents(materialize(baseDoc.root), [op]);
1429
+ const compiled = compileIntents(materialize(baseDoc.root), [op], "sequential");
1035
1430
  if (!compiled.ok) return compiled;
1036
1431
  return applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr));
1037
1432
  }
1038
1433
  function bumpClockCounter(state, ctr) {
1039
1434
  if (state.clock.ctr < ctr) state.clock.ctr = ctr;
1040
1435
  }
1041
- function compileIntents(baseJson, patch) {
1436
+ function compileIntents(baseJson, patch, semantics = "sequential") {
1042
1437
  try {
1043
1438
  return {
1044
1439
  ok: true,
1045
- intents: compileJsonPatchToIntent(baseJson, patch)
1440
+ intents: compileJsonPatchToIntent(baseJson, patch, { semantics })
1046
1441
  };
1047
1442
  } catch (error) {
1048
- return {
1049
- ok: false,
1050
- code: 409,
1051
- message: error instanceof Error ? error.message : "failed to compile patch"
1052
- };
1443
+ return toApplyError(error);
1053
1444
  }
1054
1445
  }
1055
1446
  function maxCtrInNodeForActor$1(node, actor) {
@@ -1076,6 +1467,43 @@ function maxCtrInNodeForActor$1(node, actor) {
1076
1467
  }
1077
1468
  }
1078
1469
  }
1470
+ function toApplyError(error) {
1471
+ if (error instanceof PatchCompileError) return {
1472
+ ok: false,
1473
+ code: 409,
1474
+ reason: error.reason,
1475
+ message: error.message,
1476
+ path: error.path,
1477
+ opIndex: error.opIndex
1478
+ };
1479
+ return {
1480
+ ok: false,
1481
+ code: 409,
1482
+ reason: "INVALID_PATCH",
1483
+ message: error instanceof Error ? error.message : "failed to compile patch"
1484
+ };
1485
+ }
1486
+ function toPointerParseApplyError(error, pointer, opIndex) {
1487
+ return {
1488
+ ok: false,
1489
+ code: 409,
1490
+ reason: "INVALID_POINTER",
1491
+ message: error instanceof Error ? error.message : "invalid pointer",
1492
+ path: pointer,
1493
+ opIndex
1494
+ };
1495
+ }
1496
+ function toPointerLookupApplyError(error, pointer, opIndex) {
1497
+ const mapped = mapLookupErrorToPatchReason(error);
1498
+ return {
1499
+ ok: false,
1500
+ code: 409,
1501
+ reason: mapped.reason,
1502
+ message: mapped.message,
1503
+ path: pointer,
1504
+ opIndex
1505
+ };
1506
+ }
1079
1507
 
1080
1508
  //#endregion
1081
1509
  //#region src/serialize.ts
@@ -1198,6 +1626,19 @@ function deserializeNode(node) {
1198
1626
 
1199
1627
  //#endregion
1200
1628
  //#region src/merge.ts
1629
+ /** Error thrown by throwing merge helpers (`mergeDoc` / `mergeState`). */
1630
+ var MergeError = class extends Error {
1631
+ code;
1632
+ reason;
1633
+ path;
1634
+ constructor(error) {
1635
+ super(error.message);
1636
+ this.name = "MergeError";
1637
+ this.code = error.code;
1638
+ this.reason = "LINEAGE_MISMATCH";
1639
+ this.path = error.path;
1640
+ }
1641
+ };
1201
1642
  /**
1202
1643
  * Merge two CRDT documents from different peers into one.
1203
1644
  * By default this requires shared array lineage for non-empty sequences.
@@ -1212,9 +1653,27 @@ function deserializeNode(node) {
1212
1653
  * - **Kind mismatch**: the node with the higher "representative dot" wins and replaces the other entirely.
1213
1654
  */
1214
1655
  function mergeDoc(a, b, options = {}) {
1656
+ const result = tryMergeDoc(a, b, options);
1657
+ if (!result.ok) throw new MergeError(result.error);
1658
+ return result.doc;
1659
+ }
1660
+ /** Non-throwing `mergeDoc` variant with structured conflict details. */
1661
+ function tryMergeDoc(a, b, options = {}) {
1215
1662
  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) };
1663
+ if (mismatchPath) return {
1664
+ ok: false,
1665
+ error: {
1666
+ ok: false,
1667
+ code: 409,
1668
+ reason: "LINEAGE_MISMATCH",
1669
+ message: `merge requires shared array origin at ${mismatchPath}`,
1670
+ path: mismatchPath
1671
+ }
1672
+ };
1673
+ return {
1674
+ ok: true,
1675
+ doc: { root: mergeNode(a.root, b.root) }
1676
+ };
1218
1677
  }
1219
1678
  /**
1220
1679
  * Merge two CRDT states.
@@ -1228,11 +1687,22 @@ function mergeDoc(a, b, options = {}) {
1228
1687
  * that actor across both input clocks and the merged document dots.
1229
1688
  */
1230
1689
  function mergeState(a, b, options = {}) {
1231
- const doc = mergeDoc(a.doc, b.doc, { requireSharedOrigin: options.requireSharedOrigin });
1690
+ const result = tryMergeState(a, b, options);
1691
+ if (!result.ok) throw new MergeError(result.error);
1692
+ return result.state;
1693
+ }
1694
+ /** Non-throwing `mergeState` variant with structured conflict details. */
1695
+ function tryMergeState(a, b, options = {}) {
1696
+ const mergedDoc = tryMergeDoc(a.doc, b.doc, { requireSharedOrigin: options.requireSharedOrigin });
1697
+ if (!mergedDoc.ok) return mergedDoc;
1698
+ const doc = mergedDoc.doc;
1232
1699
  const actor = options.actor ?? a.clock.actor;
1233
1700
  return {
1234
- doc,
1235
- clock: createClock(actor, maxObservedCtrForActor(doc, actor, a, b))
1701
+ ok: true,
1702
+ state: {
1703
+ doc,
1704
+ clock: createClock(actor, maxObservedCtrForActor(doc, actor, a, b))
1705
+ }
1236
1706
  };
1237
1707
  }
1238
1708
  function findSeqLineageMismatch(a, b, path) {
@@ -1439,6 +1909,18 @@ Object.defineProperty(exports, 'HEAD', {
1439
1909
  return HEAD;
1440
1910
  }
1441
1911
  });
1912
+ Object.defineProperty(exports, 'MergeError', {
1913
+ enumerable: true,
1914
+ get: function () {
1915
+ return MergeError;
1916
+ }
1917
+ });
1918
+ Object.defineProperty(exports, 'PatchCompileError', {
1919
+ enumerable: true,
1920
+ get: function () {
1921
+ return PatchCompileError;
1922
+ }
1923
+ });
1442
1924
  Object.defineProperty(exports, 'PatchError', {
1443
1925
  enumerable: true,
1444
1926
  get: function () {
@@ -1559,6 +2041,12 @@ Object.defineProperty(exports, 'dotToElemId', {
1559
2041
  return dotToElemId;
1560
2042
  }
1561
2043
  });
2044
+ Object.defineProperty(exports, 'forkState', {
2045
+ enumerable: true,
2046
+ get: function () {
2047
+ return forkState;
2048
+ }
2049
+ });
1562
2050
  Object.defineProperty(exports, 'getAtJson', {
1563
2051
  enumerable: true,
1564
2052
  get: function () {
@@ -1709,6 +2197,42 @@ Object.defineProperty(exports, 'toJson', {
1709
2197
  return toJson;
1710
2198
  }
1711
2199
  });
2200
+ Object.defineProperty(exports, 'tryApplyPatch', {
2201
+ enumerable: true,
2202
+ get: function () {
2203
+ return tryApplyPatch;
2204
+ }
2205
+ });
2206
+ Object.defineProperty(exports, 'tryApplyPatchInPlace', {
2207
+ enumerable: true,
2208
+ get: function () {
2209
+ return tryApplyPatchInPlace;
2210
+ }
2211
+ });
2212
+ Object.defineProperty(exports, 'tryJsonPatchToCrdt', {
2213
+ enumerable: true,
2214
+ get: function () {
2215
+ return tryJsonPatchToCrdt;
2216
+ }
2217
+ });
2218
+ Object.defineProperty(exports, 'tryMergeDoc', {
2219
+ enumerable: true,
2220
+ get: function () {
2221
+ return tryMergeDoc;
2222
+ }
2223
+ });
2224
+ Object.defineProperty(exports, 'tryMergeState', {
2225
+ enumerable: true,
2226
+ get: function () {
2227
+ return tryMergeState;
2228
+ }
2229
+ });
2230
+ Object.defineProperty(exports, 'validateJsonPatch', {
2231
+ enumerable: true,
2232
+ get: function () {
2233
+ return validateJsonPatch;
2234
+ }
2235
+ });
1712
2236
  Object.defineProperty(exports, 'vvHasDot', {
1713
2237
  enumerable: true,
1714
2238
  get: function () {