json-patch-to-crdt 0.4.0 → 1.0.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.
@@ -1,3 +1,129 @@
1
+ //#region src/budget.ts
2
+ function isNonNegativeSafeInteger(value) {
3
+ return Number.isSafeInteger(value) && value >= 0;
4
+ }
5
+ function formatPath(path) {
6
+ if (path === void 0 || path === "") return "";
7
+ return ` at ${path}`;
8
+ }
9
+ function formatOpIndex(opIndex) {
10
+ if (opIndex === void 0) return "";
11
+ return ` at op ${opIndex}`;
12
+ }
13
+ var ResourceBudgetError = class extends Error {
14
+ reason = "RESOURCE_BUDGET_EXCEEDED";
15
+ code = 409;
16
+ budget;
17
+ limit;
18
+ actual;
19
+ path;
20
+ opIndex;
21
+ constructor(budget, limit, actual, path, opIndex) {
22
+ super(`resource budget '${budget}' exceeded${formatPath(path)}${formatOpIndex(opIndex)}: ${actual} > ${limit}`);
23
+ this.name = "ResourceBudgetError";
24
+ this.budget = budget;
25
+ this.limit = limit;
26
+ this.actual = actual;
27
+ this.path = path;
28
+ this.opIndex = opIndex;
29
+ }
30
+ };
31
+ var ResourceBudgetMeter = class {
32
+ #budget;
33
+ #counts;
34
+ constructor(budget) {
35
+ this.#budget = validateBudget(budget);
36
+ this.#counts = {
37
+ patchOperations: 0,
38
+ objectEntries: 0,
39
+ sequenceElements: 0,
40
+ visitedNodes: 0,
41
+ serializedElements: 0,
42
+ arrayDiffCells: 0
43
+ };
44
+ }
45
+ count(kind, delta, path, opIndex) {
46
+ if (delta <= 0) return;
47
+ this.#counts[kind] += delta;
48
+ const limit = this.#budget[kind];
49
+ if (limit !== void 0 && this.#counts[kind] > limit) throw new ResourceBudgetError(kind, limit, this.#counts[kind], path, opIndex);
50
+ }
51
+ };
52
+ function createBudgetMeter(budget) {
53
+ if (budget === void 0) return;
54
+ return new ResourceBudgetMeter(budget);
55
+ }
56
+ function toBudgetApplyError(error) {
57
+ return {
58
+ ok: false,
59
+ code: error.code,
60
+ reason: error.reason,
61
+ message: error.message,
62
+ budget: error.budget,
63
+ limit: error.limit,
64
+ actual: error.actual,
65
+ path: error.path,
66
+ opIndex: error.opIndex
67
+ };
68
+ }
69
+ function toBudgetDeserializeFailure(error) {
70
+ return {
71
+ code: error.code,
72
+ reason: error.reason,
73
+ message: error.message,
74
+ budget: error.budget,
75
+ limit: error.limit,
76
+ actual: error.actual,
77
+ path: error.path
78
+ };
79
+ }
80
+ function validateBudget(budget) {
81
+ if (budget === void 0) return {};
82
+ const normalized = {};
83
+ const entries = Object.entries(budget);
84
+ for (const [key, value] of entries) {
85
+ if (value === void 0) continue;
86
+ if (!isNonNegativeSafeInteger(value)) throw new Error(`resource budget '${key}' must be a non-negative safe integer`);
87
+ normalized[key] = value;
88
+ }
89
+ return normalized;
90
+ }
91
+
92
+ //#endregion
93
+ //#region src/cancellation.ts
94
+ var OperationCancelledError = class extends Error {
95
+ reasonValue;
96
+ constructor(reason) {
97
+ super(toCancellationMessage(reason));
98
+ this.name = "OperationCancelledError";
99
+ this.reasonValue = reason;
100
+ }
101
+ };
102
+ function throwIfAborted(signal) {
103
+ if (signal?.aborted) throw new OperationCancelledError(signal.reason);
104
+ }
105
+ function toCancellationApplyError(error) {
106
+ return {
107
+ ok: false,
108
+ code: 409,
109
+ reason: "OPERATION_CANCELLED",
110
+ message: error.message
111
+ };
112
+ }
113
+ function toCancellationDeserializeFailure(error) {
114
+ return {
115
+ code: 409,
116
+ reason: "OPERATION_CANCELLED",
117
+ message: error.message
118
+ };
119
+ }
120
+ function toCancellationMessage(reason) {
121
+ if (reason instanceof Error && reason.message.length > 0) return `operation cancelled: ${reason.message}`;
122
+ if (typeof reason === "string" && reason.length > 0) return `operation cancelled: ${reason}`;
123
+ return "operation cancelled";
124
+ }
125
+
126
+ //#endregion
1
127
  //#region src/depth.ts
2
128
  const MAX_TRAVERSAL_DEPTH = 16384;
