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.
- package/README.md +127 -2
- package/dist/{compact-CDvajUfn.js → compact-BUXv4MXQ.js} +749 -129
- package/dist/{compact-Dj0BYeY5.mjs → compact-CncfNnDy.mjs} +672 -130
- package/dist/{depth-CM1kCxhm.d.mts → depth-C5m9qI-V.d.mts} +155 -17
- package/dist/{depth-NbZ6Giq9.d.ts → depth-vwQdqCBN.d.ts} +155 -17
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +18 -2
- package/dist/index.mjs +2 -2
- package/dist/internals.d.mts +4 -3
- package/dist/internals.d.ts +4 -3
- package/dist/internals.js +15 -2
- package/dist/internals.mjs +2 -2
- package/package.json +2 -1
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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]))
|
|
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]))
|
|
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++)
|
|
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++)
|
|
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--)
|
|
1349
|
-
|
|
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 ===
|
|
1440
|
-
|
|
1441
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 -
|
|
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,
|
|
2884
|
+
fail = applyObjSet(head, it, observedNewDot);
|
|
2688
2885
|
break;
|
|
2689
2886
|
case "ObjRemove":
|
|
2690
|
-
fail = applyObjRemove(head, it,
|
|
2887
|
+
fail = applyObjRemove(head, it, observedNewDot);
|
|
2691
2888
|
break;
|
|
2692
2889
|
case "ArrInsert":
|
|
2693
|
-
fail = applyArrInsert(base, head, it,
|
|
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,
|
|
2893
|
+
fail = applyArrDelete(base, head, it, observedNewDot, arrayIndexSession);
|
|
2697
2894
|
break;
|
|
2698
2895
|
case "ArrReplace":
|
|
2699
|
-
fail = applyArrReplace(base, head, it,
|
|
2896
|
+
fail = applyArrReplace(base, head, it, observedNewDot, arrayIndexSession);
|
|
2700
2897
|
break;
|
|
2701
2898
|
default: assertNever(it, "Unhandled intent type");
|
|
2702
2899
|
}
|
|
2703
|
-
if (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 =
|
|
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 =
|
|
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
|
-
|
|
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, {
|
|
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
|
-
|
|
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
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
doc,
|
|
4120
|
-
|
|
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
|
|
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
|
|
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
|
|
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))
|
|
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
|
-
|
|
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
|
-
|
|
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 = {
|
|
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(
|
|
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
|
-
|
|
4524
|
-
|
|
4525
|
-
const key
|
|
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
|
-
|
|
4532
|
-
|
|
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
|
-
|
|
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())
|
|
4692
|
-
|
|
4693
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 };
|