json-patch-to-crdt 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/{compact-CXfvMNCT.js → compact-CDvajUfn.js} +777 -227
- package/dist/{compact-BcwxBNx_.mjs → compact-Dj0BYeY5.mjs} +777 -227
- package/dist/{depth-CpJSyZE5.d.mts → depth-CM1kCxhm.d.mts} +30 -6
- package/dist/{depth-D88VeWb-.d.ts → depth-NbZ6Giq9.d.ts} +30 -6
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/internals.d.mts +11 -4
- package/dist/internals.d.ts +11 -4
- package/dist/internals.js +1 -1
- package/dist/internals.mjs +1 -1
- package/package.json +1 -1
|
@@ -27,6 +27,7 @@ function toDepthApplyError(error) {
|
|
|
27
27
|
|
|
28
28
|
//#endregion
|
|
29
29
|
//#region src/version-vector.ts
|
|
30
|
+
let observedVersionVectorObserverForTests = null;
|
|
30
31
|
function readVersionVectorCounter(vv, actor) {
|
|
31
32
|
if (!Object.prototype.hasOwnProperty.call(vv, actor)) return;
|
|
32
33
|
const counter = vv[actor];
|
|
@@ -51,6 +52,7 @@ function observeVersionVectorDot(vv, dot) {
|
|
|
51
52
|
* of the currently materialized document tree.
|
|
52
53
|
*/
|
|
53
54
|
function observedVersionVector(target) {
|
|
55
|
+
observedVersionVectorObserverForTests?.(target);
|
|
54
56
|
const doc = "doc" in target ? target.doc : target;
|
|
55
57
|
const vv = Object.create(null);
|
|
56
58
|
if ("clock" in target) observeVersionVectorDot(vv, {
|
|
@@ -494,6 +496,7 @@ function rgaPrevForInsertAtIndex(seq, index) {
|
|
|
494
496
|
//#endregion
|
|
495
497
|
//#region src/materialize.ts
|
|
496
498
|
let materializeObserver = null;
|
|
499
|
+
const EMPTY_PATH = [];
|
|
497
500
|
function createMaterializedObject() {
|
|
498
501
|
return Object.create(null);
|
|
499
502
|
}
|
|
@@ -508,7 +511,7 @@ function setMaterializedProperty(out, key, value) {
|
|
|
508
511
|
/** Convert a CRDT node graph into a plain JSON value using an explicit stack. */
|
|
509
512
|
function materialize(node) {
|
|
510
513
|
const observer = materializeObserver;
|
|
511
|
-
observer?.(
|
|
514
|
+
observer?.(EMPTY_PATH, node);
|
|
512
515
|
if (node.kind === "lww") return node.value;
|
|
513
516
|
const root = node.kind === "obj" ? createMaterializedObject() : [];
|
|
514
517
|
const stack = [];
|
|
@@ -539,7 +542,7 @@ function materialize(node) {
|
|
|
539
542
|
const child = entry.node;
|
|
540
543
|
const childDepth = frame.depth + 1;
|
|
541
544
|
assertTraversalDepth(childDepth);
|
|
542
|
-
const childPath = [...frame.path, key];
|
|
545
|
+
const childPath = observer ? [...frame.path, key] : EMPTY_PATH;
|
|
543
546
|
observer?.(childPath, child);
|
|
544
547
|
if (child.kind === "lww") {
|
|
545
548
|
setMaterializedProperty(frame.out, key, child.value);
|
|
@@ -577,7 +580,7 @@ function materialize(node) {
|
|
|
577
580
|
const child = elem.value;
|
|
578
581
|
const childDepth = frame.depth + 1;
|
|
579
582
|
assertTraversalDepth(childDepth);
|
|
580
|
-
const childPath = [...frame.path, String(frame.nextIndex)];
|
|
583
|
+
const childPath = observer ? [...frame.path, String(frame.nextIndex)] : EMPTY_PATH;
|
|
581
584
|
frame.nextIndex += 1;
|
|
582
585
|
observer?.(childPath, child);
|
|
583
586
|
if (child.kind === "lww") {
|
|
@@ -2069,51 +2072,35 @@ function docFromJson(value, nextDot) {
|
|
|
2069
2072
|
return { root: nodeFromJson(value, nextDot) };
|
|
2070
2073
|
}
|
|
2071
2074
|
/**
|
|
2072
|
-
* Legacy
|
|
2073
|
-
*
|
|
2075
|
+
* Legacy helper for tests and fixtures that seeds an entire document from one dot.
|
|
2076
|
+
*
|
|
2077
|
+
* It reuses that dot for object entries and synthesizes array child counters from the
|
|
2078
|
+
* same seed, which can produce low-quality causal metadata and unrealistic sequence
|
|
2079
|
+
* identities in production CRDT state.
|
|
2080
|
+
*
|
|
2081
|
+
* Prefer `docFromJson(value, nextDot)` so every node receives a fresh unique dot.
|
|
2082
|
+
*
|
|
2083
|
+
* @deprecated Use `docFromJson(value, nextDot)` for production documents.
|
|
2074
2084
|
*/
|
|
2075
2085
|
function docFromJsonWithDot(value, dot) {
|
|
2076
2086
|
return { root: deepNodeFromJson(value, dot) };
|
|
2077
2087
|
}
|
|
2078
2088
|
function getSeqAtPath(doc, path) {
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
if (cur.kind !== "obj") return;
|
|
2082
|
-
const ent = cur.entries.get(seg);
|
|
2083
|
-
if (!ent) return;
|
|
2084
|
-
cur = ent.node;
|
|
2085
|
-
}
|
|
2086
|
-
return cur.kind === "seq" ? cur : void 0;
|
|
2089
|
+
const node = getNodeAtPath(doc, path);
|
|
2090
|
+
return node?.kind === "seq" ? node : void 0;
|
|
2087
2091
|
}
|
|
2088
2092
|
function getObjAtPathStrict(doc, path) {
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
if (cur.kind !== "obj") return {
|
|
2093
|
-
ok: false,
|
|
2094
|
-
message: "expected object at /"
|
|
2095
|
-
};
|
|
2093
|
+
const node = getNodeAtPath(doc, path);
|
|
2094
|
+
if (!node || node.kind !== "obj") {
|
|
2095
|
+
const pointer = stringifyJsonPointer(path);
|
|
2096
2096
|
return {
|
|
2097
|
-
ok: true,
|
|
2098
|
-
obj: cur
|
|
2099
|
-
};
|
|
2100
|
-
}
|
|
2101
|
-
for (const seg of path) {
|
|
2102
|
-
if (cur.kind !== "obj") return {
|
|
2103
|
-
ok: false,
|
|
2104
|
-
message: `expected object at /${seen.join("/")}`
|
|
2105
|
-
};
|
|
2106
|
-
const entry = cur.entries.get(seg);
|
|
2107
|
-
seen.push(seg);
|
|
2108
|
-
if (!entry || entry.node.kind !== "obj") return {
|
|
2109
2097
|
ok: false,
|
|
2110
|
-
message: `expected object at
|
|
2098
|
+
message: `expected object at ${pointer === "" ? "/" : pointer}`
|
|
2111
2099
|
};
|
|
2112
|
-
cur = entry.node;
|
|
2113
2100
|
}
|
|
2114
2101
|
return {
|
|
2115
2102
|
ok: true,
|
|
2116
|
-
obj:
|
|
2103
|
+
obj: node
|
|
2117
2104
|
};
|
|
2118
2105
|
}
|
|
2119
2106
|
function ensureSeqAtPath(head, path, dotForCreate) {
|
|
@@ -2160,10 +2147,24 @@ function ensureSeqAtPath(head, path, dotForCreate) {
|
|
|
2160
2147
|
function getNodeAtPath(doc, path) {
|
|
2161
2148
|
let cur = doc.root;
|
|
2162
2149
|
for (const seg of path) {
|
|
2163
|
-
if (cur.kind
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2150
|
+
if (cur.kind === "obj") {
|
|
2151
|
+
const ent = cur.entries.get(seg);
|
|
2152
|
+
if (!ent) return;
|
|
2153
|
+
cur = ent.node;
|
|
2154
|
+
continue;
|
|
2155
|
+
}
|
|
2156
|
+
if (cur.kind === "seq") {
|
|
2157
|
+
if (!ARRAY_INDEX_TOKEN_PATTERN.test(seg)) return;
|
|
2158
|
+
const index = Number(seg);
|
|
2159
|
+
if (!Number.isSafeInteger(index)) return;
|
|
2160
|
+
const elemId = rgaIdAtIndex(cur, index);
|
|
2161
|
+
if (elemId === void 0) return;
|
|
2162
|
+
const elem = cur.elems.get(elemId);
|
|
2163
|
+
if (!elem) return;
|
|
2164
|
+
cur = elem.value;
|
|
2165
|
+
continue;
|
|
2166
|
+
}
|
|
2167
|
+
if (cur.kind === "lww") return;
|
|
2167
2168
|
}
|
|
2168
2169
|
return cur;
|
|
2169
2170
|
}
|
|
@@ -2377,38 +2378,88 @@ function getJsonAtDocPathForTest(doc, path) {
|
|
|
2377
2378
|
let cur = doc.root;
|
|
2378
2379
|
for (let i = 0; i < path.length; i++) {
|
|
2379
2380
|
const seg = path[i];
|
|
2380
|
-
|
|
2381
|
+
try {
|
|
2382
|
+
assertTraversalDepth(i + 1);
|
|
2383
|
+
} catch (error) {
|
|
2384
|
+
return {
|
|
2385
|
+
ok: false,
|
|
2386
|
+
error: error instanceof TraversalDepthError ? toDepthApplyError(error) : {
|
|
2387
|
+
ok: false,
|
|
2388
|
+
code: 409,
|
|
2389
|
+
reason: "INVALID_PATCH",
|
|
2390
|
+
message: error instanceof Error ? error.message : "invalid test path"
|
|
2391
|
+
}
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2381
2394
|
if (cur.kind === "obj") {
|
|
2382
2395
|
const ent = cur.entries.get(seg);
|
|
2383
|
-
if (!ent)
|
|
2396
|
+
if (!ent) return {
|
|
2397
|
+
ok: false,
|
|
2398
|
+
error: {
|
|
2399
|
+
ok: false,
|
|
2400
|
+
code: 409,
|
|
2401
|
+
reason: "MISSING_TARGET",
|
|
2402
|
+
message: `Missing key '${seg}'`
|
|
2403
|
+
}
|
|
2404
|
+
};
|
|
2384
2405
|
cur = ent.node;
|
|
2385
2406
|
continue;
|
|
2386
2407
|
}
|
|
2387
2408
|
if (cur.kind === "seq") {
|
|
2388
|
-
if (!ARRAY_INDEX_TOKEN_PATTERN.test(seg))
|
|
2389
|
-
|
|
2390
|
-
|
|
2409
|
+
if (!ARRAY_INDEX_TOKEN_PATTERN.test(seg)) return {
|
|
2410
|
+
ok: false,
|
|
2411
|
+
error: {
|
|
2412
|
+
ok: false,
|
|
2413
|
+
code: 409,
|
|
2414
|
+
reason: "INVALID_POINTER",
|
|
2415
|
+
message: `Expected array index, got '${seg}'`
|
|
2416
|
+
}
|
|
2417
|
+
};
|
|
2418
|
+
const idx = Number(seg);
|
|
2419
|
+
if (!Number.isSafeInteger(idx)) return {
|
|
2420
|
+
ok: false,
|
|
2421
|
+
error: {
|
|
2422
|
+
ok: false,
|
|
2423
|
+
code: 409,
|
|
2424
|
+
reason: "OUT_OF_BOUNDS",
|
|
2425
|
+
message: `Index out of bounds at '${seg}'`
|
|
2426
|
+
}
|
|
2427
|
+
};
|
|
2428
|
+
const id = rgaIdAtIndex(cur, idx);
|
|
2429
|
+
if (id === void 0) return {
|
|
2430
|
+
ok: false,
|
|
2431
|
+
error: {
|
|
2432
|
+
ok: false,
|
|
2433
|
+
code: 409,
|
|
2434
|
+
reason: "OUT_OF_BOUNDS",
|
|
2435
|
+
message: `Index out of bounds at '${seg}'`
|
|
2436
|
+
}
|
|
2437
|
+
};
|
|
2391
2438
|
cur = cur.elems.get(id).value;
|
|
2392
2439
|
continue;
|
|
2393
2440
|
}
|
|
2394
|
-
throw new Error(`Cannot traverse into non-container at '${seg}'`);
|
|
2395
|
-
}
|
|
2396
|
-
return cur.kind === "lww" ? cur.value : materialize(cur);
|
|
2397
|
-
}
|
|
2398
|
-
function applyTest(base, head, it, evalTestAgainst) {
|
|
2399
|
-
let got;
|
|
2400
|
-
try {
|
|
2401
|
-
got = getJsonAtDocPathForTest(evalTestAgainst === "head" ? head : base, it.path);
|
|
2402
|
-
} catch {
|
|
2403
2441
|
return {
|
|
2404
2442
|
ok: false,
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2443
|
+
error: {
|
|
2444
|
+
ok: false,
|
|
2445
|
+
code: 409,
|
|
2446
|
+
reason: "INVALID_TARGET",
|
|
2447
|
+
message: `Cannot traverse into non-container at '${seg}'`
|
|
2448
|
+
}
|
|
2409
2449
|
};
|
|
2410
2450
|
}
|
|
2411
|
-
|
|
2451
|
+
return {
|
|
2452
|
+
ok: true,
|
|
2453
|
+
value: cur.kind === "lww" ? cur.value : materialize(cur)
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
function applyTest(base, head, it, evalTestAgainst) {
|
|
2457
|
+
const got = getJsonAtDocPathForTest(evalTestAgainst === "head" ? head : base, it.path);
|
|
2458
|
+
if (!got.ok) return {
|
|
2459
|
+
...got.error,
|
|
2460
|
+
path: `/${it.path.join("/")}`
|
|
2461
|
+
};
|
|
2462
|
+
if (!jsonEquals(got.value, it.value)) return {
|
|
2412
2463
|
ok: false,
|
|
2413
2464
|
code: 409,
|
|
2414
2465
|
reason: "TEST_FAILED",
|
|
@@ -2713,6 +2764,46 @@ function rebaseDiffOps(path, nestedOps, out) {
|
|
|
2713
2764
|
throw new Error(`Unexpected op '${op.op}' from diffJsonPatch`);
|
|
2714
2765
|
}
|
|
2715
2766
|
}
|
|
2767
|
+
function collectLiveSequenceElements(seq) {
|
|
2768
|
+
const elems = [];
|
|
2769
|
+
const cursor = rgaCreateLinearCursor(seq);
|
|
2770
|
+
for (let elem = cursor.next(); elem; elem = cursor.next()) elems.push(elem);
|
|
2771
|
+
return elems;
|
|
2772
|
+
}
|
|
2773
|
+
function materializeSequenceWindow(elems, start, end) {
|
|
2774
|
+
const out = [];
|
|
2775
|
+
for (let i = start; i < end; i++) out.push(nodeToJsonForPatch(elems[i].value));
|
|
2776
|
+
return out;
|
|
2777
|
+
}
|
|
2778
|
+
function rebaseSequenceWindowDiffOps(path, indexOffset, nestedOps, out) {
|
|
2779
|
+
const pending = [];
|
|
2780
|
+
for (const op of nestedOps) {
|
|
2781
|
+
if (op.path === "") return false;
|
|
2782
|
+
const rebasedSegments = parseJsonPointer(op.path);
|
|
2783
|
+
const indexToken = rebasedSegments[0];
|
|
2784
|
+
if (!indexToken || !ARRAY_INDEX_TOKEN_PATTERN.test(indexToken)) return false;
|
|
2785
|
+
rebasedSegments[0] = String(Number(indexToken) + indexOffset);
|
|
2786
|
+
const rebasedPath = stringifyJsonPointer([...path, ...rebasedSegments]);
|
|
2787
|
+
if (op.op === "remove") {
|
|
2788
|
+
pending.push({
|
|
2789
|
+
op: "remove",
|
|
2790
|
+
path: rebasedPath
|
|
2791
|
+
});
|
|
2792
|
+
continue;
|
|
2793
|
+
}
|
|
2794
|
+
if (op.op === "add" || op.op === "replace") {
|
|
2795
|
+
pending.push({
|
|
2796
|
+
op: op.op,
|
|
2797
|
+
path: rebasedPath,
|
|
2798
|
+
value: op.value
|
|
2799
|
+
});
|
|
2800
|
+
continue;
|
|
2801
|
+
}
|
|
2802
|
+
return false;
|
|
2803
|
+
}
|
|
2804
|
+
out.push(...pending);
|
|
2805
|
+
return true;
|
|
2806
|
+
}
|
|
2716
2807
|
function nodesJsonEqual(baseNode, headNode, depth) {
|
|
2717
2808
|
assertTraversalDepth(depth);
|
|
2718
2809
|
if (baseNode === headNode) return true;
|
|
@@ -2837,6 +2928,35 @@ function diffObjectNodes(path, baseNode, headNode, options, ops, depth) {
|
|
|
2837
2928
|
headIndex += 1;
|
|
2838
2929
|
}
|
|
2839
2930
|
}
|
|
2931
|
+
function diffSequenceNodes(path, baseNode, headSeq, options, ops, depth) {
|
|
2932
|
+
if ((options.arrayStrategy ?? "lcs") === "atomic") {
|
|
2933
|
+
rebaseDiffOps(path, diffJsonPatch(materialize(baseNode), materialize(headSeq), options), ops);
|
|
2934
|
+
return;
|
|
2935
|
+
}
|
|
2936
|
+
const baseElems = collectLiveSequenceElements(baseNode);
|
|
2937
|
+
const headElems = collectLiveSequenceElements(headSeq);
|
|
2938
|
+
const sharedLength = Math.min(baseElems.length, headElems.length);
|
|
2939
|
+
let prefixLength = 0;
|
|
2940
|
+
while (prefixLength < sharedLength && nodesJsonEqual(baseElems[prefixLength].value, headElems[prefixLength].value, depth + 1)) prefixLength += 1;
|
|
2941
|
+
if (prefixLength === baseElems.length && prefixLength === headElems.length) return;
|
|
2942
|
+
let baseEnd = baseElems.length;
|
|
2943
|
+
let headEnd = headElems.length;
|
|
2944
|
+
while (baseEnd > prefixLength && headEnd > prefixLength && nodesJsonEqual(baseElems[baseEnd - 1].value, headElems[headEnd - 1].value, depth + 1)) {
|
|
2945
|
+
baseEnd -= 1;
|
|
2946
|
+
headEnd -= 1;
|
|
2947
|
+
}
|
|
2948
|
+
const unmatchedBaseLength = baseEnd - prefixLength;
|
|
2949
|
+
const unmatchedHeadLength = headEnd - prefixLength;
|
|
2950
|
+
if (unmatchedBaseLength === 1 && unmatchedHeadLength === 1) {
|
|
2951
|
+
path.push(String(prefixLength));
|
|
2952
|
+
diffNodeToPatch(path, baseElems[prefixLength].value, headElems[prefixLength].value, options, ops, depth + 1);
|
|
2953
|
+
path.pop();
|
|
2954
|
+
return;
|
|
2955
|
+
}
|
|
2956
|
+
const seqOps = diffJsonPatch(materializeSequenceWindow(baseElems, prefixLength, baseEnd), materializeSequenceWindow(headElems, prefixLength, headEnd), options);
|
|
2957
|
+
if (rebaseSequenceWindowDiffOps(path, prefixLength, seqOps, ops)) return;
|
|
2958
|
+
rebaseDiffOps(path, diffJsonPatch(materialize(baseNode), materialize(headSeq), options), ops);
|
|
2959
|
+
}
|
|
2840
2960
|
function diffNodeToPatch(path, baseNode, headNode, options, ops, depth) {
|
|
2841
2961
|
assertTraversalDepth(depth);
|
|
2842
2962
|
if (baseNode === headNode) return;
|
|
@@ -2862,8 +2982,7 @@ function diffNodeToPatch(path, baseNode, headNode, options, ops, depth) {
|
|
|
2862
2982
|
diffObjectNodes(path, baseNode, headNode, options, ops, depth);
|
|
2863
2983
|
return;
|
|
2864
2984
|
}
|
|
2865
|
-
|
|
2866
|
-
rebaseDiffOps(path, diffJsonPatch(materialize(baseNode), materialize(headSeq), options), ops);
|
|
2985
|
+
diffSequenceNodes(path, baseNode, headNode, options, ops, depth);
|
|
2867
2986
|
}
|
|
2868
2987
|
/**
|
|
2869
2988
|
* Generate a JSON Patch delta between two CRDT documents.
|
|
@@ -2905,7 +3024,7 @@ function jsonPatchToCrdtInternal(options) {
|
|
|
2905
3024
|
}
|
|
2906
3025
|
return applyIntentsToCrdt(options.base, options.head, intents, options.newDot, evalTestAgainst, options.bumpCounterAbove, { strictParents: options.strictParents });
|
|
2907
3026
|
}
|
|
2908
|
-
|
|
3027
|
+
const shadowBase = evalTestAgainst === "base" ? cloneDoc(options.base) : null;
|
|
2909
3028
|
let shadowCtr = 0;
|
|
2910
3029
|
const shadowDot = () => ({
|
|
2911
3030
|
actor: "__shadow__",
|
|
@@ -2914,60 +3033,340 @@ function jsonPatchToCrdtInternal(options) {
|
|
|
2914
3033
|
const shadowBump = (ctr) => {
|
|
2915
3034
|
if (shadowCtr < ctr) shadowCtr = ctr;
|
|
2916
3035
|
};
|
|
2917
|
-
const
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
3036
|
+
const session = { pointerCache: /* @__PURE__ */ new Map() };
|
|
3037
|
+
for (const [opIndex, op] of options.patch.entries()) {
|
|
3038
|
+
const step = applySequentialPatchOp(options, evalTestAgainst === "base" ? shadowBase : options.head, op, opIndex, evalTestAgainst, shadowDot, shadowBump, session);
|
|
3039
|
+
if (!step.ok) return step;
|
|
3040
|
+
}
|
|
3041
|
+
return { ok: true };
|
|
3042
|
+
}
|
|
3043
|
+
function applySequentialPatchOp(options, compileBase, op, opIndex, evalTestAgainst, shadowDot, shadowBump, session) {
|
|
3044
|
+
if (op.op === "move") {
|
|
3045
|
+
if (op.from === op.path) {
|
|
3046
|
+
const pathCheck = resolveValueAtPointerInDoc$1(compileBase, op.from, opIndex, session.pointerCache);
|
|
3047
|
+
if (!pathCheck.ok) return pathCheck;
|
|
3048
|
+
return { ok: true };
|
|
3049
|
+
}
|
|
3050
|
+
const fromResolved = resolveValueAtPointerInDoc$1(compileBase, op.from, opIndex, session.pointerCache);
|
|
3051
|
+
if (!fromResolved.ok) return fromResolved;
|
|
3052
|
+
const removeStep = applySingleSequentialPatchStep(options, compileBase, {
|
|
3053
|
+
op: "remove",
|
|
3054
|
+
path: op.from
|
|
3055
|
+
}, opIndex, evalTestAgainst, shadowDot, shadowBump, session);
|
|
3056
|
+
if (!removeStep.ok) return removeStep;
|
|
3057
|
+
return applySingleSequentialPatchStep(options, compileBase, {
|
|
3058
|
+
op: "add",
|
|
3059
|
+
path: op.path,
|
|
3060
|
+
value: structuredClone(fromResolved.value)
|
|
3061
|
+
}, opIndex, evalTestAgainst, shadowDot, shadowBump, session);
|
|
3062
|
+
}
|
|
3063
|
+
if (op.op === "copy") {
|
|
3064
|
+
const fromResolved = resolveValueAtPointerInDoc$1(compileBase, op.from, opIndex, session.pointerCache);
|
|
3065
|
+
if (!fromResolved.ok) return fromResolved;
|
|
3066
|
+
return applySingleSequentialPatchStep(options, compileBase, {
|
|
3067
|
+
op: "add",
|
|
3068
|
+
path: op.path,
|
|
3069
|
+
value: structuredClone(fromResolved.value)
|
|
3070
|
+
}, opIndex, evalTestAgainst, shadowDot, shadowBump, session);
|
|
3071
|
+
}
|
|
3072
|
+
return applySingleSequentialPatchStep(options, compileBase, op, opIndex, evalTestAgainst, shadowDot, shadowBump, session);
|
|
3073
|
+
}
|
|
3074
|
+
function applySingleSequentialPatchStep(options, compileBase, op, opIndex, evalTestAgainst, shadowDot, shadowBump, session) {
|
|
3075
|
+
const compiled = compilePreparedSingleIntentFromDoc$1(compileBase, op, session.pointerCache, opIndex);
|
|
3076
|
+
if (!compiled.ok) return compiled;
|
|
3077
|
+
const headStep = applyIntentsToCrdt(compileBase, options.head, compiled.intents, options.newDot, evalTestAgainst, options.bumpCounterAbove, { strictParents: options.strictParents });
|
|
3078
|
+
if (!headStep.ok) return withOpIndex$1(headStep, opIndex);
|
|
3079
|
+
if (op.op === "test") return { ok: true };
|
|
3080
|
+
if (evalTestAgainst === "head") return { ok: true };
|
|
3081
|
+
const shadowStep = applyIntentsToCrdt(compileBase, compileBase, compiled.intents, shadowDot, "base", shadowBump, { strictParents: options.strictParents });
|
|
3082
|
+
if (!shadowStep.ok) return withOpIndex$1(shadowStep, opIndex);
|
|
3083
|
+
return { ok: true };
|
|
3084
|
+
}
|
|
3085
|
+
function resolveValueAtPointerInDoc$1(doc, pointer, opIndex, pointerCache) {
|
|
3086
|
+
const parsedPath = parsePointerWithCache$1(pointer, pointerCache, opIndex);
|
|
3087
|
+
if (!parsedPath.ok) return parsedPath;
|
|
3088
|
+
const resolved = resolveNodeAtPath$1(doc.root, parsedPath.path);
|
|
3089
|
+
if (!resolved.ok) return {
|
|
3090
|
+
ok: false,
|
|
3091
|
+
...resolved.error,
|
|
3092
|
+
path: pointer,
|
|
3093
|
+
opIndex
|
|
3094
|
+
};
|
|
3095
|
+
return {
|
|
3096
|
+
ok: true,
|
|
3097
|
+
value: nodeToJsonForPatch(resolved.node)
|
|
3098
|
+
};
|
|
3099
|
+
}
|
|
3100
|
+
function compilePreparedSingleIntentFromDoc$1(baseDoc, op, pointerCache, opIndex) {
|
|
3101
|
+
const parsedPath = parsePointerWithCache$1(op.path, pointerCache, opIndex);
|
|
3102
|
+
if (!parsedPath.ok) return parsedPath;
|
|
3103
|
+
const path = parsedPath.path;
|
|
3104
|
+
if (op.op === "test") return {
|
|
3105
|
+
ok: true,
|
|
3106
|
+
intents: [{
|
|
3107
|
+
t: "Test",
|
|
3108
|
+
path,
|
|
3109
|
+
value: op.value
|
|
3110
|
+
}]
|
|
3111
|
+
};
|
|
3112
|
+
if (path.length === 0) {
|
|
3113
|
+
if (op.op === "remove") return {
|
|
3114
|
+
ok: false,
|
|
3115
|
+
code: 409,
|
|
3116
|
+
reason: "INVALID_TARGET",
|
|
3117
|
+
message: "remove at root path is not supported in RFC-compliant mode",
|
|
3118
|
+
path: op.path,
|
|
3119
|
+
opIndex
|
|
3120
|
+
};
|
|
3121
|
+
return {
|
|
3122
|
+
ok: true,
|
|
3123
|
+
intents: [{
|
|
3124
|
+
t: "ObjSet",
|
|
3125
|
+
path: [],
|
|
3126
|
+
key: ROOT_KEY,
|
|
3127
|
+
value: op.value
|
|
3128
|
+
}]
|
|
3129
|
+
};
|
|
3130
|
+
}
|
|
3131
|
+
const parentPath = path.slice(0, -1);
|
|
3132
|
+
const parentPointer = stringifyJsonPointer(parentPath);
|
|
3133
|
+
const key = path[path.length - 1];
|
|
3134
|
+
const resolvedParent = parentPath.length === 0 ? {
|
|
3135
|
+
ok: true,
|
|
3136
|
+
node: baseDoc.root
|
|
3137
|
+
} : resolveNodeAtPath$1(baseDoc.root, parentPath);
|
|
3138
|
+
if (!resolvedParent.ok) return {
|
|
3139
|
+
ok: false,
|
|
3140
|
+
...resolvedParent.error,
|
|
3141
|
+
path: parentPointer,
|
|
3142
|
+
opIndex
|
|
3143
|
+
};
|
|
3144
|
+
const parentNode = resolvedParent.node;
|
|
3145
|
+
if (parentNode.kind === "seq") {
|
|
3146
|
+
const parsedIndex = parseArrayIndexTokenForDoc$1(key, op.op, op.path, opIndex);
|
|
3147
|
+
if (!parsedIndex.ok) return parsedIndex;
|
|
3148
|
+
const boundedIndex = validateArrayIndexBounds$1(parsedIndex.index, op.op, rgaLength(parentNode), op.path, opIndex);
|
|
3149
|
+
if (!boundedIndex.ok) return boundedIndex;
|
|
3150
|
+
if (op.op === "add") return {
|
|
3151
|
+
ok: true,
|
|
3152
|
+
intents: [{
|
|
3153
|
+
t: "ArrInsert",
|
|
3154
|
+
path: parentPath,
|
|
3155
|
+
index: boundedIndex.index,
|
|
3156
|
+
value: op.value
|
|
3157
|
+
}]
|
|
3158
|
+
};
|
|
3159
|
+
if (op.op === "remove") return {
|
|
3160
|
+
ok: true,
|
|
3161
|
+
intents: [{
|
|
3162
|
+
t: "ArrDelete",
|
|
3163
|
+
path: parentPath,
|
|
3164
|
+
index: boundedIndex.index
|
|
3165
|
+
}]
|
|
3166
|
+
};
|
|
3167
|
+
return {
|
|
3168
|
+
ok: true,
|
|
3169
|
+
intents: [{
|
|
3170
|
+
t: "ArrReplace",
|
|
3171
|
+
path: parentPath,
|
|
3172
|
+
index: boundedIndex.index,
|
|
3173
|
+
value: op.value
|
|
3174
|
+
}]
|
|
3175
|
+
};
|
|
3176
|
+
}
|
|
3177
|
+
if (parentNode.kind !== "obj") return {
|
|
3178
|
+
ok: false,
|
|
3179
|
+
code: 409,
|
|
3180
|
+
reason: "INVALID_TARGET",
|
|
3181
|
+
message: `expected object or array parent at ${parentPointer}`,
|
|
3182
|
+
path: parentPointer,
|
|
3183
|
+
opIndex
|
|
3184
|
+
};
|
|
3185
|
+
if (key === "__proto__") return {
|
|
3186
|
+
ok: false,
|
|
3187
|
+
code: 409,
|
|
3188
|
+
reason: "INVALID_POINTER",
|
|
3189
|
+
message: `unsafe object key at ${op.path}`,
|
|
3190
|
+
path: op.path,
|
|
3191
|
+
opIndex
|
|
3192
|
+
};
|
|
3193
|
+
const entry = parentNode.entries.get(key);
|
|
3194
|
+
if ((op.op === "replace" || op.op === "remove") && !entry) return {
|
|
3195
|
+
ok: false,
|
|
3196
|
+
code: 409,
|
|
3197
|
+
reason: "MISSING_TARGET",
|
|
3198
|
+
message: `missing key ${key} at ${parentPointer}`,
|
|
3199
|
+
path: op.path,
|
|
3200
|
+
opIndex
|
|
3201
|
+
};
|
|
3202
|
+
if (op.op === "remove") return {
|
|
3203
|
+
ok: true,
|
|
3204
|
+
intents: [{
|
|
3205
|
+
t: "ObjRemove",
|
|
3206
|
+
path: parentPath,
|
|
3207
|
+
key
|
|
3208
|
+
}]
|
|
3209
|
+
};
|
|
3210
|
+
return {
|
|
3211
|
+
ok: true,
|
|
3212
|
+
intents: [{
|
|
3213
|
+
t: "ObjSet",
|
|
3214
|
+
path: parentPath,
|
|
3215
|
+
key,
|
|
3216
|
+
value: op.value,
|
|
3217
|
+
mode: op.op
|
|
3218
|
+
}]
|
|
3219
|
+
};
|
|
3220
|
+
}
|
|
3221
|
+
function parsePointerWithCache$1(pointer, pointerCache, opIndex) {
|
|
3222
|
+
const cachedPath = pointerCache.get(pointer);
|
|
3223
|
+
if (cachedPath !== void 0) return {
|
|
3224
|
+
ok: true,
|
|
3225
|
+
path: cachedPath.slice()
|
|
3226
|
+
};
|
|
3227
|
+
try {
|
|
3228
|
+
const parsedPath = parseJsonPointer(pointer);
|
|
3229
|
+
pointerCache.set(pointer, parsedPath);
|
|
3230
|
+
return {
|
|
3231
|
+
ok: true,
|
|
3232
|
+
path: parsedPath.slice()
|
|
3233
|
+
};
|
|
3234
|
+
} catch (error) {
|
|
3235
|
+
return {
|
|
3236
|
+
ok: false,
|
|
3237
|
+
code: 409,
|
|
3238
|
+
reason: "INVALID_POINTER",
|
|
3239
|
+
message: error instanceof Error ? error.message : "invalid pointer",
|
|
3240
|
+
path: pointer,
|
|
3241
|
+
opIndex
|
|
3242
|
+
};
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
function resolveNodeAtPath$1(root, path) {
|
|
3246
|
+
let current = root;
|
|
3247
|
+
for (const segment of path) {
|
|
3248
|
+
if (current.kind === "obj") {
|
|
3249
|
+
const entry = current.entries.get(segment);
|
|
3250
|
+
if (!entry) return {
|
|
3251
|
+
ok: false,
|
|
3252
|
+
error: {
|
|
3253
|
+
code: 409,
|
|
3254
|
+
reason: "MISSING_PARENT",
|
|
3255
|
+
message: `Missing key '${segment}'`
|
|
3256
|
+
}
|
|
3257
|
+
};
|
|
3258
|
+
current = entry.node;
|
|
3259
|
+
continue;
|
|
2924
3260
|
}
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
};
|
|
2933
|
-
for (let opIndex = 0; opIndex < options.patch.length; opIndex++) {
|
|
2934
|
-
const op = options.patch[opIndex];
|
|
2935
|
-
if (op.op === "move") {
|
|
2936
|
-
const baseJson = materialize(shadowBase.root);
|
|
2937
|
-
let fromValue;
|
|
2938
|
-
try {
|
|
2939
|
-
fromValue = structuredClone(getAtJson(baseJson, parseJsonPointer(op.from)));
|
|
2940
|
-
} catch {
|
|
2941
|
-
try {
|
|
2942
|
-
compileJsonPatchToIntent(baseJson, [{
|
|
2943
|
-
op: "remove",
|
|
2944
|
-
path: op.from
|
|
2945
|
-
}], { semantics: "sequential" });
|
|
2946
|
-
} catch (error) {
|
|
2947
|
-
return withOpIndex(toApplyError$1(error), opIndex);
|
|
3261
|
+
if (current.kind === "seq") {
|
|
3262
|
+
if (!ARRAY_INDEX_TOKEN_PATTERN.test(segment)) return {
|
|
3263
|
+
ok: false,
|
|
3264
|
+
error: {
|
|
3265
|
+
code: 409,
|
|
3266
|
+
reason: "INVALID_POINTER",
|
|
3267
|
+
message: `Expected array index, got '${segment}'`
|
|
2948
3268
|
}
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
if (
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
3269
|
+
};
|
|
3270
|
+
const index = Number(segment);
|
|
3271
|
+
if (!Number.isSafeInteger(index)) return {
|
|
3272
|
+
ok: false,
|
|
3273
|
+
error: {
|
|
3274
|
+
code: 409,
|
|
3275
|
+
reason: "OUT_OF_BOUNDS",
|
|
3276
|
+
message: `Index out of bounds at '${segment}'`
|
|
3277
|
+
}
|
|
3278
|
+
};
|
|
3279
|
+
const elemId = rgaIdAtIndex(current, index);
|
|
3280
|
+
if (elemId === void 0) return {
|
|
3281
|
+
ok: false,
|
|
3282
|
+
error: {
|
|
3283
|
+
code: 409,
|
|
3284
|
+
reason: "OUT_OF_BOUNDS",
|
|
3285
|
+
message: `Index out of bounds at '${segment}'`
|
|
3286
|
+
}
|
|
3287
|
+
};
|
|
3288
|
+
current = current.elems.get(elemId).value;
|
|
2963
3289
|
continue;
|
|
2964
3290
|
}
|
|
2965
|
-
|
|
2966
|
-
|
|
3291
|
+
return {
|
|
3292
|
+
ok: false,
|
|
3293
|
+
error: {
|
|
3294
|
+
code: 409,
|
|
3295
|
+
reason: "INVALID_TARGET",
|
|
3296
|
+
message: `Cannot traverse into non-container at '${segment}'`
|
|
3297
|
+
}
|
|
3298
|
+
};
|
|
2967
3299
|
}
|
|
2968
|
-
return {
|
|
3300
|
+
return {
|
|
3301
|
+
ok: true,
|
|
3302
|
+
node: current
|
|
3303
|
+
};
|
|
2969
3304
|
}
|
|
2970
|
-
function
|
|
3305
|
+
function parseArrayIndexTokenForDoc$1(token, op, path, opIndex) {
|
|
3306
|
+
if (token === "-") {
|
|
3307
|
+
if (op !== "add") return {
|
|
3308
|
+
ok: false,
|
|
3309
|
+
code: 409,
|
|
3310
|
+
reason: "INVALID_POINTER",
|
|
3311
|
+
message: `'-' index is only valid for add at ${path}`,
|
|
3312
|
+
path,
|
|
3313
|
+
opIndex
|
|
3314
|
+
};
|
|
3315
|
+
return {
|
|
3316
|
+
ok: true,
|
|
3317
|
+
index: Number.POSITIVE_INFINITY
|
|
3318
|
+
};
|
|
3319
|
+
}
|
|
3320
|
+
if (!ARRAY_INDEX_TOKEN_PATTERN.test(token)) return {
|
|
3321
|
+
ok: false,
|
|
3322
|
+
code: 409,
|
|
3323
|
+
reason: "INVALID_POINTER",
|
|
3324
|
+
message: `expected array index at ${path}`,
|
|
3325
|
+
path,
|
|
3326
|
+
opIndex
|
|
3327
|
+
};
|
|
3328
|
+
const index = Number(token);
|
|
3329
|
+
if (!Number.isSafeInteger(index)) return {
|
|
3330
|
+
ok: false,
|
|
3331
|
+
code: 409,
|
|
3332
|
+
reason: "OUT_OF_BOUNDS",
|
|
3333
|
+
message: `array index is too large at ${path}`,
|
|
3334
|
+
path,
|
|
3335
|
+
opIndex
|
|
3336
|
+
};
|
|
3337
|
+
return {
|
|
3338
|
+
ok: true,
|
|
3339
|
+
index
|
|
3340
|
+
};
|
|
3341
|
+
}
|
|
3342
|
+
function validateArrayIndexBounds$1(index, op, arrLength, path, opIndex) {
|
|
3343
|
+
if (op === "add") {
|
|
3344
|
+
if (index === Number.POSITIVE_INFINITY) return {
|
|
3345
|
+
ok: true,
|
|
3346
|
+
index
|
|
3347
|
+
};
|
|
3348
|
+
if (index > arrLength) return {
|
|
3349
|
+
ok: false,
|
|
3350
|
+
code: 409,
|
|
3351
|
+
reason: "OUT_OF_BOUNDS",
|
|
3352
|
+
message: `index out of bounds at ${path}; expected 0..${arrLength}`,
|
|
3353
|
+
path,
|
|
3354
|
+
opIndex
|
|
3355
|
+
};
|
|
3356
|
+
} else if (index >= arrLength) return {
|
|
3357
|
+
ok: false,
|
|
3358
|
+
code: 409,
|
|
3359
|
+
reason: "OUT_OF_BOUNDS",
|
|
3360
|
+
message: `index out of bounds at ${path}; expected 0..${Math.max(arrLength - 1, 0)}`,
|
|
3361
|
+
path,
|
|
3362
|
+
opIndex
|
|
3363
|
+
};
|
|
3364
|
+
return {
|
|
3365
|
+
ok: true,
|
|
3366
|
+
index
|
|
3367
|
+
};
|
|
3368
|
+
}
|
|
3369
|
+
function withOpIndex$1(error, opIndex) {
|
|
2971
3370
|
if (error.opIndex !== void 0) return error;
|
|
2972
3371
|
return {
|
|
2973
3372
|
...error,
|
|
@@ -3130,10 +3529,13 @@ function tryApplyPatchInPlace(state, patch, options = {}) {
|
|
|
3130
3529
|
* Does not mutate caller-provided values.
|
|
3131
3530
|
*/
|
|
3132
3531
|
function validateJsonPatch(base, patch, options = {}) {
|
|
3133
|
-
const result =
|
|
3532
|
+
const result = tryApplyPatchInPlace(createState(base, {
|
|
3134
3533
|
actor: "__validate__",
|
|
3135
3534
|
jsonValidation: options.jsonValidation
|
|
3136
|
-
}), patch,
|
|
3535
|
+
}), patch, {
|
|
3536
|
+
...options,
|
|
3537
|
+
atomic: false
|
|
3538
|
+
});
|
|
3137
3539
|
if (!result.ok) return {
|
|
3138
3540
|
ok: false,
|
|
3139
3541
|
error: result.error
|
|
@@ -3182,16 +3584,11 @@ function toApplyPatchOptionsForActor(options) {
|
|
|
3182
3584
|
} : void 0
|
|
3183
3585
|
};
|
|
3184
3586
|
}
|
|
3185
|
-
function applyPatchInternal(state, patch, options,
|
|
3587
|
+
function applyPatchInternal(state, patch, options, _execution) {
|
|
3186
3588
|
const preparedPatch = preparePatchPayloadsSafe(patch, options.jsonValidation ?? "none");
|
|
3187
3589
|
if (!preparedPatch.ok) return preparedPatch;
|
|
3188
3590
|
const runtimePatch = preparedPatch.patch;
|
|
3189
3591
|
if ((options.semantics ?? "sequential") === "sequential") {
|
|
3190
|
-
if (!options.base && execution === "batch") {
|
|
3191
|
-
const compiled = compilePreparedIntents(materialize(state.doc.root), runtimePatch, "sequential");
|
|
3192
|
-
if (!compiled.ok) return compiled;
|
|
3193
|
-
return applyIntentsToCrdt(state.doc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
|
|
3194
|
-
}
|
|
3195
3592
|
const explicitBaseState = options.base ? {
|
|
3196
3593
|
doc: cloneDoc(options.base.doc),
|
|
3197
3594
|
clock: createClock("__base__", 0)
|
|
@@ -3245,10 +3642,10 @@ function applySinglePatchOpSequentialStep(state, baseDoc, op, options, explicitB
|
|
|
3245
3642
|
const compiled = compilePreparedSingleIntentFromDoc(baseDoc, op, session.pointerCache, opIndex);
|
|
3246
3643
|
if (!compiled.ok) return compiled;
|
|
3247
3644
|
const headStep = applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
|
|
3248
|
-
if (!headStep.ok) return headStep;
|
|
3645
|
+
if (!headStep.ok) return withOpIndex(headStep, opIndex);
|
|
3249
3646
|
if (explicitBaseState && op.op !== "test") {
|
|
3250
3647
|
const shadowStep = applyIntentsToCrdt(explicitBaseState.doc, explicitBaseState.doc, compiled.intents, () => explicitBaseState.clock.next(), "base", (ctr) => bumpClockCounter(explicitBaseState, ctr), { strictParents: options.strictParents });
|
|
3251
|
-
if (!shadowStep.ok) return shadowStep;
|
|
3648
|
+
if (!shadowStep.ok) return withOpIndex(shadowStep, opIndex);
|
|
3252
3649
|
}
|
|
3253
3650
|
return { ok: true };
|
|
3254
3651
|
}
|
|
@@ -3256,7 +3653,7 @@ function applySinglePatchOpExplicitShadowStep(explicitBaseState, op, options, op
|
|
|
3256
3653
|
const compiled = compilePreparedSingleIntentFromDoc(explicitBaseState.doc, op, session.pointerCache, opIndex);
|
|
3257
3654
|
if (!compiled.ok) return compiled;
|
|
3258
3655
|
const shadowStep = applyIntentsToCrdt(explicitBaseState.doc, explicitBaseState.doc, compiled.intents, () => explicitBaseState.clock.next(), "base", (ctr) => bumpClockCounter(explicitBaseState, ctr), { strictParents: options.strictParents });
|
|
3259
|
-
if (!shadowStep.ok) return shadowStep;
|
|
3656
|
+
if (!shadowStep.ok) return withOpIndex(shadowStep, opIndex);
|
|
3260
3657
|
return { ok: true };
|
|
3261
3658
|
}
|
|
3262
3659
|
function resolveValueAtPointerInDoc(doc, pointer, opIndex, pointerCache) {
|
|
@@ -3619,6 +4016,13 @@ function toApplyError(error) {
|
|
|
3619
4016
|
message: error instanceof Error ? error.message : "failed to compile patch"
|
|
3620
4017
|
};
|
|
3621
4018
|
}
|
|
4019
|
+
function withOpIndex(error, opIndex) {
|
|
4020
|
+
if (error.opIndex !== void 0) return error;
|
|
4021
|
+
return {
|
|
4022
|
+
...error,
|
|
4023
|
+
opIndex
|
|
4024
|
+
};
|
|
4025
|
+
}
|
|
3622
4026
|
function toPointerParseApplyError(error, pointer, opIndex) {
|
|
3623
4027
|
return {
|
|
3624
4028
|
ok: false,
|
|
@@ -3992,20 +4396,23 @@ function mergeDoc(a, b, options = {}) {
|
|
|
3992
4396
|
/** Non-throwing `mergeDoc` variant with structured conflict details. */
|
|
3993
4397
|
function tryMergeDoc(a, b, options = {}) {
|
|
3994
4398
|
try {
|
|
3995
|
-
const
|
|
3996
|
-
if (
|
|
3997
|
-
|
|
3998
|
-
|
|
4399
|
+
const config = { unrelatedArrays: resolveUnrelatedArraysStrategy(options) };
|
|
4400
|
+
if (config.unrelatedArrays === "reject") {
|
|
4401
|
+
const mismatchPath = findSeqLineageMismatch(a.root, b.root, []);
|
|
4402
|
+
if (mismatchPath !== null) return {
|
|
3999
4403
|
ok: false,
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4404
|
+
error: {
|
|
4405
|
+
ok: false,
|
|
4406
|
+
code: 409,
|
|
4407
|
+
reason: "LINEAGE_MISMATCH",
|
|
4408
|
+
message: `merge requires shared array origin at ${mismatchPath}`,
|
|
4409
|
+
path: mismatchPath
|
|
4410
|
+
}
|
|
4411
|
+
};
|
|
4412
|
+
}
|
|
4006
4413
|
return {
|
|
4007
4414
|
ok: true,
|
|
4008
|
-
doc:
|
|
4415
|
+
doc: mergeDocRoot(a.root, b.root, config).doc
|
|
4009
4416
|
};
|
|
4010
4417
|
} catch (error) {
|
|
4011
4418
|
if (error instanceof SharedElementMetadataMismatchError) return {
|
|
@@ -4031,7 +4438,7 @@ function tryMergeDoc(a, b, options = {}) {
|
|
|
4031
4438
|
* The merged clock keeps a stable actor identity:
|
|
4032
4439
|
* - defaults to the actor from the first argument (`a`)
|
|
4033
4440
|
* - can be overridden via `options.actor`
|
|
4034
|
-
* - optional `options.
|
|
4441
|
+
* - optional `options.unrelatedArrays` controls the merge strategy for non-overlapping sequences
|
|
4035
4442
|
*
|
|
4036
4443
|
* The merged counter is lifted to the highest counter already observed for
|
|
4037
4444
|
* that actor across both input clocks and the merged document dots.
|
|
@@ -4043,17 +4450,51 @@ function mergeState(a, b, options = {}) {
|
|
|
4043
4450
|
}
|
|
4044
4451
|
/** Non-throwing `mergeState` variant with structured conflict details. */
|
|
4045
4452
|
function tryMergeState(a, b, options = {}) {
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
doc,
|
|
4054
|
-
|
|
4453
|
+
try {
|
|
4454
|
+
const actor = options.actor ?? a.clock.actor;
|
|
4455
|
+
const config = {
|
|
4456
|
+
actor,
|
|
4457
|
+
unrelatedArrays: resolveUnrelatedArraysStrategy(options)
|
|
4458
|
+
};
|
|
4459
|
+
if (config.unrelatedArrays === "reject") {
|
|
4460
|
+
const mismatchPath = findSeqLineageMismatch(a.doc.root, b.doc.root, []);
|
|
4461
|
+
if (mismatchPath !== null) return {
|
|
4462
|
+
ok: false,
|
|
4463
|
+
error: {
|
|
4464
|
+
ok: false,
|
|
4465
|
+
code: 409,
|
|
4466
|
+
reason: "LINEAGE_MISMATCH",
|
|
4467
|
+
message: `merge requires shared array origin at ${mismatchPath}`,
|
|
4468
|
+
path: mismatchPath
|
|
4469
|
+
}
|
|
4470
|
+
};
|
|
4055
4471
|
}
|
|
4056
|
-
|
|
4472
|
+
const merged = mergeDocRoot(a.doc.root, b.doc.root, config);
|
|
4473
|
+
const ctr = maxObservedCtrForActor(merged.maxObservedCtr, actor, a, b);
|
|
4474
|
+
return {
|
|
4475
|
+
ok: true,
|
|
4476
|
+
state: {
|
|
4477
|
+
doc: merged.doc,
|
|
4478
|
+
clock: createClock(actor, ctr)
|
|
4479
|
+
}
|
|
4480
|
+
};
|
|
4481
|
+
} catch (error) {
|
|
4482
|
+
if (error instanceof SharedElementMetadataMismatchError) return {
|
|
4483
|
+
ok: false,
|
|
4484
|
+
error: {
|
|
4485
|
+
ok: false,
|
|
4486
|
+
code: 409,
|
|
4487
|
+
reason: "LINEAGE_MISMATCH",
|
|
4488
|
+
message: error.message,
|
|
4489
|
+
path: error.path
|
|
4490
|
+
}
|
|
4491
|
+
};
|
|
4492
|
+
if (error instanceof TraversalDepthError) return {
|
|
4493
|
+
ok: false,
|
|
4494
|
+
error: toDepthApplyError(error)
|
|
4495
|
+
};
|
|
4496
|
+
throw error;
|
|
4497
|
+
}
|
|
4057
4498
|
}
|
|
4058
4499
|
function findSeqLineageMismatch(a, b, path) {
|
|
4059
4500
|
const stack = [{
|
|
@@ -4096,99 +4537,166 @@ function findSeqLineageMismatch(a, b, path) {
|
|
|
4096
4537
|
}
|
|
4097
4538
|
return null;
|
|
4098
4539
|
}
|
|
4099
|
-
function
|
|
4100
|
-
|
|
4540
|
+
function mergeDocRoot(a, b, config) {
|
|
4541
|
+
const merged = mergeNodeAtDepth(a, b, 0, [], config);
|
|
4542
|
+
return {
|
|
4543
|
+
doc: { root: merged.node },
|
|
4544
|
+
maxObservedCtr: merged.maxObservedCtr
|
|
4545
|
+
};
|
|
4546
|
+
}
|
|
4547
|
+
function resolveUnrelatedArraysStrategy(options) {
|
|
4548
|
+
if (options.unrelatedArrays !== void 0) return options.unrelatedArrays;
|
|
4549
|
+
if (options.requireSharedOrigin === false) return "unsafe-union";
|
|
4550
|
+
return "reject";
|
|
4551
|
+
}
|
|
4552
|
+
function maxObservedCtrForActor(docObservedCtr, actor, a, b) {
|
|
4553
|
+
let best = docObservedCtr;
|
|
4101
4554
|
if (a.clock.actor === actor && a.clock.ctr > best) best = a.clock.ctr;
|
|
4102
4555
|
if (b.clock.actor === actor && b.clock.ctr > best) best = b.clock.ctr;
|
|
4103
4556
|
return best;
|
|
4104
4557
|
}
|
|
4105
4558
|
function repDot(node) {
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4559
|
+
let best = {
|
|
4560
|
+
actor: "",
|
|
4561
|
+
ctr: 0
|
|
4562
|
+
};
|
|
4563
|
+
const stack = [{
|
|
4564
|
+
node,
|
|
4565
|
+
depth: 0
|
|
4566
|
+
}];
|
|
4567
|
+
while (stack.length > 0) {
|
|
4568
|
+
const frame = stack.pop();
|
|
4569
|
+
assertTraversalDepth(frame.depth);
|
|
4570
|
+
switch (frame.node.kind) {
|
|
4571
|
+
case "lww":
|
|
4572
|
+
if (compareDot(frame.node.dot, best) > 0) best = frame.node.dot;
|
|
4573
|
+
break;
|
|
4574
|
+
case "obj":
|
|
4575
|
+
for (const entry of frame.node.entries.values()) {
|
|
4576
|
+
if (compareDot(entry.dot, best) > 0) best = entry.dot;
|
|
4577
|
+
stack.push({
|
|
4578
|
+
node: entry.node,
|
|
4579
|
+
depth: frame.depth + 1
|
|
4580
|
+
});
|
|
4581
|
+
}
|
|
4582
|
+
for (const tombstone of frame.node.tombstone.values()) if (compareDot(tombstone, best) > 0) best = tombstone;
|
|
4583
|
+
break;
|
|
4584
|
+
case "seq":
|
|
4585
|
+
for (const elem of frame.node.elems.values()) {
|
|
4586
|
+
if (compareDot(elem.insDot, best) > 0) best = elem.insDot;
|
|
4587
|
+
if (elem.delDot && compareDot(elem.delDot, best) > 0) best = elem.delDot;
|
|
4588
|
+
stack.push({
|
|
4589
|
+
node: elem.value,
|
|
4590
|
+
depth: frame.depth + 1
|
|
4591
|
+
});
|
|
4592
|
+
}
|
|
4593
|
+
break;
|
|
4124
4594
|
}
|
|
4125
4595
|
}
|
|
4596
|
+
return best;
|
|
4126
4597
|
}
|
|
4127
|
-
function
|
|
4128
|
-
return mergeNodeAtDepth(a, b, 0, []);
|
|
4129
|
-
}
|
|
4130
|
-
function mergeNodeAtDepth(a, b, depth, path) {
|
|
4598
|
+
function mergeNodeAtDepth(a, b, depth, path, config) {
|
|
4131
4599
|
assertTraversalDepth(depth);
|
|
4132
|
-
if (a.kind === "lww" && b.kind === "lww") return mergeLww(a, b);
|
|
4133
|
-
if (a.kind === "obj" && b.kind === "obj") return mergeObj(a, b, depth + 1, path);
|
|
4134
|
-
if (a.kind === "seq" && b.kind === "seq") return mergeSeq(a, b, depth + 1, path);
|
|
4135
|
-
if (compareDot(repDot(a), repDot(b)) >= 0) return cloneNodeShallow(a, depth + 1);
|
|
4136
|
-
return cloneNodeShallow(b, depth + 1);
|
|
4600
|
+
if (a.kind === "lww" && b.kind === "lww") return mergeLww(a, b, config.actor);
|
|
4601
|
+
if (a.kind === "obj" && b.kind === "obj") return mergeObj(a, b, depth + 1, path, config);
|
|
4602
|
+
if (a.kind === "seq" && b.kind === "seq") return mergeSeq(a, b, depth + 1, path, config);
|
|
4603
|
+
if (compareDot(repDot(a), repDot(b)) >= 0) return cloneNodeShallow(a, depth + 1, config.actor);
|
|
4604
|
+
return cloneNodeShallow(b, depth + 1, config.actor);
|
|
4137
4605
|
}
|
|
4138
|
-
function mergeLww(a, b) {
|
|
4606
|
+
function mergeLww(a, b, actor) {
|
|
4139
4607
|
if (compareDot(a.dot, b.dot) >= 0) return {
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4608
|
+
node: {
|
|
4609
|
+
kind: "lww",
|
|
4610
|
+
value: structuredClone(a.value),
|
|
4611
|
+
dot: { ...a.dot }
|
|
4612
|
+
},
|
|
4613
|
+
maxObservedCtr: maxObservedCtrForDot(a.dot, actor)
|
|
4143
4614
|
};
|
|
4144
4615
|
return {
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4616
|
+
node: {
|
|
4617
|
+
kind: "lww",
|
|
4618
|
+
value: structuredClone(b.value),
|
|
4619
|
+
dot: { ...b.dot }
|
|
4620
|
+
},
|
|
4621
|
+
maxObservedCtr: maxObservedCtrForDot(b.dot, actor)
|
|
4148
4622
|
};
|
|
4149
4623
|
}
|
|
4150
|
-
function mergeObj(a, b, depth, path) {
|
|
4624
|
+
function mergeObj(a, b, depth, path, config) {
|
|
4151
4625
|
assertTraversalDepth(depth);
|
|
4152
4626
|
const entries = /* @__PURE__ */ new Map();
|
|
4153
4627
|
const tombstone = /* @__PURE__ */ new Map();
|
|
4628
|
+
let maxObservedCtr = 0;
|
|
4154
4629
|
const allTombKeys = new Set([...a.tombstone.keys(), ...b.tombstone.keys()]);
|
|
4155
4630
|
for (const key of allTombKeys) {
|
|
4156
4631
|
const da = a.tombstone.get(key);
|
|
4157
4632
|
const db = b.tombstone.get(key);
|
|
4158
|
-
if (da && db)
|
|
4159
|
-
|
|
4160
|
-
|
|
4633
|
+
if (da && db) {
|
|
4634
|
+
const mergedDot = compareDot(da, db) >= 0 ? { ...da } : { ...db };
|
|
4635
|
+
tombstone.set(key, mergedDot);
|
|
4636
|
+
maxObservedCtr = Math.max(maxObservedCtr, maxObservedCtrForDot(mergedDot, config.actor));
|
|
4637
|
+
} else if (da) {
|
|
4638
|
+
tombstone.set(key, { ...da });
|
|
4639
|
+
maxObservedCtr = Math.max(maxObservedCtr, maxObservedCtrForDot(da, config.actor));
|
|
4640
|
+
} else {
|
|
4641
|
+
tombstone.set(key, { ...db });
|
|
4642
|
+
maxObservedCtr = Math.max(maxObservedCtr, maxObservedCtrForDot(db, config.actor));
|
|
4643
|
+
}
|
|
4161
4644
|
}
|
|
4162
4645
|
const allKeys = new Set([...a.entries.keys(), ...b.entries.keys()]);
|
|
4163
4646
|
for (const key of allKeys) {
|
|
4164
4647
|
const ea = a.entries.get(key);
|
|
4165
4648
|
const eb = b.entries.get(key);
|
|
4166
4649
|
let merged;
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4650
|
+
let mergedNodeMaxObservedCtr = 0;
|
|
4651
|
+
if (ea && eb) {
|
|
4652
|
+
const mergedNode = mergeNodeAtDepth(ea.node, eb.node, depth + 1, [...path, key], config);
|
|
4653
|
+
const dot = compareDot(ea.dot, eb.dot) >= 0 ? { ...ea.dot } : { ...eb.dot };
|
|
4654
|
+
merged = {
|
|
4655
|
+
node: mergedNode.node,
|
|
4656
|
+
dot
|
|
4657
|
+
};
|
|
4658
|
+
mergedNodeMaxObservedCtr = mergedNode.maxObservedCtr;
|
|
4659
|
+
} else if (ea) {
|
|
4660
|
+
const cloned = cloneNodeShallow(ea.node, depth + 1, config.actor);
|
|
4661
|
+
merged = {
|
|
4662
|
+
node: cloned.node,
|
|
4663
|
+
dot: { ...ea.dot }
|
|
4664
|
+
};
|
|
4665
|
+
mergedNodeMaxObservedCtr = cloned.maxObservedCtr;
|
|
4666
|
+
} else {
|
|
4667
|
+
const cloned = cloneNodeShallow(eb.node, depth + 1, config.actor);
|
|
4668
|
+
merged = {
|
|
4669
|
+
node: cloned.node,
|
|
4670
|
+
dot: { ...eb.dot }
|
|
4671
|
+
};
|
|
4672
|
+
mergedNodeMaxObservedCtr = cloned.maxObservedCtr;
|
|
4673
|
+
}
|
|
4179
4674
|
const td = tombstone.get(key);
|
|
4180
4675
|
if (td && compareDot(td, merged.dot) >= 0) continue;
|
|
4181
4676
|
entries.set(key, merged);
|
|
4677
|
+
maxObservedCtr = Math.max(maxObservedCtr, mergedNodeMaxObservedCtr, maxObservedCtrForDot(merged.dot, config.actor));
|
|
4182
4678
|
}
|
|
4183
4679
|
return {
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4680
|
+
node: {
|
|
4681
|
+
kind: "obj",
|
|
4682
|
+
entries,
|
|
4683
|
+
tombstone
|
|
4684
|
+
},
|
|
4685
|
+
maxObservedCtr
|
|
4187
4686
|
};
|
|
4188
4687
|
}
|
|
4189
|
-
function mergeSeq(a, b, depth, path) {
|
|
4688
|
+
function mergeSeq(a, b, depth, path, config) {
|
|
4190
4689
|
assertTraversalDepth(depth);
|
|
4690
|
+
if (config.unrelatedArrays === "atomic-replace" && a.elems.size > 0 && b.elems.size > 0) {
|
|
4691
|
+
let shared = false;
|
|
4692
|
+
for (const id of a.elems.keys()) if (b.elems.has(id)) {
|
|
4693
|
+
shared = true;
|
|
4694
|
+
break;
|
|
4695
|
+
}
|
|
4696
|
+
if (!shared) return cloneNodeShallow(compareDot(repDot(a), repDot(b)) >= 0 ? a : b, depth, config.actor);
|
|
4697
|
+
}
|
|
4191
4698
|
const elems = /* @__PURE__ */ new Map();
|
|
4699
|
+
let maxObservedCtr = 0;
|
|
4192
4700
|
const allIds = new Set([...a.elems.keys(), ...b.elems.keys()]);
|
|
4193
4701
|
for (const id of allIds) {
|
|
4194
4702
|
const ea = a.elems.get(id);
|
|
@@ -4196,35 +4704,51 @@ function mergeSeq(a, b, depth, path) {
|
|
|
4196
4704
|
if (ea && eb) {
|
|
4197
4705
|
if (ea.prev !== eb.prev) throw new SharedElementMetadataMismatchError(stringifyJsonPointer(path), id, "prev");
|
|
4198
4706
|
if (!sameDot(ea.insDot, eb.insDot)) throw new SharedElementMetadataMismatchError(stringifyJsonPointer(path), id, "insDot");
|
|
4199
|
-
const mergedValue = mergeNodeAtDepth(ea.value, eb.value, depth + 1, [...path, id]);
|
|
4707
|
+
const mergedValue = mergeNodeAtDepth(ea.value, eb.value, depth + 1, [...path, id], config);
|
|
4708
|
+
const mergedDeleteDot = mergeDeleteDot(ea.delDot, eb.delDot);
|
|
4200
4709
|
elems.set(id, {
|
|
4201
4710
|
id,
|
|
4202
4711
|
prev: ea.prev,
|
|
4203
4712
|
tombstone: ea.tombstone || eb.tombstone,
|
|
4204
|
-
delDot:
|
|
4205
|
-
value: mergedValue,
|
|
4713
|
+
delDot: mergedDeleteDot,
|
|
4714
|
+
value: mergedValue.node,
|
|
4206
4715
|
insDot: { ...ea.insDot }
|
|
4207
4716
|
});
|
|
4208
|
-
|
|
4209
|
-
else
|
|
4717
|
+
maxObservedCtr = Math.max(maxObservedCtr, mergedValue.maxObservedCtr, maxObservedCtrForDot(ea.insDot, config.actor), maxObservedCtrForDot(mergedDeleteDot, config.actor));
|
|
4718
|
+
} else if (ea) {
|
|
4719
|
+
const cloned = cloneElem(ea, depth + 1, config.actor);
|
|
4720
|
+
elems.set(id, cloned.elem);
|
|
4721
|
+
maxObservedCtr = Math.max(maxObservedCtr, cloned.maxObservedCtr);
|
|
4722
|
+
} else {
|
|
4723
|
+
const cloned = cloneElem(eb, depth + 1, config.actor);
|
|
4724
|
+
elems.set(id, cloned.elem);
|
|
4725
|
+
maxObservedCtr = Math.max(maxObservedCtr, cloned.maxObservedCtr);
|
|
4726
|
+
}
|
|
4210
4727
|
}
|
|
4211
4728
|
return {
|
|
4212
|
-
|
|
4213
|
-
|
|
4729
|
+
node: {
|
|
4730
|
+
kind: "seq",
|
|
4731
|
+
elems
|
|
4732
|
+
},
|
|
4733
|
+
maxObservedCtr
|
|
4214
4734
|
};
|
|
4215
4735
|
}
|
|
4216
4736
|
function sameDot(a, b) {
|
|
4217
4737
|
return a.actor === b.actor && a.ctr === b.ctr;
|
|
4218
4738
|
}
|
|
4219
|
-
function cloneElem(e, depth) {
|
|
4739
|
+
function cloneElem(e, depth, actor) {
|
|
4220
4740
|
assertTraversalDepth(depth);
|
|
4741
|
+
const value = cloneNodeShallow(e.value, depth + 1, actor);
|
|
4221
4742
|
return {
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4743
|
+
elem: {
|
|
4744
|
+
id: e.id,
|
|
4745
|
+
prev: e.prev,
|
|
4746
|
+
tombstone: e.tombstone,
|
|
4747
|
+
delDot: e.delDot ? { ...e.delDot } : void 0,
|
|
4748
|
+
value: value.node,
|
|
4749
|
+
insDot: { ...e.insDot }
|
|
4750
|
+
},
|
|
4751
|
+
maxObservedCtr: Math.max(value.maxObservedCtr, maxObservedCtrForDot(e.insDot, actor), maxObservedCtrForDot(e.delDot, actor))
|
|
4228
4752
|
};
|
|
4229
4753
|
}
|
|
4230
4754
|
function mergeDeleteDot(a, b) {
|
|
@@ -4232,38 +4756,64 @@ function mergeDeleteDot(a, b) {
|
|
|
4232
4756
|
if (a) return { ...a };
|
|
4233
4757
|
if (b) return { ...b };
|
|
4234
4758
|
}
|
|
4235
|
-
function cloneNodeShallow(node, depth) {
|
|
4759
|
+
function cloneNodeShallow(node, depth, actor) {
|
|
4236
4760
|
assertTraversalDepth(depth);
|
|
4237
4761
|
switch (node.kind) {
|
|
4238
4762
|
case "lww": return {
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4763
|
+
node: {
|
|
4764
|
+
kind: "lww",
|
|
4765
|
+
value: structuredClone(node.value),
|
|
4766
|
+
dot: { ...node.dot }
|
|
4767
|
+
},
|
|
4768
|
+
maxObservedCtr: maxObservedCtrForDot(node.dot, actor)
|
|
4242
4769
|
};
|
|
4243
4770
|
case "obj": {
|
|
4244
4771
|
const entries = /* @__PURE__ */ new Map();
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4772
|
+
let maxObservedCtr = 0;
|
|
4773
|
+
for (const [k, v] of node.entries) {
|
|
4774
|
+
const cloned = cloneNodeShallow(v.node, depth + 1, actor);
|
|
4775
|
+
entries.set(k, {
|
|
4776
|
+
node: cloned.node,
|
|
4777
|
+
dot: { ...v.dot }
|
|
4778
|
+
});
|
|
4779
|
+
maxObservedCtr = Math.max(maxObservedCtr, cloned.maxObservedCtr, maxObservedCtrForDot(v.dot, actor));
|
|
4780
|
+
}
|
|
4249
4781
|
const tombstone = /* @__PURE__ */ new Map();
|
|
4250
|
-
for (const [k, d] of node.tombstone)
|
|
4782
|
+
for (const [k, d] of node.tombstone) {
|
|
4783
|
+
tombstone.set(k, { ...d });
|
|
4784
|
+
maxObservedCtr = Math.max(maxObservedCtr, maxObservedCtrForDot(d, actor));
|
|
4785
|
+
}
|
|
4251
4786
|
return {
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4787
|
+
node: {
|
|
4788
|
+
kind: "obj",
|
|
4789
|
+
entries,
|
|
4790
|
+
tombstone
|
|
4791
|
+
},
|
|
4792
|
+
maxObservedCtr
|
|
4255
4793
|
};
|
|
4256
4794
|
}
|
|
4257
4795
|
case "seq": {
|
|
4258
4796
|
const elems = /* @__PURE__ */ new Map();
|
|
4259
|
-
|
|
4797
|
+
let maxObservedCtr = 0;
|
|
4798
|
+
for (const [id, e] of node.elems) {
|
|
4799
|
+
const cloned = cloneElem(e, depth + 1, actor);
|
|
4800
|
+
elems.set(id, cloned.elem);
|
|
4801
|
+
maxObservedCtr = Math.max(maxObservedCtr, cloned.maxObservedCtr);
|
|
4802
|
+
}
|
|
4260
4803
|
return {
|
|
4261
|
-
|
|
4262
|
-
|
|
4804
|
+
node: {
|
|
4805
|
+
kind: "seq",
|
|
4806
|
+
elems
|
|
4807
|
+
},
|
|
4808
|
+
maxObservedCtr
|
|
4263
4809
|
};
|
|
4264
4810
|
}
|
|
4265
4811
|
}
|
|
4266
4812
|
}
|
|
4813
|
+
function maxObservedCtrForDot(dot, actor) {
|
|
4814
|
+
if (!dot || !actor || dot.actor !== actor) return 0;
|
|
4815
|
+
return dot.ctr;
|
|
4816
|
+
}
|
|
4267
4817
|
|
|
4268
4818
|
//#endregion
|
|
4269
4819
|
//#region src/compact.ts
|