3
129
  var TraversalDepthError = class extends Error {
@@ -26,6 +152,8 @@ function toDepthApplyError(error) {
26
152
 
27
153
  //#endregion
28
154
  //#region src/version-vector.ts
155
+ let observedVersionVectorObserverForTests = null;
156
+ const observedVersionVectorCache = /* @__PURE__ */ new WeakMap();
29
157
  function readVersionVectorCounter(vv, actor) {
30
158
  if (!Object.prototype.hasOwnProperty.call(vv, actor)) return;
31
159
  const counter = vv[actor];
@@ -42,6 +170,21 @@ function writeVersionVectorCounter(vv, actor, counter) {
42
170
  function observeVersionVectorDot(vv, dot) {
43
171
  if ((readVersionVectorCounter(vv, dot.actor) ?? 0) < dot.ctr) writeVersionVectorCounter(vv, dot.actor, dot.ctr);
44
172
  }
173
+ function cloneVersionVector(vv) {
174
+ return mergeVersionVectors(vv);
175
+ }
176
+ function readCachedObservedVersionVector(doc) {
177
+ const cached = observedVersionVectorCache.get(doc);
178
+ return cached ? cloneVersionVector(cached) : void 0;
179
+ }
180
+ function writeCachedObservedVersionVector(doc, vv) {
181
+ observedVersionVectorCache.set(doc, cloneVersionVector(vv));
182
+ }
183
+ function observeDocVersionVectorDot(doc, dot) {
184
+ const cached = observedVersionVectorCache.get(doc);
185
+ if (!cached) return;
186
+ observeVersionVectorDot(cached, dot);
187
+ }
45
188
  /**
46
189
  * Inspect a document or state and return the highest observed counter per actor.
47
190
  *
@@ -51,11 +194,14 @@ function observeVersionVectorDot(vv, dot) {
51
194
  */
52
195
  function observedVersionVector(target) {
53
196
  const doc = "doc" in target ? target.doc : target;
54
- const vv = Object.create(null);
197
+ const cached = readCachedObservedVersionVector(doc);
198
+ const vv = cached ?? Object.create(null);
55
199
  if ("clock" in target) observeVersionVectorDot(vv, {
56
200
  actor: target.clock.actor,
57
201
  ctr: target.clock.ctr
58
202
  });
203
+ if (cached) return vv;
204
+ observedVersionVectorObserverForTests?.(target);
59
205
  const stack = [{
60
206
  node: doc.root,
61
207
  depth: 0
@@ -87,6 +233,7 @@ function observedVersionVector(target) {
87
233
  });
88
234
  }
89
235
  }
236
+ writeCachedObservedVersionVector(doc, vv);
90
237
  return vv;
91
238
  }
92
239
  /** Combine version vectors using per-actor maxima. */
@@ -493,6 +640,7 @@ function rgaPrevForInsertAtIndex(seq, index) {
493
640
  //#endregion
494
641
  //#region src/materialize.ts
495
642
  let materializeObserver = null;
643
+ const EMPTY_PATH = [];
496
644
  function createMaterializedObject() {
497
645
  return Object.create(null);
498
646
  }
@@ -507,7 +655,7 @@ function setMaterializedProperty(out, key, value) {
507
655
  /** Convert a CRDT node graph into a plain JSON value using an explicit stack. */
508
656
  function materialize(node) {
509
657
  const observer = materializeObserver;
510
- observer?.([], node);
658
+ observer?.(EMPTY_PATH, node);
511
659
  if (node.kind === "lww") return node.value;
512
660
  const root = node.kind === "obj" ? createMaterializedObject() : [];
513
661
  const stack = [];
@@ -538,7 +686,7 @@ function materialize(node) {
538
686
  const child = entry.node;
539
687
  const childDepth = frame.depth + 1;
540
688
  assertTraversalDepth(childDepth);
541
- const childPath = [...frame.path, key];
689
+ const childPath = observer ? [...frame.path, key] : EMPTY_PATH;
542
690
  observer?.(childPath, child);
543
691
  if (child.kind === "lww") {
544
692
  setMaterializedProperty(frame.out, key, child.value);
@@ -576,7 +724,7 @@ function materialize(node) {
576
724
  const child = elem.value;
577
725
  const childDepth = frame.depth + 1;
578
726
  assertTraversalDepth(childDepth);
579
- const childPath = [...frame.path, String(frame.nextIndex)];
727
+ const childPath = observer ? [...frame.path, String(frame.nextIndex)] : EMPTY_PATH;
580
728
  frame.nextIndex += 1;
581
729
  observer?.(childPath, child);
582
730
  if (child.kind === "lww") {
@@ -929,11 +1077,13 @@ function getAtJson(base, path) {
929
1077
  */
930
1078
  function compileJsonPatchToIntent(baseJson, patch, options = {}) {
931
1079
  const internalOptions = options;
1080
+ const budgetMeter = internalOptions.budgetMeter ?? createBudgetMeter(options.resourceBudget);
932
1081
  const semantics = options.semantics ?? "sequential";
933
1082
  const opIndexOffset = internalOptions.opIndexOffset ?? 0;
934
1083
  let workingBase = baseJson;
935
1084
  const pointerCache = internalOptions.pointerCache ?? /* @__PURE__ */ new Map();
936
1085
  const intents = [];
1086
+ budgetMeter?.count("patchOperations", patch.length);
937
1087
  for (let opIndex = 0; opIndex < patch.length; opIndex++) {
938
1088
  const op = patch[opIndex];
939
1089
  const absoluteOpIndex = opIndex + opIndexOffset;
@@ -948,14 +1098,16 @@ function compileJsonPatchOpToIntent(baseJson, op, options = {}) {
948
1098
  const internalOptions = options;
949
1099
  const semantics = options.semantics ?? "sequential";
950
1100
  const pointerCache = internalOptions.pointerCache ?? /* @__PURE__ */ new Map();
951
- return compileSingleOp(baseJson, op, internalOptions.opIndexOffset ?? 0, semantics, pointerCache);
1101
+ const opIndex = internalOptions.opIndexOffset ?? 0;
1102
+ (internalOptions.budgetMeter ?? createBudgetMeter(options.resourceBudget))?.count("patchOperations", 1, void 0, opIndex);
1103
+ return compileSingleOp(baseJson, op, opIndex, semantics, pointerCache);
952
1104
  }
953
1105
  /**
954
1106
  * Compute a JSON Patch delta between two JSON values.
955
1107
  * By default arrays use a deterministic LCS strategy.
956
1108
  * Pass `{ arrayStrategy: "atomic" }` for single-op array replacement.
957
1109
  * Pass `{ arrayStrategy: "lcs-linear" }` for a lower-memory LCS variant.
958
- * Use `lcsLinearMaxCells` to optionally cap worst-case `lcs-linear` work and
1110
+ * Use `lcsLinearMaxCells` to cap worst-case `lcs-linear` work and
959
1111
  * fall back to an atomic array replacement for very large unmatched windows.
960
1112
  * Pass `{ emitMoves: true }` or `{ emitCopies: true }` to opt into RFC 6902
961
1113
  * move/copy emission when a deterministic rewrite is available.
@@ -965,20 +1117,24 @@ function compileJsonPatchOpToIntent(baseJson, op, options = {}) {
965
1117
  * @returns An array of JSON Patch operations that transform `base` into `next`.
966
1118
  */
967
1119
  function diffJsonPatch(base, next, options = {}) {
1120
+ const budgetMeter = createBudgetMeter(options.resourceBudget);
968
1121
  const runtimeMode = options.jsonValidation ?? "none";
969
1122
  const runtimeBase = coerceRuntimeJsonValue(base, runtimeMode);
970
1123
  const runtimeNext = coerceRuntimeJsonValue(next, runtimeMode);
971
1124
  const ops = [];
972
- diffValue([], runtimeBase, runtimeNext, ops, options);
1125
+ const path = [];
1126
+ throwIfAborted(options.signal);
1127
+ diffValue(path, runtimeBase, runtimeNext, ops, options, budgetMeter);
973
1128
  return ops;
974
1129
  }
975
- function diffValue(path, base, next, ops, options) {
1130
+ function diffValue(path, base, next, ops, options, budgetMeter) {
976
1131
  const stack = [{
977
1132
  kind: "value",
978
1133
  base,
979
1134
  next
980
1135
  }];
981
1136
  while (stack.length > 0) {
1137
+ throwIfAborted(options.signal);
982
1138
  const frame = stack.pop();
983
1139
  if (frame.kind === "path-pop") {
984
1140
  path.pop();
@@ -1004,6 +1160,7 @@ function diffValue(path, base, next, ops, options) {
1004
1160
  continue;
1005
1161
  }
1006
1162
  assertTraversalDepth(path.length);
1163
+ budgetMeter?.count("visitedNodes", 1, stringifyJsonPointer(path));
1007
1164
  if (frame.base === frame.next) continue;
1008
1165
  const baseIsArray = Array.isArray(frame.base);
1009
1166
  const nextIsArray = Array.isArray(frame.next);
@@ -1019,7 +1176,7 @@ function diffValue(path, base, next, ops, options) {
1019
1176
  if (jsonEquals(frame.base, frame.next)) continue;
1020
1177
  const arrayStrategy = options.arrayStrategy ?? "lcs";
1021
1178
  if (arrayStrategy === "lcs") {
1022
- if (!diffArrayWithLcsMatrix(path, frame.base, frame.next, ops, options)) ops.push({
1179
+ if (!diffArrayWithLcsMatrix(path, frame.base, frame.next, ops, options, budgetMeter)) ops.push({
1023
1180
  op: "replace",
1024
1181
  path: stringifyJsonPointer(path),
1025
1182
  value: frame.next
@@ -1027,7 +1184,7 @@ function diffValue(path, base, next, ops, options) {
1027
1184
  continue;
1028
1185
  }
1029
1186
  if (arrayStrategy === "lcs-linear") {
1030
- if (!diffArrayWithLinearLcs(path, frame.base, frame.next, ops, options)) ops.push({
1187
+ if (!diffArrayWithLinearLcs(path, frame.base, frame.next, ops, options, budgetMeter)) ops.push({
1031
1188
  op: "replace",
1032
1189
  path: stringifyJsonPointer(path),
1033
1190
  value: frame.next
@@ -1052,6 +1209,7 @@ function diffValue(path, base, next, ops, options) {
1052
1209
  continue;
1053
1210
  }
1054
1211
  const { sharedKeys, baseOnlyKeys, nextOnlyKeys } = collectObjectKeys(frame.base, frame.next);
1212
+ budgetMeter?.count("objectEntries", sharedKeys.length + baseOnlyKeys.length + nextOnlyKeys.length, stringifyJsonPointer(path));
1055
1213
  if (!(baseOnlyKeys.length > 0 || nextOnlyKeys.length > 0) && (path.length === 0 || sharedKeys.length > 1) && jsonEquals(frame.base, frame.next)) continue;
1056
1214
  emitObjectStructuralOps(path, frame.base, frame.next, sharedKeys, baseOnlyKeys, nextOnlyKeys, ops, options);
1057
1215
  if (sharedKeys.length > 0) stack.push({
@@ -1215,34 +1373,44 @@ function insertSortedKey(keys, key) {
1215
1373
  }
1216
1374
  keys.splice(low, 0, key);
1217
1375
  }
1218
- function diffArrayWithLcsMatrix(path, base, next, ops, options) {
1219
- const window = trimEqualArrayEdges(base, next);
1376
+ function diffArrayWithLcsMatrix(path, base, next, ops, options, budgetMeter) {
1377
+ const window = trimEqualArrayEdges(base, next, options);
1220
1378
  const baseStart = window.baseStart;
1221
1379
  const nextStart = window.nextStart;
1222
1380
  const n = window.unmatchedBaseLength;
1223
1381
  const m = window.unmatchedNextLength;
1382
+ budgetMeter?.count("sequenceElements", n + m, stringifyJsonPointer(path));
1224
1383
  if (!shouldUseLcsDiff(n, m, options.lcsMaxCells)) return false;
1384
+ budgetMeter?.count("arrayDiffCells", (n + 1) * (m + 1), stringifyJsonPointer(path));
1225
1385
  if (n === 0 && m === 0) return true;
1226
1386
  const steps = [];
1227
- buildArrayEditScriptWithMatrix(base, baseStart, baseStart + n, next, nextStart, nextStart + m, steps);
1387
+ buildArrayEditScriptWithMatrix(base, baseStart, baseStart + n, next, nextStart, nextStart + m, steps, options);
1228
1388
  pushArrayPatchOps(path, window.prefixLength, steps, ops, base, options);
1229
1389
  return true;
1230
1390
  }
1231
- function diffArrayWithLinearLcs(path, base, next, ops, options) {
1232
- const window = trimEqualArrayEdges(base, next);
1391
+ function diffArrayWithLinearLcs(path, base, next, ops, options, budgetMeter) {
1392
+ const window = trimEqualArrayEdges(base, next, options);
1393
+ budgetMeter?.count("sequenceElements", window.unmatchedBaseLength + window.unmatchedNextLength, stringifyJsonPointer(path));
1233
1394
  if (!shouldUseLinearLcsDiff(window.unmatchedBaseLength, window.unmatchedNextLength, options)) return false;
1395
+ budgetMeter?.count("arrayDiffCells", (window.unmatchedBaseLength + 1) * (window.unmatchedNextLength + 1), stringifyJsonPointer(path));
1234
1396
  const steps = [];
1235
- buildArrayEditScriptLinearSpace(base, window.baseStart, window.baseStart + window.unmatchedBaseLength, next, window.nextStart, window.nextStart + window.unmatchedNextLength, steps);
1397
+ buildArrayEditScriptLinearSpace(base, window.baseStart, window.baseStart + window.unmatchedBaseLength, next, window.nextStart, window.nextStart + window.unmatchedNextLength, steps, options);
1236
1398
  pushArrayPatchOps(path, window.prefixLength, steps, ops, base, options);
1237
1399
  return true;
1238
1400
  }
1239
- function trimEqualArrayEdges(base, next) {
1401
+ function trimEqualArrayEdges(base, next, options) {
1240
1402
  const baseLength = base.length;
1241
1403
  const nextLength = next.length;
1242
1404
  let prefixLength = 0;
1243
- while (prefixLength < baseLength && prefixLength < nextLength && jsonEquals(base[prefixLength], next[prefixLength])) prefixLength += 1;
1405
+ while (prefixLength < baseLength && prefixLength < nextLength && jsonEquals(base[prefixLength], next[prefixLength])) {
1406
+ throwIfAborted(options.signal);
1407
+ prefixLength += 1;
1408
+ }
1244
1409
  let suffixLength = 0;
1245
- while (suffixLength < baseLength - prefixLength && suffixLength < nextLength - prefixLength && jsonEquals(base[baseLength - 1 - suffixLength], next[nextLength - 1 - suffixLength])) suffixLength += 1;
1410
+ while (suffixLength < baseLength - prefixLength && suffixLength < nextLength - prefixLength && jsonEquals(base[baseLength - 1 - suffixLength], next[nextLength - 1 - suffixLength])) {
1411
+ throwIfAborted(options.signal);
1412
+ suffixLength += 1;
1413
+ }
1246
1414
  return {
1247
1415
  baseStart: prefixLength,
1248
1416
  nextStart: prefixLength,
@@ -1251,7 +1419,8 @@ function trimEqualArrayEdges(base, next) {
1251
1419
  unmatchedNextLength: nextLength - prefixLength - suffixLength
1252
1420
  };
1253
1421
  }
1254
- function buildArrayEditScriptLinearSpace(base, baseStart, baseEnd, next, nextStart, nextEnd, steps) {
1422
+ function buildArrayEditScriptLinearSpace(base, baseStart, baseEnd, next, nextStart, nextEnd, steps, options) {
1423
+ throwIfAborted(options.signal);
1255
1424
  const unmatchedBaseLength = baseEnd - baseStart;
1256
1425
  const unmatchedNextLength = nextEnd - nextStart;
1257
1426
  if (unmatchedBaseLength === 0) {
@@ -1266,20 +1435,20 @@ function buildArrayEditScriptLinearSpace(base, baseStart, baseEnd, next, nextSta
1266
1435
  return;
1267
1436
  }
1268
1437
  if (unmatchedBaseLength === 1) {
1269
- pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, steps);
1438
+ pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, steps, options);
1270
1439
  return;
1271
1440
  }
1272
1441
  if (unmatchedNextLength === 1) {
1273
- pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, steps);
1442
+ pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, steps, options);
1274
1443
  return;
1275
1444
  }
1276
1445
  if (shouldUseMatrixBaseCase(unmatchedBaseLength, unmatchedNextLength)) {
1277
- buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStart, nextEnd, steps);
1446
+ buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStart, nextEnd, steps, options);
1278
1447
  return;
1279
1448
  }
1280
1449
  const baseMid = baseStart + Math.floor(unmatchedBaseLength / 2);
1281
- const forwardScores = computeLcsPrefixLengths(base, baseStart, baseMid, next, nextStart, nextEnd);
1282
- const reverseScores = computeLcsSuffixLengths(base, baseMid, baseEnd, next, nextStart, nextEnd);
1450
+ const forwardScores = computeLcsPrefixLengths(base, baseStart, baseMid, next, nextStart, nextEnd, options);
1451
+ const reverseScores = computeLcsSuffixLengths(base, baseMid, baseEnd, next, nextStart, nextEnd, options);
1283
1452
  let bestOffset = 0;
1284
1453
  let bestScore = Number.NEGATIVE_INFINITY;
1285
1454
  for (let offset = 0; offset <= unmatchedNextLength; offset++) {
@@ -1290,11 +1459,11 @@ function buildArrayEditScriptLinearSpace(base, baseStart, baseEnd, next, nextSta
1290
1459
  }
1291
1460
  }
1292
1461
  const nextMid = nextStart + bestOffset;
1293
- buildArrayEditScriptLinearSpace(base, baseStart, baseMid, next, nextStart, nextMid, steps);
1294
- buildArrayEditScriptLinearSpace(base, baseMid, baseEnd, next, nextMid, nextEnd, steps);
1462
+ buildArrayEditScriptLinearSpace(base, baseStart, baseMid, next, nextStart, nextMid, steps, options);
1463
+ buildArrayEditScriptLinearSpace(base, baseMid, baseEnd, next, nextMid, nextEnd, steps, options);
1295
1464
  }
1296
- function pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, steps) {
1297
- const matchIndex = findFirstMatchingIndexInNext(base[baseStart], next, nextStart, nextEnd);
1465
+ function pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, steps, options) {
1466
+ const matchIndex = findFirstMatchingIndexInNext(base[baseStart], next, nextStart, nextEnd, options);
1298
1467
  if (matchIndex === -1) {
1299
1468
  steps.push({ kind: "remove" });
1300
1469
  for (let nextIndex = nextStart; nextIndex < nextEnd; nextIndex++) steps.push({
@@ -1313,8 +1482,8 @@ function pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, s
1313
1482
  value: next[nextIndex]
1314
1483
  });
1315
1484
  }
1316
- function pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, steps) {
1317
- const matchIndex = findFirstMatchingIndexInBase(next[nextStart], base, baseStart, baseEnd);
1485
+ function pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, steps, options) {
1486
+ const matchIndex = findFirstMatchingIndexInBase(next[nextStart], base, baseStart, baseEnd, options);
1318
1487
  if (matchIndex === -1) {
1319
1488
  for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) steps.push({ kind: "remove" });
1320
1489
  steps.push({
@@ -1327,26 +1496,36 @@ function pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, s
1327
1496
  steps.push({ kind: "equal" });
1328
1497
  for (let baseIndex = matchIndex + 1; baseIndex < baseEnd; baseIndex++) steps.push({ kind: "remove" });
1329
1498
  }
1330
- function findFirstMatchingIndexInNext(target, next, nextStart, nextEnd) {
1331
- for (let nextIndex = nextStart; nextIndex < nextEnd; nextIndex++) if (jsonEquals(target, next[nextIndex])) return nextIndex;
1499
+ function findFirstMatchingIndexInNext(target, next, nextStart, nextEnd, options) {
1500
+ for (let nextIndex = nextStart; nextIndex < nextEnd; nextIndex++) {
1501
+ throwIfAborted(options.signal);
1502
+ if (jsonEquals(target, next[nextIndex])) return nextIndex;
1503
+ }
1332
1504
  return -1;
1333
1505
  }
1334
- function findFirstMatchingIndexInBase(target, base, baseStart, baseEnd) {
1335
- for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) if (jsonEquals(target, base[baseIndex])) return baseIndex;
1506
+ function findFirstMatchingIndexInBase(target, base, baseStart, baseEnd, options) {
1507
+ for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) {
1508
+ throwIfAborted(options.signal);
1509
+ if (jsonEquals(target, base[baseIndex])) return baseIndex;
1510
+ }
1336
1511
  return -1;
1337
1512
  }
1338
1513
  function shouldUseMatrixBaseCase(baseLength, nextLength) {
1339
1514
  return (baseLength + 1) * (nextLength + 1) <= LINEAR_LCS_MATRIX_BASE_CASE_MAX_CELLS;
1340
1515
  }
1341
- function buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStart, nextEnd, steps) {
1516
+ function buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStart, nextEnd, steps, options) {
1342
1517
  const unmatchedBaseLength = baseEnd - baseStart;
1343
1518
  const unmatchedNextLength = nextEnd - nextStart;
1344
1519
  const lcs = Array.from({ length: unmatchedBaseLength + 1 }, () => Array(unmatchedNextLength + 1).fill(0));
1345
- 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];
1346
- else lcs[baseOffset][nextOffset] = Math.max(lcs[baseOffset + 1][nextOffset], lcs[baseOffset][nextOffset + 1]);
1520
+ for (let baseOffset = unmatchedBaseLength - 1; baseOffset >= 0; baseOffset--) {
1521
+ throwIfAborted(options.signal);
1522
+ 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];
1523
+ else lcs[baseOffset][nextOffset] = Math.max(lcs[baseOffset + 1][nextOffset], lcs[baseOffset][nextOffset + 1]);
1524
+ }
1347
1525
  let baseOffset = 0;
1348
1526
  let nextOffset = 0;
1349
1527
  while (baseOffset < unmatchedBaseLength || nextOffset < unmatchedNextLength) {
1528
+ throwIfAborted(options.signal);
1350
1529
  if (baseOffset < unmatchedBaseLength && nextOffset < unmatchedNextLength && jsonEquals(base[baseStart + baseOffset], next[nextStart + nextOffset])) {
1351
1530
  steps.push({ kind: "equal" });
1352
1531
  baseOffset += 1;
@@ -1369,11 +1548,12 @@ function buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStar
1369
1548
  }
1370
1549
  }
1371
1550
  }
1372
- function computeLcsPrefixLengths(base, baseStart, baseEnd, next, nextStart, nextEnd) {
1551
+ function computeLcsPrefixLengths(base, baseStart, baseEnd, next, nextStart, nextEnd, options) {
1373
1552
  const unmatchedNextLength = nextEnd - nextStart;
1374
1553
  let previousRow = new Int32Array(unmatchedNextLength + 1);
1375
1554
  let currentRow = new Int32Array(unmatchedNextLength + 1);
1376
1555
  for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) {
1556
+ throwIfAborted(options.signal);
1377
1557
  for (let nextOffset = 0; nextOffset < unmatchedNextLength; nextOffset++) if (jsonEquals(base[baseIndex], next[nextStart + nextOffset])) currentRow[nextOffset + 1] = previousRow[nextOffset] + 1;
1378
1558
  else currentRow[nextOffset + 1] = Math.max(previousRow[nextOffset + 1], currentRow[nextOffset]);
1379
1559
  const nextPreviousRow = currentRow;
@@ -1383,11 +1563,12 @@ function computeLcsPrefixLengths(base, baseStart, baseEnd, next, nextStart, next
1383
1563
  }
1384
1564
  return previousRow;
1385
1565
  }
1386
- function computeLcsSuffixLengths(base, baseStart, baseEnd, next, nextStart, nextEnd) {
1566
+ function computeLcsSuffixLengths(base, baseStart, baseEnd, next, nextStart, nextEnd, options) {
1387
1567
  const unmatchedNextLength = nextEnd - nextStart;
1388
1568
  let previousRow = new Int32Array(unmatchedNextLength + 1);
1389
1569
  let currentRow = new Int32Array(unmatchedNextLength + 1);
1390
1570
  for (let baseIndex = baseEnd - 1; baseIndex >= baseStart; baseIndex--) {
1571
+ throwIfAborted(options.signal);
1391
1572
  for (let nextOffset = unmatchedNextLength - 1; nextOffset >= 0; nextOffset--) if (jsonEquals(base[baseIndex], next[nextStart + nextOffset])) currentRow[nextOffset] = previousRow[nextOffset + 1] + 1;
1392
1573
  else currentRow[nextOffset] = Math.max(previousRow[nextOffset], currentRow[nextOffset + 1]);
1393
1574
  const nextPreviousRow = currentRow;
@@ -1433,9 +1614,10 @@ function shouldUseLcsDiff(baseLength, nextLength, lcsMaxCells) {
1433
1614
  }
1434
1615
  function shouldUseLinearLcsDiff(baseLength, nextLength, options) {
1435
1616
  const cap = options.lcsLinearMaxCells;
1436
- if (cap === void 0 || cap === Number.POSITIVE_INFINITY) return true;
1437
- if (!Number.isFinite(cap) || cap < 1) return false;
1438
- return (baseLength + 1) * (nextLength + 1) <= cap;
1617
+ if (cap === Number.POSITIVE_INFINITY) return true;
1618
+ const effectiveCap = cap ?? DEFAULT_LCS_MAX_CELLS;
1619
+ if (!Number.isFinite(effectiveCap) || effectiveCap < 1) return false;
1620
+ return (baseLength + 1) * (nextLength + 1) <= effectiveCap;
1439
1621
  }
1440
1622
  function finalizeArrayOps(arrayPath, base, ops, options) {
1441
1623
  if (ops.length === 0) return [];
@@ -2065,54 +2247,45 @@ function isSamePath(a, b) {
2065
2247
  * @returns A new CRDT `Doc`.
2066
2248
  */
2067
2249
  function docFromJson(value, nextDot) {
2068
- return { root: nodeFromJson(value, nextDot) };
2250
+ const vv = Object.create(null);
2251
+ const doc = { root: nodeFromJson(value, () => {
2252
+ const dot = nextDot();
2253
+ observeVersionVectorDot(vv, dot);
2254
+ return dot;
2255
+ }) };
2256
+ writeCachedObservedVersionVector(doc, vv);
2257
+ return doc;
2069
2258
  }
2070
2259
  /**
2071
- * Legacy: create a doc using a single dot with counter offsets for array children.
2072
- * Prefer `docFromJson(value, nextDot)` to ensure unique dots per node.
2260
+ * Legacy helper for tests and fixtures that seeds an entire document from one dot.
2261
+ *
2262
+ * It reuses that dot for object entries and synthesizes array child counters from the
2263
+ * same seed, which can produce low-quality causal metadata and unrealistic sequence
2264
+ * identities in production CRDT state.
2265
+ *
2266
+ * Prefer `docFromJson(value, nextDot)` so every node receives a fresh unique dot.
2267
+ *
2268
+ * @deprecated Use `docFromJson(value, nextDot)` for production documents.
2073
2269
  */
2074
2270
  function docFromJsonWithDot(value, dot) {
2075
2271
  return { root: deepNodeFromJson(value, dot) };
2076
2272
  }
2077
2273
  function getSeqAtPath(doc, path) {
2078
- let cur = doc.root;
2079
- for (const seg of path) {
2080
- if (cur.kind !== "obj") return;
2081
- const ent = cur.entries.get(seg);
2082
- if (!ent) return;
2083
- cur = ent.node;
2084
- }
2085
- return cur.kind === "seq" ? cur : void 0;
2274
+ const node = getNodeAtPath(doc, path);
2275
+ return node?.kind === "seq" ? node : void 0;
2086
2276
  }
2087
2277
  function getObjAtPathStrict(doc, path) {
2088
- let cur = doc.root;
2089
- const seen = [];
2090
- if (path.length === 0) {
2091
- if (cur.kind !== "obj") return {
2092
- ok: false,
2093
- message: "expected object at /"
2094
- };
2278
+ const node = getNodeAtPath(doc, path);
2279
+ if (!node || node.kind !== "obj") {
2280
+ const pointer = stringifyJsonPointer(path);
2095
2281
  return {
2096
- ok: true,
2097
- obj: cur
2098
- };
2099
- }
2100
- for (const seg of path) {
2101
- if (cur.kind !== "obj") return {
2102
- ok: false,
2103
- message: `expected object at /${seen.join("/")}`
2104
- };
2105
- const entry = cur.entries.get(seg);
2106
- seen.push(seg);
2107
- if (!entry || entry.node.kind !== "obj") return {
2108
2282
  ok: false,
2109
- message: `expected object at /${seen.join("/")}`
2283
+ message: `expected object at ${pointer === "" ? "/" : pointer}`
2110
2284
  };
2111
- cur = entry.node;
2112
2285
  }
2113
2286
  return {
2114
2287
  ok: true,
2115
- obj: cur
2288
+ obj: node
2116
2289
  };
2117
2290
  }
2118
2291
  function ensureSeqAtPath(head, path, dotForCreate) {
@@ -2159,10 +2332,24 @@ function ensureSeqAtPath(head, path, dotForCreate) {
2159
2332
  function getNodeAtPath(doc, path) {
2160
2333
  let cur = doc.root;
2161
2334
  for (const seg of path) {
2162
- if (cur.kind !== "obj") return;
2163
- const ent = cur.entries.get(seg);
2164
- if (!ent) return;
2165
- cur = ent.node;
2335
+ if (cur.kind === "obj") {
2336
+ const ent = cur.entries.get(seg);
2337
+ if (!ent) return;
2338
+ cur = ent.node;
2339
+ continue;
2340
+ }
2341
+ if (cur.kind === "seq") {
2342
+ if (!ARRAY_INDEX_TOKEN_PATTERN.test(seg)) return;
2343
+ const index = Number(seg);
2344
+ if (!Number.isSafeInteger(index)) return;
2345
+ const elemId = rgaIdAtIndex(cur, index);
2346
+ if (elemId === void 0) return;
2347
+ const elem = cur.elems.get(elemId);
2348
+ if (!elem) return;
2349
+ cur = elem.value;
2350
+ continue;
2351
+ }
2352
+ if (cur.kind === "lww") return;
2166
2353
  }
2167
2354
  return cur;
2168
2355
  }
@@ -2314,7 +2501,10 @@ function nodeFromJson(value, nextDot) {
2314
2501
  }
2315
2502
  /** Deep-clone a CRDT document. The clone is fully independent of the original. */
2316
2503
  function cloneDoc(doc) {
2317
- return { root: cloneNode(doc.root) };
2504
+ const cloned = { root: cloneNode(doc.root) };
2505
+ const cached = readCachedObservedVersionVector(doc);
2506
+ if (cached) writeCachedObservedVersionVector(cloned, cached);
2507
+ return cloned;
2318
2508
  }
2319
2509
  function cloneNode(node) {
2320
2510
  return cloneNodeAtDepth(node, 0);
@@ -2376,38 +2566,88 @@ function getJsonAtDocPathForTest(doc, path) {
2376
2566
  let cur = doc.root;
2377
2567
  for (let i = 0; i < path.length; i++) {
2378
2568
  const seg = path[i];
2379
- assertTraversalDepth(i + 1);
2569
+ try {
2570
+ assertTraversalDepth(i + 1);
2571
+ } catch (error) {
2572
+ return {
2573
+ ok: false,
2574
+ error: error instanceof TraversalDepthError ? toDepthApplyError(error) : {
2575
+ ok: false,
2576
+ code: 409,
2577
+ reason: "INVALID_PATCH",
2578
+ message: error instanceof Error ? error.message : "invalid test path"
2579
+ }
2580
+ };
2581
+ }
2380
2582
  if (cur.kind === "obj") {
2381
2583
  const ent = cur.entries.get(seg);
2382
- if (!ent) throw new Error(`Missing key '${seg}'`);
2584
+ if (!ent) return {
2585
+ ok: false,
2586
+ error: {
2587
+ ok: false,
2588
+ code: 409,
2589
+ reason: "MISSING_TARGET",
2590
+ message: `Missing key '${seg}'`
2591
+ }
2592
+ };
2383
2593
  cur = ent.node;
2384
2594
  continue;
2385
2595
  }
2386
2596
  if (cur.kind === "seq") {
2387
- if (!ARRAY_INDEX_TOKEN_PATTERN.test(seg)) throw new Error(`Expected array index, got '${seg}'`);
2388
- const id = rgaIdAtIndex(cur, Number(seg));
2389
- if (id === void 0) throw new Error(`Index out of bounds at '${seg}'`);
2597
+ if (!ARRAY_INDEX_TOKEN_PATTERN.test(seg)) return {
2598
+ ok: false,
2599
+ error: {
2600
+ ok: false,
2601
+ code: 409,
2602
+ reason: "INVALID_POINTER",
2603
+ message: `Expected array index, got '${seg}'`
2604
+ }
2605
+ };
2606
+ const idx = Number(seg);
2607
+ if (!Number.isSafeInteger(idx)) return {
2608
+ ok: false,
2609
+ error: {
2610
+ ok: false,
2611
+ code: 409,
2612
+ reason: "OUT_OF_BOUNDS",
2613
+ message: `Index out of bounds at '${seg}'`
2614
+ }
2615
+ };
2616
+ const id = rgaIdAtIndex(cur, idx);
2617
+ if (id === void 0) return {
2618
+ ok: false,
2619
+ error: {
2620
+ ok: false,
2621
+ code: 409,
2622
+ reason: "OUT_OF_BOUNDS",
2623
+ message: `Index out of bounds at '${seg}'`
2624
+ }
2625
+ };
2390
2626
  cur = cur.elems.get(id).value;
2391
2627
  continue;
2392
2628
  }
2393
- throw new Error(`Cannot traverse into non-container at '${seg}'`);
2394
- }
2395
- return cur.kind === "lww" ? cur.value : materialize(cur);
2396
- }
2397
- function applyTest(base, head, it, evalTestAgainst) {
2398
- let got;
2399
- try {
2400
- got = getJsonAtDocPathForTest(evalTestAgainst === "head" ? head : base, it.path);
2401
- } catch {
2402
2629
  return {
2403
2630
  ok: false,
2404
- code: 409,
2405
- reason: "MISSING_TARGET",
2406
- message: `test path missing at /${it.path.join("/")}`,
2407
- path: `/${it.path.join("/")}`
2631
+ error: {
2632
+ ok: false,
2633
+ code: 409,
2634
+ reason: "INVALID_TARGET",
2635
+ message: `Cannot traverse into non-container at '${seg}'`
2636
+ }
2408
2637
  };
2409
2638
  }
2410
- if (!jsonEquals(got, it.value)) return {
2639
+ return {
2640
+ ok: true,
2641
+ value: cur.kind === "lww" ? cur.value : materialize(cur)
2642
+ };
2643
+ }
2644
+ function applyTest(base, head, it, evalTestAgainst) {
2645
+ const got = getJsonAtDocPathForTest(evalTestAgainst === "head" ? head : base, it.path);
2646
+ if (!got.ok) return {
2647
+ ...got.error,
2648
+ path: `/${it.path.join("/")}`
2649
+ };
2650
+ if (!jsonEquals(got.value, it.value)) return {
2411
2651
  ok: false,
2412
2652
  code: 409,
2413
2653
  reason: "TEST_FAILED",
@@ -2472,7 +2712,7 @@ function createArrayIndexLookupSession() {
2472
2712
  return created;
2473
2713
  } };
2474
2714
  }
2475
- function applyArrInsert(base, head, it, newDot, indexSession, bumpCounterAbove, strictParents = false) {
2715
+ function applyArrInsert(base, head, it, newDot, indexSession, bumpCounterAbove, strictParents = true) {
2476
2716
  const pointer = `/${it.path.join("/")}`;
2477
2717
  const baseSeq = getSeqAtPath(base, it.path);
2478
2718
  if (!baseSeq) {
@@ -2621,39 +2861,51 @@ function applyArrReplace(base, head, it, newDot, indexSession) {
2621
2861
  * @param evalTestAgainst - Whether `test` ops are evaluated against `"head"` or `"base"`.
2622
2862
  * @param bumpCounterAbove - Optional hook that can fast-forward the underlying counter before inserts.
2623
2863
  * @param options - Optional behavior toggles.
2624
- * @param options.strictParents - When `true`, reject array inserts whose base parent path is missing.
2864
+ * @param options.strictParents - Reject array inserts whose base parent path is missing.
2865
+ * Defaults to `true`; pass `false` only for legacy missing-parent array auto-creation.
2625
2866
  * @returns `{ ok: true }` on success, or `{ ok: false, code: 409, message }` on conflict.
2626
2867
  */
2627
2868
  function applyIntentsToCrdt(base, head, intents, newDot, evalTestAgainst = "head", bumpCounterAbove, options = {}) {
2628
2869
  const arrayIndexSession = createArrayIndexLookupSession();
2870
+ let pendingObservedDots = [];
2871
+ const observedNewDot = () => {
2872
+ const dot = newDot();
2873
+ pendingObservedDots.push(dot);
2874
+ return dot;
2875
+ };
2629
2876
  for (const it of intents) {
2630
2877
  let fail = null;
2878
+ pendingObservedDots = [];
2631
2879
  switch (it.t) {
2632
2880
  case "Test":
2633
2881
  fail = applyTest(base, head, it, evalTestAgainst);
2634
2882
  break;
2635
2883
  case "ObjSet":
2636
- fail = applyObjSet(head, it, newDot);
2884
+ fail = applyObjSet(head, it, observedNewDot);
2637
2885
  break;
2638
2886
  case "ObjRemove":
2639
- fail = applyObjRemove(head, it, newDot);
2887
+ fail = applyObjRemove(head, it, observedNewDot);
2640
2888
  break;
2641
2889
  case "ArrInsert":
2642
- fail = applyArrInsert(base, head, it, newDot, arrayIndexSession, bumpCounterAbove, options.strictParents ?? false);
2890
+ fail = applyArrInsert(base, head, it, observedNewDot, arrayIndexSession, bumpCounterAbove, options.strictParents ?? true);
2643
2891
  break;
2644
2892
  case "ArrDelete":
2645
- fail = applyArrDelete(base, head, it, newDot, arrayIndexSession);
2893
+ fail = applyArrDelete(base, head, it, observedNewDot, arrayIndexSession);
2646
2894
  break;
2647
2895
  case "ArrReplace":
2648
- fail = applyArrReplace(base, head, it, newDot, arrayIndexSession);
2896
+ fail = applyArrReplace(base, head, it, observedNewDot, arrayIndexSession);
2649
2897
  break;
2650
2898
  default: assertNever(it, "Unhandled intent type");
2651
2899
  }
2652
- if (fail) return fail;
2900
+ if (fail) {
2901
+ pendingObservedDots = [];
2902
+ return fail;
2903
+ }
2904
+ for (const dot of pendingObservedDots) observeDocVersionVectorDot(head, dot);
2653
2905
  }
2654
2906
  return { ok: true };
2655
2907
  }
2656
- function jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove, strictParents = false) {
2908
+ function jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove, strictParents = true) {
2657
2909
  if (isJsonPatchToCrdtOptions(baseOrOptions)) return jsonPatchToCrdtInternal(baseOrOptions);
2658
2910
  if (!head || !patch || !newDot) return {
2659
2911
  ok: false,
@@ -2671,7 +2923,7 @@ function jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst = "
2671
2923
  strictParents
2672
2924
  });
2673
2925
  }
2674
- function jsonPatchToCrdtSafe(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove, strictParents = false) {
2926
+ function jsonPatchToCrdtSafe(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove, strictParents = true) {
2675
2927
  try {
2676
2928
  if (isJsonPatchToCrdtOptions(baseOrOptions)) return jsonPatchToCrdt(baseOrOptions);
2677
2929
  if (!head || !patch || !newDot) return {
@@ -2712,6 +2964,46 @@ function rebaseDiffOps(path, nestedOps, out) {
2712
2964
  throw new Error(`Unexpected op '${op.op}' from diffJsonPatch`);
2713
2965
  }
2714
2966
  }
2967
+ function collectLiveSequenceElements(seq) {
2968
+ const elems = [];
2969
+ const cursor = rgaCreateLinearCursor(seq);
2970
+ for (let elem = cursor.next(); elem; elem = cursor.next()) elems.push(elem);
2971
+ return elems;
2972
+ }
2973
+ function materializeSequenceWindow(elems, start, end) {
2974
+ const out = [];
2975
+ for (let i = start; i < end; i++) out.push(nodeToJsonForPatch(elems[i].value));
2976
+ return out;
2977
+ }
2978
+ function rebaseSequenceWindowDiffOps(path, indexOffset, nestedOps, out) {
2979
+ const pending = [];
2980
+ for (const op of nestedOps) {
2981
+ if (op.path === "") return false;
2982
+ const rebasedSegments = parseJsonPointer(op.path);
2983
+ const indexToken = rebasedSegments[0];
2984
+ if (!indexToken || !ARRAY_INDEX_TOKEN_PATTERN.test(indexToken)) return false;
2985
+ rebasedSegments[0] = String(Number(indexToken) + indexOffset);
2986
+ const rebasedPath = stringifyJsonPointer([...path, ...rebasedSegments]);
2987
+ if (op.op === "remove") {
2988
+ pending.push({
2989
+ op: "remove",
2990
+ path: rebasedPath
2991
+ });
2992
+ continue;
2993
+ }
2994
+ if (op.op === "add" || op.op === "replace") {
2995
+ pending.push({
2996
+ op: op.op,
2997
+ path: rebasedPath,
2998
+ value: op.value
2999
+ });
3000
+ continue;
3001
+ }
3002
+ return false;
3003
+ }
3004
+ out.push(...pending);
3005
+ return true;
3006
+ }
2715
3007
  function nodesJsonEqual(baseNode, headNode, depth) {
2716
3008
  assertTraversalDepth(depth);
2717
3009
  if (baseNode === headNode) return true;
@@ -2836,6 +3128,35 @@ function diffObjectNodes(path, baseNode, headNode, options, ops, depth) {
2836
3128
  headIndex += 1;
2837
3129
  }
2838
3130
  }
3131
+ function diffSequenceNodes(path, baseNode, headSeq, options, ops, depth) {
3132
+ if ((options.arrayStrategy ?? "lcs") === "atomic") {
3133
+ rebaseDiffOps(path, diffJsonPatch(materialize(baseNode), materialize(headSeq), options), ops);
3134
+ return;
3135
+ }
3136
+ const baseElems = collectLiveSequenceElements(baseNode);
3137
+ const headElems = collectLiveSequenceElements(headSeq);
3138
+ const sharedLength = Math.min(baseElems.length, headElems.length);
3139
+ let prefixLength = 0;
3140
+ while (prefixLength < sharedLength && nodesJsonEqual(baseElems[prefixLength].value, headElems[prefixLength].value, depth + 1)) prefixLength += 1;
3141
+ if (prefixLength === baseElems.length && prefixLength === headElems.length) return;
3142
+ let baseEnd = baseElems.length;
3143
+ let headEnd = headElems.length;
3144
+ while (baseEnd > prefixLength && headEnd > prefixLength && nodesJsonEqual(baseElems[baseEnd - 1].value, headElems[headEnd - 1].value, depth + 1)) {
3145
+ baseEnd -= 1;
3146
+ headEnd -= 1;
3147
+ }
3148
+ const unmatchedBaseLength = baseEnd - prefixLength;
3149
+ const unmatchedHeadLength = headEnd - prefixLength;
3150
+ if (unmatchedBaseLength === 1 && unmatchedHeadLength === 1) {
3151
+ path.push(String(prefixLength));
3152
+ diffNodeToPatch(path, baseElems[prefixLength].value, headElems[prefixLength].value, options, ops, depth + 1);
3153
+ path.pop();
3154
+ return;
3155
+ }
3156
+ const seqOps = diffJsonPatch(materializeSequenceWindow(baseElems, prefixLength, baseEnd), materializeSequenceWindow(headElems, prefixLength, headEnd), options);
3157
+ if (rebaseSequenceWindowDiffOps(path, prefixLength, seqOps, ops)) return;
3158
+ rebaseDiffOps(path, diffJsonPatch(materialize(baseNode), materialize(headSeq), options), ops);
3159
+ }
2839
3160
  function diffNodeToPatch(path, baseNode, headNode, options, ops, depth) {
2840
3161
  assertTraversalDepth(depth);
2841
3162
  if (baseNode === headNode) return;
@@ -2861,8 +3182,7 @@ function diffNodeToPatch(path, baseNode, headNode, options, ops, depth) {
2861
3182
  diffObjectNodes(path, baseNode, headNode, options, ops, depth);
2862
3183
  return;
2863
3184
  }
2864
- const headSeq = headNode;
2865
- rebaseDiffOps(path, diffJsonPatch(materialize(baseNode), materialize(headSeq), options), ops);
3185
+ diffSequenceNodes(path, baseNode, headNode, options, ops, depth);
2866
3186
  }
2867
3187
  /**
2868
3188
  * Generate a JSON Patch delta between two CRDT documents.
@@ -2872,7 +3192,7 @@ function diffNodeToPatch(path, baseNode, headNode, options, ops, depth) {
2872
3192
  * @returns An array of JSON Patch operations that transform base into head.
2873
3193
  */
2874
3194
  function crdtToJsonPatch(base, head, options) {
2875
- if ((options?.jsonValidation ?? "none") !== "none") return diffJsonPatch(materialize(base.root), materialize(head.root), options);
3195
+ if ((options?.jsonValidation ?? "none") !== "none" || options?.resourceBudget !== void 0) return diffJsonPatch(materialize(base.root), materialize(head.root), options);
2876
3196
  return crdtNodesToJsonPatch(base.root, head.root, options);
2877
3197
  }
2878
3198
  /** Internals-only helper for diffing CRDT nodes from an existing traversal depth. */
@@ -2894,17 +3214,27 @@ function crdtToFullReplace(doc) {
2894
3214
  }
2895
3215
  function jsonPatchToCrdtInternal(options) {
2896
3216
  const evalTestAgainst = options.evalTestAgainst ?? "head";
2897
- if ((options.semantics ?? "sequential") === "base") {
3217
+ const semantics = options.semantics ?? "sequential";
3218
+ const budgetMeter = createBudgetMeter(options.resourceBudget);
3219
+ try {
3220
+ budgetMeter?.count("patchOperations", options.patch.length);
3221
+ } catch (error) {
3222
+ return toApplyError$1(error);
3223
+ }
3224
+ if (semantics === "base") {
2898
3225
  const baseJson = materialize(options.base.root);
2899
3226
  let intents;
2900
3227
  try {
2901
- intents = compileJsonPatchToIntent(baseJson, options.patch, { semantics: "base" });
3228
+ intents = compileJsonPatchToIntent(baseJson, options.patch, {
3229
+ semantics: "base",
3230
+ resourceBudget: options.resourceBudget
3231
+ });
2902
3232
  } catch (error) {
2903
3233
  return toApplyError$1(error);
2904
3234
  }
2905
3235
  return applyIntentsToCrdt(options.base, options.head, intents, options.newDot, evalTestAgainst, options.bumpCounterAbove, { strictParents: options.strictParents });
2906
3236
  }
2907
- let shadowBase = cloneDoc(evalTestAgainst === "base" ? options.base : options.head);
3237
+ const shadowBase = evalTestAgainst === "base" ? cloneDoc(options.base) : null;
2908
3238
  let shadowCtr = 0;
2909
3239
  const shadowDot = () => ({
2910
3240
  actor: "__shadow__",
@@ -2913,108 +3243,396 @@ function jsonPatchToCrdtInternal(options) {
2913
3243
  const shadowBump = (ctr) => {
2914
3244
  if (shadowCtr < ctr) shadowCtr = ctr;
2915
3245
  };
2916
- const applySequentialOp = (op, opIndex) => {
2917
- const baseJson = materialize(shadowBase.root);
2918
- let intents;
2919
- try {
2920
- intents = compileJsonPatchToIntent(baseJson, [op], { semantics: "sequential" });
2921
- } catch (error) {
2922
- return withOpIndex(toApplyError$1(error), opIndex);
2923
- }
2924
- const headStep = applyIntentsToCrdt(shadowBase, options.head, intents, options.newDot, evalTestAgainst, options.bumpCounterAbove, { strictParents: options.strictParents });
2925
- if (!headStep.ok) return withOpIndex(headStep, opIndex);
2926
- if (evalTestAgainst === "base") {
2927
- const shadowStep = applyIntentsToCrdt(shadowBase, shadowBase, intents, shadowDot, "base", shadowBump, { strictParents: options.strictParents });
2928
- if (!shadowStep.ok) return withOpIndex(shadowStep, opIndex);
2929
- } else shadowBase = cloneDoc(options.head);
2930
- return { ok: true };
2931
- };
2932
- for (let opIndex = 0; opIndex < options.patch.length; opIndex++) {
2933
- const op = options.patch[opIndex];
2934
- if (op.op === "move") {
2935
- const baseJson = materialize(shadowBase.root);
2936
- let fromValue;
2937
- try {
2938
- fromValue = structuredClone(getAtJson(baseJson, parseJsonPointer(op.from)));
2939
- } catch {
2940
- try {
2941
- compileJsonPatchToIntent(baseJson, [{
2942
- op: "remove",
2943
- path: op.from
2944
- }], { semantics: "sequential" });
2945
- } catch (error) {
2946
- return withOpIndex(toApplyError$1(error), opIndex);
2947
- }
2948
- return withOpIndex(toApplyError$1(/* @__PURE__ */ new Error(`failed to resolve move source at ${op.from}`)), opIndex);
2949
- }
2950
- if (op.from === op.path) continue;
2951
- const removeStep = applySequentialOp({
2952
- op: "remove",
2953
- path: op.from
2954
- }, opIndex);
2955
- if (!removeStep.ok) return removeStep;
2956
- const addStep = applySequentialOp({
2957
- op: "add",
2958
- path: op.path,
2959
- value: fromValue
2960
- }, opIndex);
2961
- if (!addStep.ok) return addStep;
2962
- continue;
2963
- }
2964
- const step = applySequentialOp(op, opIndex);
3246
+ const session = { pointerCache: /* @__PURE__ */ new Map() };
3247
+ for (const [opIndex, op] of options.patch.entries()) {
3248
+ const step = applySequentialPatchOp(options, evalTestAgainst === "base" ? shadowBase : options.head, op, opIndex, evalTestAgainst, shadowDot, shadowBump, session);
2965
3249
  if (!step.ok) return step;
2966
3250
  }
2967
3251
  return { ok: true };
2968
3252
  }
2969
- function withOpIndex(error, opIndex) {
2970
- if (error.opIndex !== void 0) return error;
2971
- return {
2972
- ...error,
2973
- opIndex
2974
- };
3253
+ function applySequentialPatchOp(options, compileBase, op, opIndex, evalTestAgainst, shadowDot, shadowBump, session) {
3254
+ if (op.op === "move") {
3255
+ if (op.from === op.path) {
3256
+ const pathCheck = resolveValueAtPointerInDoc$1(compileBase, op.from, opIndex, session.pointerCache);
3257
+ if (!pathCheck.ok) return pathCheck;
3258
+ return { ok: true };
3259
+ }
3260
+ const fromResolved = resolveValueAtPointerInDoc$1(compileBase, op.from, opIndex, session.pointerCache);
3261
+ if (!fromResolved.ok) return fromResolved;
3262
+ const removeStep = applySingleSequentialPatchStep(options, compileBase, {
3263
+ op: "remove",
3264
+ path: op.from
3265
+ }, opIndex, evalTestAgainst, shadowDot, shadowBump, session);
3266
+ if (!removeStep.ok) return removeStep;
3267
+ return applySingleSequentialPatchStep(options, compileBase, {
3268
+ op: "add",
3269
+ path: op.path,
3270
+ value: structuredClone(fromResolved.value)
3271
+ }, opIndex, evalTestAgainst, shadowDot, shadowBump, session);
3272
+ }
3273
+ if (op.op === "copy") {
3274
+ const fromResolved = resolveValueAtPointerInDoc$1(compileBase, op.from, opIndex, session.pointerCache);
3275
+ if (!fromResolved.ok) return fromResolved;
3276
+ return applySingleSequentialPatchStep(options, compileBase, {
3277
+ op: "add",
3278
+ path: op.path,
3279
+ value: structuredClone(fromResolved.value)
3280
+ }, opIndex, evalTestAgainst, shadowDot, shadowBump, session);
3281
+ }
3282
+ return applySingleSequentialPatchStep(options, compileBase, op, opIndex, evalTestAgainst, shadowDot, shadowBump, session);
2975
3283
  }
2976
- function isJsonPatchToCrdtOptions(value) {
2977
- return typeof value === "object" && value !== null && "base" in value && "head" in value && "patch" in value && "newDot" in value;
3284
+ function applySingleSequentialPatchStep(options, compileBase, op, opIndex, evalTestAgainst, shadowDot, shadowBump, session) {
3285
+ const compiled = compilePreparedSingleIntentFromDoc$1(compileBase, op, session.pointerCache, opIndex);
3286
+ if (!compiled.ok) return compiled;
3287
+ const headStep = applyIntentsToCrdt(compileBase, options.head, compiled.intents, options.newDot, evalTestAgainst, options.bumpCounterAbove, { strictParents: options.strictParents });
3288
+ if (!headStep.ok) return withOpIndex$1(headStep, opIndex);
3289
+ if (op.op === "test") return { ok: true };
3290
+ if (evalTestAgainst === "head") return { ok: true };
3291
+ const shadowStep = applyIntentsToCrdt(compileBase, compileBase, compiled.intents, shadowDot, "base", shadowBump, { strictParents: options.strictParents });
3292
+ if (!shadowStep.ok) return withOpIndex$1(shadowStep, opIndex);
3293
+ return { ok: true };
2978
3294
  }
2979
- function toApplyError$1(error) {
2980
- if (error instanceof TraversalDepthError) return toDepthApplyError(error);
2981
- if (error instanceof PatchCompileError) return {
3295
+ function resolveValueAtPointerInDoc$1(doc, pointer, opIndex, pointerCache) {
3296
+ const parsedPath = parsePointerWithCache$1(pointer, pointerCache, opIndex);
3297
+ if (!parsedPath.ok) return parsedPath;
3298
+ const resolved = resolveNodeAtPath$1(doc.root, parsedPath.path);
3299
+ if (!resolved.ok) return {
2982
3300
  ok: false,
2983
- code: 409,
2984
- reason: error.reason,
2985
- message: error.message,
2986
- path: error.path,
2987
- opIndex: error.opIndex
3301
+ ...resolved.error,
3302
+ path: pointer,
3303
+ opIndex
2988
3304
  };
2989
3305
  return {
2990
- ok: false,
2991
- code: 409,
2992
- reason: "INVALID_PATCH",
2993
- message: error instanceof Error ? error.message : "failed to compile/apply patch"
3306
+ ok: true,
3307
+ value: nodeToJsonForPatch(resolved.node)
2994
3308
  };
2995
3309
  }
2996
- function assertNever(_value, message) {
2997
- throw new Error(message);
2998
- }
2999
-
3000
- //#endregion
3001
- //#region src/state.ts
3002
- /** Error thrown when a JSON Patch cannot be applied. Includes structured conflict metadata. */
3003
- var PatchError = class extends Error {
3004
- code;
3005
- reason;
3006
- path;
3007
- opIndex;
3008
- constructor(errorOrMessage, code = 409, reason = "INVALID_PATCH") {
3009
- super(typeof errorOrMessage === "string" ? errorOrMessage : errorOrMessage.message);
3010
- this.name = "PatchError";
3011
- if (typeof errorOrMessage === "string") {
3012
- this.code = code;
3013
- this.reason = reason;
3014
- return;
3015
- }
3016
- this.code = errorOrMessage.code;
3310
+ function compilePreparedSingleIntentFromDoc$1(baseDoc, op, pointerCache, opIndex) {
3311
+ const parsedPath = parsePointerWithCache$1(op.path, pointerCache, opIndex);
3312
+ if (!parsedPath.ok) return parsedPath;
3313
+ const path = parsedPath.path;
3314
+ if (op.op === "test") return {
3315
+ ok: true,
3316
+ intents: [{
3317
+ t: "Test",
3318
+ path,
3319
+ value: op.value
3320
+ }]
3321
+ };
3322
+ if (path.length === 0) {
3323
+ if (op.op === "remove") return {
3324
+ ok: false,
3325
+ code: 409,
3326
+ reason: "INVALID_TARGET",
3327
+ message: "remove at root path is not supported in RFC-compliant mode",
3328
+ path: op.path,
3329
+ opIndex
3330
+ };
3331
+ return {
3332
+ ok: true,
3333
+ intents: [{
3334
+ t: "ObjSet",
3335
+ path: [],
3336
+ key: ROOT_KEY,
3337
+ value: op.value
3338
+ }]
3339
+ };
3340
+ }
3341
+ const parentPath = path.slice(0, -1);
3342
+ const parentPointer = stringifyJsonPointer(parentPath);
3343
+ const key = path[path.length - 1];
3344
+ const resolvedParent = parentPath.length === 0 ? {
3345
+ ok: true,
3346
+ node: baseDoc.root
3347
+ } : resolveNodeAtPath$1(baseDoc.root, parentPath);
3348
+ if (!resolvedParent.ok) return {
3349
+ ok: false,
3350
+ ...resolvedParent.error,
3351
+ path: parentPointer,
3352
+ opIndex
3353
+ };
3354
+ const parentNode = resolvedParent.node;
3355
+ if (parentNode.kind === "seq") {
3356
+ const parsedIndex = parseArrayIndexTokenForDoc$1(key, op.op, op.path, opIndex);
3357
+ if (!parsedIndex.ok) return parsedIndex;
3358
+ const boundedIndex = validateArrayIndexBounds$1(parsedIndex.index, op.op, rgaLength(parentNode), op.path, opIndex);
3359
+ if (!boundedIndex.ok) return boundedIndex;
3360
+ if (op.op === "add") return {
3361
+ ok: true,
3362
+ intents: [{
3363
+ t: "ArrInsert",
3364
+ path: parentPath,
3365
+ index: boundedIndex.index,
3366
+ value: op.value
3367
+ }]
3368
+ };
3369
+ if (op.op === "remove") return {
3370
+ ok: true,
3371
+ intents: [{
3372
+ t: "ArrDelete",
3373
+ path: parentPath,
3374
+ index: boundedIndex.index
3375
+ }]
3376
+ };
3377
+ return {
3378
+ ok: true,
3379
+ intents: [{
3380
+ t: "ArrReplace",
3381
+ path: parentPath,
3382
+ index: boundedIndex.index,
3383
+ value: op.value
3384
+ }]
3385
+ };
3386
+ }
3387
+ if (parentNode.kind !== "obj") return {
3388
+ ok: false,
3389
+ code: 409,
3390
+ reason: "INVALID_TARGET",
3391
+ message: `expected object or array parent at ${parentPointer}`,
3392
+ path: parentPointer,
3393
+ opIndex
3394
+ };
3395
+ if (key === "__proto__") return {
3396
+ ok: false,
3397
+ code: 409,
3398
+ reason: "INVALID_POINTER",
3399
+ message: `unsafe object key at ${op.path}`,
3400
+ path: op.path,
3401
+ opIndex
3402
+ };
3403
+ const entry = parentNode.entries.get(key);
3404
+ if ((op.op === "replace" || op.op === "remove") && !entry) return {
3405
+ ok: false,
3406
+ code: 409,
3407
+ reason: "MISSING_TARGET",
3408
+ message: `missing key ${key} at ${parentPointer}`,
3409
+ path: op.path,
3410
+ opIndex
3411
+ };
3412
+ if (op.op === "remove") return {
3413
+ ok: true,
3414
+ intents: [{
3415
+ t: "ObjRemove",
3416
+ path: parentPath,
3417
+ key
3418
+ }]
3419
+ };
3420
+ return {
3421
+ ok: true,
3422
+ intents: [{
3423
+ t: "ObjSet",
3424
+ path: parentPath,
3425
+ key,
3426
+ value: op.value,
3427
+ mode: op.op
3428
+ }]
3429
+ };
3430
+ }
3431
+ function parsePointerWithCache$1(pointer, pointerCache, opIndex) {
3432
+ const cachedPath = pointerCache.get(pointer);
3433
+ if (cachedPath !== void 0) return {
3434
+ ok: true,
3435
+ path: cachedPath.slice()
3436
+ };
3437
+ try {
3438
+ const parsedPath = parseJsonPointer(pointer);
3439
+ pointerCache.set(pointer, parsedPath);
3440
+ return {
3441
+ ok: true,
3442
+ path: parsedPath.slice()
3443
+ };
3444
+ } catch (error) {
3445
+ return {
3446
+ ok: false,
3447
+ code: 409,
3448
+ reason: "INVALID_POINTER",
3449
+ message: error instanceof Error ? error.message : "invalid pointer",
3450
+ path: pointer,
3451
+ opIndex
3452
+ };
3453
+ }
3454
+ }
3455
+ function resolveNodeAtPath$1(root, path) {
3456
+ let current = root;
3457
+ for (const segment of path) {
3458
+ if (current.kind === "obj") {
3459
+ const entry = current.entries.get(segment);
3460
+ if (!entry) return {
3461
+ ok: false,
3462
+ error: {
3463
+ code: 409,
3464
+ reason: "MISSING_PARENT",
3465
+ message: `Missing key '${segment}'`
3466
+ }
3467
+ };
3468
+ current = entry.node;
3469
+ continue;
3470
+ }
3471
+ if (current.kind === "seq") {
3472
+ if (!ARRAY_INDEX_TOKEN_PATTERN.test(segment)) return {
3473
+ ok: false,
3474
+ error: {
3475
+ code: 409,
3476
+ reason: "INVALID_POINTER",
3477
+ message: `Expected array index, got '${segment}'`
3478
+ }
3479
+ };
3480
+ const index = Number(segment);
3481
+ if (!Number.isSafeInteger(index)) return {
3482
+ ok: false,
3483
+ error: {
3484
+ code: 409,
3485
+ reason: "OUT_OF_BOUNDS",
3486
+ message: `Index out of bounds at '${segment}'`
3487
+ }
3488
+ };
3489
+ const elemId = rgaIdAtIndex(current, index);
3490
+ if (elemId === void 0) return {
3491
+ ok: false,
3492
+ error: {
3493
+ code: 409,
3494
+ reason: "OUT_OF_BOUNDS",
3495
+ message: `Index out of bounds at '${segment}'`
3496
+ }
3497
+ };
3498
+ current = current.elems.get(elemId).value;
3499
+ continue;
3500
+ }
3501
+ return {
3502
+ ok: false,
3503
+ error: {
3504
+ code: 409,
3505
+ reason: "INVALID_TARGET",
3506
+ message: `Cannot traverse into non-container at '${segment}'`
3507
+ }
3508
+ };
3509
+ }
3510
+ return {
3511
+ ok: true,
3512
+ node: current
3513
+ };
3514
+ }
3515
+ function parseArrayIndexTokenForDoc$1(token, op, path, opIndex) {
3516
+ if (token === "-") {
3517
+ if (op !== "add") return {
3518
+ ok: false,
3519
+ code: 409,
3520
+ reason: "INVALID_POINTER",
3521
+ message: `'-' index is only valid for add at ${path}`,
3522
+ path,
3523
+ opIndex
3524
+ };
3525
+ return {
3526
+ ok: true,
3527
+ index: Number.POSITIVE_INFINITY
3528
+ };
3529
+ }
3530
+ if (!ARRAY_INDEX_TOKEN_PATTERN.test(token)) return {
3531
+ ok: false,
3532
+ code: 409,
3533
+ reason: "INVALID_POINTER",
3534
+ message: `expected array index at ${path}`,
3535
+ path,
3536
+ opIndex
3537
+ };
3538
+ const index = Number(token);
3539
+ if (!Number.isSafeInteger(index)) return {
3540
+ ok: false,
3541
+ code: 409,
3542
+ reason: "OUT_OF_BOUNDS",
3543
+ message: `array index is too large at ${path}`,
3544
+ path,
3545
+ opIndex
3546
+ };
3547
+ return {
3548
+ ok: true,
3549
+ index
3550
+ };
3551
+ }
3552
+ function validateArrayIndexBounds$1(index, op, arrLength, path, opIndex) {
3553
+ if (op === "add") {
3554
+ if (index === Number.POSITIVE_INFINITY) return {
3555
+ ok: true,
3556
+ index
3557
+ };
3558
+ if (index > arrLength) return {
3559
+ ok: false,
3560
+ code: 409,
3561
+ reason: "OUT_OF_BOUNDS",
3562
+ message: `index out of bounds at ${path}; expected 0..${arrLength}`,
3563
+ path,
3564
+ opIndex
3565
+ };
3566
+ } else if (index >= arrLength) return {
3567
+ ok: false,
3568
+ code: 409,
3569
+ reason: "OUT_OF_BOUNDS",
3570
+ message: `index out of bounds at ${path}; expected 0..${Math.max(arrLength - 1, 0)}`,
3571
+ path,
3572
+ opIndex
3573
+ };
3574
+ return {
3575
+ ok: true,
3576
+ index
3577
+ };
3578
+ }
3579
+ function withOpIndex$1(error, opIndex) {
3580
+ if (error.opIndex !== void 0) return error;
3581
+ return {
3582
+ ...error,
3583
+ opIndex
3584
+ };
3585
+ }
3586
+ function isJsonPatchToCrdtOptions(value) {
3587
+ return typeof value === "object" && value !== null && "base" in value && "head" in value && "patch" in value && "newDot" in value;
3588
+ }
3589
+ function toApplyError$1(error) {
3590
+ if (error instanceof TraversalDepthError) return toDepthApplyError(error);
3591
+ if (error instanceof PatchCompileError) return {
3592
+ ok: false,
3593
+ code: 409,
3594
+ reason: error.reason,
3595
+ message: error.message,
3596
+ path: error.path,
3597
+ opIndex: error.opIndex
3598
+ };
3599
+ return {
3600
+ ok: false,
3601
+ code: 409,
3602
+ reason: "INVALID_PATCH",
3603
+ message: error instanceof Error ? error.message : "failed to compile/apply patch"
3604
+ };
3605
+ }
3606
+ function assertNever(_value, message) {
3607
+ throw new Error(message);
3608
+ }
3609
+
3610
+ //#endregion
3611
+ //#region src/state.ts
3612
+ /** Error thrown when a JSON Patch cannot be applied. Includes structured conflict metadata. */
3613
+ var PatchError = class extends Error {
3614
+ code;
3615
+ reason;
3616
+ budget;
3617
+ limit;
3618
+ actual;
3619
+ path;
3620
+ opIndex;
3621
+ constructor(errorOrMessage, code = 409, reason = "INVALID_PATCH") {
3622
+ super(typeof errorOrMessage === "string" ? errorOrMessage : errorOrMessage.message);
3623
+ this.name = "PatchError";
3624
+ if (typeof errorOrMessage === "string") {
3625
+ this.code = code;
3626
+ this.reason = reason;
3627
+ return;
3628
+ }
3629
+ this.code = errorOrMessage.code;
3017
3630
  this.reason = errorOrMessage.reason;
3631
+ if (errorOrMessage.reason === "RESOURCE_BUDGET_EXCEEDED") {
3632
+ this.budget = errorOrMessage.budget;
3633
+ this.limit = errorOrMessage.limit;
3634
+ this.actual = errorOrMessage.actual;
3635
+ }
3018
3636
  this.path = errorOrMessage.path;
3019
3637
  this.opIndex = errorOrMessage.opIndex;
3020
3638
  }
@@ -3079,26 +3697,27 @@ function applyPatchInPlace(state, patch, options = {}) {
3079
3697
  }
3080
3698
  /** Non-throwing immutable patch application variant. */
3081
3699
  function tryApplyPatch(state, patch, options = {}) {
3082
- const nextState = {
3083
- doc: cloneDoc(state.doc),
3084
- clock: cloneClock(state.clock)
3085
- };
3086
3700
  try {
3701
+ throwIfAborted(options.signal);
3702
+ const nextState = {
3703
+ doc: cloneDoc(state.doc),
3704
+ clock: cloneClock(state.clock)
3705
+ };
3087
3706
  const result = applyPatchInternal(nextState, patch, options, "batch");
3088
3707
  if (!result.ok) return {
3089
3708
  ok: false,
3090
3709
  error: result
3091
3710
  };
3711
+ return {
3712
+ ok: true,
3713
+ state: nextState
3714
+ };
3092
3715
  } catch (error) {
3093
3716
  return {
3094
3717
  ok: false,
3095
3718
  error: toApplyError(error)
3096
3719
  };
3097
3720
  }
3098
- return {
3099
- ok: true,
3100
- state: nextState
3101
- };
3102
3721
  }
3103
3722
  /** Non-throwing in-place patch application variant. */
3104
3723
  function tryApplyPatchInPlace(state, patch, options = {}) {
@@ -3129,10 +3748,13 @@ function tryApplyPatchInPlace(state, patch, options = {}) {
3129
3748
  * Does not mutate caller-provided values.
3130
3749
  */
3131
3750
  function validateJsonPatch(base, patch, options = {}) {
3132
- const result = tryApplyPatch(createState(base, {
3751
+ const result = tryApplyPatchInPlace(createState(base, {
3133
3752
  actor: "__validate__",
3134
3753
  jsonValidation: options.jsonValidation
3135
- }), patch, options);
3754
+ }), patch, {
3755
+ ...options,
3756
+ atomic: false
3757
+ });
3136
3758
  if (!result.ok) return {
3137
3759
  ok: false,
3138
3760
  error: result.error
@@ -3175,28 +3797,27 @@ function toApplyPatchOptionsForActor(options) {
3175
3797
  testAgainst: options.testAgainst,
3176
3798
  strictParents: options.strictParents,
3177
3799
  jsonValidation: options.jsonValidation,
3800
+ signal: options.signal,
3178
3801
  base: options.base ? {
3179
3802
  doc: options.base,
3180
3803
  clock: createClock("__base__", 0)
3181
3804
  } : void 0
3182
3805
  };
3183
3806
  }
3184
- function applyPatchInternal(state, patch, options, execution) {
3807
+ function applyPatchInternal(state, patch, options, _execution) {
3808
+ throwIfAborted(options.signal);
3185
3809
  const preparedPatch = preparePatchPayloadsSafe(patch, options.jsonValidation ?? "none");
3186
3810
  if (!preparedPatch.ok) return preparedPatch;
3811
+ createBudgetMeter(options.resourceBudget)?.count("patchOperations", preparedPatch.patch.length);
3187
3812
  const runtimePatch = preparedPatch.patch;
3188
3813
  if ((options.semantics ?? "sequential") === "sequential") {
3189
- if (!options.base && execution === "batch") {
3190
- const compiled = compilePreparedIntents(materialize(state.doc.root), runtimePatch, "sequential");
3191
- if (!compiled.ok) return compiled;
3192
- return applyIntentsToCrdt(state.doc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
3193
- }
3194
3814
  const explicitBaseState = options.base ? {
3195
3815
  doc: cloneDoc(options.base.doc),
3196
3816
  clock: createClock("__base__", 0)
3197
3817
  } : null;
3198
3818
  const session = { pointerCache: /* @__PURE__ */ new Map() };
3199
3819
  for (const [opIndex, op] of runtimePatch.entries()) {
3820
+ throwIfAborted(options.signal);
3200
3821
  const step = applyPatchOpSequential(state, op, options, explicitBaseState ? explicitBaseState.doc : state.doc, explicitBaseState, opIndex, session);
3201
3822
  if (!step.ok) return step;
3202
3823
  }
@@ -3244,10 +3865,10 @@ function applySinglePatchOpSequentialStep(state, baseDoc, op, options, explicitB
3244
3865
  const compiled = compilePreparedSingleIntentFromDoc(baseDoc, op, session.pointerCache, opIndex);
3245
3866
  if (!compiled.ok) return compiled;
3246
3867
  const headStep = applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
3247
- if (!headStep.ok) return headStep;
3868
+ if (!headStep.ok) return withOpIndex(headStep, opIndex);
3248
3869
  if (explicitBaseState && op.op !== "test") {
3249
3870
  const shadowStep = applyIntentsToCrdt(explicitBaseState.doc, explicitBaseState.doc, compiled.intents, () => explicitBaseState.clock.next(), "base", (ctr) => bumpClockCounter(explicitBaseState, ctr), { strictParents: options.strictParents });
3250
- if (!shadowStep.ok) return shadowStep;
3871
+ if (!shadowStep.ok) return withOpIndex(shadowStep, opIndex);
3251
3872
  }
3252
3873
  return { ok: true };
3253
3874
  }
@@ -3255,7 +3876,7 @@ function applySinglePatchOpExplicitShadowStep(explicitBaseState, op, options, op
3255
3876
  const compiled = compilePreparedSingleIntentFromDoc(explicitBaseState.doc, op, session.pointerCache, opIndex);
3256
3877
  if (!compiled.ok) return compiled;
3257
3878
  const shadowStep = applyIntentsToCrdt(explicitBaseState.doc, explicitBaseState.doc, compiled.intents, () => explicitBaseState.clock.next(), "base", (ctr) => bumpClockCounter(explicitBaseState, ctr), { strictParents: options.strictParents });
3258
- if (!shadowStep.ok) return shadowStep;
3879
+ if (!shadowStep.ok) return withOpIndex(shadowStep, opIndex);
3259
3880
  return { ok: true };
3260
3881
  }
3261
3882
  function resolveValueAtPointerInDoc(doc, pointer, opIndex, pointerCache) {
@@ -3535,9 +4156,9 @@ function validateArrayIndexBounds(index, op, arrLength, path, opIndex) {
3535
4156
  function bumpClockCounter(state, ctr) {
3536
4157
  if (state.clock.ctr < ctr) state.clock.ctr = ctr;
3537
4158
  }
3538
- function compilePreparedIntents(baseJson, patch, semantics = "sequential", pointerCache, opIndexOffset = 0) {
4159
+ function compilePreparedIntents(baseJson, patch, semantics = "sequential", pointerCache, opIndexOffset = 0, budgetMeter) {
3539
4160
  try {
3540
- const compileOptions = toCompilePatchOptions(semantics, pointerCache, opIndexOffset);
4161
+ const compileOptions = toCompilePatchOptions(semantics, pointerCache, opIndexOffset, budgetMeter);
3541
4162
  if (patch.length === 1) return {
3542
4163
  ok: true,
3543
4164
  intents: compileJsonPatchOpToIntent(baseJson, patch[0], compileOptions)
@@ -3550,11 +4171,13 @@ function compilePreparedIntents(baseJson, patch, semantics = "sequential", point
3550
4171
  return toApplyError(error);
3551
4172
  }
3552
4173
  }
3553
- function toCompilePatchOptions(semantics, pointerCache, opIndexOffset = 0) {
4174
+ function toCompilePatchOptions(semantics, pointerCache, opIndexOffset = 0, budgetMeter) {
3554
4175
  return {
3555
4176
  semantics,
4177
+ resourceBudget: void 0,
3556
4178
  pointerCache,
3557
- opIndexOffset
4179
+ opIndexOffset,
4180
+ budgetMeter
3558
4181
  };
3559
4182
  }
3560
4183
  function preparePatchPayloadsSafe(patch, mode) {
@@ -3602,6 +4225,8 @@ function mergePointerPaths(basePointer, nestedPointer) {
3602
4225
  return `${basePointer}${nestedPointer}`;
3603
4226
  }
3604
4227
  function toApplyError(error) {
4228
+ if (error instanceof OperationCancelledError) return toCancellationApplyError(error);
4229
+ if (error instanceof ResourceBudgetError) return toBudgetApplyError(error);
3605
4230
  if (error instanceof TraversalDepthError) return toDepthApplyError(error);
3606
4231
  if (error instanceof PatchCompileError) return {
3607
4232
  ok: false,
@@ -3618,6 +4243,13 @@ function toApplyError(error) {
3618
4243
  message: error instanceof Error ? error.message : "failed to compile patch"
3619
4244
  };
3620
4245
  }
4246
+ function withOpIndex(error, opIndex) {
4247
+ if (error.opIndex !== void 0) return error;
4248
+ return {
4249
+ ...error,
4250
+ opIndex
4251
+ };
4252
+ }
3621
4253
  function toPointerParseApplyError(error, pointer, opIndex) {
3622
4254
  return {
3623
4255
  ok: false,
@@ -3629,6 +4261,68 @@ function toPointerParseApplyError(error, pointer, opIndex) {
3629
4261
  };
3630
4262
  }
3631
4263
 
4264
+ //#endregion
4265
+ //#region src/safe.ts
4266
+ /** Create a state with strict runtime JSON validation enabled by default. */
4267
+ function createSafeState(initial, options) {
4268
+ return createState(initial, {
4269
+ ...options,
4270
+ jsonValidation: "strict"
4271
+ });
4272
+ }
4273
+ /** Apply a patch with strict runtime JSON validation enabled by default. */
4274
+ function applySafePatch(state, patch, options = {}) {
4275
+ return applyPatch(state, patch, {
4276
+ ...options,
4277
+ jsonValidation: "strict"
4278
+ });
4279
+ }
4280
+ /** Diff JSON values with strict runtime JSON validation enabled by default. */
4281
+ function diffSafeJsonPatch(base, next, options = {}) {
4282
+ return diffJsonPatch(base, next, {
4283
+ ...options,
4284
+ jsonValidation: "strict"
4285
+ });
4286
+ }
4287
+ /** Create a state with normalizing runtime JSON validation enabled by default. */
4288
+ function createNormalizedState(initial, options) {
4289
+ return createState(initial, {
4290
+ ...options,
4291
+ jsonValidation: "normalize"
4292
+ });
4293
+ }
4294
+ /** Apply a patch with normalizing runtime JSON validation enabled by default. */
4295
+ function applyNormalizedPatch(state, patch, options = {}) {
4296
+ return applyPatch(state, patch, {
4297
+ ...options,
4298
+ jsonValidation: "normalize"
4299
+ });
4300
+ }
4301
+ /** Diff JSON values with normalizing runtime JSON validation enabled by default. */
4302
+ function diffNormalizedJsonPatch(base, next, options = {}) {
4303
+ return diffJsonPatch(base, next, {
4304
+ ...options,
4305
+ jsonValidation: "normalize"
4306
+ });
4307
+ }
4308
+
4309
+ //#endregion
4310
+ //#region src/options.ts
4311
+ /** Options that reject legacy missing-parent array auto-creation. */
4312
+ const strictRfc6902PatchOptions = { strictParents: true };
4313
+ function withStrictRfc6902Parents(options) {
4314
+ return {
4315
+ ...options,
4316
+ strictParents: true
4317
+ };
4318
+ }
4319
+ function withLegacyMissingArrayParents(options) {
4320
+ return {
4321
+ ...options,
4322
+ strictParents: false
4323
+ };
4324
+ }
4325
+
3632
4326
  //#endregion
3633
4327
  //#region src/serialize.ts
3634
4328
  const HEAD_ELEM_ID = "HEAD";
@@ -3664,18 +4358,39 @@ function serializeDoc(doc) {
3664
4358
  };
3665
4359
  }
3666
4360
  /** Reconstruct a CRDT document from its serialized form. */
3667
- function deserializeDoc(data) {
4361
+ function deserializeDoc(data, options = {}) {
4362
+ return deserializeDocInternal(data, createBudgetMeter(options.resourceBudget), options.signal);
4363
+ }
4364
+ function deserializeDocInternal(data, budgetMeter, signal) {
4365
+ throwIfAborted(signal);
3668
4366
  const raw = readSerializedDocEnvelope(data);
3669
4367
  if (!("root" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/root", "serialized doc is missing root");
3670
- return { root: deserializeNode(raw.root, "/root", 0) };
4368
+ const observed = Object.create(null);
4369
+ const doc = { root: deserializeNode(raw.root, "/root", 0, budgetMeter, observed, signal) };
4370
+ writeCachedObservedVersionVector(doc, observed);
4371
+ return doc;
3671
4372
  }
3672
4373
  /** Non-throwing `deserializeDoc` variant with typed validation details. */
3673
- function tryDeserializeDoc(data) {
4374
+ function tryDeserializeDoc(data, options = {}) {
3674
4375
  try {
3675
4376
  return {
3676
4377
  ok: true,
3677
- doc: deserializeDoc(data)
4378
+ doc: deserializeDoc(data, options)
4379
+ };
4380
+ } catch (error) {
4381
+ const deserializeError = toDeserializeFailure(error);
4382
+ if (deserializeError) return {
4383
+ ok: false,
4384
+ error: deserializeError
3678
4385
  };
4386
+ throw error;
4387
+ }
4388
+ }
4389
+ /** Validate a serialized CRDT document without throwing or returning runtime state. */
4390
+ function validateSerializedDoc(data, options = {}) {
4391
+ try {
4392
+ validateSerializedDocInternal(data, createBudgetMeter(options.resourceBudget), options.signal);
4393
+ return { ok: true };
3679
4394
  } catch (error) {
3680
4395
  const deserializeError = toDeserializeFailure(error);
3681
4396
  if (deserializeError) return {
@@ -3702,27 +4417,56 @@ function serializeState(state) {
3702
4417
  * May throw `TraversalDepthError` when the payload exceeds the maximum
3703
4418
  * supported nesting depth.
3704
4419
  */
3705
- function deserializeState(data) {
3706
- const raw = readSerializedStateEnvelope(data);
3707
- if (!("doc" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/doc", "serialized state is missing doc");
3708
- if (!("clock" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/clock", "serialized state is missing clock");
3709
- const clockRaw = asRecord(raw.clock, "/clock");
3710
- const actor = readActor(clockRaw.actor, "/clock/actor");
3711
- const ctr = readCounter(clockRaw.ctr, "/clock/ctr");
3712
- const doc = deserializeDoc(raw.doc);
3713
- const observedCtr = observedVersionVector(doc)[actor] ?? 0;
3714
- return {
3715
- doc,
3716
- clock: createClock(actor, Math.max(ctr, observedCtr))
3717
- };
4420
+ function deserializeState(data, options = {}) {
4421
+ try {
4422
+ const raw = readSerializedStateEnvelope(data);
4423
+ const budgetMeter = createBudgetMeter(options.resourceBudget);
4424
+ throwIfAborted(options.signal);
4425
+ if (!("doc" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/doc", "serialized state is missing doc");
4426
+ if (!("clock" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/clock", "serialized state is missing clock");
4427
+ const clockRaw = asRecord(raw.clock, "/clock");
4428
+ const actor = readActor(clockRaw.actor, "/clock/actor");
4429
+ const ctr = readCounter(clockRaw.ctr, "/clock/ctr");
4430
+ const doc = deserializeDocInternal(raw.doc, budgetMeter, options.signal);
4431
+ const observedCtr = readCachedObservedVersionVector(doc)?.[actor] ?? 0;
4432
+ return {
4433
+ doc,
4434
+ clock: createClock(actor, Math.max(ctr, observedCtr))
4435
+ };
4436
+ } catch (error) {
4437
+ if (error instanceof OperationCancelledError) throw new DeserializeError("OPERATION_CANCELLED", "/", error.message);
4438
+ throw error;
4439
+ }
3718
4440
  }
3719
4441
  /** Non-throwing `deserializeState` variant with typed validation details. */
3720
- function tryDeserializeState(data) {
4442
+ function tryDeserializeState(data, options = {}) {
3721
4443
  try {
3722
4444
  return {
3723
4445
  ok: true,
3724
- state: deserializeState(data)
4446
+ state: deserializeState(data, options)
4447
+ };
4448
+ } catch (error) {
4449
+ const deserializeError = toDeserializeFailure(error);
4450
+ if (deserializeError) return {
4451
+ ok: false,
4452
+ error: deserializeError
3725
4453
  };
4454
+ throw error;
4455
+ }
4456
+ }
4457
+ /** Validate a serialized CRDT state without throwing or returning runtime state. */
4458
+ function validateSerializedState(data, options = {}) {
4459
+ try {
4460
+ const raw = readSerializedStateEnvelope(data);
4461
+ const budgetMeter = createBudgetMeter(options.resourceBudget);
4462
+ throwIfAborted(options.signal);
4463
+ if (!("doc" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/doc", "serialized state is missing doc");
4464
+ if (!("clock" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/clock", "serialized state is missing clock");
4465
+ const clockRaw = asRecord(raw.clock, "/clock");
4466
+ readActor(clockRaw.actor, "/clock/actor");
4467
+ readCounter(clockRaw.ctr, "/clock/ctr");
4468
+ validateSerializedDocInternal(raw.doc, budgetMeter, options.signal);
4469
+ return { ok: true };
3726
4470
  } catch (error) {
3727
4471
  const deserializeError = toDeserializeFailure(error);
3728
4472
  if (deserializeError) return {
@@ -3794,33 +4538,55 @@ function readSerializedStateEnvelope(data) {
3794
4538
  assertSerializedEnvelopeVersion(raw, "/version", SERIALIZED_STATE_VERSION, "state");
3795
4539
  return raw;
3796
4540
  }
3797
- function deserializeNode(node, path, depth) {
4541
+ function validateSerializedDocInternal(data, budgetMeter, signal) {
4542
+ throwIfAborted(signal);
4543
+ const raw = readSerializedDocEnvelope(data);
4544
+ if (!("root" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/root", "serialized doc is missing root");
4545
+ validateSerializedNode(raw.root, "/root", 0, budgetMeter, signal);
4546
+ }
4547
+ function deserializeNode(node, path, depth, budgetMeter, observed, signal) {
4548
+ throwIfAborted(signal);
3798
4549
  assertTraversalDepth(depth);
4550
+ budgetMeter?.count("visitedNodes", 1, path);
3799
4551
  const raw = asRecord(node, path);
3800
4552
  const kind = readString(raw.kind, `${path}/kind`);
3801
4553
  if (kind === "lww") {
3802
4554
  if (!("value" in raw)) fail("INVALID_SERIALIZED_SHAPE", `${path}/value`, "lww node is missing value");
3803
4555
  if (!("dot" in raw)) fail("INVALID_SERIALIZED_SHAPE", `${path}/dot`, "lww node is missing dot");
4556
+ const dot = readDot(raw.dot, `${path}/dot`);
4557
+ if (observed) observeVersionVectorDot(observed, dot);
3804
4558
  return {
3805
4559
  kind: "lww",
3806
- value: structuredClone(readJsonValue(raw.value, `${path}/value`, depth + 1)),
3807
- dot: readDot(raw.dot, `${path}/dot`)
4560
+ value: structuredClone(readJsonValue(raw.value, `${path}/value`, depth + 1, budgetMeter, signal)),
4561
+ dot
3808
4562
  };
3809
4563
  }
3810
4564
  if (kind === "obj") {
3811
4565
  const entriesRaw = asRecord(raw.entries, `${path}/entries`);
3812
4566
  const tombstoneRaw = asRecord(raw.tombstone, `${path}/tombstone`);
4567
+ budgetMeter?.count("objectEntries", Object.keys(entriesRaw).length, `${path}/entries`);
4568
+ budgetMeter?.count("serializedElements", Object.keys(entriesRaw).length, `${path}/entries`);
4569
+ budgetMeter?.count("objectEntries", Object.keys(tombstoneRaw).length, `${path}/tombstone`);
4570
+ budgetMeter?.count("serializedElements", Object.keys(tombstoneRaw).length, `${path}/tombstone`);
3813
4571
  const entries = /* @__PURE__ */ new Map();
3814
4572
  for (const [k, v] of Object.entries(entriesRaw)) {
4573
+ throwIfAborted(signal);
3815
4574
  const entryPath = `${path}/entries/${k}`;
3816
4575
  const entryRaw = asRecord(v, entryPath);
4576
+ const dot = readDot(entryRaw.dot, `${entryPath}/dot`);
4577
+ if (observed) observeVersionVectorDot(observed, dot);
3817
4578
  entries.set(k, {
3818
- node: deserializeNode(entryRaw.node, `${entryPath}/node`, depth + 1),
3819
- dot: readDot(entryRaw.dot, `${entryPath}/dot`)
4579
+ node: deserializeNode(entryRaw.node, `${entryPath}/node`, depth + 1, budgetMeter, observed, signal),
4580
+ dot
3820
4581
  });
3821
4582
  }
3822
4583
  const tombstone = /* @__PURE__ */ new Map();
3823
- for (const [k, d] of Object.entries(tombstoneRaw)) tombstone.set(k, readDot(d, `${path}/tombstone/${k}`));
4584
+ for (const [k, d] of Object.entries(tombstoneRaw)) {
4585
+ throwIfAborted(signal);
4586
+ const dot = readDot(d, `${path}/tombstone/${k}`);
4587
+ if (observed) observeVersionVectorDot(observed, dot);
4588
+ tombstone.set(k, dot);
4589
+ }
3824
4590
  return {
3825
4591
  kind: "obj",
3826
4592
  entries,
@@ -3829,17 +4595,24 @@ function deserializeNode(node, path, depth) {
3829
4595
  }
3830
4596
  if (kind !== "seq") fail("INVALID_SERIALIZED_SHAPE", `${path}/kind`, `unsupported node kind '${kind}'`);
3831
4597
  const elemsRaw = asRecord(raw.elems, `${path}/elems`);
4598
+ budgetMeter?.count("sequenceElements", Object.keys(elemsRaw).length, `${path}/elems`);
4599
+ budgetMeter?.count("serializedElements", Object.keys(elemsRaw).length, `${path}/elems`);
3832
4600
  const elems = /* @__PURE__ */ new Map();
3833
4601
  for (const [id, rawElem] of Object.entries(elemsRaw)) {
4602
+ throwIfAborted(signal);
3834
4603
  const elemPath = `${path}/elems/${id}`;
3835
4604
  const elem = asRecord(rawElem, elemPath);
3836
4605
  const elemId = readString(elem.id, `${elemPath}/id`);
3837
4606
  if (elemId !== id) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/id`, `sequence element id '${elemId}' does not match key '${id}'`);
3838
4607
  const prev = readString(elem.prev, `${elemPath}/prev`);
3839
4608
  const tombstone = readBoolean(elem.tombstone, `${elemPath}/tombstone`);
3840
- const value = deserializeNode(elem.value, `${elemPath}/value`, depth + 1);
4609
+ const value = deserializeNode(elem.value, `${elemPath}/value`, depth + 1, budgetMeter, observed, signal);
3841
4610
  const insDot = readDot(elem.insDot, `${elemPath}/insDot`);
3842
4611
  const delDot = "delDot" in elem && elem.delDot !== void 0 ? readDot(elem.delDot, `${elemPath}/delDot`) : void 0;
4612
+ if (observed) {
4613
+ observeVersionVectorDot(observed, insDot);
4614
+ if (delDot) observeVersionVectorDot(observed, delDot);
4615
+ }
3843
4616
  if (dotToElemId(insDot) !== id) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/insDot`, "sequence element id must match its insertion dot");
3844
4617
  if (!tombstone && delDot) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/delDot`, "live sequence elements must not include delete metadata");
3845
4618
  elems.set(id, {
@@ -3852,6 +4625,7 @@ function deserializeNode(node, path, depth) {
3852
4625
  });
3853
4626
  }
3854
4627
  for (const elem of elems.values()) {
4628
+ throwIfAborted(signal);
3855
4629
  if (elem.prev === elem.id) fail("INVALID_SERIALIZED_INVARIANT", `${path}/elems/${elem.id}/prev`, "sequence element cannot reference itself as predecessor");
3856
4630
  if (elem.prev !== HEAD_ELEM_ID && !elems.has(elem.prev)) fail("INVALID_SERIALIZED_INVARIANT", `${path}/elems/${elem.id}/prev`, `sequence predecessor '${elem.prev}' does not exist`);
3857
4631
  }
@@ -3861,6 +4635,66 @@ function deserializeNode(node, path, depth) {
3861
4635
  elems
3862
4636
  };
3863
4637
  }
4638
+ function validateSerializedNode(node, path, depth, budgetMeter, signal) {
4639
+ throwIfAborted(signal);
4640
+ assertTraversalDepth(depth);
4641
+ budgetMeter?.count("visitedNodes", 1, path);
4642
+ const raw = asRecord(node, path);
4643
+ const kind = readString(raw.kind, `${path}/kind`);
4644
+ if (kind === "lww") {
4645
+ if (!("value" in raw)) fail("INVALID_SERIALIZED_SHAPE", `${path}/value`, "lww node is missing value");
4646
+ if (!("dot" in raw)) fail("INVALID_SERIALIZED_SHAPE", `${path}/dot`, "lww node is missing dot");
4647
+ readDot(raw.dot, `${path}/dot`);
4648
+ readJsonValue(raw.value, `${path}/value`, depth + 1, budgetMeter, signal);
4649
+ return;
4650
+ }
4651
+ if (kind === "obj") {
4652
+ const entriesRaw = asRecord(raw.entries, `${path}/entries`);
4653
+ const tombstoneRaw = asRecord(raw.tombstone, `${path}/tombstone`);
4654
+ budgetMeter?.count("objectEntries", Object.keys(entriesRaw).length, `${path}/entries`);
4655
+ budgetMeter?.count("serializedElements", Object.keys(entriesRaw).length, `${path}/entries`);
4656
+ budgetMeter?.count("objectEntries", Object.keys(tombstoneRaw).length, `${path}/tombstone`);
4657
+ budgetMeter?.count("serializedElements", Object.keys(tombstoneRaw).length, `${path}/tombstone`);
4658
+ for (const [k, v] of Object.entries(entriesRaw)) {
4659
+ throwIfAborted(signal);
4660
+ const entryPath = `${path}/entries/${k}`;
4661
+ const entryRaw = asRecord(v, entryPath);
4662
+ readDot(entryRaw.dot, `${entryPath}/dot`);
4663
+ validateSerializedNode(entryRaw.node, `${entryPath}/node`, depth + 1, budgetMeter, signal);
4664
+ }
4665
+ for (const [k, d] of Object.entries(tombstoneRaw)) {
4666
+ throwIfAborted(signal);
4667
+ readDot(d, `${path}/tombstone/${k}`);
4668
+ }
4669
+ return;
4670
+ }
4671
+ if (kind !== "seq") fail("INVALID_SERIALIZED_SHAPE", `${path}/kind`, `unsupported node kind '${kind}'`);
4672
+ const elemsRaw = asRecord(raw.elems, `${path}/elems`);
4673
+ budgetMeter?.count("sequenceElements", Object.keys(elemsRaw).length, `${path}/elems`);
4674
+ budgetMeter?.count("serializedElements", Object.keys(elemsRaw).length, `${path}/elems`);
4675
+ const predecessors = /* @__PURE__ */ new Map();
4676
+ for (const [id, rawElem] of Object.entries(elemsRaw)) {
4677
+ throwIfAborted(signal);
4678
+ const elemPath = `${path}/elems/${id}`;
4679
+ const elem = asRecord(rawElem, elemPath);
4680
+ const elemId = readString(elem.id, `${elemPath}/id`);
4681
+ if (elemId !== id) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/id`, `sequence element id '${elemId}' does not match key '${id}'`);
4682
+ const prev = readString(elem.prev, `${elemPath}/prev`);
4683
+ const tombstone = readBoolean(elem.tombstone, `${elemPath}/tombstone`);
4684
+ validateSerializedNode(elem.value, `${elemPath}/value`, depth + 1, budgetMeter, signal);
4685
+ const insDot = readDot(elem.insDot, `${elemPath}/insDot`);
4686
+ const delDot = "delDot" in elem && elem.delDot !== void 0 ? readDot(elem.delDot, `${elemPath}/delDot`) : void 0;
4687
+ if (dotToElemId(insDot) !== id) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/insDot`, "sequence element id must match its insertion dot");
4688
+ if (!tombstone && delDot) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/delDot`, "live sequence elements must not include delete metadata");
4689
+ predecessors.set(id, prev);
4690
+ }
4691
+ for (const [id, prev] of predecessors) {
4692
+ throwIfAborted(signal);
4693
+ if (prev === id) fail("INVALID_SERIALIZED_INVARIANT", `${path}/elems/${id}/prev`, "sequence element cannot reference itself as predecessor");
4694
+ if (prev !== HEAD_ELEM_ID && !predecessors.has(prev)) fail("INVALID_SERIALIZED_INVARIANT", `${path}/elems/${id}/prev`, `sequence predecessor '${prev}' does not exist`);
4695
+ }
4696
+ assertAcyclicSerializedRgaPredecessors(predecessors, path);
4697
+ }
3864
4698
  function assertAcyclicRgaPredecessors(elems, path) {
3865
4699
  const visitState = /* @__PURE__ */ new Map();
3866
4700
  for (const startId of elems.keys()) {
@@ -3881,6 +4715,26 @@ function assertAcyclicRgaPredecessors(elems, path) {
3881
4715
  for (const id of trail) visitState.set(id, 2);
3882
4716
  }
3883
4717
  }
4718
+ function assertAcyclicSerializedRgaPredecessors(predecessors, path) {
4719
+ const visitState = /* @__PURE__ */ new Map();
4720
+ for (const startId of predecessors.keys()) {
4721
+ if (visitState.get(startId) === 2) continue;
4722
+ const trail = [];
4723
+ const trailSet = /* @__PURE__ */ new Set();
4724
+ let currentId = startId;
4725
+ while (currentId) {
4726
+ if (trailSet.has(currentId)) fail("INVALID_SERIALIZED_INVARIANT", `${path}/elems/${currentId}/prev`, `sequence predecessor cycle detected at '${currentId}'`);
4727
+ if (visitState.get(currentId) === 2) break;
4728
+ trail.push(currentId);
4729
+ trailSet.add(currentId);
4730
+ visitState.set(currentId, 1);
4731
+ const prev = predecessors.get(currentId);
4732
+ if (!prev || prev === HEAD_ELEM_ID) break;
4733
+ currentId = prev;
4734
+ }
4735
+ for (const id of trail) visitState.set(id, 2);
4736
+ }
4737
+ }
3884
4738
  function asRecord(value, path) {
3885
4739
  if (!isRecord(value)) fail("INVALID_SERIALIZED_SHAPE", path, "expected object");
3886
4740
  return value;
@@ -3918,28 +4772,43 @@ function readBoolean(value, path) {
3918
4772
  if (typeof value !== "boolean") fail("INVALID_SERIALIZED_SHAPE", path, "expected boolean");
3919
4773
  return value;
3920
4774
  }
3921
- function readJsonValue(value, path, depth) {
3922
- assertJsonValue(value, path, depth);
4775
+ function readJsonValue(value, path, depth, budgetMeter, signal) {
4776
+ assertJsonValue(value, path, depth, budgetMeter, signal);
3923
4777
  return value;
3924
4778
  }
3925
- function assertJsonValue(value, path, depth) {
4779
+ function assertJsonValue(value, path, depth, budgetMeter, signal) {
4780
+ throwIfAborted(signal);
3926
4781
  assertTraversalDepth(depth);
4782
+ budgetMeter?.count("visitedNodes", 1, path);
3927
4783
  if (value === null || typeof value === "string" || typeof value === "boolean") return;
3928
4784
  if (typeof value === "number") {
3929
4785
  if (!Number.isFinite(value)) fail("INVALID_SERIALIZED_SHAPE", path, "json number must be finite");
3930
4786
  return;
3931
4787
  }
3932
4788
  if (Array.isArray(value)) {
3933
- for (const [index, item] of value.entries()) assertJsonValue(item, `${path}/${index}`, depth + 1);
4789
+ budgetMeter?.count("sequenceElements", value.length, path);
4790
+ budgetMeter?.count("serializedElements", value.length, path);
4791
+ for (const [index, item] of value.entries()) {
4792
+ throwIfAborted(signal);
4793
+ assertJsonValue(item, `${path}/${index}`, depth + 1, budgetMeter, signal);
4794
+ }
3934
4795
  return;
3935
4796
  }
3936
4797
  if (!isRecord(value)) fail("INVALID_SERIALIZED_SHAPE", path, "expected JSON value");
3937
- for (const [key, child] of Object.entries(value)) assertJsonValue(child, `${path}/${key}`, depth + 1);
4798
+ const entries = Object.entries(value);
4799
+ budgetMeter?.count("objectEntries", entries.length, path);
4800
+ budgetMeter?.count("serializedElements", entries.length, path);
4801
+ for (const [key, child] of entries) {
4802
+ throwIfAborted(signal);
4803
+ assertJsonValue(child, `${path}/${key}`, depth + 1, budgetMeter, signal);
4804
+ }
3938
4805
  }
3939
4806
  function fail(reason, path, message) {
3940
4807
  throw new DeserializeError(reason, path, message);
3941
4808
  }
3942
4809
  function toDeserializeFailure(error) {
4810
+ if (error instanceof ResourceBudgetError) return toBudgetDeserializeFailure(error);
4811
+ if (error instanceof OperationCancelledError) return toCancellationDeserializeFailure(error);
3943
4812
  if (error instanceof DeserializeError || error instanceof TraversalDepthError) return error;
3944
4813
  return null;
3945
4814
  }
@@ -3961,12 +4830,20 @@ var SharedElementMetadataMismatchError = class extends Error {
3961
4830
  var MergeError = class extends Error {
3962
4831
  code;
3963
4832
  reason;
4833
+ budget;
4834
+ limit;
4835
+ actual;
3964
4836
  path;
3965
4837
  constructor(error) {
3966
4838
  super(error.message);
3967
4839
  this.name = "MergeError";
3968
4840
  this.code = error.code;
3969
4841
  this.reason = error.reason;
4842
+ if (error.reason === "RESOURCE_BUDGET_EXCEEDED") {
4843
+ this.budget = error.budget;
4844
+ this.limit = error.limit;
4845
+ this.actual = error.actual;
4846
+ }
3970
4847
  this.path = error.path;
3971
4848
  }
3972
4849
  };
@@ -3991,20 +4868,27 @@ function mergeDoc(a, b, options = {}) {
3991
4868
  /** Non-throwing `mergeDoc` variant with structured conflict details. */
3992
4869
  function tryMergeDoc(a, b, options = {}) {
3993
4870
  try {
3994
- const mismatchPath = options.requireSharedOrigin ?? true ? findSeqLineageMismatch(a.root, b.root, []) : null;
3995
- if (mismatchPath !== null) return {
3996
- ok: false,
3997
- error: {
3998
- ok: false,
3999
- code: 409,
4000
- reason: "LINEAGE_MISMATCH",
4001
- message: `merge requires shared array origin at ${mismatchPath}`,
4002
- path: mismatchPath
4003
- }
4871
+ const config = {
4872
+ unrelatedArrays: resolveUnrelatedArraysStrategy(options),
4873
+ budgetMeter: createBudgetMeter(options.resourceBudget),
4874
+ signal: options.signal
4004
4875
  };
4876
+ if (config.unrelatedArrays === "reject") {
4877
+ const mismatchPath = findSeqLineageMismatch(a.root, b.root, [], config);
4878
+ if (mismatchPath !== null) return {
4879
+ ok: false,
4880
+ error: {
4881
+ ok: false,
4882
+ code: 409,
4883
+ reason: "LINEAGE_MISMATCH",
4884
+ message: `merge requires shared array origin at ${mismatchPath}`,
4885
+ path: mismatchPath
4886
+ }
4887
+ };
4888
+ }
4005
4889
  return {
4006
4890
  ok: true,
4007
- doc: { root: mergeNode(a.root, b.root) }
4891
+ doc: mergeDocRoot(a.root, b.root, config).doc
4008
4892
  };
4009
4893
  } catch (error) {
4010
4894
  if (error instanceof SharedElementMetadataMismatchError) return {
@@ -4021,6 +4905,14 @@ function tryMergeDoc(a, b, options = {}) {
4021
4905
  ok: false,
4022
4906
  error: toDepthApplyError(error)
4023
4907
  };
4908
+ if (error instanceof ResourceBudgetError) return {
4909
+ ok: false,
4910
+ error: toBudgetApplyError(error)
4911
+ };
4912
+ if (error instanceof OperationCancelledError) return {
4913
+ ok: false,
4914
+ error: toCancellationApplyError(error)
4915
+ };
4024
4916
  throw error;
4025
4917
  }
4026
4918
  }
@@ -4030,7 +4922,7 @@ function tryMergeDoc(a, b, options = {}) {
4030
4922
  * The merged clock keeps a stable actor identity:
4031
4923
  * - defaults to the actor from the first argument (`a`)
4032
4924
  * - can be overridden via `options.actor`
4033
- * - optional `options.requireSharedOrigin` controls merge lineage checks
4925
+ * - optional `options.unrelatedArrays` controls the merge strategy for non-overlapping sequences
4034
4926
  *
4035
4927
  * The merged counter is lifted to the highest counter already observed for
4036
4928
  * that actor across both input clocks and the merged document dots.
@@ -4042,29 +4934,79 @@ function mergeState(a, b, options = {}) {
4042
4934
  }
4043
4935
  /** Non-throwing `mergeState` variant with structured conflict details. */
4044
4936
  function tryMergeState(a, b, options = {}) {
4045
- const mergedDoc = tryMergeDoc(a.doc, b.doc, { requireSharedOrigin: options.requireSharedOrigin });
4046
- if (!mergedDoc.ok) return mergedDoc;
4047
- const doc = mergedDoc.doc;
4048
- const actor = options.actor ?? a.clock.actor;
4049
- return {
4050
- ok: true,
4051
- state: {
4052
- doc,
4053
- clock: createClock(actor, maxObservedCtrForActor(doc, actor, a, b))
4937
+ try {
4938
+ const actor = options.actor ?? a.clock.actor;
4939
+ const config = {
4940
+ actor,
4941
+ unrelatedArrays: resolveUnrelatedArraysStrategy(options),
4942
+ budgetMeter: createBudgetMeter(options.resourceBudget),
4943
+ signal: options.signal
4944
+ };
4945
+ if (config.unrelatedArrays === "reject") {
4946
+ const mismatchPath = findSeqLineageMismatch(a.doc.root, b.doc.root, [], config);
4947
+ if (mismatchPath !== null) return {
4948
+ ok: false,
4949
+ error: {
4950
+ ok: false,
4951
+ code: 409,
4952
+ reason: "LINEAGE_MISMATCH",
4953
+ message: `merge requires shared array origin at ${mismatchPath}`,
4954
+ path: mismatchPath
4955
+ }
4956
+ };
4054
4957
  }
4055
- };
4958
+ const merged = mergeDocRoot(a.doc.root, b.doc.root, config);
4959
+ const ctr = maxObservedCtrForActor(merged.maxObservedCtr, actor, a, b);
4960
+ return {
4961
+ ok: true,
4962
+ state: {
4963
+ doc: merged.doc,
4964
+ clock: createClock(actor, ctr)
4965
+ }
4966
+ };
4967
+ } catch (error) {
4968
+ if (error instanceof SharedElementMetadataMismatchError) return {
4969
+ ok: false,
4970
+ error: {
4971
+ ok: false,
4972
+ code: 409,
4973
+ reason: "LINEAGE_MISMATCH",
4974
+ message: error.message,
4975
+ path: error.path
4976
+ }
4977
+ };
4978
+ if (error instanceof TraversalDepthError) return {
4979
+ ok: false,
4980
+ error: toDepthApplyError(error)
4981
+ };
4982
+ if (error instanceof ResourceBudgetError) return {
4983
+ ok: false,
4984
+ error: toBudgetApplyError(error)
4985
+ };
4986
+ if (error instanceof OperationCancelledError) return {
4987
+ ok: false,
4988
+ error: toCancellationApplyError(error)
4989
+ };
4990
+ throw error;
4991
+ }
4056
4992
  }
4057
- function findSeqLineageMismatch(a, b, path) {
4993
+ function findSeqLineageMismatch(a, b, path, config) {
4994
+ const pathBuffer = [...path];
4058
4995
  const stack = [{
4059
4996
  a,
4060
4997
  b,
4061
- path,
4062
4998
  depth: path.length
4063
4999
  }];
4064
5000
  while (stack.length > 0) {
5001
+ throwIfAborted(config.signal);
4065
5002
  const frame = stack.pop();
4066
5003
  assertTraversalDepth(frame.depth);
5004
+ pathBuffer.length = frame.depth;
5005
+ if (frame.key !== void 0) pathBuffer[frame.depth - 1] = frame.key;
5006
+ const budgetPath = config.budgetMeter ? stringifyJsonPointer(pathBuffer) : void 0;
5007
+ config.budgetMeter?.count("visitedNodes", 1, budgetPath);
4067
5008
  if (frame.a.kind === "seq" && frame.b.kind === "seq") {
5009
+ config.budgetMeter?.count("sequenceElements", frame.a.elems.size + frame.b.elems.size, budgetPath);
4068
5010
  const hasElemsA = frame.a.elems.size > 0;
4069
5011
  const hasElemsB = frame.b.elems.size > 0;
4070
5012
  if (hasElemsA && hasElemsB) {
@@ -4073,157 +5015,277 @@ function findSeqLineageMismatch(a, b, path) {
4073
5015
  shared = true;
4074
5016
  break;
4075
5017
  }
4076
- if (!shared) return stringifyJsonPointer(frame.path);
5018
+ if (!shared) return stringifyJsonPointer(pathBuffer);
4077
5019
  }
4078
5020
  }
4079
5021
  if (frame.a.kind === "obj" && frame.b.kind === "obj") {
4080
5022
  const left = frame.a;
4081
5023
  const right = frame.b;
4082
- const sharedKeys = [...left.entries.keys()].filter((key) => right.entries.has(key));
4083
- for (let i = sharedKeys.length - 1; i >= 0; i--) {
4084
- const key = sharedKeys[i];
5024
+ if (config.budgetMeter) {
5025
+ let sharedKeyCount = 0;
5026
+ for (const key of left.entries.keys()) if (right.entries.has(key)) sharedKeyCount += 1;
5027
+ config.budgetMeter.count("objectEntries", sharedKeyCount, budgetPath);
5028
+ }
5029
+ const sharedKeysInOrder = [];
5030
+ for (const key of left.entries.keys()) if (right.entries.has(key)) sharedKeysInOrder.push(key);
5031
+ for (let i = sharedKeysInOrder.length - 1; i >= 0; i--) {
5032
+ const key = sharedKeysInOrder[i];
4085
5033
  const nextA = left.entries.get(key).node;
4086
5034
  const nextB = right.entries.get(key).node;
4087
5035
  stack.push({
4088
5036
  a: nextA,
4089
5037
  b: nextB,
4090
- path: [...frame.path, key],
4091
- depth: frame.depth + 1
5038
+ depth: frame.depth + 1,
5039
+ key
4092
5040
  });
4093
5041
  }
4094
5042
  }
4095
5043
  }
4096
5044
  return null;
4097
5045
  }
4098
- function maxObservedCtrForActor(doc, actor, a, b) {
4099
- let best = observedVersionVector(doc)[actor] ?? 0;
5046
+ function mergeDocRoot(a, b, config) {
5047
+ const merged = mergeNodeAtDepth(a, b, 0, [], config);
5048
+ return {
5049
+ doc: { root: merged.node },
5050
+ maxObservedCtr: merged.maxObservedCtr
5051
+ };
5052
+ }
5053
+ function resolveUnrelatedArraysStrategy(options) {
5054
+ if (options.unrelatedArrays !== void 0) return options.unrelatedArrays;
5055
+ if (options.requireSharedOrigin === false) return "unsafe-union";
5056
+ return "reject";
5057
+ }
5058
+ function maxObservedCtrForActor(docObservedCtr, actor, a, b) {
5059
+ let best = docObservedCtr;
4100
5060
  if (a.clock.actor === actor && a.clock.ctr > best) best = a.clock.ctr;
4101
5061
  if (b.clock.actor === actor && b.clock.ctr > best) best = b.clock.ctr;
4102
5062
  return best;
4103
5063
  }
4104
- function repDot(node) {
4105
- switch (node.kind) {
4106
- case "lww": return node.dot;
4107
- case "obj": {
4108
- let best = {
4109
- actor: "",
4110
- ctr: 0
4111
- };
4112
- for (const entry of node.entries.values()) if (compareDot(entry.dot, best) > 0) best = entry.dot;
4113
- for (const d of node.tombstone.values()) if (compareDot(d, best) > 0) best = d;
4114
- return best;
4115
- }
4116
- case "seq": {
4117
- let best = {
4118
- actor: "",
4119
- ctr: 0
4120
- };
4121
- for (const e of node.elems.values()) if (compareDot(e.insDot, best) > 0) best = e.insDot;
4122
- return best;
5064
+ function repDot(node, signal) {
5065
+ let best = {
5066
+ actor: "",
5067
+ ctr: 0
5068
+ };
5069
+ const stack = [{
5070
+ node,
5071
+ depth: 0
5072
+ }];
5073
+ while (stack.length > 0) {
5074
+ throwIfAborted(signal);
5075
+ const frame = stack.pop();
5076
+ assertTraversalDepth(frame.depth);
5077
+ switch (frame.node.kind) {
5078
+ case "lww":
5079
+ if (compareDot(frame.node.dot, best) > 0) best = frame.node.dot;
5080
+ break;
5081
+ case "obj":
5082
+ for (const entry of frame.node.entries.values()) {
5083
+ if (compareDot(entry.dot, best) > 0) best = entry.dot;
5084
+ stack.push({
5085
+ node: entry.node,
5086
+ depth: frame.depth + 1
5087
+ });
5088
+ }
5089
+ for (const tombstone of frame.node.tombstone.values()) if (compareDot(tombstone, best) > 0) best = tombstone;
5090
+ break;
5091
+ case "seq":
5092
+ for (const elem of frame.node.elems.values()) {
5093
+ if (compareDot(elem.insDot, best) > 0) best = elem.insDot;
5094
+ if (elem.delDot && compareDot(elem.delDot, best) > 0) best = elem.delDot;
5095
+ stack.push({
5096
+ node: elem.value,
5097
+ depth: frame.depth + 1
5098
+ });
5099
+ }
5100
+ break;
4123
5101
  }
4124
5102
  }
5103
+ return best;
4125
5104
  }
4126
- function mergeNode(a, b) {
4127
- return mergeNodeAtDepth(a, b, 0, []);
4128
- }
4129
- function mergeNodeAtDepth(a, b, depth, path) {
5105
+ function mergeNodeAtDepth(a, b, depth, path, config) {
5106
+ throwIfAborted(config.signal);
4130
5107
  assertTraversalDepth(depth);
4131
- if (a.kind === "lww" && b.kind === "lww") return mergeLww(a, b);
4132
- if (a.kind === "obj" && b.kind === "obj") return mergeObj(a, b, depth + 1, path);
4133
- if (a.kind === "seq" && b.kind === "seq") return mergeSeq(a, b, depth + 1, path);
4134
- if (compareDot(repDot(a), repDot(b)) >= 0) return cloneNodeShallow(a, depth + 1);
4135
- return cloneNodeShallow(b, depth + 1);
4136
- }
4137
- function mergeLww(a, b) {
5108
+ config.budgetMeter?.count("visitedNodes", 1, stringifyJsonPointer(path));
5109
+ if (a.kind === "lww" && b.kind === "lww") return mergeLww(a, b, config.actor);
5110
+ if (a.kind === "obj" && b.kind === "obj") return mergeObj(a, b, depth + 1, path, config);
5111
+ if (a.kind === "seq" && b.kind === "seq") return mergeSeq(a, b, depth + 1, path, config);
5112
+ if (compareDot(repDot(a, config.signal), repDot(b, config.signal)) >= 0) return cloneNodeShallow(a, depth + 1, config.actor, config.signal);
5113
+ return cloneNodeShallow(b, depth + 1, config.actor, config.signal);
5114
+ }
5115
+ function mergeLww(a, b, actor) {
4138
5116
  if (compareDot(a.dot, b.dot) >= 0) return {
4139
- kind: "lww",
4140
- value: structuredClone(a.value),
4141
- dot: { ...a.dot }
5117
+ node: {
5118
+ kind: "lww",
5119
+ value: structuredClone(a.value),
5120
+ dot: { ...a.dot }
5121
+ },
5122
+ maxObservedCtr: maxObservedCtrForDot(a.dot, actor)
4142
5123
  };
4143
5124
  return {
4144
- kind: "lww",
4145
- value: structuredClone(b.value),
4146
- dot: { ...b.dot }
5125
+ node: {
5126
+ kind: "lww",
5127
+ value: structuredClone(b.value),
5128
+ dot: { ...b.dot }
5129
+ },
5130
+ maxObservedCtr: maxObservedCtrForDot(b.dot, actor)
4147
5131
  };
4148
5132
  }
4149
- function mergeObj(a, b, depth, path) {
5133
+ function mergeObj(a, b, depth, path, config) {
4150
5134
  assertTraversalDepth(depth);
4151
5135
  const entries = /* @__PURE__ */ new Map();
4152
5136
  const tombstone = /* @__PURE__ */ new Map();
5137
+ let maxObservedCtr = 0;
4153
5138
  const allTombKeys = new Set([...a.tombstone.keys(), ...b.tombstone.keys()]);
5139
+ config.budgetMeter?.count("objectEntries", allTombKeys.size, stringifyJsonPointer(path));
4154
5140
  for (const key of allTombKeys) {
5141
+ throwIfAborted(config.signal);
4155
5142
  const da = a.tombstone.get(key);
4156
5143
  const db = b.tombstone.get(key);
4157
- if (da && db) tombstone.set(key, compareDot(da, db) >= 0 ? { ...da } : { ...db });
4158
- else if (da) tombstone.set(key, { ...da });
4159
- else tombstone.set(key, { ...db });
5144
+ if (da && db) {
5145
+ const mergedDot = compareDot(da, db) >= 0 ? { ...da } : { ...db };
5146
+ tombstone.set(key, mergedDot);
5147
+ maxObservedCtr = Math.max(maxObservedCtr, maxObservedCtrForDot(mergedDot, config.actor));
5148
+ } else if (da) {
5149
+ tombstone.set(key, { ...da });
5150
+ maxObservedCtr = Math.max(maxObservedCtr, maxObservedCtrForDot(da, config.actor));
5151
+ } else {
5152
+ tombstone.set(key, { ...db });
5153
+ maxObservedCtr = Math.max(maxObservedCtr, maxObservedCtrForDot(db, config.actor));
5154
+ }
4160
5155
  }
4161
5156
  const allKeys = new Set([...a.entries.keys(), ...b.entries.keys()]);
5157
+ config.budgetMeter?.count("objectEntries", allKeys.size, stringifyJsonPointer(path));
4162
5158
  for (const key of allKeys) {
5159
+ throwIfAborted(config.signal);
4163
5160
  const ea = a.entries.get(key);
4164
5161
  const eb = b.entries.get(key);
4165
5162
  let merged;
4166
- if (ea && eb) merged = {
4167
- node: mergeNodeAtDepth(ea.node, eb.node, depth + 1, [...path, key]),
4168
- dot: compareDot(ea.dot, eb.dot) >= 0 ? { ...ea.dot } : { ...eb.dot }
4169
- };
4170
- else if (ea) merged = {
4171
- node: cloneNodeShallow(ea.node, depth + 1),
4172
- dot: { ...ea.dot }
4173
- };
4174
- else merged = {
4175
- node: cloneNodeShallow(eb.node, depth + 1),
4176
- dot: { ...eb.dot }
4177
- };
5163
+ let mergedNodeMaxObservedCtr = 0;
5164
+ if (ea && eb) {
5165
+ path.push(key);
5166
+ const mergedNode = (() => {
5167
+ try {
5168
+ return mergeNodeAtDepth(ea.node, eb.node, depth + 1, path, config);
5169
+ } finally {
5170
+ path.pop();
5171
+ }
5172
+ })();
5173
+ const dot = compareDot(ea.dot, eb.dot) >= 0 ? { ...ea.dot } : { ...eb.dot };
5174
+ merged = {
5175
+ node: mergedNode.node,
5176
+ dot
5177
+ };
5178
+ mergedNodeMaxObservedCtr = mergedNode.maxObservedCtr;
5179
+ } else if (ea) {
5180
+ const cloned = cloneNodeShallow(ea.node, depth + 1, config.actor, config.signal);
5181
+ merged = {
5182
+ node: cloned.node,
5183
+ dot: { ...ea.dot }
5184
+ };
5185
+ mergedNodeMaxObservedCtr = cloned.maxObservedCtr;
5186
+ } else {
5187
+ const cloned = cloneNodeShallow(eb.node, depth + 1, config.actor, config.signal);
5188
+ merged = {
5189
+ node: cloned.node,
5190
+ dot: { ...eb.dot }
5191
+ };
5192
+ mergedNodeMaxObservedCtr = cloned.maxObservedCtr;
5193
+ }
4178
5194
  const td = tombstone.get(key);
4179
5195
  if (td && compareDot(td, merged.dot) >= 0) continue;
4180
5196
  entries.set(key, merged);
5197
+ maxObservedCtr = Math.max(maxObservedCtr, mergedNodeMaxObservedCtr, maxObservedCtrForDot(merged.dot, config.actor));
4181
5198
  }
4182
5199
  return {
4183
- kind: "obj",
4184
- entries,
4185
- tombstone
5200
+ node: {
5201
+ kind: "obj",
5202
+ entries,
5203
+ tombstone
5204
+ },
5205
+ maxObservedCtr
4186
5206
  };
4187
5207
  }
4188
- function mergeSeq(a, b, depth, path) {
5208
+ function mergeSeq(a, b, depth, path, config) {
4189
5209
  assertTraversalDepth(depth);
5210
+ config.budgetMeter?.count("visitedNodes", 1, stringifyJsonPointer(path));
5211
+ if (config.unrelatedArrays === "atomic-replace" && a.elems.size > 0 && b.elems.size > 0) {
5212
+ let shared = false;
5213
+ for (const id of a.elems.keys()) {
5214
+ throwIfAborted(config.signal);
5215
+ if (b.elems.has(id)) {
5216
+ shared = true;
5217
+ break;
5218
+ }
5219
+ }
5220
+ if (!shared) {
5221
+ config.budgetMeter?.count("sequenceElements", a.elems.size + b.elems.size, stringifyJsonPointer(path));
5222
+ return cloneNodeShallow(compareDot(repDot(a, config.signal), repDot(b, config.signal)) >= 0 ? a : b, depth, config.actor, config.signal);
5223
+ }
5224
+ }
4190
5225
  const elems = /* @__PURE__ */ new Map();
5226
+ let maxObservedCtr = 0;
4191
5227
  const allIds = new Set([...a.elems.keys(), ...b.elems.keys()]);
5228
+ config.budgetMeter?.count("sequenceElements", allIds.size, stringifyJsonPointer(path));
4192
5229
  for (const id of allIds) {
5230
+ throwIfAborted(config.signal);
4193
5231
  const ea = a.elems.get(id);
4194
5232
  const eb = b.elems.get(id);
4195
5233
  if (ea && eb) {
4196
5234
  if (ea.prev !== eb.prev) throw new SharedElementMetadataMismatchError(stringifyJsonPointer(path), id, "prev");
4197
5235
  if (!sameDot(ea.insDot, eb.insDot)) throw new SharedElementMetadataMismatchError(stringifyJsonPointer(path), id, "insDot");
4198
- const mergedValue = mergeNodeAtDepth(ea.value, eb.value, depth + 1, [...path, id]);
5236
+ path.push(id);
5237
+ const mergedValue = (() => {
5238
+ try {
5239
+ return mergeNodeAtDepth(ea.value, eb.value, depth + 1, path, config);
5240
+ } finally {
5241
+ path.pop();
5242
+ }
5243
+ })();
5244
+ const mergedDeleteDot = mergeDeleteDot(ea.delDot, eb.delDot);
4199
5245
  elems.set(id, {
4200
5246
  id,
4201
5247
  prev: ea.prev,
4202
5248
  tombstone: ea.tombstone || eb.tombstone,
4203
- delDot: mergeDeleteDot(ea.delDot, eb.delDot),
4204
- value: mergedValue,
5249
+ delDot: mergedDeleteDot,
5250
+ value: mergedValue.node,
4205
5251
  insDot: { ...ea.insDot }
4206
5252
  });
4207
- } else if (ea) elems.set(id, cloneElem(ea, depth + 1));
4208
- else elems.set(id, cloneElem(eb, depth + 1));
5253
+ maxObservedCtr = Math.max(maxObservedCtr, mergedValue.maxObservedCtr, maxObservedCtrForDot(ea.insDot, config.actor), maxObservedCtrForDot(mergedDeleteDot, config.actor));
5254
+ } else if (ea) {
5255
+ const cloned = cloneElem(ea, depth + 1, config.actor, config.signal);
5256
+ elems.set(id, cloned.elem);
5257
+ maxObservedCtr = Math.max(maxObservedCtr, cloned.maxObservedCtr);
5258
+ } else {
5259
+ const cloned = cloneElem(eb, depth + 1, config.actor, config.signal);
5260
+ elems.set(id, cloned.elem);
5261
+ maxObservedCtr = Math.max(maxObservedCtr, cloned.maxObservedCtr);
5262
+ }
4209
5263
  }
4210
5264
  return {
4211
- kind: "seq",
4212
- elems
5265
+ node: {
5266
+ kind: "seq",
5267
+ elems
5268
+ },
5269
+ maxObservedCtr
4213
5270
  };
4214
5271
  }
4215
5272
  function sameDot(a, b) {
4216
5273
  return a.actor === b.actor && a.ctr === b.ctr;
4217
5274
  }
4218
- function cloneElem(e, depth) {
5275
+ function cloneElem(e, depth, actor, signal) {
5276
+ throwIfAborted(signal);
4219
5277
  assertTraversalDepth(depth);
5278
+ const value = cloneNodeShallow(e.value, depth + 1, actor, signal);
4220
5279
  return {
4221
- id: e.id,
4222
- prev: e.prev,
4223
- tombstone: e.tombstone,
4224
- delDot: e.delDot ? { ...e.delDot } : void 0,
4225
- value: cloneNodeShallow(e.value, depth + 1),
4226
- insDot: { ...e.insDot }
5280
+ elem: {
5281
+ id: e.id,
5282
+ prev: e.prev,
5283
+ tombstone: e.tombstone,
5284
+ delDot: e.delDot ? { ...e.delDot } : void 0,
5285
+ value: value.node,
5286
+ insDot: { ...e.insDot }
5287
+ },
5288
+ maxObservedCtr: Math.max(value.maxObservedCtr, maxObservedCtrForDot(e.insDot, actor), maxObservedCtrForDot(e.delDot, actor))
4227
5289
  };
4228
5290
  }
4229
5291
  function mergeDeleteDot(a, b) {
@@ -4231,38 +5293,68 @@ function mergeDeleteDot(a, b) {
4231
5293
  if (a) return { ...a };
4232
5294
  if (b) return { ...b };
4233
5295
  }
4234
- function cloneNodeShallow(node, depth) {
5296
+ function cloneNodeShallow(node, depth, actor, signal) {
5297
+ throwIfAborted(signal);
4235
5298
  assertTraversalDepth(depth);
4236
5299
  switch (node.kind) {
4237
5300
  case "lww": return {
4238
- kind: "lww",
4239
- value: structuredClone(node.value),
4240
- dot: { ...node.dot }
5301
+ node: {
5302
+ kind: "lww",
5303
+ value: structuredClone(node.value),
5304
+ dot: { ...node.dot }
5305
+ },
5306
+ maxObservedCtr: maxObservedCtrForDot(node.dot, actor)
4241
5307
  };
4242
5308
  case "obj": {
4243
5309
  const entries = /* @__PURE__ */ new Map();
4244
- for (const [k, v] of node.entries) entries.set(k, {
4245
- node: cloneNodeShallow(v.node, depth + 1),
4246
- dot: { ...v.dot }
4247
- });
5310
+ let maxObservedCtr = 0;
5311
+ for (const [k, v] of node.entries) {
5312
+ throwIfAborted(signal);
5313
+ const cloned = cloneNodeShallow(v.node, depth + 1, actor, signal);
5314
+ entries.set(k, {
5315
+ node: cloned.node,
5316
+ dot: { ...v.dot }
5317
+ });
5318
+ maxObservedCtr = Math.max(maxObservedCtr, cloned.maxObservedCtr, maxObservedCtrForDot(v.dot, actor));
5319
+ }
4248
5320
  const tombstone = /* @__PURE__ */ new Map();
4249
- for (const [k, d] of node.tombstone) tombstone.set(k, { ...d });
5321
+ for (const [k, d] of node.tombstone) {
5322
+ throwIfAborted(signal);
5323
+ tombstone.set(k, { ...d });
5324
+ maxObservedCtr = Math.max(maxObservedCtr, maxObservedCtrForDot(d, actor));
5325
+ }
4250
5326
  return {
4251
- kind: "obj",
4252
- entries,
4253
- tombstone
5327
+ node: {
5328
+ kind: "obj",
5329
+ entries,
5330
+ tombstone
5331
+ },
5332
+ maxObservedCtr
4254
5333
  };
4255
5334
  }
4256
5335
  case "seq": {
4257
5336
  const elems = /* @__PURE__ */ new Map();
4258
- for (const [id, e] of node.elems) elems.set(id, cloneElem(e, depth + 1));
5337
+ let maxObservedCtr = 0;
5338
+ for (const [id, e] of node.elems) {
5339
+ throwIfAborted(signal);
5340
+ const cloned = cloneElem(e, depth + 1, actor, signal);
5341
+ elems.set(id, cloned.elem);
5342
+ maxObservedCtr = Math.max(maxObservedCtr, cloned.maxObservedCtr);
5343
+ }
4259
5344
  return {
4260
- kind: "seq",
4261
- elems
5345
+ node: {
5346
+ kind: "seq",
5347
+ elems
5348
+ },
5349
+ maxObservedCtr
4262
5350
  };
4263
5351
  }
4264
5352
  }
4265
5353
  }
5354
+ function maxObservedCtrForDot(dot, actor) {
5355
+ if (!dot || !actor || dot.actor !== actor) return 0;
5356
+ return dot.ctr;
5357
+ }
4266
5358
 
4267
5359
  //#endregion
4268
5360
  //#region src/compact.ts
@@ -4344,4 +5436,4 @@ function compactStateTombstones(state, options) {
4344
5436
  }
4345
5437
 
4346
5438
  //#endregion
4347
- export { materialize as $, crdtToJsonPatch as A, jsonEquals as B, tryApplyPatchAsActor as C, TraversalDepthError as Ct, cloneDoc as D, applyIntentsToCrdt as E, tryJsonPatchToCrdt as F, JsonValueValidationError as G, stableJsonValueKey as H, PatchCompileError as I, newReg as J, lwwSet as K, compileJsonPatchToIntent as L, docFromJsonWithDot as M, jsonPatchToCrdt as N, crdtNodesToJsonPatch as O, jsonPatchToCrdtSafe as P, objSet as Q, diffJsonPatch as R, tryApplyPatch as S, MAX_TRAVERSAL_DEPTH as St, validateJsonPatch as T, stringifyJsonPointer as U, parseJsonPointer as V, ROOT_KEY as W, objCompactTombstones as X, newSeq as Y, objRemove as Z, applyPatchAsActor as _, observeDot as _t, mergeState as a, rgaInsertAfterChecked as at, forkState as b, observedVersionVector as bt, DeserializeError as c, validateRgaSeq as ct, serializeDoc as d, vvHasDot as dt, HEAD as et, serializeState as f, vvMerge as ft, applyPatch as g, nextDotForActor as gt, PatchError as h, createClock as ht, mergeDoc as i, rgaInsertAfter as it, docFromJson as j, crdtToFullReplace as k, deserializeDoc as l, compareDot as lt, tryDeserializeState as m, cloneClock as mt, compactStateTombstones as n, rgaDelete as nt, tryMergeDoc as o, rgaLinearizeIds as ot, tryDeserializeDoc as p, ClockValidationError as pt, newObj as q, MergeError as r, rgaIdAtIndex as rt, tryMergeState as s, rgaPrevForInsertAtIndex as st, compactDocTombstones as t, rgaCompactTombstones as tt, deserializeState as u, dotToElemId as ut, applyPatchInPlace as v, intersectVersionVectors as vt, tryApplyPatchInPlace as w, toJson as x, versionVectorCovers as xt, createState as y, mergeVersionVectors as yt, getAtJson as z };
5439
+ export { stableJsonValueKey as $, createState as A, mergeVersionVectors as At, crdtToFullReplace as B, createSafeState as C, vvMerge as Ct, applyPatch as D, nextDotForActor as Dt, PatchError as E, createClock as Et, tryApplyPatchInPlace as F, OperationCancelledError as Ft, jsonPatchToCrdtSafe as G, docFromJson as H, validateJsonPatch as I, ResourceBudgetError as It, compileJsonPatchToIntent as J, tryJsonPatchToCrdt as K, applyIntentsToCrdt as L, toJson as M, versionVectorCovers as Mt, tryApplyPatch as N, MAX_TRAVERSAL_DEPTH as Nt, applyPatchAsActor as O, observeDot as Ot, tryApplyPatchAsActor as P, TraversalDepthError as Pt, parseJsonPointer as Q, cloneDoc as R, createNormalizedState as S, vvHasDot as St, diffSafeJsonPatch as T, cloneClock as Tt, docFromJsonWithDot as U, crdtToJsonPatch as V, jsonPatchToCrdt as W, getAtJson as X, diffJsonPatch as Y, jsonEquals as Z, strictRfc6902PatchOptions as _, rgaLinearizeIds as _t, mergeState as a, newReg as at, applyNormalizedPatch as b, compareDot as bt, DeserializeError as c, objRemove as ct, serializeDoc as d, HEAD as dt, stringifyJsonPointer as et, serializeState as f, rgaCompactTombstones as ft, validateSerializedState as g, rgaInsertAfterChecked as gt, validateSerializedDoc as h, rgaInsertAfter as ht, mergeDoc as i, newObj as it, forkState as j, observedVersionVector as jt, applyPatchInPlace as k, intersectVersionVectors as kt, deserializeDoc as l, objSet as lt, tryDeserializeState as m, rgaIdAtIndex as mt, compactStateTombstones as n, JsonValueValidationError as nt, tryMergeDoc as o, newSeq as ot, tryDeserializeDoc as p, rgaDelete as pt, PatchCompileError as q, MergeError as r, lwwSet as rt, tryMergeState as s, objCompactTombstones as st, compactDocTombstones as t, ROOT_KEY as tt, deserializeState as u, materialize as ut, withLegacyMissingArrayParents as v, rgaPrevForInsertAtIndex as vt, diffNormalizedJsonPatch as w, ClockValidationError as wt, applySafePatch as x, dotToElemId as xt, withStrictRfc6902Parents as y, validateRgaSeq as yt, crdtNodesToJsonPatch as z };