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.
@@ -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?.([], node);
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: create a doc using a single dot with counter offsets for array children.
2072
- * Prefer `docFromJson(value, nextDot)` to ensure unique dots per node.
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
- let cur = doc.root;
2079
- for (const seg of path) {
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
- let cur = doc.root;
2089
- const seen = [];
2090
- if (path.length === 0) {
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 /${seen.join("/")}`
2097
+ message: `expected object at ${pointer === "" ? "/" : pointer}`
2110
2098
  };
2111
- cur = entry.node;
2112
2099
  }
2113
2100
  return {
2114
2101
  ok: true,
2115
- obj: cur
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 !== "obj") return;
2163
- const ent = cur.entries.get(seg);
2164
- if (!ent) return;
2165
- cur = ent.node;
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
- assertTraversalDepth(i + 1);
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) throw new Error(`Missing key '${seg}'`);
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)) throw new Error(`Expected array index, got '${seg}'`);
2388
- const id = rgaIdAtIndex(cur, Number(seg));
2389
- if (id === void 0) throw new Error(`Index out of bounds at '${seg}'`);
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
- code: 409,
2405
- reason: "MISSING_TARGET",
2406
- message: `test path missing at /${it.path.join("/")}`,
2407
- path: `/${it.path.join("/")}`
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
- if (!jsonEquals(got, it.value)) return {
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
- const headSeq = headNode;
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
- let shadowBase = cloneDoc(evalTestAgainst === "base" ? options.base : options.head);
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 applySequentialOp = (op, opIndex) => {
2917
- const baseJson = materialize(shadowBase.root);
2918
- let intents;
2919
- try {
2920
- intents = compileJsonPatchToIntent(baseJson, [op], { semantics: "sequential" });
2921
- } catch (error) {
2922
- return withOpIndex(toApplyError$1(error), opIndex);
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
- const headStep = applyIntentsToCrdt(shadowBase, options.head, intents, options.newDot, evalTestAgainst, options.bumpCounterAbove, { strictParents: options.strictParents });
2925
- if (!headStep.ok) return withOpIndex(headStep, opIndex);
2926
- if (evalTestAgainst === "base") {
2927
- const shadowStep = applyIntentsToCrdt(shadowBase, shadowBase, intents, shadowDot, "base", shadowBump, { strictParents: options.strictParents });
2928
- if (!shadowStep.ok) return withOpIndex(shadowStep, opIndex);
2929
- } else shadowBase = cloneDoc(options.head);
2930
- return { ok: true };
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
- return withOpIndex(toApplyError$1(/* @__PURE__ */ new Error(`failed to resolve move source at ${op.from}`)), opIndex);
2949
- }
2950
- if (op.from === op.path) continue;
2951
- const removeStep = applySequentialOp({
2952
- op: "remove",
2953
- path: op.from
2954
- }, opIndex);
2955
- if (!removeStep.ok) return removeStep;
2956
- const addStep = applySequentialOp({
2957
- op: "add",
2958
- path: op.path,
2959
- value: fromValue
2960
- }, opIndex);
2961
- if (!addStep.ok) return addStep;
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
- const step = applySequentialOp(op, opIndex);
2965
- if (!step.ok) return step;
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 { ok: true };
3299
+ return {
3300
+ ok: true,
3301
+ node: current
3302
+ };
2968
3303
  }
2969
- function withOpIndex(error, opIndex) {
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 = tryApplyPatch(createState(base, {
3531
+ const result = tryApplyPatchInPlace(createState(base, {
3133
3532
  actor: "__validate__",
3134
3533
  jsonValidation: options.jsonValidation
3135
- }), patch, options);
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, execution) {
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 mismatchPath = options.requireSharedOrigin ?? true ? findSeqLineageMismatch(a.root, b.root, []) : null;
3995
- if (mismatchPath !== null) return {
3996
- ok: false,
3997
- error: {
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
- code: 409,
4000
- reason: "LINEAGE_MISMATCH",
4001
- message: `merge requires shared array origin at ${mismatchPath}`,
4002
- path: mismatchPath
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: { root: mergeNode(a.root, b.root) }
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.requireSharedOrigin` controls merge lineage checks
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
- const mergedDoc = tryMergeDoc(a.doc, b.doc, { requireSharedOrigin: options.requireSharedOrigin });
4046
- if (!mergedDoc.ok) return mergedDoc;
4047
- const doc = mergedDoc.doc;
4048
- const actor = options.actor ?? a.clock.actor;
4049
- return {
4050
- ok: true,
4051
- state: {
4052
- doc,
4053
- clock: createClock(actor, maxObservedCtrForActor(doc, actor, a, b))
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 maxObservedCtrForActor(doc, actor, a, b) {
4099
- let best = observedVersionVector(doc)[actor] ?? 0;
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
- switch (node.kind) {
4106
- case "lww": return node.dot;
4107
- case "obj": {
4108
- let best = {
4109
- actor: "",
4110
- ctr: 0
4111
- };
4112
- for (const entry of node.entries.values()) if (compareDot(entry.dot, best) > 0) best = entry.dot;
4113
- for (const d of node.tombstone.values()) if (compareDot(d, best) > 0) best = d;
4114
- return best;
4115
- }
4116
- case "seq": {
4117
- let best = {
4118
- actor: "",
4119
- ctr: 0
4120
- };
4121
- for (const e of node.elems.values()) if (compareDot(e.insDot, best) > 0) best = e.insDot;
4122
- return best;
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 mergeNode(a, b) {
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
- kind: "lww",
4140
- value: structuredClone(a.value),
4141
- dot: { ...a.dot }
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
- kind: "lww",
4145
- value: structuredClone(b.value),
4146
- dot: { ...b.dot }
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) tombstone.set(key, compareDot(da, db) >= 0 ? { ...da } : { ...db });
4158
- else if (da) tombstone.set(key, { ...da });
4159
- else tombstone.set(key, { ...db });
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
- if (ea && eb) merged = {
4167
- node: mergeNodeAtDepth(ea.node, eb.node, depth + 1, [...path, key]),
4168
- dot: compareDot(ea.dot, eb.dot) >= 0 ? { ...ea.dot } : { ...eb.dot }
4169
- };
4170
- else if (ea) merged = {
4171
- node: cloneNodeShallow(ea.node, depth + 1),
4172
- dot: { ...ea.dot }
4173
- };
4174
- else merged = {
4175
- node: cloneNodeShallow(eb.node, depth + 1),
4176
- dot: { ...eb.dot }
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
- kind: "obj",
4184
- entries,
4185
- tombstone
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: mergeDeleteDot(ea.delDot, eb.delDot),
4204
- value: mergedValue,
4712
+ delDot: mergedDeleteDot,
4713
+ value: mergedValue.node,
4205
4714
  insDot: { ...ea.insDot }
4206
4715
  });
4207
- } else if (ea) elems.set(id, cloneElem(ea, depth + 1));
4208
- else elems.set(id, cloneElem(eb, depth + 1));
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
- kind: "seq",
4212
- elems
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
- id: e.id,
4222
- prev: e.prev,
4223
- tombstone: e.tombstone,
4224
- delDot: e.delDot ? { ...e.delDot } : void 0,
4225
- value: cloneNodeShallow(e.value, depth + 1),
4226
- insDot: { ...e.insDot }
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
- kind: "lww",
4239
- value: structuredClone(node.value),
4240
- dot: { ...node.dot }
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
- for (const [k, v] of node.entries) entries.set(k, {
4245
- node: cloneNodeShallow(v.node, depth + 1),
4246
- dot: { ...v.dot }
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) tombstone.set(k, { ...d });
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
- kind: "obj",
4252
- entries,
4253
- tombstone
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
- for (const [id, e] of node.elems) elems.set(id, cloneElem(e, depth + 1));
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
- kind: "seq",
4261
- elems
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