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