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,4 +1,130 @@
|
|
|
1
1
|
|
|
2
|
+
//#region src/budget.ts
|
|
3
|
+
function isNonNegativeSafeInteger(value) {
|
|
4
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
5
|
+
}
|
|
6
|
+
function formatPath(path) {
|
|
7
|
+
if (path === void 0 || path === "") return "";
|
|
8
|
+
return ` at ${path}`;
|
|
9
|
+
}
|
|
10
|
+
function formatOpIndex(opIndex) {
|
|
11
|
+
if (opIndex === void 0) return "";
|
|
12
|
+
return ` at op ${opIndex}`;
|
|
13
|
+
}
|
|
14
|
+
var ResourceBudgetError = class extends Error {
|
|
15
|
+
reason = "RESOURCE_BUDGET_EXCEEDED";
|
|
16
|
+
code = 409;
|
|
17
|
+
budget;
|
|
18
|
+
limit;
|
|
19
|
+
actual;
|
|
20
|
+
path;
|
|
21
|
+
opIndex;
|
|
22
|
+
constructor(budget, limit, actual, path, opIndex) {
|
|
23
|
+
super(`resource budget '${budget}' exceeded${formatPath(path)}${formatOpIndex(opIndex)}: ${actual} > ${limit}`);
|
|
24
|
+
this.name = "ResourceBudgetError";
|
|
25
|
+
this.budget = budget;
|
|
26
|
+
this.limit = limit;
|
|
27
|
+
this.actual = actual;
|
|
28
|
+
this.path = path;
|
|
29
|
+
this.opIndex = opIndex;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var ResourceBudgetMeter = class {
|
|
33
|
+
#budget;
|
|
34
|
+
#counts;
|
|
35
|
+
constructor(budget) {
|
|
36
|
+
this.#budget = validateBudget(budget);
|
|
37
|
+
this.#counts = {
|
|
38
|
+
patchOperations: 0,
|
|
39
|
+
objectEntries: 0,
|
|
40
|
+
sequenceElements: 0,
|
|
41
|
+
visitedNodes: 0,
|
|
42
|
+
serializedElements: 0,
|
|
43
|
+
arrayDiffCells: 0
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
count(kind, delta, path, opIndex) {
|
|
47
|
+
if (delta <= 0) return;
|
|
48
|
+
this.#counts[kind] += delta;
|
|
49
|
+
const limit = this.#budget[kind];
|
|
50
|
+
if (limit !== void 0 && this.#counts[kind] > limit) throw new ResourceBudgetError(kind, limit, this.#counts[kind], path, opIndex);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
function createBudgetMeter(budget) {
|
|
54
|
+
if (budget === void 0) return;
|
|
55
|
+
return new ResourceBudgetMeter(budget);
|
|
56
|
+
}
|
|
57
|
+
function toBudgetApplyError(error) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
code: error.code,
|
|
61
|
+
reason: error.reason,
|
|
62
|
+
message: error.message,
|
|
63
|
+
budget: error.budget,
|
|
64
|
+
limit: error.limit,
|
|
65
|
+
actual: error.actual,
|
|
66
|
+
path: error.path,
|
|
67
|
+
opIndex: error.opIndex
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function toBudgetDeserializeFailure(error) {
|
|
71
|
+
return {
|
|
72
|
+
code: error.code,
|
|
73
|
+
reason: error.reason,
|
|
74
|
+
message: error.message,
|
|
75
|
+
budget: error.budget,
|
|
76
|
+
limit: error.limit,
|
|
77
|
+
actual: error.actual,
|
|
78
|
+
path: error.path
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function validateBudget(budget) {
|
|
82
|
+
if (budget === void 0) return {};
|
|
83
|
+
const normalized = {};
|
|
84
|
+
const entries = Object.entries(budget);
|
|
85
|
+
for (const [key, value] of entries) {
|
|
86
|
+
if (value === void 0) continue;
|
|
87
|
+
if (!isNonNegativeSafeInteger(value)) throw new Error(`resource budget '${key}' must be a non-negative safe integer`);
|
|
88
|
+
normalized[key] = value;
|
|
89
|
+
}
|
|
90
|
+
return normalized;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/cancellation.ts
|
|
95
|
+
var OperationCancelledError = class extends Error {
|
|
96
|
+
reasonValue;
|
|
97
|
+
constructor(reason) {
|
|
98
|
+
super(toCancellationMessage(reason));
|
|
99
|
+
this.name = "OperationCancelledError";
|
|
100
|
+
this.reasonValue = reason;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
function throwIfAborted(signal) {
|
|
104
|
+
if (signal?.aborted) throw new OperationCancelledError(signal.reason);
|
|
105
|
+
}
|
|
106
|
+
function toCancellationApplyError(error) {
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
code: 409,
|
|
110
|
+
reason: "OPERATION_CANCELLED",
|
|
111
|
+
message: error.message
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function toCancellationDeserializeFailure(error) {
|
|
115
|
+
return {
|
|
116
|
+
code: 409,
|
|
117
|
+
reason: "OPERATION_CANCELLED",
|
|
118
|
+
message: error.message
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function toCancellationMessage(reason) {
|
|
122
|
+
if (reason instanceof Error && reason.message.length > 0) return `operation cancelled: ${reason.message}`;
|
|
123
|
+
if (typeof reason === "string" && reason.length > 0) return `operation cancelled: ${reason}`;
|
|
124
|
+
return "operation cancelled";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
//#endregion
|
|
2
128
|
//#region src/depth.ts
|
|
3
129
|
const MAX_TRAVERSAL_DEPTH = 16384;
|
|
4
130
|
var TraversalDepthError = class extends Error {
|
|
@@ -27,6 +153,8 @@ function toDepthApplyError(error) {
|
|
|
27
153
|
|
|
28
154
|
//#endregion
|
|
29
155
|
//#region src/version-vector.ts
|
|
156
|
+
let observedVersionVectorObserverForTests = null;
|
|
157
|
+
const observedVersionVectorCache = /* @__PURE__ */ new WeakMap();
|
|
30
158
|
function readVersionVectorCounter(vv, actor) {
|
|
31
159
|
if (!Object.prototype.hasOwnProperty.call(vv, actor)) return;
|
|
32
160
|
const counter = vv[actor];
|
|
@@ -43,6 +171,21 @@ function writeVersionVectorCounter(vv, actor, counter) {
|
|
|
43
171
|
function observeVersionVectorDot(vv, dot) {
|
|
44
172
|
if ((readVersionVectorCounter(vv, dot.actor) ?? 0) < dot.ctr) writeVersionVectorCounter(vv, dot.actor, dot.ctr);
|
|
45
173
|
}
|
|
174
|
+
function cloneVersionVector(vv) {
|
|
175
|
+
return mergeVersionVectors(vv);
|
|
176
|
+
}
|
|
177
|
+
function readCachedObservedVersionVector(doc) {
|
|
178
|
+
const cached = observedVersionVectorCache.get(doc);
|
|
179
|
+
return cached ? cloneVersionVector(cached) : void 0;
|
|
180
|
+
}
|
|
181
|
+
function writeCachedObservedVersionVector(doc, vv) {
|
|
182
|
+
observedVersionVectorCache.set(doc, cloneVersionVector(vv));
|
|
183
|
+
}
|
|
184
|
+
function observeDocVersionVectorDot(doc, dot) {
|
|
185
|
+
const cached = observedVersionVectorCache.get(doc);
|
|
186
|
+
if (!cached) return;
|
|
187
|
+
observeVersionVectorDot(cached, dot);
|
|
188
|
+
}
|
|
46
189
|
/**
|
|
47
190
|
* Inspect a document or state and return the highest observed counter per actor.
|
|
48
191
|
*
|
|
@@ -52,11 +195,14 @@ function observeVersionVectorDot(vv, dot) {
|
|
|
52
195
|
*/
|
|
53
196
|
function observedVersionVector(target) {
|
|
54
197
|
const doc = "doc" in target ? target.doc : target;
|
|
55
|
-
const
|
|
198
|
+
const cached = readCachedObservedVersionVector(doc);
|
|
199
|
+
const vv = cached ?? Object.create(null);
|
|
56
200
|
if ("clock" in target) observeVersionVectorDot(vv, {
|
|
57
201
|
actor: target.clock.actor,
|
|
58
202
|
ctr: target.clock.ctr
|
|
59
203
|
});
|
|
204
|
+
if (cached) return vv;
|
|
205
|
+
observedVersionVectorObserverForTests?.(target);
|
|
60
206
|
const stack = [{
|
|
61
207
|
node: doc.root,
|
|
62
208
|
depth: 0
|
|
@@ -88,6 +234,7 @@ function observedVersionVector(target) {
|
|
|
88
234
|
});
|
|
89
235
|
}
|
|
90
236
|
}
|
|
237
|
+
writeCachedObservedVersionVector(doc, vv);
|
|
91
238
|
return vv;
|
|
92
239
|
}
|
|
93
240
|
/** Combine version vectors using per-actor maxima. */
|
|
@@ -494,6 +641,7 @@ function rgaPrevForInsertAtIndex(seq, index) {
|
|
|
494
641
|
//#endregion
|
|
495
642
|
//#region src/materialize.ts
|
|
496
643
|
let materializeObserver = null;
|
|
644
|
+
const EMPTY_PATH = [];
|
|
497
645
|
function createMaterializedObject() {
|
|
498
646
|
return Object.create(null);
|
|
499
647
|
}
|
|
@@ -508,7 +656,7 @@ function setMaterializedProperty(out, key, value) {
|
|
|
508
656
|
/** Convert a CRDT node graph into a plain JSON value using an explicit stack. */
|
|
509
657
|
function materialize(node) {
|
|
510
658
|
const observer = materializeObserver;
|
|
511
|
-
observer?.(
|
|
659
|
+
observer?.(EMPTY_PATH, node);
|
|
512
660
|
if (node.kind === "lww") return node.value;
|
|
513
661
|
const root = node.kind === "obj" ? createMaterializedObject() : [];
|
|
514
662
|
const stack = [];
|
|
@@ -539,7 +687,7 @@ function materialize(node) {
|
|
|
539
687
|
const child = entry.node;
|
|
540
688
|
const childDepth = frame.depth + 1;
|
|
541
689
|
assertTraversalDepth(childDepth);
|
|
542
|
-
const childPath = [...frame.path, key];
|
|
690
|
+
const childPath = observer ? [...frame.path, key] : EMPTY_PATH;
|
|
543
691
|
observer?.(childPath, child);
|
|
544
692
|
if (child.kind === "lww") {
|
|
545
693
|
setMaterializedProperty(frame.out, key, child.value);
|
|
@@ -577,7 +725,7 @@ function materialize(node) {
|
|
|
577
725
|
const child = elem.value;
|
|
578
726
|
const childDepth = frame.depth + 1;
|
|
579
727
|
assertTraversalDepth(childDepth);
|
|
580
|
-
const childPath = [...frame.path, String(frame.nextIndex)];
|
|
728
|
+
const childPath = observer ? [...frame.path, String(frame.nextIndex)] : EMPTY_PATH;
|
|
581
729
|
frame.nextIndex += 1;
|
|
582
730
|
observer?.(childPath, child);
|
|
583
731
|
if (child.kind === "lww") {
|
|
@@ -930,11 +1078,13 @@ function getAtJson(base, path) {
|
|
|
930
1078
|
*/
|
|
931
1079
|
function compileJsonPatchToIntent(baseJson, patch, options = {}) {
|
|
932
1080
|
const internalOptions = options;
|
|
1081
|
+
const budgetMeter = internalOptions.budgetMeter ?? createBudgetMeter(options.resourceBudget);
|
|
933
1082
|
const semantics = options.semantics ?? "sequential";
|
|
934
1083
|
const opIndexOffset = internalOptions.opIndexOffset ?? 0;
|
|
935
1084
|
let workingBase = baseJson;
|
|
936
1085
|
const pointerCache = internalOptions.pointerCache ?? /* @__PURE__ */ new Map();
|
|
937
1086
|
const intents = [];
|
|
1087
|
+
budgetMeter?.count("patchOperations", patch.length);
|
|
938
1088
|
for (let opIndex = 0; opIndex < patch.length; opIndex++) {
|
|
939
1089
|
const op = patch[opIndex];
|
|
940
1090
|
const absoluteOpIndex = opIndex + opIndexOffset;
|
|
@@ -949,14 +1099,16 @@ function compileJsonPatchOpToIntent(baseJson, op, options = {}) {
|
|
|
949
1099
|
const internalOptions = options;
|
|
950
1100
|
const semantics = options.semantics ?? "sequential";
|
|
951
1101
|
const pointerCache = internalOptions.pointerCache ?? /* @__PURE__ */ new Map();
|
|
952
|
-
|
|
1102
|
+
const opIndex = internalOptions.opIndexOffset ?? 0;
|
|
1103
|
+
(internalOptions.budgetMeter ?? createBudgetMeter(options.resourceBudget))?.count("patchOperations", 1, void 0, opIndex);
|
|
1104
|
+
return compileSingleOp(baseJson, op, opIndex, semantics, pointerCache);
|
|
953
1105
|
}
|
|
954
1106
|
/**
|
|
955
1107
|
* Compute a JSON Patch delta between two JSON values.
|
|
956
1108
|
* By default arrays use a deterministic LCS strategy.
|
|
957
1109
|
* Pass `{ arrayStrategy: "atomic" }` for single-op array replacement.
|
|
958
1110
|
* Pass `{ arrayStrategy: "lcs-linear" }` for a lower-memory LCS variant.
|
|
959
|
-
* Use `lcsLinearMaxCells` to
|
|
1111
|
+
* Use `lcsLinearMaxCells` to cap worst-case `lcs-linear` work and
|
|
960
1112
|
* fall back to an atomic array replacement for very large unmatched windows.
|
|
961
1113
|
* Pass `{ emitMoves: true }` or `{ emitCopies: true }` to opt into RFC 6902
|
|
962
1114
|
* move/copy emission when a deterministic rewrite is available.
|
|
@@ -966,20 +1118,24 @@ function compileJsonPatchOpToIntent(baseJson, op, options = {}) {
|
|
|
966
1118
|
* @returns An array of JSON Patch operations that transform `base` into `next`.
|
|
967
1119
|
*/
|
|
968
1120
|
function diffJsonPatch(base, next, options = {}) {
|
|
1121
|
+
const budgetMeter = createBudgetMeter(options.resourceBudget);
|
|
969
1122
|
const runtimeMode = options.jsonValidation ?? "none";
|
|
970
1123
|
const runtimeBase = coerceRuntimeJsonValue(base, runtimeMode);
|
|
971
1124
|
const runtimeNext = coerceRuntimeJsonValue(next, runtimeMode);
|
|
972
1125
|
const ops = [];
|
|
973
|
-
|
|
1126
|
+
const path = [];
|
|
1127
|
+
throwIfAborted(options.signal);
|
|
1128
|
+
diffValue(path, runtimeBase, runtimeNext, ops, options, budgetMeter);
|
|
974
1129
|
return ops;
|
|
975
1130
|
}
|
|
976
|
-
function diffValue(path, base, next, ops, options) {
|
|
1131
|
+
function diffValue(path, base, next, ops, options, budgetMeter) {
|
|
977
1132
|
const stack = [{
|
|
978
1133
|
kind: "value",
|
|
979
1134
|
base,
|
|
980
1135
|
next
|
|
981
1136
|
}];
|
|
982
1137
|
while (stack.length > 0) {
|
|
1138
|
+
throwIfAborted(options.signal);
|
|
983
1139
|
const frame = stack.pop();
|
|
984
1140
|
if (frame.kind === "path-pop") {
|
|
985
1141
|
path.pop();
|
|
@@ -1005,6 +1161,7 @@ function diffValue(path, base, next, ops, options) {
|
|
|
1005
1161
|
continue;
|
|
1006
1162
|
}
|
|
1007
1163
|
assertTraversalDepth(path.length);
|
|
1164
|
+
budgetMeter?.count("visitedNodes", 1, stringifyJsonPointer(path));
|
|
1008
1165
|
if (frame.base === frame.next) continue;
|
|
1009
1166
|
const baseIsArray = Array.isArray(frame.base);
|
|
1010
1167
|
const nextIsArray = Array.isArray(frame.next);
|
|
@@ -1020,7 +1177,7 @@ function diffValue(path, base, next, ops, options) {
|
|
|
1020
1177
|
if (jsonEquals(frame.base, frame.next)) continue;
|
|
1021
1178
|
const arrayStrategy = options.arrayStrategy ?? "lcs";
|
|
1022
1179
|
if (arrayStrategy === "lcs") {
|
|
1023
|
-
if (!diffArrayWithLcsMatrix(path, frame.base, frame.next, ops, options)) ops.push({
|
|
1180
|
+
if (!diffArrayWithLcsMatrix(path, frame.base, frame.next, ops, options, budgetMeter)) ops.push({
|
|
1024
1181
|
op: "replace",
|
|
1025
1182
|
path: stringifyJsonPointer(path),
|
|
1026
1183
|
value: frame.next
|
|
@@ -1028,7 +1185,7 @@ function diffValue(path, base, next, ops, options) {
|
|
|
1028
1185
|
continue;
|
|
1029
1186
|
}
|
|
1030
1187
|
if (arrayStrategy === "lcs-linear") {
|
|
1031
|
-
if (!diffArrayWithLinearLcs(path, frame.base, frame.next, ops, options)) ops.push({
|
|
1188
|
+
if (!diffArrayWithLinearLcs(path, frame.base, frame.next, ops, options, budgetMeter)) ops.push({
|
|
1032
1189
|
op: "replace",
|
|
1033
1190
|
path: stringifyJsonPointer(path),
|
|
1034
1191
|
value: frame.next
|
|
@@ -1053,6 +1210,7 @@ function diffValue(path, base, next, ops, options) {
|
|
|
1053
1210
|
continue;
|
|
1054
1211
|
}
|
|
1055
1212
|
const { sharedKeys, baseOnlyKeys, nextOnlyKeys } = collectObjectKeys(frame.base, frame.next);
|
|
1213
|
+
budgetMeter?.count("objectEntries", sharedKeys.length + baseOnlyKeys.length + nextOnlyKeys.length, stringifyJsonPointer(path));
|
|
1056
1214
|
if (!(baseOnlyKeys.length > 0 || nextOnlyKeys.length > 0) && (path.length === 0 || sharedKeys.length > 1) && jsonEquals(frame.base, frame.next)) continue;
|
|
1057
1215
|
emitObjectStructuralOps(path, frame.base, frame.next, sharedKeys, baseOnlyKeys, nextOnlyKeys, ops, options);
|
|
1058
1216
|
if (sharedKeys.length > 0) stack.push({
|
|
@@ -1216,34 +1374,44 @@ function insertSortedKey(keys, key) {
|
|
|
1216
1374
|
}
|
|
1217
1375
|
keys.splice(low, 0, key);
|
|
1218
1376
|
}
|
|
1219
|
-
function diffArrayWithLcsMatrix(path, base, next, ops, options) {
|
|
1220
|
-
const window = trimEqualArrayEdges(base, next);
|
|
1377
|
+
function diffArrayWithLcsMatrix(path, base, next, ops, options, budgetMeter) {
|
|
1378
|
+
const window = trimEqualArrayEdges(base, next, options);
|
|
1221
1379
|
const baseStart = window.baseStart;
|
|
1222
1380
|
const nextStart = window.nextStart;
|
|
1223
1381
|
const n = window.unmatchedBaseLength;
|
|
1224
1382
|
const m = window.unmatchedNextLength;
|
|
1383
|
+
budgetMeter?.count("sequenceElements", n + m, stringifyJsonPointer(path));
|
|
1225
1384
|
if (!shouldUseLcsDiff(n, m, options.lcsMaxCells)) return false;
|
|
1385
|
+
budgetMeter?.count("arrayDiffCells", (n + 1) * (m + 1), stringifyJsonPointer(path));
|
|
1226
1386
|
if (n === 0 && m === 0) return true;
|
|
1227
1387
|
const steps = [];
|
|
1228
|
-
buildArrayEditScriptWithMatrix(base, baseStart, baseStart + n, next, nextStart, nextStart + m, steps);
|
|
1388
|
+
buildArrayEditScriptWithMatrix(base, baseStart, baseStart + n, next, nextStart, nextStart + m, steps, options);
|
|
1229
1389
|
pushArrayPatchOps(path, window.prefixLength, steps, ops, base, options);
|
|
1230
1390
|
return true;
|
|
1231
1391
|
}
|
|
1232
|
-
function diffArrayWithLinearLcs(path, base, next, ops, options) {
|
|
1233
|
-
const window = trimEqualArrayEdges(base, next);
|
|
1392
|
+
function diffArrayWithLinearLcs(path, base, next, ops, options, budgetMeter) {
|
|
1393
|
+
const window = trimEqualArrayEdges(base, next, options);
|
|
1394
|
+
budgetMeter?.count("sequenceElements", window.unmatchedBaseLength + window.unmatchedNextLength, stringifyJsonPointer(path));
|
|
1234
1395
|
if (!shouldUseLinearLcsDiff(window.unmatchedBaseLength, window.unmatchedNextLength, options)) return false;
|
|
1396
|
+
budgetMeter?.count("arrayDiffCells", (window.unmatchedBaseLength + 1) * (window.unmatchedNextLength + 1), stringifyJsonPointer(path));
|
|
1235
1397
|
const steps = [];
|
|
1236
|
-
buildArrayEditScriptLinearSpace(base, window.baseStart, window.baseStart + window.unmatchedBaseLength, next, window.nextStart, window.nextStart + window.unmatchedNextLength, steps);
|
|
1398
|
+
buildArrayEditScriptLinearSpace(base, window.baseStart, window.baseStart + window.unmatchedBaseLength, next, window.nextStart, window.nextStart + window.unmatchedNextLength, steps, options);
|
|
1237
1399
|
pushArrayPatchOps(path, window.prefixLength, steps, ops, base, options);
|
|
1238
1400
|
return true;
|
|
1239
1401
|
}
|
|
1240
|
-
function trimEqualArrayEdges(base, next) {
|
|
1402
|
+
function trimEqualArrayEdges(base, next, options) {
|
|
1241
1403
|
const baseLength = base.length;
|
|
1242
1404
|
const nextLength = next.length;
|
|
1243
1405
|
let prefixLength = 0;
|
|
1244
|
-
while (prefixLength < baseLength && prefixLength < nextLength && jsonEquals(base[prefixLength], next[prefixLength]))
|
|
1406
|
+
while (prefixLength < baseLength && prefixLength < nextLength && jsonEquals(base[prefixLength], next[prefixLength])) {
|
|
1407
|
+
throwIfAborted(options.signal);
|
|
1408
|
+
prefixLength += 1;
|
|
1409
|
+
}
|
|
1245
1410
|
let suffixLength = 0;
|
|
1246
|
-
while (suffixLength < baseLength - prefixLength && suffixLength < nextLength - prefixLength && jsonEquals(base[baseLength - 1 - suffixLength], next[nextLength - 1 - suffixLength]))
|
|
1411
|
+
while (suffixLength < baseLength - prefixLength && suffixLength < nextLength - prefixLength && jsonEquals(base[baseLength - 1 - suffixLength], next[nextLength - 1 - suffixLength])) {
|
|
1412
|
+
throwIfAborted(options.signal);
|
|
1413
|
+
suffixLength += 1;
|
|
1414
|
+
}
|
|
1247
1415
|
return {
|
|
1248
1416
|
baseStart: prefixLength,
|
|
1249
1417
|
nextStart: prefixLength,
|
|
@@ -1252,7 +1420,8 @@ function trimEqualArrayEdges(base, next) {
|
|
|
1252
1420
|
unmatchedNextLength: nextLength - prefixLength - suffixLength
|
|
1253
1421
|
};
|
|
1254
1422
|
}
|
|
1255
|
-
function buildArrayEditScriptLinearSpace(base, baseStart, baseEnd, next, nextStart, nextEnd, steps) {
|
|
1423
|
+
function buildArrayEditScriptLinearSpace(base, baseStart, baseEnd, next, nextStart, nextEnd, steps, options) {
|
|
1424
|
+
throwIfAborted(options.signal);
|
|
1256
1425
|
const unmatchedBaseLength = baseEnd - baseStart;
|
|
1257
1426
|
const unmatchedNextLength = nextEnd - nextStart;
|
|
1258
1427
|
if (unmatchedBaseLength === 0) {
|
|
@@ -1267,20 +1436,20 @@ function buildArrayEditScriptLinearSpace(base, baseStart, baseEnd, next, nextSta
|
|
|
1267
1436
|
return;
|
|
1268
1437
|
}
|
|
1269
1438
|
if (unmatchedBaseLength === 1) {
|
|
1270
|
-
pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, steps);
|
|
1439
|
+
pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, steps, options);
|
|
1271
1440
|
return;
|
|
1272
1441
|
}
|
|
1273
1442
|
if (unmatchedNextLength === 1) {
|
|
1274
|
-
pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, steps);
|
|
1443
|
+
pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, steps, options);
|
|
1275
1444
|
return;
|
|
1276
1445
|
}
|
|
1277
1446
|
if (shouldUseMatrixBaseCase(unmatchedBaseLength, unmatchedNextLength)) {
|
|
1278
|
-
buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStart, nextEnd, steps);
|
|
1447
|
+
buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStart, nextEnd, steps, options);
|
|
1279
1448
|
return;
|
|
1280
1449
|
}
|
|
1281
1450
|
const baseMid = baseStart + Math.floor(unmatchedBaseLength / 2);
|
|
1282
|
-
const forwardScores = computeLcsPrefixLengths(base, baseStart, baseMid, next, nextStart, nextEnd);
|
|
1283
|
-
const reverseScores = computeLcsSuffixLengths(base, baseMid, baseEnd, next, nextStart, nextEnd);
|
|
1451
|
+
const forwardScores = computeLcsPrefixLengths(base, baseStart, baseMid, next, nextStart, nextEnd, options);
|
|
1452
|
+
const reverseScores = computeLcsSuffixLengths(base, baseMid, baseEnd, next, nextStart, nextEnd, options);
|
|
1284
1453
|
let bestOffset = 0;
|
|
1285
1454
|
let bestScore = Number.NEGATIVE_INFINITY;
|
|
1286
1455
|
for (let offset = 0; offset <= unmatchedNextLength; offset++) {
|
|
@@ -1291,11 +1460,11 @@ function buildArrayEditScriptLinearSpace(base, baseStart, baseEnd, next, nextSta
|
|
|
1291
1460
|
}
|
|
1292
1461
|
}
|
|
1293
1462
|
const nextMid = nextStart + bestOffset;
|
|
1294
|
-
buildArrayEditScriptLinearSpace(base, baseStart, baseMid, next, nextStart, nextMid, steps);
|
|
1295
|
-
buildArrayEditScriptLinearSpace(base, baseMid, baseEnd, next, nextMid, nextEnd, steps);
|
|
1463
|
+
buildArrayEditScriptLinearSpace(base, baseStart, baseMid, next, nextStart, nextMid, steps, options);
|
|
1464
|
+
buildArrayEditScriptLinearSpace(base, baseMid, baseEnd, next, nextMid, nextEnd, steps, options);
|
|
1296
1465
|
}
|
|
1297
|
-
function pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, steps) {
|
|
1298
|
-
const matchIndex = findFirstMatchingIndexInNext(base[baseStart], next, nextStart, nextEnd);
|
|
1466
|
+
function pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, steps, options) {
|
|
1467
|
+
const matchIndex = findFirstMatchingIndexInNext(base[baseStart], next, nextStart, nextEnd, options);
|
|
1299
1468
|
if (matchIndex === -1) {
|
|
1300
1469
|
steps.push({ kind: "remove" });
|
|
1301
1470
|
for (let nextIndex = nextStart; nextIndex < nextEnd; nextIndex++) steps.push({
|
|
@@ -1314,8 +1483,8 @@ function pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, s
|
|
|
1314
1483
|
value: next[nextIndex]
|
|
1315
1484
|
});
|
|
1316
1485
|
}
|
|
1317
|
-
function pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, steps) {
|
|
1318
|
-
const matchIndex = findFirstMatchingIndexInBase(next[nextStart], base, baseStart, baseEnd);
|
|
1486
|
+
function pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, steps, options) {
|
|
1487
|
+
const matchIndex = findFirstMatchingIndexInBase(next[nextStart], base, baseStart, baseEnd, options);
|
|
1319
1488
|
if (matchIndex === -1) {
|
|
1320
1489
|
for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) steps.push({ kind: "remove" });
|
|
1321
1490
|
steps.push({
|
|
@@ -1328,26 +1497,36 @@ function pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, s
|
|
|
1328
1497
|
steps.push({ kind: "equal" });
|
|
1329
1498
|
for (let baseIndex = matchIndex + 1; baseIndex < baseEnd; baseIndex++) steps.push({ kind: "remove" });
|
|
1330
1499
|
}
|
|
1331
|
-
function findFirstMatchingIndexInNext(target, next, nextStart, nextEnd) {
|
|
1332
|
-
for (let nextIndex = nextStart; nextIndex < nextEnd; nextIndex++)
|
|
1500
|
+
function findFirstMatchingIndexInNext(target, next, nextStart, nextEnd, options) {
|
|
1501
|
+
for (let nextIndex = nextStart; nextIndex < nextEnd; nextIndex++) {
|
|
1502
|
+
throwIfAborted(options.signal);
|
|
1503
|
+
if (jsonEquals(target, next[nextIndex])) return nextIndex;
|
|
1504
|
+
}
|
|
1333
1505
|
return -1;
|
|
1334
1506
|
}
|
|
1335
|
-
function findFirstMatchingIndexInBase(target, base, baseStart, baseEnd) {
|
|
1336
|
-
for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++)
|
|
1507
|
+
function findFirstMatchingIndexInBase(target, base, baseStart, baseEnd, options) {
|
|
1508
|
+
for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) {
|
|
1509
|
+
throwIfAborted(options.signal);
|
|
1510
|
+
if (jsonEquals(target, base[baseIndex])) return baseIndex;
|
|
1511
|
+
}
|
|
1337
1512
|
return -1;
|
|
1338
1513
|
}
|
|
1339
1514
|
function shouldUseMatrixBaseCase(baseLength, nextLength) {
|
|
1340
1515
|
return (baseLength + 1) * (nextLength + 1) <= LINEAR_LCS_MATRIX_BASE_CASE_MAX_CELLS;
|
|
1341
1516
|
}
|
|
1342
|
-
function buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStart, nextEnd, steps) {
|
|
1517
|
+
function buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStart, nextEnd, steps, options) {
|
|
1343
1518
|
const unmatchedBaseLength = baseEnd - baseStart;
|
|
1344
1519
|
const unmatchedNextLength = nextEnd - nextStart;
|
|
1345
1520
|
const lcs = Array.from({ length: unmatchedBaseLength + 1 }, () => Array(unmatchedNextLength + 1).fill(0));
|
|
1346
|
-
for (let baseOffset = unmatchedBaseLength - 1; baseOffset >= 0; baseOffset--)
|
|
1347
|
-
|
|
1521
|
+
for (let baseOffset = unmatchedBaseLength - 1; baseOffset >= 0; baseOffset--) {
|
|
1522
|
+
throwIfAborted(options.signal);
|
|
1523
|
+
for (let nextOffset = unmatchedNextLength - 1; nextOffset >= 0; nextOffset--) if (jsonEquals(base[baseStart + baseOffset], next[nextStart + nextOffset])) lcs[baseOffset][nextOffset] = 1 + lcs[baseOffset + 1][nextOffset + 1];
|
|
1524
|
+
else lcs[baseOffset][nextOffset] = Math.max(lcs[baseOffset + 1][nextOffset], lcs[baseOffset][nextOffset + 1]);
|
|
1525
|
+
}
|
|
1348
1526
|
let baseOffset = 0;
|
|
1349
1527
|
let nextOffset = 0;
|
|
1350
1528
|
while (baseOffset < unmatchedBaseLength || nextOffset < unmatchedNextLength) {
|
|
1529
|
+
throwIfAborted(options.signal);
|
|
1351
1530
|
if (baseOffset < unmatchedBaseLength && nextOffset < unmatchedNextLength && jsonEquals(base[baseStart + baseOffset], next[nextStart + nextOffset])) {
|
|
1352
1531
|
steps.push({ kind: "equal" });
|
|
1353
1532
|
baseOffset += 1;
|
|
@@ -1370,11 +1549,12 @@ function buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStar
|
|
|
1370
1549
|
}
|
|
1371
1550
|
}
|
|
1372
1551
|
}
|
|
1373
|
-
function computeLcsPrefixLengths(base, baseStart, baseEnd, next, nextStart, nextEnd) {
|
|
1552
|
+
function computeLcsPrefixLengths(base, baseStart, baseEnd, next, nextStart, nextEnd, options) {
|
|
1374
1553
|
const unmatchedNextLength = nextEnd - nextStart;
|
|
1375
1554
|
let previousRow = new Int32Array(unmatchedNextLength + 1);
|
|
1376
1555
|
let currentRow = new Int32Array(unmatchedNextLength + 1);
|
|
1377
1556
|
for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) {
|
|
1557
|
+
throwIfAborted(options.signal);
|
|
1378
1558
|
for (let nextOffset = 0; nextOffset < unmatchedNextLength; nextOffset++) if (jsonEquals(base[baseIndex], next[nextStart + nextOffset])) currentRow[nextOffset + 1] = previousRow[nextOffset] + 1;
|
|
1379
1559
|
else currentRow[nextOffset + 1] = Math.max(previousRow[nextOffset + 1], currentRow[nextOffset]);
|
|
1380
1560
|
const nextPreviousRow = currentRow;
|
|
@@ -1384,11 +1564,12 @@ function computeLcsPrefixLengths(base, baseStart, baseEnd, next, nextStart, next
|
|
|
1384
1564
|
}
|
|
1385
1565
|
return previousRow;
|
|
1386
1566
|
}
|
|
1387
|
-
function computeLcsSuffixLengths(base, baseStart, baseEnd, next, nextStart, nextEnd) {
|
|
1567
|
+
function computeLcsSuffixLengths(base, baseStart, baseEnd, next, nextStart, nextEnd, options) {
|
|
1388
1568
|
const unmatchedNextLength = nextEnd - nextStart;
|
|
1389
1569
|
let previousRow = new Int32Array(unmatchedNextLength + 1);
|
|
1390
1570
|
let currentRow = new Int32Array(unmatchedNextLength + 1);
|
|
1391
1571
|
for (let baseIndex = baseEnd - 1; baseIndex >= baseStart; baseIndex--) {
|
|
1572
|
+
throwIfAborted(options.signal);
|
|
1392
1573
|
for (let nextOffset = unmatchedNextLength - 1; nextOffset >= 0; nextOffset--) if (jsonEquals(base[baseIndex], next[nextStart + nextOffset])) currentRow[nextOffset] = previousRow[nextOffset + 1] + 1;
|
|
1393
1574
|
else currentRow[nextOffset] = Math.max(previousRow[nextOffset], currentRow[nextOffset + 1]);
|
|
1394
1575
|
const nextPreviousRow = currentRow;
|
|
@@ -1434,9 +1615,10 @@ function shouldUseLcsDiff(baseLength, nextLength, lcsMaxCells) {
|
|
|
1434
1615
|
}
|
|
1435
1616
|
function shouldUseLinearLcsDiff(baseLength, nextLength, options) {
|
|
1436
1617
|
const cap = options.lcsLinearMaxCells;
|
|
1437
|
-
if (cap ===
|
|
1438
|
-
|
|
1439
|
-
|
|
1618
|
+
if (cap === Number.POSITIVE_INFINITY) return true;
|
|
1619
|
+
const effectiveCap = cap ?? DEFAULT_LCS_MAX_CELLS;
|
|
1620
|
+
if (!Number.isFinite(effectiveCap) || effectiveCap < 1) return false;
|
|
1621
|
+
return (baseLength + 1) * (nextLength + 1) <= effectiveCap;
|
|
1440
1622
|
}
|
|
1441
1623
|
function finalizeArrayOps(arrayPath, base, ops, options) {
|
|
1442
1624
|
if (ops.length === 0) return [];
|
|
@@ -2066,54 +2248,45 @@ function isSamePath(a, b) {
|
|
|
2066
2248
|
* @returns A new CRDT `Doc`.
|
|
2067
2249
|
*/
|
|
2068
2250
|
function docFromJson(value, nextDot) {
|
|
2069
|
-
|
|
2251
|
+
const vv = Object.create(null);
|
|
2252
|
+
const doc = { root: nodeFromJson(value, () => {
|
|
2253
|
+
const dot = nextDot();
|
|
2254
|
+
observeVersionVectorDot(vv, dot);
|
|
2255
|
+
return dot;
|
|
2256
|
+
}) };
|
|
2257
|
+
writeCachedObservedVersionVector(doc, vv);
|
|
2258
|
+
return doc;
|
|
2070
2259
|
}
|
|
2071
2260
|
/**
|
|
2072
|
-
* Legacy
|
|
2073
|
-
*
|
|
2261
|
+
* Legacy helper for tests and fixtures that seeds an entire document from one dot.
|
|
2262
|
+
*
|
|
2263
|
+
* It reuses that dot for object entries and synthesizes array child counters from the
|
|
2264
|
+
* same seed, which can produce low-quality causal metadata and unrealistic sequence
|
|
2265
|
+
* identities in production CRDT state.
|
|
2266
|
+
*
|
|
2267
|
+
* Prefer `docFromJson(value, nextDot)` so every node receives a fresh unique dot.
|
|
2268
|
+
*
|
|
2269
|
+
* @deprecated Use `docFromJson(value, nextDot)` for production documents.
|
|
2074
2270
|
*/
|
|
2075
2271
|
function docFromJsonWithDot(value, dot) {
|
|
2076
2272
|
return { root: deepNodeFromJson(value, dot) };
|
|
2077
2273
|
}
|
|
2078
2274
|
function getSeqAtPath(doc, path) {
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
if (cur.kind !== "obj") return;
|
|
2082
|
-
const ent = cur.entries.get(seg);
|
|
2083
|
-
if (!ent) return;
|
|
2084
|
-
cur = ent.node;
|
|
2085
|
-
}
|
|
2086
|
-
return cur.kind === "seq" ? cur : void 0;
|
|
2275
|
+
const node = getNodeAtPath(doc, path);
|
|
2276
|
+
return node?.kind === "seq" ? node : void 0;
|
|
2087
2277
|
}
|
|
2088
2278
|
function getObjAtPathStrict(doc, path) {
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
if (cur.kind !== "obj") return {
|
|
2093
|
-
ok: false,
|
|
2094
|
-
message: "expected object at /"
|
|
2095
|
-
};
|
|
2279
|
+
const node = getNodeAtPath(doc, path);
|
|
2280
|
+
if (!node || node.kind !== "obj") {
|
|
2281
|
+
const pointer = stringifyJsonPointer(path);
|
|
2096
2282
|
return {
|
|
2097
|
-
ok: true,
|
|
2098
|
-
obj: cur
|
|
2099
|
-
};
|
|
2100
|
-
}
|
|
2101
|
-
for (const seg of path) {
|
|
2102
|
-
if (cur.kind !== "obj") return {
|
|
2103
|
-
ok: false,
|
|
2104
|
-
message: `expected object at /${seen.join("/")}`
|
|
2105
|
-
};
|
|
2106
|
-
const entry = cur.entries.get(seg);
|
|
2107
|
-
seen.push(seg);
|
|
2108
|
-
if (!entry || entry.node.kind !== "obj") return {
|
|
2109
2283
|
ok: false,
|
|
2110
|
-
message: `expected object at
|
|
2284
|
+
message: `expected object at ${pointer === "" ? "/" : pointer}`
|
|
2111
2285
|
};
|
|
2112
|
-
cur = entry.node;
|
|
2113
2286
|
}
|
|
2114
2287
|
return {
|
|
2115
2288
|
ok: true,
|
|
2116
|
-
obj:
|
|
2289
|
+
obj: node
|
|
2117
2290
|
};
|
|
2118
2291
|
}
|
|
2119
2292
|
function ensureSeqAtPath(head, path, dotForCreate) {
|
|
@@ -2160,10 +2333,24 @@ function ensureSeqAtPath(head, path, dotForCreate) {
|
|
|
2160
2333
|
function getNodeAtPath(doc, path) {
|
|
2161
2334
|
let cur = doc.root;
|
|
2162
2335
|
for (const seg of path) {
|
|
2163
|
-
if (cur.kind
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2336
|
+
if (cur.kind === "obj") {
|
|
2337
|
+
const ent = cur.entries.get(seg);
|
|
2338
|
+
if (!ent) return;
|
|
2339
|
+
cur = ent.node;
|
|
2340
|
+
continue;
|
|
2341
|
+
}
|
|
2342
|
+
if (cur.kind === "seq") {
|
|
2343
|
+
if (!ARRAY_INDEX_TOKEN_PATTERN.test(seg)) return;
|
|
2344
|
+
const index = Number(seg);
|
|
2345
|
+
if (!Number.isSafeInteger(index)) return;
|
|
2346
|
+
const elemId = rgaIdAtIndex(cur, index);
|
|
2347
|
+
if (elemId === void 0) return;
|
|
2348
|
+
const elem = cur.elems.get(elemId);
|
|
2349
|
+
if (!elem) return;
|
|
2350
|
+
cur = elem.value;
|
|
2351
|
+
continue;
|
|
2352
|
+
}
|
|
2353
|
+
if (cur.kind === "lww") return;
|
|
2167
2354
|
}
|
|
2168
2355
|
return cur;
|
|
2169
2356
|
}
|
|
@@ -2315,7 +2502,10 @@ function nodeFromJson(value, nextDot) {
|
|
|
2315
2502
|
}
|
|
2316
2503
|
/** Deep-clone a CRDT document. The clone is fully independent of the original. */
|
|
2317
2504
|
function cloneDoc(doc) {
|
|
2318
|
-
|
|
2505
|
+
const cloned = { root: cloneNode(doc.root) };
|
|
2506
|
+
const cached = readCachedObservedVersionVector(doc);
|
|
2507
|
+
if (cached) writeCachedObservedVersionVector(cloned, cached);
|
|
2508
|
+
return cloned;
|
|
2319
2509
|
}
|
|
2320
2510
|
function cloneNode(node) {
|
|
2321
2511
|
return cloneNodeAtDepth(node, 0);
|
|
@@ -2377,38 +2567,88 @@ function getJsonAtDocPathForTest(doc, path) {
|
|
|
2377
2567
|
let cur = doc.root;
|
|
2378
2568
|
for (let i = 0; i < path.length; i++) {
|
|
2379
2569
|
const seg = path[i];
|
|
2380
|
-
|
|
2570
|
+
try {
|
|
2571
|
+
assertTraversalDepth(i + 1);
|
|
2572
|
+
} catch (error) {
|
|
2573
|
+
return {
|
|
2574
|
+
ok: false,
|
|
2575
|
+
error: error instanceof TraversalDepthError ? toDepthApplyError(error) : {
|
|
2576
|
+
ok: false,
|
|
2577
|
+
code: 409,
|
|
2578
|
+
reason: "INVALID_PATCH",
|
|
2579
|
+
message: error instanceof Error ? error.message : "invalid test path"
|
|
2580
|
+
}
|
|
2581
|
+
};
|
|
2582
|
+
}
|
|
2381
2583
|
if (cur.kind === "obj") {
|
|
2382
2584
|
const ent = cur.entries.get(seg);
|
|
2383
|
-
if (!ent)
|
|
2585
|
+
if (!ent) return {
|
|
2586
|
+
ok: false,
|
|
2587
|
+
error: {
|
|
2588
|
+
ok: false,
|
|
2589
|
+
code: 409,
|
|
2590
|
+
reason: "MISSING_TARGET",
|
|
2591
|
+
message: `Missing key '${seg}'`
|
|
2592
|
+
}
|
|
2593
|
+
};
|
|
2384
2594
|
cur = ent.node;
|
|
2385
2595
|
continue;
|
|
2386
2596
|
}
|
|
2387
2597
|
if (cur.kind === "seq") {
|
|
2388
|
-
if (!ARRAY_INDEX_TOKEN_PATTERN.test(seg))
|
|
2389
|
-
|
|
2390
|
-
|
|
2598
|
+
if (!ARRAY_INDEX_TOKEN_PATTERN.test(seg)) return {
|
|
2599
|
+
ok: false,
|
|
2600
|
+
error: {
|
|
2601
|
+
ok: false,
|
|
2602
|
+
code: 409,
|
|
2603
|
+
reason: "INVALID_POINTER",
|
|
2604
|
+
message: `Expected array index, got '${seg}'`
|
|
2605
|
+
}
|
|
2606
|
+
};
|
|
2607
|
+
const idx = Number(seg);
|
|
2608
|
+
if (!Number.isSafeInteger(idx)) return {
|
|
2609
|
+
ok: false,
|
|
2610
|
+
error: {
|
|
2611
|
+
ok: false,
|
|
2612
|
+
code: 409,
|
|
2613
|
+
reason: "OUT_OF_BOUNDS",
|
|
2614
|
+
message: `Index out of bounds at '${seg}'`
|
|
2615
|
+
}
|
|
2616
|
+
};
|
|
2617
|
+
const id = rgaIdAtIndex(cur, idx);
|
|
2618
|
+
if (id === void 0) return {
|
|
2619
|
+
ok: false,
|
|
2620
|
+
error: {
|
|
2621
|
+
ok: false,
|
|
2622
|
+
code: 409,
|
|
2623
|
+
reason: "OUT_OF_BOUNDS",
|
|
2624
|
+
message: `Index out of bounds at '${seg}'`
|
|
2625
|
+
}
|
|
2626
|
+
};
|
|
2391
2627
|
cur = cur.elems.get(id).value;
|
|
2392
2628
|
continue;
|
|
2393
2629
|
}
|
|
2394
|
-
throw new Error(`Cannot traverse into non-container at '${seg}'`);
|
|
2395
|
-
}
|
|
2396
|
-
return cur.kind === "lww" ? cur.value : materialize(cur);
|
|
2397
|
-
}
|
|
2398
|
-
function applyTest(base, head, it, evalTestAgainst) {
|
|
2399
|
-
let got;
|
|
2400
|
-
try {
|
|
2401
|
-
got = getJsonAtDocPathForTest(evalTestAgainst === "head" ? head : base, it.path);
|
|
2402
|
-
} catch {
|
|
2403
2630
|
return {
|
|
2404
2631
|
ok: false,
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2632
|
+
error: {
|
|
2633
|
+
ok: false,
|
|
2634
|
+
code: 409,
|
|
2635
|
+
reason: "INVALID_TARGET",
|
|
2636
|
+
message: `Cannot traverse into non-container at '${seg}'`
|
|
2637
|
+
}
|
|
2409
2638
|
};
|
|
2410
2639
|
}
|
|
2411
|
-
|
|
2640
|
+
return {
|
|
2641
|
+
ok: true,
|
|
2642
|
+
value: cur.kind === "lww" ? cur.value : materialize(cur)
|
|
2643
|
+
};
|
|
2644
|
+
}
|
|
2645
|
+
function applyTest(base, head, it, evalTestAgainst) {
|
|
2646
|
+
const got = getJsonAtDocPathForTest(evalTestAgainst === "head" ? head : base, it.path);
|
|
2647
|
+
if (!got.ok) return {
|
|
2648
|
+
...got.error,
|
|
2649
|
+
path: `/${it.path.join("/")}`
|
|
2650
|
+
};
|
|
2651
|
+
if (!jsonEquals(got.value, it.value)) return {
|
|
2412
2652
|
ok: false,
|
|
2413
2653
|
code: 409,
|
|
2414
2654
|
reason: "TEST_FAILED",
|
|
@@ -2473,7 +2713,7 @@ function createArrayIndexLookupSession() {
|
|
|
2473
2713
|
return created;
|
|
2474
2714
|
} };
|
|
2475
2715
|
}
|
|
2476
|
-
function applyArrInsert(base, head, it, newDot, indexSession, bumpCounterAbove, strictParents =
|
|
2716
|
+
function applyArrInsert(base, head, it, newDot, indexSession, bumpCounterAbove, strictParents = true) {
|
|
2477
2717
|
const pointer = `/${it.path.join("/")}`;
|
|
2478
2718
|
const baseSeq = getSeqAtPath(base, it.path);
|
|
2479
2719
|
if (!baseSeq) {
|
|
@@ -2622,39 +2862,51 @@ function applyArrReplace(base, head, it, newDot, indexSession) {
|
|
|
2622
2862
|
* @param evalTestAgainst - Whether `test` ops are evaluated against `"head"` or `"base"`.
|
|
2623
2863
|
* @param bumpCounterAbove - Optional hook that can fast-forward the underlying counter before inserts.
|
|
2624
2864
|
* @param options - Optional behavior toggles.
|
|
2625
|
-
* @param options.strictParents -
|
|
2865
|
+
* @param options.strictParents - Reject array inserts whose base parent path is missing.
|
|
2866
|
+
* Defaults to `true`; pass `false` only for legacy missing-parent array auto-creation.
|
|
2626
2867
|
* @returns `{ ok: true }` on success, or `{ ok: false, code: 409, message }` on conflict.
|
|
2627
2868
|
*/
|
|
2628
2869
|
function applyIntentsToCrdt(base, head, intents, newDot, evalTestAgainst = "head", bumpCounterAbove, options = {}) {
|
|
2629
2870
|
const arrayIndexSession = createArrayIndexLookupSession();
|
|
2871
|
+
let pendingObservedDots = [];
|
|
2872
|
+
const observedNewDot = () => {
|
|
2873
|
+
const dot = newDot();
|
|
2874
|
+
pendingObservedDots.push(dot);
|
|
2875
|
+
return dot;
|
|
2876
|
+
};
|
|
2630
2877
|
for (const it of intents) {
|
|
2631
2878
|
let fail = null;
|
|
2879
|
+
pendingObservedDots = [];
|
|
2632
2880
|
switch (it.t) {
|
|
2633
2881
|
case "Test":
|
|
2634
2882
|
fail = applyTest(base, head, it, evalTestAgainst);
|
|
2635
2883
|
break;
|
|
2636
2884
|
case "ObjSet":
|
|
2637
|
-
fail = applyObjSet(head, it,
|
|
2885
|
+
fail = applyObjSet(head, it, observedNewDot);
|
|
2638
2886
|
break;
|
|
2639
2887
|
case "ObjRemove":
|
|
2640
|
-
fail = applyObjRemove(head, it,
|
|
2888
|
+
fail = applyObjRemove(head, it, observedNewDot);
|
|
2641
2889
|
break;
|
|
2642
2890
|
case "ArrInsert":
|
|
2643
|
-
fail = applyArrInsert(base, head, it,
|
|
2891
|
+
fail = applyArrInsert(base, head, it, observedNewDot, arrayIndexSession, bumpCounterAbove, options.strictParents ?? true);
|
|
2644
2892
|
break;
|
|
2645
2893
|
case "ArrDelete":
|
|
2646
|
-
fail = applyArrDelete(base, head, it,
|
|
2894
|
+
fail = applyArrDelete(base, head, it, observedNewDot, arrayIndexSession);
|
|
2647
2895
|
break;
|
|
2648
2896
|
case "ArrReplace":
|
|
2649
|
-
fail = applyArrReplace(base, head, it,
|
|
2897
|
+
fail = applyArrReplace(base, head, it, observedNewDot, arrayIndexSession);
|
|
2650
2898
|
break;
|
|
2651
2899
|
default: assertNever(it, "Unhandled intent type");
|
|
2652
2900
|
}
|
|
2653
|
-
if (fail)
|
|
2901
|
+
if (fail) {
|
|
2902
|
+
pendingObservedDots = [];
|
|
2903
|
+
return fail;
|
|
2904
|
+
}
|
|
2905
|
+
for (const dot of pendingObservedDots) observeDocVersionVectorDot(head, dot);
|
|
2654
2906
|
}
|
|
2655
2907
|
return { ok: true };
|
|
2656
2908
|
}
|
|
2657
|
-
function jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove, strictParents =
|
|
2909
|
+
function jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove, strictParents = true) {
|
|
2658
2910
|
if (isJsonPatchToCrdtOptions(baseOrOptions)) return jsonPatchToCrdtInternal(baseOrOptions);
|
|
2659
2911
|
if (!head || !patch || !newDot) return {
|
|
2660
2912
|
ok: false,
|
|
@@ -2672,7 +2924,7 @@ function jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst = "
|
|
|
2672
2924
|
strictParents
|
|
2673
2925
|
});
|
|
2674
2926
|
}
|
|
2675
|
-
function jsonPatchToCrdtSafe(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove, strictParents =
|
|
2927
|
+
function jsonPatchToCrdtSafe(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove, strictParents = true) {
|
|
2676
2928
|
try {
|
|
2677
2929
|
if (isJsonPatchToCrdtOptions(baseOrOptions)) return jsonPatchToCrdt(baseOrOptions);
|
|
2678
2930
|
if (!head || !patch || !newDot) return {
|
|
@@ -2713,6 +2965,46 @@ function rebaseDiffOps(path, nestedOps, out) {
|
|
|
2713
2965
|
throw new Error(`Unexpected op '${op.op}' from diffJsonPatch`);
|
|
2714
2966
|
}
|
|
2715
2967
|
}
|
|
2968
|
+
function collectLiveSequenceElements(seq) {
|
|
2969
|
+
const elems = [];
|
|
2970
|
+
const cursor = rgaCreateLinearCursor(seq);
|
|
2971
|
+
for (let elem = cursor.next(); elem; elem = cursor.next()) elems.push(elem);
|
|
2972
|
+
return elems;
|
|
2973
|
+
}
|
|
2974
|
+
function materializeSequenceWindow(elems, start, end) {
|
|
2975
|
+
const out = [];
|
|
2976
|
+
for (let i = start; i < end; i++) out.push(nodeToJsonForPatch(elems[i].value));
|
|
2977
|
+
return out;
|
|
2978
|
+
}
|
|
2979
|
+
function rebaseSequenceWindowDiffOps(path, indexOffset, nestedOps, out) {
|
|
2980
|
+
const pending = [];
|
|
2981
|
+
for (const op of nestedOps) {
|
|
2982
|
+
if (op.path === "") return false;
|
|
2983
|
+
const rebasedSegments = parseJsonPointer(op.path);
|
|
2984
|
+
const indexToken = rebasedSegments[0];
|
|
2985
|
+
if (!indexToken || !ARRAY_INDEX_TOKEN_PATTERN.test(indexToken)) return false;
|
|
2986
|
+
rebasedSegments[0] = String(Number(indexToken) + indexOffset);
|
|
2987
|
+
const rebasedPath = stringifyJsonPointer([...path, ...rebasedSegments]);
|
|
2988
|
+
if (op.op === "remove") {
|
|
2989
|
+
pending.push({
|
|
2990
|
+
op: "remove",
|
|
2991
|
+
path: rebasedPath
|
|
2992
|
+
});
|
|
2993
|
+
continue;
|
|
2994
|
+
}
|
|
2995
|
+
if (op.op === "add" || op.op === "replace") {
|
|
2996
|
+
pending.push({
|
|
2997
|
+
op: op.op,
|
|
2998
|
+
path: rebasedPath,
|
|
2999
|
+
value: op.value
|
|
3000
|
+
});
|
|
3001
|
+
continue;
|
|
3002
|
+
}
|
|
3003
|
+
return false;
|
|
3004
|
+
}
|
|
3005
|
+
out.push(...pending);
|
|
3006
|
+
return true;
|
|
3007
|
+
}
|
|
2716
3008
|
function nodesJsonEqual(baseNode, headNode, depth) {
|
|
2717
3009
|
assertTraversalDepth(depth);
|
|
2718
3010
|
if (baseNode === headNode) return true;
|
|
@@ -2837,6 +3129,35 @@ function diffObjectNodes(path, baseNode, headNode, options, ops, depth) {
|
|
|
2837
3129
|
headIndex += 1;
|
|
2838
3130
|
}
|
|
2839
3131
|
}
|
|
3132
|
+
function diffSequenceNodes(path, baseNode, headSeq, options, ops, depth) {
|
|
3133
|
+
if ((options.arrayStrategy ?? "lcs") === "atomic") {
|
|
3134
|
+
rebaseDiffOps(path, diffJsonPatch(materialize(baseNode), materialize(headSeq), options), ops);
|
|
3135
|
+
return;
|
|
3136
|
+
}
|
|
3137
|
+
const baseElems = collectLiveSequenceElements(baseNode);
|
|
3138
|
+
const headElems = collectLiveSequenceElements(headSeq);
|
|
3139
|
+
const sharedLength = Math.min(baseElems.length, headElems.length);
|
|
3140
|
+
let prefixLength = 0;
|
|
3141
|
+
while (prefixLength < sharedLength && nodesJsonEqual(baseElems[prefixLength].value, headElems[prefixLength].value, depth + 1)) prefixLength += 1;
|
|
3142
|
+
if (prefixLength === baseElems.length && prefixLength === headElems.length) return;
|
|
3143
|
+
let baseEnd = baseElems.length;
|
|
3144
|
+
let headEnd = headElems.length;
|
|
3145
|
+
while (baseEnd > prefixLength && headEnd > prefixLength && nodesJsonEqual(baseElems[baseEnd - 1].value, headElems[headEnd - 1].value, depth + 1)) {
|
|
3146
|
+
baseEnd -= 1;
|
|
3147
|
+
headEnd -= 1;
|
|
3148
|
+
}
|
|
3149
|
+
const unmatchedBaseLength = baseEnd - prefixLength;
|
|
3150
|
+
const unmatchedHeadLength = headEnd - prefixLength;
|
|
3151
|
+
if (unmatchedBaseLength === 1 && unmatchedHeadLength === 1) {
|
|
3152
|
+
path.push(String(prefixLength));
|
|
3153
|
+
diffNodeToPatch(path, baseElems[prefixLength].value, headElems[prefixLength].value, options, ops, depth + 1);
|
|
3154
|
+
path.pop();
|
|
3155
|
+
return;
|
|
3156
|
+
}
|
|
3157
|
+
const seqOps = diffJsonPatch(materializeSequenceWindow(baseElems, prefixLength, baseEnd), materializeSequenceWindow(headElems, prefixLength, headEnd), options);
|
|
3158
|
+
if (rebaseSequenceWindowDiffOps(path, prefixLength, seqOps, ops)) return;
|
|
3159
|
+
rebaseDiffOps(path, diffJsonPatch(materialize(baseNode), materialize(headSeq), options), ops);
|
|
3160
|
+
}
|
|
2840
3161
|
function diffNodeToPatch(path, baseNode, headNode, options, ops, depth) {
|
|
2841
3162
|
assertTraversalDepth(depth);
|
|
2842
3163
|
if (baseNode === headNode) return;
|
|
@@ -2862,8 +3183,7 @@ function diffNodeToPatch(path, baseNode, headNode, options, ops, depth) {
|
|
|
2862
3183
|
diffObjectNodes(path, baseNode, headNode, options, ops, depth);
|
|
2863
3184
|
return;
|
|
2864
3185
|
}
|
|
2865
|
-
|
|
2866
|
-
rebaseDiffOps(path, diffJsonPatch(materialize(baseNode), materialize(headSeq), options), ops);
|
|
3186
|
+
diffSequenceNodes(path, baseNode, headNode, options, ops, depth);
|
|
2867
3187
|
}
|
|
2868
3188
|
/**
|
|
2869
3189
|
* Generate a JSON Patch delta between two CRDT documents.
|
|
@@ -2873,7 +3193,7 @@ function diffNodeToPatch(path, baseNode, headNode, options, ops, depth) {
|
|
|
2873
3193
|
* @returns An array of JSON Patch operations that transform base into head.
|
|
2874
3194
|
*/
|
|
2875
3195
|
function crdtToJsonPatch(base, head, options) {
|
|
2876
|
-
if ((options?.jsonValidation ?? "none") !== "none") return diffJsonPatch(materialize(base.root), materialize(head.root), options);
|
|
3196
|
+
if ((options?.jsonValidation ?? "none") !== "none" || options?.resourceBudget !== void 0) return diffJsonPatch(materialize(base.root), materialize(head.root), options);
|
|
2877
3197
|
return crdtNodesToJsonPatch(base.root, head.root, options);
|
|
2878
3198
|
}
|
|
2879
3199
|
/** Internals-only helper for diffing CRDT nodes from an existing traversal depth. */
|
|
@@ -2895,17 +3215,27 @@ function crdtToFullReplace(doc) {
|
|
|
2895
3215
|
}
|
|
2896
3216
|
function jsonPatchToCrdtInternal(options) {
|
|
2897
3217
|
const evalTestAgainst = options.evalTestAgainst ?? "head";
|
|
2898
|
-
|
|
3218
|
+
const semantics = options.semantics ?? "sequential";
|
|
3219
|
+
const budgetMeter = createBudgetMeter(options.resourceBudget);
|
|
3220
|
+
try {
|
|
3221
|
+
budgetMeter?.count("patchOperations", options.patch.length);
|
|
3222
|
+
} catch (error) {
|
|
3223
|
+
return toApplyError$1(error);
|
|
3224
|
+
}
|
|
3225
|
+
if (semantics === "base") {
|
|
2899
3226
|
const baseJson = materialize(options.base.root);
|
|
2900
3227
|
let intents;
|
|
2901
3228
|
try {
|
|
2902
|
-
intents = compileJsonPatchToIntent(baseJson, options.patch, {
|
|
3229
|
+
intents = compileJsonPatchToIntent(baseJson, options.patch, {
|
|
3230
|
+
semantics: "base",
|
|
3231
|
+
resourceBudget: options.resourceBudget
|
|
3232
|
+
});
|
|
2903
3233
|
} catch (error) {
|
|
2904
3234
|
return toApplyError$1(error);
|
|
2905
3235
|
}
|
|
2906
3236
|
return applyIntentsToCrdt(options.base, options.head, intents, options.newDot, evalTestAgainst, options.bumpCounterAbove, { strictParents: options.strictParents });
|
|
2907
3237
|
}
|
|
2908
|
-
|
|
3238
|
+
const shadowBase = evalTestAgainst === "base" ? cloneDoc(options.base) : null;
|
|
2909
3239
|
let shadowCtr = 0;
|
|
2910
3240
|
const shadowDot = () => ({
|
|
2911
3241
|
actor: "__shadow__",
|
|
@@ -2914,108 +3244,396 @@ function jsonPatchToCrdtInternal(options) {
|
|
|
2914
3244
|
const shadowBump = (ctr) => {
|
|
2915
3245
|
if (shadowCtr < ctr) shadowCtr = ctr;
|
|
2916
3246
|
};
|
|
2917
|
-
const
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
try {
|
|
2921
|
-
intents = compileJsonPatchToIntent(baseJson, [op], { semantics: "sequential" });
|
|
2922
|
-
} catch (error) {
|
|
2923
|
-
return withOpIndex(toApplyError$1(error), opIndex);
|
|
2924
|
-
}
|
|
2925
|
-
const headStep = applyIntentsToCrdt(shadowBase, options.head, intents, options.newDot, evalTestAgainst, options.bumpCounterAbove, { strictParents: options.strictParents });
|
|
2926
|
-
if (!headStep.ok) return withOpIndex(headStep, opIndex);
|
|
2927
|
-
if (evalTestAgainst === "base") {
|
|
2928
|
-
const shadowStep = applyIntentsToCrdt(shadowBase, shadowBase, intents, shadowDot, "base", shadowBump, { strictParents: options.strictParents });
|
|
2929
|
-
if (!shadowStep.ok) return withOpIndex(shadowStep, opIndex);
|
|
2930
|
-
} else shadowBase = cloneDoc(options.head);
|
|
2931
|
-
return { ok: true };
|
|
2932
|
-
};
|
|
2933
|
-
for (let opIndex = 0; opIndex < options.patch.length; opIndex++) {
|
|
2934
|
-
const op = options.patch[opIndex];
|
|
2935
|
-
if (op.op === "move") {
|
|
2936
|
-
const baseJson = materialize(shadowBase.root);
|
|
2937
|
-
let fromValue;
|
|
2938
|
-
try {
|
|
2939
|
-
fromValue = structuredClone(getAtJson(baseJson, parseJsonPointer(op.from)));
|
|
2940
|
-
} catch {
|
|
2941
|
-
try {
|
|
2942
|
-
compileJsonPatchToIntent(baseJson, [{
|
|
2943
|
-
op: "remove",
|
|
2944
|
-
path: op.from
|
|
2945
|
-
}], { semantics: "sequential" });
|
|
2946
|
-
} catch (error) {
|
|
2947
|
-
return withOpIndex(toApplyError$1(error), opIndex);
|
|
2948
|
-
}
|
|
2949
|
-
return withOpIndex(toApplyError$1(/* @__PURE__ */ new Error(`failed to resolve move source at ${op.from}`)), opIndex);
|
|
2950
|
-
}
|
|
2951
|
-
if (op.from === op.path) continue;
|
|
2952
|
-
const removeStep = applySequentialOp({
|
|
2953
|
-
op: "remove",
|
|
2954
|
-
path: op.from
|
|
2955
|
-
}, opIndex);
|
|
2956
|
-
if (!removeStep.ok) return removeStep;
|
|
2957
|
-
const addStep = applySequentialOp({
|
|
2958
|
-
op: "add",
|
|
2959
|
-
path: op.path,
|
|
2960
|
-
value: fromValue
|
|
2961
|
-
}, opIndex);
|
|
2962
|
-
if (!addStep.ok) return addStep;
|
|
2963
|
-
continue;
|
|
2964
|
-
}
|
|
2965
|
-
const step = applySequentialOp(op, opIndex);
|
|
3247
|
+
const session = { pointerCache: /* @__PURE__ */ new Map() };
|
|
3248
|
+
for (const [opIndex, op] of options.patch.entries()) {
|
|
3249
|
+
const step = applySequentialPatchOp(options, evalTestAgainst === "base" ? shadowBase : options.head, op, opIndex, evalTestAgainst, shadowDot, shadowBump, session);
|
|
2966
3250
|
if (!step.ok) return step;
|
|
2967
3251
|
}
|
|
2968
3252
|
return { ok: true };
|
|
2969
3253
|
}
|
|
2970
|
-
function
|
|
2971
|
-
if (
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
3254
|
+
function applySequentialPatchOp(options, compileBase, op, opIndex, evalTestAgainst, shadowDot, shadowBump, session) {
|
|
3255
|
+
if (op.op === "move") {
|
|
3256
|
+
if (op.from === op.path) {
|
|
3257
|
+
const pathCheck = resolveValueAtPointerInDoc$1(compileBase, op.from, opIndex, session.pointerCache);
|
|
3258
|
+
if (!pathCheck.ok) return pathCheck;
|
|
3259
|
+
return { ok: true };
|
|
3260
|
+
}
|
|
3261
|
+
const fromResolved = resolveValueAtPointerInDoc$1(compileBase, op.from, opIndex, session.pointerCache);
|
|
3262
|
+
if (!fromResolved.ok) return fromResolved;
|
|
3263
|
+
const removeStep = applySingleSequentialPatchStep(options, compileBase, {
|
|
3264
|
+
op: "remove",
|
|
3265
|
+
path: op.from
|
|
3266
|
+
}, opIndex, evalTestAgainst, shadowDot, shadowBump, session);
|
|
3267
|
+
if (!removeStep.ok) return removeStep;
|
|
3268
|
+
return applySingleSequentialPatchStep(options, compileBase, {
|
|
3269
|
+
op: "add",
|
|
3270
|
+
path: op.path,
|
|
3271
|
+
value: structuredClone(fromResolved.value)
|
|
3272
|
+
}, opIndex, evalTestAgainst, shadowDot, shadowBump, session);
|
|
3273
|
+
}
|
|
3274
|
+
if (op.op === "copy") {
|
|
3275
|
+
const fromResolved = resolveValueAtPointerInDoc$1(compileBase, op.from, opIndex, session.pointerCache);
|
|
3276
|
+
if (!fromResolved.ok) return fromResolved;
|
|
3277
|
+
return applySingleSequentialPatchStep(options, compileBase, {
|
|
3278
|
+
op: "add",
|
|
3279
|
+
path: op.path,
|
|
3280
|
+
value: structuredClone(fromResolved.value)
|
|
3281
|
+
}, opIndex, evalTestAgainst, shadowDot, shadowBump, session);
|
|
3282
|
+
}
|
|
3283
|
+
return applySingleSequentialPatchStep(options, compileBase, op, opIndex, evalTestAgainst, shadowDot, shadowBump, session);
|
|
2976
3284
|
}
|
|
2977
|
-
function
|
|
2978
|
-
|
|
3285
|
+
function applySingleSequentialPatchStep(options, compileBase, op, opIndex, evalTestAgainst, shadowDot, shadowBump, session) {
|
|
3286
|
+
const compiled = compilePreparedSingleIntentFromDoc$1(compileBase, op, session.pointerCache, opIndex);
|
|
3287
|
+
if (!compiled.ok) return compiled;
|
|
3288
|
+
const headStep = applyIntentsToCrdt(compileBase, options.head, compiled.intents, options.newDot, evalTestAgainst, options.bumpCounterAbove, { strictParents: options.strictParents });
|
|
3289
|
+
if (!headStep.ok) return withOpIndex$1(headStep, opIndex);
|
|
3290
|
+
if (op.op === "test") return { ok: true };
|
|
3291
|
+
if (evalTestAgainst === "head") return { ok: true };
|
|
3292
|
+
const shadowStep = applyIntentsToCrdt(compileBase, compileBase, compiled.intents, shadowDot, "base", shadowBump, { strictParents: options.strictParents });
|
|
3293
|
+
if (!shadowStep.ok) return withOpIndex$1(shadowStep, opIndex);
|
|
3294
|
+
return { ok: true };
|
|
2979
3295
|
}
|
|
2980
|
-
function
|
|
2981
|
-
|
|
2982
|
-
if (
|
|
3296
|
+
function resolveValueAtPointerInDoc$1(doc, pointer, opIndex, pointerCache) {
|
|
3297
|
+
const parsedPath = parsePointerWithCache$1(pointer, pointerCache, opIndex);
|
|
3298
|
+
if (!parsedPath.ok) return parsedPath;
|
|
3299
|
+
const resolved = resolveNodeAtPath$1(doc.root, parsedPath.path);
|
|
3300
|
+
if (!resolved.ok) return {
|
|
2983
3301
|
ok: false,
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
path: error.path,
|
|
2988
|
-
opIndex: error.opIndex
|
|
3302
|
+
...resolved.error,
|
|
3303
|
+
path: pointer,
|
|
3304
|
+
opIndex
|
|
2989
3305
|
};
|
|
2990
3306
|
return {
|
|
2991
|
-
ok:
|
|
2992
|
-
|
|
2993
|
-
reason: "INVALID_PATCH",
|
|
2994
|
-
message: error instanceof Error ? error.message : "failed to compile/apply patch"
|
|
3307
|
+
ok: true,
|
|
3308
|
+
value: nodeToJsonForPatch(resolved.node)
|
|
2995
3309
|
};
|
|
2996
3310
|
}
|
|
2997
|
-
function
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3311
|
+
function compilePreparedSingleIntentFromDoc$1(baseDoc, op, pointerCache, opIndex) {
|
|
3312
|
+
const parsedPath = parsePointerWithCache$1(op.path, pointerCache, opIndex);
|
|
3313
|
+
if (!parsedPath.ok) return parsedPath;
|
|
3314
|
+
const path = parsedPath.path;
|
|
3315
|
+
if (op.op === "test") return {
|
|
3316
|
+
ok: true,
|
|
3317
|
+
intents: [{
|
|
3318
|
+
t: "Test",
|
|
3319
|
+
path,
|
|
3320
|
+
value: op.value
|
|
3321
|
+
}]
|
|
3322
|
+
};
|
|
3323
|
+
if (path.length === 0) {
|
|
3324
|
+
if (op.op === "remove") return {
|
|
3325
|
+
ok: false,
|
|
3326
|
+
code: 409,
|
|
3327
|
+
reason: "INVALID_TARGET",
|
|
3328
|
+
message: "remove at root path is not supported in RFC-compliant mode",
|
|
3329
|
+
path: op.path,
|
|
3330
|
+
opIndex
|
|
3331
|
+
};
|
|
3332
|
+
return {
|
|
3333
|
+
ok: true,
|
|
3334
|
+
intents: [{
|
|
3335
|
+
t: "ObjSet",
|
|
3336
|
+
path: [],
|
|
3337
|
+
key: ROOT_KEY,
|
|
3338
|
+
value: op.value
|
|
3339
|
+
}]
|
|
3340
|
+
};
|
|
3341
|
+
}
|
|
3342
|
+
const parentPath = path.slice(0, -1);
|
|
3343
|
+
const parentPointer = stringifyJsonPointer(parentPath);
|
|
3344
|
+
const key = path[path.length - 1];
|
|
3345
|
+
const resolvedParent = parentPath.length === 0 ? {
|
|
3346
|
+
ok: true,
|
|
3347
|
+
node: baseDoc.root
|
|
3348
|
+
} : resolveNodeAtPath$1(baseDoc.root, parentPath);
|
|
3349
|
+
if (!resolvedParent.ok) return {
|
|
3350
|
+
ok: false,
|
|
3351
|
+
...resolvedParent.error,
|
|
3352
|
+
path: parentPointer,
|
|
3353
|
+
opIndex
|
|
3354
|
+
};
|
|
3355
|
+
const parentNode = resolvedParent.node;
|
|
3356
|
+
if (parentNode.kind === "seq") {
|
|
3357
|
+
const parsedIndex = parseArrayIndexTokenForDoc$1(key, op.op, op.path, opIndex);
|
|
3358
|
+
if (!parsedIndex.ok) return parsedIndex;
|
|
3359
|
+
const boundedIndex = validateArrayIndexBounds$1(parsedIndex.index, op.op, rgaLength(parentNode), op.path, opIndex);
|
|
3360
|
+
if (!boundedIndex.ok) return boundedIndex;
|
|
3361
|
+
if (op.op === "add") return {
|
|
3362
|
+
ok: true,
|
|
3363
|
+
intents: [{
|
|
3364
|
+
t: "ArrInsert",
|
|
3365
|
+
path: parentPath,
|
|
3366
|
+
index: boundedIndex.index,
|
|
3367
|
+
value: op.value
|
|
3368
|
+
}]
|
|
3369
|
+
};
|
|
3370
|
+
if (op.op === "remove") return {
|
|
3371
|
+
ok: true,
|
|
3372
|
+
intents: [{
|
|
3373
|
+
t: "ArrDelete",
|
|
3374
|
+
path: parentPath,
|
|
3375
|
+
index: boundedIndex.index
|
|
3376
|
+
}]
|
|
3377
|
+
};
|
|
3378
|
+
return {
|
|
3379
|
+
ok: true,
|
|
3380
|
+
intents: [{
|
|
3381
|
+
t: "ArrReplace",
|
|
3382
|
+
path: parentPath,
|
|
3383
|
+
index: boundedIndex.index,
|
|
3384
|
+
value: op.value
|
|
3385
|
+
}]
|
|
3386
|
+
};
|
|
3387
|
+
}
|
|
3388
|
+
if (parentNode.kind !== "obj") return {
|
|
3389
|
+
ok: false,
|
|
3390
|
+
code: 409,
|
|
3391
|
+
reason: "INVALID_TARGET",
|
|
3392
|
+
message: `expected object or array parent at ${parentPointer}`,
|
|
3393
|
+
path: parentPointer,
|
|
3394
|
+
opIndex
|
|
3395
|
+
};
|
|
3396
|
+
if (key === "__proto__") return {
|
|
3397
|
+
ok: false,
|
|
3398
|
+
code: 409,
|
|
3399
|
+
reason: "INVALID_POINTER",
|
|
3400
|
+
message: `unsafe object key at ${op.path}`,
|
|
3401
|
+
path: op.path,
|
|
3402
|
+
opIndex
|
|
3403
|
+
};
|
|
3404
|
+
const entry = parentNode.entries.get(key);
|
|
3405
|
+
if ((op.op === "replace" || op.op === "remove") && !entry) return {
|
|
3406
|
+
ok: false,
|
|
3407
|
+
code: 409,
|
|
3408
|
+
reason: "MISSING_TARGET",
|
|
3409
|
+
message: `missing key ${key} at ${parentPointer}`,
|
|
3410
|
+
path: op.path,
|
|
3411
|
+
opIndex
|
|
3412
|
+
};
|
|
3413
|
+
if (op.op === "remove") return {
|
|
3414
|
+
ok: true,
|
|
3415
|
+
intents: [{
|
|
3416
|
+
t: "ObjRemove",
|
|
3417
|
+
path: parentPath,
|
|
3418
|
+
key
|
|
3419
|
+
}]
|
|
3420
|
+
};
|
|
3421
|
+
return {
|
|
3422
|
+
ok: true,
|
|
3423
|
+
intents: [{
|
|
3424
|
+
t: "ObjSet",
|
|
3425
|
+
path: parentPath,
|
|
3426
|
+
key,
|
|
3427
|
+
value: op.value,
|
|
3428
|
+
mode: op.op
|
|
3429
|
+
}]
|
|
3430
|
+
};
|
|
3431
|
+
}
|
|
3432
|
+
function parsePointerWithCache$1(pointer, pointerCache, opIndex) {
|
|
3433
|
+
const cachedPath = pointerCache.get(pointer);
|
|
3434
|
+
if (cachedPath !== void 0) return {
|
|
3435
|
+
ok: true,
|
|
3436
|
+
path: cachedPath.slice()
|
|
3437
|
+
};
|
|
3438
|
+
try {
|
|
3439
|
+
const parsedPath = parseJsonPointer(pointer);
|
|
3440
|
+
pointerCache.set(pointer, parsedPath);
|
|
3441
|
+
return {
|
|
3442
|
+
ok: true,
|
|
3443
|
+
path: parsedPath.slice()
|
|
3444
|
+
};
|
|
3445
|
+
} catch (error) {
|
|
3446
|
+
return {
|
|
3447
|
+
ok: false,
|
|
3448
|
+
code: 409,
|
|
3449
|
+
reason: "INVALID_POINTER",
|
|
3450
|
+
message: error instanceof Error ? error.message : "invalid pointer",
|
|
3451
|
+
path: pointer,
|
|
3452
|
+
opIndex
|
|
3453
|
+
};
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
function resolveNodeAtPath$1(root, path) {
|
|
3457
|
+
let current = root;
|
|
3458
|
+
for (const segment of path) {
|
|
3459
|
+
if (current.kind === "obj") {
|
|
3460
|
+
const entry = current.entries.get(segment);
|
|
3461
|
+
if (!entry) return {
|
|
3462
|
+
ok: false,
|
|
3463
|
+
error: {
|
|
3464
|
+
code: 409,
|
|
3465
|
+
reason: "MISSING_PARENT",
|
|
3466
|
+
message: `Missing key '${segment}'`
|
|
3467
|
+
}
|
|
3468
|
+
};
|
|
3469
|
+
current = entry.node;
|
|
3470
|
+
continue;
|
|
3471
|
+
}
|
|
3472
|
+
if (current.kind === "seq") {
|
|
3473
|
+
if (!ARRAY_INDEX_TOKEN_PATTERN.test(segment)) return {
|
|
3474
|
+
ok: false,
|
|
3475
|
+
error: {
|
|
3476
|
+
code: 409,
|
|
3477
|
+
reason: "INVALID_POINTER",
|
|
3478
|
+
message: `Expected array index, got '${segment}'`
|
|
3479
|
+
}
|
|
3480
|
+
};
|
|
3481
|
+
const index = Number(segment);
|
|
3482
|
+
if (!Number.isSafeInteger(index)) return {
|
|
3483
|
+
ok: false,
|
|
3484
|
+
error: {
|
|
3485
|
+
code: 409,
|
|
3486
|
+
reason: "OUT_OF_BOUNDS",
|
|
3487
|
+
message: `Index out of bounds at '${segment}'`
|
|
3488
|
+
}
|
|
3489
|
+
};
|
|
3490
|
+
const elemId = rgaIdAtIndex(current, index);
|
|
3491
|
+
if (elemId === void 0) return {
|
|
3492
|
+
ok: false,
|
|
3493
|
+
error: {
|
|
3494
|
+
code: 409,
|
|
3495
|
+
reason: "OUT_OF_BOUNDS",
|
|
3496
|
+
message: `Index out of bounds at '${segment}'`
|
|
3497
|
+
}
|
|
3498
|
+
};
|
|
3499
|
+
current = current.elems.get(elemId).value;
|
|
3500
|
+
continue;
|
|
3501
|
+
}
|
|
3502
|
+
return {
|
|
3503
|
+
ok: false,
|
|
3504
|
+
error: {
|
|
3505
|
+
code: 409,
|
|
3506
|
+
reason: "INVALID_TARGET",
|
|
3507
|
+
message: `Cannot traverse into non-container at '${segment}'`
|
|
3508
|
+
}
|
|
3509
|
+
};
|
|
3510
|
+
}
|
|
3511
|
+
return {
|
|
3512
|
+
ok: true,
|
|
3513
|
+
node: current
|
|
3514
|
+
};
|
|
3515
|
+
}
|
|
3516
|
+
function parseArrayIndexTokenForDoc$1(token, op, path, opIndex) {
|
|
3517
|
+
if (token === "-") {
|
|
3518
|
+
if (op !== "add") return {
|
|
3519
|
+
ok: false,
|
|
3520
|
+
code: 409,
|
|
3521
|
+
reason: "INVALID_POINTER",
|
|
3522
|
+
message: `'-' index is only valid for add at ${path}`,
|
|
3523
|
+
path,
|
|
3524
|
+
opIndex
|
|
3525
|
+
};
|
|
3526
|
+
return {
|
|
3527
|
+
ok: true,
|
|
3528
|
+
index: Number.POSITIVE_INFINITY
|
|
3529
|
+
};
|
|
3530
|
+
}
|
|
3531
|
+
if (!ARRAY_INDEX_TOKEN_PATTERN.test(token)) return {
|
|
3532
|
+
ok: false,
|
|
3533
|
+
code: 409,
|
|
3534
|
+
reason: "INVALID_POINTER",
|
|
3535
|
+
message: `expected array index at ${path}`,
|
|
3536
|
+
path,
|
|
3537
|
+
opIndex
|
|
3538
|
+
};
|
|
3539
|
+
const index = Number(token);
|
|
3540
|
+
if (!Number.isSafeInteger(index)) return {
|
|
3541
|
+
ok: false,
|
|
3542
|
+
code: 409,
|
|
3543
|
+
reason: "OUT_OF_BOUNDS",
|
|
3544
|
+
message: `array index is too large at ${path}`,
|
|
3545
|
+
path,
|
|
3546
|
+
opIndex
|
|
3547
|
+
};
|
|
3548
|
+
return {
|
|
3549
|
+
ok: true,
|
|
3550
|
+
index
|
|
3551
|
+
};
|
|
3552
|
+
}
|
|
3553
|
+
function validateArrayIndexBounds$1(index, op, arrLength, path, opIndex) {
|
|
3554
|
+
if (op === "add") {
|
|
3555
|
+
if (index === Number.POSITIVE_INFINITY) return {
|
|
3556
|
+
ok: true,
|
|
3557
|
+
index
|
|
3558
|
+
};
|
|
3559
|
+
if (index > arrLength) return {
|
|
3560
|
+
ok: false,
|
|
3561
|
+
code: 409,
|
|
3562
|
+
reason: "OUT_OF_BOUNDS",
|
|
3563
|
+
message: `index out of bounds at ${path}; expected 0..${arrLength}`,
|
|
3564
|
+
path,
|
|
3565
|
+
opIndex
|
|
3566
|
+
};
|
|
3567
|
+
} else if (index >= arrLength) return {
|
|
3568
|
+
ok: false,
|
|
3569
|
+
code: 409,
|
|
3570
|
+
reason: "OUT_OF_BOUNDS",
|
|
3571
|
+
message: `index out of bounds at ${path}; expected 0..${Math.max(arrLength - 1, 0)}`,
|
|
3572
|
+
path,
|
|
3573
|
+
opIndex
|
|
3574
|
+
};
|
|
3575
|
+
return {
|
|
3576
|
+
ok: true,
|
|
3577
|
+
index
|
|
3578
|
+
};
|
|
3579
|
+
}
|
|
3580
|
+
function withOpIndex$1(error, opIndex) {
|
|
3581
|
+
if (error.opIndex !== void 0) return error;
|
|
3582
|
+
return {
|
|
3583
|
+
...error,
|
|
3584
|
+
opIndex
|
|
3585
|
+
};
|
|
3586
|
+
}
|
|
3587
|
+
function isJsonPatchToCrdtOptions(value) {
|
|
3588
|
+
return typeof value === "object" && value !== null && "base" in value && "head" in value && "patch" in value && "newDot" in value;
|
|
3589
|
+
}
|
|
3590
|
+
function toApplyError$1(error) {
|
|
3591
|
+
if (error instanceof TraversalDepthError) return toDepthApplyError(error);
|
|
3592
|
+
if (error instanceof PatchCompileError) return {
|
|
3593
|
+
ok: false,
|
|
3594
|
+
code: 409,
|
|
3595
|
+
reason: error.reason,
|
|
3596
|
+
message: error.message,
|
|
3597
|
+
path: error.path,
|
|
3598
|
+
opIndex: error.opIndex
|
|
3599
|
+
};
|
|
3600
|
+
return {
|
|
3601
|
+
ok: false,
|
|
3602
|
+
code: 409,
|
|
3603
|
+
reason: "INVALID_PATCH",
|
|
3604
|
+
message: error instanceof Error ? error.message : "failed to compile/apply patch"
|
|
3605
|
+
};
|
|
3606
|
+
}
|
|
3607
|
+
function assertNever(_value, message) {
|
|
3608
|
+
throw new Error(message);
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
//#endregion
|
|
3612
|
+
//#region src/state.ts
|
|
3613
|
+
/** Error thrown when a JSON Patch cannot be applied. Includes structured conflict metadata. */
|
|
3614
|
+
var PatchError = class extends Error {
|
|
3615
|
+
code;
|
|
3616
|
+
reason;
|
|
3617
|
+
budget;
|
|
3618
|
+
limit;
|
|
3619
|
+
actual;
|
|
3620
|
+
path;
|
|
3621
|
+
opIndex;
|
|
3622
|
+
constructor(errorOrMessage, code = 409, reason = "INVALID_PATCH") {
|
|
3623
|
+
super(typeof errorOrMessage === "string" ? errorOrMessage : errorOrMessage.message);
|
|
3624
|
+
this.name = "PatchError";
|
|
3625
|
+
if (typeof errorOrMessage === "string") {
|
|
3626
|
+
this.code = code;
|
|
3627
|
+
this.reason = reason;
|
|
3628
|
+
return;
|
|
3629
|
+
}
|
|
3630
|
+
this.code = errorOrMessage.code;
|
|
3018
3631
|
this.reason = errorOrMessage.reason;
|
|
3632
|
+
if (errorOrMessage.reason === "RESOURCE_BUDGET_EXCEEDED") {
|
|
3633
|
+
this.budget = errorOrMessage.budget;
|
|
3634
|
+
this.limit = errorOrMessage.limit;
|
|
3635
|
+
this.actual = errorOrMessage.actual;
|
|
3636
|
+
}
|
|
3019
3637
|
this.path = errorOrMessage.path;
|
|
3020
3638
|
this.opIndex = errorOrMessage.opIndex;
|
|
3021
3639
|
}
|
|
@@ -3080,26 +3698,27 @@ function applyPatchInPlace(state, patch, options = {}) {
|
|
|
3080
3698
|
}
|
|
3081
3699
|
/** Non-throwing immutable patch application variant. */
|
|
3082
3700
|
function tryApplyPatch(state, patch, options = {}) {
|
|
3083
|
-
const nextState = {
|
|
3084
|
-
doc: cloneDoc(state.doc),
|
|
3085
|
-
clock: cloneClock(state.clock)
|
|
3086
|
-
};
|
|
3087
3701
|
try {
|
|
3702
|
+
throwIfAborted(options.signal);
|
|
3703
|
+
const nextState = {
|
|
3704
|
+
doc: cloneDoc(state.doc),
|
|
3705
|
+
clock: cloneClock(state.clock)
|
|
3706
|
+
};
|
|
3088
3707
|
const result = applyPatchInternal(nextState, patch, options, "batch");
|
|
3089
3708
|
if (!result.ok) return {
|
|
3090
3709
|
ok: false,
|
|
3091
3710
|
error: result
|
|
3092
3711
|
};
|
|
3712
|
+
return {
|
|
3713
|
+
ok: true,
|
|
3714
|
+
state: nextState
|
|
3715
|
+
};
|
|
3093
3716
|
} catch (error) {
|
|
3094
3717
|
return {
|
|
3095
3718
|
ok: false,
|
|
3096
3719
|
error: toApplyError(error)
|
|
3097
3720
|
};
|
|
3098
3721
|
}
|
|
3099
|
-
return {
|
|
3100
|
-
ok: true,
|
|
3101
|
-
state: nextState
|
|
3102
|
-
};
|
|
3103
3722
|
}
|
|
3104
3723
|
/** Non-throwing in-place patch application variant. */
|
|
3105
3724
|
function tryApplyPatchInPlace(state, patch, options = {}) {
|
|
@@ -3130,10 +3749,13 @@ function tryApplyPatchInPlace(state, patch, options = {}) {
|
|
|
3130
3749
|
* Does not mutate caller-provided values.
|
|
3131
3750
|
*/
|
|
3132
3751
|
function validateJsonPatch(base, patch, options = {}) {
|
|
3133
|
-
const result =
|
|
3752
|
+
const result = tryApplyPatchInPlace(createState(base, {
|
|
3134
3753
|
actor: "__validate__",
|
|
3135
3754
|
jsonValidation: options.jsonValidation
|
|
3136
|
-
}), patch,
|
|
3755
|
+
}), patch, {
|
|
3756
|
+
...options,
|
|
3757
|
+
atomic: false
|
|
3758
|
+
});
|
|
3137
3759
|
if (!result.ok) return {
|
|
3138
3760
|
ok: false,
|
|
3139
3761
|
error: result.error
|
|
@@ -3176,28 +3798,27 @@ function toApplyPatchOptionsForActor(options) {
|
|
|
3176
3798
|
testAgainst: options.testAgainst,
|
|
3177
3799
|
strictParents: options.strictParents,
|
|
3178
3800
|
jsonValidation: options.jsonValidation,
|
|
3801
|
+
signal: options.signal,
|
|
3179
3802
|
base: options.base ? {
|
|
3180
3803
|
doc: options.base,
|
|
3181
3804
|
clock: createClock("__base__", 0)
|
|
3182
3805
|
} : void 0
|
|
3183
3806
|
};
|
|
3184
3807
|
}
|
|
3185
|
-
function applyPatchInternal(state, patch, options,
|
|
3808
|
+
function applyPatchInternal(state, patch, options, _execution) {
|
|
3809
|
+
throwIfAborted(options.signal);
|
|
3186
3810
|
const preparedPatch = preparePatchPayloadsSafe(patch, options.jsonValidation ?? "none");
|
|
3187
3811
|
if (!preparedPatch.ok) return preparedPatch;
|
|
3812
|
+
createBudgetMeter(options.resourceBudget)?.count("patchOperations", preparedPatch.patch.length);
|
|
3188
3813
|
const runtimePatch = preparedPatch.patch;
|
|
3189
3814
|
if ((options.semantics ?? "sequential") === "sequential") {
|
|
3190
|
-
if (!options.base && execution === "batch") {
|
|
3191
|
-
const compiled = compilePreparedIntents(materialize(state.doc.root), runtimePatch, "sequential");
|
|
3192
|
-
if (!compiled.ok) return compiled;
|
|
3193
|
-
return applyIntentsToCrdt(state.doc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
|
|
3194
|
-
}
|
|
3195
3815
|
const explicitBaseState = options.base ? {
|
|
3196
3816
|
doc: cloneDoc(options.base.doc),
|
|
3197
3817
|
clock: createClock("__base__", 0)
|
|
3198
3818
|
} : null;
|
|
3199
3819
|
const session = { pointerCache: /* @__PURE__ */ new Map() };
|
|
3200
3820
|
for (const [opIndex, op] of runtimePatch.entries()) {
|
|
3821
|
+
throwIfAborted(options.signal);
|
|
3201
3822
|
const step = applyPatchOpSequential(state, op, options, explicitBaseState ? explicitBaseState.doc : state.doc, explicitBaseState, opIndex, session);
|
|
3202
3823
|
if (!step.ok) return step;
|
|
3203
3824
|
}
|
|
@@ -3245,10 +3866,10 @@ function applySinglePatchOpSequentialStep(state, baseDoc, op, options, explicitB
|
|
|
3245
3866
|
const compiled = compilePreparedSingleIntentFromDoc(baseDoc, op, session.pointerCache, opIndex);
|
|
3246
3867
|
if (!compiled.ok) return compiled;
|
|
3247
3868
|
const headStep = applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
|
|
3248
|
-
if (!headStep.ok) return headStep;
|
|
3869
|
+
if (!headStep.ok) return withOpIndex(headStep, opIndex);
|
|
3249
3870
|
if (explicitBaseState && op.op !== "test") {
|
|
3250
3871
|
const shadowStep = applyIntentsToCrdt(explicitBaseState.doc, explicitBaseState.doc, compiled.intents, () => explicitBaseState.clock.next(), "base", (ctr) => bumpClockCounter(explicitBaseState, ctr), { strictParents: options.strictParents });
|
|
3251
|
-
if (!shadowStep.ok) return shadowStep;
|
|
3872
|
+
if (!shadowStep.ok) return withOpIndex(shadowStep, opIndex);
|
|
3252
3873
|
}
|
|
3253
3874
|
return { ok: true };
|
|
3254
3875
|
}
|
|
@@ -3256,7 +3877,7 @@ function applySinglePatchOpExplicitShadowStep(explicitBaseState, op, options, op
|
|
|
3256
3877
|
const compiled = compilePreparedSingleIntentFromDoc(explicitBaseState.doc, op, session.pointerCache, opIndex);
|
|
3257
3878
|
if (!compiled.ok) return compiled;
|
|
3258
3879
|
const shadowStep = applyIntentsToCrdt(explicitBaseState.doc, explicitBaseState.doc, compiled.intents, () => explicitBaseState.clock.next(), "base", (ctr) => bumpClockCounter(explicitBaseState, ctr), { strictParents: options.strictParents });
|
|
3259
|
-
if (!shadowStep.ok) return shadowStep;
|
|
3880
|
+
if (!shadowStep.ok) return withOpIndex(shadowStep, opIndex);
|
|
3260
3881
|
return { ok: true };
|
|
3261
3882
|
}
|
|
3262
3883
|
function resolveValueAtPointerInDoc(doc, pointer, opIndex, pointerCache) {
|
|
@@ -3536,9 +4157,9 @@ function validateArrayIndexBounds(index, op, arrLength, path, opIndex) {
|
|
|
3536
4157
|
function bumpClockCounter(state, ctr) {
|
|
3537
4158
|
if (state.clock.ctr < ctr) state.clock.ctr = ctr;
|
|
3538
4159
|
}
|
|
3539
|
-
function compilePreparedIntents(baseJson, patch, semantics = "sequential", pointerCache, opIndexOffset = 0) {
|
|
4160
|
+
function compilePreparedIntents(baseJson, patch, semantics = "sequential", pointerCache, opIndexOffset = 0, budgetMeter) {
|
|
3540
4161
|
try {
|
|
3541
|
-
const compileOptions = toCompilePatchOptions(semantics, pointerCache, opIndexOffset);
|
|
4162
|
+
const compileOptions = toCompilePatchOptions(semantics, pointerCache, opIndexOffset, budgetMeter);
|
|
3542
4163
|
if (patch.length === 1) return {
|
|
3543
4164
|
ok: true,
|
|
3544
4165
|
intents: compileJsonPatchOpToIntent(baseJson, patch[0], compileOptions)
|
|
@@ -3551,11 +4172,13 @@ function compilePreparedIntents(baseJson, patch, semantics = "sequential", point
|
|
|
3551
4172
|
return toApplyError(error);
|
|
3552
4173
|
}
|
|
3553
4174
|
}
|
|
3554
|
-
function toCompilePatchOptions(semantics, pointerCache, opIndexOffset = 0) {
|
|
4175
|
+
function toCompilePatchOptions(semantics, pointerCache, opIndexOffset = 0, budgetMeter) {
|
|
3555
4176
|
return {
|
|
3556
4177
|
semantics,
|
|
4178
|
+
resourceBudget: void 0,
|
|
3557
4179
|
pointerCache,
|
|
3558
|
-
opIndexOffset
|
|
4180
|
+
opIndexOffset,
|
|
4181
|
+
budgetMeter
|
|
3559
4182
|
};
|
|
3560
4183
|
}
|
|
3561
4184
|
function preparePatchPayloadsSafe(patch, mode) {
|
|
@@ -3603,6 +4226,8 @@ function mergePointerPaths(basePointer, nestedPointer) {
|
|
|
3603
4226
|
return `${basePointer}${nestedPointer}`;
|
|
3604
4227
|
}
|
|
3605
4228
|
function toApplyError(error) {
|
|
4229
|
+
if (error instanceof OperationCancelledError) return toCancellationApplyError(error);
|
|
4230
|
+
if (error instanceof ResourceBudgetError) return toBudgetApplyError(error);
|
|
3606
4231
|
if (error instanceof TraversalDepthError) return toDepthApplyError(error);
|
|
3607
4232
|
if (error instanceof PatchCompileError) return {
|
|
3608
4233
|
ok: false,
|
|
@@ -3619,6 +4244,13 @@ function toApplyError(error) {
|
|
|
3619
4244
|
message: error instanceof Error ? error.message : "failed to compile patch"
|
|
3620
4245
|
};
|
|
3621
4246
|
}
|
|
4247
|
+
function withOpIndex(error, opIndex) {
|
|
4248
|
+
if (error.opIndex !== void 0) return error;
|
|
4249
|
+
return {
|
|
4250
|
+
...error,
|
|
4251
|
+
opIndex
|
|
4252
|
+
};
|
|
4253
|
+
}
|
|
3622
4254
|
function toPointerParseApplyError(error, pointer, opIndex) {
|
|
3623
4255
|
return {
|
|
3624
4256
|
ok: false,
|
|
@@ -3630,6 +4262,68 @@ function toPointerParseApplyError(error, pointer, opIndex) {
|
|
|
3630
4262
|
};
|
|
3631
4263
|
}
|
|
3632
4264
|
|
|
4265
|
+
//#endregion
|
|
4266
|
+
//#region src/safe.ts
|
|
4267
|
+
/** Create a state with strict runtime JSON validation enabled by default. */
|
|
4268
|
+
function createSafeState(initial, options) {
|
|
4269
|
+
return createState(initial, {
|
|
4270
|
+
...options,
|
|
4271
|
+
jsonValidation: "strict"
|
|
4272
|
+
});
|
|
4273
|
+
}
|
|
4274
|
+
/** Apply a patch with strict runtime JSON validation enabled by default. */
|
|
4275
|
+
function applySafePatch(state, patch, options = {}) {
|
|
4276
|
+
return applyPatch(state, patch, {
|
|
4277
|
+
...options,
|
|
4278
|
+
jsonValidation: "strict"
|
|
4279
|
+
});
|
|
4280
|
+
}
|
|
4281
|
+
/** Diff JSON values with strict runtime JSON validation enabled by default. */
|
|
4282
|
+
function diffSafeJsonPatch(base, next, options = {}) {
|
|
4283
|
+
return diffJsonPatch(base, next, {
|
|
4284
|
+
...options,
|
|
4285
|
+
jsonValidation: "strict"
|
|
4286
|
+
});
|
|
4287
|
+
}
|
|
4288
|
+
/** Create a state with normalizing runtime JSON validation enabled by default. */
|
|
4289
|
+
function createNormalizedState(initial, options) {
|
|
4290
|
+
return createState(initial, {
|
|
4291
|
+
...options,
|
|
4292
|
+
jsonValidation: "normalize"
|
|
4293
|
+
});
|
|
4294
|
+
}
|
|
4295
|
+
/** Apply a patch with normalizing runtime JSON validation enabled by default. */
|
|
4296
|
+
function applyNormalizedPatch(state, patch, options = {}) {
|
|
4297
|
+
return applyPatch(state, patch, {
|
|
4298
|
+
...options,
|
|
4299
|
+
jsonValidation: "normalize"
|
|
4300
|
+
});
|
|
4301
|
+
}
|
|
4302
|
+
/** Diff JSON values with normalizing runtime JSON validation enabled by default. */
|
|
4303
|
+
function diffNormalizedJsonPatch(base, next, options = {}) {
|
|
4304
|
+
return diffJsonPatch(base, next, {
|
|
4305
|
+
...options,
|
|
4306
|
+
jsonValidation: "normalize"
|
|
4307
|
+
});
|
|
4308
|
+
}
|
|
4309
|
+
|
|
4310
|
+
//#endregion
|
|
4311
|
+
//#region src/options.ts
|
|
4312
|
+
/** Options that reject legacy missing-parent array auto-creation. */
|
|
4313
|
+
const strictRfc6902PatchOptions = { strictParents: true };
|
|
4314
|
+
function withStrictRfc6902Parents(options) {
|
|
4315
|
+
return {
|
|
4316
|
+
...options,
|
|
4317
|
+
strictParents: true
|
|
4318
|
+
};
|
|
4319
|
+
}
|
|
4320
|
+
function withLegacyMissingArrayParents(options) {
|
|
4321
|
+
return {
|
|
4322
|
+
...options,
|
|
4323
|
+
strictParents: false
|
|
4324
|
+
};
|
|
4325
|
+
}
|
|
4326
|
+
|
|
3633
4327
|
//#endregion
|
|
3634
4328
|
//#region src/serialize.ts
|
|
3635
4329
|
const HEAD_ELEM_ID = "HEAD";
|
|
@@ -3665,17 +4359,24 @@ function serializeDoc(doc) {
|
|
|
3665
4359
|
};
|
|
3666
4360
|
}
|
|
3667
4361
|
/** Reconstruct a CRDT document from its serialized form. */
|
|
3668
|
-
function deserializeDoc(data) {
|
|
4362
|
+
function deserializeDoc(data, options = {}) {
|
|
4363
|
+
return deserializeDocInternal(data, createBudgetMeter(options.resourceBudget), options.signal);
|
|
4364
|
+
}
|
|
4365
|
+
function deserializeDocInternal(data, budgetMeter, signal) {
|
|
4366
|
+
throwIfAborted(signal);
|
|
3669
4367
|
const raw = readSerializedDocEnvelope(data);
|
|
3670
4368
|
if (!("root" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/root", "serialized doc is missing root");
|
|
3671
|
-
|
|
4369
|
+
const observed = Object.create(null);
|
|
4370
|
+
const doc = { root: deserializeNode(raw.root, "/root", 0, budgetMeter, observed, signal) };
|
|
4371
|
+
writeCachedObservedVersionVector(doc, observed);
|
|
4372
|
+
return doc;
|
|
3672
4373
|
}
|
|
3673
4374
|
/** Non-throwing `deserializeDoc` variant with typed validation details. */
|
|
3674
|
-
function tryDeserializeDoc(data) {
|
|
4375
|
+
function tryDeserializeDoc(data, options = {}) {
|
|
3675
4376
|
try {
|
|
3676
4377
|
return {
|
|
3677
4378
|
ok: true,
|
|
3678
|
-
doc: deserializeDoc(data)
|
|
4379
|
+
doc: deserializeDoc(data, options)
|
|
3679
4380
|
};
|
|
3680
4381
|
} catch (error) {
|
|
3681
4382
|
const deserializeError = toDeserializeFailure(error);
|
|
@@ -3686,6 +4387,20 @@ function tryDeserializeDoc(data) {
|
|
|
3686
4387
|
throw error;
|
|
3687
4388
|
}
|
|
3688
4389
|
}
|
|
4390
|
+
/** Validate a serialized CRDT document without throwing or returning runtime state. */
|
|
4391
|
+
function validateSerializedDoc(data, options = {}) {
|
|
4392
|
+
try {
|
|
4393
|
+
validateSerializedDocInternal(data, createBudgetMeter(options.resourceBudget), options.signal);
|
|
4394
|
+
return { ok: true };
|
|
4395
|
+
} catch (error) {
|
|
4396
|
+
const deserializeError = toDeserializeFailure(error);
|
|
4397
|
+
if (deserializeError) return {
|
|
4398
|
+
ok: false,
|
|
4399
|
+
error: deserializeError
|
|
4400
|
+
};
|
|
4401
|
+
throw error;
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
3689
4404
|
/** Serialize a full CRDT state (document + clock) to a JSON-safe representation. */
|
|
3690
4405
|
function serializeState(state) {
|
|
3691
4406
|
return {
|
|
@@ -3703,27 +4418,56 @@ function serializeState(state) {
|
|
|
3703
4418
|
* May throw `TraversalDepthError` when the payload exceeds the maximum
|
|
3704
4419
|
* supported nesting depth.
|
|
3705
4420
|
*/
|
|
3706
|
-
function deserializeState(data) {
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
doc,
|
|
3717
|
-
|
|
3718
|
-
|
|
4421
|
+
function deserializeState(data, options = {}) {
|
|
4422
|
+
try {
|
|
4423
|
+
const raw = readSerializedStateEnvelope(data);
|
|
4424
|
+
const budgetMeter = createBudgetMeter(options.resourceBudget);
|
|
4425
|
+
throwIfAborted(options.signal);
|
|
4426
|
+
if (!("doc" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/doc", "serialized state is missing doc");
|
|
4427
|
+
if (!("clock" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/clock", "serialized state is missing clock");
|
|
4428
|
+
const clockRaw = asRecord(raw.clock, "/clock");
|
|
4429
|
+
const actor = readActor(clockRaw.actor, "/clock/actor");
|
|
4430
|
+
const ctr = readCounter(clockRaw.ctr, "/clock/ctr");
|
|
4431
|
+
const doc = deserializeDocInternal(raw.doc, budgetMeter, options.signal);
|
|
4432
|
+
const observedCtr = readCachedObservedVersionVector(doc)?.[actor] ?? 0;
|
|
4433
|
+
return {
|
|
4434
|
+
doc,
|
|
4435
|
+
clock: createClock(actor, Math.max(ctr, observedCtr))
|
|
4436
|
+
};
|
|
4437
|
+
} catch (error) {
|
|
4438
|
+
if (error instanceof OperationCancelledError) throw new DeserializeError("OPERATION_CANCELLED", "/", error.message);
|
|
4439
|
+
throw error;
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
/** Non-throwing `deserializeState` variant with typed validation details. */
|
|
4443
|
+
function tryDeserializeState(data, options = {}) {
|
|
4444
|
+
try {
|
|
4445
|
+
return {
|
|
4446
|
+
ok: true,
|
|
4447
|
+
state: deserializeState(data, options)
|
|
4448
|
+
};
|
|
4449
|
+
} catch (error) {
|
|
4450
|
+
const deserializeError = toDeserializeFailure(error);
|
|
4451
|
+
if (deserializeError) return {
|
|
4452
|
+
ok: false,
|
|
4453
|
+
error: deserializeError
|
|
4454
|
+
};
|
|
4455
|
+
throw error;
|
|
4456
|
+
}
|
|
3719
4457
|
}
|
|
3720
|
-
/**
|
|
3721
|
-
function
|
|
4458
|
+
/** Validate a serialized CRDT state without throwing or returning runtime state. */
|
|
4459
|
+
function validateSerializedState(data, options = {}) {
|
|
3722
4460
|
try {
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
4461
|
+
const raw = readSerializedStateEnvelope(data);
|
|
4462
|
+
const budgetMeter = createBudgetMeter(options.resourceBudget);
|
|
4463
|
+
throwIfAborted(options.signal);
|
|
4464
|
+
if (!("doc" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/doc", "serialized state is missing doc");
|
|
4465
|
+
if (!("clock" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/clock", "serialized state is missing clock");
|
|
4466
|
+
const clockRaw = asRecord(raw.clock, "/clock");
|
|
4467
|
+
readActor(clockRaw.actor, "/clock/actor");
|
|
4468
|
+
readCounter(clockRaw.ctr, "/clock/ctr");
|
|
4469
|
+
validateSerializedDocInternal(raw.doc, budgetMeter, options.signal);
|
|
4470
|
+
return { ok: true };
|
|
3727
4471
|
} catch (error) {
|
|
3728
4472
|
const deserializeError = toDeserializeFailure(error);
|
|
3729
4473
|
if (deserializeError) return {
|
|
@@ -3795,33 +4539,55 @@ function readSerializedStateEnvelope(data) {
|
|
|
3795
4539
|
assertSerializedEnvelopeVersion(raw, "/version", SERIALIZED_STATE_VERSION, "state");
|
|
3796
4540
|
return raw;
|
|
3797
4541
|
}
|
|
3798
|
-
function
|
|
4542
|
+
function validateSerializedDocInternal(data, budgetMeter, signal) {
|
|
4543
|
+
throwIfAborted(signal);
|
|
4544
|
+
const raw = readSerializedDocEnvelope(data);
|
|
4545
|
+
if (!("root" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/root", "serialized doc is missing root");
|
|
4546
|
+
validateSerializedNode(raw.root, "/root", 0, budgetMeter, signal);
|
|
4547
|
+
}
|
|
4548
|
+
function deserializeNode(node, path, depth, budgetMeter, observed, signal) {
|
|
4549
|
+
throwIfAborted(signal);
|
|
3799
4550
|
assertTraversalDepth(depth);
|
|
4551
|
+
budgetMeter?.count("visitedNodes", 1, path);
|
|
3800
4552
|
const raw = asRecord(node, path);
|
|
3801
4553
|
const kind = readString(raw.kind, `${path}/kind`);
|
|
3802
4554
|
if (kind === "lww") {
|
|
3803
4555
|
if (!("value" in raw)) fail("INVALID_SERIALIZED_SHAPE", `${path}/value`, "lww node is missing value");
|
|
3804
4556
|
if (!("dot" in raw)) fail("INVALID_SERIALIZED_SHAPE", `${path}/dot`, "lww node is missing dot");
|
|
4557
|
+
const dot = readDot(raw.dot, `${path}/dot`);
|
|
4558
|
+
if (observed) observeVersionVectorDot(observed, dot);
|
|
3805
4559
|
return {
|
|
3806
4560
|
kind: "lww",
|
|
3807
|
-
value: structuredClone(readJsonValue(raw.value, `${path}/value`, depth + 1)),
|
|
3808
|
-
dot
|
|
4561
|
+
value: structuredClone(readJsonValue(raw.value, `${path}/value`, depth + 1, budgetMeter, signal)),
|
|
4562
|
+
dot
|
|
3809
4563
|
};
|
|
3810
4564
|
}
|
|
3811
4565
|
if (kind === "obj") {
|
|
3812
4566
|
const entriesRaw = asRecord(raw.entries, `${path}/entries`);
|
|
3813
4567
|
const tombstoneRaw = asRecord(raw.tombstone, `${path}/tombstone`);
|
|
4568
|
+
budgetMeter?.count("objectEntries", Object.keys(entriesRaw).length, `${path}/entries`);
|
|
4569
|
+
budgetMeter?.count("serializedElements", Object.keys(entriesRaw).length, `${path}/entries`);
|
|
4570
|
+
budgetMeter?.count("objectEntries", Object.keys(tombstoneRaw).length, `${path}/tombstone`);
|
|
4571
|
+
budgetMeter?.count("serializedElements", Object.keys(tombstoneRaw).length, `${path}/tombstone`);
|
|
3814
4572
|
const entries = /* @__PURE__ */ new Map();
|
|
3815
4573
|
for (const [k, v] of Object.entries(entriesRaw)) {
|
|
4574
|
+
throwIfAborted(signal);
|
|
3816
4575
|
const entryPath = `${path}/entries/${k}`;
|
|
3817
4576
|
const entryRaw = asRecord(v, entryPath);
|
|
4577
|
+
const dot = readDot(entryRaw.dot, `${entryPath}/dot`);
|
|
4578
|
+
if (observed) observeVersionVectorDot(observed, dot);
|
|
3818
4579
|
entries.set(k, {
|
|
3819
|
-
node: deserializeNode(entryRaw.node, `${entryPath}/node`, depth + 1),
|
|
3820
|
-
dot
|
|
4580
|
+
node: deserializeNode(entryRaw.node, `${entryPath}/node`, depth + 1, budgetMeter, observed, signal),
|
|
4581
|
+
dot
|
|
3821
4582
|
});
|
|
3822
4583
|
}
|
|
3823
4584
|
const tombstone = /* @__PURE__ */ new Map();
|
|
3824
|
-
for (const [k, d] of Object.entries(tombstoneRaw))
|
|
4585
|
+
for (const [k, d] of Object.entries(tombstoneRaw)) {
|
|
4586
|
+
throwIfAborted(signal);
|
|
4587
|
+
const dot = readDot(d, `${path}/tombstone/${k}`);
|
|
4588
|
+
if (observed) observeVersionVectorDot(observed, dot);
|
|
4589
|
+
tombstone.set(k, dot);
|
|
4590
|
+
}
|
|
3825
4591
|
return {
|
|
3826
4592
|
kind: "obj",
|
|
3827
4593
|
entries,
|
|
@@ -3830,17 +4596,24 @@ function deserializeNode(node, path, depth) {
|
|
|
3830
4596
|
}
|
|
3831
4597
|
if (kind !== "seq") fail("INVALID_SERIALIZED_SHAPE", `${path}/kind`, `unsupported node kind '${kind}'`);
|
|
3832
4598
|
const elemsRaw = asRecord(raw.elems, `${path}/elems`);
|
|
4599
|
+
budgetMeter?.count("sequenceElements", Object.keys(elemsRaw).length, `${path}/elems`);
|
|
4600
|
+
budgetMeter?.count("serializedElements", Object.keys(elemsRaw).length, `${path}/elems`);
|
|
3833
4601
|
const elems = /* @__PURE__ */ new Map();
|
|
3834
4602
|
for (const [id, rawElem] of Object.entries(elemsRaw)) {
|
|
4603
|
+
throwIfAborted(signal);
|
|
3835
4604
|
const elemPath = `${path}/elems/${id}`;
|
|
3836
4605
|
const elem = asRecord(rawElem, elemPath);
|
|
3837
4606
|
const elemId = readString(elem.id, `${elemPath}/id`);
|
|
3838
4607
|
if (elemId !== id) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/id`, `sequence element id '${elemId}' does not match key '${id}'`);
|
|
3839
4608
|
const prev = readString(elem.prev, `${elemPath}/prev`);
|
|
3840
4609
|
const tombstone = readBoolean(elem.tombstone, `${elemPath}/tombstone`);
|
|
3841
|
-
const value = deserializeNode(elem.value, `${elemPath}/value`, depth + 1);
|
|
4610
|
+
const value = deserializeNode(elem.value, `${elemPath}/value`, depth + 1, budgetMeter, observed, signal);
|
|
3842
4611
|
const insDot = readDot(elem.insDot, `${elemPath}/insDot`);
|
|
3843
4612
|
const delDot = "delDot" in elem && elem.delDot !== void 0 ? readDot(elem.delDot, `${elemPath}/delDot`) : void 0;
|
|
4613
|
+
if (observed) {
|
|
4614
|
+
observeVersionVectorDot(observed, insDot);
|
|
4615
|
+
if (delDot) observeVersionVectorDot(observed, delDot);
|
|
4616
|
+
}
|
|
3844
4617
|
if (dotToElemId(insDot) !== id) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/insDot`, "sequence element id must match its insertion dot");
|
|
3845
4618
|
if (!tombstone && delDot) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/delDot`, "live sequence elements must not include delete metadata");
|
|
3846
4619
|
elems.set(id, {
|
|
@@ -3853,6 +4626,7 @@ function deserializeNode(node, path, depth) {
|
|
|
3853
4626
|
});
|
|
3854
4627
|
}
|
|
3855
4628
|
for (const elem of elems.values()) {
|
|
4629
|
+
throwIfAborted(signal);
|
|
3856
4630
|
if (elem.prev === elem.id) fail("INVALID_SERIALIZED_INVARIANT", `${path}/elems/${elem.id}/prev`, "sequence element cannot reference itself as predecessor");
|
|
3857
4631
|
if (elem.prev !== HEAD_ELEM_ID && !elems.has(elem.prev)) fail("INVALID_SERIALIZED_INVARIANT", `${path}/elems/${elem.id}/prev`, `sequence predecessor '${elem.prev}' does not exist`);
|
|
3858
4632
|
}
|
|
@@ -3862,6 +4636,66 @@ function deserializeNode(node, path, depth) {
|
|
|
3862
4636
|
elems
|
|
3863
4637
|
};
|
|
3864
4638
|
}
|
|
4639
|
+
function validateSerializedNode(node, path, depth, budgetMeter, signal) {
|
|
4640
|
+
throwIfAborted(signal);
|
|
4641
|
+
assertTraversalDepth(depth);
|
|
4642
|
+
budgetMeter?.count("visitedNodes", 1, path);
|
|
4643
|
+
const raw = asRecord(node, path);
|
|
4644
|
+
const kind = readString(raw.kind, `${path}/kind`);
|
|
4645
|
+
if (kind === "lww") {
|
|
4646
|
+
if (!("value" in raw)) fail("INVALID_SERIALIZED_SHAPE", `${path}/value`, "lww node is missing value");
|
|
4647
|
+
if (!("dot" in raw)) fail("INVALID_SERIALIZED_SHAPE", `${path}/dot`, "lww node is missing dot");
|
|
4648
|
+
readDot(raw.dot, `${path}/dot`);
|
|
4649
|
+
readJsonValue(raw.value, `${path}/value`, depth + 1, budgetMeter, signal);
|
|
4650
|
+
return;
|
|
4651
|
+
}
|
|
4652
|
+
if (kind === "obj") {
|
|
4653
|
+
const entriesRaw = asRecord(raw.entries, `${path}/entries`);
|
|
4654
|
+
const tombstoneRaw = asRecord(raw.tombstone, `${path}/tombstone`);
|
|
4655
|
+
budgetMeter?.count("objectEntries", Object.keys(entriesRaw).length, `${path}/entries`);
|
|
4656
|
+
budgetMeter?.count("serializedElements", Object.keys(entriesRaw).length, `${path}/entries`);
|
|
4657
|
+
budgetMeter?.count("objectEntries", Object.keys(tombstoneRaw).length, `${path}/tombstone`);
|
|
4658
|
+
budgetMeter?.count("serializedElements", Object.keys(tombstoneRaw).length, `${path}/tombstone`);
|
|
4659
|
+
for (const [k, v] of Object.entries(entriesRaw)) {
|
|
4660
|
+
throwIfAborted(signal);
|
|
4661
|
+
const entryPath = `${path}/entries/${k}`;
|
|
4662
|
+
const entryRaw = asRecord(v, entryPath);
|
|
4663
|
+
readDot(entryRaw.dot, `${entryPath}/dot`);
|
|
4664
|
+
validateSerializedNode(entryRaw.node, `${entryPath}/node`, depth + 1, budgetMeter, signal);
|
|
4665
|
+
}
|
|
4666
|
+
for (const [k, d] of Object.entries(tombstoneRaw)) {
|
|
4667
|
+
throwIfAborted(signal);
|
|
4668
|
+
readDot(d, `${path}/tombstone/${k}`);
|
|
4669
|
+
}
|
|
4670
|
+
return;
|
|
4671
|
+
}
|
|
4672
|
+
if (kind !== "seq") fail("INVALID_SERIALIZED_SHAPE", `${path}/kind`, `unsupported node kind '${kind}'`);
|
|
4673
|
+
const elemsRaw = asRecord(raw.elems, `${path}/elems`);
|
|
4674
|
+
budgetMeter?.count("sequenceElements", Object.keys(elemsRaw).length, `${path}/elems`);
|
|
4675
|
+
budgetMeter?.count("serializedElements", Object.keys(elemsRaw).length, `${path}/elems`);
|
|
4676
|
+
const predecessors = /* @__PURE__ */ new Map();
|
|
4677
|
+
for (const [id, rawElem] of Object.entries(elemsRaw)) {
|
|
4678
|
+
throwIfAborted(signal);
|
|
4679
|
+
const elemPath = `${path}/elems/${id}`;
|
|
4680
|
+
const elem = asRecord(rawElem, elemPath);
|
|
4681
|
+
const elemId = readString(elem.id, `${elemPath}/id`);
|
|
4682
|
+
if (elemId !== id) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/id`, `sequence element id '${elemId}' does not match key '${id}'`);
|
|
4683
|
+
const prev = readString(elem.prev, `${elemPath}/prev`);
|
|
4684
|
+
const tombstone = readBoolean(elem.tombstone, `${elemPath}/tombstone`);
|
|
4685
|
+
validateSerializedNode(elem.value, `${elemPath}/value`, depth + 1, budgetMeter, signal);
|
|
4686
|
+
const insDot = readDot(elem.insDot, `${elemPath}/insDot`);
|
|
4687
|
+
const delDot = "delDot" in elem && elem.delDot !== void 0 ? readDot(elem.delDot, `${elemPath}/delDot`) : void 0;
|
|
4688
|
+
if (dotToElemId(insDot) !== id) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/insDot`, "sequence element id must match its insertion dot");
|
|
4689
|
+
if (!tombstone && delDot) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/delDot`, "live sequence elements must not include delete metadata");
|
|
4690
|
+
predecessors.set(id, prev);
|
|
4691
|
+
}
|
|
4692
|
+
for (const [id, prev] of predecessors) {
|
|
4693
|
+
throwIfAborted(signal);
|
|
4694
|
+
if (prev === id) fail("INVALID_SERIALIZED_INVARIANT", `${path}/elems/${id}/prev`, "sequence element cannot reference itself as predecessor");
|
|
4695
|
+
if (prev !== HEAD_ELEM_ID && !predecessors.has(prev)) fail("INVALID_SERIALIZED_INVARIANT", `${path}/elems/${id}/prev`, `sequence predecessor '${prev}' does not exist`);
|
|
4696
|
+
}
|
|
4697
|
+
assertAcyclicSerializedRgaPredecessors(predecessors, path);
|
|
4698
|
+
}
|
|
3865
4699
|
function assertAcyclicRgaPredecessors(elems, path) {
|
|
3866
4700
|
const visitState = /* @__PURE__ */ new Map();
|
|
3867
4701
|
for (const startId of elems.keys()) {
|
|
@@ -3882,6 +4716,26 @@ function assertAcyclicRgaPredecessors(elems, path) {
|
|
|
3882
4716
|
for (const id of trail) visitState.set(id, 2);
|
|
3883
4717
|
}
|
|
3884
4718
|
}
|
|
4719
|
+
function assertAcyclicSerializedRgaPredecessors(predecessors, path) {
|
|
4720
|
+
const visitState = /* @__PURE__ */ new Map();
|
|
4721
|
+
for (const startId of predecessors.keys()) {
|
|
4722
|
+
if (visitState.get(startId) === 2) continue;
|
|
4723
|
+
const trail = [];
|
|
4724
|
+
const trailSet = /* @__PURE__ */ new Set();
|
|
4725
|
+
let currentId = startId;
|
|
4726
|
+
while (currentId) {
|
|
4727
|
+
if (trailSet.has(currentId)) fail("INVALID_SERIALIZED_INVARIANT", `${path}/elems/${currentId}/prev`, `sequence predecessor cycle detected at '${currentId}'`);
|
|
4728
|
+
if (visitState.get(currentId) === 2) break;
|
|
4729
|
+
trail.push(currentId);
|
|
4730
|
+
trailSet.add(currentId);
|
|
4731
|
+
visitState.set(currentId, 1);
|
|
4732
|
+
const prev = predecessors.get(currentId);
|
|
4733
|
+
if (!prev || prev === HEAD_ELEM_ID) break;
|
|
4734
|
+
currentId = prev;
|
|
4735
|
+
}
|
|
4736
|
+
for (const id of trail) visitState.set(id, 2);
|
|
4737
|
+
}
|
|
4738
|
+
}
|
|
3885
4739
|
function asRecord(value, path) {
|
|
3886
4740
|
if (!isRecord(value)) fail("INVALID_SERIALIZED_SHAPE", path, "expected object");
|
|
3887
4741
|
return value;
|
|
@@ -3919,28 +4773,43 @@ function readBoolean(value, path) {
|
|
|
3919
4773
|
if (typeof value !== "boolean") fail("INVALID_SERIALIZED_SHAPE", path, "expected boolean");
|
|
3920
4774
|
return value;
|
|
3921
4775
|
}
|
|
3922
|
-
function readJsonValue(value, path, depth) {
|
|
3923
|
-
assertJsonValue(value, path, depth);
|
|
4776
|
+
function readJsonValue(value, path, depth, budgetMeter, signal) {
|
|
4777
|
+
assertJsonValue(value, path, depth, budgetMeter, signal);
|
|
3924
4778
|
return value;
|
|
3925
4779
|
}
|
|
3926
|
-
function assertJsonValue(value, path, depth) {
|
|
4780
|
+
function assertJsonValue(value, path, depth, budgetMeter, signal) {
|
|
4781
|
+
throwIfAborted(signal);
|
|
3927
4782
|
assertTraversalDepth(depth);
|
|
4783
|
+
budgetMeter?.count("visitedNodes", 1, path);
|
|
3928
4784
|
if (value === null || typeof value === "string" || typeof value === "boolean") return;
|
|
3929
4785
|
if (typeof value === "number") {
|
|
3930
4786
|
if (!Number.isFinite(value)) fail("INVALID_SERIALIZED_SHAPE", path, "json number must be finite");
|
|
3931
4787
|
return;
|
|
3932
4788
|
}
|
|
3933
4789
|
if (Array.isArray(value)) {
|
|
3934
|
-
|
|
4790
|
+
budgetMeter?.count("sequenceElements", value.length, path);
|
|
4791
|
+
budgetMeter?.count("serializedElements", value.length, path);
|
|
4792
|
+
for (const [index, item] of value.entries()) {
|
|
4793
|
+
throwIfAborted(signal);
|
|
4794
|
+
assertJsonValue(item, `${path}/${index}`, depth + 1, budgetMeter, signal);
|
|
4795
|
+
}
|
|
3935
4796
|
return;
|
|
3936
4797
|
}
|
|
3937
4798
|
if (!isRecord(value)) fail("INVALID_SERIALIZED_SHAPE", path, "expected JSON value");
|
|
3938
|
-
|
|
4799
|
+
const entries = Object.entries(value);
|
|
4800
|
+
budgetMeter?.count("objectEntries", entries.length, path);
|
|
4801
|
+
budgetMeter?.count("serializedElements", entries.length, path);
|
|
4802
|
+
for (const [key, child] of entries) {
|
|
4803
|
+
throwIfAborted(signal);
|
|
4804
|
+
assertJsonValue(child, `${path}/${key}`, depth + 1, budgetMeter, signal);
|
|
4805
|
+
}
|
|
3939
4806
|
}
|
|
3940
4807
|
function fail(reason, path, message) {
|
|
3941
4808
|
throw new DeserializeError(reason, path, message);
|
|
3942
4809
|
}
|
|
3943
4810
|
function toDeserializeFailure(error) {
|
|
4811
|
+
if (error instanceof ResourceBudgetError) return toBudgetDeserializeFailure(error);
|
|
4812
|
+
if (error instanceof OperationCancelledError) return toCancellationDeserializeFailure(error);
|
|
3944
4813
|
if (error instanceof DeserializeError || error instanceof TraversalDepthError) return error;
|
|
3945
4814
|
return null;
|
|
3946
4815
|
}
|
|
@@ -3962,12 +4831,20 @@ var SharedElementMetadataMismatchError = class extends Error {
|
|
|
3962
4831
|
var MergeError = class extends Error {
|
|
3963
4832
|
code;
|
|
3964
4833
|
reason;
|
|
4834
|
+
budget;
|
|
4835
|
+
limit;
|
|
4836
|
+
actual;
|
|
3965
4837
|
path;
|
|
3966
4838
|
constructor(error) {
|
|
3967
4839
|
super(error.message);
|
|
3968
4840
|
this.name = "MergeError";
|
|
3969
4841
|
this.code = error.code;
|
|
3970
4842
|
this.reason = error.reason;
|
|
4843
|
+
if (error.reason === "RESOURCE_BUDGET_EXCEEDED") {
|
|
4844
|
+
this.budget = error.budget;
|
|
4845
|
+
this.limit = error.limit;
|
|
4846
|
+
this.actual = error.actual;
|
|
4847
|
+
}
|
|
3971
4848
|
this.path = error.path;
|
|
3972
4849
|
}
|
|
3973
4850
|
};
|
|
@@ -3992,20 +4869,27 @@ function mergeDoc(a, b, options = {}) {
|
|
|
3992
4869
|
/** Non-throwing `mergeDoc` variant with structured conflict details. */
|
|
3993
4870
|
function tryMergeDoc(a, b, options = {}) {
|
|
3994
4871
|
try {
|
|
3995
|
-
const
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
ok: false,
|
|
4000
|
-
code: 409,
|
|
4001
|
-
reason: "LINEAGE_MISMATCH",
|
|
4002
|
-
message: `merge requires shared array origin at ${mismatchPath}`,
|
|
4003
|
-
path: mismatchPath
|
|
4004
|
-
}
|
|
4872
|
+
const config = {
|
|
4873
|
+
unrelatedArrays: resolveUnrelatedArraysStrategy(options),
|
|
4874
|
+
budgetMeter: createBudgetMeter(options.resourceBudget),
|
|
4875
|
+
signal: options.signal
|
|
4005
4876
|
};
|
|
4877
|
+
if (config.unrelatedArrays === "reject") {
|
|
4878
|
+
const mismatchPath = findSeqLineageMismatch(a.root, b.root, [], config);
|
|
4879
|
+
if (mismatchPath !== null) return {
|
|
4880
|
+
ok: false,
|
|
4881
|
+
error: {
|
|
4882
|
+
ok: false,
|
|
4883
|
+
code: 409,
|
|
4884
|
+
reason: "LINEAGE_MISMATCH",
|
|
4885
|
+
message: `merge requires shared array origin at ${mismatchPath}`,
|
|
4886
|
+
path: mismatchPath
|
|
4887
|
+
}
|
|
4888
|
+
};
|
|
4889
|
+
}
|
|
4006
4890
|
return {
|
|
4007
4891
|
ok: true,
|
|
4008
|
-
doc:
|
|
4892
|
+
doc: mergeDocRoot(a.root, b.root, config).doc
|
|
4009
4893
|
};
|
|
4010
4894
|
} catch (error) {
|
|
4011
4895
|
if (error instanceof SharedElementMetadataMismatchError) return {
|
|
@@ -4022,6 +4906,14 @@ function tryMergeDoc(a, b, options = {}) {
|
|
|
4022
4906
|
ok: false,
|
|
4023
4907
|
error: toDepthApplyError(error)
|
|
4024
4908
|
};
|
|
4909
|
+
if (error instanceof ResourceBudgetError) return {
|
|
4910
|
+
ok: false,
|
|
4911
|
+
error: toBudgetApplyError(error)
|
|
4912
|
+
};
|
|
4913
|
+
if (error instanceof OperationCancelledError) return {
|
|
4914
|
+
ok: false,
|
|
4915
|
+
error: toCancellationApplyError(error)
|
|
4916
|
+
};
|
|
4025
4917
|
throw error;
|
|
4026
4918
|
}
|
|
4027
4919
|
}
|
|
@@ -4031,7 +4923,7 @@ function tryMergeDoc(a, b, options = {}) {
|
|
|
4031
4923
|
* The merged clock keeps a stable actor identity:
|
|
4032
4924
|
* - defaults to the actor from the first argument (`a`)
|
|
4033
4925
|
* - can be overridden via `options.actor`
|
|
4034
|
-
* - optional `options.
|
|
4926
|
+
* - optional `options.unrelatedArrays` controls the merge strategy for non-overlapping sequences
|
|
4035
4927
|
*
|
|
4036
4928
|
* The merged counter is lifted to the highest counter already observed for
|
|
4037
4929
|
* that actor across both input clocks and the merged document dots.
|
|
@@ -4043,29 +4935,79 @@ function mergeState(a, b, options = {}) {
|
|
|
4043
4935
|
}
|
|
4044
4936
|
/** Non-throwing `mergeState` variant with structured conflict details. */
|
|
4045
4937
|
function tryMergeState(a, b, options = {}) {
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4938
|
+
try {
|
|
4939
|
+
const actor = options.actor ?? a.clock.actor;
|
|
4940
|
+
const config = {
|
|
4941
|
+
actor,
|
|
4942
|
+
unrelatedArrays: resolveUnrelatedArraysStrategy(options),
|
|
4943
|
+
budgetMeter: createBudgetMeter(options.resourceBudget),
|
|
4944
|
+
signal: options.signal
|
|
4945
|
+
};
|
|
4946
|
+
if (config.unrelatedArrays === "reject") {
|
|
4947
|
+
const mismatchPath = findSeqLineageMismatch(a.doc.root, b.doc.root, [], config);
|
|
4948
|
+
if (mismatchPath !== null) return {
|
|
4949
|
+
ok: false,
|
|
4950
|
+
error: {
|
|
4951
|
+
ok: false,
|
|
4952
|
+
code: 409,
|
|
4953
|
+
reason: "LINEAGE_MISMATCH",
|
|
4954
|
+
message: `merge requires shared array origin at ${mismatchPath}`,
|
|
4955
|
+
path: mismatchPath
|
|
4956
|
+
}
|
|
4957
|
+
};
|
|
4055
4958
|
}
|
|
4056
|
-
|
|
4959
|
+
const merged = mergeDocRoot(a.doc.root, b.doc.root, config);
|
|
4960
|
+
const ctr = maxObservedCtrForActor(merged.maxObservedCtr, actor, a, b);
|
|
4961
|
+
return {
|
|
4962
|
+
ok: true,
|
|
4963
|
+
state: {
|
|
4964
|
+
doc: merged.doc,
|
|
4965
|
+
clock: createClock(actor, ctr)
|
|
4966
|
+
}
|
|
4967
|
+
};
|
|
4968
|
+
} catch (error) {
|
|
4969
|
+
if (error instanceof SharedElementMetadataMismatchError) return {
|
|
4970
|
+
ok: false,
|
|
4971
|
+
error: {
|
|
4972
|
+
ok: false,
|
|
4973
|
+
code: 409,
|
|
4974
|
+
reason: "LINEAGE_MISMATCH",
|
|
4975
|
+
message: error.message,
|
|
4976
|
+
path: error.path
|
|
4977
|
+
}
|
|
4978
|
+
};
|
|
4979
|
+
if (error instanceof TraversalDepthError) return {
|
|
4980
|
+
ok: false,
|
|
4981
|
+
error: toDepthApplyError(error)
|
|
4982
|
+
};
|
|
4983
|
+
if (error instanceof ResourceBudgetError) return {
|
|
4984
|
+
ok: false,
|
|
4985
|
+
error: toBudgetApplyError(error)
|
|
4986
|
+
};
|
|
4987
|
+
if (error instanceof OperationCancelledError) return {
|
|
4988
|
+
ok: false,
|
|
4989
|
+
error: toCancellationApplyError(error)
|
|
4990
|
+
};
|
|
4991
|
+
throw error;
|
|
4992
|
+
}
|
|
4057
4993
|
}
|
|
4058
|
-
function findSeqLineageMismatch(a, b, path) {
|
|
4994
|
+
function findSeqLineageMismatch(a, b, path, config) {
|
|
4995
|
+
const pathBuffer = [...path];
|
|
4059
4996
|
const stack = [{
|
|
4060
4997
|
a,
|
|
4061
4998
|
b,
|
|
4062
|
-
path,
|
|
4063
4999
|
depth: path.length
|
|
4064
5000
|
}];
|
|
4065
5001
|
while (stack.length > 0) {
|
|
5002
|
+
throwIfAborted(config.signal);
|
|
4066
5003
|
const frame = stack.pop();
|
|
4067
5004
|
assertTraversalDepth(frame.depth);
|
|
5005
|
+
pathBuffer.length = frame.depth;
|
|
5006
|
+
if (frame.key !== void 0) pathBuffer[frame.depth - 1] = frame.key;
|
|
5007
|
+
const budgetPath = config.budgetMeter ? stringifyJsonPointer(pathBuffer) : void 0;
|
|
5008
|
+
config.budgetMeter?.count("visitedNodes", 1, budgetPath);
|
|
4068
5009
|
if (frame.a.kind === "seq" && frame.b.kind === "seq") {
|
|
5010
|
+
config.budgetMeter?.count("sequenceElements", frame.a.elems.size + frame.b.elems.size, budgetPath);
|
|
4069
5011
|
const hasElemsA = frame.a.elems.size > 0;
|
|
4070
5012
|
const hasElemsB = frame.b.elems.size > 0;
|
|
4071
5013
|
if (hasElemsA && hasElemsB) {
|
|
@@ -4074,157 +5016,277 @@ function findSeqLineageMismatch(a, b, path) {
|
|
|
4074
5016
|
shared = true;
|
|
4075
5017
|
break;
|
|
4076
5018
|
}
|
|
4077
|
-
if (!shared) return stringifyJsonPointer(
|
|
5019
|
+
if (!shared) return stringifyJsonPointer(pathBuffer);
|
|
4078
5020
|
}
|
|
4079
5021
|
}
|
|
4080
5022
|
if (frame.a.kind === "obj" && frame.b.kind === "obj") {
|
|
4081
5023
|
const left = frame.a;
|
|
4082
5024
|
const right = frame.b;
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
const key
|
|
5025
|
+
if (config.budgetMeter) {
|
|
5026
|
+
let sharedKeyCount = 0;
|
|
5027
|
+
for (const key of left.entries.keys()) if (right.entries.has(key)) sharedKeyCount += 1;
|
|
5028
|
+
config.budgetMeter.count("objectEntries", sharedKeyCount, budgetPath);
|
|
5029
|
+
}
|
|
5030
|
+
const sharedKeysInOrder = [];
|
|
5031
|
+
for (const key of left.entries.keys()) if (right.entries.has(key)) sharedKeysInOrder.push(key);
|
|
5032
|
+
for (let i = sharedKeysInOrder.length - 1; i >= 0; i--) {
|
|
5033
|
+
const key = sharedKeysInOrder[i];
|
|
4086
5034
|
const nextA = left.entries.get(key).node;
|
|
4087
5035
|
const nextB = right.entries.get(key).node;
|
|
4088
5036
|
stack.push({
|
|
4089
5037
|
a: nextA,
|
|
4090
5038
|
b: nextB,
|
|
4091
|
-
|
|
4092
|
-
|
|
5039
|
+
depth: frame.depth + 1,
|
|
5040
|
+
key
|
|
4093
5041
|
});
|
|
4094
5042
|
}
|
|
4095
5043
|
}
|
|
4096
5044
|
}
|
|
4097
5045
|
return null;
|
|
4098
5046
|
}
|
|
4099
|
-
function
|
|
4100
|
-
|
|
5047
|
+
function mergeDocRoot(a, b, config) {
|
|
5048
|
+
const merged = mergeNodeAtDepth(a, b, 0, [], config);
|
|
5049
|
+
return {
|
|
5050
|
+
doc: { root: merged.node },
|
|
5051
|
+
maxObservedCtr: merged.maxObservedCtr
|
|
5052
|
+
};
|
|
5053
|
+
}
|
|
5054
|
+
function resolveUnrelatedArraysStrategy(options) {
|
|
5055
|
+
if (options.unrelatedArrays !== void 0) return options.unrelatedArrays;
|
|
5056
|
+
if (options.requireSharedOrigin === false) return "unsafe-union";
|
|
5057
|
+
return "reject";
|
|
5058
|
+
}
|
|
5059
|
+
function maxObservedCtrForActor(docObservedCtr, actor, a, b) {
|
|
5060
|
+
let best = docObservedCtr;
|
|
4101
5061
|
if (a.clock.actor === actor && a.clock.ctr > best) best = a.clock.ctr;
|
|
4102
5062
|
if (b.clock.actor === actor && b.clock.ctr > best) best = b.clock.ctr;
|
|
4103
5063
|
return best;
|
|
4104
5064
|
}
|
|
4105
|
-
function repDot(node) {
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
5065
|
+
function repDot(node, signal) {
|
|
5066
|
+
let best = {
|
|
5067
|
+
actor: "",
|
|
5068
|
+
ctr: 0
|
|
5069
|
+
};
|
|
5070
|
+
const stack = [{
|
|
5071
|
+
node,
|
|
5072
|
+
depth: 0
|
|
5073
|
+
}];
|
|
5074
|
+
while (stack.length > 0) {
|
|
5075
|
+
throwIfAborted(signal);
|
|
5076
|
+
const frame = stack.pop();
|
|
5077
|
+
assertTraversalDepth(frame.depth);
|
|
5078
|
+
switch (frame.node.kind) {
|
|
5079
|
+
case "lww":
|
|
5080
|
+
if (compareDot(frame.node.dot, best) > 0) best = frame.node.dot;
|
|
5081
|
+
break;
|
|
5082
|
+
case "obj":
|
|
5083
|
+
for (const entry of frame.node.entries.values()) {
|
|
5084
|
+
if (compareDot(entry.dot, best) > 0) best = entry.dot;
|
|
5085
|
+
stack.push({
|
|
5086
|
+
node: entry.node,
|
|
5087
|
+
depth: frame.depth + 1
|
|
5088
|
+
});
|
|
5089
|
+
}
|
|
5090
|
+
for (const tombstone of frame.node.tombstone.values()) if (compareDot(tombstone, best) > 0) best = tombstone;
|
|
5091
|
+
break;
|
|
5092
|
+
case "seq":
|
|
5093
|
+
for (const elem of frame.node.elems.values()) {
|
|
5094
|
+
if (compareDot(elem.insDot, best) > 0) best = elem.insDot;
|
|
5095
|
+
if (elem.delDot && compareDot(elem.delDot, best) > 0) best = elem.delDot;
|
|
5096
|
+
stack.push({
|
|
5097
|
+
node: elem.value,
|
|
5098
|
+
depth: frame.depth + 1
|
|
5099
|
+
});
|
|
5100
|
+
}
|
|
5101
|
+
break;
|
|
4124
5102
|
}
|
|
4125
5103
|
}
|
|
5104
|
+
return best;
|
|
4126
5105
|
}
|
|
4127
|
-
function
|
|
4128
|
-
|
|
4129
|
-
}
|
|
4130
|
-
function mergeNodeAtDepth(a, b, depth, path) {
|
|
5106
|
+
function mergeNodeAtDepth(a, b, depth, path, config) {
|
|
5107
|
+
throwIfAborted(config.signal);
|
|
4131
5108
|
assertTraversalDepth(depth);
|
|
4132
|
-
|
|
4133
|
-
if (a.kind === "
|
|
4134
|
-
if (a.kind === "
|
|
4135
|
-
if (
|
|
4136
|
-
return cloneNodeShallow(
|
|
4137
|
-
|
|
4138
|
-
|
|
5109
|
+
config.budgetMeter?.count("visitedNodes", 1, stringifyJsonPointer(path));
|
|
5110
|
+
if (a.kind === "lww" && b.kind === "lww") return mergeLww(a, b, config.actor);
|
|
5111
|
+
if (a.kind === "obj" && b.kind === "obj") return mergeObj(a, b, depth + 1, path, config);
|
|
5112
|
+
if (a.kind === "seq" && b.kind === "seq") return mergeSeq(a, b, depth + 1, path, config);
|
|
5113
|
+
if (compareDot(repDot(a, config.signal), repDot(b, config.signal)) >= 0) return cloneNodeShallow(a, depth + 1, config.actor, config.signal);
|
|
5114
|
+
return cloneNodeShallow(b, depth + 1, config.actor, config.signal);
|
|
5115
|
+
}
|
|
5116
|
+
function mergeLww(a, b, actor) {
|
|
4139
5117
|
if (compareDot(a.dot, b.dot) >= 0) return {
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
5118
|
+
node: {
|
|
5119
|
+
kind: "lww",
|
|
5120
|
+
value: structuredClone(a.value),
|
|
5121
|
+
dot: { ...a.dot }
|
|
5122
|
+
},
|
|
5123
|
+
maxObservedCtr: maxObservedCtrForDot(a.dot, actor)
|
|
4143
5124
|
};
|
|
4144
5125
|
return {
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
5126
|
+
node: {
|
|
5127
|
+
kind: "lww",
|
|
5128
|
+
value: structuredClone(b.value),
|
|
5129
|
+
dot: { ...b.dot }
|
|
5130
|
+
},
|
|
5131
|
+
maxObservedCtr: maxObservedCtrForDot(b.dot, actor)
|
|
4148
5132
|
};
|
|
4149
5133
|
}
|
|
4150
|
-
function mergeObj(a, b, depth, path) {
|
|
5134
|
+
function mergeObj(a, b, depth, path, config) {
|
|
4151
5135
|
assertTraversalDepth(depth);
|
|
4152
5136
|
const entries = /* @__PURE__ */ new Map();
|
|
4153
5137
|
const tombstone = /* @__PURE__ */ new Map();
|
|
5138
|
+
let maxObservedCtr = 0;
|
|
4154
5139
|
const allTombKeys = new Set([...a.tombstone.keys(), ...b.tombstone.keys()]);
|
|
5140
|
+
config.budgetMeter?.count("objectEntries", allTombKeys.size, stringifyJsonPointer(path));
|
|
4155
5141
|
for (const key of allTombKeys) {
|
|
5142
|
+
throwIfAborted(config.signal);
|
|
4156
5143
|
const da = a.tombstone.get(key);
|
|
4157
5144
|
const db = b.tombstone.get(key);
|
|
4158
|
-
if (da && db)
|
|
4159
|
-
|
|
4160
|
-
|
|
5145
|
+
if (da && db) {
|
|
5146
|
+
const mergedDot = compareDot(da, db) >= 0 ? { ...da } : { ...db };
|
|
5147
|
+
tombstone.set(key, mergedDot);
|
|
5148
|
+
maxObservedCtr = Math.max(maxObservedCtr, maxObservedCtrForDot(mergedDot, config.actor));
|
|
5149
|
+
} else if (da) {
|
|
5150
|
+
tombstone.set(key, { ...da });
|
|
5151
|
+
maxObservedCtr = Math.max(maxObservedCtr, maxObservedCtrForDot(da, config.actor));
|
|
5152
|
+
} else {
|
|
5153
|
+
tombstone.set(key, { ...db });
|
|
5154
|
+
maxObservedCtr = Math.max(maxObservedCtr, maxObservedCtrForDot(db, config.actor));
|
|
5155
|
+
}
|
|
4161
5156
|
}
|
|
4162
5157
|
const allKeys = new Set([...a.entries.keys(), ...b.entries.keys()]);
|
|
5158
|
+
config.budgetMeter?.count("objectEntries", allKeys.size, stringifyJsonPointer(path));
|
|
4163
5159
|
for (const key of allKeys) {
|
|
5160
|
+
throwIfAborted(config.signal);
|
|
4164
5161
|
const ea = a.entries.get(key);
|
|
4165
5162
|
const eb = b.entries.get(key);
|
|
4166
5163
|
let merged;
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
dot: { ...eb.dot }
|
|
4178
|
-
|
|
5164
|
+
let mergedNodeMaxObservedCtr = 0;
|
|
5165
|
+
if (ea && eb) {
|
|
5166
|
+
path.push(key);
|
|
5167
|
+
const mergedNode = (() => {
|
|
5168
|
+
try {
|
|
5169
|
+
return mergeNodeAtDepth(ea.node, eb.node, depth + 1, path, config);
|
|
5170
|
+
} finally {
|
|
5171
|
+
path.pop();
|
|
5172
|
+
}
|
|
5173
|
+
})();
|
|
5174
|
+
const dot = compareDot(ea.dot, eb.dot) >= 0 ? { ...ea.dot } : { ...eb.dot };
|
|
5175
|
+
merged = {
|
|
5176
|
+
node: mergedNode.node,
|
|
5177
|
+
dot
|
|
5178
|
+
};
|
|
5179
|
+
mergedNodeMaxObservedCtr = mergedNode.maxObservedCtr;
|
|
5180
|
+
} else if (ea) {
|
|
5181
|
+
const cloned = cloneNodeShallow(ea.node, depth + 1, config.actor, config.signal);
|
|
5182
|
+
merged = {
|
|
5183
|
+
node: cloned.node,
|
|
5184
|
+
dot: { ...ea.dot }
|
|
5185
|
+
};
|
|
5186
|
+
mergedNodeMaxObservedCtr = cloned.maxObservedCtr;
|
|
5187
|
+
} else {
|
|
5188
|
+
const cloned = cloneNodeShallow(eb.node, depth + 1, config.actor, config.signal);
|
|
5189
|
+
merged = {
|
|
5190
|
+
node: cloned.node,
|
|
5191
|
+
dot: { ...eb.dot }
|
|
5192
|
+
};
|
|
5193
|
+
mergedNodeMaxObservedCtr = cloned.maxObservedCtr;
|
|
5194
|
+
}
|
|
4179
5195
|
const td = tombstone.get(key);
|
|
4180
5196
|
if (td && compareDot(td, merged.dot) >= 0) continue;
|
|
4181
5197
|
entries.set(key, merged);
|
|
5198
|
+
maxObservedCtr = Math.max(maxObservedCtr, mergedNodeMaxObservedCtr, maxObservedCtrForDot(merged.dot, config.actor));
|
|
4182
5199
|
}
|
|
4183
5200
|
return {
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
5201
|
+
node: {
|
|
5202
|
+
kind: "obj",
|
|
5203
|
+
entries,
|
|
5204
|
+
tombstone
|
|
5205
|
+
},
|
|
5206
|
+
maxObservedCtr
|
|
4187
5207
|
};
|
|
4188
5208
|
}
|
|
4189
|
-
function mergeSeq(a, b, depth, path) {
|
|
5209
|
+
function mergeSeq(a, b, depth, path, config) {
|
|
4190
5210
|
assertTraversalDepth(depth);
|
|
5211
|
+
config.budgetMeter?.count("visitedNodes", 1, stringifyJsonPointer(path));
|
|
5212
|
+
if (config.unrelatedArrays === "atomic-replace" && a.elems.size > 0 && b.elems.size > 0) {
|
|
5213
|
+
let shared = false;
|
|
5214
|
+
for (const id of a.elems.keys()) {
|
|
5215
|
+
throwIfAborted(config.signal);
|
|
5216
|
+
if (b.elems.has(id)) {
|
|
5217
|
+
shared = true;
|
|
5218
|
+
break;
|
|
5219
|
+
}
|
|
5220
|
+
}
|
|
5221
|
+
if (!shared) {
|
|
5222
|
+
config.budgetMeter?.count("sequenceElements", a.elems.size + b.elems.size, stringifyJsonPointer(path));
|
|
5223
|
+
return cloneNodeShallow(compareDot(repDot(a, config.signal), repDot(b, config.signal)) >= 0 ? a : b, depth, config.actor, config.signal);
|
|
5224
|
+
}
|
|
5225
|
+
}
|
|
4191
5226
|
const elems = /* @__PURE__ */ new Map();
|
|
5227
|
+
let maxObservedCtr = 0;
|
|
4192
5228
|
const allIds = new Set([...a.elems.keys(), ...b.elems.keys()]);
|
|
5229
|
+
config.budgetMeter?.count("sequenceElements", allIds.size, stringifyJsonPointer(path));
|
|
4193
5230
|
for (const id of allIds) {
|
|
5231
|
+
throwIfAborted(config.signal);
|
|
4194
5232
|
const ea = a.elems.get(id);
|
|
4195
5233
|
const eb = b.elems.get(id);
|
|
4196
5234
|
if (ea && eb) {
|
|
4197
5235
|
if (ea.prev !== eb.prev) throw new SharedElementMetadataMismatchError(stringifyJsonPointer(path), id, "prev");
|
|
4198
5236
|
if (!sameDot(ea.insDot, eb.insDot)) throw new SharedElementMetadataMismatchError(stringifyJsonPointer(path), id, "insDot");
|
|
4199
|
-
|
|
5237
|
+
path.push(id);
|
|
5238
|
+
const mergedValue = (() => {
|
|
5239
|
+
try {
|
|
5240
|
+
return mergeNodeAtDepth(ea.value, eb.value, depth + 1, path, config);
|
|
5241
|
+
} finally {
|
|
5242
|
+
path.pop();
|
|
5243
|
+
}
|
|
5244
|
+
})();
|
|
5245
|
+
const mergedDeleteDot = mergeDeleteDot(ea.delDot, eb.delDot);
|
|
4200
5246
|
elems.set(id, {
|
|
4201
5247
|
id,
|
|
4202
5248
|
prev: ea.prev,
|
|
4203
5249
|
tombstone: ea.tombstone || eb.tombstone,
|
|
4204
|
-
delDot:
|
|
4205
|
-
value: mergedValue,
|
|
5250
|
+
delDot: mergedDeleteDot,
|
|
5251
|
+
value: mergedValue.node,
|
|
4206
5252
|
insDot: { ...ea.insDot }
|
|
4207
5253
|
});
|
|
4208
|
-
|
|
4209
|
-
else
|
|
5254
|
+
maxObservedCtr = Math.max(maxObservedCtr, mergedValue.maxObservedCtr, maxObservedCtrForDot(ea.insDot, config.actor), maxObservedCtrForDot(mergedDeleteDot, config.actor));
|
|
5255
|
+
} else if (ea) {
|
|
5256
|
+
const cloned = cloneElem(ea, depth + 1, config.actor, config.signal);
|
|
5257
|
+
elems.set(id, cloned.elem);
|
|
5258
|
+
maxObservedCtr = Math.max(maxObservedCtr, cloned.maxObservedCtr);
|
|
5259
|
+
} else {
|
|
5260
|
+
const cloned = cloneElem(eb, depth + 1, config.actor, config.signal);
|
|
5261
|
+
elems.set(id, cloned.elem);
|
|
5262
|
+
maxObservedCtr = Math.max(maxObservedCtr, cloned.maxObservedCtr);
|
|
5263
|
+
}
|
|
4210
5264
|
}
|
|
4211
5265
|
return {
|
|
4212
|
-
|
|
4213
|
-
|
|
5266
|
+
node: {
|
|
5267
|
+
kind: "seq",
|
|
5268
|
+
elems
|
|
5269
|
+
},
|
|
5270
|
+
maxObservedCtr
|
|
4214
5271
|
};
|
|
4215
5272
|
}
|
|
4216
5273
|
function sameDot(a, b) {
|
|
4217
5274
|
return a.actor === b.actor && a.ctr === b.ctr;
|
|
4218
5275
|
}
|
|
4219
|
-
function cloneElem(e, depth) {
|
|
5276
|
+
function cloneElem(e, depth, actor, signal) {
|
|
5277
|
+
throwIfAborted(signal);
|
|
4220
5278
|
assertTraversalDepth(depth);
|
|
5279
|
+
const value = cloneNodeShallow(e.value, depth + 1, actor, signal);
|
|
4221
5280
|
return {
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
5281
|
+
elem: {
|
|
5282
|
+
id: e.id,
|
|
5283
|
+
prev: e.prev,
|
|
5284
|
+
tombstone: e.tombstone,
|
|
5285
|
+
delDot: e.delDot ? { ...e.delDot } : void 0,
|
|
5286
|
+
value: value.node,
|
|
5287
|
+
insDot: { ...e.insDot }
|
|
5288
|
+
},
|
|
5289
|
+
maxObservedCtr: Math.max(value.maxObservedCtr, maxObservedCtrForDot(e.insDot, actor), maxObservedCtrForDot(e.delDot, actor))
|
|
4228
5290
|
};
|
|
4229
5291
|
}
|
|
4230
5292
|
function mergeDeleteDot(a, b) {
|
|
@@ -4232,38 +5294,68 @@ function mergeDeleteDot(a, b) {
|
|
|
4232
5294
|
if (a) return { ...a };
|
|
4233
5295
|
if (b) return { ...b };
|
|
4234
5296
|
}
|
|
4235
|
-
function cloneNodeShallow(node, depth) {
|
|
5297
|
+
function cloneNodeShallow(node, depth, actor, signal) {
|
|
5298
|
+
throwIfAborted(signal);
|
|
4236
5299
|
assertTraversalDepth(depth);
|
|
4237
5300
|
switch (node.kind) {
|
|
4238
5301
|
case "lww": return {
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
5302
|
+
node: {
|
|
5303
|
+
kind: "lww",
|
|
5304
|
+
value: structuredClone(node.value),
|
|
5305
|
+
dot: { ...node.dot }
|
|
5306
|
+
},
|
|
5307
|
+
maxObservedCtr: maxObservedCtrForDot(node.dot, actor)
|
|
4242
5308
|
};
|
|
4243
5309
|
case "obj": {
|
|
4244
5310
|
const entries = /* @__PURE__ */ new Map();
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
5311
|
+
let maxObservedCtr = 0;
|
|
5312
|
+
for (const [k, v] of node.entries) {
|
|
5313
|
+
throwIfAborted(signal);
|
|
5314
|
+
const cloned = cloneNodeShallow(v.node, depth + 1, actor, signal);
|
|
5315
|
+
entries.set(k, {
|
|
5316
|
+
node: cloned.node,
|
|
5317
|
+
dot: { ...v.dot }
|
|
5318
|
+
});
|
|
5319
|
+
maxObservedCtr = Math.max(maxObservedCtr, cloned.maxObservedCtr, maxObservedCtrForDot(v.dot, actor));
|
|
5320
|
+
}
|
|
4249
5321
|
const tombstone = /* @__PURE__ */ new Map();
|
|
4250
|
-
for (const [k, d] of node.tombstone)
|
|
5322
|
+
for (const [k, d] of node.tombstone) {
|
|
5323
|
+
throwIfAborted(signal);
|
|
5324
|
+
tombstone.set(k, { ...d });
|
|
5325
|
+
maxObservedCtr = Math.max(maxObservedCtr, maxObservedCtrForDot(d, actor));
|
|
5326
|
+
}
|
|
4251
5327
|
return {
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
5328
|
+
node: {
|
|
5329
|
+
kind: "obj",
|
|
5330
|
+
entries,
|
|
5331
|
+
tombstone
|
|
5332
|
+
},
|
|
5333
|
+
maxObservedCtr
|
|
4255
5334
|
};
|
|
4256
5335
|
}
|
|
4257
5336
|
case "seq": {
|
|
4258
5337
|
const elems = /* @__PURE__ */ new Map();
|
|
4259
|
-
|
|
5338
|
+
let maxObservedCtr = 0;
|
|
5339
|
+
for (const [id, e] of node.elems) {
|
|
5340
|
+
throwIfAborted(signal);
|
|
5341
|
+
const cloned = cloneElem(e, depth + 1, actor, signal);
|
|
5342
|
+
elems.set(id, cloned.elem);
|
|
5343
|
+
maxObservedCtr = Math.max(maxObservedCtr, cloned.maxObservedCtr);
|
|
5344
|
+
}
|
|
4260
5345
|
return {
|
|
4261
|
-
|
|
4262
|
-
|
|
5346
|
+
node: {
|
|
5347
|
+
kind: "seq",
|
|
5348
|
+
elems
|
|
5349
|
+
},
|
|
5350
|
+
maxObservedCtr
|
|
4263
5351
|
};
|
|
4264
5352
|
}
|
|
4265
5353
|
}
|
|
4266
5354
|
}
|
|
5355
|
+
function maxObservedCtrForDot(dot, actor) {
|
|
5356
|
+
if (!dot || !actor || dot.actor !== actor) return 0;
|
|
5357
|
+
return dot.ctr;
|
|
5358
|
+
}
|
|
4267
5359
|
|
|
4268
5360
|
//#endregion
|
|
4269
5361
|
//#region src/compact.ts
|
|
@@ -4381,6 +5473,12 @@ Object.defineProperty(exports, 'MergeError', {
|
|
|
4381
5473
|
return MergeError;
|
|
4382
5474
|
}
|
|
4383
5475
|
});
|
|
5476
|
+
Object.defineProperty(exports, 'OperationCancelledError', {
|
|
5477
|
+
enumerable: true,
|
|
5478
|
+
get: function () {
|
|
5479
|
+
return OperationCancelledError;
|
|
5480
|
+
}
|
|
5481
|
+
});
|
|
4384
5482
|
Object.defineProperty(exports, 'PatchCompileError', {
|
|
4385
5483
|
enumerable: true,
|
|
4386
5484
|
get: function () {
|
|
@@ -4399,6 +5497,12 @@ Object.defineProperty(exports, 'ROOT_KEY', {
|
|
|
4399
5497
|
return ROOT_KEY;
|
|
4400
5498
|
}
|
|
4401
5499
|
});
|
|
5500
|
+
Object.defineProperty(exports, 'ResourceBudgetError', {
|
|
5501
|
+
enumerable: true,
|
|
5502
|
+
get: function () {
|
|
5503
|
+
return ResourceBudgetError;
|
|
5504
|
+
}
|
|
5505
|
+
});
|
|
4402
5506
|
Object.defineProperty(exports, 'TraversalDepthError', {
|
|
4403
5507
|
enumerable: true,
|
|
4404
5508
|
get: function () {
|
|
@@ -4411,6 +5515,12 @@ Object.defineProperty(exports, 'applyIntentsToCrdt', {
|
|
|
4411
5515
|
return applyIntentsToCrdt;
|
|
4412
5516
|
}
|
|
4413
5517
|
});
|
|
5518
|
+
Object.defineProperty(exports, 'applyNormalizedPatch', {
|
|
5519
|
+
enumerable: true,
|
|
5520
|
+
get: function () {
|
|
5521
|
+
return applyNormalizedPatch;
|
|
5522
|
+
}
|
|
5523
|
+
});
|
|
4414
5524
|
Object.defineProperty(exports, 'applyPatch', {
|
|
4415
5525
|
enumerable: true,
|
|
4416
5526
|
get: function () {
|
|
@@ -4429,6 +5539,12 @@ Object.defineProperty(exports, 'applyPatchInPlace', {
|
|
|
4429
5539
|
return applyPatchInPlace;
|
|
4430
5540
|
}
|
|
4431
5541
|
});
|
|
5542
|
+
Object.defineProperty(exports, 'applySafePatch', {
|
|
5543
|
+
enumerable: true,
|
|
5544
|
+
get: function () {
|
|
5545
|
+
return applySafePatch;
|
|
5546
|
+
}
|
|
5547
|
+
});
|
|
4432
5548
|
Object.defineProperty(exports, 'cloneClock', {
|
|
4433
5549
|
enumerable: true,
|
|
4434
5550
|
get: function () {
|
|
@@ -4489,6 +5605,18 @@ Object.defineProperty(exports, 'createClock', {
|
|
|
4489
5605
|
return createClock;
|
|
4490
5606
|
}
|
|
4491
5607
|
});
|
|
5608
|
+
Object.defineProperty(exports, 'createNormalizedState', {
|
|
5609
|
+
enumerable: true,
|
|
5610
|
+
get: function () {
|
|
5611
|
+
return createNormalizedState;
|
|
5612
|
+
}
|
|
5613
|
+
});
|
|
5614
|
+
Object.defineProperty(exports, 'createSafeState', {
|
|
5615
|
+
enumerable: true,
|
|
5616
|
+
get: function () {
|
|
5617
|
+
return createSafeState;
|
|
5618
|
+
}
|
|
5619
|
+
});
|
|
4492
5620
|
Object.defineProperty(exports, 'createState', {
|
|
4493
5621
|
enumerable: true,
|
|
4494
5622
|
get: function () {
|
|
@@ -4513,6 +5641,18 @@ Object.defineProperty(exports, 'diffJsonPatch', {
|
|
|
4513
5641
|
return diffJsonPatch;
|
|
4514
5642
|
}
|
|
4515
5643
|
});
|
|
5644
|
+
Object.defineProperty(exports, 'diffNormalizedJsonPatch', {
|
|
5645
|
+
enumerable: true,
|
|
5646
|
+
get: function () {
|
|
5647
|
+
return diffNormalizedJsonPatch;
|
|
5648
|
+
}
|
|
5649
|
+
});
|
|
5650
|
+
Object.defineProperty(exports, 'diffSafeJsonPatch', {
|
|
5651
|
+
enumerable: true,
|
|
5652
|
+
get: function () {
|
|
5653
|
+
return diffSafeJsonPatch;
|
|
5654
|
+
}
|
|
5655
|
+
});
|
|
4516
5656
|
Object.defineProperty(exports, 'docFromJson', {
|
|
4517
5657
|
enumerable: true,
|
|
4518
5658
|
get: function () {
|
|
@@ -4717,6 +5857,12 @@ Object.defineProperty(exports, 'stableJsonValueKey', {
|
|
|
4717
5857
|
return stableJsonValueKey;
|
|
4718
5858
|
}
|
|
4719
5859
|
});
|
|
5860
|
+
Object.defineProperty(exports, 'strictRfc6902PatchOptions', {
|
|
5861
|
+
enumerable: true,
|
|
5862
|
+
get: function () {
|
|
5863
|
+
return strictRfc6902PatchOptions;
|
|
5864
|
+
}
|
|
5865
|
+
});
|
|
4720
5866
|
Object.defineProperty(exports, 'stringifyJsonPointer', {
|
|
4721
5867
|
enumerable: true,
|
|
4722
5868
|
get: function () {
|
|
@@ -4789,6 +5935,18 @@ Object.defineProperty(exports, 'validateRgaSeq', {
|
|
|
4789
5935
|
return validateRgaSeq;
|
|
4790
5936
|
}
|
|
4791
5937
|
});
|
|
5938
|
+
Object.defineProperty(exports, 'validateSerializedDoc', {
|
|
5939
|
+
enumerable: true,
|
|
5940
|
+
get: function () {
|
|
5941
|
+
return validateSerializedDoc;
|
|
5942
|
+
}
|
|
5943
|
+
});
|
|
5944
|
+
Object.defineProperty(exports, 'validateSerializedState', {
|
|
5945
|
+
enumerable: true,
|
|
5946
|
+
get: function () {
|
|
5947
|
+
return validateSerializedState;
|
|
5948
|
+
}
|
|
5949
|
+
});
|
|
4792
5950
|
Object.defineProperty(exports, 'versionVectorCovers', {
|
|
4793
5951
|
enumerable: true,
|
|
4794
5952
|
get: function () {
|
|
@@ -4806,4 +5964,16 @@ Object.defineProperty(exports, 'vvMerge', {
|
|
|
4806
5964
|
get: function () {
|
|
4807
5965
|
return vvMerge;
|
|
4808
5966
|
}
|
|
5967
|
+
});
|
|
5968
|
+
Object.defineProperty(exports, 'withLegacyMissingArrayParents', {
|
|
5969
|
+
enumerable: true,
|
|
5970
|
+
get: function () {
|
|
5971
|
+
return withLegacyMissingArrayParents;
|
|
5972
|
+
}
|
|
5973
|
+
});
|
|
5974
|
+
Object.defineProperty(exports, 'withStrictRfc6902Parents', {
|
|
5975
|
+
enumerable: true,
|
|
5976
|
+
get: function () {
|
|
5977
|
+
return withStrictRfc6902Parents;
|
|
5978
|
+
}
|
|
4809
5979
|
});
|