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