json-patch-to-crdt 0.4.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -2
- package/dist/{compact-CXfvMNCT.js → compact-BUXv4MXQ.js} +1551 -381
- package/dist/{compact-BcwxBNx_.mjs → compact-CncfNnDy.mjs} +1470 -378
- package/dist/{depth-CpJSyZE5.d.mts → depth-C5m9qI-V.d.mts} +184 -22
- package/dist/{depth-D88VeWb-.d.ts → depth-vwQdqCBN.d.ts} +184 -22
- 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 +13 -5
- package/dist/internals.d.ts +13 -5
- 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 {
|
|
@@ -26,6 +152,8 @@ function toDepthApplyError(error) {
|
|
|
26
152
|
|
|
27
153
|
//#endregion
|
|
28
154
|
//#region src/version-vector.ts
|
|
155
|
+
let observedVersionVectorObserverForTests = null;
|
|
156
|
+
const observedVersionVectorCache = /* @__PURE__ */ new WeakMap();
|
|
29
157
|
function readVersionVectorCounter(vv, actor) {
|
|
30
158
|
if (!Object.prototype.hasOwnProperty.call(vv, actor)) return;
|
|
31
159
|
const counter = vv[actor];
|
|
@@ -42,6 +170,21 @@ function writeVersionVectorCounter(vv, actor, counter) {
|
|
|
42
170
|
function observeVersionVectorDot(vv, dot) {
|
|
43
171
|
if ((readVersionVectorCounter(vv, dot.actor) ?? 0) < dot.ctr) writeVersionVectorCounter(vv, dot.actor, dot.ctr);
|
|
44
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
|
+
}
|
|
45
188
|
/**
|
|
46
189
|
* Inspect a document or state and return the highest observed counter per actor.
|
|
47
190
|
*
|
|
@@ -51,11 +194,14 @@ function observeVersionVectorDot(vv, dot) {
|
|
|
51
194
|
*/
|
|
52
195
|
function observedVersionVector(target) {
|
|
53
196
|
const doc = "doc" in target ? target.doc : target;
|
|
54
|
-
const
|
|
197
|
+
const cached = readCachedObservedVersionVector(doc);
|
|
198
|
+
const vv = cached ?? Object.create(null);
|
|
55
199
|
if ("clock" in target) observeVersionVectorDot(vv, {
|
|
56
200
|
actor: target.clock.actor,
|
|
57
201
|
ctr: target.clock.ctr
|
|
58
202
|
});
|
|
203
|
+
if (cached) return vv;
|
|
204
|
+
observedVersionVectorObserverForTests?.(target);
|
|
59
205
|
const stack = [{
|
|
60
206
|
node: doc.root,
|
|
61
207
|
depth: 0
|
|
@@ -87,6 +233,7 @@ function observedVersionVector(target) {
|
|
|
87
233
|
});
|
|
88
234
|
}
|
|
89
235
|
}
|
|
236
|
+
writeCachedObservedVersionVector(doc, vv);
|
|
90
237
|
return vv;
|
|
91
238
|
}
|
|
92
239
|
/** Combine version vectors using per-actor maxima. */
|
|
@@ -493,6 +640,7 @@ function rgaPrevForInsertAtIndex(seq, index) {
|
|
|
493
640
|
//#endregion
|
|
494
641
|
//#region src/materialize.ts
|
|
495
642
|
let materializeObserver = null;
|
|
643
|
+
const EMPTY_PATH = [];
|
|
496
644
|
function createMaterializedObject() {
|
|
497
645
|
return Object.create(null);
|
|
498
646
|
}
|
|
@@ -507,7 +655,7 @@ function setMaterializedProperty(out, key, value) {
|
|
|
507
655
|
/** Convert a CRDT node graph into a plain JSON value using an explicit stack. */
|
|
508
656
|
function materialize(node) {
|
|
509
657
|
const observer = materializeObserver;
|
|
510
|
-
observer?.(
|
|
658
|
+
observer?.(EMPTY_PATH, node);
|
|
511
659
|
if (node.kind === "lww") return node.value;
|
|
512
660
|
const root = node.kind === "obj" ? createMaterializedObject() : [];
|
|
513
661
|
const stack = [];
|
|
@@ -538,7 +686,7 @@ function materialize(node) {
|
|
|
538
686
|
const child = entry.node;
|
|
539
687
|
const childDepth = frame.depth + 1;
|
|
540
688
|
assertTraversalDepth(childDepth);
|
|
541
|
-
const childPath = [...frame.path, key];
|
|
689
|
+
const childPath = observer ? [...frame.path, key] : EMPTY_PATH;
|
|
542
690
|
observer?.(childPath, child);
|
|
543
691
|
if (child.kind === "lww") {
|
|
544
692
|
setMaterializedProperty(frame.out, key, child.value);
|
|
@@ -576,7 +724,7 @@ function materialize(node) {
|
|
|
576
724
|
const child = elem.value;
|
|
577
725
|
const childDepth = frame.depth + 1;
|
|
578
726
|
assertTraversalDepth(childDepth);
|
|
579
|
-
const childPath = [...frame.path, String(frame.nextIndex)];
|
|
727
|
+
const childPath = observer ? [...frame.path, String(frame.nextIndex)] : EMPTY_PATH;
|
|
580
728
|
frame.nextIndex += 1;
|
|
581
729
|
observer?.(childPath, child);
|
|
582
730
|
if (child.kind === "lww") {
|
|
@@ -929,11 +1077,13 @@ function getAtJson(base, path) {
|
|
|
929
1077
|
*/
|
|
930
1078
|
function compileJsonPatchToIntent(baseJson, patch, options = {}) {
|
|
931
1079
|
const internalOptions = options;
|
|
1080
|
+
const budgetMeter = internalOptions.budgetMeter ?? createBudgetMeter(options.resourceBudget);
|
|
932
1081
|
const semantics = options.semantics ?? "sequential";
|
|
933
1082
|
const opIndexOffset = internalOptions.opIndexOffset ?? 0;
|
|
934
1083
|
let workingBase = baseJson;
|
|
935
1084
|
const pointerCache = internalOptions.pointerCache ?? /* @__PURE__ */ new Map();
|
|
936
1085
|
const intents = [];
|
|
1086
|
+
budgetMeter?.count("patchOperations", patch.length);
|
|
937
1087
|
for (let opIndex = 0; opIndex < patch.length; opIndex++) {
|
|
938
1088
|
const op = patch[opIndex];
|
|
939
1089
|
const absoluteOpIndex = opIndex + opIndexOffset;
|
|
@@ -948,14 +1098,16 @@ function compileJsonPatchOpToIntent(baseJson, op, options = {}) {
|
|
|
948
1098
|
const internalOptions = options;
|
|
949
1099
|
const semantics = options.semantics ?? "sequential";
|
|
950
1100
|
const pointerCache = internalOptions.pointerCache ?? /* @__PURE__ */ new Map();
|
|
951
|
-
|
|
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);
|
|
952
1104
|
}
|
|
953
1105
|
/**
|
|
954
1106
|
* Compute a JSON Patch delta between two JSON values.
|
|
955
1107
|
* By default arrays use a deterministic LCS strategy.
|
|
956
1108
|
* Pass `{ arrayStrategy: "atomic" }` for single-op array replacement.
|
|
957
1109
|
* Pass `{ arrayStrategy: "lcs-linear" }` for a lower-memory LCS variant.
|
|
958
|
-
* Use `lcsLinearMaxCells` to
|
|
1110
|
+
* Use `lcsLinearMaxCells` to cap worst-case `lcs-linear` work and
|
|
959
1111
|
* fall back to an atomic array replacement for very large unmatched windows.
|
|
960
1112
|
* Pass `{ emitMoves: true }` or `{ emitCopies: true }` to opt into RFC 6902
|
|
961
1113
|
* move/copy emission when a deterministic rewrite is available.
|
|
@@ -965,20 +1117,24 @@ function compileJsonPatchOpToIntent(baseJson, op, options = {}) {
|
|
|
965
1117
|
* @returns An array of JSON Patch operations that transform `base` into `next`.
|
|
966
1118
|
*/
|
|
967
1119
|
function diffJsonPatch(base, next, options = {}) {
|
|
1120
|
+
const budgetMeter = createBudgetMeter(options.resourceBudget);
|
|
968
1121
|
const runtimeMode = options.jsonValidation ?? "none";
|
|
969
1122
|
const runtimeBase = coerceRuntimeJsonValue(base, runtimeMode);
|
|
970
1123
|
const runtimeNext = coerceRuntimeJsonValue(next, runtimeMode);
|
|
971
1124
|
const ops = [];
|
|
972
|
-
|
|
1125
|
+
const path = [];
|
|
1126
|
+
throwIfAborted(options.signal);
|
|
1127
|
+
diffValue(path, runtimeBase, runtimeNext, ops, options, budgetMeter);
|
|
973
1128
|
return ops;
|
|
974
1129
|
}
|
|
975
|
-
function diffValue(path, base, next, ops, options) {
|
|
1130
|
+
function diffValue(path, base, next, ops, options, budgetMeter) {
|
|
976
1131
|
const stack = [{
|
|
977
1132
|
kind: "value",
|
|
978
1133
|
base,
|
|
979
1134
|
next
|
|
980
1135
|
}];
|
|
981
1136
|
while (stack.length > 0) {
|
|
1137
|
+
throwIfAborted(options.signal);
|
|
982
1138
|
const frame = stack.pop();
|
|
983
1139
|
if (frame.kind === "path-pop") {
|
|
984
1140
|
path.pop();
|
|
@@ -1004,6 +1160,7 @@ function diffValue(path, base, next, ops, options) {
|
|
|
1004
1160
|
continue;
|
|
1005
1161
|
}
|
|
1006
1162
|
assertTraversalDepth(path.length);
|
|
1163
|
+
budgetMeter?.count("visitedNodes", 1, stringifyJsonPointer(path));
|
|
1007
1164
|
if (frame.base === frame.next) continue;
|
|
1008
1165
|
const baseIsArray = Array.isArray(frame.base);
|
|
1009
1166
|
const nextIsArray = Array.isArray(frame.next);
|
|
@@ -1019,7 +1176,7 @@ function diffValue(path, base, next, ops, options) {
|
|
|
1019
1176
|
if (jsonEquals(frame.base, frame.next)) continue;
|
|
1020
1177
|
const arrayStrategy = options.arrayStrategy ?? "lcs";
|
|
1021
1178
|
if (arrayStrategy === "lcs") {
|
|
1022
|
-
if (!diffArrayWithLcsMatrix(path, frame.base, frame.next, ops, options)) ops.push({
|
|
1179
|
+
if (!diffArrayWithLcsMatrix(path, frame.base, frame.next, ops, options, budgetMeter)) ops.push({
|
|
1023
1180
|
op: "replace",
|
|
1024
1181
|
path: stringifyJsonPointer(path),
|
|
1025
1182
|
value: frame.next
|
|
@@ -1027,7 +1184,7 @@ function diffValue(path, base, next, ops, options) {
|
|
|
1027
1184
|
continue;
|
|
1028
1185
|
}
|
|
1029
1186
|
if (arrayStrategy === "lcs-linear") {
|
|
1030
|
-
if (!diffArrayWithLinearLcs(path, frame.base, frame.next, ops, options)) ops.push({
|
|
1187
|
+
if (!diffArrayWithLinearLcs(path, frame.base, frame.next, ops, options, budgetMeter)) ops.push({
|
|
1031
1188
|
op: "replace",
|
|
1032
1189
|
path: stringifyJsonPointer(path),
|
|
1033
1190
|
value: frame.next
|
|
@@ -1052,6 +1209,7 @@ function diffValue(path, base, next, ops, options) {
|
|
|
1052
1209
|
continue;
|
|
1053
1210
|
}
|
|
1054
1211
|
const { sharedKeys, baseOnlyKeys, nextOnlyKeys } = collectObjectKeys(frame.base, frame.next);
|
|
1212
|
+
budgetMeter?.count("objectEntries", sharedKeys.length + baseOnlyKeys.length + nextOnlyKeys.length, stringifyJsonPointer(path));
|
|
1055
1213
|
if (!(baseOnlyKeys.length > 0 || nextOnlyKeys.length > 0) && (path.length === 0 || sharedKeys.length > 1) && jsonEquals(frame.base, frame.next)) continue;
|
|
1056
1214
|
emitObjectStructuralOps(path, frame.base, frame.next, sharedKeys, baseOnlyKeys, nextOnlyKeys, ops, options);
|
|
1057
1215
|
if (sharedKeys.length > 0) stack.push({
|
|
@@ -1215,34 +1373,44 @@ function insertSortedKey(keys, key) {
|
|
|
1215
1373
|
}
|
|
1216
1374
|
keys.splice(low, 0, key);
|
|
1217
1375
|
}
|
|
1218
|
-
function diffArrayWithLcsMatrix(path, base, next, ops, options) {
|
|
1219
|
-
const window = trimEqualArrayEdges(base, next);
|
|
1376
|
+
function diffArrayWithLcsMatrix(path, base, next, ops, options, budgetMeter) {
|
|
1377
|
+
const window = trimEqualArrayEdges(base, next, options);
|
|
1220
1378
|
const baseStart = window.baseStart;
|
|
1221
1379
|
const nextStart = window.nextStart;
|
|
1222
1380
|
const n = window.unmatchedBaseLength;
|
|
1223
1381
|
const m = window.unmatchedNextLength;
|
|
1382
|
+
budgetMeter?.count("sequenceElements", n + m, stringifyJsonPointer(path));
|
|
1224
1383
|
if (!shouldUseLcsDiff(n, m, options.lcsMaxCells)) return false;
|
|
1384
|
+
budgetMeter?.count("arrayDiffCells", (n + 1) * (m + 1), stringifyJsonPointer(path));
|
|
1225
1385
|
if (n === 0 && m === 0) return true;
|
|
1226
1386
|
const steps = [];
|
|
1227
|
-
buildArrayEditScriptWithMatrix(base, baseStart, baseStart + n, next, nextStart, nextStart + m, steps);
|
|
1387
|
+
buildArrayEditScriptWithMatrix(base, baseStart, baseStart + n, next, nextStart, nextStart + m, steps, options);
|
|
1228
1388
|
pushArrayPatchOps(path, window.prefixLength, steps, ops, base, options);
|
|
1229
1389
|
return true;
|
|
1230
1390
|
}
|
|
1231
|
-
function diffArrayWithLinearLcs(path, base, next, ops, options) {
|
|
1232
|
-
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));
|
|
1233
1394
|
if (!shouldUseLinearLcsDiff(window.unmatchedBaseLength, window.unmatchedNextLength, options)) return false;
|
|
1395
|
+
budgetMeter?.count("arrayDiffCells", (window.unmatchedBaseLength + 1) * (window.unmatchedNextLength + 1), stringifyJsonPointer(path));
|
|
1234
1396
|
const steps = [];
|
|
1235
|
-
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);
|
|
1236
1398
|
pushArrayPatchOps(path, window.prefixLength, steps, ops, base, options);
|
|
1237
1399
|
return true;
|
|
1238
1400
|
}
|
|
1239
|
-
function trimEqualArrayEdges(base, next) {
|
|
1401
|
+
function trimEqualArrayEdges(base, next, options) {
|
|
1240
1402
|
const baseLength = base.length;
|
|
1241
1403
|
const nextLength = next.length;
|
|
1242
1404
|
let prefixLength = 0;
|
|
1243
|
-
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
|
+
}
|
|
1244
1409
|
let suffixLength = 0;
|
|
1245
|
-
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
|
+
}
|
|
1246
1414
|
return {
|
|
1247
1415
|
baseStart: prefixLength,
|
|
1248
1416
|
nextStart: prefixLength,
|
|
@@ -1251,7 +1419,8 @@ function trimEqualArrayEdges(base, next) {
|
|
|
1251
1419
|
unmatchedNextLength: nextLength - prefixLength - suffixLength
|
|
1252
1420
|
};
|
|
1253
1421
|
}
|
|
1254
|
-
function buildArrayEditScriptLinearSpace(base, baseStart, baseEnd, next, nextStart, nextEnd, steps) {
|
|
1422
|
+
function buildArrayEditScriptLinearSpace(base, baseStart, baseEnd, next, nextStart, nextEnd, steps, options) {
|
|
1423
|
+
throwIfAborted(options.signal);
|
|
1255
1424
|
const unmatchedBaseLength = baseEnd - baseStart;
|
|
1256
1425
|
const unmatchedNextLength = nextEnd - nextStart;
|
|
1257
1426
|
if (unmatchedBaseLength === 0) {
|
|
@@ -1266,20 +1435,20 @@ function buildArrayEditScriptLinearSpace(base, baseStart, baseEnd, next, nextSta
|
|
|
1266
1435
|
return;
|
|
1267
1436
|
}
|
|
1268
1437
|
if (unmatchedBaseLength === 1) {
|
|
1269
|
-
pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, steps);
|
|
1438
|
+
pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, steps, options);
|
|
1270
1439
|
return;
|
|
1271
1440
|
}
|
|
1272
1441
|
if (unmatchedNextLength === 1) {
|
|
1273
|
-
pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, steps);
|
|
1442
|
+
pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, steps, options);
|
|
1274
1443
|
return;
|
|
1275
1444
|
}
|
|
1276
1445
|
if (shouldUseMatrixBaseCase(unmatchedBaseLength, unmatchedNextLength)) {
|
|
1277
|
-
buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStart, nextEnd, steps);
|
|
1446
|
+
buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStart, nextEnd, steps, options);
|
|
1278
1447
|
return;
|
|
1279
1448
|
}
|
|
1280
1449
|
const baseMid = baseStart + Math.floor(unmatchedBaseLength / 2);
|
|
1281
|
-
const forwardScores = computeLcsPrefixLengths(base, baseStart, baseMid, next, nextStart, nextEnd);
|
|
1282
|
-
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);
|
|
1283
1452
|
let bestOffset = 0;
|
|
1284
1453
|
let bestScore = Number.NEGATIVE_INFINITY;
|
|
1285
1454
|
for (let offset = 0; offset <= unmatchedNextLength; offset++) {
|
|
@@ -1290,11 +1459,11 @@ function buildArrayEditScriptLinearSpace(base, baseStart, baseEnd, next, nextSta
|
|
|
1290
1459
|
}
|
|
1291
1460
|
}
|
|
1292
1461
|
const nextMid = nextStart + bestOffset;
|
|
1293
|
-
buildArrayEditScriptLinearSpace(base, baseStart, baseMid, next, nextStart, nextMid, steps);
|
|
1294
|
-
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);
|
|
1295
1464
|
}
|
|
1296
|
-
function pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, steps) {
|
|
1297
|
-
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);
|
|
1298
1467
|
if (matchIndex === -1) {
|
|
1299
1468
|
steps.push({ kind: "remove" });
|
|
1300
1469
|
for (let nextIndex = nextStart; nextIndex < nextEnd; nextIndex++) steps.push({
|
|
@@ -1313,8 +1482,8 @@ function pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, s
|
|
|
1313
1482
|
value: next[nextIndex]
|
|
1314
1483
|
});
|
|
1315
1484
|
}
|
|
1316
|
-
function pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, steps) {
|
|
1317
|
-
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);
|
|
1318
1487
|
if (matchIndex === -1) {
|
|
1319
1488
|
for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) steps.push({ kind: "remove" });
|
|
1320
1489
|
steps.push({
|
|
@@ -1327,26 +1496,36 @@ function pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, s
|
|
|
1327
1496
|
steps.push({ kind: "equal" });
|
|
1328
1497
|
for (let baseIndex = matchIndex + 1; baseIndex < baseEnd; baseIndex++) steps.push({ kind: "remove" });
|
|
1329
1498
|
}
|
|
1330
|
-
function findFirstMatchingIndexInNext(target, next, nextStart, nextEnd) {
|
|
1331
|
-
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
|
+
}
|
|
1332
1504
|
return -1;
|
|
1333
1505
|
}
|
|
1334
|
-
function findFirstMatchingIndexInBase(target, base, baseStart, baseEnd) {
|
|
1335
|
-
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
|
+
}
|
|
1336
1511
|
return -1;
|
|
1337
1512
|
}
|
|
1338
1513
|
function shouldUseMatrixBaseCase(baseLength, nextLength) {
|
|
1339
1514
|
return (baseLength + 1) * (nextLength + 1) <= LINEAR_LCS_MATRIX_BASE_CASE_MAX_CELLS;
|
|
1340
1515
|
}
|
|
1341
|
-
function buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStart, nextEnd, steps) {
|
|
1516
|
+
function buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStart, nextEnd, steps, options) {
|
|
1342
1517
|
const unmatchedBaseLength = baseEnd - baseStart;
|
|
1343
1518
|
const unmatchedNextLength = nextEnd - nextStart;
|
|
1344
1519
|
const lcs = Array.from({ length: unmatchedBaseLength + 1 }, () => Array(unmatchedNextLength + 1).fill(0));
|
|
1345
|
-
for (let baseOffset = unmatchedBaseLength - 1; baseOffset >= 0; baseOffset--)
|
|
1346
|
-
|
|
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
|
+
}
|
|
1347
1525
|
let baseOffset = 0;
|
|
1348
1526
|
let nextOffset = 0;
|
|
1349
1527
|
while (baseOffset < unmatchedBaseLength || nextOffset < unmatchedNextLength) {
|
|
1528
|
+
throwIfAborted(options.signal);
|
|
1350
1529
|
if (baseOffset < unmatchedBaseLength && nextOffset < unmatchedNextLength && jsonEquals(base[baseStart + baseOffset], next[nextStart + nextOffset])) {
|
|
1351
1530
|
steps.push({ kind: "equal" });
|
|
1352
1531
|
baseOffset += 1;
|
|
@@ -1369,11 +1548,12 @@ function buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStar
|
|
|
1369
1548
|
}
|
|
1370
1549
|
}
|
|
1371
1550
|
}
|
|
1372
|
-
function computeLcsPrefixLengths(base, baseStart, baseEnd, next, nextStart, nextEnd) {
|
|
1551
|
+
function computeLcsPrefixLengths(base, baseStart, baseEnd, next, nextStart, nextEnd, options) {
|
|
1373
1552
|
const unmatchedNextLength = nextEnd - nextStart;
|
|
1374
1553
|
let previousRow = new Int32Array(unmatchedNextLength + 1);
|
|
1375
1554
|
let currentRow = new Int32Array(unmatchedNextLength + 1);
|
|
1376
1555
|
for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) {
|
|
1556
|
+
throwIfAborted(options.signal);
|
|
1377
1557
|
for (let nextOffset = 0; nextOffset < unmatchedNextLength; nextOffset++) if (jsonEquals(base[baseIndex], next[nextStart + nextOffset])) currentRow[nextOffset + 1] = previousRow[nextOffset] + 1;
|
|
1378
1558
|
else currentRow[nextOffset + 1] = Math.max(previousRow[nextOffset + 1], currentRow[nextOffset]);
|
|
1379
1559
|
const nextPreviousRow = currentRow;
|
|
@@ -1383,11 +1563,12 @@ function computeLcsPrefixLengths(base, baseStart, baseEnd, next, nextStart, next
|
|
|
1383
1563
|
}
|
|
1384
1564
|
return previousRow;
|
|
1385
1565
|
}
|
|
1386
|
-
function computeLcsSuffixLengths(base, baseStart, baseEnd, next, nextStart, nextEnd) {
|
|
1566
|
+
function computeLcsSuffixLengths(base, baseStart, baseEnd, next, nextStart, nextEnd, options) {
|
|
1387
1567
|
const unmatchedNextLength = nextEnd - nextStart;
|
|
1388
1568
|
let previousRow = new Int32Array(unmatchedNextLength + 1);
|
|
1389
1569
|
let currentRow = new Int32Array(unmatchedNextLength + 1);
|
|
1390
1570
|
for (let baseIndex = baseEnd - 1; baseIndex >= baseStart; baseIndex--) {
|
|
1571
|
+
throwIfAborted(options.signal);
|
|
1391
1572
|
for (let nextOffset = unmatchedNextLength - 1; nextOffset >= 0; nextOffset--) if (jsonEquals(base[baseIndex], next[nextStart + nextOffset])) currentRow[nextOffset] = previousRow[nextOffset + 1] + 1;
|
|
1392
1573
|
else currentRow[nextOffset] = Math.max(previousRow[nextOffset], currentRow[nextOffset + 1]);
|
|
1393
1574
|
const nextPreviousRow = currentRow;
|
|
@@ -1433,9 +1614,10 @@ function shouldUseLcsDiff(baseLength, nextLength, lcsMaxCells) {
|
|
|
1433
1614
|
}
|
|
1434
1615
|
function shouldUseLinearLcsDiff(baseLength, nextLength, options) {
|
|
1435
1616
|
const cap = options.lcsLinearMaxCells;
|
|
1436
|
-
if (cap ===
|
|
1437
|
-
|
|
1438
|
-
|
|
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;
|
|
1439
1621
|
}
|
|
1440
1622
|
function finalizeArrayOps(arrayPath, base, ops, options) {
|
|
1441
1623
|
if (ops.length === 0) return [];
|
|
@@ -2065,54 +2247,45 @@ function isSamePath(a, b) {
|
|
|
2065
2247
|
* @returns A new CRDT `Doc`.
|
|
2066
2248
|
*/
|
|
2067
2249
|
function docFromJson(value, nextDot) {
|
|
2068
|
-
|
|
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;
|
|
2069
2258
|
}
|
|
2070
2259
|
/**
|
|
2071
|
-
* Legacy
|
|
2072
|
-
*
|
|
2260
|
+
* Legacy helper for tests and fixtures that seeds an entire document from one dot.
|
|
2261
|
+
*
|
|
2262
|
+
* It reuses that dot for object entries and synthesizes array child counters from the
|
|
2263
|
+
* same seed, which can produce low-quality causal metadata and unrealistic sequence
|
|
2264
|
+
* identities in production CRDT state.
|
|
2265
|
+
*
|
|
2266
|
+
* Prefer `docFromJson(value, nextDot)` so every node receives a fresh unique dot.
|
|
2267
|
+
*
|
|
2268
|
+
* @deprecated Use `docFromJson(value, nextDot)` for production documents.
|
|
2073
2269
|
*/
|
|
2074
2270
|
function docFromJsonWithDot(value, dot) {
|
|
2075
2271
|
return { root: deepNodeFromJson(value, dot) };
|
|
2076
2272
|
}
|
|
2077
2273
|
function getSeqAtPath(doc, path) {
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
if (cur.kind !== "obj") return;
|
|
2081
|
-
const ent = cur.entries.get(seg);
|
|
2082
|
-
if (!ent) return;
|
|
2083
|
-
cur = ent.node;
|
|
2084
|
-
}
|
|
2085
|
-
return cur.kind === "seq" ? cur : void 0;
|
|
2274
|
+
const node = getNodeAtPath(doc, path);
|
|
2275
|
+
return node?.kind === "seq" ? node : void 0;
|
|
2086
2276
|
}
|
|
2087
2277
|
function getObjAtPathStrict(doc, path) {
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
if (cur.kind !== "obj") return {
|
|
2092
|
-
ok: false,
|
|
2093
|
-
message: "expected object at /"
|
|
2094
|
-
};
|
|
2278
|
+
const node = getNodeAtPath(doc, path);
|
|
2279
|
+
if (!node || node.kind !== "obj") {
|
|
2280
|
+
const pointer = stringifyJsonPointer(path);
|
|
2095
2281
|
return {
|
|
2096
|
-
ok: true,
|
|
2097
|
-
obj: cur
|
|
2098
|
-
};
|
|
2099
|
-
}
|
|
2100
|
-
for (const seg of path) {
|
|
2101
|
-
if (cur.kind !== "obj") return {
|
|
2102
|
-
ok: false,
|
|
2103
|
-
message: `expected object at /${seen.join("/")}`
|
|
2104
|
-
};
|
|
2105
|
-
const entry = cur.entries.get(seg);
|
|
2106
|
-
seen.push(seg);
|
|
2107
|
-
if (!entry || entry.node.kind !== "obj") return {
|
|
2108
2282
|
ok: false,
|
|
2109
|
-
message: `expected object at
|
|
2283
|
+
message: `expected object at ${pointer === "" ? "/" : pointer}`
|
|
2110
2284
|
};
|
|
2111
|
-
cur = entry.node;
|
|
2112
2285
|
}
|
|
2113
2286
|
return {
|
|
2114
2287
|
ok: true,
|
|
2115
|
-
obj:
|
|
2288
|
+
obj: node
|
|
2116
2289
|
};
|
|
2117
2290
|
}
|
|
2118
2291
|
function ensureSeqAtPath(head, path, dotForCreate) {
|
|
@@ -2159,10 +2332,24 @@ function ensureSeqAtPath(head, path, dotForCreate) {
|
|
|
2159
2332
|
function getNodeAtPath(doc, path) {
|
|
2160
2333
|
let cur = doc.root;
|
|
2161
2334
|
for (const seg of path) {
|
|
2162
|
-
if (cur.kind
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2335
|
+
if (cur.kind === "obj") {
|
|
2336
|
+
const ent = cur.entries.get(seg);
|
|
2337
|
+
if (!ent) return;
|
|
2338
|
+
cur = ent.node;
|
|
2339
|
+
continue;
|
|
2340
|
+
}
|
|
2341
|
+
if (cur.kind === "seq") {
|
|
2342
|
+
if (!ARRAY_INDEX_TOKEN_PATTERN.test(seg)) return;
|
|
2343
|
+
const index = Number(seg);
|
|
2344
|
+
if (!Number.isSafeInteger(index)) return;
|
|
2345
|
+
const elemId = rgaIdAtIndex(cur, index);
|
|
2346
|
+
if (elemId === void 0) return;
|
|
2347
|
+
const elem = cur.elems.get(elemId);
|
|
2348
|
+
if (!elem) return;
|
|
2349
|
+
cur = elem.value;
|
|
2350
|
+
continue;
|
|
2351
|
+
}
|
|
2352
|
+
if (cur.kind === "lww") return;
|
|
2166
2353
|
}
|
|
2167
2354
|
return cur;
|
|
2168
2355
|
}
|
|
@@ -2314,7 +2501,10 @@ function nodeFromJson(value, nextDot) {
|
|
|
2314
2501
|
}
|
|
2315
2502
|
/** Deep-clone a CRDT document. The clone is fully independent of the original. */
|
|
2316
2503
|
function cloneDoc(doc) {
|
|
2317
|
-
|
|
2504
|
+
const cloned = { root: cloneNode(doc.root) };
|
|
2505
|
+
const cached = readCachedObservedVersionVector(doc);
|
|
2506
|
+
if (cached) writeCachedObservedVersionVector(cloned, cached);
|
|
2507
|
+
return cloned;
|
|
2318
2508
|
}
|
|
2319
2509
|
function cloneNode(node) {
|
|
2320
2510
|
return cloneNodeAtDepth(node, 0);
|
|
@@ -2376,38 +2566,88 @@ function getJsonAtDocPathForTest(doc, path) {
|
|
|
2376
2566
|
let cur = doc.root;
|
|
2377
2567
|
for (let i = 0; i < path.length; i++) {
|
|
2378
2568
|
const seg = path[i];
|
|
2379
|
-
|
|
2569
|
+
try {
|
|
2570
|
+
assertTraversalDepth(i + 1);
|
|
2571
|
+
} catch (error) {
|
|
2572
|
+
return {
|
|
2573
|
+
ok: false,
|
|
2574
|
+
error: error instanceof TraversalDepthError ? toDepthApplyError(error) : {
|
|
2575
|
+
ok: false,
|
|
2576
|
+
code: 409,
|
|
2577
|
+
reason: "INVALID_PATCH",
|
|
2578
|
+
message: error instanceof Error ? error.message : "invalid test path"
|
|
2579
|
+
}
|
|
2580
|
+
};
|
|
2581
|
+
}
|
|
2380
2582
|
if (cur.kind === "obj") {
|
|
2381
2583
|
const ent = cur.entries.get(seg);
|
|
2382
|
-
if (!ent)
|
|
2584
|
+
if (!ent) return {
|
|
2585
|
+
ok: false,
|
|
2586
|
+
error: {
|
|
2587
|
+
ok: false,
|
|
2588
|
+
code: 409,
|
|
2589
|
+
reason: "MISSING_TARGET",
|
|
2590
|
+
message: `Missing key '${seg}'`
|
|
2591
|
+
}
|
|
2592
|
+
};
|
|
2383
2593
|
cur = ent.node;
|
|
2384
2594
|
continue;
|
|
2385
2595
|
}
|
|
2386
2596
|
if (cur.kind === "seq") {
|
|
2387
|
-
if (!ARRAY_INDEX_TOKEN_PATTERN.test(seg))
|
|
2388
|
-
|
|
2389
|
-
|
|
2597
|
+
if (!ARRAY_INDEX_TOKEN_PATTERN.test(seg)) return {
|
|
2598
|
+
ok: false,
|
|
2599
|
+
error: {
|
|
2600
|
+
ok: false,
|
|
2601
|
+
code: 409,
|
|
2602
|
+
reason: "INVALID_POINTER",
|
|
2603
|
+
message: `Expected array index, got '${seg}'`
|
|
2604
|
+
}
|
|
2605
|
+
};
|
|
2606
|
+
const idx = Number(seg);
|
|
2607
|
+
if (!Number.isSafeInteger(idx)) return {
|
|
2608
|
+
ok: false,
|
|
2609
|
+
error: {
|
|
2610
|
+
ok: false,
|
|
2611
|
+
code: 409,
|
|
2612
|
+
reason: "OUT_OF_BOUNDS",
|
|
2613
|
+
message: `Index out of bounds at '${seg}'`
|
|
2614
|
+
}
|
|
2615
|
+
};
|
|
2616
|
+
const id = rgaIdAtIndex(cur, idx);
|
|
2617
|
+
if (id === void 0) return {
|
|
2618
|
+
ok: false,
|
|
2619
|
+
error: {
|
|
2620
|
+
ok: false,
|
|
2621
|
+
code: 409,
|
|
2622
|
+
reason: "OUT_OF_BOUNDS",
|
|
2623
|
+
message: `Index out of bounds at '${seg}'`
|
|
2624
|
+
}
|
|
2625
|
+
};
|
|
2390
2626
|
cur = cur.elems.get(id).value;
|
|
2391
2627
|
continue;
|
|
2392
2628
|
}
|
|
2393
|
-
throw new Error(`Cannot traverse into non-container at '${seg}'`);
|
|
2394
|
-
}
|
|
2395
|
-
return cur.kind === "lww" ? cur.value : materialize(cur);
|
|
2396
|
-
}
|
|
2397
|
-
function applyTest(base, head, it, evalTestAgainst) {
|
|
2398
|
-
let got;
|
|
2399
|
-
try {
|
|
2400
|
-
got = getJsonAtDocPathForTest(evalTestAgainst === "head" ? head : base, it.path);
|
|
2401
|
-
} catch {
|
|
2402
2629
|
return {
|
|
2403
2630
|
ok: false,
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2631
|
+
error: {
|
|
2632
|
+
ok: false,
|
|
2633
|
+
code: 409,
|
|
2634
|
+
reason: "INVALID_TARGET",
|
|
2635
|
+
message: `Cannot traverse into non-container at '${seg}'`
|
|
2636
|
+
}
|
|
2408
2637
|
};
|
|
2409
2638
|
}
|
|
2410
|
-
|
|
2639
|
+
return {
|
|
2640
|
+
ok: true,
|
|
2641
|
+
value: cur.kind === "lww" ? cur.value : materialize(cur)
|
|
2642
|
+
};
|
|
2643
|
+
}
|
|
2644
|
+
function applyTest(base, head, it, evalTestAgainst) {
|
|
2645
|
+
const got = getJsonAtDocPathForTest(evalTestAgainst === "head" ? head : base, it.path);
|
|
2646
|
+
if (!got.ok) return {
|
|
2647
|
+
...got.error,
|
|
2648
|
+
path: `/${it.path.join("/")}`
|
|
2649
|
+
};
|
|
2650
|
+
if (!jsonEquals(got.value, it.value)) return {
|
|
2411
2651
|
ok: false,
|
|
2412
2652
|
code: 409,
|
|
2413
2653
|
reason: "TEST_FAILED",
|
|
@@ -2472,7 +2712,7 @@ function createArrayIndexLookupSession() {
|
|
|
2472
2712
|
return created;
|
|
2473
2713
|
} };
|
|
2474
2714
|
}
|
|
2475
|
-
function applyArrInsert(base, head, it, newDot, indexSession, bumpCounterAbove, strictParents =
|
|
2715
|
+
function applyArrInsert(base, head, it, newDot, indexSession, bumpCounterAbove, strictParents = true) {
|
|
2476
2716
|
const pointer = `/${it.path.join("/")}`;
|
|
2477
2717
|
const baseSeq = getSeqAtPath(base, it.path);
|
|
2478
2718
|
if (!baseSeq) {
|
|
@@ -2621,39 +2861,51 @@ function applyArrReplace(base, head, it, newDot, indexSession) {
|
|
|
2621
2861
|
* @param evalTestAgainst - Whether `test` ops are evaluated against `"head"` or `"base"`.
|
|
2622
2862
|
* @param bumpCounterAbove - Optional hook that can fast-forward the underlying counter before inserts.
|
|
2623
2863
|
* @param options - Optional behavior toggles.
|
|
2624
|
-
* @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.
|
|
2625
2866
|
* @returns `{ ok: true }` on success, or `{ ok: false, code: 409, message }` on conflict.
|
|
2626
2867
|
*/
|
|
2627
2868
|
function applyIntentsToCrdt(base, head, intents, newDot, evalTestAgainst = "head", bumpCounterAbove, options = {}) {
|
|
2628
2869
|
const arrayIndexSession = createArrayIndexLookupSession();
|
|
2870
|
+
let pendingObservedDots = [];
|
|
2871
|
+
const observedNewDot = () => {
|
|
2872
|
+
const dot = newDot();
|
|
2873
|
+
pendingObservedDots.push(dot);
|
|
2874
|
+
return dot;
|
|
2875
|
+
};
|
|
2629
2876
|
for (const it of intents) {
|
|
2630
2877
|
let fail = null;
|
|
2878
|
+
pendingObservedDots = [];
|
|
2631
2879
|
switch (it.t) {
|
|
2632
2880
|
case "Test":
|
|
2633
2881
|
fail = applyTest(base, head, it, evalTestAgainst);
|
|
2634
2882
|
break;
|
|
2635
2883
|
case "ObjSet":
|
|
2636
|
-
fail = applyObjSet(head, it,
|
|
2884
|
+
fail = applyObjSet(head, it, observedNewDot);
|
|
2637
2885
|
break;
|
|
2638
2886
|
case "ObjRemove":
|
|
2639
|
-
fail = applyObjRemove(head, it,
|
|
2887
|
+
fail = applyObjRemove(head, it, observedNewDot);
|
|
2640
2888
|
break;
|
|
2641
2889
|
case "ArrInsert":
|
|
2642
|
-
fail = applyArrInsert(base, head, it,
|
|
2890
|
+
fail = applyArrInsert(base, head, it, observedNewDot, arrayIndexSession, bumpCounterAbove, options.strictParents ?? true);
|
|
2643
2891
|
break;
|
|
2644
2892
|
case "ArrDelete":
|
|
2645
|
-
fail = applyArrDelete(base, head, it,
|
|
2893
|
+
fail = applyArrDelete(base, head, it, observedNewDot, arrayIndexSession);
|
|
2646
2894
|
break;
|
|
2647
2895
|
case "ArrReplace":
|
|
2648
|
-
fail = applyArrReplace(base, head, it,
|
|
2896
|
+
fail = applyArrReplace(base, head, it, observedNewDot, arrayIndexSession);
|
|
2649
2897
|
break;
|
|
2650
2898
|
default: assertNever(it, "Unhandled intent type");
|
|
2651
2899
|
}
|
|
2652
|
-
if (fail)
|
|
2900
|
+
if (fail) {
|
|
2901
|
+
pendingObservedDots = [];
|
|
2902
|
+
return fail;
|
|
2903
|
+
}
|
|
2904
|
+
for (const dot of pendingObservedDots) observeDocVersionVectorDot(head, dot);
|
|
2653
2905
|
}
|
|
2654
2906
|
return { ok: true };
|
|
2655
2907
|
}
|
|
2656
|
-
function jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove, strictParents =
|
|
2908
|
+
function jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove, strictParents = true) {
|
|
2657
2909
|
if (isJsonPatchToCrdtOptions(baseOrOptions)) return jsonPatchToCrdtInternal(baseOrOptions);
|
|
2658
2910
|
if (!head || !patch || !newDot) return {
|
|
2659
2911
|
ok: false,
|
|
@@ -2671,7 +2923,7 @@ function jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst = "
|
|
|
2671
2923
|
strictParents
|
|
2672
2924
|
});
|
|
2673
2925
|
}
|
|
2674
|
-
function jsonPatchToCrdtSafe(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove, strictParents =
|
|
2926
|
+
function jsonPatchToCrdtSafe(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove, strictParents = true) {
|
|
2675
2927
|
try {
|
|
2676
2928
|
if (isJsonPatchToCrdtOptions(baseOrOptions)) return jsonPatchToCrdt(baseOrOptions);
|
|
2677
2929
|
if (!head || !patch || !newDot) return {
|
|
@@ -2712,6 +2964,46 @@ function rebaseDiffOps(path, nestedOps, out) {
|
|
|
2712
2964
|
throw new Error(`Unexpected op '${op.op}' from diffJsonPatch`);
|
|
2713
2965
|
}
|
|
2714
2966
|
}
|
|
2967
|
+
function collectLiveSequenceElements(seq) {
|
|
2968
|
+
const elems = [];
|
|
2969
|
+
const cursor = rgaCreateLinearCursor(seq);
|
|
2970
|
+
for (let elem = cursor.next(); elem; elem = cursor.next()) elems.push(elem);
|
|
2971
|
+
return elems;
|
|
2972
|
+
}
|
|
2973
|
+
function materializeSequenceWindow(elems, start, end) {
|
|
2974
|
+
const out = [];
|
|
2975
|
+
for (let i = start; i < end; i++) out.push(nodeToJsonForPatch(elems[i].value));
|
|
2976
|
+
return out;
|
|
2977
|
+
}
|
|
2978
|
+
function rebaseSequenceWindowDiffOps(path, indexOffset, nestedOps, out) {
|
|
2979
|
+
const pending = [];
|
|
2980
|
+
for (const op of nestedOps) {
|
|
2981
|
+
if (op.path === "") return false;
|
|
2982
|
+
const rebasedSegments = parseJsonPointer(op.path);
|
|
2983
|
+
const indexToken = rebasedSegments[0];
|
|
2984
|
+
if (!indexToken || !ARRAY_INDEX_TOKEN_PATTERN.test(indexToken)) return false;
|
|
2985
|
+
rebasedSegments[0] = String(Number(indexToken) + indexOffset);
|
|
2986
|
+
const rebasedPath = stringifyJsonPointer([...path, ...rebasedSegments]);
|
|
2987
|
+
if (op.op === "remove") {
|
|
2988
|
+
pending.push({
|
|
2989
|
+
op: "remove",
|
|
2990
|
+
path: rebasedPath
|
|
2991
|
+
});
|
|
2992
|
+
continue;
|
|
2993
|
+
}
|
|
2994
|
+
if (op.op === "add" || op.op === "replace") {
|
|
2995
|
+
pending.push({
|
|
2996
|
+
op: op.op,
|
|
2997
|
+
path: rebasedPath,
|
|
2998
|
+
value: op.value
|
|
2999
|
+
});
|
|
3000
|
+
continue;
|
|
3001
|
+
}
|
|
3002
|
+
return false;
|
|
3003
|
+
}
|
|
3004
|
+
out.push(...pending);
|
|
3005
|
+
return true;
|
|
3006
|
+
}
|
|
2715
3007
|
function nodesJsonEqual(baseNode, headNode, depth) {
|
|
2716
3008
|
assertTraversalDepth(depth);
|
|
2717
3009
|
if (baseNode === headNode) return true;
|
|
@@ -2836,6 +3128,35 @@ function diffObjectNodes(path, baseNode, headNode, options, ops, depth) {
|
|
|
2836
3128
|
headIndex += 1;
|
|
2837
3129
|
}
|
|
2838
3130
|
}
|
|
3131
|
+
function diffSequenceNodes(path, baseNode, headSeq, options, ops, depth) {
|
|
3132
|
+
if ((options.arrayStrategy ?? "lcs") === "atomic") {
|
|
3133
|
+
rebaseDiffOps(path, diffJsonPatch(materialize(baseNode), materialize(headSeq), options), ops);
|
|
3134
|
+
return;
|
|
3135
|
+
}
|
|
3136
|
+
const baseElems = collectLiveSequenceElements(baseNode);
|
|
3137
|
+
const headElems = collectLiveSequenceElements(headSeq);
|
|
3138
|
+
const sharedLength = Math.min(baseElems.length, headElems.length);
|
|
3139
|
+
let prefixLength = 0;
|
|
3140
|
+
while (prefixLength < sharedLength && nodesJsonEqual(baseElems[prefixLength].value, headElems[prefixLength].value, depth + 1)) prefixLength += 1;
|
|
3141
|
+
if (prefixLength === baseElems.length && prefixLength === headElems.length) return;
|
|
3142
|
+
let baseEnd = baseElems.length;
|
|
3143
|
+
let headEnd = headElems.length;
|
|
3144
|
+
while (baseEnd > prefixLength && headEnd > prefixLength && nodesJsonEqual(baseElems[baseEnd - 1].value, headElems[headEnd - 1].value, depth + 1)) {
|
|
3145
|
+
baseEnd -= 1;
|
|
3146
|
+
headEnd -= 1;
|
|
3147
|
+
}
|
|
3148
|
+
const unmatchedBaseLength = baseEnd - prefixLength;
|
|
3149
|
+
const unmatchedHeadLength = headEnd - prefixLength;
|
|
3150
|
+
if (unmatchedBaseLength === 1 && unmatchedHeadLength === 1) {
|
|
3151
|
+
path.push(String(prefixLength));
|
|
3152
|
+
diffNodeToPatch(path, baseElems[prefixLength].value, headElems[prefixLength].value, options, ops, depth + 1);
|
|
3153
|
+
path.pop();
|
|
3154
|
+
return;
|
|
3155
|
+
}
|
|
3156
|
+
const seqOps = diffJsonPatch(materializeSequenceWindow(baseElems, prefixLength, baseEnd), materializeSequenceWindow(headElems, prefixLength, headEnd), options);
|
|
3157
|
+
if (rebaseSequenceWindowDiffOps(path, prefixLength, seqOps, ops)) return;
|
|
3158
|
+
rebaseDiffOps(path, diffJsonPatch(materialize(baseNode), materialize(headSeq), options), ops);
|
|
3159
|
+
}
|
|
2839
3160
|
function diffNodeToPatch(path, baseNode, headNode, options, ops, depth) {
|
|
2840
3161
|
assertTraversalDepth(depth);
|
|
2841
3162
|
if (baseNode === headNode) return;
|
|
@@ -2861,8 +3182,7 @@ function diffNodeToPatch(path, baseNode, headNode, options, ops, depth) {
|
|
|
2861
3182
|
diffObjectNodes(path, baseNode, headNode, options, ops, depth);
|
|
2862
3183
|
return;
|
|
2863
3184
|
}
|
|
2864
|
-
|
|
2865
|
-
rebaseDiffOps(path, diffJsonPatch(materialize(baseNode), materialize(headSeq), options), ops);
|
|
3185
|
+
diffSequenceNodes(path, baseNode, headNode, options, ops, depth);
|
|
2866
3186
|
}
|
|
2867
3187
|
/**
|
|
2868
3188
|
* Generate a JSON Patch delta between two CRDT documents.
|
|
@@ -2872,7 +3192,7 @@ function diffNodeToPatch(path, baseNode, headNode, options, ops, depth) {
|
|
|
2872
3192
|
* @returns An array of JSON Patch operations that transform base into head.
|
|
2873
3193
|
*/
|
|
2874
3194
|
function crdtToJsonPatch(base, head, options) {
|
|
2875
|
-
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);
|
|
2876
3196
|
return crdtNodesToJsonPatch(base.root, head.root, options);
|
|
2877
3197
|
}
|
|
2878
3198
|
/** Internals-only helper for diffing CRDT nodes from an existing traversal depth. */
|
|
@@ -2894,17 +3214,27 @@ function crdtToFullReplace(doc) {
|
|
|
2894
3214
|
}
|
|
2895
3215
|
function jsonPatchToCrdtInternal(options) {
|
|
2896
3216
|
const evalTestAgainst = options.evalTestAgainst ?? "head";
|
|
2897
|
-
|
|
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") {
|
|
2898
3225
|
const baseJson = materialize(options.base.root);
|
|
2899
3226
|
let intents;
|
|
2900
3227
|
try {
|
|
2901
|
-
intents = compileJsonPatchToIntent(baseJson, options.patch, {
|
|
3228
|
+
intents = compileJsonPatchToIntent(baseJson, options.patch, {
|
|
3229
|
+
semantics: "base",
|
|
3230
|
+
resourceBudget: options.resourceBudget
|
|
3231
|
+
});
|
|
2902
3232
|
} catch (error) {
|
|
2903
3233
|
return toApplyError$1(error);
|
|
2904
3234
|
}
|
|
2905
3235
|
return applyIntentsToCrdt(options.base, options.head, intents, options.newDot, evalTestAgainst, options.bumpCounterAbove, { strictParents: options.strictParents });
|
|
2906
3236
|
}
|
|
2907
|
-
|
|
3237
|
+
const shadowBase = evalTestAgainst === "base" ? cloneDoc(options.base) : null;
|
|
2908
3238
|
let shadowCtr = 0;
|
|
2909
3239
|
const shadowDot = () => ({
|
|
2910
3240
|
actor: "__shadow__",
|
|
@@ -2913,108 +3243,396 @@ function jsonPatchToCrdtInternal(options) {
|
|
|
2913
3243
|
const shadowBump = (ctr) => {
|
|
2914
3244
|
if (shadowCtr < ctr) shadowCtr = ctr;
|
|
2915
3245
|
};
|
|
2916
|
-
const
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
try {
|
|
2920
|
-
intents = compileJsonPatchToIntent(baseJson, [op], { semantics: "sequential" });
|
|
2921
|
-
} catch (error) {
|
|
2922
|
-
return withOpIndex(toApplyError$1(error), opIndex);
|
|
2923
|
-
}
|
|
2924
|
-
const headStep = applyIntentsToCrdt(shadowBase, options.head, intents, options.newDot, evalTestAgainst, options.bumpCounterAbove, { strictParents: options.strictParents });
|
|
2925
|
-
if (!headStep.ok) return withOpIndex(headStep, opIndex);
|
|
2926
|
-
if (evalTestAgainst === "base") {
|
|
2927
|
-
const shadowStep = applyIntentsToCrdt(shadowBase, shadowBase, intents, shadowDot, "base", shadowBump, { strictParents: options.strictParents });
|
|
2928
|
-
if (!shadowStep.ok) return withOpIndex(shadowStep, opIndex);
|
|
2929
|
-
} else shadowBase = cloneDoc(options.head);
|
|
2930
|
-
return { ok: true };
|
|
2931
|
-
};
|
|
2932
|
-
for (let opIndex = 0; opIndex < options.patch.length; opIndex++) {
|
|
2933
|
-
const op = options.patch[opIndex];
|
|
2934
|
-
if (op.op === "move") {
|
|
2935
|
-
const baseJson = materialize(shadowBase.root);
|
|
2936
|
-
let fromValue;
|
|
2937
|
-
try {
|
|
2938
|
-
fromValue = structuredClone(getAtJson(baseJson, parseJsonPointer(op.from)));
|
|
2939
|
-
} catch {
|
|
2940
|
-
try {
|
|
2941
|
-
compileJsonPatchToIntent(baseJson, [{
|
|
2942
|
-
op: "remove",
|
|
2943
|
-
path: op.from
|
|
2944
|
-
}], { semantics: "sequential" });
|
|
2945
|
-
} catch (error) {
|
|
2946
|
-
return withOpIndex(toApplyError$1(error), opIndex);
|
|
2947
|
-
}
|
|
2948
|
-
return withOpIndex(toApplyError$1(/* @__PURE__ */ new Error(`failed to resolve move source at ${op.from}`)), opIndex);
|
|
2949
|
-
}
|
|
2950
|
-
if (op.from === op.path) continue;
|
|
2951
|
-
const removeStep = applySequentialOp({
|
|
2952
|
-
op: "remove",
|
|
2953
|
-
path: op.from
|
|
2954
|
-
}, opIndex);
|
|
2955
|
-
if (!removeStep.ok) return removeStep;
|
|
2956
|
-
const addStep = applySequentialOp({
|
|
2957
|
-
op: "add",
|
|
2958
|
-
path: op.path,
|
|
2959
|
-
value: fromValue
|
|
2960
|
-
}, opIndex);
|
|
2961
|
-
if (!addStep.ok) return addStep;
|
|
2962
|
-
continue;
|
|
2963
|
-
}
|
|
2964
|
-
const step = applySequentialOp(op, opIndex);
|
|
3246
|
+
const session = { pointerCache: /* @__PURE__ */ new Map() };
|
|
3247
|
+
for (const [opIndex, op] of options.patch.entries()) {
|
|
3248
|
+
const step = applySequentialPatchOp(options, evalTestAgainst === "base" ? shadowBase : options.head, op, opIndex, evalTestAgainst, shadowDot, shadowBump, session);
|
|
2965
3249
|
if (!step.ok) return step;
|
|
2966
3250
|
}
|
|
2967
3251
|
return { ok: true };
|
|
2968
3252
|
}
|
|
2969
|
-
function
|
|
2970
|
-
if (
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
3253
|
+
function applySequentialPatchOp(options, compileBase, op, opIndex, evalTestAgainst, shadowDot, shadowBump, session) {
|
|
3254
|
+
if (op.op === "move") {
|
|
3255
|
+
if (op.from === op.path) {
|
|
3256
|
+
const pathCheck = resolveValueAtPointerInDoc$1(compileBase, op.from, opIndex, session.pointerCache);
|
|
3257
|
+
if (!pathCheck.ok) return pathCheck;
|
|
3258
|
+
return { ok: true };
|
|
3259
|
+
}
|
|
3260
|
+
const fromResolved = resolveValueAtPointerInDoc$1(compileBase, op.from, opIndex, session.pointerCache);
|
|
3261
|
+
if (!fromResolved.ok) return fromResolved;
|
|
3262
|
+
const removeStep = applySingleSequentialPatchStep(options, compileBase, {
|
|
3263
|
+
op: "remove",
|
|
3264
|
+
path: op.from
|
|
3265
|
+
}, opIndex, evalTestAgainst, shadowDot, shadowBump, session);
|
|
3266
|
+
if (!removeStep.ok) return removeStep;
|
|
3267
|
+
return applySingleSequentialPatchStep(options, compileBase, {
|
|
3268
|
+
op: "add",
|
|
3269
|
+
path: op.path,
|
|
3270
|
+
value: structuredClone(fromResolved.value)
|
|
3271
|
+
}, opIndex, evalTestAgainst, shadowDot, shadowBump, session);
|
|
3272
|
+
}
|
|
3273
|
+
if (op.op === "copy") {
|
|
3274
|
+
const fromResolved = resolveValueAtPointerInDoc$1(compileBase, op.from, opIndex, session.pointerCache);
|
|
3275
|
+
if (!fromResolved.ok) return fromResolved;
|
|
3276
|
+
return applySingleSequentialPatchStep(options, compileBase, {
|
|
3277
|
+
op: "add",
|
|
3278
|
+
path: op.path,
|
|
3279
|
+
value: structuredClone(fromResolved.value)
|
|
3280
|
+
}, opIndex, evalTestAgainst, shadowDot, shadowBump, session);
|
|
3281
|
+
}
|
|
3282
|
+
return applySingleSequentialPatchStep(options, compileBase, op, opIndex, evalTestAgainst, shadowDot, shadowBump, session);
|
|
2975
3283
|
}
|
|
2976
|
-
function
|
|
2977
|
-
|
|
3284
|
+
function applySingleSequentialPatchStep(options, compileBase, op, opIndex, evalTestAgainst, shadowDot, shadowBump, session) {
|
|
3285
|
+
const compiled = compilePreparedSingleIntentFromDoc$1(compileBase, op, session.pointerCache, opIndex);
|
|
3286
|
+
if (!compiled.ok) return compiled;
|
|
3287
|
+
const headStep = applyIntentsToCrdt(compileBase, options.head, compiled.intents, options.newDot, evalTestAgainst, options.bumpCounterAbove, { strictParents: options.strictParents });
|
|
3288
|
+
if (!headStep.ok) return withOpIndex$1(headStep, opIndex);
|
|
3289
|
+
if (op.op === "test") return { ok: true };
|
|
3290
|
+
if (evalTestAgainst === "head") return { ok: true };
|
|
3291
|
+
const shadowStep = applyIntentsToCrdt(compileBase, compileBase, compiled.intents, shadowDot, "base", shadowBump, { strictParents: options.strictParents });
|
|
3292
|
+
if (!shadowStep.ok) return withOpIndex$1(shadowStep, opIndex);
|
|
3293
|
+
return { ok: true };
|
|
2978
3294
|
}
|
|
2979
|
-
function
|
|
2980
|
-
|
|
2981
|
-
if (
|
|
3295
|
+
function resolveValueAtPointerInDoc$1(doc, pointer, opIndex, pointerCache) {
|
|
3296
|
+
const parsedPath = parsePointerWithCache$1(pointer, pointerCache, opIndex);
|
|
3297
|
+
if (!parsedPath.ok) return parsedPath;
|
|
3298
|
+
const resolved = resolveNodeAtPath$1(doc.root, parsedPath.path);
|
|
3299
|
+
if (!resolved.ok) return {
|
|
2982
3300
|
ok: false,
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
path: error.path,
|
|
2987
|
-
opIndex: error.opIndex
|
|
3301
|
+
...resolved.error,
|
|
3302
|
+
path: pointer,
|
|
3303
|
+
opIndex
|
|
2988
3304
|
};
|
|
2989
3305
|
return {
|
|
2990
|
-
ok:
|
|
2991
|
-
|
|
2992
|
-
reason: "INVALID_PATCH",
|
|
2993
|
-
message: error instanceof Error ? error.message : "failed to compile/apply patch"
|
|
3306
|
+
ok: true,
|
|
3307
|
+
value: nodeToJsonForPatch(resolved.node)
|
|
2994
3308
|
};
|
|
2995
3309
|
}
|
|
2996
|
-
function
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3310
|
+
function compilePreparedSingleIntentFromDoc$1(baseDoc, op, pointerCache, opIndex) {
|
|
3311
|
+
const parsedPath = parsePointerWithCache$1(op.path, pointerCache, opIndex);
|
|
3312
|
+
if (!parsedPath.ok) return parsedPath;
|
|
3313
|
+
const path = parsedPath.path;
|
|
3314
|
+
if (op.op === "test") return {
|
|
3315
|
+
ok: true,
|
|
3316
|
+
intents: [{
|
|
3317
|
+
t: "Test",
|
|
3318
|
+
path,
|
|
3319
|
+
value: op.value
|
|
3320
|
+
}]
|
|
3321
|
+
};
|
|
3322
|
+
if (path.length === 0) {
|
|
3323
|
+
if (op.op === "remove") return {
|
|
3324
|
+
ok: false,
|
|
3325
|
+
code: 409,
|
|
3326
|
+
reason: "INVALID_TARGET",
|
|
3327
|
+
message: "remove at root path is not supported in RFC-compliant mode",
|
|
3328
|
+
path: op.path,
|
|
3329
|
+
opIndex
|
|
3330
|
+
};
|
|
3331
|
+
return {
|
|
3332
|
+
ok: true,
|
|
3333
|
+
intents: [{
|
|
3334
|
+
t: "ObjSet",
|
|
3335
|
+
path: [],
|
|
3336
|
+
key: ROOT_KEY,
|
|
3337
|
+
value: op.value
|
|
3338
|
+
}]
|
|
3339
|
+
};
|
|
3340
|
+
}
|
|
3341
|
+
const parentPath = path.slice(0, -1);
|
|
3342
|
+
const parentPointer = stringifyJsonPointer(parentPath);
|
|
3343
|
+
const key = path[path.length - 1];
|
|
3344
|
+
const resolvedParent = parentPath.length === 0 ? {
|
|
3345
|
+
ok: true,
|
|
3346
|
+
node: baseDoc.root
|
|
3347
|
+
} : resolveNodeAtPath$1(baseDoc.root, parentPath);
|
|
3348
|
+
if (!resolvedParent.ok) return {
|
|
3349
|
+
ok: false,
|
|
3350
|
+
...resolvedParent.error,
|
|
3351
|
+
path: parentPointer,
|
|
3352
|
+
opIndex
|
|
3353
|
+
};
|
|
3354
|
+
const parentNode = resolvedParent.node;
|
|
3355
|
+
if (parentNode.kind === "seq") {
|
|
3356
|
+
const parsedIndex = parseArrayIndexTokenForDoc$1(key, op.op, op.path, opIndex);
|
|
3357
|
+
if (!parsedIndex.ok) return parsedIndex;
|
|
3358
|
+
const boundedIndex = validateArrayIndexBounds$1(parsedIndex.index, op.op, rgaLength(parentNode), op.path, opIndex);
|
|
3359
|
+
if (!boundedIndex.ok) return boundedIndex;
|
|
3360
|
+
if (op.op === "add") return {
|
|
3361
|
+
ok: true,
|
|
3362
|
+
intents: [{
|
|
3363
|
+
t: "ArrInsert",
|
|
3364
|
+
path: parentPath,
|
|
3365
|
+
index: boundedIndex.index,
|
|
3366
|
+
value: op.value
|
|
3367
|
+
}]
|
|
3368
|
+
};
|
|
3369
|
+
if (op.op === "remove") return {
|
|
3370
|
+
ok: true,
|
|
3371
|
+
intents: [{
|
|
3372
|
+
t: "ArrDelete",
|
|
3373
|
+
path: parentPath,
|
|
3374
|
+
index: boundedIndex.index
|
|
3375
|
+
}]
|
|
3376
|
+
};
|
|
3377
|
+
return {
|
|
3378
|
+
ok: true,
|
|
3379
|
+
intents: [{
|
|
3380
|
+
t: "ArrReplace",
|
|
3381
|
+
path: parentPath,
|
|
3382
|
+
index: boundedIndex.index,
|
|
3383
|
+
value: op.value
|
|
3384
|
+
}]
|
|
3385
|
+
};
|
|
3386
|
+
}
|
|
3387
|
+
if (parentNode.kind !== "obj") return {
|
|
3388
|
+
ok: false,
|
|
3389
|
+
code: 409,
|
|
3390
|
+
reason: "INVALID_TARGET",
|
|
3391
|
+
message: `expected object or array parent at ${parentPointer}`,
|
|
3392
|
+
path: parentPointer,
|
|
3393
|
+
opIndex
|
|
3394
|
+
};
|
|
3395
|
+
if (key === "__proto__") return {
|
|
3396
|
+
ok: false,
|
|
3397
|
+
code: 409,
|
|
3398
|
+
reason: "INVALID_POINTER",
|
|
3399
|
+
message: `unsafe object key at ${op.path}`,
|
|
3400
|
+
path: op.path,
|
|
3401
|
+
opIndex
|
|
3402
|
+
};
|
|
3403
|
+
const entry = parentNode.entries.get(key);
|
|
3404
|
+
if ((op.op === "replace" || op.op === "remove") && !entry) return {
|
|
3405
|
+
ok: false,
|
|
3406
|
+
code: 409,
|
|
3407
|
+
reason: "MISSING_TARGET",
|
|
3408
|
+
message: `missing key ${key} at ${parentPointer}`,
|
|
3409
|
+
path: op.path,
|
|
3410
|
+
opIndex
|
|
3411
|
+
};
|
|
3412
|
+
if (op.op === "remove") return {
|
|
3413
|
+
ok: true,
|
|
3414
|
+
intents: [{
|
|
3415
|
+
t: "ObjRemove",
|
|
3416
|
+
path: parentPath,
|
|
3417
|
+
key
|
|
3418
|
+
}]
|
|
3419
|
+
};
|
|
3420
|
+
return {
|
|
3421
|
+
ok: true,
|
|
3422
|
+
intents: [{
|
|
3423
|
+
t: "ObjSet",
|
|
3424
|
+
path: parentPath,
|
|
3425
|
+
key,
|
|
3426
|
+
value: op.value,
|
|
3427
|
+
mode: op.op
|
|
3428
|
+
}]
|
|
3429
|
+
};
|
|
3430
|
+
}
|
|
3431
|
+
function parsePointerWithCache$1(pointer, pointerCache, opIndex) {
|
|
3432
|
+
const cachedPath = pointerCache.get(pointer);
|
|
3433
|
+
if (cachedPath !== void 0) return {
|
|
3434
|
+
ok: true,
|
|
3435
|
+
path: cachedPath.slice()
|
|
3436
|
+
};
|
|
3437
|
+
try {
|
|
3438
|
+
const parsedPath = parseJsonPointer(pointer);
|
|
3439
|
+
pointerCache.set(pointer, parsedPath);
|
|
3440
|
+
return {
|
|
3441
|
+
ok: true,
|
|
3442
|
+
path: parsedPath.slice()
|
|
3443
|
+
};
|
|
3444
|
+
} catch (error) {
|
|
3445
|
+
return {
|
|
3446
|
+
ok: false,
|
|
3447
|
+
code: 409,
|
|
3448
|
+
reason: "INVALID_POINTER",
|
|
3449
|
+
message: error instanceof Error ? error.message : "invalid pointer",
|
|
3450
|
+
path: pointer,
|
|
3451
|
+
opIndex
|
|
3452
|
+
};
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
function resolveNodeAtPath$1(root, path) {
|
|
3456
|
+
let current = root;
|
|
3457
|
+
for (const segment of path) {
|
|
3458
|
+
if (current.kind === "obj") {
|
|
3459
|
+
const entry = current.entries.get(segment);
|
|
3460
|
+
if (!entry) return {
|
|
3461
|
+
ok: false,
|
|
3462
|
+
error: {
|
|
3463
|
+
code: 409,
|
|
3464
|
+
reason: "MISSING_PARENT",
|
|
3465
|
+
message: `Missing key '${segment}'`
|
|
3466
|
+
}
|
|
3467
|
+
};
|
|
3468
|
+
current = entry.node;
|
|
3469
|
+
continue;
|
|
3470
|
+
}
|
|
3471
|
+
if (current.kind === "seq") {
|
|
3472
|
+
if (!ARRAY_INDEX_TOKEN_PATTERN.test(segment)) return {
|
|
3473
|
+
ok: false,
|
|
3474
|
+
error: {
|
|
3475
|
+
code: 409,
|
|
3476
|
+
reason: "INVALID_POINTER",
|
|
3477
|
+
message: `Expected array index, got '${segment}'`
|
|
3478
|
+
}
|
|
3479
|
+
};
|
|
3480
|
+
const index = Number(segment);
|
|
3481
|
+
if (!Number.isSafeInteger(index)) return {
|
|
3482
|
+
ok: false,
|
|
3483
|
+
error: {
|
|
3484
|
+
code: 409,
|
|
3485
|
+
reason: "OUT_OF_BOUNDS",
|
|
3486
|
+
message: `Index out of bounds at '${segment}'`
|
|
3487
|
+
}
|
|
3488
|
+
};
|
|
3489
|
+
const elemId = rgaIdAtIndex(current, index);
|
|
3490
|
+
if (elemId === void 0) return {
|
|
3491
|
+
ok: false,
|
|
3492
|
+
error: {
|
|
3493
|
+
code: 409,
|
|
3494
|
+
reason: "OUT_OF_BOUNDS",
|
|
3495
|
+
message: `Index out of bounds at '${segment}'`
|
|
3496
|
+
}
|
|
3497
|
+
};
|
|
3498
|
+
current = current.elems.get(elemId).value;
|
|
3499
|
+
continue;
|
|
3500
|
+
}
|
|
3501
|
+
return {
|
|
3502
|
+
ok: false,
|
|
3503
|
+
error: {
|
|
3504
|
+
code: 409,
|
|
3505
|
+
reason: "INVALID_TARGET",
|
|
3506
|
+
message: `Cannot traverse into non-container at '${segment}'`
|
|
3507
|
+
}
|
|
3508
|
+
};
|
|
3509
|
+
}
|
|
3510
|
+
return {
|
|
3511
|
+
ok: true,
|
|
3512
|
+
node: current
|
|
3513
|
+
};
|
|
3514
|
+
}
|
|
3515
|
+
function parseArrayIndexTokenForDoc$1(token, op, path, opIndex) {
|
|
3516
|
+
if (token === "-") {
|
|
3517
|
+
if (op !== "add") return {
|
|
3518
|
+
ok: false,
|
|
3519
|
+
code: 409,
|
|
3520
|
+
reason: "INVALID_POINTER",
|
|
3521
|
+
message: `'-' index is only valid for add at ${path}`,
|
|
3522
|
+
path,
|
|
3523
|
+
opIndex
|
|
3524
|
+
};
|
|
3525
|
+
return {
|
|
3526
|
+
ok: true,
|
|
3527
|
+
index: Number.POSITIVE_INFINITY
|
|
3528
|
+
};
|
|
3529
|
+
}
|
|
3530
|
+
if (!ARRAY_INDEX_TOKEN_PATTERN.test(token)) return {
|
|
3531
|
+
ok: false,
|
|
3532
|
+
code: 409,
|
|
3533
|
+
reason: "INVALID_POINTER",
|
|
3534
|
+
message: `expected array index at ${path}`,
|
|
3535
|
+
path,
|
|
3536
|
+
opIndex
|
|
3537
|
+
};
|
|
3538
|
+
const index = Number(token);
|
|
3539
|
+
if (!Number.isSafeInteger(index)) return {
|
|
3540
|
+
ok: false,
|
|
3541
|
+
code: 409,
|
|
3542
|
+
reason: "OUT_OF_BOUNDS",
|
|
3543
|
+
message: `array index is too large at ${path}`,
|
|
3544
|
+
path,
|
|
3545
|
+
opIndex
|
|
3546
|
+
};
|
|
3547
|
+
return {
|
|
3548
|
+
ok: true,
|
|
3549
|
+
index
|
|
3550
|
+
};
|
|
3551
|
+
}
|
|
3552
|
+
function validateArrayIndexBounds$1(index, op, arrLength, path, opIndex) {
|
|
3553
|
+
if (op === "add") {
|
|
3554
|
+
if (index === Number.POSITIVE_INFINITY) return {
|
|
3555
|
+
ok: true,
|
|
3556
|
+
index
|
|
3557
|
+
};
|
|
3558
|
+
if (index > arrLength) return {
|
|
3559
|
+
ok: false,
|
|
3560
|
+
code: 409,
|
|
3561
|
+
reason: "OUT_OF_BOUNDS",
|
|
3562
|
+
message: `index out of bounds at ${path}; expected 0..${arrLength}`,
|
|
3563
|
+
path,
|
|
3564
|
+
opIndex
|
|
3565
|
+
};
|
|
3566
|
+
} else if (index >= arrLength) return {
|
|
3567
|
+
ok: false,
|
|
3568
|
+
code: 409,
|
|
3569
|
+
reason: "OUT_OF_BOUNDS",
|
|
3570
|
+
message: `index out of bounds at ${path}; expected 0..${Math.max(arrLength - 1, 0)}`,
|
|
3571
|
+
path,
|
|
3572
|
+
opIndex
|
|
3573
|
+
};
|
|
3574
|
+
return {
|
|
3575
|
+
ok: true,
|
|
3576
|
+
index
|
|
3577
|
+
};
|
|
3578
|
+
}
|
|
3579
|
+
function withOpIndex$1(error, opIndex) {
|
|
3580
|
+
if (error.opIndex !== void 0) return error;
|
|
3581
|
+
return {
|
|
3582
|
+
...error,
|
|
3583
|
+
opIndex
|
|
3584
|
+
};
|
|
3585
|
+
}
|
|
3586
|
+
function isJsonPatchToCrdtOptions(value) {
|
|
3587
|
+
return typeof value === "object" && value !== null && "base" in value && "head" in value && "patch" in value && "newDot" in value;
|
|
3588
|
+
}
|
|
3589
|
+
function toApplyError$1(error) {
|
|
3590
|
+
if (error instanceof TraversalDepthError) return toDepthApplyError(error);
|
|
3591
|
+
if (error instanceof PatchCompileError) return {
|
|
3592
|
+
ok: false,
|
|
3593
|
+
code: 409,
|
|
3594
|
+
reason: error.reason,
|
|
3595
|
+
message: error.message,
|
|
3596
|
+
path: error.path,
|
|
3597
|
+
opIndex: error.opIndex
|
|
3598
|
+
};
|
|
3599
|
+
return {
|
|
3600
|
+
ok: false,
|
|
3601
|
+
code: 409,
|
|
3602
|
+
reason: "INVALID_PATCH",
|
|
3603
|
+
message: error instanceof Error ? error.message : "failed to compile/apply patch"
|
|
3604
|
+
};
|
|
3605
|
+
}
|
|
3606
|
+
function assertNever(_value, message) {
|
|
3607
|
+
throw new Error(message);
|
|
3608
|
+
}
|
|
3609
|
+
|
|
3610
|
+
//#endregion
|
|
3611
|
+
//#region src/state.ts
|
|
3612
|
+
/** Error thrown when a JSON Patch cannot be applied. Includes structured conflict metadata. */
|
|
3613
|
+
var PatchError = class extends Error {
|
|
3614
|
+
code;
|
|
3615
|
+
reason;
|
|
3616
|
+
budget;
|
|
3617
|
+
limit;
|
|
3618
|
+
actual;
|
|
3619
|
+
path;
|
|
3620
|
+
opIndex;
|
|
3621
|
+
constructor(errorOrMessage, code = 409, reason = "INVALID_PATCH") {
|
|
3622
|
+
super(typeof errorOrMessage === "string" ? errorOrMessage : errorOrMessage.message);
|
|
3623
|
+
this.name = "PatchError";
|
|
3624
|
+
if (typeof errorOrMessage === "string") {
|
|
3625
|
+
this.code = code;
|
|
3626
|
+
this.reason = reason;
|
|
3627
|
+
return;
|
|
3628
|
+
}
|
|
3629
|
+
this.code = errorOrMessage.code;
|
|
3017
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
|
+
}
|
|
3018
3636
|
this.path = errorOrMessage.path;
|
|
3019
3637
|
this.opIndex = errorOrMessage.opIndex;
|
|
3020
3638
|
}
|
|
@@ -3079,26 +3697,27 @@ function applyPatchInPlace(state, patch, options = {}) {
|
|
|
3079
3697
|
}
|
|
3080
3698
|
/** Non-throwing immutable patch application variant. */
|
|
3081
3699
|
function tryApplyPatch(state, patch, options = {}) {
|
|
3082
|
-
const nextState = {
|
|
3083
|
-
doc: cloneDoc(state.doc),
|
|
3084
|
-
clock: cloneClock(state.clock)
|
|
3085
|
-
};
|
|
3086
3700
|
try {
|
|
3701
|
+
throwIfAborted(options.signal);
|
|
3702
|
+
const nextState = {
|
|
3703
|
+
doc: cloneDoc(state.doc),
|
|
3704
|
+
clock: cloneClock(state.clock)
|
|
3705
|
+
};
|
|
3087
3706
|
const result = applyPatchInternal(nextState, patch, options, "batch");
|
|
3088
3707
|
if (!result.ok) return {
|
|
3089
3708
|
ok: false,
|
|
3090
3709
|
error: result
|
|
3091
3710
|
};
|
|
3711
|
+
return {
|
|
3712
|
+
ok: true,
|
|
3713
|
+
state: nextState
|
|
3714
|
+
};
|
|
3092
3715
|
} catch (error) {
|
|
3093
3716
|
return {
|
|
3094
3717
|
ok: false,
|
|
3095
3718
|
error: toApplyError(error)
|
|
3096
3719
|
};
|
|
3097
3720
|
}
|
|
3098
|
-
return {
|
|
3099
|
-
ok: true,
|
|
3100
|
-
state: nextState
|
|
3101
|
-
};
|
|
3102
3721
|
}
|
|
3103
3722
|
/** Non-throwing in-place patch application variant. */
|
|
3104
3723
|
function tryApplyPatchInPlace(state, patch, options = {}) {
|
|
@@ -3129,10 +3748,13 @@ function tryApplyPatchInPlace(state, patch, options = {}) {
|
|
|
3129
3748
|
* Does not mutate caller-provided values.
|
|
3130
3749
|
*/
|
|
3131
3750
|
function validateJsonPatch(base, patch, options = {}) {
|
|
3132
|
-
const result =
|
|
3751
|
+
const result = tryApplyPatchInPlace(createState(base, {
|
|
3133
3752
|
actor: "__validate__",
|
|
3134
3753
|
jsonValidation: options.jsonValidation
|
|
3135
|
-
}), patch,
|
|
3754
|
+
}), patch, {
|
|
3755
|
+
...options,
|
|
3756
|
+
atomic: false
|
|
3757
|
+
});
|
|
3136
3758
|
if (!result.ok) return {
|
|
3137
3759
|
ok: false,
|
|
3138
3760
|
error: result.error
|
|
@@ -3175,28 +3797,27 @@ function toApplyPatchOptionsForActor(options) {
|
|
|
3175
3797
|
testAgainst: options.testAgainst,
|
|
3176
3798
|
strictParents: options.strictParents,
|
|
3177
3799
|
jsonValidation: options.jsonValidation,
|
|
3800
|
+
signal: options.signal,
|
|
3178
3801
|
base: options.base ? {
|
|
3179
3802
|
doc: options.base,
|
|
3180
3803
|
clock: createClock("__base__", 0)
|
|
3181
3804
|
} : void 0
|
|
3182
3805
|
};
|
|
3183
3806
|
}
|
|
3184
|
-
function applyPatchInternal(state, patch, options,
|
|
3807
|
+
function applyPatchInternal(state, patch, options, _execution) {
|
|
3808
|
+
throwIfAborted(options.signal);
|
|
3185
3809
|
const preparedPatch = preparePatchPayloadsSafe(patch, options.jsonValidation ?? "none");
|
|
3186
3810
|
if (!preparedPatch.ok) return preparedPatch;
|
|
3811
|
+
createBudgetMeter(options.resourceBudget)?.count("patchOperations", preparedPatch.patch.length);
|
|
3187
3812
|
const runtimePatch = preparedPatch.patch;
|
|
3188
3813
|
if ((options.semantics ?? "sequential") === "sequential") {
|
|
3189
|
-
if (!options.base && execution === "batch") {
|
|
3190
|
-
const compiled = compilePreparedIntents(materialize(state.doc.root), runtimePatch, "sequential");
|
|
3191
|
-
if (!compiled.ok) return compiled;
|
|
3192
|
-
return applyIntentsToCrdt(state.doc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
|
|
3193
|
-
}
|
|
3194
3814
|
const explicitBaseState = options.base ? {
|
|
3195
3815
|
doc: cloneDoc(options.base.doc),
|
|
3196
3816
|
clock: createClock("__base__", 0)
|
|
3197
3817
|
} : null;
|
|
3198
3818
|
const session = { pointerCache: /* @__PURE__ */ new Map() };
|
|
3199
3819
|
for (const [opIndex, op] of runtimePatch.entries()) {
|
|
3820
|
+
throwIfAborted(options.signal);
|
|
3200
3821
|
const step = applyPatchOpSequential(state, op, options, explicitBaseState ? explicitBaseState.doc : state.doc, explicitBaseState, opIndex, session);
|
|
3201
3822
|
if (!step.ok) return step;
|
|
3202
3823
|
}
|
|
@@ -3244,10 +3865,10 @@ function applySinglePatchOpSequentialStep(state, baseDoc, op, options, explicitB
|
|
|
3244
3865
|
const compiled = compilePreparedSingleIntentFromDoc(baseDoc, op, session.pointerCache, opIndex);
|
|
3245
3866
|
if (!compiled.ok) return compiled;
|
|
3246
3867
|
const headStep = applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
|
|
3247
|
-
if (!headStep.ok) return headStep;
|
|
3868
|
+
if (!headStep.ok) return withOpIndex(headStep, opIndex);
|
|
3248
3869
|
if (explicitBaseState && op.op !== "test") {
|
|
3249
3870
|
const shadowStep = applyIntentsToCrdt(explicitBaseState.doc, explicitBaseState.doc, compiled.intents, () => explicitBaseState.clock.next(), "base", (ctr) => bumpClockCounter(explicitBaseState, ctr), { strictParents: options.strictParents });
|
|
3250
|
-
if (!shadowStep.ok) return shadowStep;
|
|
3871
|
+
if (!shadowStep.ok) return withOpIndex(shadowStep, opIndex);
|
|
3251
3872
|
}
|
|
3252
3873
|
return { ok: true };
|
|
3253
3874
|
}
|
|
@@ -3255,7 +3876,7 @@ function applySinglePatchOpExplicitShadowStep(explicitBaseState, op, options, op
|
|
|
3255
3876
|
const compiled = compilePreparedSingleIntentFromDoc(explicitBaseState.doc, op, session.pointerCache, opIndex);
|
|
3256
3877
|
if (!compiled.ok) return compiled;
|
|
3257
3878
|
const shadowStep = applyIntentsToCrdt(explicitBaseState.doc, explicitBaseState.doc, compiled.intents, () => explicitBaseState.clock.next(), "base", (ctr) => bumpClockCounter(explicitBaseState, ctr), { strictParents: options.strictParents });
|
|
3258
|
-
if (!shadowStep.ok) return shadowStep;
|
|
3879
|
+
if (!shadowStep.ok) return withOpIndex(shadowStep, opIndex);
|
|
3259
3880
|
return { ok: true };
|
|
3260
3881
|
}
|
|
3261
3882
|
function resolveValueAtPointerInDoc(doc, pointer, opIndex, pointerCache) {
|
|
@@ -3535,9 +4156,9 @@ function validateArrayIndexBounds(index, op, arrLength, path, opIndex) {
|
|
|
3535
4156
|
function bumpClockCounter(state, ctr) {
|
|
3536
4157
|
if (state.clock.ctr < ctr) state.clock.ctr = ctr;
|
|
3537
4158
|
}
|
|
3538
|
-
function compilePreparedIntents(baseJson, patch, semantics = "sequential", pointerCache, opIndexOffset = 0) {
|
|
4159
|
+
function compilePreparedIntents(baseJson, patch, semantics = "sequential", pointerCache, opIndexOffset = 0, budgetMeter) {
|
|
3539
4160
|
try {
|
|
3540
|
-
const compileOptions = toCompilePatchOptions(semantics, pointerCache, opIndexOffset);
|
|
4161
|
+
const compileOptions = toCompilePatchOptions(semantics, pointerCache, opIndexOffset, budgetMeter);
|
|
3541
4162
|
if (patch.length === 1) return {
|
|
3542
4163
|
ok: true,
|
|
3543
4164
|
intents: compileJsonPatchOpToIntent(baseJson, patch[0], compileOptions)
|
|
@@ -3550,11 +4171,13 @@ function compilePreparedIntents(baseJson, patch, semantics = "sequential", point
|
|
|
3550
4171
|
return toApplyError(error);
|
|
3551
4172
|
}
|
|
3552
4173
|
}
|
|
3553
|
-
function toCompilePatchOptions(semantics, pointerCache, opIndexOffset = 0) {
|
|
4174
|
+
function toCompilePatchOptions(semantics, pointerCache, opIndexOffset = 0, budgetMeter) {
|
|
3554
4175
|
return {
|
|
3555
4176
|
semantics,
|
|
4177
|
+
resourceBudget: void 0,
|
|
3556
4178
|
pointerCache,
|
|
3557
|
-
opIndexOffset
|
|
4179
|
+
opIndexOffset,
|
|
4180
|
+
budgetMeter
|
|
3558
4181
|
};
|
|
3559
4182
|
}
|
|
3560
4183
|
function preparePatchPayloadsSafe(patch, mode) {
|
|
@@ -3602,6 +4225,8 @@ function mergePointerPaths(basePointer, nestedPointer) {
|
|
|
3602
4225
|
return `${basePointer}${nestedPointer}`;
|
|
3603
4226
|
}
|
|
3604
4227
|
function toApplyError(error) {
|
|
4228
|
+
if (error instanceof OperationCancelledError) return toCancellationApplyError(error);
|
|
4229
|
+
if (error instanceof ResourceBudgetError) return toBudgetApplyError(error);
|
|
3605
4230
|
if (error instanceof TraversalDepthError) return toDepthApplyError(error);
|
|
3606
4231
|
if (error instanceof PatchCompileError) return {
|
|
3607
4232
|
ok: false,
|
|
@@ -3618,6 +4243,13 @@ function toApplyError(error) {
|
|
|
3618
4243
|
message: error instanceof Error ? error.message : "failed to compile patch"
|
|
3619
4244
|
};
|
|
3620
4245
|
}
|
|
4246
|
+
function withOpIndex(error, opIndex) {
|
|
4247
|
+
if (error.opIndex !== void 0) return error;
|
|
4248
|
+
return {
|
|
4249
|
+
...error,
|
|
4250
|
+
opIndex
|
|
4251
|
+
};
|
|
4252
|
+
}
|
|
3621
4253
|
function toPointerParseApplyError(error, pointer, opIndex) {
|
|
3622
4254
|
return {
|
|
3623
4255
|
ok: false,
|
|
@@ -3629,6 +4261,68 @@ function toPointerParseApplyError(error, pointer, opIndex) {
|
|
|
3629
4261
|
};
|
|
3630
4262
|
}
|
|
3631
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
|
+
|
|
3632
4326
|
//#endregion
|
|
3633
4327
|
//#region src/serialize.ts
|
|
3634
4328
|
const HEAD_ELEM_ID = "HEAD";
|
|
@@ -3664,18 +4358,39 @@ function serializeDoc(doc) {
|
|
|
3664
4358
|
};
|
|
3665
4359
|
}
|
|
3666
4360
|
/** Reconstruct a CRDT document from its serialized form. */
|
|
3667
|
-
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);
|
|
3668
4366
|
const raw = readSerializedDocEnvelope(data);
|
|
3669
4367
|
if (!("root" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/root", "serialized doc is missing root");
|
|
3670
|
-
|
|
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;
|
|
3671
4372
|
}
|
|
3672
4373
|
/** Non-throwing `deserializeDoc` variant with typed validation details. */
|
|
3673
|
-
function tryDeserializeDoc(data) {
|
|
4374
|
+
function tryDeserializeDoc(data, options = {}) {
|
|
3674
4375
|
try {
|
|
3675
4376
|
return {
|
|
3676
4377
|
ok: true,
|
|
3677
|
-
doc: deserializeDoc(data)
|
|
4378
|
+
doc: deserializeDoc(data, options)
|
|
4379
|
+
};
|
|
4380
|
+
} catch (error) {
|
|
4381
|
+
const deserializeError = toDeserializeFailure(error);
|
|
4382
|
+
if (deserializeError) return {
|
|
4383
|
+
ok: false,
|
|
4384
|
+
error: deserializeError
|
|
3678
4385
|
};
|
|
4386
|
+
throw error;
|
|
4387
|
+
}
|
|
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 };
|
|
3679
4394
|
} catch (error) {
|
|
3680
4395
|
const deserializeError = toDeserializeFailure(error);
|
|
3681
4396
|
if (deserializeError) return {
|
|
@@ -3702,27 +4417,56 @@ function serializeState(state) {
|
|
|
3702
4417
|
* May throw `TraversalDepthError` when the payload exceeds the maximum
|
|
3703
4418
|
* supported nesting depth.
|
|
3704
4419
|
*/
|
|
3705
|
-
function deserializeState(data) {
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
doc,
|
|
3716
|
-
|
|
3717
|
-
|
|
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
|
+
}
|
|
3718
4440
|
}
|
|
3719
4441
|
/** Non-throwing `deserializeState` variant with typed validation details. */
|
|
3720
|
-
function tryDeserializeState(data) {
|
|
4442
|
+
function tryDeserializeState(data, options = {}) {
|
|
3721
4443
|
try {
|
|
3722
4444
|
return {
|
|
3723
4445
|
ok: true,
|
|
3724
|
-
state: deserializeState(data)
|
|
4446
|
+
state: deserializeState(data, options)
|
|
4447
|
+
};
|
|
4448
|
+
} catch (error) {
|
|
4449
|
+
const deserializeError = toDeserializeFailure(error);
|
|
4450
|
+
if (deserializeError) return {
|
|
4451
|
+
ok: false,
|
|
4452
|
+
error: deserializeError
|
|
3725
4453
|
};
|
|
4454
|
+
throw error;
|
|
4455
|
+
}
|
|
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 };
|
|
3726
4470
|
} catch (error) {
|
|
3727
4471
|
const deserializeError = toDeserializeFailure(error);
|
|
3728
4472
|
if (deserializeError) return {
|
|
@@ -3794,33 +4538,55 @@ function readSerializedStateEnvelope(data) {
|
|
|
3794
4538
|
assertSerializedEnvelopeVersion(raw, "/version", SERIALIZED_STATE_VERSION, "state");
|
|
3795
4539
|
return raw;
|
|
3796
4540
|
}
|
|
3797
|
-
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);
|
|
3798
4549
|
assertTraversalDepth(depth);
|
|
4550
|
+
budgetMeter?.count("visitedNodes", 1, path);
|
|
3799
4551
|
const raw = asRecord(node, path);
|
|
3800
4552
|
const kind = readString(raw.kind, `${path}/kind`);
|
|
3801
4553
|
if (kind === "lww") {
|
|
3802
4554
|
if (!("value" in raw)) fail("INVALID_SERIALIZED_SHAPE", `${path}/value`, "lww node is missing value");
|
|
3803
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);
|
|
3804
4558
|
return {
|
|
3805
4559
|
kind: "lww",
|
|
3806
|
-
value: structuredClone(readJsonValue(raw.value, `${path}/value`, depth + 1)),
|
|
3807
|
-
dot
|
|
4560
|
+
value: structuredClone(readJsonValue(raw.value, `${path}/value`, depth + 1, budgetMeter, signal)),
|
|
4561
|
+
dot
|
|
3808
4562
|
};
|
|
3809
4563
|
}
|
|
3810
4564
|
if (kind === "obj") {
|
|
3811
4565
|
const entriesRaw = asRecord(raw.entries, `${path}/entries`);
|
|
3812
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`);
|
|
3813
4571
|
const entries = /* @__PURE__ */ new Map();
|
|
3814
4572
|
for (const [k, v] of Object.entries(entriesRaw)) {
|
|
4573
|
+
throwIfAborted(signal);
|
|
3815
4574
|
const entryPath = `${path}/entries/${k}`;
|
|
3816
4575
|
const entryRaw = asRecord(v, entryPath);
|
|
4576
|
+
const dot = readDot(entryRaw.dot, `${entryPath}/dot`);
|
|
4577
|
+
if (observed) observeVersionVectorDot(observed, dot);
|
|
3817
4578
|
entries.set(k, {
|
|
3818
|
-
node: deserializeNode(entryRaw.node, `${entryPath}/node`, depth + 1),
|
|
3819
|
-
dot
|
|
4579
|
+
node: deserializeNode(entryRaw.node, `${entryPath}/node`, depth + 1, budgetMeter, observed, signal),
|
|
4580
|
+
dot
|
|
3820
4581
|
});
|
|
3821
4582
|
}
|
|
3822
4583
|
const tombstone = /* @__PURE__ */ new Map();
|
|
3823
|
-
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
|
+
}
|
|
3824
4590
|
return {
|
|
3825
4591
|
kind: "obj",
|
|
3826
4592
|
entries,
|
|
@@ -3829,17 +4595,24 @@ function deserializeNode(node, path, depth) {
|
|
|
3829
4595
|
}
|
|
3830
4596
|
if (kind !== "seq") fail("INVALID_SERIALIZED_SHAPE", `${path}/kind`, `unsupported node kind '${kind}'`);
|
|
3831
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`);
|
|
3832
4600
|
const elems = /* @__PURE__ */ new Map();
|
|
3833
4601
|
for (const [id, rawElem] of Object.entries(elemsRaw)) {
|
|
4602
|
+
throwIfAborted(signal);
|
|
3834
4603
|
const elemPath = `${path}/elems/${id}`;
|
|
3835
4604
|
const elem = asRecord(rawElem, elemPath);
|
|
3836
4605
|
const elemId = readString(elem.id, `${elemPath}/id`);
|
|
3837
4606
|
if (elemId !== id) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/id`, `sequence element id '${elemId}' does not match key '${id}'`);
|
|
3838
4607
|
const prev = readString(elem.prev, `${elemPath}/prev`);
|
|
3839
4608
|
const tombstone = readBoolean(elem.tombstone, `${elemPath}/tombstone`);
|
|
3840
|
-
const value = deserializeNode(elem.value, `${elemPath}/value`, depth + 1);
|
|
4609
|
+
const value = deserializeNode(elem.value, `${elemPath}/value`, depth + 1, budgetMeter, observed, signal);
|
|
3841
4610
|
const insDot = readDot(elem.insDot, `${elemPath}/insDot`);
|
|
3842
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
|
+
}
|
|
3843
4616
|
if (dotToElemId(insDot) !== id) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/insDot`, "sequence element id must match its insertion dot");
|
|
3844
4617
|
if (!tombstone && delDot) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/delDot`, "live sequence elements must not include delete metadata");
|
|
3845
4618
|
elems.set(id, {
|
|
@@ -3852,6 +4625,7 @@ function deserializeNode(node, path, depth) {
|
|
|
3852
4625
|
});
|
|
3853
4626
|
}
|
|
3854
4627
|
for (const elem of elems.values()) {
|
|
4628
|
+
throwIfAborted(signal);
|
|
3855
4629
|
if (elem.prev === elem.id) fail("INVALID_SERIALIZED_INVARIANT", `${path}/elems/${elem.id}/prev`, "sequence element cannot reference itself as predecessor");
|
|
3856
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`);
|
|
3857
4631
|
}
|
|
@@ -3861,6 +4635,66 @@ function deserializeNode(node, path, depth) {
|
|
|
3861
4635
|
elems
|
|
3862
4636
|
};
|
|
3863
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
|
+
}
|
|
3864
4698
|
function assertAcyclicRgaPredecessors(elems, path) {
|
|
3865
4699
|
const visitState = /* @__PURE__ */ new Map();
|
|
3866
4700
|
for (const startId of elems.keys()) {
|
|
@@ -3881,6 +4715,26 @@ function assertAcyclicRgaPredecessors(elems, path) {
|
|
|
3881
4715
|
for (const id of trail) visitState.set(id, 2);
|
|
3882
4716
|
}
|
|
3883
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
|
+
}
|
|
3884
4738
|
function asRecord(value, path) {
|
|
3885
4739
|
if (!isRecord(value)) fail("INVALID_SERIALIZED_SHAPE", path, "expected object");
|
|
3886
4740
|
return value;
|
|
@@ -3918,28 +4772,43 @@ function readBoolean(value, path) {
|
|
|
3918
4772
|
if (typeof value !== "boolean") fail("INVALID_SERIALIZED_SHAPE", path, "expected boolean");
|
|
3919
4773
|
return value;
|
|
3920
4774
|
}
|
|
3921
|
-
function readJsonValue(value, path, depth) {
|
|
3922
|
-
assertJsonValue(value, path, depth);
|
|
4775
|
+
function readJsonValue(value, path, depth, budgetMeter, signal) {
|
|
4776
|
+
assertJsonValue(value, path, depth, budgetMeter, signal);
|
|
3923
4777
|
return value;
|
|
3924
4778
|
}
|
|
3925
|
-
function assertJsonValue(value, path, depth) {
|
|
4779
|
+
function assertJsonValue(value, path, depth, budgetMeter, signal) {
|
|
4780
|
+
throwIfAborted(signal);
|
|
3926
4781
|
assertTraversalDepth(depth);
|
|
4782
|
+
budgetMeter?.count("visitedNodes", 1, path);
|
|
3927
4783
|
if (value === null || typeof value === "string" || typeof value === "boolean") return;
|
|
3928
4784
|
if (typeof value === "number") {
|
|
3929
4785
|
if (!Number.isFinite(value)) fail("INVALID_SERIALIZED_SHAPE", path, "json number must be finite");
|
|
3930
4786
|
return;
|
|
3931
4787
|
}
|
|
3932
4788
|
if (Array.isArray(value)) {
|
|
3933
|
-
|
|
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
|
+
}
|
|
3934
4795
|
return;
|
|
3935
4796
|
}
|
|
3936
4797
|
if (!isRecord(value)) fail("INVALID_SERIALIZED_SHAPE", path, "expected JSON value");
|
|
3937
|
-
|
|
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
|
+
}
|
|
3938
4805
|
}
|
|
3939
4806
|
function fail(reason, path, message) {
|
|
3940
4807
|
throw new DeserializeError(reason, path, message);
|
|
3941
4808
|
}
|
|
3942
4809
|
function toDeserializeFailure(error) {
|
|
4810
|
+
if (error instanceof ResourceBudgetError) return toBudgetDeserializeFailure(error);
|
|
4811
|
+
if (error instanceof OperationCancelledError) return toCancellationDeserializeFailure(error);
|
|
3943
4812
|
if (error instanceof DeserializeError || error instanceof TraversalDepthError) return error;
|
|
3944
4813
|
return null;
|
|
3945
4814
|
}
|
|
@@ -3961,12 +4830,20 @@ var SharedElementMetadataMismatchError = class extends Error {
|
|
|
3961
4830
|
var MergeError = class extends Error {
|
|
3962
4831
|
code;
|
|
3963
4832
|
reason;
|
|
4833
|
+
budget;
|
|
4834
|
+
limit;
|
|
4835
|
+
actual;
|
|
3964
4836
|
path;
|
|
3965
4837
|
constructor(error) {
|
|
3966
4838
|
super(error.message);
|
|
3967
4839
|
this.name = "MergeError";
|
|
3968
4840
|
this.code = error.code;
|
|
3969
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
|
+
}
|
|
3970
4847
|
this.path = error.path;
|
|
3971
4848
|
}
|
|
3972
4849
|
};
|
|
@@ -3991,20 +4868,27 @@ function mergeDoc(a, b, options = {}) {
|
|
|
3991
4868
|
/** Non-throwing `mergeDoc` variant with structured conflict details. */
|
|
3992
4869
|
function tryMergeDoc(a, b, options = {}) {
|
|
3993
4870
|
try {
|
|
3994
|
-
const
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
ok: false,
|
|
3999
|
-
code: 409,
|
|
4000
|
-
reason: "LINEAGE_MISMATCH",
|
|
4001
|
-
message: `merge requires shared array origin at ${mismatchPath}`,
|
|
4002
|
-
path: mismatchPath
|
|
4003
|
-
}
|
|
4871
|
+
const config = {
|
|
4872
|
+
unrelatedArrays: resolveUnrelatedArraysStrategy(options),
|
|
4873
|
+
budgetMeter: createBudgetMeter(options.resourceBudget),
|
|
4874
|
+
signal: options.signal
|
|
4004
4875
|
};
|
|
4876
|
+
if (config.unrelatedArrays === "reject") {
|
|
4877
|
+
const mismatchPath = findSeqLineageMismatch(a.root, b.root, [], config);
|
|
4878
|
+
if (mismatchPath !== null) return {
|
|
4879
|
+
ok: false,
|
|
4880
|
+
error: {
|
|
4881
|
+
ok: false,
|
|
4882
|
+
code: 409,
|
|
4883
|
+
reason: "LINEAGE_MISMATCH",
|
|
4884
|
+
message: `merge requires shared array origin at ${mismatchPath}`,
|
|
4885
|
+
path: mismatchPath
|
|
4886
|
+
}
|
|
4887
|
+
};
|
|
4888
|
+
}
|
|
4005
4889
|
return {
|
|
4006
4890
|
ok: true,
|
|
4007
|
-
doc:
|
|
4891
|
+
doc: mergeDocRoot(a.root, b.root, config).doc
|
|
4008
4892
|
};
|
|
4009
4893
|
} catch (error) {
|
|
4010
4894
|
if (error instanceof SharedElementMetadataMismatchError) return {
|
|
@@ -4021,6 +4905,14 @@ function tryMergeDoc(a, b, options = {}) {
|
|
|
4021
4905
|
ok: false,
|
|
4022
4906
|
error: toDepthApplyError(error)
|
|
4023
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
|
+
};
|
|
4024
4916
|
throw error;
|
|
4025
4917
|
}
|
|
4026
4918
|
}
|
|
@@ -4030,7 +4922,7 @@ function tryMergeDoc(a, b, options = {}) {
|
|
|
4030
4922
|
* The merged clock keeps a stable actor identity:
|
|
4031
4923
|
* - defaults to the actor from the first argument (`a`)
|
|
4032
4924
|
* - can be overridden via `options.actor`
|
|
4033
|
-
* - optional `options.
|
|
4925
|
+
* - optional `options.unrelatedArrays` controls the merge strategy for non-overlapping sequences
|
|
4034
4926
|
*
|
|
4035
4927
|
* The merged counter is lifted to the highest counter already observed for
|
|
4036
4928
|
* that actor across both input clocks and the merged document dots.
|
|
@@ -4042,29 +4934,79 @@ function mergeState(a, b, options = {}) {
|
|
|
4042
4934
|
}
|
|
4043
4935
|
/** Non-throwing `mergeState` variant with structured conflict details. */
|
|
4044
4936
|
function tryMergeState(a, b, options = {}) {
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4937
|
+
try {
|
|
4938
|
+
const actor = options.actor ?? a.clock.actor;
|
|
4939
|
+
const config = {
|
|
4940
|
+
actor,
|
|
4941
|
+
unrelatedArrays: resolveUnrelatedArraysStrategy(options),
|
|
4942
|
+
budgetMeter: createBudgetMeter(options.resourceBudget),
|
|
4943
|
+
signal: options.signal
|
|
4944
|
+
};
|
|
4945
|
+
if (config.unrelatedArrays === "reject") {
|
|
4946
|
+
const mismatchPath = findSeqLineageMismatch(a.doc.root, b.doc.root, [], config);
|
|
4947
|
+
if (mismatchPath !== null) return {
|
|
4948
|
+
ok: false,
|
|
4949
|
+
error: {
|
|
4950
|
+
ok: false,
|
|
4951
|
+
code: 409,
|
|
4952
|
+
reason: "LINEAGE_MISMATCH",
|
|
4953
|
+
message: `merge requires shared array origin at ${mismatchPath}`,
|
|
4954
|
+
path: mismatchPath
|
|
4955
|
+
}
|
|
4956
|
+
};
|
|
4054
4957
|
}
|
|
4055
|
-
|
|
4958
|
+
const merged = mergeDocRoot(a.doc.root, b.doc.root, config);
|
|
4959
|
+
const ctr = maxObservedCtrForActor(merged.maxObservedCtr, actor, a, b);
|
|
4960
|
+
return {
|
|
4961
|
+
ok: true,
|
|
4962
|
+
state: {
|
|
4963
|
+
doc: merged.doc,
|
|
4964
|
+
clock: createClock(actor, ctr)
|
|
4965
|
+
}
|
|
4966
|
+
};
|
|
4967
|
+
} catch (error) {
|
|
4968
|
+
if (error instanceof SharedElementMetadataMismatchError) return {
|
|
4969
|
+
ok: false,
|
|
4970
|
+
error: {
|
|
4971
|
+
ok: false,
|
|
4972
|
+
code: 409,
|
|
4973
|
+
reason: "LINEAGE_MISMATCH",
|
|
4974
|
+
message: error.message,
|
|
4975
|
+
path: error.path
|
|
4976
|
+
}
|
|
4977
|
+
};
|
|
4978
|
+
if (error instanceof TraversalDepthError) return {
|
|
4979
|
+
ok: false,
|
|
4980
|
+
error: toDepthApplyError(error)
|
|
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
|
+
};
|
|
4990
|
+
throw error;
|
|
4991
|
+
}
|
|
4056
4992
|
}
|
|
4057
|
-
function findSeqLineageMismatch(a, b, path) {
|
|
4993
|
+
function findSeqLineageMismatch(a, b, path, config) {
|
|
4994
|
+
const pathBuffer = [...path];
|
|
4058
4995
|
const stack = [{
|
|
4059
4996
|
a,
|
|
4060
4997
|
b,
|
|
4061
|
-
path,
|
|
4062
4998
|
depth: path.length
|
|
4063
4999
|
}];
|
|
4064
5000
|
while (stack.length > 0) {
|
|
5001
|
+
throwIfAborted(config.signal);
|
|
4065
5002
|
const frame = stack.pop();
|
|
4066
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);
|
|
4067
5008
|
if (frame.a.kind === "seq" && frame.b.kind === "seq") {
|
|
5009
|
+
config.budgetMeter?.count("sequenceElements", frame.a.elems.size + frame.b.elems.size, budgetPath);
|
|
4068
5010
|
const hasElemsA = frame.a.elems.size > 0;
|
|
4069
5011
|
const hasElemsB = frame.b.elems.size > 0;
|
|
4070
5012
|
if (hasElemsA && hasElemsB) {
|
|
@@ -4073,157 +5015,277 @@ function findSeqLineageMismatch(a, b, path) {
|
|
|
4073
5015
|
shared = true;
|
|
4074
5016
|
break;
|
|
4075
5017
|
}
|
|
4076
|
-
if (!shared) return stringifyJsonPointer(
|
|
5018
|
+
if (!shared) return stringifyJsonPointer(pathBuffer);
|
|
4077
5019
|
}
|
|
4078
5020
|
}
|
|
4079
5021
|
if (frame.a.kind === "obj" && frame.b.kind === "obj") {
|
|
4080
5022
|
const left = frame.a;
|
|
4081
5023
|
const right = frame.b;
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
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];
|
|
4085
5033
|
const nextA = left.entries.get(key).node;
|
|
4086
5034
|
const nextB = right.entries.get(key).node;
|
|
4087
5035
|
stack.push({
|
|
4088
5036
|
a: nextA,
|
|
4089
5037
|
b: nextB,
|
|
4090
|
-
|
|
4091
|
-
|
|
5038
|
+
depth: frame.depth + 1,
|
|
5039
|
+
key
|
|
4092
5040
|
});
|
|
4093
5041
|
}
|
|
4094
5042
|
}
|
|
4095
5043
|
}
|
|
4096
5044
|
return null;
|
|
4097
5045
|
}
|
|
4098
|
-
function
|
|
4099
|
-
|
|
5046
|
+
function mergeDocRoot(a, b, config) {
|
|
5047
|
+
const merged = mergeNodeAtDepth(a, b, 0, [], config);
|
|
5048
|
+
return {
|
|
5049
|
+
doc: { root: merged.node },
|
|
5050
|
+
maxObservedCtr: merged.maxObservedCtr
|
|
5051
|
+
};
|
|
5052
|
+
}
|
|
5053
|
+
function resolveUnrelatedArraysStrategy(options) {
|
|
5054
|
+
if (options.unrelatedArrays !== void 0) return options.unrelatedArrays;
|
|
5055
|
+
if (options.requireSharedOrigin === false) return "unsafe-union";
|
|
5056
|
+
return "reject";
|
|
5057
|
+
}
|
|
5058
|
+
function maxObservedCtrForActor(docObservedCtr, actor, a, b) {
|
|
5059
|
+
let best = docObservedCtr;
|
|
4100
5060
|
if (a.clock.actor === actor && a.clock.ctr > best) best = a.clock.ctr;
|
|
4101
5061
|
if (b.clock.actor === actor && b.clock.ctr > best) best = b.clock.ctr;
|
|
4102
5062
|
return best;
|
|
4103
5063
|
}
|
|
4104
|
-
function repDot(node) {
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
5064
|
+
function repDot(node, signal) {
|
|
5065
|
+
let best = {
|
|
5066
|
+
actor: "",
|
|
5067
|
+
ctr: 0
|
|
5068
|
+
};
|
|
5069
|
+
const stack = [{
|
|
5070
|
+
node,
|
|
5071
|
+
depth: 0
|
|
5072
|
+
}];
|
|
5073
|
+
while (stack.length > 0) {
|
|
5074
|
+
throwIfAborted(signal);
|
|
5075
|
+
const frame = stack.pop();
|
|
5076
|
+
assertTraversalDepth(frame.depth);
|
|
5077
|
+
switch (frame.node.kind) {
|
|
5078
|
+
case "lww":
|
|
5079
|
+
if (compareDot(frame.node.dot, best) > 0) best = frame.node.dot;
|
|
5080
|
+
break;
|
|
5081
|
+
case "obj":
|
|
5082
|
+
for (const entry of frame.node.entries.values()) {
|
|
5083
|
+
if (compareDot(entry.dot, best) > 0) best = entry.dot;
|
|
5084
|
+
stack.push({
|
|
5085
|
+
node: entry.node,
|
|
5086
|
+
depth: frame.depth + 1
|
|
5087
|
+
});
|
|
5088
|
+
}
|
|
5089
|
+
for (const tombstone of frame.node.tombstone.values()) if (compareDot(tombstone, best) > 0) best = tombstone;
|
|
5090
|
+
break;
|
|
5091
|
+
case "seq":
|
|
5092
|
+
for (const elem of frame.node.elems.values()) {
|
|
5093
|
+
if (compareDot(elem.insDot, best) > 0) best = elem.insDot;
|
|
5094
|
+
if (elem.delDot && compareDot(elem.delDot, best) > 0) best = elem.delDot;
|
|
5095
|
+
stack.push({
|
|
5096
|
+
node: elem.value,
|
|
5097
|
+
depth: frame.depth + 1
|
|
5098
|
+
});
|
|
5099
|
+
}
|
|
5100
|
+
break;
|
|
4123
5101
|
}
|
|
4124
5102
|
}
|
|
5103
|
+
return best;
|
|
4125
5104
|
}
|
|
4126
|
-
function
|
|
4127
|
-
|
|
4128
|
-
}
|
|
4129
|
-
function mergeNodeAtDepth(a, b, depth, path) {
|
|
5105
|
+
function mergeNodeAtDepth(a, b, depth, path, config) {
|
|
5106
|
+
throwIfAborted(config.signal);
|
|
4130
5107
|
assertTraversalDepth(depth);
|
|
4131
|
-
|
|
4132
|
-
if (a.kind === "
|
|
4133
|
-
if (a.kind === "
|
|
4134
|
-
if (
|
|
4135
|
-
return cloneNodeShallow(
|
|
4136
|
-
|
|
4137
|
-
|
|
5108
|
+
config.budgetMeter?.count("visitedNodes", 1, stringifyJsonPointer(path));
|
|
5109
|
+
if (a.kind === "lww" && b.kind === "lww") return mergeLww(a, b, config.actor);
|
|
5110
|
+
if (a.kind === "obj" && b.kind === "obj") return mergeObj(a, b, depth + 1, path, config);
|
|
5111
|
+
if (a.kind === "seq" && b.kind === "seq") return mergeSeq(a, b, depth + 1, path, config);
|
|
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);
|
|
5114
|
+
}
|
|
5115
|
+
function mergeLww(a, b, actor) {
|
|
4138
5116
|
if (compareDot(a.dot, b.dot) >= 0) return {
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
5117
|
+
node: {
|
|
5118
|
+
kind: "lww",
|
|
5119
|
+
value: structuredClone(a.value),
|
|
5120
|
+
dot: { ...a.dot }
|
|
5121
|
+
},
|
|
5122
|
+
maxObservedCtr: maxObservedCtrForDot(a.dot, actor)
|
|
4142
5123
|
};
|
|
4143
5124
|
return {
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
5125
|
+
node: {
|
|
5126
|
+
kind: "lww",
|
|
5127
|
+
value: structuredClone(b.value),
|
|
5128
|
+
dot: { ...b.dot }
|
|
5129
|
+
},
|
|
5130
|
+
maxObservedCtr: maxObservedCtrForDot(b.dot, actor)
|
|
4147
5131
|
};
|
|
4148
5132
|
}
|
|
4149
|
-
function mergeObj(a, b, depth, path) {
|
|
5133
|
+
function mergeObj(a, b, depth, path, config) {
|
|
4150
5134
|
assertTraversalDepth(depth);
|
|
4151
5135
|
const entries = /* @__PURE__ */ new Map();
|
|
4152
5136
|
const tombstone = /* @__PURE__ */ new Map();
|
|
5137
|
+
let maxObservedCtr = 0;
|
|
4153
5138
|
const allTombKeys = new Set([...a.tombstone.keys(), ...b.tombstone.keys()]);
|
|
5139
|
+
config.budgetMeter?.count("objectEntries", allTombKeys.size, stringifyJsonPointer(path));
|
|
4154
5140
|
for (const key of allTombKeys) {
|
|
5141
|
+
throwIfAborted(config.signal);
|
|
4155
5142
|
const da = a.tombstone.get(key);
|
|
4156
5143
|
const db = b.tombstone.get(key);
|
|
4157
|
-
if (da && db)
|
|
4158
|
-
|
|
4159
|
-
|
|
5144
|
+
if (da && db) {
|
|
5145
|
+
const mergedDot = compareDot(da, db) >= 0 ? { ...da } : { ...db };
|
|
5146
|
+
tombstone.set(key, mergedDot);
|
|
5147
|
+
maxObservedCtr = Math.max(maxObservedCtr, maxObservedCtrForDot(mergedDot, config.actor));
|
|
5148
|
+
} else if (da) {
|
|
5149
|
+
tombstone.set(key, { ...da });
|
|
5150
|
+
maxObservedCtr = Math.max(maxObservedCtr, maxObservedCtrForDot(da, config.actor));
|
|
5151
|
+
} else {
|
|
5152
|
+
tombstone.set(key, { ...db });
|
|
5153
|
+
maxObservedCtr = Math.max(maxObservedCtr, maxObservedCtrForDot(db, config.actor));
|
|
5154
|
+
}
|
|
4160
5155
|
}
|
|
4161
5156
|
const allKeys = new Set([...a.entries.keys(), ...b.entries.keys()]);
|
|
5157
|
+
config.budgetMeter?.count("objectEntries", allKeys.size, stringifyJsonPointer(path));
|
|
4162
5158
|
for (const key of allKeys) {
|
|
5159
|
+
throwIfAborted(config.signal);
|
|
4163
5160
|
const ea = a.entries.get(key);
|
|
4164
5161
|
const eb = b.entries.get(key);
|
|
4165
5162
|
let merged;
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
dot: { ...eb.dot }
|
|
4177
|
-
|
|
5163
|
+
let mergedNodeMaxObservedCtr = 0;
|
|
5164
|
+
if (ea && eb) {
|
|
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
|
+
})();
|
|
5173
|
+
const dot = compareDot(ea.dot, eb.dot) >= 0 ? { ...ea.dot } : { ...eb.dot };
|
|
5174
|
+
merged = {
|
|
5175
|
+
node: mergedNode.node,
|
|
5176
|
+
dot
|
|
5177
|
+
};
|
|
5178
|
+
mergedNodeMaxObservedCtr = mergedNode.maxObservedCtr;
|
|
5179
|
+
} else if (ea) {
|
|
5180
|
+
const cloned = cloneNodeShallow(ea.node, depth + 1, config.actor, config.signal);
|
|
5181
|
+
merged = {
|
|
5182
|
+
node: cloned.node,
|
|
5183
|
+
dot: { ...ea.dot }
|
|
5184
|
+
};
|
|
5185
|
+
mergedNodeMaxObservedCtr = cloned.maxObservedCtr;
|
|
5186
|
+
} else {
|
|
5187
|
+
const cloned = cloneNodeShallow(eb.node, depth + 1, config.actor, config.signal);
|
|
5188
|
+
merged = {
|
|
5189
|
+
node: cloned.node,
|
|
5190
|
+
dot: { ...eb.dot }
|
|
5191
|
+
};
|
|
5192
|
+
mergedNodeMaxObservedCtr = cloned.maxObservedCtr;
|
|
5193
|
+
}
|
|
4178
5194
|
const td = tombstone.get(key);
|
|
4179
5195
|
if (td && compareDot(td, merged.dot) >= 0) continue;
|
|
4180
5196
|
entries.set(key, merged);
|
|
5197
|
+
maxObservedCtr = Math.max(maxObservedCtr, mergedNodeMaxObservedCtr, maxObservedCtrForDot(merged.dot, config.actor));
|
|
4181
5198
|
}
|
|
4182
5199
|
return {
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
5200
|
+
node: {
|
|
5201
|
+
kind: "obj",
|
|
5202
|
+
entries,
|
|
5203
|
+
tombstone
|
|
5204
|
+
},
|
|
5205
|
+
maxObservedCtr
|
|
4186
5206
|
};
|
|
4187
5207
|
}
|
|
4188
|
-
function mergeSeq(a, b, depth, path) {
|
|
5208
|
+
function mergeSeq(a, b, depth, path, config) {
|
|
4189
5209
|
assertTraversalDepth(depth);
|
|
5210
|
+
config.budgetMeter?.count("visitedNodes", 1, stringifyJsonPointer(path));
|
|
5211
|
+
if (config.unrelatedArrays === "atomic-replace" && a.elems.size > 0 && b.elems.size > 0) {
|
|
5212
|
+
let shared = false;
|
|
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);
|
|
5223
|
+
}
|
|
5224
|
+
}
|
|
4190
5225
|
const elems = /* @__PURE__ */ new Map();
|
|
5226
|
+
let maxObservedCtr = 0;
|
|
4191
5227
|
const allIds = new Set([...a.elems.keys(), ...b.elems.keys()]);
|
|
5228
|
+
config.budgetMeter?.count("sequenceElements", allIds.size, stringifyJsonPointer(path));
|
|
4192
5229
|
for (const id of allIds) {
|
|
5230
|
+
throwIfAborted(config.signal);
|
|
4193
5231
|
const ea = a.elems.get(id);
|
|
4194
5232
|
const eb = b.elems.get(id);
|
|
4195
5233
|
if (ea && eb) {
|
|
4196
5234
|
if (ea.prev !== eb.prev) throw new SharedElementMetadataMismatchError(stringifyJsonPointer(path), id, "prev");
|
|
4197
5235
|
if (!sameDot(ea.insDot, eb.insDot)) throw new SharedElementMetadataMismatchError(stringifyJsonPointer(path), id, "insDot");
|
|
4198
|
-
|
|
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
|
+
})();
|
|
5244
|
+
const mergedDeleteDot = mergeDeleteDot(ea.delDot, eb.delDot);
|
|
4199
5245
|
elems.set(id, {
|
|
4200
5246
|
id,
|
|
4201
5247
|
prev: ea.prev,
|
|
4202
5248
|
tombstone: ea.tombstone || eb.tombstone,
|
|
4203
|
-
delDot:
|
|
4204
|
-
value: mergedValue,
|
|
5249
|
+
delDot: mergedDeleteDot,
|
|
5250
|
+
value: mergedValue.node,
|
|
4205
5251
|
insDot: { ...ea.insDot }
|
|
4206
5252
|
});
|
|
4207
|
-
|
|
4208
|
-
else
|
|
5253
|
+
maxObservedCtr = Math.max(maxObservedCtr, mergedValue.maxObservedCtr, maxObservedCtrForDot(ea.insDot, config.actor), maxObservedCtrForDot(mergedDeleteDot, config.actor));
|
|
5254
|
+
} else if (ea) {
|
|
5255
|
+
const cloned = cloneElem(ea, depth + 1, config.actor, config.signal);
|
|
5256
|
+
elems.set(id, cloned.elem);
|
|
5257
|
+
maxObservedCtr = Math.max(maxObservedCtr, cloned.maxObservedCtr);
|
|
5258
|
+
} else {
|
|
5259
|
+
const cloned = cloneElem(eb, depth + 1, config.actor, config.signal);
|
|
5260
|
+
elems.set(id, cloned.elem);
|
|
5261
|
+
maxObservedCtr = Math.max(maxObservedCtr, cloned.maxObservedCtr);
|
|
5262
|
+
}
|
|
4209
5263
|
}
|
|
4210
5264
|
return {
|
|
4211
|
-
|
|
4212
|
-
|
|
5265
|
+
node: {
|
|
5266
|
+
kind: "seq",
|
|
5267
|
+
elems
|
|
5268
|
+
},
|
|
5269
|
+
maxObservedCtr
|
|
4213
5270
|
};
|
|
4214
5271
|
}
|
|
4215
5272
|
function sameDot(a, b) {
|
|
4216
5273
|
return a.actor === b.actor && a.ctr === b.ctr;
|
|
4217
5274
|
}
|
|
4218
|
-
function cloneElem(e, depth) {
|
|
5275
|
+
function cloneElem(e, depth, actor, signal) {
|
|
5276
|
+
throwIfAborted(signal);
|
|
4219
5277
|
assertTraversalDepth(depth);
|
|
5278
|
+
const value = cloneNodeShallow(e.value, depth + 1, actor, signal);
|
|
4220
5279
|
return {
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
5280
|
+
elem: {
|
|
5281
|
+
id: e.id,
|
|
5282
|
+
prev: e.prev,
|
|
5283
|
+
tombstone: e.tombstone,
|
|
5284
|
+
delDot: e.delDot ? { ...e.delDot } : void 0,
|
|
5285
|
+
value: value.node,
|
|
5286
|
+
insDot: { ...e.insDot }
|
|
5287
|
+
},
|
|
5288
|
+
maxObservedCtr: Math.max(value.maxObservedCtr, maxObservedCtrForDot(e.insDot, actor), maxObservedCtrForDot(e.delDot, actor))
|
|
4227
5289
|
};
|
|
4228
5290
|
}
|
|
4229
5291
|
function mergeDeleteDot(a, b) {
|
|
@@ -4231,38 +5293,68 @@ function mergeDeleteDot(a, b) {
|
|
|
4231
5293
|
if (a) return { ...a };
|
|
4232
5294
|
if (b) return { ...b };
|
|
4233
5295
|
}
|
|
4234
|
-
function cloneNodeShallow(node, depth) {
|
|
5296
|
+
function cloneNodeShallow(node, depth, actor, signal) {
|
|
5297
|
+
throwIfAborted(signal);
|
|
4235
5298
|
assertTraversalDepth(depth);
|
|
4236
5299
|
switch (node.kind) {
|
|
4237
5300
|
case "lww": return {
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
5301
|
+
node: {
|
|
5302
|
+
kind: "lww",
|
|
5303
|
+
value: structuredClone(node.value),
|
|
5304
|
+
dot: { ...node.dot }
|
|
5305
|
+
},
|
|
5306
|
+
maxObservedCtr: maxObservedCtrForDot(node.dot, actor)
|
|
4241
5307
|
};
|
|
4242
5308
|
case "obj": {
|
|
4243
5309
|
const entries = /* @__PURE__ */ new Map();
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
5310
|
+
let maxObservedCtr = 0;
|
|
5311
|
+
for (const [k, v] of node.entries) {
|
|
5312
|
+
throwIfAborted(signal);
|
|
5313
|
+
const cloned = cloneNodeShallow(v.node, depth + 1, actor, signal);
|
|
5314
|
+
entries.set(k, {
|
|
5315
|
+
node: cloned.node,
|
|
5316
|
+
dot: { ...v.dot }
|
|
5317
|
+
});
|
|
5318
|
+
maxObservedCtr = Math.max(maxObservedCtr, cloned.maxObservedCtr, maxObservedCtrForDot(v.dot, actor));
|
|
5319
|
+
}
|
|
4248
5320
|
const tombstone = /* @__PURE__ */ new Map();
|
|
4249
|
-
for (const [k, d] of node.tombstone)
|
|
5321
|
+
for (const [k, d] of node.tombstone) {
|
|
5322
|
+
throwIfAborted(signal);
|
|
5323
|
+
tombstone.set(k, { ...d });
|
|
5324
|
+
maxObservedCtr = Math.max(maxObservedCtr, maxObservedCtrForDot(d, actor));
|
|
5325
|
+
}
|
|
4250
5326
|
return {
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
5327
|
+
node: {
|
|
5328
|
+
kind: "obj",
|
|
5329
|
+
entries,
|
|
5330
|
+
tombstone
|
|
5331
|
+
},
|
|
5332
|
+
maxObservedCtr
|
|
4254
5333
|
};
|
|
4255
5334
|
}
|
|
4256
5335
|
case "seq": {
|
|
4257
5336
|
const elems = /* @__PURE__ */ new Map();
|
|
4258
|
-
|
|
5337
|
+
let maxObservedCtr = 0;
|
|
5338
|
+
for (const [id, e] of node.elems) {
|
|
5339
|
+
throwIfAborted(signal);
|
|
5340
|
+
const cloned = cloneElem(e, depth + 1, actor, signal);
|
|
5341
|
+
elems.set(id, cloned.elem);
|
|
5342
|
+
maxObservedCtr = Math.max(maxObservedCtr, cloned.maxObservedCtr);
|
|
5343
|
+
}
|
|
4259
5344
|
return {
|
|
4260
|
-
|
|
4261
|
-
|
|
5345
|
+
node: {
|
|
5346
|
+
kind: "seq",
|
|
5347
|
+
elems
|
|
5348
|
+
},
|
|
5349
|
+
maxObservedCtr
|
|
4262
5350
|
};
|
|
4263
5351
|
}
|
|
4264
5352
|
}
|
|
4265
5353
|
}
|
|
5354
|
+
function maxObservedCtrForDot(dot, actor) {
|
|
5355
|
+
if (!dot || !actor || dot.actor !== actor) return 0;
|
|
5356
|
+
return dot.ctr;
|
|
5357
|
+
}
|
|
4266
5358
|
|
|
4267
5359
|
//#endregion
|
|
4268
5360
|
//#region src/compact.ts
|
|
@@ -4344,4 +5436,4 @@ function compactStateTombstones(state, options) {
|
|
|
4344
5436
|
}
|
|
4345
5437
|
|
|
4346
5438
|
//#endregion
|
|
4347
|
-
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 };
|