json-patch-to-crdt 0.0.0 → 0.1.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.
- package/README.md +66 -78
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +8 -23
- package/dist/index.mjs +2 -2
- package/dist/internals.d.mts +80 -2
- package/dist/internals.d.ts +80 -2
- package/dist/internals.js +10 -1
- package/dist/internals.mjs +2 -2
- package/dist/{merge-DikOFBWc.mjs → merge-BqROEw61.mjs} +491 -162
- package/dist/{merge-BpAUNaPe.d.mts → merge-BrNGGkXj.d.mts} +124 -98
- package/dist/{merge-B1BFMhJJ.js → merge-CtJfKEt1.js} +544 -161
- package/dist/{merge-QmPXxE6_.d.ts → merge-DW1-p9Hj.d.ts} +124 -98
- package/package.json +4 -1
|
@@ -194,6 +194,19 @@ const ROOT_KEY = "@@crdt/root";
|
|
|
194
194
|
|
|
195
195
|
//#endregion
|
|
196
196
|
//#region src/patch.ts
|
|
197
|
+
/** Structured compile error used to map patch validation failures to typed reasons. */
|
|
198
|
+
var PatchCompileError = class extends Error {
|
|
199
|
+
reason;
|
|
200
|
+
path;
|
|
201
|
+
opIndex;
|
|
202
|
+
constructor(reason, message, path, opIndex) {
|
|
203
|
+
super(message);
|
|
204
|
+
this.name = "PatchCompileError";
|
|
205
|
+
this.reason = reason;
|
|
206
|
+
this.path = path;
|
|
207
|
+
this.opIndex = opIndex;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
197
210
|
/**
|
|
198
211
|
* Parse an RFC 6901 JSON Pointer into a path array, unescaping `~1` and `~0`.
|
|
199
212
|
* @param ptr - A JSON Pointer string (e.g. `"/a/b"` or `""`).
|
|
@@ -234,94 +247,15 @@ function getAtJson(base, path) {
|
|
|
234
247
|
* @param patch - Array of JSON Patch operations.
|
|
235
248
|
* @returns An array of `IntentOp` ready for `applyIntentsToCrdt`.
|
|
236
249
|
*/
|
|
237
|
-
function compileJsonPatchToIntent(baseJson, patch) {
|
|
250
|
+
function compileJsonPatchToIntent(baseJson, patch, options = {}) {
|
|
251
|
+
const semantics = options.semantics ?? "sequential";
|
|
252
|
+
let workingBase = semantics === "sequential" ? structuredClone(baseJson) : baseJson;
|
|
238
253
|
const intents = [];
|
|
239
|
-
for (
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
value: op.value
|
|
245
|
-
});
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
if (op.op === "copy" || op.op === "move") {
|
|
249
|
-
const val = getAtJson(baseJson, parseJsonPointer(op.from));
|
|
250
|
-
intents.push(...compileJsonPatchToIntent(baseJson, [{
|
|
251
|
-
op: "add",
|
|
252
|
-
path: op.path,
|
|
253
|
-
value: val
|
|
254
|
-
}]));
|
|
255
|
-
if (op.op === "move") intents.push(...compileJsonPatchToIntent(baseJson, [{
|
|
256
|
-
op: "remove",
|
|
257
|
-
path: op.from
|
|
258
|
-
}]));
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
const path = parseJsonPointer(op.path);
|
|
262
|
-
const parent = path.slice(0, -1);
|
|
263
|
-
const last = path[path.length - 1];
|
|
264
|
-
if (path.length === 0) {
|
|
265
|
-
if (op.op === "replace" || op.op === "add") intents.push({
|
|
266
|
-
t: "ObjSet",
|
|
267
|
-
path: [],
|
|
268
|
-
key: ROOT_KEY,
|
|
269
|
-
value: op.value
|
|
270
|
-
});
|
|
271
|
-
else if (op.op === "remove") intents.push({
|
|
272
|
-
t: "ObjSet",
|
|
273
|
-
path: [],
|
|
274
|
-
key: ROOT_KEY,
|
|
275
|
-
value: null
|
|
276
|
-
});
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
const isIndexLike = (s) => s === "-" || /^[0-9]+$/.test(s);
|
|
280
|
-
if (isIndexLike(last)) {
|
|
281
|
-
const index = last === "-" ? Number.POSITIVE_INFINITY : Number(last);
|
|
282
|
-
if (op.op === "add") intents.push({
|
|
283
|
-
t: "ArrInsert",
|
|
284
|
-
path: parent,
|
|
285
|
-
index,
|
|
286
|
-
value: op.value
|
|
287
|
-
});
|
|
288
|
-
else if (op.op === "remove") intents.push({
|
|
289
|
-
t: "ArrDelete",
|
|
290
|
-
path: parent,
|
|
291
|
-
index
|
|
292
|
-
});
|
|
293
|
-
else if (op.op === "replace") intents.push({
|
|
294
|
-
t: "ArrReplace",
|
|
295
|
-
path: parent,
|
|
296
|
-
index,
|
|
297
|
-
value: op.value
|
|
298
|
-
});
|
|
299
|
-
else assertNever$1(op, "Unsupported op at array index path");
|
|
300
|
-
} else {
|
|
301
|
-
const parentValue = pathValueAt(baseJson, parent);
|
|
302
|
-
if (!isPlainObject(parentValue)) throw new Error(`Expected object parent at ${stringifyJsonPointer(parent)}`);
|
|
303
|
-
if ((op.op === "replace" || op.op === "remove") && !hasOwn(parentValue, last)) throw new Error(`Missing key ${last} at ${stringifyJsonPointer(parent)}`);
|
|
304
|
-
if (op.op === "add") intents.push({
|
|
305
|
-
t: "ObjSet",
|
|
306
|
-
path: parent,
|
|
307
|
-
key: last,
|
|
308
|
-
value: op.value,
|
|
309
|
-
mode: "add"
|
|
310
|
-
});
|
|
311
|
-
else if (op.op === "replace") intents.push({
|
|
312
|
-
t: "ObjSet",
|
|
313
|
-
path: parent,
|
|
314
|
-
key: last,
|
|
315
|
-
value: op.value,
|
|
316
|
-
mode: "replace"
|
|
317
|
-
});
|
|
318
|
-
else if (op.op === "remove") intents.push({
|
|
319
|
-
t: "ObjRemove",
|
|
320
|
-
path: parent,
|
|
321
|
-
key: last
|
|
322
|
-
});
|
|
323
|
-
else assertNever$1(op, "Unsupported op");
|
|
324
|
-
}
|
|
254
|
+
for (let opIndex = 0; opIndex < patch.length; opIndex++) {
|
|
255
|
+
const op = patch[opIndex];
|
|
256
|
+
const compileBase = semantics === "sequential" ? workingBase : baseJson;
|
|
257
|
+
intents.push(...compileSingleOp(compileBase, op, opIndex));
|
|
258
|
+
if (semantics === "sequential") workingBase = applyPatchOpToJson(workingBase, op, opIndex);
|
|
325
259
|
}
|
|
326
260
|
return intents;
|
|
327
261
|
}
|
|
@@ -473,6 +407,185 @@ function pathValueAt(base, path) {
|
|
|
473
407
|
function assertNever$1(_value, message) {
|
|
474
408
|
throw new Error(message);
|
|
475
409
|
}
|
|
410
|
+
function compileSingleOp(baseJson, op, opIndex) {
|
|
411
|
+
if (op.op === "test") return [{
|
|
412
|
+
t: "Test",
|
|
413
|
+
path: parsePointerOrThrow(op.path, op.path, opIndex),
|
|
414
|
+
value: op.value
|
|
415
|
+
}];
|
|
416
|
+
if (op.op === "copy" || op.op === "move") {
|
|
417
|
+
const fromPath = parsePointerOrThrow(op.from, op.from, opIndex);
|
|
418
|
+
const toPath = parsePointerOrThrow(op.path, op.path, opIndex);
|
|
419
|
+
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);
|
|
420
|
+
const val = lookupValueOrThrow(baseJson, fromPath, op.from, opIndex);
|
|
421
|
+
const out = compileSingleOp(baseJson, {
|
|
422
|
+
op: "add",
|
|
423
|
+
path: op.path,
|
|
424
|
+
value: val
|
|
425
|
+
}, opIndex);
|
|
426
|
+
if (op.op === "move") out.push(...compileSingleOp(baseJson, {
|
|
427
|
+
op: "remove",
|
|
428
|
+
path: op.from
|
|
429
|
+
}, opIndex));
|
|
430
|
+
return out;
|
|
431
|
+
}
|
|
432
|
+
const path = parsePointerOrThrow(op.path, op.path, opIndex);
|
|
433
|
+
if (path.length === 0) {
|
|
434
|
+
if (op.op === "replace" || op.op === "add") return [{
|
|
435
|
+
t: "ObjSet",
|
|
436
|
+
path: [],
|
|
437
|
+
key: ROOT_KEY,
|
|
438
|
+
value: op.value
|
|
439
|
+
}];
|
|
440
|
+
throw compileError("INVALID_TARGET", "remove at root path is not supported in RFC-compliant mode", op.path, opIndex);
|
|
441
|
+
}
|
|
442
|
+
const parent = path.slice(0, -1);
|
|
443
|
+
const token = path[path.length - 1];
|
|
444
|
+
const parentPath = stringifyJsonPointer(parent);
|
|
445
|
+
const parentValue = getParentValue(baseJson, parent, opIndex);
|
|
446
|
+
if (Array.isArray(parentValue)) {
|
|
447
|
+
const index = parseArrayIndexToken(token, op.op, parentValue.length, op.path, opIndex);
|
|
448
|
+
if (op.op === "add") return [{
|
|
449
|
+
t: "ArrInsert",
|
|
450
|
+
path: parent,
|
|
451
|
+
index,
|
|
452
|
+
value: op.value
|
|
453
|
+
}];
|
|
454
|
+
if (op.op === "remove") return [{
|
|
455
|
+
t: "ArrDelete",
|
|
456
|
+
path: parent,
|
|
457
|
+
index
|
|
458
|
+
}];
|
|
459
|
+
if (op.op === "replace") return [{
|
|
460
|
+
t: "ArrReplace",
|
|
461
|
+
path: parent,
|
|
462
|
+
index,
|
|
463
|
+
value: op.value
|
|
464
|
+
}];
|
|
465
|
+
return assertNever$1(op, "Unsupported op at array path");
|
|
466
|
+
}
|
|
467
|
+
if (!isPlainObject(parentValue)) throw compileError("INVALID_TARGET", `expected object or array parent at ${parentPath}`, parentPath, opIndex);
|
|
468
|
+
if ((op.op === "replace" || op.op === "remove") && !hasOwn(parentValue, token)) throw compileError("MISSING_TARGET", `missing key ${token} at ${parentPath}`, op.path, opIndex);
|
|
469
|
+
if (op.op === "add") return [{
|
|
470
|
+
t: "ObjSet",
|
|
471
|
+
path: parent,
|
|
472
|
+
key: token,
|
|
473
|
+
value: op.value,
|
|
474
|
+
mode: "add"
|
|
475
|
+
}];
|
|
476
|
+
if (op.op === "replace") return [{
|
|
477
|
+
t: "ObjSet",
|
|
478
|
+
path: parent,
|
|
479
|
+
key: token,
|
|
480
|
+
value: op.value,
|
|
481
|
+
mode: "replace"
|
|
482
|
+
}];
|
|
483
|
+
if (op.op === "remove") return [{
|
|
484
|
+
t: "ObjRemove",
|
|
485
|
+
path: parent,
|
|
486
|
+
key: token
|
|
487
|
+
}];
|
|
488
|
+
return assertNever$1(op, "Unsupported op");
|
|
489
|
+
}
|
|
490
|
+
function applyPatchOpToJson(baseJson, op, opIndex) {
|
|
491
|
+
let doc = structuredClone(baseJson);
|
|
492
|
+
if (op.op === "test") return doc;
|
|
493
|
+
if (op.op === "copy" || op.op === "move") {
|
|
494
|
+
const fromPath = parsePointerOrThrow(op.from, op.from, opIndex);
|
|
495
|
+
const value = structuredClone(lookupValueOrThrow(doc, fromPath, op.from, opIndex));
|
|
496
|
+
if (op.op === "move") doc = applyPatchOpToJson(doc, {
|
|
497
|
+
op: "remove",
|
|
498
|
+
path: op.from
|
|
499
|
+
}, opIndex);
|
|
500
|
+
return applyPatchOpToJson(doc, {
|
|
501
|
+
op: "add",
|
|
502
|
+
path: op.path,
|
|
503
|
+
value
|
|
504
|
+
}, opIndex);
|
|
505
|
+
}
|
|
506
|
+
const path = parsePointerOrThrow(op.path, op.path, opIndex);
|
|
507
|
+
if (path.length === 0) {
|
|
508
|
+
if (op.op === "add" || op.op === "replace") return structuredClone(op.value);
|
|
509
|
+
throw compileError("INVALID_TARGET", "remove at root path is not supported in RFC-compliant mode", op.path, opIndex);
|
|
510
|
+
}
|
|
511
|
+
const parentPath = path.slice(0, -1);
|
|
512
|
+
const token = path[path.length - 1];
|
|
513
|
+
let parent;
|
|
514
|
+
if (parentPath.length === 0) parent = doc;
|
|
515
|
+
else parent = lookupValueOrThrow(doc, parentPath, op.path, opIndex);
|
|
516
|
+
if (Array.isArray(parent)) {
|
|
517
|
+
const index = parseArrayIndexToken(token, op.op, parent.length, op.path, opIndex);
|
|
518
|
+
if (op.op === "add") {
|
|
519
|
+
const insertAt = index === Number.POSITIVE_INFINITY ? parent.length : index;
|
|
520
|
+
parent.splice(insertAt, 0, structuredClone(op.value));
|
|
521
|
+
return doc;
|
|
522
|
+
}
|
|
523
|
+
if (op.op === "replace") {
|
|
524
|
+
parent[index] = structuredClone(op.value);
|
|
525
|
+
return doc;
|
|
526
|
+
}
|
|
527
|
+
parent.splice(index, 1);
|
|
528
|
+
return doc;
|
|
529
|
+
}
|
|
530
|
+
if (!isPlainObject(parent)) throw compileError("INVALID_TARGET", `expected object or array parent at ${stringifyJsonPointer(parentPath)}`, op.path, opIndex);
|
|
531
|
+
if (op.op === "add" || op.op === "replace") {
|
|
532
|
+
parent[token] = structuredClone(op.value);
|
|
533
|
+
return doc;
|
|
534
|
+
}
|
|
535
|
+
delete parent[token];
|
|
536
|
+
return doc;
|
|
537
|
+
}
|
|
538
|
+
function parsePointerOrThrow(ptr, path, opIndex) {
|
|
539
|
+
try {
|
|
540
|
+
return parseJsonPointer(ptr);
|
|
541
|
+
} catch (error) {
|
|
542
|
+
throw compileError("INVALID_POINTER", error instanceof Error ? error.message : "invalid pointer", path, opIndex);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function lookupValueOrThrow(baseJson, path, pointer, opIndex) {
|
|
546
|
+
try {
|
|
547
|
+
return getAtJson(baseJson, path);
|
|
548
|
+
} catch (error) {
|
|
549
|
+
throw compileErrorFromLookup(error, pointer, opIndex);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
function getParentValue(baseJson, parent, opIndex) {
|
|
553
|
+
if (parent.length === 0) return baseJson;
|
|
554
|
+
try {
|
|
555
|
+
return pathValueAt(baseJson, parent);
|
|
556
|
+
} catch (error) {
|
|
557
|
+
throw compileErrorFromLookup(error, stringifyJsonPointer(parent), opIndex);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function parseArrayIndexToken(token, op, arrLength, path, opIndex) {
|
|
561
|
+
if (token === "-") {
|
|
562
|
+
if (op !== "add") throw compileError("INVALID_POINTER", `'-' index is only valid for add at ${path}`, path, opIndex);
|
|
563
|
+
return Number.POSITIVE_INFINITY;
|
|
564
|
+
}
|
|
565
|
+
if (!/^[0-9]+$/.test(token)) throw compileError("INVALID_POINTER", `expected array index at ${path}`, path, opIndex);
|
|
566
|
+
const index = Number(token);
|
|
567
|
+
if (!Number.isSafeInteger(index)) throw compileError("OUT_OF_BOUNDS", `array index is too large at ${path}`, path, opIndex);
|
|
568
|
+
if (op === "add") {
|
|
569
|
+
if (index > arrLength) throw compileError("OUT_OF_BOUNDS", `index out of bounds at ${path}; expected 0..${arrLength}`, path, opIndex);
|
|
570
|
+
} else if (index >= arrLength) throw compileError("OUT_OF_BOUNDS", `index out of bounds at ${path}; expected 0..${Math.max(arrLength - 1, 0)}`, path, opIndex);
|
|
571
|
+
return index;
|
|
572
|
+
}
|
|
573
|
+
function compileErrorFromLookup(error, path, opIndex) {
|
|
574
|
+
const message = error instanceof Error ? error.message : "invalid path";
|
|
575
|
+
if (message.includes("Expected array index")) return compileError("INVALID_POINTER", message, path, opIndex);
|
|
576
|
+
if (message.includes("Index out of bounds")) return compileError("OUT_OF_BOUNDS", message, path, opIndex);
|
|
577
|
+
if (message.includes("Missing key")) return compileError("MISSING_PARENT", message, path, opIndex);
|
|
578
|
+
if (message.includes("Cannot traverse into non-container")) return compileError("INVALID_TARGET", message, path, opIndex);
|
|
579
|
+
return compileError("INVALID_PATCH", message, path, opIndex);
|
|
580
|
+
}
|
|
581
|
+
function compileError(reason, message, path, opIndex) {
|
|
582
|
+
return new PatchCompileError(reason, message, path, opIndex);
|
|
583
|
+
}
|
|
584
|
+
function isStrictDescendantPath(from, to) {
|
|
585
|
+
if (to.length <= from.length) return false;
|
|
586
|
+
for (let i = 0; i < from.length; i++) if (from[i] !== to[i]) return false;
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
476
589
|
|
|
477
590
|
//#endregion
|
|
478
591
|
//#region src/doc.ts
|
|
@@ -673,13 +786,17 @@ function applyTest(base, head, it, evalTestAgainst) {
|
|
|
673
786
|
return {
|
|
674
787
|
ok: false,
|
|
675
788
|
code: 409,
|
|
676
|
-
|
|
789
|
+
reason: "MISSING_TARGET",
|
|
790
|
+
message: `test path missing at /${it.path.join("/")}`,
|
|
791
|
+
path: `/${it.path.join("/")}`
|
|
677
792
|
};
|
|
678
793
|
}
|
|
679
794
|
if (!jsonEquals(got, it.value)) return {
|
|
680
795
|
ok: false,
|
|
681
796
|
code: 409,
|
|
682
|
-
|
|
797
|
+
reason: "TEST_FAILED",
|
|
798
|
+
message: `test failed at /${it.path.join("/")}`,
|
|
799
|
+
path: `/${it.path.join("/")}`
|
|
683
800
|
};
|
|
684
801
|
return null;
|
|
685
802
|
}
|
|
@@ -692,12 +809,16 @@ function applyObjSet(head, it, newDot) {
|
|
|
692
809
|
if (!parentRes.ok) return {
|
|
693
810
|
ok: false,
|
|
694
811
|
code: 409,
|
|
695
|
-
|
|
812
|
+
reason: "MISSING_PARENT",
|
|
813
|
+
message: parentRes.message,
|
|
814
|
+
path: `/${it.path.join("/")}`
|
|
696
815
|
};
|
|
697
816
|
if (it.mode === "replace" && !parentRes.obj.entries.has(it.key)) return {
|
|
698
817
|
ok: false,
|
|
699
818
|
code: 409,
|
|
700
|
-
|
|
819
|
+
reason: "MISSING_TARGET",
|
|
820
|
+
message: `no value at /${[...it.path, it.key].join("/")}`,
|
|
821
|
+
path: `/${[...it.path, it.key].join("/")}`
|
|
701
822
|
};
|
|
702
823
|
const d = newDot();
|
|
703
824
|
const parentObj = parentRes.obj;
|
|
@@ -709,12 +830,16 @@ function applyObjRemove(head, it, newDot) {
|
|
|
709
830
|
if (!parentRes.ok) return {
|
|
710
831
|
ok: false,
|
|
711
832
|
code: 409,
|
|
712
|
-
|
|
833
|
+
reason: "MISSING_PARENT",
|
|
834
|
+
message: parentRes.message,
|
|
835
|
+
path: `/${it.path.join("/")}`
|
|
713
836
|
};
|
|
714
837
|
if (!parentRes.obj.entries.has(it.key)) return {
|
|
715
838
|
ok: false,
|
|
716
839
|
code: 409,
|
|
717
|
-
|
|
840
|
+
reason: "MISSING_TARGET",
|
|
841
|
+
message: `no value at /${[...it.path, it.key].join("/")}`,
|
|
842
|
+
path: `/${[...it.path, it.key].join("/")}`
|
|
718
843
|
};
|
|
719
844
|
const d = newDot();
|
|
720
845
|
const parentObj = parentRes.obj;
|
|
@@ -734,7 +859,9 @@ function applyArrInsert(base, head, it, newDot, bumpCounterAbove) {
|
|
|
734
859
|
return {
|
|
735
860
|
ok: false,
|
|
736
861
|
code: 409,
|
|
737
|
-
|
|
862
|
+
reason: "MISSING_PARENT",
|
|
863
|
+
message: `base array missing at /${it.path.join("/")}`,
|
|
864
|
+
path: `/${it.path.join("/")}`
|
|
738
865
|
};
|
|
739
866
|
}
|
|
740
867
|
const headSeq = ensureSeqAtPath(head, it.path, newDot());
|
|
@@ -743,7 +870,9 @@ function applyArrInsert(base, head, it, newDot, bumpCounterAbove) {
|
|
|
743
870
|
if (idx < 0 || idx > baseLen) return {
|
|
744
871
|
ok: false,
|
|
745
872
|
code: 409,
|
|
746
|
-
|
|
873
|
+
reason: "OUT_OF_BOUNDS",
|
|
874
|
+
message: `index out of bounds at /${it.path.join("/")}/${it.index}`,
|
|
875
|
+
path: `/${it.path.join("/")}/${it.index}`
|
|
747
876
|
};
|
|
748
877
|
const prev = idx === 0 ? HEAD : rgaIdAtIndex(baseSeq, idx - 1) ?? HEAD;
|
|
749
878
|
const d = nextInsertDotForPrev(headSeq, prev, newDot, bumpCounterAbove);
|
|
@@ -767,14 +896,18 @@ function applyArrDelete(base, head, it, newDot) {
|
|
|
767
896
|
if (!baseSeq) return {
|
|
768
897
|
ok: false,
|
|
769
898
|
code: 409,
|
|
770
|
-
|
|
899
|
+
reason: "MISSING_PARENT",
|
|
900
|
+
message: `base array missing at /${it.path.join("/")}`,
|
|
901
|
+
path: `/${it.path.join("/")}`
|
|
771
902
|
};
|
|
772
903
|
const headSeq = ensureSeqAtPath(head, it.path, d);
|
|
773
904
|
const baseId = rgaIdAtIndex(baseSeq, it.index);
|
|
774
905
|
if (!baseId) return {
|
|
775
906
|
ok: false,
|
|
776
907
|
code: 409,
|
|
777
|
-
|
|
908
|
+
reason: "MISSING_TARGET",
|
|
909
|
+
message: `no base element at index ${it.index}`,
|
|
910
|
+
path: `/${it.path.join("/")}/${it.index}`
|
|
778
911
|
};
|
|
779
912
|
rgaDelete(headSeq, baseId);
|
|
780
913
|
return null;
|
|
@@ -785,20 +918,26 @@ function applyArrReplace(base, head, it, newDot) {
|
|
|
785
918
|
if (!baseSeq) return {
|
|
786
919
|
ok: false,
|
|
787
920
|
code: 409,
|
|
788
|
-
|
|
921
|
+
reason: "MISSING_PARENT",
|
|
922
|
+
message: `base array missing at /${it.path.join("/")}`,
|
|
923
|
+
path: `/${it.path.join("/")}`
|
|
789
924
|
};
|
|
790
925
|
const headSeq = ensureSeqAtPath(head, it.path, d);
|
|
791
926
|
const baseId = rgaIdAtIndex(baseSeq, it.index);
|
|
792
927
|
if (!baseId) return {
|
|
793
928
|
ok: false,
|
|
794
929
|
code: 409,
|
|
795
|
-
|
|
930
|
+
reason: "MISSING_TARGET",
|
|
931
|
+
message: `no base element at index ${it.index}`,
|
|
932
|
+
path: `/${it.path.join("/")}/${it.index}`
|
|
796
933
|
};
|
|
797
934
|
const e = headSeq.elems.get(baseId);
|
|
798
935
|
if (!e || e.tombstone) return {
|
|
799
936
|
ok: false,
|
|
800
937
|
code: 409,
|
|
801
|
-
|
|
938
|
+
reason: "MISSING_TARGET",
|
|
939
|
+
message: `element already deleted at index ${it.index}`,
|
|
940
|
+
path: `/${it.path.join("/")}/${it.index}`
|
|
802
941
|
};
|
|
803
942
|
e.value = nodeFromJson(it.value, newDot);
|
|
804
943
|
return null;
|
|
@@ -842,34 +981,39 @@ function applyIntentsToCrdt(base, head, intents, newDot, evalTestAgainst = "head
|
|
|
842
981
|
}
|
|
843
982
|
return { ok: true };
|
|
844
983
|
}
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
984
|
+
function jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove) {
|
|
985
|
+
if (isJsonPatchToCrdtOptions(baseOrOptions)) return jsonPatchToCrdtInternal(baseOrOptions);
|
|
986
|
+
if (!head || !patch || !newDot) return {
|
|
987
|
+
ok: false,
|
|
988
|
+
code: 409,
|
|
989
|
+
reason: "INVALID_PATCH",
|
|
990
|
+
message: "invalid jsonPatchToCrdt call signature"
|
|
991
|
+
};
|
|
992
|
+
return jsonPatchToCrdtInternal({
|
|
993
|
+
base: baseOrOptions,
|
|
994
|
+
head,
|
|
995
|
+
patch,
|
|
996
|
+
newDot,
|
|
997
|
+
evalTestAgainst,
|
|
998
|
+
bumpCounterAbove
|
|
999
|
+
});
|
|
857
1000
|
}
|
|
858
|
-
|
|
859
|
-
* Safe wrapper around `jsonPatchToCrdt` that converts compile-time errors into `409` results.
|
|
860
|
-
* This function never throws for malformed/invalid patch paths.
|
|
861
|
-
*/
|
|
862
|
-
function jsonPatchToCrdtSafe(base, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove) {
|
|
1001
|
+
function jsonPatchToCrdtSafe(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove) {
|
|
863
1002
|
try {
|
|
864
|
-
return jsonPatchToCrdt(
|
|
865
|
-
|
|
866
|
-
return {
|
|
1003
|
+
if (isJsonPatchToCrdtOptions(baseOrOptions)) return jsonPatchToCrdt(baseOrOptions);
|
|
1004
|
+
if (!head || !patch || !newDot) return {
|
|
867
1005
|
ok: false,
|
|
868
1006
|
code: 409,
|
|
869
|
-
|
|
1007
|
+
reason: "INVALID_PATCH",
|
|
1008
|
+
message: "invalid jsonPatchToCrdtSafe call signature"
|
|
870
1009
|
};
|
|
1010
|
+
return jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst, bumpCounterAbove);
|
|
1011
|
+
} catch (error) {
|
|
1012
|
+
return toApplyError$1(error);
|
|
871
1013
|
}
|
|
872
1014
|
}
|
|
1015
|
+
/** Alias for codebases that prefer `try*` naming for non-throwing APIs. */
|
|
1016
|
+
const tryJsonPatchToCrdt = jsonPatchToCrdtSafe;
|
|
873
1017
|
/**
|
|
874
1018
|
* Generate a JSON Patch delta between two CRDT documents.
|
|
875
1019
|
* @param base - The base document snapshot.
|
|
@@ -891,19 +1035,95 @@ function crdtToFullReplace(doc) {
|
|
|
891
1035
|
value: materialize(doc.root)
|
|
892
1036
|
}];
|
|
893
1037
|
}
|
|
1038
|
+
function jsonPatchToCrdtInternal(options) {
|
|
1039
|
+
const evalTestAgainst = options.evalTestAgainst ?? "head";
|
|
1040
|
+
if ((options.semantics ?? "sequential") === "base") {
|
|
1041
|
+
const baseJson = materialize(options.base.root);
|
|
1042
|
+
let intents;
|
|
1043
|
+
try {
|
|
1044
|
+
intents = compileJsonPatchToIntent(baseJson, options.patch, { semantics: "base" });
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
return toApplyError$1(error);
|
|
1047
|
+
}
|
|
1048
|
+
return applyIntentsToCrdt(options.base, options.head, intents, options.newDot, evalTestAgainst, options.bumpCounterAbove);
|
|
1049
|
+
}
|
|
1050
|
+
let shadowBase = cloneDoc(evalTestAgainst === "base" ? options.base : options.head);
|
|
1051
|
+
let shadowCtr = 0;
|
|
1052
|
+
const shadowDot = () => ({
|
|
1053
|
+
actor: "__shadow__",
|
|
1054
|
+
ctr: ++shadowCtr
|
|
1055
|
+
});
|
|
1056
|
+
const shadowBump = (ctr) => {
|
|
1057
|
+
if (shadowCtr < ctr) shadowCtr = ctr;
|
|
1058
|
+
};
|
|
1059
|
+
for (let opIndex = 0; opIndex < options.patch.length; opIndex++) {
|
|
1060
|
+
const op = options.patch[opIndex];
|
|
1061
|
+
const baseJson = materialize(shadowBase.root);
|
|
1062
|
+
let intents;
|
|
1063
|
+
try {
|
|
1064
|
+
intents = compileJsonPatchToIntent(baseJson, [op], { semantics: "sequential" });
|
|
1065
|
+
} catch (error) {
|
|
1066
|
+
return withOpIndex(toApplyError$1(error), opIndex);
|
|
1067
|
+
}
|
|
1068
|
+
const headStep = applyIntentsToCrdt(shadowBase, options.head, intents, options.newDot, evalTestAgainst, options.bumpCounterAbove);
|
|
1069
|
+
if (!headStep.ok) return withOpIndex(headStep, opIndex);
|
|
1070
|
+
if (evalTestAgainst === "base") {
|
|
1071
|
+
const shadowStep = applyIntentsToCrdt(shadowBase, shadowBase, intents, shadowDot, "base", shadowBump);
|
|
1072
|
+
if (!shadowStep.ok) return withOpIndex(shadowStep, opIndex);
|
|
1073
|
+
} else shadowBase = cloneDoc(options.head);
|
|
1074
|
+
}
|
|
1075
|
+
return { ok: true };
|
|
1076
|
+
}
|
|
1077
|
+
function withOpIndex(error, opIndex) {
|
|
1078
|
+
if (error.opIndex !== void 0) return error;
|
|
1079
|
+
return {
|
|
1080
|
+
...error,
|
|
1081
|
+
opIndex
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
function isJsonPatchToCrdtOptions(value) {
|
|
1085
|
+
return typeof value === "object" && value !== null && "base" in value && "head" in value && "patch" in value && "newDot" in value;
|
|
1086
|
+
}
|
|
1087
|
+
function toApplyError$1(error) {
|
|
1088
|
+
if (error instanceof PatchCompileError) return {
|
|
1089
|
+
ok: false,
|
|
1090
|
+
code: 409,
|
|
1091
|
+
reason: error.reason,
|
|
1092
|
+
message: error.message,
|
|
1093
|
+
path: error.path,
|
|
1094
|
+
opIndex: error.opIndex
|
|
1095
|
+
};
|
|
1096
|
+
return {
|
|
1097
|
+
ok: false,
|
|
1098
|
+
code: 409,
|
|
1099
|
+
reason: "INVALID_PATCH",
|
|
1100
|
+
message: error instanceof Error ? error.message : "failed to compile/apply patch"
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
894
1103
|
function assertNever(_value, message) {
|
|
895
1104
|
throw new Error(message);
|
|
896
1105
|
}
|
|
897
1106
|
|
|
898
1107
|
//#endregion
|
|
899
1108
|
//#region src/state.ts
|
|
900
|
-
/** Error thrown when a JSON Patch cannot be applied. Includes
|
|
1109
|
+
/** Error thrown when a JSON Patch cannot be applied. Includes structured conflict metadata. */
|
|
901
1110
|
var PatchError = class extends Error {
|
|
902
1111
|
code;
|
|
903
|
-
|
|
904
|
-
|
|
1112
|
+
reason;
|
|
1113
|
+
path;
|
|
1114
|
+
opIndex;
|
|
1115
|
+
constructor(errorOrMessage, code = 409, reason = "INVALID_PATCH") {
|
|
1116
|
+
super(typeof errorOrMessage === "string" ? errorOrMessage : errorOrMessage.message);
|
|
905
1117
|
this.name = "PatchError";
|
|
906
|
-
|
|
1118
|
+
if (typeof errorOrMessage === "string") {
|
|
1119
|
+
this.code = code;
|
|
1120
|
+
this.reason = reason;
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
this.code = errorOrMessage.code;
|
|
1124
|
+
this.reason = errorOrMessage.reason;
|
|
1125
|
+
this.path = errorOrMessage.path;
|
|
1126
|
+
this.opIndex = errorOrMessage.opIndex;
|
|
907
1127
|
}
|
|
908
1128
|
};
|
|
909
1129
|
/**
|
|
@@ -920,6 +1140,16 @@ function createState(initial, options) {
|
|
|
920
1140
|
};
|
|
921
1141
|
}
|
|
922
1142
|
/**
|
|
1143
|
+
* Fork a replica from a shared origin state while assigning a new local actor ID.
|
|
1144
|
+
* The forked state has an independent document clone and clock.
|
|
1145
|
+
*/
|
|
1146
|
+
function forkState(origin, actor) {
|
|
1147
|
+
return {
|
|
1148
|
+
doc: cloneDoc(origin.doc),
|
|
1149
|
+
clock: createClock(actor, origin.clock.ctr)
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
923
1153
|
* Materialize a CRDT document or state back to a plain JSON value.
|
|
924
1154
|
* @param target - A `Doc` or `CrdtState` to materialize.
|
|
925
1155
|
* @returns The JSON representation of the current document.
|
|
@@ -933,34 +1163,69 @@ function toJson(target) {
|
|
|
933
1163
|
* Throws `PatchError` on conflict (e.g. out-of-bounds index, failed test op).
|
|
934
1164
|
* @param state - The current CRDT state.
|
|
935
1165
|
* @param patch - Array of RFC 6902 JSON Patch operations.
|
|
936
|
-
* @param options - Optional base
|
|
1166
|
+
* @param options - Optional base state snapshot and patch semantics.
|
|
937
1167
|
* @returns A new `CrdtState` with the patch applied.
|
|
938
1168
|
*/
|
|
939
1169
|
function applyPatch(state, patch, options = {}) {
|
|
940
|
-
const
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
};
|
|
944
|
-
const result = applyPatchInternal(nextState, patch, options);
|
|
945
|
-
if (!result.ok) throw new PatchError(result.message, result.code);
|
|
946
|
-
return nextState;
|
|
1170
|
+
const result = tryApplyPatch(state, patch, options);
|
|
1171
|
+
if (!result.ok) throw new PatchError(result.error);
|
|
1172
|
+
return result.state;
|
|
947
1173
|
}
|
|
948
1174
|
/**
|
|
949
1175
|
* Apply a JSON Patch to the state in place, mutating the existing state.
|
|
950
1176
|
* Throws `PatchError` on conflict.
|
|
951
1177
|
* @param state - The CRDT state to mutate.
|
|
952
1178
|
* @param patch - Array of RFC 6902 JSON Patch operations.
|
|
953
|
-
* @param options - Optional base
|
|
1179
|
+
* @param options - Optional base state snapshot, patch semantics, and atomicity.
|
|
954
1180
|
*/
|
|
955
1181
|
function applyPatchInPlace(state, patch, options = {}) {
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1182
|
+
const result = tryApplyPatchInPlace(state, patch, options);
|
|
1183
|
+
if (!result.ok) throw new PatchError(result.error);
|
|
1184
|
+
}
|
|
1185
|
+
/** Non-throwing immutable patch application variant. */
|
|
1186
|
+
function tryApplyPatch(state, patch, options = {}) {
|
|
1187
|
+
const nextState = {
|
|
1188
|
+
doc: cloneDoc(state.doc),
|
|
1189
|
+
clock: cloneClock(state.clock)
|
|
1190
|
+
};
|
|
1191
|
+
const result = applyPatchInternal(nextState, patch, options);
|
|
1192
|
+
if (!result.ok) return {
|
|
1193
|
+
ok: false,
|
|
1194
|
+
error: result
|
|
1195
|
+
};
|
|
1196
|
+
return {
|
|
1197
|
+
ok: true,
|
|
1198
|
+
state: nextState
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
/** Non-throwing in-place patch application variant. */
|
|
1202
|
+
function tryApplyPatchInPlace(state, patch, options = {}) {
|
|
1203
|
+
const { atomic = true, ...applyOptions } = options;
|
|
1204
|
+
if (atomic) {
|
|
1205
|
+
const next = tryApplyPatch(state, patch, applyOptions);
|
|
1206
|
+
if (!next.ok) return next;
|
|
1207
|
+
state.doc = next.state.doc;
|
|
1208
|
+
state.clock = next.state.clock;
|
|
1209
|
+
return { ok: true };
|
|
961
1210
|
}
|
|
962
|
-
const result = applyPatchInternal(state, patch,
|
|
963
|
-
if (!result.ok)
|
|
1211
|
+
const result = applyPatchInternal(state, patch, applyOptions);
|
|
1212
|
+
if (!result.ok) return {
|
|
1213
|
+
ok: false,
|
|
1214
|
+
error: result
|
|
1215
|
+
};
|
|
1216
|
+
return { ok: true };
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Validate whether a patch is applicable against a JSON base value under the chosen options.
|
|
1220
|
+
* Does not mutate caller-provided values.
|
|
1221
|
+
*/
|
|
1222
|
+
function validateJsonPatch(base, patch, options = {}) {
|
|
1223
|
+
const result = tryApplyPatch(createState(base, { actor: "__validate__" }), patch, options);
|
|
1224
|
+
if (!result.ok) return {
|
|
1225
|
+
ok: false,
|
|
1226
|
+
error: result.error
|
|
1227
|
+
};
|
|
1228
|
+
return { ok: true };
|
|
964
1229
|
}
|
|
965
1230
|
/**
|
|
966
1231
|
* Apply a JSON Patch as a specific actor while maintaining an external version vector.
|
|
@@ -971,7 +1236,7 @@ function applyPatchAsActor(doc, vv, actor, patch, options = {}) {
|
|
|
971
1236
|
const state = applyPatch({
|
|
972
1237
|
doc,
|
|
973
1238
|
clock: createClock(actor, Math.max(vv[actor] ?? 0, observedCtr))
|
|
974
|
-
}, patch, options);
|
|
1239
|
+
}, patch, toApplyPatchOptionsForActor(options));
|
|
975
1240
|
return {
|
|
976
1241
|
state,
|
|
977
1242
|
vv: {
|
|
@@ -980,27 +1245,37 @@ function applyPatchAsActor(doc, vv, actor, patch, options = {}) {
|
|
|
980
1245
|
}
|
|
981
1246
|
};
|
|
982
1247
|
}
|
|
1248
|
+
function toApplyPatchOptionsForActor(options) {
|
|
1249
|
+
return {
|
|
1250
|
+
semantics: options.semantics,
|
|
1251
|
+
testAgainst: options.testAgainst,
|
|
1252
|
+
base: options.base ? {
|
|
1253
|
+
doc: options.base,
|
|
1254
|
+
clock: createClock("__base__", 0)
|
|
1255
|
+
} : void 0
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
983
1258
|
function applyPatchInternal(state, patch, options) {
|
|
984
|
-
if ((options.semantics ?? "
|
|
1259
|
+
if ((options.semantics ?? "sequential") === "sequential") {
|
|
985
1260
|
const explicitBaseState = options.base ? {
|
|
986
|
-
doc: cloneDoc(options.base),
|
|
1261
|
+
doc: cloneDoc(options.base.doc),
|
|
987
1262
|
clock: createClock("__base__", 0)
|
|
988
1263
|
} : null;
|
|
989
1264
|
for (const op of patch) {
|
|
990
1265
|
const step = applyPatchOpSequential(state, op, options, explicitBaseState ? explicitBaseState.doc : cloneDoc(state.doc));
|
|
991
1266
|
if (!step.ok) return step;
|
|
992
|
-
if (explicitBaseState) {
|
|
1267
|
+
if (explicitBaseState && op.op !== "test") {
|
|
993
1268
|
const baseStep = applyPatchInternal(explicitBaseState, [op], {
|
|
994
1269
|
semantics: "sequential",
|
|
995
|
-
testAgainst:
|
|
1270
|
+
testAgainst: "base"
|
|
996
1271
|
});
|
|
997
1272
|
if (!baseStep.ok) return baseStep;
|
|
998
1273
|
}
|
|
999
1274
|
}
|
|
1000
1275
|
return { ok: true };
|
|
1001
1276
|
}
|
|
1002
|
-
const baseDoc = options.base ? options.base : cloneDoc(state.doc);
|
|
1003
|
-
const compiled = compileIntents(materialize(baseDoc.root), patch);
|
|
1277
|
+
const baseDoc = options.base ? options.base.doc : cloneDoc(state.doc);
|
|
1278
|
+
const compiled = compileIntents(materialize(baseDoc.root), patch, "base");
|
|
1004
1279
|
if (!compiled.ok) return compiled;
|
|
1005
1280
|
return applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr));
|
|
1006
1281
|
}
|
|
@@ -1030,25 +1305,21 @@ function applyPatchOpSequential(state, op, options, baseDoc) {
|
|
|
1030
1305
|
return applySinglePatchOp(state, baseDoc, op, options);
|
|
1031
1306
|
}
|
|
1032
1307
|
function applySinglePatchOp(state, baseDoc, op, options) {
|
|
1033
|
-
const compiled = compileIntents(materialize(baseDoc.root), [op]);
|
|
1308
|
+
const compiled = compileIntents(materialize(baseDoc.root), [op], "sequential");
|
|
1034
1309
|
if (!compiled.ok) return compiled;
|
|
1035
1310
|
return applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr));
|
|
1036
1311
|
}
|
|
1037
1312
|
function bumpClockCounter(state, ctr) {
|
|
1038
1313
|
if (state.clock.ctr < ctr) state.clock.ctr = ctr;
|
|
1039
1314
|
}
|
|
1040
|
-
function compileIntents(baseJson, patch) {
|
|
1315
|
+
function compileIntents(baseJson, patch, semantics = "sequential") {
|
|
1041
1316
|
try {
|
|
1042
1317
|
return {
|
|
1043
1318
|
ok: true,
|
|
1044
|
-
intents: compileJsonPatchToIntent(baseJson, patch)
|
|
1319
|
+
intents: compileJsonPatchToIntent(baseJson, patch, { semantics })
|
|
1045
1320
|
};
|
|
1046
1321
|
} catch (error) {
|
|
1047
|
-
return
|
|
1048
|
-
ok: false,
|
|
1049
|
-
code: 409,
|
|
1050
|
-
message: error instanceof Error ? error.message : "failed to compile patch"
|
|
1051
|
-
};
|
|
1322
|
+
return toApplyError(error);
|
|
1052
1323
|
}
|
|
1053
1324
|
}
|
|
1054
1325
|
function maxCtrInNodeForActor$1(node, actor) {
|
|
@@ -1075,6 +1346,22 @@ function maxCtrInNodeForActor$1(node, actor) {
|
|
|
1075
1346
|
}
|
|
1076
1347
|
}
|
|
1077
1348
|
}
|
|
1349
|
+
function toApplyError(error) {
|
|
1350
|
+
if (error instanceof PatchCompileError) return {
|
|
1351
|
+
ok: false,
|
|
1352
|
+
code: 409,
|
|
1353
|
+
reason: error.reason,
|
|
1354
|
+
message: error.message,
|
|
1355
|
+
path: error.path,
|
|
1356
|
+
opIndex: error.opIndex
|
|
1357
|
+
};
|
|
1358
|
+
return {
|
|
1359
|
+
ok: false,
|
|
1360
|
+
code: 409,
|
|
1361
|
+
reason: "INVALID_PATCH",
|
|
1362
|
+
message: error instanceof Error ? error.message : "failed to compile patch"
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1078
1365
|
|
|
1079
1366
|
//#endregion
|
|
1080
1367
|
//#region src/serialize.ts
|
|
@@ -1197,6 +1484,19 @@ function deserializeNode(node) {
|
|
|
1197
1484
|
|
|
1198
1485
|
//#endregion
|
|
1199
1486
|
//#region src/merge.ts
|
|
1487
|
+
/** Error thrown by throwing merge helpers (`mergeDoc` / `mergeState`). */
|
|
1488
|
+
var MergeError = class extends Error {
|
|
1489
|
+
code;
|
|
1490
|
+
reason;
|
|
1491
|
+
path;
|
|
1492
|
+
constructor(error) {
|
|
1493
|
+
super(error.message);
|
|
1494
|
+
this.name = "MergeError";
|
|
1495
|
+
this.code = error.code;
|
|
1496
|
+
this.reason = "LINEAGE_MISMATCH";
|
|
1497
|
+
this.path = error.path;
|
|
1498
|
+
}
|
|
1499
|
+
};
|
|
1200
1500
|
/**
|
|
1201
1501
|
* Merge two CRDT documents from different peers into one.
|
|
1202
1502
|
* By default this requires shared array lineage for non-empty sequences.
|
|
@@ -1211,9 +1511,27 @@ function deserializeNode(node) {
|
|
|
1211
1511
|
* - **Kind mismatch**: the node with the higher "representative dot" wins and replaces the other entirely.
|
|
1212
1512
|
*/
|
|
1213
1513
|
function mergeDoc(a, b, options = {}) {
|
|
1514
|
+
const result = tryMergeDoc(a, b, options);
|
|
1515
|
+
if (!result.ok) throw new MergeError(result.error);
|
|
1516
|
+
return result.doc;
|
|
1517
|
+
}
|
|
1518
|
+
/** Non-throwing `mergeDoc` variant with structured conflict details. */
|
|
1519
|
+
function tryMergeDoc(a, b, options = {}) {
|
|
1214
1520
|
const mismatchPath = options.requireSharedOrigin ?? true ? findSeqLineageMismatch(a.root, b.root, []) : null;
|
|
1215
|
-
if (mismatchPath)
|
|
1216
|
-
|
|
1521
|
+
if (mismatchPath) return {
|
|
1522
|
+
ok: false,
|
|
1523
|
+
error: {
|
|
1524
|
+
ok: false,
|
|
1525
|
+
code: 409,
|
|
1526
|
+
reason: "LINEAGE_MISMATCH",
|
|
1527
|
+
message: `merge requires shared array origin at ${mismatchPath}`,
|
|
1528
|
+
path: mismatchPath
|
|
1529
|
+
}
|
|
1530
|
+
};
|
|
1531
|
+
return {
|
|
1532
|
+
ok: true,
|
|
1533
|
+
doc: { root: mergeNode(a.root, b.root) }
|
|
1534
|
+
};
|
|
1217
1535
|
}
|
|
1218
1536
|
/**
|
|
1219
1537
|
* Merge two CRDT states.
|
|
@@ -1227,11 +1545,22 @@ function mergeDoc(a, b, options = {}) {
|
|
|
1227
1545
|
* that actor across both input clocks and the merged document dots.
|
|
1228
1546
|
*/
|
|
1229
1547
|
function mergeState(a, b, options = {}) {
|
|
1230
|
-
const
|
|
1548
|
+
const result = tryMergeState(a, b, options);
|
|
1549
|
+
if (!result.ok) throw new MergeError(result.error);
|
|
1550
|
+
return result.state;
|
|
1551
|
+
}
|
|
1552
|
+
/** Non-throwing `mergeState` variant with structured conflict details. */
|
|
1553
|
+
function tryMergeState(a, b, options = {}) {
|
|
1554
|
+
const mergedDoc = tryMergeDoc(a.doc, b.doc, { requireSharedOrigin: options.requireSharedOrigin });
|
|
1555
|
+
if (!mergedDoc.ok) return mergedDoc;
|
|
1556
|
+
const doc = mergedDoc.doc;
|
|
1231
1557
|
const actor = options.actor ?? a.clock.actor;
|
|
1232
1558
|
return {
|
|
1233
|
-
|
|
1234
|
-
|
|
1559
|
+
ok: true,
|
|
1560
|
+
state: {
|
|
1561
|
+
doc,
|
|
1562
|
+
clock: createClock(actor, maxObservedCtrForActor(doc, actor, a, b))
|
|
1563
|
+
}
|
|
1235
1564
|
};
|
|
1236
1565
|
}
|
|
1237
1566
|
function findSeqLineageMismatch(a, b, path) {
|
|
@@ -1432,4 +1761,4 @@ function cloneNodeShallow(node) {
|
|
|
1432
1761
|
}
|
|
1433
1762
|
|
|
1434
1763
|
//#endregion
|
|
1435
|
-
export {
|
|
1764
|
+
export { vvMerge as $, compileJsonPatchToIntent as A, newSeq as B, crdtToJsonPatch as C, jsonPatchToCrdtSafe as D, jsonPatchToCrdt as E, stringifyJsonPointer as F, rgaDelete as G, objSet as H, ROOT_KEY as I, rgaLinearizeIds as J, rgaIdAtIndex as K, lwwSet as L, getAtJson as M, jsonEquals as N, tryJsonPatchToCrdt as O, parseJsonPointer as P, vvHasDot as Q, newObj as R, crdtToFullReplace as S, docFromJsonWithDot as T, materialize as U, objRemove as V, HEAD as W, compareDot as X, rgaPrevForInsertAtIndex as Y, dotToElemId as Z, tryApplyPatch as _, tryMergeState as a, applyIntentsToCrdt as b, serializeDoc as c, applyPatch as d, cloneClock as et, applyPatchAsActor as f, toJson as g, forkState as h, tryMergeDoc as i, diffJsonPatch as j, PatchCompileError as k, serializeState as l, createState as m, mergeDoc as n, nextDotForActor as nt, deserializeDoc as o, applyPatchInPlace as p, rgaInsertAfter as q, mergeState as r, observeDot as rt, deserializeState as s, MergeError as t, createClock as tt, PatchError as u, tryApplyPatchInPlace as v, docFromJson as w, cloneDoc as x, validateJsonPatch as y, newReg as z };
|