json-patch-to-crdt 0.5.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,129 @@
1
+ //#region src/budget.ts
2
+ function isNonNegativeSafeInteger(value) {
3
+ return Number.isSafeInteger(value) && value >= 0;
4
+ }
5
+ function formatPath(path) {
6
+ if (path === void 0 || path === "") return "";
7
+ return ` at ${path}`;
8
+ }
9
+ function formatOpIndex(opIndex) {
10
+ if (opIndex === void 0) return "";
11
+ return ` at op ${opIndex}`;
12
+ }
13
+ var ResourceBudgetError = class extends Error {
14
+ reason = "RESOURCE_BUDGET_EXCEEDED";
15
+ code = 409;
16
+ budget;
17
+ limit;
18
+ actual;
19
+ path;
20
+ opIndex;
21
+ constructor(budget, limit, actual, path, opIndex) {
22
+ super(`resource budget '${budget}' exceeded${formatPath(path)}${formatOpIndex(opIndex)}: ${actual} > ${limit}`);
23
+ this.name = "ResourceBudgetError";
24
+ this.budget = budget;
25
+ this.limit = limit;
26
+ this.actual = actual;
27
+ this.path = path;
28
+ this.opIndex = opIndex;
29
+ }
30
+ };
31
+ var ResourceBudgetMeter = class {
32
+ #budget;
33
+ #counts;
34
+ constructor(budget) {
35
+ this.#budget = validateBudget(budget);
36
+ this.#counts = {
37
+ patchOperations: 0,
38
+ objectEntries: 0,
39
+ sequenceElements: 0,
40
+ visitedNodes: 0,
41
+ serializedElements: 0,
42
+ arrayDiffCells: 0
43
+ };
44
+ }
45
+ count(kind, delta, path, opIndex) {
46
+ if (delta <= 0) return;
47
+ this.#counts[kind] += delta;
48
+ const limit = this.#budget[kind];
49
+ if (limit !== void 0 && this.#counts[kind] > limit) throw new ResourceBudgetError(kind, limit, this.#counts[kind], path, opIndex);
50
+ }
51
+ };
52
+ function createBudgetMeter(budget) {
53
+ if (budget === void 0) return;
54
+ return new ResourceBudgetMeter(budget);
55
+ }
56
+ function toBudgetApplyError(error) {
57
+ return {
58
+ ok: false,
59
+ code: error.code,
60
+ reason: error.reason,
61
+ message: error.message,
62
+ budget: error.budget,
63
+ limit: error.limit,
64
+ actual: error.actual,
65
+ path: error.path,
66
+ opIndex: error.opIndex
67
+ };
68
+ }
69
+ function toBudgetDeserializeFailure(error) {
70
+ return {
71
+ code: error.code,
72
+ reason: error.reason,
73
+ message: error.message,
74
+ budget: error.budget,
75
+ limit: error.limit,
76
+ actual: error.actual,
77
+ path: error.path
78
+ };
79
+ }
80
+ function validateBudget(budget) {
81
+ if (budget === void 0) return {};
82
+ const normalized = {};
83
+ const entries = Object.entries(budget);
84
+ for (const [key, value] of entries) {
85
+ if (value === void 0) continue;
86
+ if (!isNonNegativeSafeInteger(value)) throw new Error(`resource budget '${key}' must be a non-negative safe integer`);
87
+ normalized[key] = value;
88
+ }
89
+ return normalized;
90
+ }
91
+
92
+ //#endregion
93
+ //#region src/cancellation.ts
94
+ var OperationCancelledError = class extends Error {
95
+ reasonValue;
96
+ constructor(reason) {
97
+ super(toCancellationMessage(reason));
98
+ this.name = "OperationCancelledError";
99
+ this.reasonValue = reason;
100
+ }
101
+ };
102
+ function throwIfAborted(signal) {
103
+ if (signal?.aborted) throw new OperationCancelledError(signal.reason);
104
+ }
105
+ function toCancellationApplyError(error) {
106
+ return {
107
+ ok: false,
108
+ code: 409,
109
+ reason: "OPERATION_CANCELLED",
110
+ message: error.message
111
+ };
112
+ }
113
+ function toCancellationDeserializeFailure(error) {
114
+ return {
115
+ code: 409,
116
+ reason: "OPERATION_CANCELLED",
117
+ message: error.message
118
+ };
119
+ }
120
+ function toCancellationMessage(reason) {
121
+ if (reason instanceof Error && reason.message.length > 0) return `operation cancelled: ${reason.message}`;
122
+ if (typeof reason === "string" && reason.length > 0) return `operation cancelled: ${reason}`;
123
+ return "operation cancelled";
124
+ }
125
+
126
+ //#endregion
1
127
  //#region src/depth.ts
2
128
  const MAX_TRAVERSAL_DEPTH = 16384;
