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,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 {
@@ -28,6 +154,7 @@ function toDepthApplyError(error) {
28
154
  //#endregion
29
155
  //#region src/version-vector.ts
30
156
  let observedVersionVectorObserverForTests = null;
157
+ const observedVersionVectorCache = /* @__PURE__ */ new WeakMap();
31
158
  function readVersionVectorCounter(vv, actor) {
32
159
  if (!Object.prototype.hasOwnProperty.call(vv, actor)) return;
33
160
  const counter = vv[actor];
@@ -44,6 +171,21 @@ function writeVersionVectorCounter(vv, actor, counter) {
44
171
  function observeVersionVectorDot(vv, dot) {
45
172
  if ((readVersionVectorCounter(vv, dot.actor) ?? 0) < dot.ctr) writeVersionVectorCounter(vv, dot.actor, dot.ctr);
46
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
+ }
47
189
  /**
48
190
  * Inspect a document or state and return the highest observed counter per actor.
49
191
  *
@@ -52,13 +194,15 @@ function observeVersionVectorDot(vv, dot) {
52
194
  * of the currently materialized document tree.
53
195
  */
54
196
  function observedVersionVector(target) {
55
- observedVersionVectorObserverForTests?.(target);
56
197
  const doc = "doc" in target ? target.doc : target;
57
- const vv = Object.create(null);
198
+ const cached = readCachedObservedVersionVector(doc);
199
+ const vv = cached ?? Object.create(null);
58
200
  if ("clock" in target) observeVersionVectorDot(vv, {
59
201
  actor: target.clock.actor,
60
202
  ctr: target.clock.ctr
61
203
  });
204
+ if (cached) return vv;
205
+ observedVersionVectorObserverForTests?.(target);
62
206
  const stack = [{
63
207
  node: doc.root,
64
208
  depth: 0
@@ -90,6 +234,7 @@ function observedVersionVector(target) {
90
234
  });
91
235
  }
92
236
  }
237
+ writeCachedObservedVersionVector(doc, vv);
93
238
  return vv;
94
239
  }
95
240
  /** Combine version vectors using per-actor maxima. */
@@ -933,11 +1078,13 @@ function getAtJson(base, path) {
933
1078
  */
934
1079
  function compileJsonPatchToIntent(baseJson, patch, options = {}) {
935
1080
  const internalOptions = options;
1081
+ const budgetMeter = internalOptions.budgetMeter ?? createBudgetMeter(options.resourceBudget);
936
1082
  const semantics = options.semantics ?? "sequential";
937
1083
  const opIndexOffset = internalOptions.opIndexOffset ?? 0;
938
1084
  let workingBase = baseJson;
939
1085
  const pointerCache = internalOptions.pointerCache ?? /* @__PURE__ */ new Map();
940
1086
  const intents = [];
1087
+ budgetMeter?.count("patchOperations", patch.length);
941
1088
  for (let opIndex = 0; opIndex < patch.length; opIndex++) {
942
1089
  const op = patch[opIndex];
943
1090
  const absoluteOpIndex = opIndex + opIndexOffset;
@@ -952,14 +1099,16 @@ function compileJsonPatchOpToIntent(baseJson, op, options = {}) {
952
1099
  const internalOptions = options;
953
1100
  const semantics = options.semantics ?? "sequential";
954
1101
  const pointerCache = internalOptions.pointerCache ?? /* @__PURE__ */ new Map();
955
- 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);
956
1105
  }
957
1106
  /**
958
1107
  * Compute a JSON Patch delta between two JSON values.
959
1108
  * By default arrays use a deterministic LCS strategy.
960
1109
  * Pass `{ arrayStrategy: "atomic" }` for single-op array replacement.
961
1110
  * Pass `{ arrayStrategy: "lcs-linear" }` for a lower-memory LCS variant.
962
- * Use `lcsLinearMaxCells` to optionally cap worst-case `lcs-linear` work and
1111
+ * Use `lcsLinearMaxCells` to cap worst-case `lcs-linear` work and
963
1112
  * fall back to an atomic array replacement for very large unmatched windows.
964
1113
  * Pass `{ emitMoves: true }` or `{ emitCopies: true }` to opt into RFC 6902
965
1114
  * move/copy emission when a deterministic rewrite is available.
@@ -969,20 +1118,24 @@ function compileJsonPatchOpToIntent(baseJson, op, options = {}) {
969
1118
  * @returns An array of JSON Patch operations that transform `base` into `next`.
970
1119
  */
971
1120
  function diffJsonPatch(base, next, options = {}) {
1121
+ const budgetMeter = createBudgetMeter(options.resourceBudget);
972
1122
  const runtimeMode = options.jsonValidation ?? "none";
973
1123
  const runtimeBase = coerceRuntimeJsonValue(base, runtimeMode);
974
1124
  const runtimeNext = coerceRuntimeJsonValue(next, runtimeMode);
975
1125
  const ops = [];
976
- diffValue([], runtimeBase, runtimeNext, ops, options);
1126
+ const path = [];
1127
+ throwIfAborted(options.signal);
1128
+ diffValue(path, runtimeBase, runtimeNext, ops, options, budgetMeter);
977
1129
  return ops;
978
1130
  }
979
- function diffValue(path, base, next, ops, options) {
1131
+ function diffValue(path, base, next, ops, options, budgetMeter) {
980
1132
  const stack = [{
981
1133
  kind: "value",
982
1134
  base,
983
1135
  next
984
1136
  }];
985
1137
  while (stack.length > 0) {
1138
+ throwIfAborted(options.signal);
986
1139
  const frame = stack.pop();
987
1140
  if (frame.kind === "path-pop") {
988
1141
  path.pop();
@@ -1008,6 +1161,7 @@ function diffValue(path, base, next, ops, options) {
1008
1161
  continue;
1009
1162
  }
1010
1163
  assertTraversalDepth(path.length);
1164
+ budgetMeter?.count("visitedNodes", 1, stringifyJsonPointer(path));
1011
1165
  if (frame.base === frame.next) continue;
1012
1166
  const baseIsArray = Array.isArray(frame.base);
1013
1167
  const nextIsArray = Array.isArray(frame.next);
@@ -1023,7 +1177,7 @@ function diffValue(path, base, next, ops, options) {
1023
1177
  if (jsonEquals(frame.base, frame.next)) continue;
1024
1178
  const arrayStrategy = options.arrayStrategy ?? "lcs";
1025
1179
  if (arrayStrategy === "lcs") {
1026
- if (!diffArrayWithLcsMatrix(path, frame.base, frame.next, ops, options)) ops.push({
1180
+ if (!diffArrayWithLcsMatrix(path, frame.base, frame.next, ops, options, budgetMeter)) ops.push({
1027
1181
  op: "replace",
1028
1182
  path: stringifyJsonPointer(path),
1029
1183
  value: frame.next
@@ -1031,7 +1185,7 @@ function diffValue(path, base, next, ops, options) {
1031
1185
  continue;
1032
1186
  }
1033
1187
  if (arrayStrategy === "lcs-linear") {
1034
- if (!diffArrayWithLinearLcs(path, frame.base, frame.next, ops, options)) ops.push({
1188
+ if (!diffArrayWithLinearLcs(path, frame.base, frame.next, ops, options, budgetMeter)) ops.push({
1035
1189
  op: "replace",
1036
1190
  path: stringifyJsonPointer(path),
1037
1191
  value: frame.next
@@ -1056,6 +1210,7 @@ function diffValue(path, base, next, ops, options) {
1056
1210
  continue;
1057
1211
  }
1058
1212
  const { sharedKeys, baseOnlyKeys, nextOnlyKeys } = collectObjectKeys(frame.base, frame.next);
1213
+ budgetMeter?.count("objectEntries", sharedKeys.length + baseOnlyKeys.length + nextOnlyKeys.length, stringifyJsonPointer(path));
1059
1214
  if (!(baseOnlyKeys.length > 0 || nextOnlyKeys.length > 0) && (path.length === 0 || sharedKeys.length > 1) && jsonEquals(frame.base, frame.next)) continue;
1060
1215
  emitObjectStructuralOps(path, frame.base, frame.next, sharedKeys, baseOnlyKeys, nextOnlyKeys, ops, options);
1061
1216
  if (sharedKeys.length > 0) stack.push({
@@ -1219,34 +1374,44 @@ function insertSortedKey(keys, key) {
1219
1374
  }
1220
1375
  keys.splice(low, 0, key);
1221
1376
  }
1222
- function diffArrayWithLcsMatrix(path, base, next, ops, options) {
1223
- const window = trimEqualArrayEdges(base, next);
1377
+ function diffArrayWithLcsMatrix(path, base, next, ops, options, budgetMeter) {
1378
+ const window = trimEqualArrayEdges(base, next, options);
1224
1379
  const baseStart = window.baseStart;
1225
1380
  const nextStart = window.nextStart;
1226
1381
  const n = window.unmatchedBaseLength;
1227
1382
  const m = window.unmatchedNextLength;
1383
+ budgetMeter?.count("sequenceElements", n + m, stringifyJsonPointer(path));
1228
1384
  if (!shouldUseLcsDiff(n, m, options.lcsMaxCells)) return false;
1385
+ budgetMeter?.count("arrayDiffCells", (n + 1) * (m + 1), stringifyJsonPointer(path));
1229
1386
  if (n === 0 && m === 0) return true;
1230
1387
  const steps = [];
1231
- buildArrayEditScriptWithMatrix(base, baseStart, baseStart + n, next, nextStart, nextStart + m, steps);
1388
+ buildArrayEditScriptWithMatrix(base, baseStart, baseStart + n, next, nextStart, nextStart + m, steps, options);
1232
1389
  pushArrayPatchOps(path, window.prefixLength, steps, ops, base, options);
1233
1390
  return true;
1234
1391
  }
1235
- function diffArrayWithLinearLcs(path, base, next, ops, options) {
1236
- 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));
1237
1395
  if (!shouldUseLinearLcsDiff(window.unmatchedBaseLength, window.unmatchedNextLength, options)) return false;
1396
+ budgetMeter?.count("arrayDiffCells", (window.unmatchedBaseLength + 1) * (window.unmatchedNextLength + 1), stringifyJsonPointer(path));
1238
1397
  const steps = [];
1239
- 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);
1240
1399
  pushArrayPatchOps(path, window.prefixLength, steps, ops, base, options);
1241
1400
  return true;
1242
1401
  }
1243
- function trimEqualArrayEdges(base, next) {
1402
+ function trimEqualArrayEdges(base, next, options) {
1244
1403
  const baseLength = base.length;
1245
1404
  const nextLength = next.length;
1246
1405
  let prefixLength = 0;
1247
- 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
+ }
1248
1410
  let suffixLength = 0;
1249
- 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
+ }
1250
1415
  return {
1251
1416
  baseStart: prefixLength,
1252
1417
  nextStart: prefixLength,
@@ -1255,7 +1420,8 @@ function trimEqualArrayEdges(base, next) {
1255
1420
  unmatchedNextLength: nextLength - prefixLength - suffixLength
1256
1421
  };
1257
1422
  }
1258
- function buildArrayEditScriptLinearSpace(base, baseStart, baseEnd, next, nextStart, nextEnd, steps) {
1423
+ function buildArrayEditScriptLinearSpace(base, baseStart, baseEnd, next, nextStart, nextEnd, steps, options) {
1424
+ throwIfAborted(options.signal);
1259
1425
  const unmatchedBaseLength = baseEnd - baseStart;
1260
1426
  const unmatchedNextLength = nextEnd - nextStart;
1261
1427
  if (unmatchedBaseLength === 0) {
@@ -1270,20 +1436,20 @@ function buildArrayEditScriptLinearSpace(base, baseStart, baseEnd, next, nextSta
1270
1436
  return;
1271
1437
  }
1272
1438
  if (unmatchedBaseLength === 1) {
1273
- pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, steps);
1439
+ pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, steps, options);
1274
1440
  return;
1275
1441
  }
1276
1442
  if (unmatchedNextLength === 1) {
1277
- pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, steps);
1443
+ pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, steps, options);
1278
1444
  return;
1279
1445
  }
1280
1446
  if (shouldUseMatrixBaseCase(unmatchedBaseLength, unmatchedNextLength)) {
1281
- buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStart, nextEnd, steps);
1447
+ buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStart, nextEnd, steps, options);
1282
1448
  return;
1283
1449
  }
1284
1450
  const baseMid = baseStart + Math.floor(unmatchedBaseLength / 2);
1285
- const forwardScores = computeLcsPrefixLengths(base, baseStart, baseMid, next, nextStart, nextEnd);
1286
- 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);
1287
1453
  let bestOffset = 0;
1288
1454
  let bestScore = Number.NEGATIVE_INFINITY;
1289
1455
  for (let offset = 0; offset <= unmatchedNextLength; offset++) {
@@ -1294,11 +1460,11 @@ function buildArrayEditScriptLinearSpace(base, baseStart, baseEnd, next, nextSta
1294
1460
  }
1295
1461
  }
1296
1462
  const nextMid = nextStart + bestOffset;
1297
- buildArrayEditScriptLinearSpace(base, baseStart, baseMid, next, nextStart, nextMid, steps);
1298
- 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);
1299
1465
  }
1300
- function pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, steps) {
1301
- 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);
1302
1468
  if (matchIndex === -1) {
1303
1469
  steps.push({ kind: "remove" });
1304
1470
  for (let nextIndex = nextStart; nextIndex < nextEnd; nextIndex++) steps.push({
@@ -1317,8 +1483,8 @@ function pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, s
1317
1483
  value: next[nextIndex]
1318
1484
  });
1319
1485
  }
1320
- function pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, steps) {
1321
- 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);
1322
1488
  if (matchIndex === -1) {
1323
1489
  for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) steps.push({ kind: "remove" });
1324
1490
  steps.push({
@@ -1331,26 +1497,36 @@ function pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, s
1331
1497
  steps.push({ kind: "equal" });
1332
1498
  for (let baseIndex = matchIndex + 1; baseIndex < baseEnd; baseIndex++) steps.push({ kind: "remove" });
1333
1499
  }
1334
- function findFirstMatchingIndexInNext(target, next, nextStart, nextEnd) {
1335
- 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
+ }
1336
1505
  return -1;
1337
1506
  }
1338
- function findFirstMatchingIndexInBase(target, base, baseStart, baseEnd) {
1339
- 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
+ }
1340
1512
  return -1;
1341
1513
  }
1342
1514
  function shouldUseMatrixBaseCase(baseLength, nextLength) {
1343
1515
  return (baseLength + 1) * (nextLength + 1) <= LINEAR_LCS_MATRIX_BASE_CASE_MAX_CELLS;
1344
1516
  }
1345
- function buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStart, nextEnd, steps) {
1517
+ function buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStart, nextEnd, steps, options) {
1346
1518
  const unmatchedBaseLength = baseEnd - baseStart;
1347
1519
  const unmatchedNextLength = nextEnd - nextStart;
1348
1520
  const lcs = Array.from({ length: unmatchedBaseLength + 1 }, () => Array(unmatchedNextLength + 1).fill(0));
1349
- 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];
1350
- 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
+ }
1351
1526
  let baseOffset = 0;
1352
1527
  let nextOffset = 0;
1353
1528
  while (baseOffset < unmatchedBaseLength || nextOffset < unmatchedNextLength) {
1529
+ throwIfAborted(options.signal);
1354
1530
  if (baseOffset < unmatchedBaseLength && nextOffset < unmatchedNextLength && jsonEquals(base[baseStart + baseOffset], next[nextStart + nextOffset])) {
1355
1531
  steps.push({ kind: "equal" });
1356
1532
  baseOffset += 1;
@@ -1373,11 +1549,12 @@ function buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStar
1373
1549
  }
1374
1550
  }
1375
1551
  }
1376
- function computeLcsPrefixLengths(base, baseStart, baseEnd, next, nextStart, nextEnd) {
1552
+ function computeLcsPrefixLengths(base, baseStart, baseEnd, next, nextStart, nextEnd, options) {
1377
1553
  const unmatchedNextLength = nextEnd - nextStart;
1378
1554
  let previousRow = new Int32Array(unmatchedNextLength + 1);
1379
1555
  let currentRow = new Int32Array(unmatchedNextLength + 1);
1380
1556
  for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) {
1557
+ throwIfAborted(options.signal);
1381
1558
  for (let nextOffset = 0; nextOffset < unmatchedNextLength; nextOffset++) if (jsonEquals(base[baseIndex], next[nextStart + nextOffset])) currentRow[nextOffset + 1] = previousRow[nextOffset] + 1;
1382
1559
  else currentRow[nextOffset + 1] = Math.max(previousRow[nextOffset + 1], currentRow[nextOffset]);
1383
1560
  const nextPreviousRow = currentRow;
@@ -1387,11 +1564,12 @@ function computeLcsPrefixLengths(base, baseStart, baseEnd, next, nextStart, next
1387
1564
  }
1388
1565
  return previousRow;
1389
1566
  }
1390
- function computeLcsSuffixLengths(base, baseStart, baseEnd, next, nextStart, nextEnd) {
1567
+ function computeLcsSuffixLengths(base, baseStart, baseEnd, next, nextStart, nextEnd, options) {
1391
1568
  const unmatchedNextLength = nextEnd - nextStart;
1392
1569
  let previousRow = new Int32Array(unmatchedNextLength + 1);
1393
1570
  let currentRow = new Int32Array(unmatchedNextLength + 1);
1394
1571
  for (let baseIndex = baseEnd - 1; baseIndex >= baseStart; baseIndex--) {
1572
+ throwIfAborted(options.signal);
1395
1573
  for (let nextOffset = unmatchedNextLength - 1; nextOffset >= 0; nextOffset--) if (jsonEquals(base[baseIndex], next[nextStart + nextOffset])) currentRow[nextOffset] = previousRow[nextOffset + 1] + 1;
1396
1574
  else currentRow[nextOffset] = Math.max(previousRow[nextOffset], currentRow[nextOffset + 1]);
1397
1575
  const nextPreviousRow = currentRow;
@@ -1437,9 +1615,10 @@ function shouldUseLcsDiff(baseLength, nextLength, lcsMaxCells) {
1437
1615
  }
1438
1616
  function shouldUseLinearLcsDiff(baseLength, nextLength, options) {
1439
1617
  const cap = options.lcsLinearMaxCells;
1440
- if (cap === void 0 || cap === Number.POSITIVE_INFINITY) return true;
1441
- if (!Number.isFinite(cap) || cap < 1) return false;
1442
- 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;
1443
1622
  }
1444
1623
  function finalizeArrayOps(arrayPath, base, ops, options) {
1445
1624
  if (ops.length === 0) return [];
@@ -2069,7 +2248,14 @@ function isSamePath(a, b) {
2069
2248
  * @returns A new CRDT `Doc`.
2070
2249
  */
2071
2250
  function docFromJson(value, nextDot) {
2072
- 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;
2073
2259
  }
2074
2260
  /**
2075
2261
  * Legacy helper for tests and fixtures that seeds an entire document from one dot.
@@ -2316,7 +2502,10 @@ function nodeFromJson(value, nextDot) {
2316
2502
  }
2317
2503
  /** Deep-clone a CRDT document. The clone is fully independent of the original. */
2318
2504
  function cloneDoc(doc) {
2319
- 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;
2320
2509
  }
2321
2510
  function cloneNode(node) {
2322
2511
  return cloneNodeAtDepth(node, 0);
@@ -2524,7 +2713,7 @@ function createArrayIndexLookupSession() {
2524
2713
  return created;
2525
2714
  } };
2526
2715
  }
2527
- function applyArrInsert(base, head, it, newDot, indexSession, bumpCounterAbove, strictParents = false) {
2716
+ function applyArrInsert(base, head, it, newDot, indexSession, bumpCounterAbove, strictParents = true) {
2528
2717
  const pointer = `/${it.path.join("/")}`;
2529
2718
  const baseSeq = getSeqAtPath(base, it.path);
2530
2719
  if (!baseSeq) {
@@ -2673,39 +2862,51 @@ function applyArrReplace(base, head, it, newDot, indexSession) {
2673
2862
  * @param evalTestAgainst - Whether `test` ops are evaluated against `"head"` or `"base"`.
2674
2863
  * @param bumpCounterAbove - Optional hook that can fast-forward the underlying counter before inserts.
2675
2864
  * @param options - Optional behavior toggles.
2676
- * @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.
2677
2867
  * @returns `{ ok: true }` on success, or `{ ok: false, code: 409, message }` on conflict.
2678
2868
  */
2679
2869
  function applyIntentsToCrdt(base, head, intents, newDot, evalTestAgainst = "head", bumpCounterAbove, options = {}) {
2680
2870
  const arrayIndexSession = createArrayIndexLookupSession();
2871
+ let pendingObservedDots = [];
2872
+ const observedNewDot = () => {
2873
+ const dot = newDot();
2874
+ pendingObservedDots.push(dot);
2875
+ return dot;
2876
+ };
2681
2877
  for (const it of intents) {
2682
2878
  let fail = null;
2879
+ pendingObservedDots = [];
2683
2880
  switch (it.t) {
2684
2881
  case "Test":
2685
2882
  fail = applyTest(base, head, it, evalTestAgainst);
2686
2883
  break;
2687
2884
  case "ObjSet":
2688
- fail = applyObjSet(head, it, newDot);
2885
+ fail = applyObjSet(head, it, observedNewDot);
2689
2886
  break;
2690
2887
  case "ObjRemove":
2691
- fail = applyObjRemove(head, it, newDot);
2888
+ fail = applyObjRemove(head, it, observedNewDot);
2692
2889
  break;
2693
2890
  case "ArrInsert":
2694
- fail = applyArrInsert(base, head, it, newDot, arrayIndexSession, bumpCounterAbove, options.strictParents ?? false);
2891
+ fail = applyArrInsert(base, head, it, observedNewDot, arrayIndexSession, bumpCounterAbove, options.strictParents ?? true);
2695
2892
  break;
2696
2893
  case "ArrDelete":
2697
- fail = applyArrDelete(base, head, it, newDot, arrayIndexSession);
2894
+ fail = applyArrDelete(base, head, it, observedNewDot, arrayIndexSession);
2698
2895
  break;
2699
2896
  case "ArrReplace":
2700
- fail = applyArrReplace(base, head, it, newDot, arrayIndexSession);
2897
+ fail = applyArrReplace(base, head, it, observedNewDot, arrayIndexSession);
2701
2898
  break;
2702
2899
  default: assertNever(it, "Unhandled intent type");
2703
2900
  }
2704
- if (fail) return fail;
2901
+ if (fail) {
2902
+ pendingObservedDots = [];
2903
+ return fail;
2904
+ }
2905
+ for (const dot of pendingObservedDots) observeDocVersionVectorDot(head, dot);
2705
2906
  }
2706
2907
  return { ok: true };
2707
2908
  }
2708
- function jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove, strictParents = false) {
2909
+ function jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove, strictParents = true) {
2709
2910
  if (isJsonPatchToCrdtOptions(baseOrOptions)) return jsonPatchToCrdtInternal(baseOrOptions);
2710
2911
  if (!head || !patch || !newDot) return {
2711
2912
  ok: false,
@@ -2723,7 +2924,7 @@ function jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst = "
2723
2924
  strictParents
2724
2925
  });
2725
2926
  }
2726
- function jsonPatchToCrdtSafe(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove, strictParents = false) {
2927
+ function jsonPatchToCrdtSafe(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove, strictParents = true) {
2727
2928
  try {
2728
2929
  if (isJsonPatchToCrdtOptions(baseOrOptions)) return jsonPatchToCrdt(baseOrOptions);
2729
2930
  if (!head || !patch || !newDot) return {
@@ -2992,7 +3193,7 @@ function diffNodeToPatch(path, baseNode, headNode, options, ops, depth) {
2992
3193
  * @returns An array of JSON Patch operations that transform base into head.
2993
3194
  */
2994
3195
  function crdtToJsonPatch(base, head, options) {
2995
- 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);
2996
3197
  return crdtNodesToJsonPatch(base.root, head.root, options);
2997
3198
  }
2998
3199
  /** Internals-only helper for diffing CRDT nodes from an existing traversal depth. */
@@ -3014,11 +3215,21 @@ function crdtToFullReplace(doc) {
3014
3215
  }
3015
3216
  function jsonPatchToCrdtInternal(options) {
3016
3217
  const evalTestAgainst = options.evalTestAgainst ?? "head";
3017
- 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") {
3018
3226
  const baseJson = materialize(options.base.root);
3019
3227
  let intents;
3020
3228
  try {
3021
- intents = compileJsonPatchToIntent(baseJson, options.patch, { semantics: "base" });
3229
+ intents = compileJsonPatchToIntent(baseJson, options.patch, {
3230
+ semantics: "base",
3231
+ resourceBudget: options.resourceBudget
3232
+ });
3022
3233
  } catch (error) {
3023
3234
  return toApplyError$1(error);
3024
3235
  }
@@ -3403,6 +3614,9 @@ function assertNever(_value, message) {
3403
3614
  var PatchError = class extends Error {
3404
3615
  code;
3405
3616
  reason;
3617
+ budget;
3618
+ limit;
3619
+ actual;
3406
3620
  path;
3407
3621
  opIndex;
3408
3622
  constructor(errorOrMessage, code = 409, reason = "INVALID_PATCH") {
@@ -3415,6 +3629,11 @@ var PatchError = class extends Error {
3415
3629
  }
3416
3630
  this.code = errorOrMessage.code;
3417
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
+ }
3418
3637
  this.path = errorOrMessage.path;
3419
3638
  this.opIndex = errorOrMessage.opIndex;
3420
3639
  }
@@ -3479,26 +3698,27 @@ function applyPatchInPlace(state, patch, options = {}) {
3479
3698
  }
3480
3699
  /** Non-throwing immutable patch application variant. */
3481
3700
  function tryApplyPatch(state, patch, options = {}) {
3482
- const nextState = {
3483
- doc: cloneDoc(state.doc),
3484
- clock: cloneClock(state.clock)
3485
- };
3486
3701
  try {
3702
+ throwIfAborted(options.signal);
3703
+ const nextState = {
3704
+ doc: cloneDoc(state.doc),
3705
+ clock: cloneClock(state.clock)
3706
+ };
3487
3707
  const result = applyPatchInternal(nextState, patch, options, "batch");
3488
3708
  if (!result.ok) return {
3489
3709
  ok: false,
3490
3710
  error: result
3491
3711
  };
3712
+ return {
3713
+ ok: true,
3714
+ state: nextState
3715
+ };
3492
3716
  } catch (error) {
3493
3717
  return {
3494
3718
  ok: false,
3495
3719
  error: toApplyError(error)
3496
3720
  };
3497
3721
  }
3498
- return {
3499
- ok: true,
3500
- state: nextState
3501
- };
3502
3722
  }
3503
3723
  /** Non-throwing in-place patch application variant. */
3504
3724
  function tryApplyPatchInPlace(state, patch, options = {}) {
@@ -3578,6 +3798,7 @@ function toApplyPatchOptionsForActor(options) {
3578
3798
  testAgainst: options.testAgainst,
3579
3799
  strictParents: options.strictParents,
3580
3800
  jsonValidation: options.jsonValidation,
3801
+ signal: options.signal,
3581
3802
  base: options.base ? {
3582
3803
  doc: options.base,
3583
3804
  clock: createClock("__base__", 0)
@@ -3585,8 +3806,10 @@ function toApplyPatchOptionsForActor(options) {
3585
3806
  };
3586
3807
  }
3587
3808
  function applyPatchInternal(state, patch, options, _execution) {
3809
+ throwIfAborted(options.signal);
3588
3810
  const preparedPatch = preparePatchPayloadsSafe(patch, options.jsonValidation ?? "none");
3589
3811
  if (!preparedPatch.ok) return preparedPatch;
3812
+ createBudgetMeter(options.resourceBudget)?.count("patchOperations", preparedPatch.patch.length);
3590
3813
  const runtimePatch = preparedPatch.patch;
3591
3814
  if ((options.semantics ?? "sequential") === "sequential") {
3592
3815
  const explicitBaseState = options.base ? {
@@ -3595,6 +3818,7 @@ function applyPatchInternal(state, patch, options, _execution) {
3595
3818
  } : null;
3596
3819
  const session = { pointerCache: /* @__PURE__ */ new Map() };
3597
3820
  for (const [opIndex, op] of runtimePatch.entries()) {
3821
+ throwIfAborted(options.signal);
3598
3822
  const step = applyPatchOpSequential(state, op, options, explicitBaseState ? explicitBaseState.doc : state.doc, explicitBaseState, opIndex, session);
3599
3823
  if (!step.ok) return step;
3600
3824
  }
@@ -3933,9 +4157,9 @@ function validateArrayIndexBounds(index, op, arrLength, path, opIndex) {
3933
4157
  function bumpClockCounter(state, ctr) {
3934
4158
  if (state.clock.ctr < ctr) state.clock.ctr = ctr;
3935
4159
  }
3936
- function compilePreparedIntents(baseJson, patch, semantics = "sequential", pointerCache, opIndexOffset = 0) {
4160
+ function compilePreparedIntents(baseJson, patch, semantics = "sequential", pointerCache, opIndexOffset = 0, budgetMeter) {
3937
4161
  try {
3938
- const compileOptions = toCompilePatchOptions(semantics, pointerCache, opIndexOffset);
4162
+ const compileOptions = toCompilePatchOptions(semantics, pointerCache, opIndexOffset, budgetMeter);
3939
4163
  if (patch.length === 1) return {
3940
4164
  ok: true,
3941
4165
  intents: compileJsonPatchOpToIntent(baseJson, patch[0], compileOptions)
@@ -3948,11 +4172,13 @@ function compilePreparedIntents(baseJson, patch, semantics = "sequential", point
3948
4172
  return toApplyError(error);
3949
4173
  }
3950
4174
  }
3951
- function toCompilePatchOptions(semantics, pointerCache, opIndexOffset = 0) {
4175
+ function toCompilePatchOptions(semantics, pointerCache, opIndexOffset = 0, budgetMeter) {
3952
4176
  return {
3953
4177
  semantics,
4178
+ resourceBudget: void 0,
3954
4179
  pointerCache,
3955
- opIndexOffset
4180
+ opIndexOffset,
4181
+ budgetMeter
3956
4182
  };
3957
4183
  }
3958
4184
  function preparePatchPayloadsSafe(patch, mode) {
@@ -4000,6 +4226,8 @@ function mergePointerPaths(basePointer, nestedPointer) {
4000
4226
  return `${basePointer}${nestedPointer}`;
4001
4227
  }
4002
4228
  function toApplyError(error) {
4229
+ if (error instanceof OperationCancelledError) return toCancellationApplyError(error);
4230
+ if (error instanceof ResourceBudgetError) return toBudgetApplyError(error);
4003
4231
  if (error instanceof TraversalDepthError) return toDepthApplyError(error);
4004
4232
  if (error instanceof PatchCompileError) return {
4005
4233
  ok: false,
@@ -4034,6 +4262,68 @@ function toPointerParseApplyError(error, pointer, opIndex) {
4034
4262
  };
4035
4263
  }
4036
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
+
4037
4327
  //#endregion
4038
4328
  //#region src/serialize.ts
4039
4329
  const HEAD_ELEM_ID = "HEAD";
@@ -4069,17 +4359,24 @@ function serializeDoc(doc) {
4069
4359
  };
4070
4360
  }
4071
4361
  /** Reconstruct a CRDT document from its serialized form. */
4072
- 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);
4073
4367
  const raw = readSerializedDocEnvelope(data);
4074
4368
  if (!("root" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/root", "serialized doc is missing root");
4075
- 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;
4076
4373
  }
4077
4374
  /** Non-throwing `deserializeDoc` variant with typed validation details. */
4078
- function tryDeserializeDoc(data) {
4375
+ function tryDeserializeDoc(data, options = {}) {
4079
4376
  try {
4080
4377
  return {
4081
4378
  ok: true,
4082
- doc: deserializeDoc(data)
4379
+ doc: deserializeDoc(data, options)
4083
4380
  };
4084
4381
  } catch (error) {
4085
4382
  const deserializeError = toDeserializeFailure(error);
@@ -4090,6 +4387,20 @@ function tryDeserializeDoc(data) {
4090
4387
  throw error;
4091
4388
  }
4092
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
+ }
4093
4404
  /** Serialize a full CRDT state (document + clock) to a JSON-safe representation. */
4094
4405
  function serializeState(state) {
4095
4406
  return {
@@ -4107,27 +4418,56 @@ function serializeState(state) {
4107
4418
  * May throw `TraversalDepthError` when the payload exceeds the maximum
4108
4419
  * supported nesting depth.
4109
4420
  */
4110
- function deserializeState(data) {
4111
- const raw = readSerializedStateEnvelope(data);
4112
- if (!("doc" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/doc", "serialized state is missing doc");
4113
- if (!("clock" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/clock", "serialized state is missing clock");
4114
- const clockRaw = asRecord(raw.clock, "/clock");
4115
- const actor = readActor(clockRaw.actor, "/clock/actor");
4116
- const ctr = readCounter(clockRaw.ctr, "/clock/ctr");
4117
- const doc = deserializeDoc(raw.doc);
4118
- const observedCtr = observedVersionVector(doc)[actor] ?? 0;
4119
- return {
4120
- doc,
4121
- clock: createClock(actor, Math.max(ctr, observedCtr))
4122
- };
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
+ }
4123
4441
  }
4124
4442
  /** Non-throwing `deserializeState` variant with typed validation details. */
4125
- function tryDeserializeState(data) {
4443
+ function tryDeserializeState(data, options = {}) {
4126
4444
  try {
4127
4445
  return {
4128
4446
  ok: true,
4129
- state: deserializeState(data)
4447
+ state: deserializeState(data, options)
4448
+ };
4449
+ } catch (error) {
4450
+ const deserializeError = toDeserializeFailure(error);
4451
+ if (deserializeError) return {
4452
+ ok: false,
4453
+ error: deserializeError
4130
4454
  };
4455
+ throw error;
4456
+ }
4457
+ }
4458
+ /** Validate a serialized CRDT state without throwing or returning runtime state. */
4459
+ function validateSerializedState(data, options = {}) {
4460
+ try {
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 };
4131
4471
  } catch (error) {
4132
4472
  const deserializeError = toDeserializeFailure(error);
4133
4473
  if (deserializeError) return {
@@ -4199,33 +4539,55 @@ function readSerializedStateEnvelope(data) {
4199
4539
  assertSerializedEnvelopeVersion(raw, "/version", SERIALIZED_STATE_VERSION, "state");
4200
4540
  return raw;
4201
4541
  }
4202
- 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);
4203
4550
  assertTraversalDepth(depth);
4551
+ budgetMeter?.count("visitedNodes", 1, path);
4204
4552
  const raw = asRecord(node, path);
4205
4553
  const kind = readString(raw.kind, `${path}/kind`);
4206
4554
  if (kind === "lww") {
4207
4555
  if (!("value" in raw)) fail("INVALID_SERIALIZED_SHAPE", `${path}/value`, "lww node is missing value");
4208
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);
4209
4559
  return {
4210
4560
  kind: "lww",
4211
- value: structuredClone(readJsonValue(raw.value, `${path}/value`, depth + 1)),
4212
- dot: readDot(raw.dot, `${path}/dot`)
4561
+ value: structuredClone(readJsonValue(raw.value, `${path}/value`, depth + 1, budgetMeter, signal)),
4562
+ dot
4213
4563
  };
4214
4564
  }
4215
4565
  if (kind === "obj") {
4216
4566
  const entriesRaw = asRecord(raw.entries, `${path}/entries`);
4217
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`);
4218
4572
  const entries = /* @__PURE__ */ new Map();
4219
4573
  for (const [k, v] of Object.entries(entriesRaw)) {
4574
+ throwIfAborted(signal);
4220
4575
  const entryPath = `${path}/entries/${k}`;
4221
4576
  const entryRaw = asRecord(v, entryPath);
4577
+ const dot = readDot(entryRaw.dot, `${entryPath}/dot`);
4578
+ if (observed) observeVersionVectorDot(observed, dot);
4222
4579
  entries.set(k, {
4223
- node: deserializeNode(entryRaw.node, `${entryPath}/node`, depth + 1),
4224
- dot: readDot(entryRaw.dot, `${entryPath}/dot`)
4580
+ node: deserializeNode(entryRaw.node, `${entryPath}/node`, depth + 1, budgetMeter, observed, signal),
4581
+ dot
4225
4582
  });
4226
4583
  }
4227
4584
  const tombstone = /* @__PURE__ */ new Map();
4228
- 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
+ }
4229
4591
  return {
4230
4592
  kind: "obj",
4231
4593
  entries,
@@ -4234,17 +4596,24 @@ function deserializeNode(node, path, depth) {
4234
4596
  }
4235
4597
  if (kind !== "seq") fail("INVALID_SERIALIZED_SHAPE", `${path}/kind`, `unsupported node kind '${kind}'`);
4236
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`);
4237
4601
  const elems = /* @__PURE__ */ new Map();
4238
4602
  for (const [id, rawElem] of Object.entries(elemsRaw)) {
4603
+ throwIfAborted(signal);
4239
4604
  const elemPath = `${path}/elems/${id}`;
4240
4605
  const elem = asRecord(rawElem, elemPath);
4241
4606
  const elemId = readString(elem.id, `${elemPath}/id`);
4242
4607
  if (elemId !== id) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/id`, `sequence element id '${elemId}' does not match key '${id}'`);
4243
4608
  const prev = readString(elem.prev, `${elemPath}/prev`);
4244
4609
  const tombstone = readBoolean(elem.tombstone, `${elemPath}/tombstone`);
4245
- const value = deserializeNode(elem.value, `${elemPath}/value`, depth + 1);
4610
+ const value = deserializeNode(elem.value, `${elemPath}/value`, depth + 1, budgetMeter, observed, signal);
4246
4611
  const insDot = readDot(elem.insDot, `${elemPath}/insDot`);
4247
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
+ }
4248
4617
  if (dotToElemId(insDot) !== id) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/insDot`, "sequence element id must match its insertion dot");
4249
4618
  if (!tombstone && delDot) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/delDot`, "live sequence elements must not include delete metadata");
4250
4619
  elems.set(id, {
@@ -4257,6 +4626,7 @@ function deserializeNode(node, path, depth) {
4257
4626
  });
4258
4627
  }
4259
4628
  for (const elem of elems.values()) {
4629
+ throwIfAborted(signal);
4260
4630
  if (elem.prev === elem.id) fail("INVALID_SERIALIZED_INVARIANT", `${path}/elems/${elem.id}/prev`, "sequence element cannot reference itself as predecessor");
4261
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`);
4262
4632
  }
@@ -4266,6 +4636,66 @@ function deserializeNode(node, path, depth) {
4266
4636
  elems
4267
4637
  };
4268
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
+ }
4269
4699
  function assertAcyclicRgaPredecessors(elems, path) {
4270
4700
  const visitState = /* @__PURE__ */ new Map();
4271
4701
  for (const startId of elems.keys()) {
@@ -4286,6 +4716,26 @@ function assertAcyclicRgaPredecessors(elems, path) {
4286
4716
  for (const id of trail) visitState.set(id, 2);
4287
4717
  }
4288
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
+ }
4289
4739
  function asRecord(value, path) {
4290
4740
  if (!isRecord(value)) fail("INVALID_SERIALIZED_SHAPE", path, "expected object");
4291
4741
  return value;
@@ -4323,28 +4773,43 @@ function readBoolean(value, path) {
4323
4773
  if (typeof value !== "boolean") fail("INVALID_SERIALIZED_SHAPE", path, "expected boolean");
4324
4774
  return value;
4325
4775
  }
4326
- function readJsonValue(value, path, depth) {
4327
- assertJsonValue(value, path, depth);
4776
+ function readJsonValue(value, path, depth, budgetMeter, signal) {
4777
+ assertJsonValue(value, path, depth, budgetMeter, signal);
4328
4778
  return value;
4329
4779
  }
4330
- function assertJsonValue(value, path, depth) {
4780
+ function assertJsonValue(value, path, depth, budgetMeter, signal) {
4781
+ throwIfAborted(signal);
4331
4782
  assertTraversalDepth(depth);
4783
+ budgetMeter?.count("visitedNodes", 1, path);
4332
4784
  if (value === null || typeof value === "string" || typeof value === "boolean") return;
4333
4785
  if (typeof value === "number") {
4334
4786
  if (!Number.isFinite(value)) fail("INVALID_SERIALIZED_SHAPE", path, "json number must be finite");
4335
4787
  return;
4336
4788
  }
4337
4789
  if (Array.isArray(value)) {
4338
- 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
+ }
4339
4796
  return;
4340
4797
  }
4341
4798
  if (!isRecord(value)) fail("INVALID_SERIALIZED_SHAPE", path, "expected JSON value");
4342
- 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
+ }
4343
4806
  }
4344
4807
  function fail(reason, path, message) {
4345
4808
  throw new DeserializeError(reason, path, message);
4346
4809
  }
4347
4810
  function toDeserializeFailure(error) {
4811
+ if (error instanceof ResourceBudgetError) return toBudgetDeserializeFailure(error);
4812
+ if (error instanceof OperationCancelledError) return toCancellationDeserializeFailure(error);
4348
4813
  if (error instanceof DeserializeError || error instanceof TraversalDepthError) return error;
4349
4814
  return null;
4350
4815
  }
@@ -4366,12 +4831,20 @@ var SharedElementMetadataMismatchError = class extends Error {
4366
4831
  var MergeError = class extends Error {
4367
4832
  code;
4368
4833
  reason;
4834
+ budget;
4835
+ limit;
4836
+ actual;
4369
4837
  path;
4370
4838
  constructor(error) {
4371
4839
  super(error.message);
4372
4840
  this.name = "MergeError";
4373
4841
  this.code = error.code;
4374
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
+ }
4375
4848
  this.path = error.path;
4376
4849
  }
4377
4850
  };
@@ -4396,9 +4869,13 @@ function mergeDoc(a, b, options = {}) {
4396
4869
  /** Non-throwing `mergeDoc` variant with structured conflict details. */
4397
4870
  function tryMergeDoc(a, b, options = {}) {
4398
4871
  try {
4399
- const config = { unrelatedArrays: resolveUnrelatedArraysStrategy(options) };
4872
+ const config = {
4873
+ unrelatedArrays: resolveUnrelatedArraysStrategy(options),
4874
+ budgetMeter: createBudgetMeter(options.resourceBudget),
4875
+ signal: options.signal
4876
+ };
4400
4877
  if (config.unrelatedArrays === "reject") {
4401
- const mismatchPath = findSeqLineageMismatch(a.root, b.root, []);
4878
+ const mismatchPath = findSeqLineageMismatch(a.root, b.root, [], config);
4402
4879
  if (mismatchPath !== null) return {
4403
4880
  ok: false,
4404
4881
  error: {
@@ -4429,6 +4906,14 @@ function tryMergeDoc(a, b, options = {}) {
4429
4906
  ok: false,
4430
4907
  error: toDepthApplyError(error)
4431
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
+ };
4432
4917
  throw error;
4433
4918
  }
4434
4919
  }
@@ -4454,10 +4939,12 @@ function tryMergeState(a, b, options = {}) {
4454
4939
  const actor = options.actor ?? a.clock.actor;
4455
4940
  const config = {
4456
4941
  actor,
4457
- unrelatedArrays: resolveUnrelatedArraysStrategy(options)
4942
+ unrelatedArrays: resolveUnrelatedArraysStrategy(options),
4943
+ budgetMeter: createBudgetMeter(options.resourceBudget),
4944
+ signal: options.signal
4458
4945
  };
4459
4946
  if (config.unrelatedArrays === "reject") {
4460
- const mismatchPath = findSeqLineageMismatch(a.doc.root, b.doc.root, []);
4947
+ const mismatchPath = findSeqLineageMismatch(a.doc.root, b.doc.root, [], config);
4461
4948
  if (mismatchPath !== null) return {
4462
4949
  ok: false,
4463
4950
  error: {
@@ -4493,20 +4980,34 @@ function tryMergeState(a, b, options = {}) {
4493
4980
  ok: false,
4494
4981
  error: toDepthApplyError(error)
4495
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
+ };
4496
4991
  throw error;
4497
4992
  }
4498
4993
  }
4499
- function findSeqLineageMismatch(a, b, path) {
4994
+ function findSeqLineageMismatch(a, b, path, config) {
4995
+ const pathBuffer = [...path];
4500
4996
  const stack = [{
4501
4997
  a,
4502
4998
  b,
4503
- path,
4504
4999
  depth: path.length
4505
5000
  }];
4506
5001
  while (stack.length > 0) {
5002
+ throwIfAborted(config.signal);
4507
5003
  const frame = stack.pop();
4508
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);
4509
5009
  if (frame.a.kind === "seq" && frame.b.kind === "seq") {
5010
+ config.budgetMeter?.count("sequenceElements", frame.a.elems.size + frame.b.elems.size, budgetPath);
4510
5011
  const hasElemsA = frame.a.elems.size > 0;
4511
5012
  const hasElemsB = frame.b.elems.size > 0;
4512
5013
  if (hasElemsA && hasElemsB) {
@@ -4515,22 +5016,28 @@ function findSeqLineageMismatch(a, b, path) {
4515
5016
  shared = true;
4516
5017
  break;
4517
5018
  }
4518
- if (!shared) return stringifyJsonPointer(frame.path);
5019
+ if (!shared) return stringifyJsonPointer(pathBuffer);
4519
5020
  }
4520
5021
  }
4521
5022
  if (frame.a.kind === "obj" && frame.b.kind === "obj") {
4522
5023
  const left = frame.a;
4523
5024
  const right = frame.b;
4524
- const sharedKeys = [...left.entries.keys()].filter((key) => right.entries.has(key));
4525
- for (let i = sharedKeys.length - 1; i >= 0; i--) {
4526
- 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];
4527
5034
  const nextA = left.entries.get(key).node;
4528
5035
  const nextB = right.entries.get(key).node;
4529
5036
  stack.push({
4530
5037
  a: nextA,
4531
5038
  b: nextB,
4532
- path: [...frame.path, key],
4533
- depth: frame.depth + 1
5039
+ depth: frame.depth + 1,
5040
+ key
4534
5041
  });
4535
5042
  }
4536
5043
  }
@@ -4555,7 +5062,7 @@ function maxObservedCtrForActor(docObservedCtr, actor, a, b) {
4555
5062
  if (b.clock.actor === actor && b.clock.ctr > best) best = b.clock.ctr;
4556
5063
  return best;
4557
5064
  }
4558
- function repDot(node) {
5065
+ function repDot(node, signal) {
4559
5066
  let best = {
4560
5067
  actor: "",
4561
5068
  ctr: 0
@@ -4565,6 +5072,7 @@ function repDot(node) {
4565
5072
  depth: 0
4566
5073
  }];
4567
5074
  while (stack.length > 0) {
5075
+ throwIfAborted(signal);
4568
5076
  const frame = stack.pop();
4569
5077
  assertTraversalDepth(frame.depth);
4570
5078
  switch (frame.node.kind) {
@@ -4596,12 +5104,14 @@ function repDot(node) {
4596
5104
  return best;
4597
5105
  }
4598
5106
  function mergeNodeAtDepth(a, b, depth, path, config) {
5107
+ throwIfAborted(config.signal);
4599
5108
  assertTraversalDepth(depth);
5109
+ config.budgetMeter?.count("visitedNodes", 1, stringifyJsonPointer(path));
4600
5110
  if (a.kind === "lww" && b.kind === "lww") return mergeLww(a, b, config.actor);
4601
5111
  if (a.kind === "obj" && b.kind === "obj") return mergeObj(a, b, depth + 1, path, config);
4602
5112
  if (a.kind === "seq" && b.kind === "seq") return mergeSeq(a, b, depth + 1, path, config);
4603
- if (compareDot(repDot(a), repDot(b)) >= 0) return cloneNodeShallow(a, depth + 1, config.actor);
4604
- return cloneNodeShallow(b, depth + 1, config.actor);
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);
4605
5115
  }
4606
5116
  function mergeLww(a, b, actor) {
4607
5117
  if (compareDot(a.dot, b.dot) >= 0) return {
@@ -4627,7 +5137,9 @@ function mergeObj(a, b, depth, path, config) {
4627
5137
  const tombstone = /* @__PURE__ */ new Map();
4628
5138
  let maxObservedCtr = 0;
4629
5139
  const allTombKeys = new Set([...a.tombstone.keys(), ...b.tombstone.keys()]);
5140
+ config.budgetMeter?.count("objectEntries", allTombKeys.size, stringifyJsonPointer(path));
4630
5141
  for (const key of allTombKeys) {
5142
+ throwIfAborted(config.signal);
4631
5143
  const da = a.tombstone.get(key);
4632
5144
  const db = b.tombstone.get(key);
4633
5145
  if (da && db) {
@@ -4643,13 +5155,22 @@ function mergeObj(a, b, depth, path, config) {
4643
5155
  }
4644
5156
  }
4645
5157
  const allKeys = new Set([...a.entries.keys(), ...b.entries.keys()]);
5158
+ config.budgetMeter?.count("objectEntries", allKeys.size, stringifyJsonPointer(path));
4646
5159
  for (const key of allKeys) {
5160
+ throwIfAborted(config.signal);
4647
5161
  const ea = a.entries.get(key);
4648
5162
  const eb = b.entries.get(key);
4649
5163
  let merged;
4650
5164
  let mergedNodeMaxObservedCtr = 0;
4651
5165
  if (ea && eb) {
4652
- const mergedNode = mergeNodeAtDepth(ea.node, eb.node, depth + 1, [...path, key], config);
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
+ })();
4653
5174
  const dot = compareDot(ea.dot, eb.dot) >= 0 ? { ...ea.dot } : { ...eb.dot };
4654
5175
  merged = {
4655
5176
  node: mergedNode.node,
@@ -4657,14 +5178,14 @@ function mergeObj(a, b, depth, path, config) {
4657
5178
  };
4658
5179
  mergedNodeMaxObservedCtr = mergedNode.maxObservedCtr;
4659
5180
  } else if (ea) {
4660
- const cloned = cloneNodeShallow(ea.node, depth + 1, config.actor);
5181
+ const cloned = cloneNodeShallow(ea.node, depth + 1, config.actor, config.signal);
4661
5182
  merged = {
4662
5183
  node: cloned.node,
4663
5184
  dot: { ...ea.dot }
4664
5185
  };
4665
5186
  mergedNodeMaxObservedCtr = cloned.maxObservedCtr;
4666
5187
  } else {
4667
- const cloned = cloneNodeShallow(eb.node, depth + 1, config.actor);
5188
+ const cloned = cloneNodeShallow(eb.node, depth + 1, config.actor, config.signal);
4668
5189
  merged = {
4669
5190
  node: cloned.node,
4670
5191
  dot: { ...eb.dot }
@@ -4687,24 +5208,40 @@ function mergeObj(a, b, depth, path, config) {
4687
5208
  }
4688
5209
  function mergeSeq(a, b, depth, path, config) {
4689
5210
  assertTraversalDepth(depth);
5211
+ config.budgetMeter?.count("visitedNodes", 1, stringifyJsonPointer(path));
4690
5212
  if (config.unrelatedArrays === "atomic-replace" && a.elems.size > 0 && b.elems.size > 0) {
4691
5213
  let shared = false;
4692
- for (const id of a.elems.keys()) if (b.elems.has(id)) {
4693
- shared = true;
4694
- break;
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);
4695
5224
  }
4696
- if (!shared) return cloneNodeShallow(compareDot(repDot(a), repDot(b)) >= 0 ? a : b, depth, config.actor);
4697
5225
  }
4698
5226
  const elems = /* @__PURE__ */ new Map();
4699
5227
  let maxObservedCtr = 0;
4700
5228
  const allIds = new Set([...a.elems.keys(), ...b.elems.keys()]);
5229
+ config.budgetMeter?.count("sequenceElements", allIds.size, stringifyJsonPointer(path));
4701
5230
  for (const id of allIds) {
5231
+ throwIfAborted(config.signal);
4702
5232
  const ea = a.elems.get(id);
4703
5233
  const eb = b.elems.get(id);
4704
5234
  if (ea && eb) {
4705
5235
  if (ea.prev !== eb.prev) throw new SharedElementMetadataMismatchError(stringifyJsonPointer(path), id, "prev");
4706
5236
  if (!sameDot(ea.insDot, eb.insDot)) throw new SharedElementMetadataMismatchError(stringifyJsonPointer(path), id, "insDot");
4707
- const mergedValue = mergeNodeAtDepth(ea.value, eb.value, depth + 1, [...path, id], config);
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
+ })();
4708
5245
  const mergedDeleteDot = mergeDeleteDot(ea.delDot, eb.delDot);
4709
5246
  elems.set(id, {
4710
5247
  id,
@@ -4716,11 +5253,11 @@ function mergeSeq(a, b, depth, path, config) {
4716
5253
  });
4717
5254
  maxObservedCtr = Math.max(maxObservedCtr, mergedValue.maxObservedCtr, maxObservedCtrForDot(ea.insDot, config.actor), maxObservedCtrForDot(mergedDeleteDot, config.actor));
4718
5255
  } else if (ea) {
4719
- const cloned = cloneElem(ea, depth + 1, config.actor);
5256
+ const cloned = cloneElem(ea, depth + 1, config.actor, config.signal);
4720
5257
  elems.set(id, cloned.elem);
4721
5258
  maxObservedCtr = Math.max(maxObservedCtr, cloned.maxObservedCtr);
4722
5259
  } else {
4723
- const cloned = cloneElem(eb, depth + 1, config.actor);
5260
+ const cloned = cloneElem(eb, depth + 1, config.actor, config.signal);
4724
5261
  elems.set(id, cloned.elem);
4725
5262
  maxObservedCtr = Math.max(maxObservedCtr, cloned.maxObservedCtr);
4726
5263
  }
@@ -4736,9 +5273,10 @@ function mergeSeq(a, b, depth, path, config) {
4736
5273
  function sameDot(a, b) {
4737
5274
  return a.actor === b.actor && a.ctr === b.ctr;
4738
5275
  }
4739
- function cloneElem(e, depth, actor) {
5276
+ function cloneElem(e, depth, actor, signal) {
5277
+ throwIfAborted(signal);
4740
5278
  assertTraversalDepth(depth);
4741
- const value = cloneNodeShallow(e.value, depth + 1, actor);
5279
+ const value = cloneNodeShallow(e.value, depth + 1, actor, signal);
4742
5280
  return {
4743
5281
  elem: {
4744
5282
  id: e.id,
@@ -4756,7 +5294,8 @@ function mergeDeleteDot(a, b) {
4756
5294
  if (a) return { ...a };
4757
5295
  if (b) return { ...b };
4758
5296
  }
4759
- function cloneNodeShallow(node, depth, actor) {
5297
+ function cloneNodeShallow(node, depth, actor, signal) {
5298
+ throwIfAborted(signal);
4760
5299
  assertTraversalDepth(depth);
4761
5300
  switch (node.kind) {
4762
5301
  case "lww": return {
@@ -4771,7 +5310,8 @@ function cloneNodeShallow(node, depth, actor) {
4771
5310
  const entries = /* @__PURE__ */ new Map();
4772
5311
  let maxObservedCtr = 0;
4773
5312
  for (const [k, v] of node.entries) {
4774
- const cloned = cloneNodeShallow(v.node, depth + 1, actor);
5313
+ throwIfAborted(signal);
5314
+ const cloned = cloneNodeShallow(v.node, depth + 1, actor, signal);
4775
5315
  entries.set(k, {
4776
5316
  node: cloned.node,
4777
5317
  dot: { ...v.dot }
@@ -4780,6 +5320,7 @@ function cloneNodeShallow(node, depth, actor) {
4780
5320
  }
4781
5321
  const tombstone = /* @__PURE__ */ new Map();
4782
5322
  for (const [k, d] of node.tombstone) {
5323
+ throwIfAborted(signal);
4783
5324
  tombstone.set(k, { ...d });
4784
5325
  maxObservedCtr = Math.max(maxObservedCtr, maxObservedCtrForDot(d, actor));
4785
5326
  }
@@ -4796,7 +5337,8 @@ function cloneNodeShallow(node, depth, actor) {
4796
5337
  const elems = /* @__PURE__ */ new Map();
4797
5338
  let maxObservedCtr = 0;
4798
5339
  for (const [id, e] of node.elems) {
4799
- const cloned = cloneElem(e, depth + 1, actor);
5340
+ throwIfAborted(signal);
5341
+ const cloned = cloneElem(e, depth + 1, actor, signal);
4800
5342
  elems.set(id, cloned.elem);
4801
5343
  maxObservedCtr = Math.max(maxObservedCtr, cloned.maxObservedCtr);
4802
5344
  }
@@ -4931,6 +5473,12 @@ Object.defineProperty(exports, 'MergeError', {
4931
5473
  return MergeError;
4932
5474
  }
4933
5475
  });
5476
+ Object.defineProperty(exports, 'OperationCancelledError', {
5477
+ enumerable: true,
5478
+ get: function () {
5479
+ return OperationCancelledError;
5480
+ }
5481
+ });
4934
5482
  Object.defineProperty(exports, 'PatchCompileError', {
4935
5483
  enumerable: true,
4936
5484
  get: function () {
@@ -4949,6 +5497,12 @@ Object.defineProperty(exports, 'ROOT_KEY', {
4949
5497
  return ROOT_KEY;
4950
5498
  }
4951
5499
  });
5500
+ Object.defineProperty(exports, 'ResourceBudgetError', {
5501
+ enumerable: true,
5502
+ get: function () {
5503
+ return ResourceBudgetError;
5504
+ }
5505
+ });
4952
5506
  Object.defineProperty(exports, 'TraversalDepthError', {
4953
5507
  enumerable: true,
4954
5508
  get: function () {
@@ -4961,6 +5515,12 @@ Object.defineProperty(exports, 'applyIntentsToCrdt', {
4961
5515
  return applyIntentsToCrdt;
4962
5516
  }
4963
5517
  });
5518
+ Object.defineProperty(exports, 'applyNormalizedPatch', {
5519
+ enumerable: true,
5520
+ get: function () {
5521
+ return applyNormalizedPatch;
5522
+ }
5523
+ });
4964
5524
  Object.defineProperty(exports, 'applyPatch', {
4965
5525
  enumerable: true,
4966
5526
  get: function () {
@@ -4979,6 +5539,12 @@ Object.defineProperty(exports, 'applyPatchInPlace', {
4979
5539
  return applyPatchInPlace;
4980
5540
  }
4981
5541
  });
5542
+ Object.defineProperty(exports, 'applySafePatch', {
5543
+ enumerable: true,
5544
+ get: function () {
5545
+ return applySafePatch;
5546
+ }
5547
+ });
4982
5548
  Object.defineProperty(exports, 'cloneClock', {
4983
5549
  enumerable: true,
4984
5550
  get: function () {
@@ -5039,6 +5605,18 @@ Object.defineProperty(exports, 'createClock', {
5039
5605
  return createClock;
5040
5606
  }
5041
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
+ });
5042
5620
  Object.defineProperty(exports, 'createState', {
5043
5621
  enumerable: true,
5044
5622
  get: function () {
@@ -5063,6 +5641,18 @@ Object.defineProperty(exports, 'diffJsonPatch', {
5063
5641
  return diffJsonPatch;
5064
5642
  }
5065
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
+ });
5066
5656
  Object.defineProperty(exports, 'docFromJson', {
5067
5657
  enumerable: true,
5068
5658
  get: function () {
@@ -5267,6 +5857,12 @@ Object.defineProperty(exports, 'stableJsonValueKey', {
5267
5857
  return stableJsonValueKey;
5268
5858
  }
5269
5859
  });
5860
+ Object.defineProperty(exports, 'strictRfc6902PatchOptions', {
5861
+ enumerable: true,
5862
+ get: function () {
5863
+ return strictRfc6902PatchOptions;
5864
+ }
5865
+ });
5270
5866
  Object.defineProperty(exports, 'stringifyJsonPointer', {
5271
5867
  enumerable: true,
5272
5868
  get: function () {
@@ -5339,6 +5935,18 @@ Object.defineProperty(exports, 'validateRgaSeq', {
5339
5935
  return validateRgaSeq;
5340
5936
  }
5341
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
+ });
5342
5950
  Object.defineProperty(exports, 'versionVectorCovers', {
5343
5951
  enumerable: true,
5344
5952
  get: function () {
@@ -5356,4 +5964,16 @@ Object.defineProperty(exports, 'vvMerge', {
5356
5964
  get: function () {
5357
5965
  return vvMerge;
5358
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
+ }
5359
5979
  });