json-patch-to-crdt 0.2.0 → 0.3.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.
@@ -133,12 +133,31 @@ function dotToElemId(d) {
133
133
  const HEAD = "HEAD";
134
134
  const linearCache = /* @__PURE__ */ new WeakMap();
135
135
  const seqVersion = /* @__PURE__ */ new WeakMap();
136
+ const maxSiblingInsDotByPrevCache = /* @__PURE__ */ new WeakMap();
136
137
  function getVersion(seq) {
137
138
  return seqVersion.get(seq) ?? 0;
138
139
  }
139
140
  function bumpVersion(seq) {
140
141
  seqVersion.set(seq, getVersion(seq) + 1);
141
142
  }
143
+ function buildMaxSiblingInsDotByPrevIndex(seq) {
144
+ const index = /* @__PURE__ */ new Map();
145
+ for (const elem of seq.elems.values()) {
146
+ const current = index.get(elem.prev);
147
+ if (!current || compareDot(elem.insDot, current) > 0) index.set(elem.prev, elem.insDot);
148
+ }
149
+ maxSiblingInsDotByPrevCache.set(seq, index);
150
+ return index;
151
+ }
152
+ function getMaxSiblingInsDotByPrevIndex(seq) {
153
+ return maxSiblingInsDotByPrevCache.get(seq) ?? buildMaxSiblingInsDotByPrevIndex(seq);
154
+ }
155
+ function trackInsertedSiblingDot(seq, prev, insDot) {
156
+ const index = maxSiblingInsDotByPrevCache.get(seq);
157
+ if (!index) return;
158
+ const current = index.get(prev);
159
+ if (!current || compareDot(insDot, current) > 0) index.set(prev, insDot);
160
+ }
142
161
  function rgaChildrenIndex(seq) {
143
162
  const idx = /* @__PURE__ */ new Map();
144
163
  for (const e of seq.elems.values()) {
@@ -187,6 +206,30 @@ function rgaLinearizeIds(seq) {
187
206
  });
188
207
  return [...out];
189
208
  }
209
+ function rgaCreateIndexedIdSnapshot(seq) {
210
+ const ids = rgaLinearizeIds(seq);
211
+ return {
212
+ length() {
213
+ return ids.length;
214
+ },
215
+ idAt(index) {
216
+ return ids[index];
217
+ },
218
+ prevForInsertAt(index) {
219
+ if (index <= 0) return HEAD;
220
+ return ids[index - 1] ?? (ids.length > 0 ? ids[ids.length - 1] : HEAD);
221
+ },
222
+ insertAt(index, id) {
223
+ const at = Math.max(0, Math.min(index, ids.length));
224
+ ids.splice(at, 0, id);
225
+ },
226
+ deleteAt(index) {
227
+ if (index < 0 || index >= ids.length) return;
228
+ const [removed] = ids.splice(index, 1);
229
+ return removed;
230
+ }
231
+ };
232
+ }
190
233
  function rgaInsertAfter(seq, prev, id, insDot, value) {
191
234
  if (seq.elems.has(id)) return;
192
235
  seq.elems.set(id, {
@@ -196,15 +239,110 @@ function rgaInsertAfter(seq, prev, id, insDot, value) {
196
239
  value,
197
240
  insDot
198
241
  });
242
+ trackInsertedSiblingDot(seq, prev, insDot);
199
243
  bumpVersion(seq);
200
244
  }
201
- function rgaDelete(seq, id) {
245
+ function rgaInsertAfterChecked(seq, prev, id, insDot, value) {
246
+ if (seq.elems.has(id)) return;
247
+ if (prev !== HEAD && !seq.elems.has(prev)) throw new Error(`RGA predecessor '${prev}' does not exist`);
248
+ rgaInsertAfter(seq, prev, id, insDot, value);
249
+ }
250
+ function rgaDelete(seq, id, delDot) {
202
251
  const e = seq.elems.get(id);
203
252
  if (!e) return;
204
- if (e.tombstone) return;
253
+ if (e.tombstone) {
254
+ if (delDot && (!e.delDot || compareDot(delDot, e.delDot) > 0)) {
255
+ e.delDot = {
256
+ actor: delDot.actor,
257
+ ctr: delDot.ctr
258
+ };
259
+ bumpVersion(seq);
260
+ }
261
+ return;
262
+ }
205
263
  e.tombstone = true;
264
+ if (delDot) e.delDot = {
265
+ actor: delDot.actor,
266
+ ctr: delDot.ctr
267
+ };
206
268
  bumpVersion(seq);
207
269
  }
270
+ function validateRgaSeq(seq) {
271
+ const issues = [];
272
+ for (const elem of seq.elems.values()) if (elem.prev !== HEAD && !seq.elems.has(elem.prev)) issues.push({
273
+ code: "MISSING_PREDECESSOR",
274
+ id: elem.id,
275
+ prev: elem.prev,
276
+ message: `RGA element '${elem.id}' references missing predecessor '${elem.prev}'`
277
+ });
278
+ const cycleIds = /* @__PURE__ */ new Set();
279
+ const visitState = /* @__PURE__ */ new Map();
280
+ const sortedIds = [...seq.elems.keys()].sort();
281
+ for (const startId of sortedIds) {
282
+ if (visitState.get(startId) === 2) continue;
283
+ const trail = [];
284
+ const trailIndex = /* @__PURE__ */ new Map();
285
+ let currentId = startId;
286
+ while (currentId !== void 0) {
287
+ const seenAt = trailIndex.get(currentId);
288
+ if (seenAt !== void 0) {
289
+ for (let i = seenAt; i < trail.length; i++) cycleIds.add(trail[i]);
290
+ break;
291
+ }
292
+ if (visitState.get(currentId) === 2) break;
293
+ const elem = seq.elems.get(currentId);
294
+ if (!elem) break;
295
+ trailIndex.set(currentId, trail.length);
296
+ trail.push(currentId);
297
+ if (elem.prev === HEAD) break;
298
+ currentId = elem.prev;
299
+ }
300
+ for (const id of trail) visitState.set(id, 2);
301
+ }
302
+ for (const id of [...cycleIds].sort()) {
303
+ const elem = seq.elems.get(id);
304
+ issues.push({
305
+ code: "PREDECESSOR_CYCLE",
306
+ id,
307
+ prev: elem.prev,
308
+ message: `RGA predecessor cycle detected at '${id}'`
309
+ });
310
+ }
311
+ const children = rgaChildrenIndex(seq);
312
+ const reachable = /* @__PURE__ */ new Set();
313
+ const stack = [...children.get(HEAD) ?? []];
314
+ while (stack.length > 0) {
315
+ const elem = stack.pop();
316
+ if (reachable.has(elem.id)) continue;
317
+ reachable.add(elem.id);
318
+ const descendants = children.get(elem.id);
319
+ if (descendants) stack.push(...descendants);
320
+ }
321
+ for (const id of sortedIds) {
322
+ if (reachable.has(id)) continue;
323
+ const elem = seq.elems.get(id);
324
+ issues.push({
325
+ code: "ORPHANED_ELEMENT",
326
+ id,
327
+ prev: elem.prev,
328
+ message: `RGA element '${id}' is unreachable from HEAD`
329
+ });
330
+ }
331
+ if (issues.length === 0) return {
332
+ ok: true,
333
+ issues: []
334
+ };
335
+ const issueOrder = {
336
+ MISSING_PREDECESSOR: 0,
337
+ PREDECESSOR_CYCLE: 1,
338
+ ORPHANED_ELEMENT: 2
339
+ };
340
+ issues.sort((a, b) => a.id.localeCompare(b.id) || issueOrder[a.code] - issueOrder[b.code] || a.prev.localeCompare(b.prev));
341
+ return {
342
+ ok: false,
343
+ issues
344
+ };
345
+ }
208
346
  /**
209
347
  * Prune tombstoned elements that are causally stable and have no live descendants
210
348
  * depending on them for sequence traversal.
@@ -251,15 +389,19 @@ function rgaCompactTombstones(seq, isStable) {
251
389
  continue;
252
390
  }
253
391
  const elem = seq.elems.get(frame.id);
254
- if (!elem || !elem.tombstone || !isStable(elem.insDot)) continue;
392
+ if (!elem || !elem.tombstone || !elem.delDot || !isStable(elem.delDot)) continue;
255
393
  const childIds = children.get(frame.id);
256
394
  if (!childIds || childIds.every((childId) => removable.has(childId))) removable.add(frame.id);
257
395
  }
258
396
  if (removable.size === 0) return 0;
259
397
  for (const id of removable) seq.elems.delete(id);
398
+ maxSiblingInsDotByPrevCache.delete(seq);
260
399
  bumpVersion(seq);
261
400
  return removable.size;
262
401
  }
402
+ function rgaMaxInsertDotForPrev(seq, prev) {
403
+ return getMaxSiblingInsDotByPrevIndex(seq).get(prev) ?? null;
404
+ }
263
405
  function rgaIdAtIndex(seq, index) {
264
406
  return rgaLinearizeIds(seq)[index];
265
407
  }
@@ -587,6 +729,7 @@ const ROOT_KEY = "@@crdt/root";
587
729
  //#endregion
588
730
  //#region src/patch.ts
589
731
  const DEFAULT_LCS_MAX_CELLS = 25e4;
732
+ const LINEAR_LCS_MATRIX_BASE_CASE_MAX_CELLS = 4096;
590
733
  /** Structured compile error used to map patch validation failures to typed reasons. */
591
734
  var PatchCompileError = class extends Error {
592
735
  reason;
@@ -677,22 +820,37 @@ function getAtJson(base, path) {
677
820
  * @returns An array of `IntentOp` ready for `applyIntentsToCrdt`.
678
821
  */
679
822
  function compileJsonPatchToIntent(baseJson, patch, options = {}) {
823
+ const internalOptions = options;
680
824
  const semantics = options.semantics ?? "sequential";
825
+ const opIndexOffset = internalOptions.opIndexOffset ?? 0;
681
826
  let workingBase = baseJson;
682
- const pointerCache = /* @__PURE__ */ new Map();
827
+ const pointerCache = internalOptions.pointerCache ?? /* @__PURE__ */ new Map();
683
828
  const intents = [];
684
829
  for (let opIndex = 0; opIndex < patch.length; opIndex++) {
685
830
  const op = patch[opIndex];
831
+ const absoluteOpIndex = opIndex + opIndexOffset;
686
832
  const compileBase = semantics === "sequential" ? workingBase : baseJson;
687
- intents.push(...compileSingleOp(compileBase, op, opIndex, semantics, pointerCache));
688
- if (semantics === "sequential") workingBase = applyPatchOpToJsonWithStructuralSharing(workingBase, op, opIndex, pointerCache);
833
+ intents.push(...compileSingleOp(compileBase, op, absoluteOpIndex, semantics, pointerCache));
834
+ if (semantics === "sequential") workingBase = applyPatchOpToJsonWithStructuralSharing(workingBase, op, absoluteOpIndex, pointerCache);
689
835
  }
690
836
  return intents;
691
837
  }
838
+ /** Compile a single JSON Patch operation into CRDT intents. */
839
+ function compileJsonPatchOpToIntent(baseJson, op, options = {}) {
840
+ const internalOptions = options;
841
+ const semantics = options.semantics ?? "sequential";
842
+ const pointerCache = internalOptions.pointerCache ?? /* @__PURE__ */ new Map();
843
+ return compileSingleOp(baseJson, op, internalOptions.opIndexOffset ?? 0, semantics, pointerCache);
844
+ }
692
845
  /**
693
846
  * Compute a JSON Patch delta between two JSON values.
694
847
  * By default arrays use a deterministic LCS strategy.
695
848
  * Pass `{ arrayStrategy: "atomic" }` for single-op array replacement.
849
+ * Pass `{ arrayStrategy: "lcs-linear" }` for a lower-memory LCS variant.
850
+ * Note that `lcs-linear` still runs in `O(n * m)` time and does not have an
851
+ * automatic fallback for very large unmatched windows.
852
+ * Pass `{ emitMoves: true }` or `{ emitCopies: true }` to opt into RFC 6902
853
+ * move/copy emission when a deterministic rewrite is available.
696
854
  * @param base - The original JSON value.
697
855
  * @param next - The target JSON value.
698
856
  * @param options - Diff options.
@@ -709,14 +867,19 @@ function diffJsonPatch(base, next, options = {}) {
709
867
  function diffValue(path, base, next, ops, options) {
710
868
  if (jsonEquals(base, next)) return;
711
869
  if (Array.isArray(base) || Array.isArray(next)) {
712
- if ((options.arrayStrategy ?? "lcs") === "lcs" && Array.isArray(base) && Array.isArray(next)) {
713
- if (!diffArray(path, base, next, ops, options.lcsMaxCells)) ops.push({
870
+ const arrayStrategy = options.arrayStrategy ?? "lcs";
871
+ if (arrayStrategy === "lcs" && Array.isArray(base) && Array.isArray(next)) {
872
+ if (!diffArrayWithLcsMatrix(path, base, next, ops, options)) ops.push({
714
873
  op: "replace",
715
874
  path: stringifyJsonPointer(path),
716
875
  value: next
717
876
  });
718
877
  return;
719
878
  }
879
+ if (arrayStrategy === "lcs-linear" && Array.isArray(base) && Array.isArray(next)) {
880
+ diffArrayWithLinearLcs(path, base, next, ops, options);
881
+ return;
882
+ }
720
883
  ops.push({
721
884
  op: "replace",
722
885
  path: stringifyJsonPointer(path),
@@ -732,150 +895,368 @@ function diffValue(path, base, next, ops, options) {
732
895
  });
733
896
  return;
734
897
  }
898
+ diffObject(path, base, next, ops, options);
899
+ }
900
+ function diffObject(path, base, next, ops, options) {
735
901
  const baseKeys = Object.keys(base).sort();
736
902
  const nextKeys = Object.keys(next).sort();
903
+ const baseOnlyKeys = [];
904
+ const nextOnlyKeys = [];
905
+ const sharedKeys = [];
737
906
  let baseIndex = 0;
738
907
  let nextIndex = 0;
739
908
  while (baseIndex < baseKeys.length && nextIndex < nextKeys.length) {
740
909
  const baseKey = baseKeys[baseIndex];
741
910
  const nextKey = nextKeys[nextIndex];
742
911
  if (baseKey === nextKey) {
912
+ sharedKeys.push(baseKey);
743
913
  baseIndex += 1;
744
914
  nextIndex += 1;
745
915
  continue;
746
916
  }
747
917
  if (baseKey < nextKey) {
918
+ baseOnlyKeys.push(baseKey);
919
+ baseIndex += 1;
920
+ continue;
921
+ }
922
+ nextOnlyKeys.push(nextKey);
923
+ nextIndex += 1;
924
+ }
925
+ while (baseIndex < baseKeys.length) {
926
+ baseOnlyKeys.push(baseKeys[baseIndex]);
927
+ baseIndex += 1;
928
+ }
929
+ while (nextIndex < nextKeys.length) {
930
+ nextOnlyKeys.push(nextKeys[nextIndex]);
931
+ nextIndex += 1;
932
+ }
933
+ emitObjectStructuralOps(path, base, next, sharedKeys, baseOnlyKeys, nextOnlyKeys, ops, options);
934
+ for (const key of sharedKeys) {
935
+ path.push(key);
936
+ diffValue(path, base[key], next[key], ops, options);
937
+ path.pop();
938
+ }
939
+ }
940
+ function emitObjectStructuralOps(path, base, next, sharedKeys, baseOnlyKeys, nextOnlyKeys, ops, options) {
941
+ if (!options.emitMoves && !options.emitCopies) {
942
+ for (const baseKey of baseOnlyKeys) {
748
943
  path.push(baseKey);
749
944
  ops.push({
750
945
  op: "remove",
751
946
  path: stringifyJsonPointer(path)
752
947
  });
753
948
  path.pop();
754
- baseIndex += 1;
755
- continue;
756
949
  }
757
- nextIndex += 1;
950
+ for (const nextKey of nextOnlyKeys) {
951
+ path.push(nextKey);
952
+ ops.push({
953
+ op: "add",
954
+ path: stringifyJsonPointer(path),
955
+ value: next[nextKey]
956
+ });
957
+ path.pop();
958
+ }
959
+ return;
758
960
  }
759
- while (baseIndex < baseKeys.length) {
760
- const baseKey = baseKeys[baseIndex];
761
- path.push(baseKey);
762
- ops.push({
763
- op: "remove",
764
- path: stringifyJsonPointer(path)
765
- });
766
- path.pop();
767
- baseIndex += 1;
961
+ const matchedMoveSources = /* @__PURE__ */ new Set();
962
+ const moveTargets = /* @__PURE__ */ new Map();
963
+ if (options.emitMoves) {
964
+ const moveSourceBuckets = /* @__PURE__ */ new Map();
965
+ for (const baseKey of baseOnlyKeys) {
966
+ const bucketKey = stableJsonValueKey(base[baseKey]);
967
+ const bucket = moveSourceBuckets.get(bucketKey);
968
+ if (bucket) bucket.push(baseKey);
969
+ else moveSourceBuckets.set(bucketKey, [baseKey]);
970
+ }
971
+ for (const nextKey of nextOnlyKeys) {
972
+ const bucket = moveSourceBuckets.get(stableJsonValueKey(next[nextKey]));
973
+ if (!bucket) continue;
974
+ if (bucket.length > 0) {
975
+ const candidate = bucket.shift();
976
+ matchedMoveSources.add(candidate);
977
+ moveTargets.set(nextKey, candidate);
978
+ }
979
+ }
768
980
  }
769
- baseIndex = 0;
770
- nextIndex = 0;
771
- while (baseIndex < baseKeys.length && nextIndex < nextKeys.length) {
772
- const baseKey = baseKeys[baseIndex];
773
- const nextKey = nextKeys[nextIndex];
774
- if (baseKey === nextKey) {
775
- baseIndex += 1;
776
- nextIndex += 1;
981
+ const availableSources = /* @__PURE__ */ new Map();
982
+ const availableSourceKeys = [];
983
+ for (const key of sharedKeys) {
984
+ if (!jsonEquals(base[key], next[key])) continue;
985
+ availableSources.set(key, base[key]);
986
+ availableSourceKeys.push(key);
987
+ }
988
+ for (const nextKey of nextOnlyKeys) {
989
+ path.push(nextKey);
990
+ const targetPath = stringifyJsonPointer(path);
991
+ path.pop();
992
+ const moveSource = moveTargets.get(nextKey);
993
+ if (moveSource !== void 0) {
994
+ path.push(moveSource);
995
+ const fromPath = stringifyJsonPointer(path);
996
+ path.pop();
997
+ ops.push({
998
+ op: "move",
999
+ from: fromPath,
1000
+ path: targetPath
1001
+ });
1002
+ availableSources.set(nextKey, next[nextKey]);
1003
+ insertSortedKey(availableSourceKeys, nextKey);
777
1004
  continue;
778
1005
  }
779
- if (baseKey < nextKey) {
780
- baseIndex += 1;
781
- continue;
1006
+ if (options.emitCopies) {
1007
+ const copySource = findObjectCopySource(availableSourceKeys, availableSources, next[nextKey]);
1008
+ if (copySource !== void 0) {
1009
+ path.push(copySource);
1010
+ const fromPath = stringifyJsonPointer(path);
1011
+ path.pop();
1012
+ ops.push({
1013
+ op: "copy",
1014
+ from: fromPath,
1015
+ path: targetPath
1016
+ });
1017
+ availableSources.set(nextKey, next[nextKey]);
1018
+ insertSortedKey(availableSourceKeys, nextKey);
1019
+ continue;
1020
+ }
782
1021
  }
783
- path.push(nextKey);
784
1022
  ops.push({
785
1023
  op: "add",
786
- path: stringifyJsonPointer(path),
1024
+ path: targetPath,
787
1025
  value: next[nextKey]
788
1026
  });
789
- path.pop();
790
- nextIndex += 1;
1027
+ availableSources.set(nextKey, next[nextKey]);
1028
+ insertSortedKey(availableSourceKeys, nextKey);
791
1029
  }
792
- while (nextIndex < nextKeys.length) {
793
- const nextKey = nextKeys[nextIndex];
794
- path.push(nextKey);
1030
+ for (const baseKey of baseOnlyKeys) {
1031
+ if (matchedMoveSources.has(baseKey)) continue;
1032
+ path.push(baseKey);
795
1033
  ops.push({
796
- op: "add",
797
- path: stringifyJsonPointer(path),
798
- value: next[nextKey]
1034
+ op: "remove",
1035
+ path: stringifyJsonPointer(path)
799
1036
  });
800
1037
  path.pop();
801
- nextIndex += 1;
802
1038
  }
803
- baseIndex = 0;
804
- nextIndex = 0;
805
- while (baseIndex < baseKeys.length && nextIndex < nextKeys.length) {
806
- const baseKey = baseKeys[baseIndex];
807
- const nextKey = nextKeys[nextIndex];
808
- if (baseKey === nextKey) {
809
- path.push(baseKey);
810
- diffValue(path, base[baseKey], next[nextKey], ops, options);
811
- path.pop();
812
- baseIndex += 1;
813
- nextIndex += 1;
1039
+ }
1040
+ function findObjectCopySource(sortedKeys, availableSources, target) {
1041
+ for (const key of sortedKeys) if (jsonEquals(availableSources.get(key), target)) return key;
1042
+ }
1043
+ function insertSortedKey(keys, key) {
1044
+ let low = 0;
1045
+ let high = keys.length;
1046
+ while (low < high) {
1047
+ const mid = Math.floor((low + high) / 2);
1048
+ if (keys[mid] < key) low = mid + 1;
1049
+ else high = mid;
1050
+ }
1051
+ keys.splice(low, 0, key);
1052
+ }
1053
+ function diffArrayWithLcsMatrix(path, base, next, ops, options) {
1054
+ const window = trimEqualArrayEdges(base, next);
1055
+ const baseStart = window.baseStart;
1056
+ const nextStart = window.nextStart;
1057
+ const n = window.unmatchedBaseLength;
1058
+ const m = window.unmatchedNextLength;
1059
+ if (!shouldUseLcsDiff(n, m, options.lcsMaxCells)) return false;
1060
+ if (n === 0 && m === 0) return true;
1061
+ const steps = [];
1062
+ buildArrayEditScriptWithMatrix(base, baseStart, baseStart + n, next, nextStart, nextStart + m, steps);
1063
+ pushArrayPatchOps(path, window.prefixLength, steps, ops, base, options);
1064
+ return true;
1065
+ }
1066
+ function diffArrayWithLinearLcs(path, base, next, ops, options) {
1067
+ const window = trimEqualArrayEdges(base, next);
1068
+ const steps = [];
1069
+ buildArrayEditScriptLinearSpace(base, window.baseStart, window.baseStart + window.unmatchedBaseLength, next, window.nextStart, window.nextStart + window.unmatchedNextLength, steps);
1070
+ pushArrayPatchOps(path, window.prefixLength, steps, ops, base, options);
1071
+ }
1072
+ function trimEqualArrayEdges(base, next) {
1073
+ const baseLength = base.length;
1074
+ const nextLength = next.length;
1075
+ let prefixLength = 0;
1076
+ while (prefixLength < baseLength && prefixLength < nextLength && jsonEquals(base[prefixLength], next[prefixLength])) prefixLength += 1;
1077
+ let suffixLength = 0;
1078
+ while (suffixLength < baseLength - prefixLength && suffixLength < nextLength - prefixLength && jsonEquals(base[baseLength - 1 - suffixLength], next[nextLength - 1 - suffixLength])) suffixLength += 1;
1079
+ return {
1080
+ baseStart: prefixLength,
1081
+ nextStart: prefixLength,
1082
+ prefixLength,
1083
+ unmatchedBaseLength: baseLength - prefixLength - suffixLength,
1084
+ unmatchedNextLength: nextLength - prefixLength - suffixLength
1085
+ };
1086
+ }
1087
+ function buildArrayEditScriptLinearSpace(base, baseStart, baseEnd, next, nextStart, nextEnd, steps) {
1088
+ const unmatchedBaseLength = baseEnd - baseStart;
1089
+ const unmatchedNextLength = nextEnd - nextStart;
1090
+ if (unmatchedBaseLength === 0) {
1091
+ for (let nextIndex = nextStart; nextIndex < nextEnd; nextIndex++) steps.push({
1092
+ kind: "add",
1093
+ value: next[nextIndex]
1094
+ });
1095
+ return;
1096
+ }
1097
+ if (unmatchedNextLength === 0) {
1098
+ for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) steps.push({ kind: "remove" });
1099
+ return;
1100
+ }
1101
+ if (unmatchedBaseLength === 1) {
1102
+ pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, steps);
1103
+ return;
1104
+ }
1105
+ if (unmatchedNextLength === 1) {
1106
+ pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, steps);
1107
+ return;
1108
+ }
1109
+ if (shouldUseMatrixBaseCase(unmatchedBaseLength, unmatchedNextLength)) {
1110
+ buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStart, nextEnd, steps);
1111
+ return;
1112
+ }
1113
+ const baseMid = baseStart + Math.floor(unmatchedBaseLength / 2);
1114
+ const forwardScores = computeLcsPrefixLengths(base, baseStart, baseMid, next, nextStart, nextEnd);
1115
+ const reverseScores = computeLcsSuffixLengths(base, baseMid, baseEnd, next, nextStart, nextEnd);
1116
+ let bestOffset = 0;
1117
+ let bestScore = Number.NEGATIVE_INFINITY;
1118
+ for (let offset = 0; offset <= unmatchedNextLength; offset++) {
1119
+ const score = forwardScores[offset] + reverseScores[offset];
1120
+ if (score > bestScore) {
1121
+ bestScore = score;
1122
+ bestOffset = offset;
1123
+ }
1124
+ }
1125
+ const nextMid = nextStart + bestOffset;
1126
+ buildArrayEditScriptLinearSpace(base, baseStart, baseMid, next, nextStart, nextMid, steps);
1127
+ buildArrayEditScriptLinearSpace(base, baseMid, baseEnd, next, nextMid, nextEnd, steps);
1128
+ }
1129
+ function pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, steps) {
1130
+ const matchIndex = findFirstMatchingIndexInNext(base[baseStart], next, nextStart, nextEnd);
1131
+ if (matchIndex === -1) {
1132
+ steps.push({ kind: "remove" });
1133
+ for (let nextIndex = nextStart; nextIndex < nextEnd; nextIndex++) steps.push({
1134
+ kind: "add",
1135
+ value: next[nextIndex]
1136
+ });
1137
+ return;
1138
+ }
1139
+ for (let nextIndex = nextStart; nextIndex < matchIndex; nextIndex++) steps.push({
1140
+ kind: "add",
1141
+ value: next[nextIndex]
1142
+ });
1143
+ steps.push({ kind: "equal" });
1144
+ for (let nextIndex = matchIndex + 1; nextIndex < nextEnd; nextIndex++) steps.push({
1145
+ kind: "add",
1146
+ value: next[nextIndex]
1147
+ });
1148
+ }
1149
+ function pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, steps) {
1150
+ const matchIndex = findFirstMatchingIndexInBase(next[nextStart], base, baseStart, baseEnd);
1151
+ if (matchIndex === -1) {
1152
+ for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) steps.push({ kind: "remove" });
1153
+ steps.push({
1154
+ kind: "add",
1155
+ value: next[nextStart]
1156
+ });
1157
+ return;
1158
+ }
1159
+ for (let baseIndex = baseStart; baseIndex < matchIndex; baseIndex++) steps.push({ kind: "remove" });
1160
+ steps.push({ kind: "equal" });
1161
+ for (let baseIndex = matchIndex + 1; baseIndex < baseEnd; baseIndex++) steps.push({ kind: "remove" });
1162
+ }
1163
+ function findFirstMatchingIndexInNext(target, next, nextStart, nextEnd) {
1164
+ for (let nextIndex = nextStart; nextIndex < nextEnd; nextIndex++) if (jsonEquals(target, next[nextIndex])) return nextIndex;
1165
+ return -1;
1166
+ }
1167
+ function findFirstMatchingIndexInBase(target, base, baseStart, baseEnd) {
1168
+ for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) if (jsonEquals(target, base[baseIndex])) return baseIndex;
1169
+ return -1;
1170
+ }
1171
+ function shouldUseMatrixBaseCase(baseLength, nextLength) {
1172
+ return (baseLength + 1) * (nextLength + 1) <= LINEAR_LCS_MATRIX_BASE_CASE_MAX_CELLS;
1173
+ }
1174
+ function buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStart, nextEnd, steps) {
1175
+ const unmatchedBaseLength = baseEnd - baseStart;
1176
+ const unmatchedNextLength = nextEnd - nextStart;
1177
+ const lcs = Array.from({ length: unmatchedBaseLength + 1 }, () => Array(unmatchedNextLength + 1).fill(0));
1178
+ for (let baseOffset = unmatchedBaseLength - 1; baseOffset >= 0; baseOffset--) for (let nextOffset = unmatchedNextLength - 1; nextOffset >= 0; nextOffset--) if (jsonEquals(base[baseStart + baseOffset], next[nextStart + nextOffset])) lcs[baseOffset][nextOffset] = 1 + lcs[baseOffset + 1][nextOffset + 1];
1179
+ else lcs[baseOffset][nextOffset] = Math.max(lcs[baseOffset + 1][nextOffset], lcs[baseOffset][nextOffset + 1]);
1180
+ let baseOffset = 0;
1181
+ let nextOffset = 0;
1182
+ while (baseOffset < unmatchedBaseLength || nextOffset < unmatchedNextLength) {
1183
+ if (baseOffset < unmatchedBaseLength && nextOffset < unmatchedNextLength && jsonEquals(base[baseStart + baseOffset], next[nextStart + nextOffset])) {
1184
+ steps.push({ kind: "equal" });
1185
+ baseOffset += 1;
1186
+ nextOffset += 1;
814
1187
  continue;
815
1188
  }
816
- if (baseKey < nextKey) {
817
- baseIndex += 1;
1189
+ const lcsDown = baseOffset < unmatchedBaseLength ? lcs[baseOffset + 1][nextOffset] : -1;
1190
+ const lcsRight = nextOffset < unmatchedNextLength ? lcs[baseOffset][nextOffset + 1] : -1;
1191
+ if (nextOffset < unmatchedNextLength && (baseOffset === unmatchedBaseLength || lcsRight > lcsDown)) {
1192
+ steps.push({
1193
+ kind: "add",
1194
+ value: next[nextStart + nextOffset]
1195
+ });
1196
+ nextOffset += 1;
818
1197
  continue;
819
1198
  }
820
- nextIndex += 1;
1199
+ if (baseOffset < unmatchedBaseLength) {
1200
+ steps.push({ kind: "remove" });
1201
+ baseOffset += 1;
1202
+ }
821
1203
  }
822
1204
  }
823
- function diffArray(path, base, next, ops, lcsMaxCells) {
824
- const baseLength = base.length;
825
- const nextLength = next.length;
826
- let prefix = 0;
827
- while (prefix < baseLength && prefix < nextLength && jsonEquals(base[prefix], next[prefix])) prefix += 1;
828
- let suffix = 0;
829
- while (suffix < baseLength - prefix && suffix < nextLength - prefix && jsonEquals(base[baseLength - 1 - suffix], next[nextLength - 1 - suffix])) suffix += 1;
830
- const baseStart = prefix;
831
- const nextStart = prefix;
832
- const n = baseLength - prefix - suffix;
833
- const m = nextLength - prefix - suffix;
834
- if (!shouldUseLcsDiff(n, m, lcsMaxCells)) return false;
835
- if (n === 0 && m === 0) return true;
836
- const lcs = Array.from({ length: n + 1 }, () => Array(m + 1).fill(0));
837
- for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--) if (jsonEquals(base[baseStart + i], next[nextStart + j])) lcs[i][j] = 1 + lcs[i + 1][j + 1];
838
- else lcs[i][j] = Math.max(lcs[i + 1][j], lcs[i][j + 1]);
1205
+ function computeLcsPrefixLengths(base, baseStart, baseEnd, next, nextStart, nextEnd) {
1206
+ const unmatchedNextLength = nextEnd - nextStart;
1207
+ let previousRow = new Int32Array(unmatchedNextLength + 1);
1208
+ let currentRow = new Int32Array(unmatchedNextLength + 1);
1209
+ for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) {
1210
+ for (let nextOffset = 0; nextOffset < unmatchedNextLength; nextOffset++) if (jsonEquals(base[baseIndex], next[nextStart + nextOffset])) currentRow[nextOffset + 1] = previousRow[nextOffset] + 1;
1211
+ else currentRow[nextOffset + 1] = Math.max(previousRow[nextOffset + 1], currentRow[nextOffset]);
1212
+ const nextPreviousRow = currentRow;
1213
+ currentRow = previousRow;
1214
+ previousRow = nextPreviousRow;
1215
+ currentRow.fill(0);
1216
+ }
1217
+ return previousRow;
1218
+ }
1219
+ function computeLcsSuffixLengths(base, baseStart, baseEnd, next, nextStart, nextEnd) {
1220
+ const unmatchedNextLength = nextEnd - nextStart;
1221
+ let previousRow = new Int32Array(unmatchedNextLength + 1);
1222
+ let currentRow = new Int32Array(unmatchedNextLength + 1);
1223
+ for (let baseIndex = baseEnd - 1; baseIndex >= baseStart; baseIndex--) {
1224
+ for (let nextOffset = unmatchedNextLength - 1; nextOffset >= 0; nextOffset--) if (jsonEquals(base[baseIndex], next[nextStart + nextOffset])) currentRow[nextOffset] = previousRow[nextOffset + 1] + 1;
1225
+ else currentRow[nextOffset] = Math.max(previousRow[nextOffset], currentRow[nextOffset + 1]);
1226
+ const nextPreviousRow = currentRow;
1227
+ currentRow = previousRow;
1228
+ previousRow = nextPreviousRow;
1229
+ currentRow.fill(0);
1230
+ }
1231
+ return previousRow;
1232
+ }
1233
+ function pushArrayPatchOps(path, startIndex, steps, ops, base, options) {
839
1234
  const localOps = [];
840
- let i = 0;
841
- let j = 0;
842
- let index = prefix;
843
- while (i < n || j < m) {
844
- if (i < n && j < m && jsonEquals(base[baseStart + i], next[nextStart + j])) {
845
- i += 1;
846
- j += 1;
1235
+ let index = startIndex;
1236
+ for (const step of steps) {
1237
+ if (step.kind === "equal") {
847
1238
  index += 1;
848
1239
  continue;
849
1240
  }
850
- const lcsDown = i < n ? lcs[i + 1][j] : -1;
851
- const lcsRight = j < m ? lcs[i][j + 1] : -1;
852
- if (j < m && (i === n || lcsRight > lcsDown)) {
853
- const indexSegment = String(index);
854
- path.push(indexSegment);
1241
+ const indexSegment = String(index);
1242
+ path.push(indexSegment);
1243
+ if (step.kind === "add") {
855
1244
  localOps.push({
856
1245
  op: "add",
857
1246
  path: stringifyJsonPointer(path),
858
- value: next[nextStart + j]
1247
+ value: step.value
859
1248
  });
860
- path.pop();
861
- j += 1;
862
1249
  index += 1;
863
- continue;
864
- }
865
- if (i < n) {
866
- const indexSegment = String(index);
867
- path.push(indexSegment);
868
- localOps.push({
869
- op: "remove",
870
- path: stringifyJsonPointer(path)
871
- });
872
1250
  path.pop();
873
- i += 1;
874
1251
  continue;
875
1252
  }
1253
+ localOps.push({
1254
+ op: "remove",
1255
+ path: stringifyJsonPointer(path)
1256
+ });
1257
+ path.pop();
876
1258
  }
877
- ops.push(...compactArrayOps(localOps));
878
- return true;
1259
+ ops.push(...finalizeArrayOps(path, base, localOps, options));
879
1260
  }
880
1261
  function shouldUseLcsDiff(baseLength, nextLength, lcsMaxCells) {
881
1262
  if (lcsMaxCells === Number.POSITIVE_INFINITY) return true;
@@ -883,6 +1264,99 @@ function shouldUseLcsDiff(baseLength, nextLength, lcsMaxCells) {
883
1264
  if (!Number.isFinite(cap) || cap < 1) return false;
884
1265
  return (baseLength + 1) * (nextLength + 1) <= cap;
885
1266
  }
1267
+ function finalizeArrayOps(arrayPath, base, ops, options) {
1268
+ if (ops.length === 0) return [];
1269
+ if (!options.emitMoves && !options.emitCopies) return compactArrayOps(ops);
1270
+ const out = [];
1271
+ const working = base.slice();
1272
+ for (let i = 0; i < ops.length; i++) {
1273
+ const op = ops[i];
1274
+ const next = ops[i + 1];
1275
+ if (op.op === "remove" && next && next.op === "add") {
1276
+ const removedValue = working[getArrayOpIndex(op.path, arrayPath)];
1277
+ const valuesMatch = jsonEquals(removedValue, next.value);
1278
+ if (op.path === next.path) {
1279
+ const replaceOp = {
1280
+ op: "replace",
1281
+ path: op.path,
1282
+ value: next.value
1283
+ };
1284
+ out.push(replaceOp);
1285
+ applyArrayOptimizationOp(working, replaceOp, arrayPath);
1286
+ i += 1;
1287
+ continue;
1288
+ }
1289
+ if (options.emitMoves && valuesMatch) {
1290
+ const moveOp = {
1291
+ op: "move",
1292
+ from: op.path,
1293
+ path: next.path
1294
+ };
1295
+ out.push(moveOp);
1296
+ applyArrayOptimizationOp(working, moveOp, arrayPath);
1297
+ i += 1;
1298
+ continue;
1299
+ }
1300
+ if (valuesMatch) {
1301
+ out.push(op);
1302
+ applyArrayOptimizationOp(working, op, arrayPath);
1303
+ out.push(next);
1304
+ applyArrayOptimizationOp(working, next, arrayPath);
1305
+ i += 1;
1306
+ continue;
1307
+ }
1308
+ out.push(op);
1309
+ applyArrayOptimizationOp(working, op, arrayPath);
1310
+ continue;
1311
+ }
1312
+ if (op.op === "add" && next && next.op === "remove") {
1313
+ const targetIndex = getArrayOpIndex(op.path, arrayPath);
1314
+ const removeIndex = getArrayOpIndex(next.path, arrayPath);
1315
+ const sourceIndex = removeIndex - (targetIndex <= removeIndex ? 1 : 0);
1316
+ const matchesPendingRemove = sourceIndex >= 0 && sourceIndex < working.length && jsonEquals(working[sourceIndex], op.value);
1317
+ if (options.emitMoves && matchesPendingRemove) {
1318
+ const moveOp = {
1319
+ op: "move",
1320
+ from: stringifyJsonPointer([...arrayPath, String(sourceIndex)]),
1321
+ path: op.path
1322
+ };
1323
+ out.push(moveOp);
1324
+ applyArrayOptimizationOp(working, moveOp, arrayPath);
1325
+ i += 1;
1326
+ continue;
1327
+ }
1328
+ if (matchesPendingRemove) {
1329
+ out.push(op);
1330
+ applyArrayOptimizationOp(working, op, arrayPath);
1331
+ out.push(next);
1332
+ applyArrayOptimizationOp(working, next, arrayPath);
1333
+ i += 1;
1334
+ continue;
1335
+ }
1336
+ }
1337
+ if (op.op === "add" && options.emitCopies) {
1338
+ const copySourceIndex = findArrayCopySourceIndex(working, op.value);
1339
+ if (copySourceIndex !== -1) {
1340
+ const copyOp = {
1341
+ op: "copy",
1342
+ from: stringifyJsonPointer([...arrayPath, String(copySourceIndex)]),
1343
+ path: op.path
1344
+ };
1345
+ out.push(copyOp);
1346
+ applyArrayOptimizationOp(working, copyOp, arrayPath);
1347
+ continue;
1348
+ }
1349
+ }
1350
+ out.push(op);
1351
+ applyArrayOptimizationOp(working, op, arrayPath);
1352
+ }
1353
+ return out;
1354
+ }
1355
+ function stableJsonValueKey(value) {
1356
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
1357
+ if (Array.isArray(value)) return `[${value.map(stableJsonValueKey).join(",")}]`;
1358
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJsonValueKey(value[key])}`).join(",")}}`;
1359
+ }
886
1360
  function compactArrayOps(ops) {
887
1361
  const out = [];
888
1362
  for (let i = 0; i < ops.length; i++) {
@@ -901,6 +1375,47 @@ function compactArrayOps(ops) {
901
1375
  }
902
1376
  return out;
903
1377
  }
1378
+ function findArrayCopySourceIndex(working, value) {
1379
+ for (let index = 0; index < working.length; index++) if (jsonEquals(working[index], value)) return index;
1380
+ return -1;
1381
+ }
1382
+ function getArrayOpIndex(ptr, arrayPath) {
1383
+ const parsed = parseJsonPointer(ptr);
1384
+ if (parsed.length !== arrayPath.length + 1) throw new Error(`Expected array operation under ${stringifyJsonPointer(arrayPath)}: ${ptr}`);
1385
+ for (let index = 0; index < arrayPath.length; index++) if (parsed[index] !== arrayPath[index]) throw new Error(`Expected array operation under ${stringifyJsonPointer(arrayPath)}: ${ptr}`);
1386
+ const token = parsed[arrayPath.length];
1387
+ if (!ARRAY_INDEX_TOKEN_PATTERN.test(token)) throw new Error(`Expected numeric array index at ${ptr}`);
1388
+ return Number(token);
1389
+ }
1390
+ function applyArrayOptimizationOp(working, op, arrayPath) {
1391
+ if (op.op === "add") {
1392
+ working.splice(getArrayOpIndex(op.path, arrayPath), 0, structuredClone(op.value));
1393
+ return;
1394
+ }
1395
+ if (op.op === "remove") {
1396
+ working.splice(getArrayOpIndex(op.path, arrayPath), 1);
1397
+ return;
1398
+ }
1399
+ if (op.op === "replace") {
1400
+ working[getArrayOpIndex(op.path, arrayPath)] = structuredClone(op.value);
1401
+ return;
1402
+ }
1403
+ if (op.op === "copy") {
1404
+ const fromIndex = getArrayOpIndex(op.from, arrayPath);
1405
+ if (fromIndex < 0 || fromIndex >= working.length) throw new Error(`applyArrayOptimizationOp: copy from index ${fromIndex} is out of bounds (length ${working.length})`);
1406
+ const value = structuredClone(working[fromIndex]);
1407
+ working.splice(getArrayOpIndex(op.path, arrayPath), 0, value);
1408
+ return;
1409
+ }
1410
+ if (op.op === "move") {
1411
+ const fromIndex = getArrayOpIndex(op.from, arrayPath);
1412
+ if (fromIndex < 0 || fromIndex >= working.length) throw new Error(`applyArrayOptimizationOp: move from index ${fromIndex} is out of bounds (length ${working.length})`);
1413
+ const [value] = working.splice(fromIndex, 1);
1414
+ working.splice(getArrayOpIndex(op.path, arrayPath), 0, value);
1415
+ return;
1416
+ }
1417
+ throw new Error(`applyArrayOptimizationOp: unexpected op type "${op.op}"`);
1418
+ }
904
1419
  function escapeJsonPointer(token) {
905
1420
  return token.replace(/~/g, "~0").replace(/\//g, "~1");
906
1421
  }
@@ -1497,6 +2012,10 @@ function cloneNodeAtDepth(node, depth) {
1497
2012
  id: e.id,
1498
2013
  prev: e.prev,
1499
2014
  tombstone: e.tombstone,
2015
+ delDot: e.delDot ? {
2016
+ actor: e.delDot.actor,
2017
+ ctr: e.delDot.ctr
2018
+ } : void 0,
1500
2019
  value: cloneNodeAtDepth(e.value, depth + 1),
1501
2020
  insDot: {
1502
2021
  actor: e.insDot.actor,
@@ -1601,7 +2120,17 @@ function applyObjRemove(head, it, newDot) {
1601
2120
  objRemove(parentObj, it.key, d);
1602
2121
  return null;
1603
2122
  }
1604
- function applyArrInsert(base, head, it, newDot, bumpCounterAbove, strictParents = false) {
2123
+ function createArrayIndexLookupSession() {
2124
+ const bySeq = /* @__PURE__ */ new WeakMap();
2125
+ return { get(seq) {
2126
+ const cached = bySeq.get(seq);
2127
+ if (cached) return cached;
2128
+ const created = rgaCreateIndexedIdSnapshot(seq);
2129
+ bySeq.set(seq, created);
2130
+ return created;
2131
+ } };
2132
+ }
2133
+ function applyArrInsert(base, head, it, newDot, indexSession, bumpCounterAbove, strictParents = false) {
1605
2134
  const pointer = `/${it.path.join("/")}`;
1606
2135
  const baseSeq = getSeqAtPath(base, it.path);
1607
2136
  if (!baseSeq) {
@@ -1633,8 +2162,9 @@ function applyArrInsert(base, head, it, newDot, bumpCounterAbove, strictParents
1633
2162
  const headSeqRes = getHeadSeqForBaseArrayIntent(head, it.path);
1634
2163
  if (!headSeqRes.ok) return headSeqRes;
1635
2164
  const headSeq = headSeqRes.seq;
1636
- const idx = it.index === Number.POSITIVE_INFINITY ? rgaLinearizeIds(baseSeq).length : it.index;
1637
- const baseLen = rgaLinearizeIds(baseSeq).length;
2165
+ const baseIndex = indexSession.get(baseSeq);
2166
+ const baseLen = baseIndex.length();
2167
+ const idx = it.index === Number.POSITIVE_INFINITY ? baseLen : it.index;
1638
2168
  if (idx < 0 || idx > baseLen) return {
1639
2169
  ok: false,
1640
2170
  code: 409,
@@ -1642,20 +2172,18 @@ function applyArrInsert(base, head, it, newDot, bumpCounterAbove, strictParents
1642
2172
  message: `index out of bounds at /${it.path.join("/")}/${it.index}`,
1643
2173
  path: `/${it.path.join("/")}/${it.index}`
1644
2174
  };
1645
- const prev = idx === 0 ? HEAD : rgaIdAtIndex(baseSeq, idx - 1) ?? HEAD;
2175
+ const prev = baseIndex.prevForInsertAt(idx);
1646
2176
  const dotRes = nextInsertDotForPrev(headSeq, prev, newDot, pointer, bumpCounterAbove);
1647
2177
  if (!dotRes.ok) return dotRes;
1648
2178
  const d = dotRes.dot;
1649
- rgaInsertAfter(headSeq, prev, dotToElemId(d), d, nodeFromJson(it.value, newDot));
2179
+ const id = dotToElemId(d);
2180
+ rgaInsertAfter(headSeq, prev, id, d, nodeFromJson(it.value, newDot));
2181
+ if (baseSeq === headSeq) baseIndex.insertAt(idx, id);
1650
2182
  return null;
1651
2183
  }
1652
2184
  function nextInsertDotForPrev(seq, prev, newDot, path, bumpCounterAbove) {
1653
2185
  const MAX_INSERT_DOT_ATTEMPTS = 1024;
1654
- let maxSiblingDot = null;
1655
- for (const elem of seq.elems.values()) {
1656
- if (elem.prev !== prev) continue;
1657
- if (!maxSiblingDot || compareDot(elem.insDot, maxSiblingDot) > 0) maxSiblingDot = elem.insDot;
1658
- }
2186
+ const maxSiblingDot = rgaMaxInsertDotForPrev(seq, prev);
1659
2187
  if (maxSiblingDot) bumpCounterAbove?.(maxSiblingDot.ctr);
1660
2188
  if (!maxSiblingDot) return {
1661
2189
  ok: true,
@@ -1676,8 +2204,8 @@ function nextInsertDotForPrev(seq, prev, newDot, path, bumpCounterAbove) {
1676
2204
  path
1677
2205
  };
1678
2206
  }
1679
- function applyArrDelete(base, head, it, newDot) {
1680
- newDot();
2207
+ function applyArrDelete(base, head, it, newDot, indexSession) {
2208
+ const _d = newDot();
1681
2209
  const baseSeq = getSeqAtPath(base, it.path);
1682
2210
  if (!baseSeq) return {
1683
2211
  ok: false,
@@ -1689,7 +2217,8 @@ function applyArrDelete(base, head, it, newDot) {
1689
2217
  const headSeqRes = getHeadSeqForBaseArrayIntent(head, it.path);
1690
2218
  if (!headSeqRes.ok) return headSeqRes;
1691
2219
  const headSeq = headSeqRes.seq;
1692
- const baseId = rgaIdAtIndex(baseSeq, it.index);
2220
+ const baseIndex = indexSession.get(baseSeq);
2221
+ const baseId = baseIndex.idAt(it.index);
1693
2222
  if (!baseId) return {
1694
2223
  ok: false,
1695
2224
  code: 409,
@@ -1704,10 +2233,11 @@ function applyArrDelete(base, head, it, newDot) {
1704
2233
  message: `element missing in head lineage at index ${it.index}`,
1705
2234
  path: `/${it.path.join("/")}/${it.index}`
1706
2235
  };
1707
- rgaDelete(headSeq, baseId);
2236
+ rgaDelete(headSeq, baseId, _d);
2237
+ if (baseSeq === headSeq) baseIndex.deleteAt(it.index);
1708
2238
  return null;
1709
2239
  }
1710
- function applyArrReplace(base, head, it, newDot) {
2240
+ function applyArrReplace(base, head, it, newDot, indexSession) {
1711
2241
  newDot();
1712
2242
  const baseSeq = getSeqAtPath(base, it.path);
1713
2243
  if (!baseSeq) return {
@@ -1720,7 +2250,7 @@ function applyArrReplace(base, head, it, newDot) {
1720
2250
  const headSeqRes = getHeadSeqForBaseArrayIntent(head, it.path);
1721
2251
  if (!headSeqRes.ok) return headSeqRes;
1722
2252
  const headSeq = headSeqRes.seq;
1723
- const baseId = rgaIdAtIndex(baseSeq, it.index);
2253
+ const baseId = indexSession.get(baseSeq).idAt(it.index);
1724
2254
  if (!baseId) return {
1725
2255
  ok: false,
1726
2256
  code: 409,
@@ -1753,6 +2283,7 @@ function applyArrReplace(base, head, it, newDot) {
1753
2283
  * @returns `{ ok: true }` on success, or `{ ok: false, code: 409, message }` on conflict.
1754
2284
  */
1755
2285
  function applyIntentsToCrdt(base, head, intents, newDot, evalTestAgainst = "head", bumpCounterAbove, options = {}) {
2286
+ const arrayIndexSession = createArrayIndexLookupSession();
1756
2287
  for (const it of intents) {
1757
2288
  let fail = null;
1758
2289
  switch (it.t) {
@@ -1766,13 +2297,13 @@ function applyIntentsToCrdt(base, head, intents, newDot, evalTestAgainst = "head
1766
2297
  fail = applyObjRemove(head, it, newDot);
1767
2298
  break;
1768
2299
  case "ArrInsert":
1769
- fail = applyArrInsert(base, head, it, newDot, bumpCounterAbove, options.strictParents ?? false);
2300
+ fail = applyArrInsert(base, head, it, newDot, arrayIndexSession, bumpCounterAbove, options.strictParents ?? false);
1770
2301
  break;
1771
2302
  case "ArrDelete":
1772
- fail = applyArrDelete(base, head, it, newDot);
2303
+ fail = applyArrDelete(base, head, it, newDot, arrayIndexSession);
1773
2304
  break;
1774
2305
  case "ArrReplace":
1775
- fail = applyArrReplace(base, head, it, newDot);
2306
+ fail = applyArrReplace(base, head, it, newDot, arrayIndexSession);
1776
2307
  break;
1777
2308
  default: assertNever(it, "Unhandled intent type");
1778
2309
  }
@@ -1814,15 +2345,199 @@ function jsonPatchToCrdtSafe(baseOrOptions, head, patch, newDot, evalTestAgainst
1814
2345
  }
1815
2346
  /** Alias for codebases that prefer `try*` naming for non-throwing APIs. */
1816
2347
  const tryJsonPatchToCrdt = jsonPatchToCrdtSafe;
2348
+ function nodeToJsonForPatch(node) {
2349
+ return node.kind === "lww" ? node.value : materialize(node);
2350
+ }
2351
+ function rebaseDiffOps(path, nestedOps, out) {
2352
+ const prefix = stringifyJsonPointer(path);
2353
+ for (const op of nestedOps) {
2354
+ const rebasedPath = prefix === "" ? op.path : op.path === "" ? prefix : `${prefix}${op.path}`;
2355
+ if (op.op === "remove") {
2356
+ out.push({
2357
+ op: "remove",
2358
+ path: rebasedPath
2359
+ });
2360
+ continue;
2361
+ }
2362
+ if (op.op === "add" || op.op === "replace") {
2363
+ out.push({
2364
+ op: op.op,
2365
+ path: rebasedPath,
2366
+ value: op.value
2367
+ });
2368
+ continue;
2369
+ }
2370
+ throw new Error(`Unexpected op '${op.op}' from diffJsonPatch`);
2371
+ }
2372
+ }
2373
+ function nodesJsonEqual(baseNode, headNode, depth) {
2374
+ assertTraversalDepth(depth);
2375
+ if (baseNode === headNode) return true;
2376
+ if (baseNode.kind !== headNode.kind) return false;
2377
+ if (baseNode.kind === "lww") {
2378
+ const headLww = headNode;
2379
+ return jsonEquals(baseNode.value, headLww.value);
2380
+ }
2381
+ if (baseNode.kind === "obj") {
2382
+ const headObj = headNode;
2383
+ if (baseNode.entries.size !== headObj.entries.size) return false;
2384
+ for (const [key, baseEntry] of baseNode.entries.entries()) {
2385
+ const headEntry = headObj.entries.get(key);
2386
+ if (!headEntry) return false;
2387
+ if (!nodesJsonEqual(baseEntry.node, headEntry.node, depth + 1)) return false;
2388
+ }
2389
+ return true;
2390
+ }
2391
+ const headSeq = headNode;
2392
+ const baseCursor = rgaCreateLinearCursor(baseNode);
2393
+ const headCursor = rgaCreateLinearCursor(headSeq);
2394
+ while (true) {
2395
+ const baseElem = baseCursor.next();
2396
+ const headElem = headCursor.next();
2397
+ if (baseElem === void 0 || headElem === void 0) return baseElem === void 0 && headElem === void 0;
2398
+ if (!nodesJsonEqual(baseElem.value, headElem.value, depth + 1)) return false;
2399
+ }
2400
+ }
2401
+ function diffObjectNodes(path, baseNode, headNode, options, ops, depth) {
2402
+ assertTraversalDepth(depth);
2403
+ const baseKeys = [...baseNode.entries.keys()].sort();
2404
+ const headKeys = [...headNode.entries.keys()].sort();
2405
+ let baseIndex = 0;
2406
+ let headIndex = 0;
2407
+ while (baseIndex < baseKeys.length && headIndex < headKeys.length) {
2408
+ const baseKey = baseKeys[baseIndex];
2409
+ const headKey = headKeys[headIndex];
2410
+ if (baseKey === headKey) {
2411
+ baseIndex += 1;
2412
+ headIndex += 1;
2413
+ continue;
2414
+ }
2415
+ if (baseKey < headKey) {
2416
+ path.push(baseKey);
2417
+ ops.push({
2418
+ op: "remove",
2419
+ path: stringifyJsonPointer(path)
2420
+ });
2421
+ path.pop();
2422
+ baseIndex += 1;
2423
+ continue;
2424
+ }
2425
+ headIndex += 1;
2426
+ }
2427
+ while (baseIndex < baseKeys.length) {
2428
+ const baseKey = baseKeys[baseIndex];
2429
+ path.push(baseKey);
2430
+ ops.push({
2431
+ op: "remove",
2432
+ path: stringifyJsonPointer(path)
2433
+ });
2434
+ path.pop();
2435
+ baseIndex += 1;
2436
+ }
2437
+ baseIndex = 0;
2438
+ headIndex = 0;
2439
+ while (baseIndex < baseKeys.length && headIndex < headKeys.length) {
2440
+ const baseKey = baseKeys[baseIndex];
2441
+ const headKey = headKeys[headIndex];
2442
+ if (baseKey === headKey) {
2443
+ baseIndex += 1;
2444
+ headIndex += 1;
2445
+ continue;
2446
+ }
2447
+ if (baseKey < headKey) {
2448
+ baseIndex += 1;
2449
+ continue;
2450
+ }
2451
+ const headEntry = headNode.entries.get(headKey);
2452
+ path.push(headKey);
2453
+ ops.push({
2454
+ op: "add",
2455
+ path: stringifyJsonPointer(path),
2456
+ value: nodeToJsonForPatch(headEntry.node)
2457
+ });
2458
+ path.pop();
2459
+ headIndex += 1;
2460
+ }
2461
+ while (headIndex < headKeys.length) {
2462
+ const headKey = headKeys[headIndex];
2463
+ const headEntry = headNode.entries.get(headKey);
2464
+ path.push(headKey);
2465
+ ops.push({
2466
+ op: "add",
2467
+ path: stringifyJsonPointer(path),
2468
+ value: nodeToJsonForPatch(headEntry.node)
2469
+ });
2470
+ path.pop();
2471
+ headIndex += 1;
2472
+ }
2473
+ baseIndex = 0;
2474
+ headIndex = 0;
2475
+ while (baseIndex < baseKeys.length && headIndex < headKeys.length) {
2476
+ const baseKey = baseKeys[baseIndex];
2477
+ const headKey = headKeys[headIndex];
2478
+ if (baseKey === headKey) {
2479
+ const baseEntry = baseNode.entries.get(baseKey);
2480
+ const headEntry = headNode.entries.get(headKey);
2481
+ if (!nodesJsonEqual(baseEntry.node, headEntry.node, depth + 1)) {
2482
+ path.push(baseKey);
2483
+ diffNodeToPatch(path, baseEntry.node, headEntry.node, options, ops, depth + 1);
2484
+ path.pop();
2485
+ }
2486
+ baseIndex += 1;
2487
+ headIndex += 1;
2488
+ continue;
2489
+ }
2490
+ if (baseKey < headKey) {
2491
+ baseIndex += 1;
2492
+ continue;
2493
+ }
2494
+ headIndex += 1;
2495
+ }
2496
+ }
2497
+ function diffNodeToPatch(path, baseNode, headNode, options, ops, depth) {
2498
+ assertTraversalDepth(depth);
2499
+ if (baseNode === headNode) return;
2500
+ if (baseNode.kind !== headNode.kind) {
2501
+ ops.push({
2502
+ op: "replace",
2503
+ path: stringifyJsonPointer(path),
2504
+ value: nodeToJsonForPatch(headNode)
2505
+ });
2506
+ return;
2507
+ }
2508
+ if (baseNode.kind === "lww") {
2509
+ const headLww = headNode;
2510
+ if (jsonEquals(baseNode.value, headLww.value)) return;
2511
+ ops.push({
2512
+ op: "replace",
2513
+ path: stringifyJsonPointer(path),
2514
+ value: headLww.value
2515
+ });
2516
+ return;
2517
+ }
2518
+ if (baseNode.kind === "obj") {
2519
+ diffObjectNodes(path, baseNode, headNode, options, ops, depth);
2520
+ return;
2521
+ }
2522
+ const headSeq = headNode;
2523
+ rebaseDiffOps(path, diffJsonPatch(materialize(baseNode), materialize(headSeq), options), ops);
2524
+ }
1817
2525
  /**
1818
2526
  * Generate a JSON Patch delta between two CRDT documents.
1819
2527
  * @param base - The base document snapshot.
1820
2528
  * @param head - The current document state.
1821
- * @param options - Diff options (e.g. `{ arrayStrategy: "lcs" }`).
2529
+ * @param options - Diff options (e.g. `{ arrayStrategy: "lcs" }` or `{ arrayStrategy: "lcs-linear" }`).
1822
2530
  * @returns An array of JSON Patch operations that transform base into head.
1823
2531
  */
1824
2532
  function crdtToJsonPatch(base, head, options) {
1825
- return diffJsonPatch(materialize(base.root), materialize(head.root), options);
2533
+ if ((options?.jsonValidation ?? "none") !== "none") return diffJsonPatch(materialize(base.root), materialize(head.root), options);
2534
+ return crdtNodesToJsonPatch(base.root, head.root, options);
2535
+ }
2536
+ /** Internals-only helper for diffing CRDT nodes from an existing traversal depth. */
2537
+ function crdtNodesToJsonPatch(baseNode, headNode, options, depth = 0) {
2538
+ const ops = [];
2539
+ diffNodeToPatch([], baseNode, headNode, options ?? {}, ops, depth);
2540
+ return ops;
1826
2541
  }
1827
2542
  /**
1828
2543
  * Emit a single root `replace` patch representing the full document state.
@@ -2125,9 +2840,12 @@ function toApplyPatchOptionsForActor(options) {
2125
2840
  };
2126
2841
  }
2127
2842
  function applyPatchInternal(state, patch, options, execution) {
2843
+ const preparedPatch = preparePatchPayloadsSafe(patch, options.jsonValidation ?? "none");
2844
+ if (!preparedPatch.ok) return preparedPatch;
2845
+ const runtimePatch = preparedPatch.patch;
2128
2846
  if ((options.semantics ?? "sequential") === "sequential") {
2129
2847
  if (!options.base && execution === "batch") {
2130
- const compiled = compileIntents(materialize(state.doc.root), patch, "sequential", options.jsonValidation ?? "none");
2848
+ const compiled = compilePreparedIntents(materialize(state.doc.root), runtimePatch, "sequential");
2131
2849
  if (!compiled.ok) return compiled;
2132
2850
  return applyIntentsToCrdt(state.doc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
2133
2851
  }
@@ -2135,10 +2853,15 @@ function applyPatchInternal(state, patch, options, execution) {
2135
2853
  doc: cloneDoc(options.base.doc),
2136
2854
  clock: createClock("__base__", 0)
2137
2855
  } : null;
2856
+ const session = {
2857
+ pointerCache: /* @__PURE__ */ new Map(),
2858
+ baseShadowParentCache: /* @__PURE__ */ new Map(),
2859
+ headShadowParentCache: /* @__PURE__ */ new Map()
2860
+ };
2138
2861
  let sequentialHeadJson = materialize(state.doc.root);
2139
2862
  let sequentialBaseJson = explicitBaseState ? materialize(explicitBaseState.doc.root) : sequentialHeadJson;
2140
- for (const [opIndex, op] of patch.entries()) {
2141
- const step = applyPatchOpSequential(state, op, options, explicitBaseState ? explicitBaseState.doc : state.doc, sequentialBaseJson, sequentialHeadJson, explicitBaseState, opIndex);
2863
+ for (const [opIndex, op] of runtimePatch.entries()) {
2864
+ const step = applyPatchOpSequential(state, op, options, explicitBaseState ? explicitBaseState.doc : state.doc, sequentialBaseJson, sequentialHeadJson, explicitBaseState, opIndex, session);
2142
2865
  if (!step.ok) return step;
2143
2866
  sequentialBaseJson = step.baseJson;
2144
2867
  sequentialHeadJson = step.headJson;
@@ -2146,29 +2869,29 @@ function applyPatchInternal(state, patch, options, execution) {
2146
2869
  return { ok: true };
2147
2870
  }
2148
2871
  const baseDoc = options.base ? options.base.doc : cloneDoc(state.doc);
2149
- const compiled = compileIntents(materialize(baseDoc.root), patch, "base", options.jsonValidation ?? "none");
2872
+ const compiled = compilePreparedIntents(materialize(baseDoc.root), runtimePatch, "base");
2150
2873
  if (!compiled.ok) return compiled;
2151
2874
  return applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
2152
2875
  }
2153
- function applyPatchOpSequential(state, op, options, baseDoc, baseJson, headJson, explicitBaseState, opIndex) {
2876
+ function applyPatchOpSequential(state, op, options, baseDoc, baseJson, headJson, explicitBaseState, opIndex, session) {
2154
2877
  if (op.op === "move") {
2155
- const fromResolved = resolveValueAtPointer(baseJson, op.from, opIndex);
2878
+ const fromResolved = resolveValueAtPointer(baseJson, op.from, opIndex, session.pointerCache);
2156
2879
  if (!fromResolved.ok) return fromResolved;
2157
2880
  const fromValue = structuredClone(fromResolved.value);
2158
2881
  const removeRes = applySinglePatchOpSequentialStep(state, baseDoc, baseJson, headJson, {
2159
2882
  op: "remove",
2160
2883
  path: op.from
2161
- }, options, explicitBaseState);
2884
+ }, options, explicitBaseState, opIndex, session);
2162
2885
  if (!removeRes.ok) return removeRes;
2163
2886
  const addOp = {
2164
2887
  op: "add",
2165
2888
  path: op.path,
2166
2889
  value: fromValue
2167
2890
  };
2168
- if (!explicitBaseState) return applySinglePatchOpSequentialStep(state, state.doc, removeRes.baseJson, removeRes.headJson, addOp, options, null);
2169
- const headAddRes = applySinglePatchOpSequentialStep(state, state.doc, removeRes.headJson, removeRes.headJson, addOp, options, null);
2891
+ if (!explicitBaseState) return applySinglePatchOpSequentialStep(state, state.doc, removeRes.baseJson, removeRes.headJson, addOp, options, null, opIndex, session);
2892
+ const headAddRes = applySinglePatchOpSequentialStep(state, state.doc, removeRes.headJson, removeRes.headJson, addOp, options, null, opIndex, session);
2170
2893
  if (!headAddRes.ok) return headAddRes;
2171
- const shadowAddRes = applySinglePatchOpExplicitShadowStep(explicitBaseState, removeRes.baseJson, addOp, options);
2894
+ const shadowAddRes = applySinglePatchOpExplicitShadowStep(explicitBaseState, removeRes.baseJson, addOp, options, opIndex, session);
2172
2895
  if (!shadowAddRes.ok) return shadowAddRes;
2173
2896
  return {
2174
2897
  ok: true,
@@ -2177,18 +2900,18 @@ function applyPatchOpSequential(state, op, options, baseDoc, baseJson, headJson,
2177
2900
  };
2178
2901
  }
2179
2902
  if (op.op === "copy") {
2180
- const fromResolved = resolveValueAtPointer(baseJson, op.from, opIndex);
2903
+ const fromResolved = resolveValueAtPointer(baseJson, op.from, opIndex, session.pointerCache);
2181
2904
  if (!fromResolved.ok) return fromResolved;
2182
2905
  return applySinglePatchOpSequentialStep(state, baseDoc, baseJson, headJson, {
2183
2906
  op: "add",
2184
2907
  path: op.path,
2185
2908
  value: structuredClone(fromResolved.value)
2186
- }, options, explicitBaseState);
2909
+ }, options, explicitBaseState, opIndex, session);
2187
2910
  }
2188
- return applySinglePatchOpSequentialStep(state, baseDoc, baseJson, headJson, op, options, explicitBaseState);
2911
+ return applySinglePatchOpSequentialStep(state, baseDoc, baseJson, headJson, op, options, explicitBaseState, opIndex, session);
2189
2912
  }
2190
- function applySinglePatchOpSequentialStep(state, baseDoc, baseJson, headJson, op, options, explicitBaseState) {
2191
- const compiled = compileIntents(baseJson, [op], "sequential", options.jsonValidation ?? "none");
2913
+ function applySinglePatchOpSequentialStep(state, baseDoc, baseJson, headJson, op, options, explicitBaseState, opIndex, session) {
2914
+ const compiled = compilePreparedIntents(baseJson, [op], "base", session.pointerCache, opIndex);
2192
2915
  if (!compiled.ok) return compiled;
2193
2916
  const headStep = applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
2194
2917
  if (!headStep.ok) return headStep;
@@ -2201,15 +2924,21 @@ function applySinglePatchOpSequentialStep(state, baseDoc, baseJson, headJson, op
2201
2924
  baseJson,
2202
2925
  headJson
2203
2926
  };
2204
- const nextBaseJson = applyJsonPatchOpToShadow(baseJson, op);
2927
+ const nextBaseJson = applyJsonPatchOpToShadow(baseJson, op, explicitBaseState ? session.baseShadowParentCache : session.headShadowParentCache, {
2928
+ pointerCache: session.pointerCache,
2929
+ opIndex
2930
+ });
2205
2931
  return {
2206
2932
  ok: true,
2207
2933
  baseJson: nextBaseJson,
2208
- headJson: explicitBaseState ? applyJsonPatchOpToShadow(headJson, op) : nextBaseJson
2934
+ headJson: explicitBaseState ? applyJsonPatchOpToShadow(headJson, op, session.headShadowParentCache, {
2935
+ pointerCache: session.pointerCache,
2936
+ opIndex
2937
+ }) : nextBaseJson
2209
2938
  };
2210
2939
  }
2211
- function applySinglePatchOpExplicitShadowStep(explicitBaseState, baseJson, op, options) {
2212
- const compiled = compileIntents(baseJson, [op], "sequential", options.jsonValidation ?? "none");
2940
+ function applySinglePatchOpExplicitShadowStep(explicitBaseState, baseJson, op, options, opIndex, session) {
2941
+ const compiled = compilePreparedIntents(baseJson, [op], "base", session.pointerCache, opIndex);
2213
2942
  if (!compiled.ok) return compiled;
2214
2943
  const shadowStep = applyIntentsToCrdt(explicitBaseState.doc, explicitBaseState.doc, compiled.intents, () => explicitBaseState.clock.next(), "base", (ctr) => bumpClockCounter(explicitBaseState, ctr), { strictParents: options.strictParents });
2215
2944
  if (!shadowStep.ok) return shadowStep;
@@ -2219,32 +2948,46 @@ function applySinglePatchOpExplicitShadowStep(explicitBaseState, baseJson, op, o
2219
2948
  };
2220
2949
  return {
2221
2950
  ok: true,
2222
- baseJson: applyJsonPatchOpToShadow(baseJson, op)
2951
+ baseJson: applyJsonPatchOpToShadow(baseJson, op, session.baseShadowParentCache, {
2952
+ pointerCache: session.pointerCache,
2953
+ opIndex
2954
+ })
2223
2955
  };
2224
2956
  }
2225
- function applyJsonPatchOpToShadow(baseJson, op) {
2226
- const path = parseJsonPointer(op.path);
2957
+ function applyJsonPatchOpToShadow(baseJson, op, parentCache, pointerContext) {
2958
+ let path;
2959
+ try {
2960
+ path = parsePointerWithCache(op.path, pointerContext.pointerCache);
2961
+ } catch (error) {
2962
+ throw toPointerParseCompileError(error, op.path, pointerContext.opIndex);
2963
+ }
2227
2964
  if (path.length === 0) {
2965
+ parentCache.clear();
2228
2966
  if (op.op === "test") return baseJson;
2229
2967
  if (op.op === "remove") return null;
2230
2968
  return structuredClone(op.value);
2231
2969
  }
2970
+ const pathPointer = op.path;
2232
2971
  const parentPath = path.slice(0, -1);
2972
+ const parentPointer = pointerParent(pathPointer);
2233
2973
  const key = path[path.length - 1];
2234
- const parent = getAtJson(baseJson, parentPath);
2974
+ const parent = resolveShadowParent(baseJson, parentPath, parentPointer, parentCache);
2235
2975
  if (Array.isArray(parent)) {
2236
2976
  const idx = key === "-" ? parent.length : Number(key);
2237
2977
  if (!Number.isInteger(idx)) throw new Error(`Invalid array index ${key}`);
2238
2978
  if (op.op === "add") {
2239
2979
  parent.splice(idx, 0, structuredClone(op.value));
2980
+ invalidateArrayShadowParentCache(parentCache, parentPointer);
2240
2981
  return baseJson;
2241
2982
  }
2242
2983
  if (op.op === "remove") {
2243
2984
  parent.splice(idx, 1);
2985
+ invalidateArrayShadowParentCache(parentCache, parentPointer);
2244
2986
  return baseJson;
2245
2987
  }
2246
2988
  if (op.op === "replace") {
2247
2989
  parent[idx] = structuredClone(op.value);
2990
+ invalidateShadowPointerCache(parentCache, pathPointer);
2248
2991
  return baseJson;
2249
2992
  }
2250
2993
  return baseJson;
@@ -2252,18 +2995,57 @@ function applyJsonPatchOpToShadow(baseJson, op) {
2252
2995
  const obj = parent;
2253
2996
  if (op.op === "add" || op.op === "replace") {
2254
2997
  obj[key] = structuredClone(op.value);
2998
+ invalidateShadowPointerCache(parentCache, pathPointer);
2255
2999
  return baseJson;
2256
3000
  }
2257
3001
  if (op.op === "remove") {
2258
3002
  delete obj[key];
3003
+ invalidateShadowPointerCache(parentCache, pathPointer);
2259
3004
  return baseJson;
2260
3005
  }
2261
3006
  return baseJson;
2262
3007
  }
2263
- function resolveValueAtPointer(baseJson, pointer, opIndex) {
3008
+ function resolveShadowParent(baseJson, parentPath, parentPointer, parentCache) {
3009
+ const cachedParent = parentCache.get(parentPointer);
3010
+ if (cachedParent !== void 0) return cachedParent;
3011
+ const parentValue = parentPath.length === 0 ? baseJson : getAtJson(baseJson, parentPath);
3012
+ if (!Array.isArray(parentValue) && !(parentValue && typeof parentValue === "object")) throw new Error(`Cannot mutate JSON shadow at non-container parent ${parentPointer || "<root>"}`);
3013
+ parentCache.set(parentPointer, parentValue);
3014
+ return parentValue;
3015
+ }
3016
+ function invalidateShadowPointerCache(parentCache, pointer) {
3017
+ if (pointer === "") {
3018
+ parentCache.clear();
3019
+ return;
3020
+ }
3021
+ const pointerPrefix = `${pointer}/`;
3022
+ for (const cachedPointer of parentCache.keys()) if (cachedPointer === pointer || cachedPointer.startsWith(pointerPrefix)) parentCache.delete(cachedPointer);
3023
+ }
3024
+ function invalidateArrayShadowParentCache(parentCache, parentPointer) {
3025
+ if (parentPointer === "") {
3026
+ for (const cachedPointer of parentCache.keys()) if (cachedPointer !== "") parentCache.delete(cachedPointer);
3027
+ return;
3028
+ }
3029
+ const pointerPrefix = `${parentPointer}/`;
3030
+ for (const cachedPointer of parentCache.keys()) if (cachedPointer.startsWith(pointerPrefix)) parentCache.delete(cachedPointer);
3031
+ }
3032
+ function pointerParent(pointer) {
3033
+ if (pointer === "") return "";
3034
+ const lastSlash = pointer.lastIndexOf("/");
3035
+ if (lastSlash <= 0) return "";
3036
+ return pointer.slice(0, lastSlash);
3037
+ }
3038
+ function parsePointerWithCache(pointer, pointerCache) {
3039
+ const cachedPath = pointerCache.get(pointer);
3040
+ if (cachedPath !== void 0) return cachedPath.slice();
3041
+ const parsedPath = parseJsonPointer(pointer);
3042
+ pointerCache.set(pointer, parsedPath);
3043
+ return parsedPath.slice();
3044
+ }
3045
+ function resolveValueAtPointer(baseJson, pointer, opIndex, pointerCache) {
2264
3046
  let path;
2265
3047
  try {
2266
- path = parseJsonPointer(pointer);
3048
+ path = parsePointerWithCache(pointer, pointerCache);
2267
3049
  } catch (error) {
2268
3050
  return toPointerParseApplyError(error, pointer, opIndex);
2269
3051
  }
@@ -2279,11 +3061,33 @@ function resolveValueAtPointer(baseJson, pointer, opIndex) {
2279
3061
  function bumpClockCounter(state, ctr) {
2280
3062
  if (state.clock.ctr < ctr) state.clock.ctr = ctr;
2281
3063
  }
2282
- function compileIntents(baseJson, patch, semantics = "sequential", jsonValidation = "none") {
3064
+ function compilePreparedIntents(baseJson, patch, semantics = "sequential", pointerCache, opIndexOffset = 0) {
3065
+ try {
3066
+ const compileOptions = toCompilePatchOptions(semantics, pointerCache, opIndexOffset);
3067
+ if (patch.length === 1) return {
3068
+ ok: true,
3069
+ intents: compileJsonPatchOpToIntent(baseJson, patch[0], compileOptions)
3070
+ };
3071
+ return {
3072
+ ok: true,
3073
+ intents: compileJsonPatchToIntent(baseJson, patch, compileOptions)
3074
+ };
3075
+ } catch (error) {
3076
+ return toApplyError(error);
3077
+ }
3078
+ }
3079
+ function toCompilePatchOptions(semantics, pointerCache, opIndexOffset = 0) {
3080
+ return {
3081
+ semantics,
3082
+ pointerCache,
3083
+ opIndexOffset
3084
+ };
3085
+ }
3086
+ function preparePatchPayloadsSafe(patch, mode) {
2283
3087
  try {
2284
3088
  return {
2285
3089
  ok: true,
2286
- intents: compileJsonPatchToIntent(baseJson, preparePatchPayloads(patch, jsonValidation), { semantics })
3090
+ patch: preparePatchPayloads(patch, mode)
2287
3091
  };
2288
3092
  } catch (error) {
2289
3093
  return toApplyError(error);
@@ -2384,6 +3188,9 @@ function toPointerParseApplyError(error, pointer, opIndex) {
2384
3188
  opIndex
2385
3189
  };
2386
3190
  }
3191
+ function toPointerParseCompileError(error, pointer, opIndex) {
3192
+ return new PatchCompileError("INVALID_POINTER", error instanceof Error ? error.message : "invalid pointer", pointer, opIndex);
3193
+ }
2387
3194
  function toPointerLookupApplyError(error, pointer, opIndex) {
2388
3195
  const mapped = mapLookupErrorToPatchReason(error);
2389
3196
  return {
@@ -2463,10 +3270,13 @@ function deserializeState(data) {
2463
3270
  if (!("doc" in data)) fail("INVALID_SERIALIZED_SHAPE", "/doc", "serialized state is missing doc");
2464
3271
  if (!("clock" in data)) fail("INVALID_SERIALIZED_SHAPE", "/clock", "serialized state is missing clock");
2465
3272
  const clockRaw = asRecord(data.clock, "/clock");
2466
- const clock = createClock(readActor(clockRaw.actor, "/clock/actor"), readCounter(clockRaw.ctr, "/clock/ctr"));
3273
+ const actor = readActor(clockRaw.actor, "/clock/actor");
3274
+ const ctr = readCounter(clockRaw.ctr, "/clock/ctr");
3275
+ const doc = deserializeDoc(data.doc);
3276
+ const observedCtr = maxObservedCounterForActorInNode(doc.root, actor);
2467
3277
  return {
2468
- doc: deserializeDoc(data.doc),
2469
- clock
3278
+ doc,
3279
+ clock: createClock(actor, Math.max(ctr, observedCtr))
2470
3280
  };
2471
3281
  }
2472
3282
  /** Non-throwing `deserializeState` variant with typed validation details. */
@@ -2515,16 +3325,23 @@ function serializeNode(node) {
2515
3325
  };
2516
3326
  }
2517
3327
  const elems = createSerializedRecord();
2518
- for (const [id, e] of node.elems.entries()) setSerializedRecordValue(elems, id, {
2519
- id: e.id,
2520
- prev: e.prev,
2521
- tombstone: e.tombstone,
2522
- value: serializeNode(e.value),
2523
- insDot: {
2524
- actor: e.insDot.actor,
2525
- ctr: e.insDot.ctr
2526
- }
2527
- });
3328
+ for (const [id, e] of node.elems.entries()) {
3329
+ const serializedElem = {
3330
+ id: e.id,
3331
+ prev: e.prev,
3332
+ tombstone: e.tombstone,
3333
+ value: serializeNode(e.value),
3334
+ insDot: {
3335
+ actor: e.insDot.actor,
3336
+ ctr: e.insDot.ctr
3337
+ }
3338
+ };
3339
+ if (e.delDot) serializedElem.delDot = {
3340
+ actor: e.delDot.actor,
3341
+ ctr: e.delDot.ctr
3342
+ };
3343
+ setSerializedRecordValue(elems, id, serializedElem);
3344
+ }
2528
3345
  return {
2529
3346
  kind: "seq",
2530
3347
  elems
@@ -2575,11 +3392,14 @@ function deserializeNode(node, path, depth) {
2575
3392
  const tombstone = readBoolean(elem.tombstone, `${elemPath}/tombstone`);
2576
3393
  const value = deserializeNode(elem.value, `${elemPath}/value`, depth + 1);
2577
3394
  const insDot = readDot(elem.insDot, `${elemPath}/insDot`);
3395
+ const delDot = "delDot" in elem && elem.delDot !== void 0 ? readDot(elem.delDot, `${elemPath}/delDot`) : void 0;
2578
3396
  if (dotToElemId(insDot) !== id) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/insDot`, "sequence element id must match its insertion dot");
3397
+ if (!tombstone && delDot) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/delDot`, "live sequence elements must not include delete metadata");
2579
3398
  elems.set(id, {
2580
3399
  id,
2581
3400
  prev,
2582
3401
  tombstone,
3402
+ delDot,
2583
3403
  value,
2584
3404
  insDot
2585
3405
  });
@@ -2614,6 +3434,24 @@ function assertAcyclicRgaPredecessors(elems, path) {
2614
3434
  for (const id of trail) visitState.set(id, 2);
2615
3435
  }
2616
3436
  }
3437
+ function maxObservedCounterForActorInNode(node, actor) {
3438
+ if (node.kind === "lww") return node.dot.actor === actor ? node.dot.ctr : 0;
3439
+ if (node.kind === "obj") {
3440
+ let maxCtr = 0;
3441
+ for (const entry of node.entries.values()) {
3442
+ if (entry.dot.actor === actor) maxCtr = Math.max(maxCtr, entry.dot.ctr);
3443
+ maxCtr = Math.max(maxCtr, maxObservedCounterForActorInNode(entry.node, actor));
3444
+ }
3445
+ for (const tombstoneDot of node.tombstone.values()) if (tombstoneDot.actor === actor) maxCtr = Math.max(maxCtr, tombstoneDot.ctr);
3446
+ return maxCtr;
3447
+ }
3448
+ let maxCtr = 0;
3449
+ for (const elem of node.elems.values()) {
3450
+ if (elem.insDot.actor === actor) maxCtr = Math.max(maxCtr, elem.insDot.ctr);
3451
+ maxCtr = Math.max(maxCtr, maxObservedCounterForActorInNode(elem.value, actor));
3452
+ }
3453
+ return maxCtr;
3454
+ }
2617
3455
  function asRecord(value, path) {
2618
3456
  if (!isRecord(value)) fail("INVALID_SERIALIZED_SHAPE", path, "expected object");
2619
3457
  return value;
@@ -2958,6 +3796,7 @@ function mergeSeq(a, b, depth, path) {
2958
3796
  id,
2959
3797
  prev: ea.prev,
2960
3798
  tombstone: ea.tombstone || eb.tombstone,
3799
+ delDot: mergeDeleteDot(ea.delDot, eb.delDot),
2961
3800
  value: mergedValue,
2962
3801
  insDot: { ...ea.insDot }
2963
3802
  });
@@ -2982,10 +3821,16 @@ function cloneElem(e, depth) {
2982
3821
  id: e.id,
2983
3822
  prev: e.prev,
2984
3823
  tombstone: e.tombstone,
3824
+ delDot: e.delDot ? { ...e.delDot } : void 0,
2985
3825
  value: cloneNodeShallow(e.value, depth + 1),
2986
3826
  insDot: { ...e.insDot }
2987
3827
  };
2988
3828
  }
3829
+ function mergeDeleteDot(a, b) {
3830
+ if (a && b) return compareDot(a, b) >= 0 ? { ...a } : { ...b };
3831
+ if (a) return { ...a };
3832
+ if (b) return { ...b };
3833
+ }
2989
3834
  function cloneNodeShallow(node, depth) {
2990
3835
  assertTraversalDepth(depth);
2991
3836
  switch (node.kind) {
@@ -3219,6 +4064,12 @@ Object.defineProperty(exports, 'compileJsonPatchToIntent', {
3219
4064
  return compileJsonPatchToIntent;
3220
4065
  }
3221
4066
  });
4067
+ Object.defineProperty(exports, 'crdtNodesToJsonPatch', {
4068
+ enumerable: true,
4069
+ get: function () {
4070
+ return crdtNodesToJsonPatch;
4071
+ }
4072
+ });
3222
4073
  Object.defineProperty(exports, 'crdtToFullReplace', {
3223
4074
  enumerable: true,
3224
4075
  get: function () {
@@ -3411,6 +4262,12 @@ Object.defineProperty(exports, 'rgaInsertAfter', {
3411
4262
  return rgaInsertAfter;
3412
4263
  }
3413
4264
  });
4265
+ Object.defineProperty(exports, 'rgaInsertAfterChecked', {
4266
+ enumerable: true,
4267
+ get: function () {
4268
+ return rgaInsertAfterChecked;
4269
+ }
4270
+ });
3414
4271
  Object.defineProperty(exports, 'rgaLinearizeIds', {
3415
4272
  enumerable: true,
3416
4273
  get: function () {
@@ -3501,6 +4358,12 @@ Object.defineProperty(exports, 'validateJsonPatch', {
3501
4358
  return validateJsonPatch;
3502
4359
  }
3503
4360
  });
4361
+ Object.defineProperty(exports, 'validateRgaSeq', {
4362
+ enumerable: true,
4363
+ get: function () {
4364
+ return validateRgaSeq;
4365
+ }
4366
+ });
3504
4367
  Object.defineProperty(exports, 'vvHasDot', {
3505
4368
  enumerable: true,
3506
4369
  get: function () {