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,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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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]))
|
|
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]))
|
|
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++)
|
|
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++)
|
|
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--)
|
|
1350
|
-
|
|
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 ===
|
|
1441
|
-
|
|
1442
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 -
|
|
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,
|
|
2885
|
+
fail = applyObjSet(head, it, observedNewDot);
|
|
2689
2886
|
break;
|
|
2690
2887
|
case "ObjRemove":
|
|
2691
|
-
fail = applyObjRemove(head, it,
|
|
2888
|
+
fail = applyObjRemove(head, it, observedNewDot);
|
|
2692
2889
|
break;
|
|
2693
2890
|
case "ArrInsert":
|
|
2694
|
-
fail = applyArrInsert(base, head, it,
|
|
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,
|
|
2894
|
+
fail = applyArrDelete(base, head, it, observedNewDot, arrayIndexSession);
|
|
2698
2895
|
break;
|
|
2699
2896
|
case "ArrReplace":
|
|
2700
|
-
fail = applyArrReplace(base, head, it,
|
|
2897
|
+
fail = applyArrReplace(base, head, it, observedNewDot, arrayIndexSession);
|
|
2701
2898
|
break;
|
|
2702
2899
|
default: assertNever(it, "Unhandled intent type");
|
|
2703
2900
|
}
|
|
2704
|
-
if (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 =
|
|
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 =
|
|
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
|
-
|
|
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, {
|
|
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
|
-
|
|
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
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
doc,
|
|
4121
|
-
|
|
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
|
|
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
|
|
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
|
|
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))
|
|
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
|
-
|
|
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
|
-
|
|
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 = {
|
|
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(
|
|
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
|
-
|
|
4525
|
-
|
|
4526
|
-
const key
|
|
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
|
-
|
|
4533
|
-
|
|
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
|
-
|
|
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())
|
|
4693
|
-
|
|
4694
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|