3
129
  var TraversalDepthError = class extends Error {
@@ -27,6 +153,7 @@ function toDepthApplyError(error) {
27
153
  //#endregion
28
154
  //#region src/version-vector.ts
29
155
  let observedVersionVectorObserverForTests = null;
156
+ const observedVersionVectorCache = /* @__PURE__ */ new WeakMap();
30
157
  function readVersionVectorCounter(vv, actor) {
31
158
  if (!Object.prototype.hasOwnProperty.call(vv, actor)) return;
32
159
  const counter = vv[actor];
@@ -43,6 +170,21 @@ function writeVersionVectorCounter(vv, actor, counter) {
43
170
  function observeVersionVectorDot(vv, dot) {
44
171
  if ((readVersionVectorCounter(vv, dot.actor) ?? 0) < dot.ctr) writeVersionVectorCounter(vv, dot.actor, dot.ctr);
45
172
  }
173
+ function cloneVersionVector(vv) {
174
+ return mergeVersionVectors(vv);
175
+ }
176
+ function readCachedObservedVersionVector(doc) {
177
+ const cached = observedVersionVectorCache.get(doc);
178
+ return cached ? cloneVersionVector(cached) : void 0;
179
+ }
180
+ function writeCachedObservedVersionVector(doc, vv) {
181
+ observedVersionVectorCache.set(doc, cloneVersionVector(vv));
182
+ }
183
+ function observeDocVersionVectorDot(doc, dot) {
184
+ const cached = observedVersionVectorCache.get(doc);
185
+ if (!cached) return;
186
+ observeVersionVectorDot(cached, dot);
187
+ }
46
188
  /**
47
189
  * Inspect a document or state and return the highest observed counter per actor.
48
190
  *
@@ -51,13 +193,15 @@ function observeVersionVectorDot(vv, dot) {
51
193
  * of the currently materialized document tree.
52
194
  */
53
195
  function observedVersionVector(target) {
54
- observedVersionVectorObserverForTests?.(target);
55
196
  const doc = "doc" in target ? target.doc : target;
56
- const vv = Object.create(null);
197
+ const cached = readCachedObservedVersionVector(doc);
198
+ const vv = cached ?? Object.create(null);
57
199
  if ("clock" in target) observeVersionVectorDot(vv, {
58
200
  actor: target.clock.actor,
59
201
  ctr: target.clock.ctr
60
202
  });
203
+ if (cached) return vv;
204
+ observedVersionVectorObserverForTests?.(target);
61
205
  const stack = [{
62
206
  node: doc.root,
63
207
  depth: 0
@@ -89,6 +233,7 @@ function observedVersionVector(target) {
89
233
  });
90
234
  }
91
235
  }
236
+ writeCachedObservedVersionVector(doc, vv);
92
237
  return vv;
93
238
  }
94
239
  /** Combine version vectors using per-actor maxima. */
@@ -932,11 +1077,13 @@ function getAtJson(base, path) {
932
1077
  */
933
1078
  function compileJsonPatchToIntent(baseJson, patch, options = {}) {
934
1079
  const internalOptions = options;
1080
+ const budgetMeter = internalOptions.budgetMeter ?? createBudgetMeter(options.resourceBudget);
935
1081
  const semantics = options.semantics ?? "sequential";
936
1082
  const opIndexOffset = internalOptions.opIndexOffset ?? 0;
937
1083
  let workingBase = baseJson;
938
1084
  const pointerCache = internalOptions.pointerCache ?? /* @__PURE__ */ new Map();
939
1085
  const intents = [];
1086
+ budgetMeter?.count("patchOperations", patch.length);
940
1087
  for (let opIndex = 0; opIndex < patch.length; opIndex++) {
941
1088
  const op = patch[opIndex];
942
1089
  const absoluteOpIndex = opIndex + opIndexOffset;
@@ -951,14 +1098,16 @@ function compileJsonPatchOpToIntent(baseJson, op, options = {}) {
951
1098
  const internalOptions = options;
952
1099
  const semantics = options.semantics ?? "sequential";
953
1100
  const pointerCache = internalOptions.pointerCache ?? /* @__PURE__ */ new Map();
954
- return compileSingleOp(baseJson, op, internalOptions.opIndexOffset ?? 0, semantics, pointerCache);
1101
+ const opIndex = internalOptions.opIndexOffset ?? 0;
1102
+ (internalOptions.budgetMeter ?? createBudgetMeter(options.resourceBudget))?.count("patchOperations", 1, void 0, opIndex);
1103
+ return compileSingleOp(baseJson, op, opIndex, semantics, pointerCache);
955
1104
  }
956
1105
  /**
957
1106
  * Compute a JSON Patch delta between two JSON values.
958
1107
  * By default arrays use a deterministic LCS strategy.
959
1108
  * Pass `{ arrayStrategy: "atomic" }` for single-op array replacement.
960
1109
  * Pass `{ arrayStrategy: "lcs-linear" }` for a lower-memory LCS variant.
961
- * Use `lcsLinearMaxCells` to optionally cap worst-case `lcs-linear` work and
1110
+ * Use `lcsLinearMaxCells` to cap worst-case `lcs-linear` work and
962
1111
  * fall back to an atomic array replacement for very large unmatched windows.
963
1112
  * Pass `{ emitMoves: true }` or `{ emitCopies: true }` to opt into RFC 6902
964
1113
  * move/copy emission when a deterministic rewrite is available.
@@ -968,20 +1117,24 @@ function compileJsonPatchOpToIntent(baseJson, op, options = {}) {
968
1117
  * @returns An array of JSON Patch operations that transform `base` into `next`.
969
1118
  */
970
1119
  function diffJsonPatch(base, next, options = {}) {
1120
+ const budgetMeter = createBudgetMeter(options.resourceBudget);
971
1121
  const runtimeMode = options.jsonValidation ?? "none";
972
1122
  const runtimeBase = coerceRuntimeJsonValue(base, runtimeMode);
973
1123
  const runtimeNext = coerceRuntimeJsonValue(next, runtimeMode);
974
1124
  const ops = [];
975
- diffValue([], runtimeBase, runtimeNext, ops, options);
1125
+ const path = [];
1126
+ throwIfAborted(options.signal);
1127
+ diffValue(path, runtimeBase, runtimeNext, ops, options, budgetMeter);
976
1128
  return ops;
977
1129
  }
978
- function diffValue(path, base, next, ops, options) {
1130
+ function diffValue(path, base, next, ops, options, budgetMeter) {
979
1131
  const stack = [{
980
1132
  kind: "value",
981
1133
  base,
982
1134
  next
983
1135
  }];
984
1136
  while (stack.length > 0) {
1137
+ throwIfAborted(options.signal);
985
1138
  const frame = stack.pop();
986
1139
  if (frame.kind === "path-pop") {
987
1140
  path.pop();
@@ -1007,6 +1160,7 @@ function diffValue(path, base, next, ops, options) {
1007
1160
  continue;
1008
1161
  }
1009
1162
  assertTraversalDepth(path.length);
1163
+ budgetMeter?.count("visitedNodes", 1, stringifyJsonPointer(path));
1010
1164
  if (frame.base === frame.next) continue;
1011
1165
  const baseIsArray = Array.isArray(frame.base);
1012
1166
  const nextIsArray = Array.isArray(frame.next);
@@ -1022,7 +1176,7 @@ function diffValue(path, base, next, ops, options) {
1022
1176
  if (jsonEquals(frame.base, frame.next)) continue;
1023
1177
  const arrayStrategy = options.arrayStrategy ?? "lcs";
1024
1178
  if (arrayStrategy === "lcs") {
1025
- if (!diffArrayWithLcsMatrix(path, frame.base, frame.next, ops, options)) ops.push({
1179
+ if (!diffArrayWithLcsMatrix(path, frame.base, frame.next, ops, options, budgetMeter)) ops.push({
1026
1180
  op: "replace",
1027
1181
  path: stringifyJsonPointer(path),
1028
1182
  value: frame.next
@@ -1030,7 +1184,7 @@ function diffValue(path, base, next, ops, options) {
1030
1184
  continue;
1031
1185
  }
1032
1186
  if (arrayStrategy === "lcs-linear") {
1033
- if (!diffArrayWithLinearLcs(path, frame.base, frame.next, ops, options)) ops.push({
1187
+ if (!diffArrayWithLinearLcs(path, frame.base, frame.next, ops, options, budgetMeter)) ops.push({
1034
1188
  op: "replace",
1035
1189
  path: stringifyJsonPointer(path),
1036
1190
  value: frame.next
@@ -1055,6 +1209,7 @@ function diffValue(path, base, next, ops, options) {
1055
1209
  continue;
1056
1210
  }
1057
1211
  const { sharedKeys, baseOnlyKeys, nextOnlyKeys } = collectObjectKeys(frame.base, frame.next);
1212
+ budgetMeter?.count("objectEntries", sharedKeys.length + baseOnlyKeys.length + nextOnlyKeys.length, stringifyJsonPointer(path));
1058
1213
  if (!(baseOnlyKeys.length > 0 || nextOnlyKeys.length > 0) && (path.length === 0 || sharedKeys.length > 1) && jsonEquals(frame.base, frame.next)) continue;
1059
1214
  emitObjectStructuralOps(path, frame.base, frame.next, sharedKeys, baseOnlyKeys, nextOnlyKeys, ops, options);
1060
1215
  if (sharedKeys.length > 0) stack.push({
@@ -1218,34 +1373,44 @@ function insertSortedKey(keys, key) {
1218
1373
  }
1219
1374
  keys.splice(low, 0, key);
1220
1375
  }
1221
- function diffArrayWithLcsMatrix(path, base, next, ops, options) {
1222
- const window = trimEqualArrayEdges(base, next);
1376
+ function diffArrayWithLcsMatrix(path, base, next, ops, options, budgetMeter) {
1377
+ const window = trimEqualArrayEdges(base, next, options);
1223
1378
  const baseStart = window.baseStart;
1224
1379
  const nextStart = window.nextStart;
1225
1380
  const n = window.unmatchedBaseLength;
1226
1381
  const m = window.unmatchedNextLength;
1382
+ budgetMeter?.count("sequenceElements", n + m, stringifyJsonPointer(path));
1227
1383
  if (!shouldUseLcsDiff(n, m, options.lcsMaxCells)) return false;
1384
+ budgetMeter?.count("arrayDiffCells", (n + 1) * (m + 1), stringifyJsonPointer(path));
1228
1385
  if (n === 0 && m === 0) return true;
1229
1386
  const steps = [];
1230
- buildArrayEditScriptWithMatrix(base, baseStart, baseStart + n, next, nextStart, nextStart + m, steps);
1387
+ buildArrayEditScriptWithMatrix(base, baseStart, baseStart + n, next, nextStart, nextStart + m, steps, options);
1231
1388
  pushArrayPatchOps(path, window.prefixLength, steps, ops, base, options);
1232
1389
  return true;
1233
1390
  }
1234
- function diffArrayWithLinearLcs(path, base, next, ops, options) {
1235
- const window = trimEqualArrayEdges(base, next);
1391
+ function diffArrayWithLinearLcs(path, base, next, ops, options, budgetMeter) {
1392
+ const window = trimEqualArrayEdges(base, next, options);
1393
+ budgetMeter?.count("sequenceElements", window.unmatchedBaseLength + window.unmatchedNextLength, stringifyJsonPointer(path));
1236
1394
  if (!shouldUseLinearLcsDiff(window.unmatchedBaseLength, window.unmatchedNextLength, options)) return false;
1395
+ budgetMeter?.count("arrayDiffCells", (window.unmatchedBaseLength + 1) * (window.unmatchedNextLength + 1), stringifyJsonPointer(path));
1237
1396
  const steps = [];
1238
- buildArrayEditScriptLinearSpace(base, window.baseStart, window.baseStart + window.unmatchedBaseLength, next, window.nextStart, window.nextStart + window.unmatchedNextLength, steps);
1397
+ buildArrayEditScriptLinearSpace(base, window.baseStart, window.baseStart + window.unmatchedBaseLength, next, window.nextStart, window.nextStart + window.unmatchedNextLength, steps, options);
1239
1398
  pushArrayPatchOps(path, window.prefixLength, steps, ops, base, options);
1240
1399
  return true;
1241
1400
  }
1242
- function trimEqualArrayEdges(base, next) {
1401
+ function trimEqualArrayEdges(base, next, options) {
1243
1402
  const baseLength = base.length;
1244
1403
  const nextLength = next.length;
1245
1404
  let prefixLength = 0;
1246
- while (prefixLength < baseLength && prefixLength < nextLength && jsonEquals(base[prefixLength], next[prefixLength])) prefixLength += 1;
1405
+ while (prefixLength < baseLength && prefixLength < nextLength && jsonEquals(base[prefixLength], next[prefixLength])) {
1406
+ throwIfAborted(options.signal);
1407
+ prefixLength += 1;
1408
+ }
1247
1409
  let suffixLength = 0;
1248
- while (suffixLength < baseLength - prefixLength && suffixLength < nextLength - prefixLength && jsonEquals(base[baseLength - 1 - suffixLength], next[nextLength - 1 - suffixLength])) suffixLength += 1;
1410
+ while (suffixLength < baseLength - prefixLength && suffixLength < nextLength - prefixLength && jsonEquals(base[baseLength - 1 - suffixLength], next[nextLength - 1 - suffixLength])) {
1411
+ throwIfAborted(options.signal);
1412
+ suffixLength += 1;
1413
+ }
1249
1414
  return {
1250
1415
  baseStart: prefixLength,
1251
1416
  nextStart: prefixLength,
@@ -1254,7 +1419,8 @@ function trimEqualArrayEdges(base, next) {
1254
1419
  unmatchedNextLength: nextLength - prefixLength - suffixLength
1255
1420
  };
1256
1421
  }
1257
- function buildArrayEditScriptLinearSpace(base, baseStart, baseEnd, next, nextStart, nextEnd, steps) {
1422
+ function buildArrayEditScriptLinearSpace(base, baseStart, baseEnd, next, nextStart, nextEnd, steps, options) {
1423
+ throwIfAborted(options.signal);
1258
1424
  const unmatchedBaseLength = baseEnd - baseStart;
1259
1425
  const unmatchedNextLength = nextEnd - nextStart;
1260
1426
  if (unmatchedBaseLength === 0) {
@@ -1269,20 +1435,20 @@ function buildArrayEditScriptLinearSpace(base, baseStart, baseEnd, next, nextSta
1269
1435
  return;
1270
1436
  }
1271
1437
  if (unmatchedBaseLength === 1) {
1272
- pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, steps);
1438
+ pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, steps, options);
1273
1439
  return;
1274
1440
  }
1275
1441
  if (unmatchedNextLength === 1) {
1276
- pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, steps);
1442
+ pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, steps, options);
1277
1443
  return;
1278
1444
  }
1279
1445
  if (shouldUseMatrixBaseCase(unmatchedBaseLength, unmatchedNextLength)) {
1280
- buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStart, nextEnd, steps);
1446
+ buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStart, nextEnd, steps, options);
1281
1447
  return;
1282
1448
  }
1283
1449
  const baseMid = baseStart + Math.floor(unmatchedBaseLength / 2);
1284
- const forwardScores = computeLcsPrefixLengths(base, baseStart, baseMid, next, nextStart, nextEnd);
1285
- const reverseScores = computeLcsSuffixLengths(base, baseMid, baseEnd, next, nextStart, nextEnd);
1450
+ const forwardScores = computeLcsPrefixLengths(base, baseStart, baseMid, next, nextStart, nextEnd, options);
1451
+ const reverseScores = computeLcsSuffixLengths(base, baseMid, baseEnd, next, nextStart, nextEnd, options);
1286
1452
  let bestOffset = 0;
1287
1453
  let bestScore = Number.NEGATIVE_INFINITY;
1288
1454
  for (let offset = 0; offset <= unmatchedNextLength; offset++) {
@@ -1293,11 +1459,11 @@ function buildArrayEditScriptLinearSpace(base, baseStart, baseEnd, next, nextSta
1293
1459
  }
1294
1460
  }
1295
1461
  const nextMid = nextStart + bestOffset;
1296
- buildArrayEditScriptLinearSpace(base, baseStart, baseMid, next, nextStart, nextMid, steps);
1297
- buildArrayEditScriptLinearSpace(base, baseMid, baseEnd, next, nextMid, nextEnd, steps);
1462
+ buildArrayEditScriptLinearSpace(base, baseStart, baseMid, next, nextStart, nextMid, steps, options);
1463
+ buildArrayEditScriptLinearSpace(base, baseMid, baseEnd, next, nextMid, nextEnd, steps, options);
1298
1464
  }
1299
- function pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, steps) {
1300
- const matchIndex = findFirstMatchingIndexInNext(base[baseStart], next, nextStart, nextEnd);
1465
+ function pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, steps, options) {
1466
+ const matchIndex = findFirstMatchingIndexInNext(base[baseStart], next, nextStart, nextEnd, options);
1301
1467
  if (matchIndex === -1) {
1302
1468
  steps.push({ kind: "remove" });
1303
1469
  for (let nextIndex = nextStart; nextIndex < nextEnd; nextIndex++) steps.push({
@@ -1316,8 +1482,8 @@ function pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, s
1316
1482
  value: next[nextIndex]
1317
1483
  });
1318
1484
  }
1319
- function pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, steps) {
1320
- const matchIndex = findFirstMatchingIndexInBase(next[nextStart], base, baseStart, baseEnd);
1485
+ function pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, steps, options) {
1486
+ const matchIndex = findFirstMatchingIndexInBase(next[nextStart], base, baseStart, baseEnd, options);
1321
1487
  if (matchIndex === -1) {
1322
1488
  for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) steps.push({ kind: "remove" });
1323
1489
  steps.push({
@@ -1330,26 +1496,36 @@ function pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, s
1330
1496
  steps.push({ kind: "equal" });
1331
1497
  for (let baseIndex = matchIndex + 1; baseIndex < baseEnd; baseIndex++) steps.push({ kind: "remove" });
1332
1498
  }
1333
- function findFirstMatchingIndexInNext(target, next, nextStart, nextEnd) {
1334
- for (let nextIndex = nextStart; nextIndex < nextEnd; nextIndex++) if (jsonEquals(target, next[nextIndex])) return nextIndex;
1499
+ function findFirstMatchingIndexInNext(target, next, nextStart, nextEnd, options) {
1500
+ for (let nextIndex = nextStart; nextIndex < nextEnd; nextIndex++) {
1501
+ throwIfAborted(options.signal);
1502
+ if (jsonEquals(target, next[nextIndex])) return nextIndex;
1503
+ }
1335
1504
  return -1;
1336
1505
  }
1337
- function findFirstMatchingIndexInBase(target, base, baseStart, baseEnd) {
1338
- for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) if (jsonEquals(target, base[baseIndex])) return baseIndex;
1506
+ function findFirstMatchingIndexInBase(target, base, baseStart, baseEnd, options) {
1507
+ for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) {
1508
+ throwIfAborted(options.signal);
1509
+ if (jsonEquals(target, base[baseIndex])) return baseIndex;
1510
+ }
1339
1511
  return -1;
1340
1512
  }
1341
1513
  function shouldUseMatrixBaseCase(baseLength, nextLength) {
1342
1514
  return (baseLength + 1) * (nextLength + 1) <= LINEAR_LCS_MATRIX_BASE_CASE_MAX_CELLS;
1343
1515
  }
1344
- function buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStart, nextEnd, steps) {
1516
+ function buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStart, nextEnd, steps, options) {
1345
1517
  const unmatchedBaseLength = baseEnd - baseStart;
1346
1518
  const unmatchedNextLength = nextEnd - nextStart;
1347
1519
  const lcs = Array.from({ length: unmatchedBaseLength + 1 }, () => Array(unmatchedNextLength + 1).fill(0));
1348
- 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];
1349
- else lcs[baseOffset][nextOffset] = Math.max(lcs[baseOffset + 1][nextOffset], lcs[baseOffset][nextOffset + 1]);
1520
+ for (let baseOffset = unmatchedBaseLength - 1; baseOffset >= 0; baseOffset--) {
1521
+ throwIfAborted(options.signal);
1522
+ for (let nextOffset = unmatchedNextLength - 1; nextOffset >= 0; nextOffset--) if (jsonEquals(base[baseStart + baseOffset], next[nextStart + nextOffset])) lcs[baseOffset][nextOffset] = 1 + lcs[baseOffset + 1][nextOffset + 1];
1523
+ else lcs[baseOffset][nextOffset] = Math.max(lcs[baseOffset + 1][nextOffset], lcs[baseOffset][nextOffset + 1]);
1524
+ }
1350
1525
  let baseOffset = 0;
1351
1526
  let nextOffset = 0;
1352
1527
  while (baseOffset < unmatchedBaseLength || nextOffset < unmatchedNextLength) {
1528
+ throwIfAborted(options.signal);
1353
1529
  if (baseOffset < unmatchedBaseLength && nextOffset < unmatchedNextLength && jsonEquals(base[baseStart + baseOffset], next[nextStart + nextOffset])) {
1354
1530
  steps.push({ kind: "equal" });
1355
1531
  baseOffset += 1;
@@ -1372,11 +1548,12 @@ function buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStar
1372
1548
  }
1373
1549
  }
1374
1550
  }
1375
- function computeLcsPrefixLengths(base, baseStart, baseEnd, next, nextStart, nextEnd) {
1551
+ function computeLcsPrefixLengths(base, baseStart, baseEnd, next, nextStart, nextEnd, options) {
1376
1552
  const unmatchedNextLength = nextEnd - nextStart;
1377
1553
  let previousRow = new Int32Array(unmatchedNextLength + 1);
1378
1554
  let currentRow = new Int32Array(unmatchedNextLength + 1);
1379
1555
  for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) {
1556
+ throwIfAborted(options.signal);
1380
1557
  for (let nextOffset = 0; nextOffset < unmatchedNextLength; nextOffset++) if (jsonEquals(base[baseIndex], next[nextStart + nextOffset])) currentRow[nextOffset + 1] = previousRow[nextOffset] + 1;
1381
1558
  else currentRow[nextOffset + 1] = Math.max(previousRow[nextOffset + 1], currentRow[nextOffset]);
1382
1559
  const nextPreviousRow = currentRow;
@@ -1386,11 +1563,12 @@ function computeLcsPrefixLengths(base, baseStart, baseEnd, next, nextStart, next
1386
1563
  }
1387
1564
  return previousRow;
1388
1565
  }
1389
- function computeLcsSuffixLengths(base, baseStart, baseEnd, next, nextStart, nextEnd) {
1566
+ function computeLcsSuffixLengths(base, baseStart, baseEnd, next, nextStart, nextEnd, options) {
1390
1567
  const unmatchedNextLength = nextEnd - nextStart;
1391
1568
  let previousRow = new Int32Array(unmatchedNextLength + 1);
1392
1569
  let currentRow = new Int32Array(unmatchedNextLength + 1);
1393
1570
  for (let baseIndex = baseEnd - 1; baseIndex >= baseStart; baseIndex--) {
1571
+ throwIfAborted(options.signal);
1394
1572
  for (let nextOffset = unmatchedNextLength - 1; nextOffset >= 0; nextOffset--) if (jsonEquals(base[baseIndex], next[nextStart + nextOffset])) currentRow[nextOffset] = previousRow[nextOffset + 1] + 1;
1395
1573
  else currentRow[nextOffset] = Math.max(previousRow[nextOffset], currentRow[nextOffset + 1]);
1396
1574
  const nextPreviousRow = currentRow;
@@ -1436,9 +1614,10 @@ function shouldUseLcsDiff(baseLength, nextLength, lcsMaxCells) {
1436
1614
  }
1437
1615
  function shouldUseLinearLcsDiff(baseLength, nextLength, options) {
1438
1616
  const cap = options.lcsLinearMaxCells;
1439
- if (cap === void 0 || cap === Number.POSITIVE_INFINITY) return true;
1440
- if (!Number.isFinite(cap) || cap < 1) return false;
1441
- return (baseLength + 1) * (nextLength + 1) <= cap;
1617
+ if (cap === Number.POSITIVE_INFINITY) return true;
1618
+ const effectiveCap = cap ?? DEFAULT_LCS_MAX_CELLS;
1619
+ if (!Number.isFinite(effectiveCap) || effectiveCap < 1) return false;
1620
+ return (baseLength + 1) * (nextLength + 1) <= effectiveCap;
1442
1621
  }
1443
1622
  function finalizeArrayOps(arrayPath, base, ops, options) {
1444
1623
  if (ops.length === 0) return [];
@@ -2068,7 +2247,14 @@ function isSamePath(a, b) {
2068
2247
  * @returns A new CRDT `Doc`.
2069
2248
  */
2070
2249
  function docFromJson(value, nextDot) {
2071
- return { root: nodeFromJson(value, nextDot) };
2250
+ const vv = Object.create(null);
2251
+ const doc = { root: nodeFromJson(value, () => {
2252
+ const dot = nextDot();
2253
+ observeVersionVectorDot(vv, dot);
2254
+ return dot;
2255
+ }) };
2256
+ writeCachedObservedVersionVector(doc, vv);
2257
+ return doc;
2072
2258
  }
2073
2259
  /**
2074
2260
  * Legacy helper for tests and fixtures that seeds an entire document from one dot.
@@ -2315,7 +2501,10 @@ function nodeFromJson(value, nextDot) {
2315
2501
  }
2316
2502
  /** Deep-clone a CRDT document. The clone is fully independent of the original. */
2317
2503
  function cloneDoc(doc) {
2318
- return { root: cloneNode(doc.root) };
2504
+ const cloned = { root: cloneNode(doc.root) };
2505
+ const cached = readCachedObservedVersionVector(doc);
2506
+ if (cached) writeCachedObservedVersionVector(cloned, cached);
2507
+ return cloned;
2319
2508
  }
2320
2509
  function cloneNode(node) {
2321
2510
  return cloneNodeAtDepth(node, 0);
@@ -2523,7 +2712,7 @@ function createArrayIndexLookupSession() {
2523
2712
  return created;
2524
2713
  } };
2525
2714
  }
2526
- function applyArrInsert(base, head, it, newDot, indexSession, bumpCounterAbove, strictParents = false) {
2715
+ function applyArrInsert(base, head, it, newDot, indexSession, bumpCounterAbove, strictParents = true) {
2527
2716
  const pointer = `/${it.path.join("/")}`;
2528
2717
  const baseSeq = getSeqAtPath(base, it.path);
2529
2718
  if (!baseSeq) {
@@ -2672,39 +2861,51 @@ function applyArrReplace(base, head, it, newDot, indexSession) {
2672
2861
  * @param evalTestAgainst - Whether `test` ops are evaluated against `"head"` or `"base"`.
2673
2862
  * @param bumpCounterAbove - Optional hook that can fast-forward the underlying counter before inserts.
2674
2863
  * @param options - Optional behavior toggles.
2675
- * @param options.strictParents - When `true`, reject array inserts whose base parent path is missing.
2864
+ * @param options.strictParents - Reject array inserts whose base parent path is missing.
2865
+ * Defaults to `true`; pass `false` only for legacy missing-parent array auto-creation.
2676
2866
  * @returns `{ ok: true }` on success, or `{ ok: false, code: 409, message }` on conflict.
2677
2867
  */
2678
2868
  function applyIntentsToCrdt(base, head, intents, newDot, evalTestAgainst = "head", bumpCounterAbove, options = {}) {
2679
2869
  const arrayIndexSession = createArrayIndexLookupSession();
2870
+ let pendingObservedDots = [];
2871
+ const observedNewDot = () => {
2872
+ const dot = newDot();
2873
+ pendingObservedDots.push(dot);
2874
+ return dot;
2875
+ };
2680
2876
  for (const it of intents) {
2681
2877
  let fail = null;
2878
+ pendingObservedDots = [];
2682
2879
  switch (it.t) {
2683
2880
  case "Test":
2684
2881
  fail = applyTest(base, head, it, evalTestAgainst);
2685
2882
  break;
2686
2883
  case "ObjSet":
2687
- fail = applyObjSet(head, it, newDot);
2884
+ fail = applyObjSet(head, it, observedNewDot);
2688
2885
  break;
2689
2886
  case "ObjRemove":
2690
- fail = applyObjRemove(head, it, newDot);
2887
+ fail = applyObjRemove(head, it, observedNewDot);
2691
2888
  break;
2692
2889
  case "ArrInsert":
2693
- fail = applyArrInsert(base, head, it, newDot, arrayIndexSession, bumpCounterAbove, options.strictParents ?? false);
2890
+ fail = applyArrInsert(base, head, it, observedNewDot, arrayIndexSession, bumpCounterAbove, options.strictParents ?? true);
2694
2891
  break;
2695
2892
  case "ArrDelete":
2696
- fail = applyArrDelete(base, head, it, newDot, arrayIndexSession);
2893
+ fail = applyArrDelete(base, head, it, observedNewDot, arrayIndexSession);
2697
2894
  break;
2698
2895
  case "ArrReplace":
2699
- fail = applyArrReplace(base, head, it, newDot, arrayIndexSession);
2896
+ fail = applyArrReplace(base, head, it, observedNewDot, arrayIndexSession);
2700
2897
  break;
2701
2898
  default: assertNever(it, "Unhandled intent type");
2702
2899
  }
2703
- if (fail) return fail;
2900
+ if (fail) {
2901
+ pendingObservedDots = [];
2902
+ return fail;
2903
+ }
2904
+ for (const dot of pendingObservedDots) observeDocVersionVectorDot(head, dot);
2704
2905
  }
2705
2906
  return { ok: true };
2706
2907
  }
2707
- function jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove, strictParents = false) {
2908
+ function jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove, strictParents = true) {
2708
2909
  if (isJsonPatchToCrdtOptions(baseOrOptions)) return jsonPatchToCrdtInternal(baseOrOptions);
2709
2910
  if (!head || !patch || !newDot) return {
2710
2911
  ok: false,
@@ -2722,7 +2923,7 @@ function jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst = "
2722
2923
  strictParents
2723
2924
  });
2724
2925
  }
2725
- function jsonPatchToCrdtSafe(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove, strictParents = false) {
2926
+ function jsonPatchToCrdtSafe(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove, strictParents = true) {
2726
2927
  try {
2727
2928
  if (isJsonPatchToCrdtOptions(baseOrOptions)) return jsonPatchToCrdt(baseOrOptions);
2728
2929
  if (!head || !patch || !newDot) return {
@@ -2991,7 +3192,7 @@ function diffNodeToPatch(path, baseNode, headNode, options, ops, depth) {
2991
3192
  * @returns An array of JSON Patch operations that transform base into head.
2992
3193
  */
2993
3194
  function crdtToJsonPatch(base, head, options) {
2994
- if ((options?.jsonValidation ?? "none") !== "none") return diffJsonPatch(materialize(base.root), materialize(head.root), options);
3195
+ if ((options?.jsonValidation ?? "none") !== "none" || options?.resourceBudget !== void 0) return diffJsonPatch(materialize(base.root), materialize(head.root), options);
2995
3196
  return crdtNodesToJsonPatch(base.root, head.root, options);
2996
3197
  }
2997
3198
  /** Internals-only helper for diffing CRDT nodes from an existing traversal depth. */
@@ -3013,11 +3214,21 @@ function crdtToFullReplace(doc) {
3013
3214
  }
3014
3215
  function jsonPatchToCrdtInternal(options) {
3015
3216
  const evalTestAgainst = options.evalTestAgainst ?? "head";
3016
- if ((options.semantics ?? "sequential") === "base") {
3217
+ const semantics = options.semantics ?? "sequential";
3218
+ const budgetMeter = createBudgetMeter(options.resourceBudget);
3219
+ try {
3220
+ budgetMeter?.count("patchOperations", options.patch.length);
3221
+ } catch (error) {
3222
+ return toApplyError$1(error);
3223
+ }
3224
+ if (semantics === "base") {
3017
3225
  const baseJson = materialize(options.base.root);
3018
3226
  let intents;
3019
3227
  try {
3020
- intents = compileJsonPatchToIntent(baseJson, options.patch, { semantics: "base" });
3228
+ intents = compileJsonPatchToIntent(baseJson, options.patch, {
3229
+ semantics: "base",
3230
+ resourceBudget: options.resourceBudget
3231
+ });
3021
3232
  } catch (error) {
3022
3233
  return toApplyError$1(error);
3023
3234
  }
@@ -3402,6 +3613,9 @@ function assertNever(_value, message) {
3402
3613
  var PatchError = class extends Error {
3403
3614
  code;
3404
3615
  reason;
3616
+ budget;
3617
+ limit;
3618
+ actual;
3405
3619
  path;
3406
3620
  opIndex;
3407
3621
  constructor(errorOrMessage, code = 409, reason = "INVALID_PATCH") {
@@ -3414,6 +3628,11 @@ var PatchError = class extends Error {
3414
3628
  }
3415
3629
  this.code = errorOrMessage.code;
3416
3630
  this.reason = errorOrMessage.reason;
3631
+ if (errorOrMessage.reason === "RESOURCE_BUDGET_EXCEEDED") {
3632
+ this.budget = errorOrMessage.budget;
3633
+ this.limit = errorOrMessage.limit;
3634
+ this.actual = errorOrMessage.actual;
3635
+ }
3417
3636
  this.path = errorOrMessage.path;
3418
3637
  this.opIndex = errorOrMessage.opIndex;
3419
3638
  }
@@ -3478,26 +3697,27 @@ function applyPatchInPlace(state, patch, options = {}) {
3478
3697
  }
3479
3698
  /** Non-throwing immutable patch application variant. */
3480
3699
  function tryApplyPatch(state, patch, options = {}) {
3481
- const nextState = {
3482
- doc: cloneDoc(state.doc),
3483
- clock: cloneClock(state.clock)
3484
- };
3485
3700
  try {
3701
+ throwIfAborted(options.signal);
3702
+ const nextState = {
3703
+ doc: cloneDoc(state.doc),
3704
+ clock: cloneClock(state.clock)
3705
+ };
3486
3706
  const result = applyPatchInternal(nextState, patch, options, "batch");
3487
3707
  if (!result.ok) return {
3488
3708
  ok: false,
3489
3709
  error: result
3490
3710
  };
3711
+ return {
3712
+ ok: true,
3713
+ state: nextState
3714
+ };
3491
3715
  } catch (error) {
3492
3716
  return {
3493
3717
  ok: false,
3494
3718
  error: toApplyError(error)
3495
3719
  };
3496
3720
  }
3497
- return {
3498
- ok: true,
3499
- state: nextState
3500
- };
3501
3721
  }
3502
3722
  /** Non-throwing in-place patch application variant. */
3503
3723
  function tryApplyPatchInPlace(state, patch, options = {}) {
@@ -3577,6 +3797,7 @@ function toApplyPatchOptionsForActor(options) {
3577
3797
  testAgainst: options.testAgainst,
3578
3798
  strictParents: options.strictParents,
3579
3799
  jsonValidation: options.jsonValidation,
3800
+ signal: options.signal,
3580
3801
  base: options.base ? {
3581
3802
  doc: options.base,
3582
3803
  clock: createClock("__base__", 0)
@@ -3584,8 +3805,10 @@ function toApplyPatchOptionsForActor(options) {
3584
3805
  };
3585
3806
  }
3586
3807
  function applyPatchInternal(state, patch, options, _execution) {
3808
+ throwIfAborted(options.signal);
3587
3809
  const preparedPatch = preparePatchPayloadsSafe(patch, options.jsonValidation ?? "none");
3588
3810
  if (!preparedPatch.ok) return preparedPatch;
3811
+ createBudgetMeter(options.resourceBudget)?.count("patchOperations", preparedPatch.patch.length);
3589
3812
  const runtimePatch = preparedPatch.patch;
3590
3813
  if ((options.semantics ?? "sequential") === "sequential") {
3591
3814
  const explicitBaseState = options.base ? {
@@ -3594,6 +3817,7 @@ function applyPatchInternal(state, patch, options, _execution) {
3594
3817
  } : null;
3595
3818
  const session = { pointerCache: /* @__PURE__ */ new Map() };
3596
3819
  for (const [opIndex, op] of runtimePatch.entries()) {
3820
+ throwIfAborted(options.signal);
3597
3821
  const step = applyPatchOpSequential(state, op, options, explicitBaseState ? explicitBaseState.doc : state.doc, explicitBaseState, opIndex, session);
3598
3822
  if (!step.ok) return step;
3599
3823
  }
@@ -3932,9 +4156,9 @@ function validateArrayIndexBounds(index, op, arrLength, path, opIndex) {
3932
4156
  function bumpClockCounter(state, ctr) {
3933
4157
  if (state.clock.ctr < ctr) state.clock.ctr = ctr;
3934
4158
  }
3935
- function compilePreparedIntents(baseJson, patch, semantics = "sequential", pointerCache, opIndexOffset = 0) {
4159
+ function compilePreparedIntents(baseJson, patch, semantics = "sequential", pointerCache, opIndexOffset = 0, budgetMeter) {
3936
4160
  try {
3937
- const compileOptions = toCompilePatchOptions(semantics, pointerCache, opIndexOffset);
4161
+ const compileOptions = toCompilePatchOptions(semantics, pointerCache, opIndexOffset, budgetMeter);
3938
4162
  if (patch.length === 1) return {
3939
4163
  ok: true,
3940
4164
  intents: compileJsonPatchOpToIntent(baseJson, patch[0], compileOptions)
@@ -3947,11 +4171,13 @@ function compilePreparedIntents(baseJson, patch, semantics = "sequential", point
3947
4171
  return toApplyError(error);
3948
4172
  }
3949
4173
  }
3950
- function toCompilePatchOptions(semantics, pointerCache, opIndexOffset = 0) {
4174
+ function toCompilePatchOptions(semantics, pointerCache, opIndexOffset = 0, budgetMeter) {
3951
4175
  return {
3952
4176
  semantics,
4177
+ resourceBudget: void 0,
3953
4178
  pointerCache,
3954
- opIndexOffset
4179
+ opIndexOffset,
4180
+ budgetMeter
3955
4181
  };
3956
4182
  }
3957
4183
  function preparePatchPayloadsSafe(patch, mode) {
@@ -3999,6 +4225,8 @@ function mergePointerPaths(basePointer, nestedPointer) {
3999
4225
  return `${basePointer}${nestedPointer}`;
4000
4226
  }
4001
4227
  function toApplyError(error) {
4228
+ if (error instanceof OperationCancelledError) return toCancellationApplyError(error);
4229
+ if (error instanceof ResourceBudgetError) return toBudgetApplyError(error);
4002
4230
  if (error instanceof TraversalDepthError) return toDepthApplyError(error);
4003
4231
  if (error instanceof PatchCompileError) return {
4004
4232
  ok: false,
@@ -4033,6 +4261,68 @@ function toPointerParseApplyError(error, pointer, opIndex) {
4033
4261
  };
4034
4262
  }
4035
4263
 
4264
+ //#endregion
4265
+ //#region src/safe.ts
4266
+ /** Create a state with strict runtime JSON validation enabled by default. */
4267
+ function createSafeState(initial, options) {
4268
+ return createState(initial, {
4269
+ ...options,
4270
+ jsonValidation: "strict"
4271
+ });
4272
+ }
4273
+ /** Apply a patch with strict runtime JSON validation enabled by default. */
4274
+ function applySafePatch(state, patch, options = {}) {
4275
+ return applyPatch(state, patch, {
4276
+ ...options,
4277
+ jsonValidation: "strict"
4278
+ });
4279
+ }
4280
+ /** Diff JSON values with strict runtime JSON validation enabled by default. */
4281
+ function diffSafeJsonPatch(base, next, options = {}) {
4282
+ return diffJsonPatch(base, next, {
4283
+ ...options,
4284
+ jsonValidation: "strict"
4285
+ });
4286
+ }
4287
+ /** Create a state with normalizing runtime JSON validation enabled by default. */
4288
+ function createNormalizedState(initial, options) {
4289
+ return createState(initial, {
4290
+ ...options,
4291
+ jsonValidation: "normalize"
4292
+ });
4293
+ }
4294
+ /** Apply a patch with normalizing runtime JSON validation enabled by default. */
4295
+ function applyNormalizedPatch(state, patch, options = {}) {
4296
+ return applyPatch(state, patch, {
4297
+ ...options,
4298
+ jsonValidation: "normalize"
4299
+ });
4300
+ }
4301
+ /** Diff JSON values with normalizing runtime JSON validation enabled by default. */
4302
+ function diffNormalizedJsonPatch(base, next, options = {}) {
4303
+ return diffJsonPatch(base, next, {
4304
+ ...options,
4305
+ jsonValidation: "normalize"
4306
+ });
4307
+ }
4308
+
4309
+ //#endregion
4310
+ //#region src/options.ts
4311
+ /** Options that reject legacy missing-parent array auto-creation. */
4312
+ const strictRfc6902PatchOptions = { strictParents: true };
4313
+ function withStrictRfc6902Parents(options) {
4314
+ return {
4315
+ ...options,
4316
+ strictParents: true
4317
+ };
4318
+ }
4319
+ function withLegacyMissingArrayParents(options) {
4320
+ return {
4321
+ ...options,
4322
+ strictParents: false
4323
+ };
4324
+ }
4325
+
4036
4326
  //#endregion
4037
4327
  //#region src/serialize.ts
4038
4328
  const HEAD_ELEM_ID = "HEAD";
@@ -4068,17 +4358,24 @@ function serializeDoc(doc) {
4068
4358
  };
4069
4359
  }
4070
4360
  /** Reconstruct a CRDT document from its serialized form. */
4071
- function deserializeDoc(data) {
4361
+ function deserializeDoc(data, options = {}) {
4362
+ return deserializeDocInternal(data, createBudgetMeter(options.resourceBudget), options.signal);
4363
+ }
4364
+ function deserializeDocInternal(data, budgetMeter, signal) {
4365
+ throwIfAborted(signal);
4072
4366
  const raw = readSerializedDocEnvelope(data);
4073
4367
  if (!("root" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/root", "serialized doc is missing root");
4074
- return { root: deserializeNode(raw.root, "/root", 0) };
4368
+ const observed = Object.create(null);
4369
+ const doc = { root: deserializeNode(raw.root, "/root", 0, budgetMeter, observed, signal) };
4370
+ writeCachedObservedVersionVector(doc, observed);
4371
+ return doc;
4075
4372
  }
4076
4373
  /** Non-throwing `deserializeDoc` variant with typed validation details. */
4077
- function tryDeserializeDoc(data) {
4374
+ function tryDeserializeDoc(data, options = {}) {
4078
4375
  try {
4079
4376
  return {
4080
4377
  ok: true,
4081
- doc: deserializeDoc(data)
4378
+ doc: deserializeDoc(data, options)
4082
4379
  };
4083
4380
  } catch (error) {
4084
4381
  const deserializeError = toDeserializeFailure(error);
@@ -4089,6 +4386,20 @@ function tryDeserializeDoc(data) {
4089
4386
  throw error;
4090
4387
  }
4091
4388
  }
4389
+ /** Validate a serialized CRDT document without throwing or returning runtime state. */
4390
+ function validateSerializedDoc(data, options = {}) {
4391
+ try {
4392
+ validateSerializedDocInternal(data, createBudgetMeter(options.resourceBudget), options.signal);
4393
+ return { ok: true };
4394
+ } catch (error) {
4395
+ const deserializeError = toDeserializeFailure(error);
4396
+ if (deserializeError) return {
4397
+ ok: false,
4398
+ error: deserializeError
4399
+ };
4400
+ throw error;
4401
+ }
4402
+ }
4092
4403
  /** Serialize a full CRDT state (document + clock) to a JSON-safe representation. */
4093
4404
  function serializeState(state) {
4094
4405
  return {
@@ -4106,26 +4417,33 @@ function serializeState(state) {
4106
4417
  * May throw `TraversalDepthError` when the payload exceeds the maximum
4107
4418
  * supported nesting depth.
4108
4419
  */
4109
- function deserializeState(data) {
4110
- const raw = readSerializedStateEnvelope(data);
4111
- if (!("doc" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/doc", "serialized state is missing doc");
4112
- if (!("clock" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/clock", "serialized state is missing clock");
4113
- const clockRaw = asRecord(raw.clock, "/clock");
4114
- const actor = readActor(clockRaw.actor, "/clock/actor");
4115
- const ctr = readCounter(clockRaw.ctr, "/clock/ctr");
4116
- const doc = deserializeDoc(raw.doc);
4117
- const observedCtr = observedVersionVector(doc)[actor] ?? 0;
4118
- return {
4119
- doc,
4120
- clock: createClock(actor, Math.max(ctr, observedCtr))
4121
- };
4420
+ function deserializeState(data, options = {}) {
4421
+ try {
4422
+ const raw = readSerializedStateEnvelope(data);
4423
+ const budgetMeter = createBudgetMeter(options.resourceBudget);
4424
+ throwIfAborted(options.signal);
4425
+ if (!("doc" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/doc", "serialized state is missing doc");
4426
+ if (!("clock" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/clock", "serialized state is missing clock");
4427
+ const clockRaw = asRecord(raw.clock, "/clock");
4428
+ const actor = readActor(clockRaw.actor, "/clock/actor");
4429
+ const ctr = readCounter(clockRaw.ctr, "/clock/ctr");
4430
+ const doc = deserializeDocInternal(raw.doc, budgetMeter, options.signal);
4431
+ const observedCtr = readCachedObservedVersionVector(doc)?.[actor] ?? 0;
4432
+ return {
4433
+ doc,
4434
+ clock: createClock(actor, Math.max(ctr, observedCtr))
4435
+ };
4436
+ } catch (error) {
4437
+ if (error instanceof OperationCancelledError) throw new DeserializeError("OPERATION_CANCELLED", "/", error.message);
4438
+ throw error;
4439
+ }
4122
4440
  }
4123
4441
  /** Non-throwing `deserializeState` variant with typed validation details. */
4124
- function tryDeserializeState(data) {
4442
+ function tryDeserializeState(data, options = {}) {
4125
4443
  try {
4126
4444
  return {
4127
4445
  ok: true,
4128
- state: deserializeState(data)
4446
+ state: deserializeState(data, options)
4129
4447
  };
4130
4448
  } catch (error) {
4131
4449
  const deserializeError = toDeserializeFailure(error);
@@ -4136,6 +4454,28 @@ function tryDeserializeState(data) {
4136
4454
  throw error;
4137
4455
  }
4138
4456
  }
4457
+ /** Validate a serialized CRDT state without throwing or returning runtime state. */
4458
+ function validateSerializedState(data, options = {}) {
4459
+ try {
4460
+ const raw = readSerializedStateEnvelope(data);
4461
+ const budgetMeter = createBudgetMeter(options.resourceBudget);
4462
+ throwIfAborted(options.signal);
4463
+ if (!("doc" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/doc", "serialized state is missing doc");
4464
+ if (!("clock" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/clock", "serialized state is missing clock");
4465
+ const clockRaw = asRecord(raw.clock, "/clock");
4466
+ readActor(clockRaw.actor, "/clock/actor");
4467
+ readCounter(clockRaw.ctr, "/clock/ctr");
4468
+ validateSerializedDocInternal(raw.doc, budgetMeter, options.signal);
4469
+ return { ok: true };
4470
+ } catch (error) {
4471
+ const deserializeError = toDeserializeFailure(error);
4472
+ if (deserializeError) return {
4473
+ ok: false,
4474
+ error: deserializeError
4475
+ };
4476
+ throw error;
4477
+ }
4478
+ }
4139
4479
  function serializeNode(node) {
4140
4480
  if (node.kind === "lww") return {
4141
4481
  kind: "lww",
@@ -4198,33 +4538,55 @@ function readSerializedStateEnvelope(data) {
4198
4538
  assertSerializedEnvelopeVersion(raw, "/version", SERIALIZED_STATE_VERSION, "state");
4199
4539
  return raw;
4200
4540
  }
4201
- function deserializeNode(node, path, depth) {
4541
+ function validateSerializedDocInternal(data, budgetMeter, signal) {
4542
+ throwIfAborted(signal);
4543
+ const raw = readSerializedDocEnvelope(data);
4544
+ if (!("root" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/root", "serialized doc is missing root");
4545
+ validateSerializedNode(raw.root, "/root", 0, budgetMeter, signal);
4546
+ }
4547
+ function deserializeNode(node, path, depth, budgetMeter, observed, signal) {
4548
+ throwIfAborted(signal);
4202
4549
  assertTraversalDepth(depth);
4550
+ budgetMeter?.count("visitedNodes", 1, path);
4203
4551
  const raw = asRecord(node, path);
4204
4552
  const kind = readString(raw.kind, `${path}/kind`);
4205
4553
  if (kind === "lww") {
4206
4554
  if (!("value" in raw)) fail("INVALID_SERIALIZED_SHAPE", `${path}/value`, "lww node is missing value");
4207
4555
  if (!("dot" in raw)) fail("INVALID_SERIALIZED_SHAPE", `${path}/dot`, "lww node is missing dot");
4556
+ const dot = readDot(raw.dot, `${path}/dot`);
4557
+ if (observed) observeVersionVectorDot(observed, dot);
4208
4558
  return {
4209
4559
  kind: "lww",
4210
- value: structuredClone(readJsonValue(raw.value, `${path}/value`, depth + 1)),
4211
- dot: readDot(raw.dot, `${path}/dot`)
4560
+ value: structuredClone(readJsonValue(raw.value, `${path}/value`, depth + 1, budgetMeter, signal)),
4561
+ dot
4212
4562
  };
4213
4563
  }
4214
4564
  if (kind === "obj") {
4215
4565
  const entriesRaw = asRecord(raw.entries, `${path}/entries`);
4216
4566
  const tombstoneRaw = asRecord(raw.tombstone, `${path}/tombstone`);
4567
+ budgetMeter?.count("objectEntries", Object.keys(entriesRaw).length, `${path}/entries`);
4568
+ budgetMeter?.count("serializedElements", Object.keys(entriesRaw).length, `${path}/entries`);
4569
+ budgetMeter?.count("objectEntries", Object.keys(tombstoneRaw).length, `${path}/tombstone`);
4570
+ budgetMeter?.count("serializedElements", Object.keys(tombstoneRaw).length, `${path}/tombstone`);
4217
4571
  const entries = /* @__PURE__ */ new Map();
4218
4572
  for (const [k, v] of Object.entries(entriesRaw)) {
4573
+ throwIfAborted(signal);
4219
4574
  const entryPath = `${path}/entries/${k}`;
4220
4575
  const entryRaw = asRecord(v, entryPath);
4576
+ const dot = readDot(entryRaw.dot, `${entryPath}/dot`);
4577
+ if (observed) observeVersionVectorDot(observed, dot);
4221
4578
  entries.set(k, {
4222
- node: deserializeNode(entryRaw.node, `${entryPath}/node`, depth + 1),
4223
- dot: readDot(entryRaw.dot, `${entryPath}/dot`)
4579
+ node: deserializeNode(entryRaw.node, `${entryPath}/node`, depth + 1, budgetMeter, observed, signal),
4580
+ dot
4224
4581
  });
4225
4582
  }
4226
4583
  const tombstone = /* @__PURE__ */ new Map();
4227
- for (const [k, d] of Object.entries(tombstoneRaw)) tombstone.set(k, readDot(d, `${path}/tombstone/${k}`));
4584
+ for (const [k, d] of Object.entries(tombstoneRaw)) {
4585
+ throwIfAborted(signal);
4586
+ const dot = readDot(d, `${path}/tombstone/${k}`);
4587
+ if (observed) observeVersionVectorDot(observed, dot);
4588
+ tombstone.set(k, dot);
4589
+ }
4228
4590
  return {
4229
4591
  kind: "obj",
4230
4592
  entries,
@@ -4233,17 +4595,24 @@ function deserializeNode(node, path, depth) {
4233
4595
  }
4234
4596
  if (kind !== "seq") fail("INVALID_SERIALIZED_SHAPE", `${path}/kind`, `unsupported node kind '${kind}'`);
4235
4597
  const elemsRaw = asRecord(raw.elems, `${path}/elems`);
4598
+ budgetMeter?.count("sequenceElements", Object.keys(elemsRaw).length, `${path}/elems`);
4599
+ budgetMeter?.count("serializedElements", Object.keys(elemsRaw).length, `${path}/elems`);
4236
4600
  const elems = /* @__PURE__ */ new Map();
4237
4601
  for (const [id, rawElem] of Object.entries(elemsRaw)) {
4602
+ throwIfAborted(signal);
4238
4603
  const elemPath = `${path}/elems/${id}`;
4239
4604
  const elem = asRecord(rawElem, elemPath);
4240
4605
  const elemId = readString(elem.id, `${elemPath}/id`);
4241
4606
  if (elemId !== id) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/id`, `sequence element id '${elemId}' does not match key '${id}'`);
4242
4607
  const prev = readString(elem.prev, `${elemPath}/prev`);
4243
4608
  const tombstone = readBoolean(elem.tombstone, `${elemPath}/tombstone`);
4244
- const value = deserializeNode(elem.value, `${elemPath}/value`, depth + 1);
4609
+ const value = deserializeNode(elem.value, `${elemPath}/value`, depth + 1, budgetMeter, observed, signal);
4245
4610
  const insDot = readDot(elem.insDot, `${elemPath}/insDot`);
4246
4611
  const delDot = "delDot" in elem && elem.delDot !== void 0 ? readDot(elem.delDot, `${elemPath}/delDot`) : void 0;
4612
+ if (observed) {
4613
+ observeVersionVectorDot(observed, insDot);
4614
+ if (delDot) observeVersionVectorDot(observed, delDot);
4615
+ }
4247
4616
  if (dotToElemId(insDot) !== id) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/insDot`, "sequence element id must match its insertion dot");
4248
4617
  if (!tombstone && delDot) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/delDot`, "live sequence elements must not include delete metadata");
4249
4618
  elems.set(id, {
@@ -4256,6 +4625,7 @@ function deserializeNode(node, path, depth) {
4256
4625
  });
4257
4626
  }
4258
4627
  for (const elem of elems.values()) {
4628
+ throwIfAborted(signal);
4259
4629
  if (elem.prev === elem.id) fail("INVALID_SERIALIZED_INVARIANT", `${path}/elems/${elem.id}/prev`, "sequence element cannot reference itself as predecessor");
4260
4630
  if (elem.prev !== HEAD_ELEM_ID && !elems.has(elem.prev)) fail("INVALID_SERIALIZED_INVARIANT", `${path}/elems/${elem.id}/prev`, `sequence predecessor '${elem.prev}' does not exist`);
4261
4631
  }
@@ -4265,6 +4635,66 @@ function deserializeNode(node, path, depth) {
4265
4635
  elems
4266
4636
  };
4267
4637
  }
4638
+ function validateSerializedNode(node, path, depth, budgetMeter, signal) {
4639
+ throwIfAborted(signal);
4640
+ assertTraversalDepth(depth);
4641
+ budgetMeter?.count("visitedNodes", 1, path);
4642
+ const raw = asRecord(node, path);
4643
+ const kind = readString(raw.kind, `${path}/kind`);
4644
+ if (kind === "lww") {
4645
+ if (!("value" in raw)) fail("INVALID_SERIALIZED_SHAPE", `${path}/value`, "lww node is missing value");
4646
+ if (!("dot" in raw)) fail("INVALID_SERIALIZED_SHAPE", `${path}/dot`, "lww node is missing dot");
4647
+ readDot(raw.dot, `${path}/dot`);
4648
+ readJsonValue(raw.value, `${path}/value`, depth + 1, budgetMeter, signal);
4649
+ return;
4650
+ }
4651
+ if (kind === "obj") {
4652
+ const entriesRaw = asRecord(raw.entries, `${path}/entries`);
4653
+ const tombstoneRaw = asRecord(raw.tombstone, `${path}/tombstone`);
4654
+ budgetMeter?.count("objectEntries", Object.keys(entriesRaw).length, `${path}/entries`);
4655
+ budgetMeter?.count("serializedElements", Object.keys(entriesRaw).length, `${path}/entries`);
4656
+ budgetMeter?.count("objectEntries", Object.keys(tombstoneRaw).length, `${path}/tombstone`);
4657
+ budgetMeter?.count("serializedElements", Object.keys(tombstoneRaw).length, `${path}/tombstone`);
4658
+ for (const [k, v] of Object.entries(entriesRaw)) {
4659
+ throwIfAborted(signal);
4660
+ const entryPath = `${path}/entries/${k}`;
4661
+ const entryRaw = asRecord(v, entryPath);
4662
+ readDot(entryRaw.dot, `${entryPath}/dot`);
4663
+ validateSerializedNode(entryRaw.node, `${entryPath}/node`, depth + 1, budgetMeter, signal);
4664
+ }
4665
+ for (const [k, d] of Object.entries(tombstoneRaw)) {
4666
+ throwIfAborted(signal);
4667
+ readDot(d, `${path}/tombstone/${k}`);
4668
+ }
4669
+ return;
4670
+ }
4671
+ if (kind !== "seq") fail("INVALID_SERIALIZED_SHAPE", `${path}/kind`, `unsupported node kind '${kind}'`);
4672
+ const elemsRaw = asRecord(raw.elems, `${path}/elems`);
4673
+ budgetMeter?.count("sequenceElements", Object.keys(elemsRaw).length, `${path}/elems`);
4674
+ budgetMeter?.count("serializedElements", Object.keys(elemsRaw).length, `${path}/elems`);
4675
+ const predecessors = /* @__PURE__ */ new Map();
4676
+ for (const [id, rawElem] of Object.entries(elemsRaw)) {
4677
+ throwIfAborted(signal);
4678
+ const elemPath = `${path}/elems/${id}`;
4679
+ const elem = asRecord(rawElem, elemPath);
4680
+ const elemId = readString(elem.id, `${elemPath}/id`);
4681
+ if (elemId !== id) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/id`, `sequence element id '${elemId}' does not match key '${id}'`);
4682
+ const prev = readString(elem.prev, `${elemPath}/prev`);
4683
+ const tombstone = readBoolean(elem.tombstone, `${elemPath}/tombstone`);
4684
+ validateSerializedNode(elem.value, `${elemPath}/value`, depth + 1, budgetMeter, signal);
4685
+ const insDot = readDot(elem.insDot, `${elemPath}/insDot`);
4686
+ const delDot = "delDot" in elem && elem.delDot !== void 0 ? readDot(elem.delDot, `${elemPath}/delDot`) : void 0;
4687
+ if (dotToElemId(insDot) !== id) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/insDot`, "sequence element id must match its insertion dot");
4688
+ if (!tombstone && delDot) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/delDot`, "live sequence elements must not include delete metadata");
4689
+ predecessors.set(id, prev);
4690
+ }
4691
+ for (const [id, prev] of predecessors) {
4692
+ throwIfAborted(signal);
4693
+ if (prev === id) fail("INVALID_SERIALIZED_INVARIANT", `${path}/elems/${id}/prev`, "sequence element cannot reference itself as predecessor");
4694
+ if (prev !== HEAD_ELEM_ID && !predecessors.has(prev)) fail("INVALID_SERIALIZED_INVARIANT", `${path}/elems/${id}/prev`, `sequence predecessor '${prev}' does not exist`);
4695
+ }
4696
+ assertAcyclicSerializedRgaPredecessors(predecessors, path);
4697
+ }
4268
4698
  function assertAcyclicRgaPredecessors(elems, path) {
4269
4699
  const visitState = /* @__PURE__ */ new Map();
4270
4700
  for (const startId of elems.keys()) {
@@ -4285,6 +4715,26 @@ function assertAcyclicRgaPredecessors(elems, path) {
4285
4715
  for (const id of trail) visitState.set(id, 2);
4286
4716
  }
4287
4717
  }
4718
+ function assertAcyclicSerializedRgaPredecessors(predecessors, path) {
4719
+ const visitState = /* @__PURE__ */ new Map();
4720
+ for (const startId of predecessors.keys()) {
4721
+ if (visitState.get(startId) === 2) continue;
4722
+ const trail = [];
4723
+ const trailSet = /* @__PURE__ */ new Set();
4724
+ let currentId = startId;
4725
+ while (currentId) {
4726
+ if (trailSet.has(currentId)) fail("INVALID_SERIALIZED_INVARIANT", `${path}/elems/${currentId}/prev`, `sequence predecessor cycle detected at '${currentId}'`);
4727
+ if (visitState.get(currentId) === 2) break;
4728
+ trail.push(currentId);
4729
+ trailSet.add(currentId);
4730
+ visitState.set(currentId, 1);
4731
+ const prev = predecessors.get(currentId);
4732
+ if (!prev || prev === HEAD_ELEM_ID) break;
4733
+ currentId = prev;
4734
+ }
4735
+ for (const id of trail) visitState.set(id, 2);
4736
+ }
4737
+ }
4288
4738
  function asRecord(value, path) {
4289
4739
  if (!isRecord(value)) fail("INVALID_SERIALIZED_SHAPE", path, "expected object");
4290
4740
  return value;
@@ -4322,28 +4772,43 @@ function readBoolean(value, path) {
4322
4772
  if (typeof value !== "boolean") fail("INVALID_SERIALIZED_SHAPE", path, "expected boolean");
4323
4773
  return value;
4324
4774
  }
4325
- function readJsonValue(value, path, depth) {
4326
- assertJsonValue(value, path, depth);
4775
+ function readJsonValue(value, path, depth, budgetMeter, signal) {
4776
+ assertJsonValue(value, path, depth, budgetMeter, signal);
4327
4777
  return value;
4328
4778
  }
4329
- function assertJsonValue(value, path, depth) {
4779
+ function assertJsonValue(value, path, depth, budgetMeter, signal) {
4780
+ throwIfAborted(signal);
4330
4781
  assertTraversalDepth(depth);
4782
+ budgetMeter?.count("visitedNodes", 1, path);
4331
4783
  if (value === null || typeof value === "string" || typeof value === "boolean") return;
4332
4784
  if (typeof value === "number") {
4333
4785
  if (!Number.isFinite(value)) fail("INVALID_SERIALIZED_SHAPE", path, "json number must be finite");
4334
4786
  return;
4335
4787
  }
4336
4788
  if (Array.isArray(value)) {
4337
- for (const [index, item] of value.entries()) assertJsonValue(item, `${path}/${index}`, depth + 1);
4789
+ budgetMeter?.count("sequenceElements", value.length, path);
4790
+ budgetMeter?.count("serializedElements", value.length, path);
4791
+ for (const [index, item] of value.entries()) {
4792
+ throwIfAborted(signal);
4793
+ assertJsonValue(item, `${path}/${index}`, depth + 1, budgetMeter, signal);
4794
+ }
4338
4795
  return;
4339
4796
  }
4340
4797
  if (!isRecord(value)) fail("INVALID_SERIALIZED_SHAPE", path, "expected JSON value");
4341
- for (const [key, child] of Object.entries(value)) assertJsonValue(child, `${path}/${key}`, depth + 1);
4798
+ const entries = Object.entries(value);
4799
+ budgetMeter?.count("objectEntries", entries.length, path);
4800
+ budgetMeter?.count("serializedElements", entries.length, path);
4801
+ for (const [key, child] of entries) {
4802
+ throwIfAborted(signal);
4803
+ assertJsonValue(child, `${path}/${key}`, depth + 1, budgetMeter, signal);
4804
+ }
4342
4805
  }
4343
4806
  function fail(reason, path, message) {
4344
4807
  throw new DeserializeError(reason, path, message);
4345
4808
  }
4346
4809
  function toDeserializeFailure(error) {
4810
+ if (error instanceof ResourceBudgetError) return toBudgetDeserializeFailure(error);
4811
+ if (error instanceof OperationCancelledError) return toCancellationDeserializeFailure(error);
4347
4812
  if (error instanceof DeserializeError || error instanceof TraversalDepthError) return error;
4348
4813
  return null;
4349
4814
  }
@@ -4365,12 +4830,20 @@ var SharedElementMetadataMismatchError = class extends Error {
4365
4830
  var MergeError = class extends Error {
4366
4831
  code;
4367
4832
  reason;
4833
+ budget;
4834
+ limit;
4835
+ actual;
4368
4836
  path;
4369
4837
  constructor(error) {
4370
4838
  super(error.message);
4371
4839
  this.name = "MergeError";
4372
4840
  this.code = error.code;
4373
4841
  this.reason = error.reason;
4842
+ if (error.reason === "RESOURCE_BUDGET_EXCEEDED") {
4843
+ this.budget = error.budget;
4844
+ this.limit = error.limit;
4845
+ this.actual = error.actual;
4846
+ }
4374
4847
  this.path = error.path;
4375
4848
  }
4376
4849
  };
@@ -4395,9 +4868,13 @@ function mergeDoc(a, b, options = {}) {
4395
4868
  /** Non-throwing `mergeDoc` variant with structured conflict details. */
4396
4869
  function tryMergeDoc(a, b, options = {}) {
4397
4870
  try {
4398
- const config = { unrelatedArrays: resolveUnrelatedArraysStrategy(options) };
4871
+ const config = {
4872
+ unrelatedArrays: resolveUnrelatedArraysStrategy(options),
4873
+ budgetMeter: createBudgetMeter(options.resourceBudget),
4874
+ signal: options.signal
4875
+ };
4399
4876
  if (config.unrelatedArrays === "reject") {
4400
- const mismatchPath = findSeqLineageMismatch(a.root, b.root, []);
4877
+ const mismatchPath = findSeqLineageMismatch(a.root, b.root, [], config);
4401
4878
  if (mismatchPath !== null) return {
4402
4879
  ok: false,
4403
4880
  error: {
@@ -4428,6 +4905,14 @@ function tryMergeDoc(a, b, options = {}) {
4428
4905
  ok: false,
4429
4906
  error: toDepthApplyError(error)
4430
4907
  };
4908
+ if (error instanceof ResourceBudgetError) return {
4909
+ ok: false,
4910
+ error: toBudgetApplyError(error)
4911
+ };
4912
+ if (error instanceof OperationCancelledError) return {
4913
+ ok: false,
4914
+ error: toCancellationApplyError(error)
4915
+ };
4431
4916
  throw error;
4432
4917
  }
4433
4918
  }
@@ -4453,10 +4938,12 @@ function tryMergeState(a, b, options = {}) {
4453
4938
  const actor = options.actor ?? a.clock.actor;
4454
4939
  const config = {
4455
4940
  actor,
4456
- unrelatedArrays: resolveUnrelatedArraysStrategy(options)
4941
+ unrelatedArrays: resolveUnrelatedArraysStrategy(options),
4942
+ budgetMeter: createBudgetMeter(options.resourceBudget),
4943
+ signal: options.signal
4457
4944
  };
4458
4945
  if (config.unrelatedArrays === "reject") {
4459
- const mismatchPath = findSeqLineageMismatch(a.doc.root, b.doc.root, []);
4946
+ const mismatchPath = findSeqLineageMismatch(a.doc.root, b.doc.root, [], config);
4460
4947
  if (mismatchPath !== null) return {
4461
4948
  ok: false,
4462
4949
  error: {
@@ -4492,20 +4979,34 @@ function tryMergeState(a, b, options = {}) {
4492
4979
  ok: false,
4493
4980
  error: toDepthApplyError(error)
4494
4981
  };
4982
+ if (error instanceof ResourceBudgetError) return {
4983
+ ok: false,
4984
+ error: toBudgetApplyError(error)
4985
+ };
4986
+ if (error instanceof OperationCancelledError) return {
4987
+ ok: false,
4988
+ error: toCancellationApplyError(error)
4989
+ };
4495
4990
  throw error;
4496
4991
  }
4497
4992
  }
4498
- function findSeqLineageMismatch(a, b, path) {
4993
+ function findSeqLineageMismatch(a, b, path, config) {
4994
+ const pathBuffer = [...path];
4499
4995
  const stack = [{
4500
4996
  a,
4501
4997
  b,
4502
- path,
4503
4998
  depth: path.length
4504
4999
  }];
4505
5000
  while (stack.length > 0) {
5001
+ throwIfAborted(config.signal);
4506
5002
  const frame = stack.pop();
4507
5003
  assertTraversalDepth(frame.depth);
5004
+ pathBuffer.length = frame.depth;
5005
+ if (frame.key !== void 0) pathBuffer[frame.depth - 1] = frame.key;
5006
+ const budgetPath = config.budgetMeter ? stringifyJsonPointer(pathBuffer) : void 0;
5007
+ config.budgetMeter?.count("visitedNodes", 1, budgetPath);
4508
5008
  if (frame.a.kind === "seq" && frame.b.kind === "seq") {
5009
+ config.budgetMeter?.count("sequenceElements", frame.a.elems.size + frame.b.elems.size, budgetPath);
4509
5010
  const hasElemsA = frame.a.elems.size > 0;
4510
5011
  const hasElemsB = frame.b.elems.size > 0;
4511
5012
  if (hasElemsA && hasElemsB) {
@@ -4514,22 +5015,28 @@ function findSeqLineageMismatch(a, b, path) {
4514
5015
  shared = true;
4515
5016
  break;
4516
5017
  }
4517
- if (!shared) return stringifyJsonPointer(frame.path);
5018
+ if (!shared) return stringifyJsonPointer(pathBuffer);
4518
5019
  }
4519
5020
  }
4520
5021
  if (frame.a.kind === "obj" && frame.b.kind === "obj") {
4521
5022
  const left = frame.a;
4522
5023
  const right = frame.b;
4523
- const sharedKeys = [...left.entries.keys()].filter((key) => right.entries.has(key));
4524
- for (let i = sharedKeys.length - 1; i >= 0; i--) {
4525
- const key = sharedKeys[i];
5024
+ if (config.budgetMeter) {
5025
+ let sharedKeyCount = 0;
5026
+ for (const key of left.entries.keys()) if (right.entries.has(key)) sharedKeyCount += 1;
5027
+ config.budgetMeter.count("objectEntries", sharedKeyCount, budgetPath);
5028
+ }
5029
+ const sharedKeysInOrder = [];
5030
+ for (const key of left.entries.keys()) if (right.entries.has(key)) sharedKeysInOrder.push(key);
5031
+ for (let i = sharedKeysInOrder.length - 1; i >= 0; i--) {
5032
+ const key = sharedKeysInOrder[i];
4526
5033
  const nextA = left.entries.get(key).node;
4527
5034
  const nextB = right.entries.get(key).node;
4528
5035
  stack.push({
4529
5036
  a: nextA,
4530
5037
  b: nextB,
4531
- path: [...frame.path, key],
4532
- depth: frame.depth + 1
5038
+ depth: frame.depth + 1,
5039
+ key
4533
5040
  });
4534
5041
  }
4535
5042
  }
@@ -4554,7 +5061,7 @@ function maxObservedCtrForActor(docObservedCtr, actor, a, b) {
4554
5061
  if (b.clock.actor === actor && b.clock.ctr > best) best = b.clock.ctr;
4555
5062
  return best;
4556
5063
  }
4557
- function repDot(node) {
5064
+ function repDot(node, signal) {
4558
5065
  let best = {
4559
5066
  actor: "",
4560
5067
  ctr: 0
@@ -4564,6 +5071,7 @@ function repDot(node) {
4564
5071
  depth: 0
4565
5072
  }];
4566
5073
  while (stack.length > 0) {
5074
+ throwIfAborted(signal);
4567
5075
  const frame = stack.pop();
4568
5076
  assertTraversalDepth(frame.depth);
4569
5077
  switch (frame.node.kind) {
@@ -4595,12 +5103,14 @@ function repDot(node) {
4595
5103
  return best;
4596
5104
  }
4597
5105
  function mergeNodeAtDepth(a, b, depth, path, config) {
5106
+ throwIfAborted(config.signal);
4598
5107
  assertTraversalDepth(depth);
5108
+ config.budgetMeter?.count("visitedNodes", 1, stringifyJsonPointer(path));
4599
5109
  if (a.kind === "lww" && b.kind === "lww") return mergeLww(a, b, config.actor);
4600
5110
  if (a.kind === "obj" && b.kind === "obj") return mergeObj(a, b, depth + 1, path, config);
4601
5111
  if (a.kind === "seq" && b.kind === "seq") return mergeSeq(a, b, depth + 1, path, config);
4602
- if (compareDot(repDot(a), repDot(b)) >= 0) return cloneNodeShallow(a, depth + 1, config.actor);
4603
- return cloneNodeShallow(b, depth + 1, config.actor);
5112
+ if (compareDot(repDot(a, config.signal), repDot(b, config.signal)) >= 0) return cloneNodeShallow(a, depth + 1, config.actor, config.signal);
5113
+ return cloneNodeShallow(b, depth + 1, config.actor, config.signal);
4604
5114
  }
4605
5115
  function mergeLww(a, b, actor) {
4606
5116
  if (compareDot(a.dot, b.dot) >= 0) return {
@@ -4626,7 +5136,9 @@ function mergeObj(a, b, depth, path, config) {
4626
5136
  const tombstone = /* @__PURE__ */ new Map();
4627
5137
  let maxObservedCtr = 0;
4628
5138
  const allTombKeys = new Set([...a.tombstone.keys(), ...b.tombstone.keys()]);
5139
+ config.budgetMeter?.count("objectEntries", allTombKeys.size, stringifyJsonPointer(path));
4629
5140
  for (const key of allTombKeys) {
5141
+ throwIfAborted(config.signal);
4630
5142
  const da = a.tombstone.get(key);
4631
5143
  const db = b.tombstone.get(key);
4632
5144
  if (da && db) {
@@ -4642,13 +5154,22 @@ function mergeObj(a, b, depth, path, config) {
4642
5154
  }
4643
5155
  }
4644
5156
  const allKeys = new Set([...a.entries.keys(), ...b.entries.keys()]);
5157
+ config.budgetMeter?.count("objectEntries", allKeys.size, stringifyJsonPointer(path));
4645
5158
  for (const key of allKeys) {
5159
+ throwIfAborted(config.signal);
4646
5160
  const ea = a.entries.get(key);
4647
5161
  const eb = b.entries.get(key);
4648
5162
  let merged;
4649
5163
  let mergedNodeMaxObservedCtr = 0;
4650
5164
  if (ea && eb) {
4651
- const mergedNode = mergeNodeAtDepth(ea.node, eb.node, depth + 1, [...path, key], config);
5165
+ path.push(key);
5166
+ const mergedNode = (() => {
5167
+ try {
5168
+ return mergeNodeAtDepth(ea.node, eb.node, depth + 1, path, config);
5169
+ } finally {
5170
+ path.pop();
5171
+ }
5172
+ })();
4652
5173
  const dot = compareDot(ea.dot, eb.dot) >= 0 ? { ...ea.dot } : { ...eb.dot };
4653
5174
  merged = {
4654
5175
  node: mergedNode.node,
@@ -4656,14 +5177,14 @@ function mergeObj(a, b, depth, path, config) {
4656
5177
  };
4657
5178
  mergedNodeMaxObservedCtr = mergedNode.maxObservedCtr;
4658
5179
  } else if (ea) {
4659
- const cloned = cloneNodeShallow(ea.node, depth + 1, config.actor);
5180
+ const cloned = cloneNodeShallow(ea.node, depth + 1, config.actor, config.signal);
4660
5181
  merged = {
4661
5182
  node: cloned.node,
4662
5183
  dot: { ...ea.dot }
4663
5184
  };
4664
5185
  mergedNodeMaxObservedCtr = cloned.maxObservedCtr;
4665
5186
  } else {
4666
- const cloned = cloneNodeShallow(eb.node, depth + 1, config.actor);
5187
+ const cloned = cloneNodeShallow(eb.node, depth + 1, config.actor, config.signal);
4667
5188
  merged = {
4668
5189
  node: cloned.node,
4669
5190
  dot: { ...eb.dot }
@@ -4686,24 +5207,40 @@ function mergeObj(a, b, depth, path, config) {
4686
5207
  }
4687
5208
  function mergeSeq(a, b, depth, path, config) {
4688
5209
  assertTraversalDepth(depth);
5210
+ config.budgetMeter?.count("visitedNodes", 1, stringifyJsonPointer(path));
4689
5211
  if (config.unrelatedArrays === "atomic-replace" && a.elems.size > 0 && b.elems.size > 0) {
4690
5212
  let shared = false;
4691
- for (const id of a.elems.keys()) if (b.elems.has(id)) {
4692
- shared = true;
4693
- break;
5213
+ for (const id of a.elems.keys()) {
5214
+ throwIfAborted(config.signal);
5215
+ if (b.elems.has(id)) {
5216
+ shared = true;
5217
+ break;
5218
+ }
5219
+ }
5220
+ if (!shared) {
5221
+ config.budgetMeter?.count("sequenceElements", a.elems.size + b.elems.size, stringifyJsonPointer(path));
5222
+ return cloneNodeShallow(compareDot(repDot(a, config.signal), repDot(b, config.signal)) >= 0 ? a : b, depth, config.actor, config.signal);
4694
5223
  }
4695
- if (!shared) return cloneNodeShallow(compareDot(repDot(a), repDot(b)) >= 0 ? a : b, depth, config.actor);
4696
5224
  }
4697
5225
  const elems = /* @__PURE__ */ new Map();
4698
5226
  let maxObservedCtr = 0;
4699
5227
  const allIds = new Set([...a.elems.keys(), ...b.elems.keys()]);
5228
+ config.budgetMeter?.count("sequenceElements", allIds.size, stringifyJsonPointer(path));
4700
5229
  for (const id of allIds) {
5230
+ throwIfAborted(config.signal);
4701
5231
  const ea = a.elems.get(id);
4702
5232
  const eb = b.elems.get(id);
4703
5233
  if (ea && eb) {
4704
5234
  if (ea.prev !== eb.prev) throw new SharedElementMetadataMismatchError(stringifyJsonPointer(path), id, "prev");
4705
5235
  if (!sameDot(ea.insDot, eb.insDot)) throw new SharedElementMetadataMismatchError(stringifyJsonPointer(path), id, "insDot");
4706
- const mergedValue = mergeNodeAtDepth(ea.value, eb.value, depth + 1, [...path, id], config);
5236
+ path.push(id);
5237
+ const mergedValue = (() => {
5238
+ try {
5239
+ return mergeNodeAtDepth(ea.value, eb.value, depth + 1, path, config);
5240
+ } finally {
5241
+ path.pop();
5242
+ }
5243
+ })();
4707
5244
  const mergedDeleteDot = mergeDeleteDot(ea.delDot, eb.delDot);
4708
5245
  elems.set(id, {
4709
5246
  id,
@@ -4715,11 +5252,11 @@ function mergeSeq(a, b, depth, path, config) {
4715
5252
  });
4716
5253
  maxObservedCtr = Math.max(maxObservedCtr, mergedValue.maxObservedCtr, maxObservedCtrForDot(ea.insDot, config.actor), maxObservedCtrForDot(mergedDeleteDot, config.actor));
4717
5254
  } else if (ea) {
4718
- const cloned = cloneElem(ea, depth + 1, config.actor);
5255
+ const cloned = cloneElem(ea, depth + 1, config.actor, config.signal);
4719
5256
  elems.set(id, cloned.elem);
4720
5257
  maxObservedCtr = Math.max(maxObservedCtr, cloned.maxObservedCtr);
4721
5258
  } else {
4722
- const cloned = cloneElem(eb, depth + 1, config.actor);
5259
+ const cloned = cloneElem(eb, depth + 1, config.actor, config.signal);
4723
5260
  elems.set(id, cloned.elem);
4724
5261
  maxObservedCtr = Math.max(maxObservedCtr, cloned.maxObservedCtr);
4725
5262
  }
@@ -4735,9 +5272,10 @@ function mergeSeq(a, b, depth, path, config) {
4735
5272
  function sameDot(a, b) {
4736
5273
  return a.actor === b.actor && a.ctr === b.ctr;
4737
5274
  }
4738
- function cloneElem(e, depth, actor) {
5275
+ function cloneElem(e, depth, actor, signal) {
5276
+ throwIfAborted(signal);
4739
5277
  assertTraversalDepth(depth);
4740
- const value = cloneNodeShallow(e.value, depth + 1, actor);
5278
+ const value = cloneNodeShallow(e.value, depth + 1, actor, signal);
4741
5279
  return {
4742
5280
  elem: {
4743
5281
  id: e.id,
@@ -4755,7 +5293,8 @@ function mergeDeleteDot(a, b) {
4755
5293
  if (a) return { ...a };
4756
5294
  if (b) return { ...b };
4757
5295
  }
4758
- function cloneNodeShallow(node, depth, actor) {
5296
+ function cloneNodeShallow(node, depth, actor, signal) {
5297
+ throwIfAborted(signal);
4759
5298
  assertTraversalDepth(depth);
4760
5299
  switch (node.kind) {
4761
5300
  case "lww": return {
@@ -4770,7 +5309,8 @@ function cloneNodeShallow(node, depth, actor) {
4770
5309
  const entries = /* @__PURE__ */ new Map();
4771
5310
  let maxObservedCtr = 0;
4772
5311
  for (const [k, v] of node.entries) {
4773
- const cloned = cloneNodeShallow(v.node, depth + 1, actor);
5312
+ throwIfAborted(signal);
5313
+ const cloned = cloneNodeShallow(v.node, depth + 1, actor, signal);
4774
5314
  entries.set(k, {
4775
5315
  node: cloned.node,
4776
5316
  dot: { ...v.dot }
@@ -4779,6 +5319,7 @@ function cloneNodeShallow(node, depth, actor) {
4779
5319
  }
4780
5320
  const tombstone = /* @__PURE__ */ new Map();
4781
5321
  for (const [k, d] of node.tombstone) {
5322
+ throwIfAborted(signal);
4782
5323
  tombstone.set(k, { ...d });
4783
5324
  maxObservedCtr = Math.max(maxObservedCtr, maxObservedCtrForDot(d, actor));
4784
5325
  }
@@ -4795,7 +5336,8 @@ function cloneNodeShallow(node, depth, actor) {
4795
5336
  const elems = /* @__PURE__ */ new Map();
4796
5337
  let maxObservedCtr = 0;
4797
5338
  for (const [id, e] of node.elems) {
4798
- const cloned = cloneElem(e, depth + 1, actor);
5339
+ throwIfAborted(signal);
5340
+ const cloned = cloneElem(e, depth + 1, actor, signal);
4799
5341
  elems.set(id, cloned.elem);
4800
5342
  maxObservedCtr = Math.max(maxObservedCtr, cloned.maxObservedCtr);
4801
5343
  }
@@ -4894,4 +5436,4 @@ function compactStateTombstones(state, options) {
4894
5436
  }
4895
5437
 
4896
5438
  //#endregion
4897
- export { materialize as $, crdtToJsonPatch as A, jsonEquals as B, tryApplyPatchAsActor as C, TraversalDepthError as Ct, cloneDoc as D, applyIntentsToCrdt as E, tryJsonPatchToCrdt as F, JsonValueValidationError as G, stableJsonValueKey as H, PatchCompileError as I, newReg as J, lwwSet as K, compileJsonPatchToIntent as L, docFromJsonWithDot as M, jsonPatchToCrdt as N, crdtNodesToJsonPatch as O, jsonPatchToCrdtSafe as P, objSet as Q, diffJsonPatch as R, tryApplyPatch as S, MAX_TRAVERSAL_DEPTH as St, validateJsonPatch as T, stringifyJsonPointer as U, parseJsonPointer as V, ROOT_KEY as W, objCompactTombstones as X, newSeq as Y, objRemove as Z, applyPatchAsActor as _, observeDot as _t, mergeState as a, rgaInsertAfterChecked as at, forkState as b, observedVersionVector as bt, DeserializeError as c, validateRgaSeq as ct, serializeDoc as d, vvHasDot as dt, HEAD as et, serializeState as f, vvMerge as ft, applyPatch as g, nextDotForActor as gt, PatchError as h, createClock as ht, mergeDoc as i, rgaInsertAfter as it, docFromJson as j, crdtToFullReplace as k, deserializeDoc as l, compareDot as lt, tryDeserializeState as m, cloneClock as mt, compactStateTombstones as n, rgaDelete as nt, tryMergeDoc as o, rgaLinearizeIds as ot, tryDeserializeDoc as p, ClockValidationError as pt, newObj as q, MergeError as r, rgaIdAtIndex as rt, tryMergeState as s, rgaPrevForInsertAtIndex as st, compactDocTombstones as t, rgaCompactTombstones as tt, deserializeState as u, dotToElemId as ut, applyPatchInPlace as v, intersectVersionVectors as vt, tryApplyPatchInPlace as w, toJson as x, versionVectorCovers as xt, createState as y, mergeVersionVectors as yt, getAtJson as z };
5439
+ export { stableJsonValueKey as $, createState as A, mergeVersionVectors as At, crdtToFullReplace as B, createSafeState as C, vvMerge as Ct, applyPatch as D, nextDotForActor as Dt, PatchError as E, createClock as Et, tryApplyPatchInPlace as F, OperationCancelledError as Ft, jsonPatchToCrdtSafe as G, docFromJson as H, validateJsonPatch as I, ResourceBudgetError as It, compileJsonPatchToIntent as J, tryJsonPatchToCrdt as K, applyIntentsToCrdt as L, toJson as M, versionVectorCovers as Mt, tryApplyPatch as N, MAX_TRAVERSAL_DEPTH as Nt, applyPatchAsActor as O, observeDot as Ot, tryApplyPatchAsActor as P, TraversalDepthError as Pt, parseJsonPointer as Q, cloneDoc as R, createNormalizedState as S, vvHasDot as St, diffSafeJsonPatch as T, cloneClock as Tt, docFromJsonWithDot as U, crdtToJsonPatch as V, jsonPatchToCrdt as W, getAtJson as X, diffJsonPatch as Y, jsonEquals as Z, strictRfc6902PatchOptions as _, rgaLinearizeIds as _t, mergeState as a, newReg as at, applyNormalizedPatch as b, compareDot as bt, DeserializeError as c, objRemove as ct, serializeDoc as d, HEAD as dt, stringifyJsonPointer as et, serializeState as f, rgaCompactTombstones as ft, validateSerializedState as g, rgaInsertAfterChecked as gt, validateSerializedDoc as h, rgaInsertAfter as ht, mergeDoc as i, newObj as it, forkState as j, observedVersionVector as jt, applyPatchInPlace as k, intersectVersionVectors as kt, deserializeDoc as l, objSet as lt, tryDeserializeState as m, rgaIdAtIndex as mt, compactStateTombstones as n, JsonValueValidationError as nt, tryMergeDoc as o, newSeq as ot, tryDeserializeDoc as p, rgaDelete as pt, PatchCompileError as q, MergeError as r, lwwSet as rt, tryMergeState as s, objCompactTombstones as st, compactDocTombstones as t, ROOT_KEY as tt, deserializeState as u, materialize as ut, withLegacyMissingArrayParents as v, rgaPrevForInsertAtIndex as vt, diffNormalizedJsonPatch as w, ClockValidationError as wt, applySafePatch as x, dotToElemId as xt, withStrictRfc6902Parents as y, validateRgaSeq as yt, crdtNodesToJsonPatch as z };