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