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