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.
- package/README.md +70 -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-BpAUNaPe.d.mts → merge-B8nmGV-o.d.ts} +133 -98
- package/dist/{merge-B1BFMhJJ.js → merge-BAfuC6bf.js} +693 -169
- package/dist/{merge-DikOFBWc.mjs → merge-CKcP1ZPt.mjs} +640 -170
- package/dist/{merge-QmPXxE6_.d.ts → merge-DQ_KDtnE.d.mts} +133 -98
- package/package.json +4 -4
|
@@ -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(
|
|
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
|
-
|
|
221
|
-
|
|
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 (
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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(
|
|
866
|
-
|
|
867
|
-
return {
|
|
1067
|
+
if (isJsonPatchToCrdtOptions(baseOrOptions)) return jsonPatchToCrdt(baseOrOptions);
|
|
1068
|
+
if (!head || !patch || !newDot) return {
|
|
868
1069
|
ok: false,
|
|
869
1070
|
code: 409,
|
|
870
|
-
|
|
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
|
|
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
|
-
|
|
905
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
942
|
-
|
|
943
|
-
|
|
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
|
|
1280
|
+
* @param options - Optional base state snapshot, patch semantics, and atomicity.
|
|
955
1281
|
*/
|
|
956
1282
|
function applyPatchInPlace(state, patch, options = {}) {
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
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,
|
|
964
|
-
if (!result.ok)
|
|
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 ?? "
|
|
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:
|
|
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
|
|
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
|
|
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)
|
|
1217
|
-
|
|
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
|
|
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
|
-
|
|
1235
|
-
|
|
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 () {
|