json-patch-to-crdt 0.1.1 → 0.1.3
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 +103 -3
- package/dist/{merge-CKcP1ZPt.mjs → compact-BdTuOQK-.mjs} +1191 -268
- package/dist/{merge-BAfuC6bf.js → compact-DoM9CJNR.js} +1244 -267
- package/dist/{merge-B8nmGV-o.d.ts → depth-Dl_yOAKU.d.ts} +153 -7
- package/dist/{merge-DQ_KDtnE.d.mts → depth-IvWvLAkt.d.mts} +153 -7
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +22 -16
- package/dist/index.mjs +2 -2
- package/dist/internals.d.mts +26 -25
- package/dist/internals.d.ts +26 -25
- package/dist/internals.js +67 -58
- package/dist/internals.mjs +2 -2
- package/package.json +12 -2
|
@@ -1,11 +1,34 @@
|
|
|
1
1
|
|
|
2
2
|
//#region src/clock.ts
|
|
3
|
+
var ClockValidationError = class extends TypeError {
|
|
4
|
+
reason;
|
|
5
|
+
constructor(reason, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "ClockValidationError";
|
|
8
|
+
this.reason = reason;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
function readVvCounter$1(vv, actor) {
|
|
12
|
+
if (!Object.prototype.hasOwnProperty.call(vv, actor)) return 0;
|
|
13
|
+
const counter = vv[actor];
|
|
14
|
+
return typeof counter === "number" ? counter : 0;
|
|
15
|
+
}
|
|
16
|
+
function writeVvCounter$1(vv, actor, counter) {
|
|
17
|
+
Object.defineProperty(vv, actor, {
|
|
18
|
+
configurable: true,
|
|
19
|
+
enumerable: true,
|
|
20
|
+
value: counter,
|
|
21
|
+
writable: true
|
|
22
|
+
});
|
|
23
|
+
}
|
|
3
24
|
/**
|
|
4
25
|
* Create a new clock for the given actor. Each call to `clock.next()` yields a fresh `Dot`.
|
|
5
26
|
* @param actor - Unique identifier for this peer.
|
|
6
27
|
* @param start - Initial counter value (defaults to 0).
|
|
7
28
|
*/
|
|
8
29
|
function createClock(actor, start = 0) {
|
|
30
|
+
assertActorId(actor);
|
|
31
|
+
assertCounter(start);
|
|
9
32
|
const clock = {
|
|
10
33
|
actor,
|
|
11
34
|
ctr: start,
|
|
@@ -19,6 +42,12 @@ function createClock(actor, start = 0) {
|
|
|
19
42
|
};
|
|
20
43
|
return clock;
|
|
21
44
|
}
|
|
45
|
+
function assertActorId(actor) {
|
|
46
|
+
if (actor.length === 0) throw new ClockValidationError("INVALID_ACTOR", "actor must not be empty");
|
|
47
|
+
}
|
|
48
|
+
function assertCounter(counter) {
|
|
49
|
+
if (!Number.isSafeInteger(counter) || counter < 0) throw new ClockValidationError("INVALID_COUNTER", "counter must be a non-negative safe integer");
|
|
50
|
+
}
|
|
22
51
|
/** Create an independent copy of a clock at the same counter position. */
|
|
23
52
|
function cloneClock(clock) {
|
|
24
53
|
return createClock(clock.actor, clock.ctr);
|
|
@@ -28,8 +57,8 @@ function cloneClock(clock) {
|
|
|
28
57
|
* Useful when a server needs to mint dots for many actors.
|
|
29
58
|
*/
|
|
30
59
|
function nextDotForActor(vv, actor) {
|
|
31
|
-
const ctr = (vv
|
|
32
|
-
vv
|
|
60
|
+
const ctr = readVvCounter$1(vv, actor) + 1;
|
|
61
|
+
writeVvCounter$1(vv, actor, ctr);
|
|
33
62
|
return {
|
|
34
63
|
actor,
|
|
35
64
|
ctr
|
|
@@ -37,21 +66,62 @@ function nextDotForActor(vv, actor) {
|
|
|
37
66
|
}
|
|
38
67
|
/** Record an observed dot in a version vector. */
|
|
39
68
|
function observeDot(vv, dot) {
|
|
40
|
-
if ((vv
|
|
69
|
+
if (readVvCounter$1(vv, dot.actor) < dot.ctr) writeVvCounter$1(vv, dot.actor, dot.ctr);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/depth.ts
|
|
74
|
+
const MAX_TRAVERSAL_DEPTH = 16384;
|
|
75
|
+
var TraversalDepthError = class extends Error {
|
|
76
|
+
code = 409;
|
|
77
|
+
reason = "MAX_DEPTH_EXCEEDED";
|
|
78
|
+
depth;
|
|
79
|
+
maxDepth;
|
|
80
|
+
constructor(depth, maxDepth = MAX_TRAVERSAL_DEPTH) {
|
|
81
|
+
super(`maximum nesting depth ${maxDepth} exceeded at depth ${depth}`);
|
|
82
|
+
this.name = "TraversalDepthError";
|
|
83
|
+
this.depth = depth;
|
|
84
|
+
this.maxDepth = maxDepth;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
function assertTraversalDepth(depth, maxDepth = MAX_TRAVERSAL_DEPTH) {
|
|
88
|
+
if (depth > maxDepth) throw new TraversalDepthError(depth, maxDepth);
|
|
89
|
+
}
|
|
90
|
+
function toDepthApplyError(error) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
code: error.code,
|
|
94
|
+
reason: error.reason,
|
|
95
|
+
message: error.message
|
|
96
|
+
};
|
|
41
97
|
}
|
|
42
98
|
|
|
43
99
|
//#endregion
|
|
44
100
|
//#region src/dot.ts
|
|
101
|
+
function readVvCounter(vv, actor) {
|
|
102
|
+
if (!Object.prototype.hasOwnProperty.call(vv, actor)) return;
|
|
103
|
+
const counter = vv[actor];
|
|
104
|
+
return typeof counter === "number" ? counter : void 0;
|
|
105
|
+
}
|
|
106
|
+
function writeVvCounter(vv, actor, counter) {
|
|
107
|
+
Object.defineProperty(vv, actor, {
|
|
108
|
+
configurable: true,
|
|
109
|
+
enumerable: true,
|
|
110
|
+
value: counter,
|
|
111
|
+
writable: true
|
|
112
|
+
});
|
|
113
|
+
}
|
|
45
114
|
function compareDot(a, b) {
|
|
46
115
|
if (a.ctr !== b.ctr) return a.ctr - b.ctr;
|
|
47
116
|
return a.actor < b.actor ? -1 : a.actor > b.actor ? 1 : 0;
|
|
48
117
|
}
|
|
49
118
|
function vvHasDot(vv, d) {
|
|
50
|
-
return (vv
|
|
119
|
+
return (readVvCounter(vv, d.actor) ?? 0) >= d.ctr;
|
|
51
120
|
}
|
|
52
121
|
function vvMerge(a, b) {
|
|
53
|
-
const out =
|
|
54
|
-
for (const [actor, ctr] of Object.entries(
|
|
122
|
+
const out = Object.create(null);
|
|
123
|
+
for (const [actor, ctr] of Object.entries(a)) writeVvCounter(out, actor, ctr);
|
|
124
|
+
for (const [actor, ctr] of Object.entries(b)) writeVvCounter(out, actor, Math.max(readVvCounter(out, actor) ?? 0, ctr));
|
|
55
125
|
return out;
|
|
56
126
|
}
|
|
57
127
|
function dotToElemId(d) {
|
|
@@ -85,15 +155,26 @@ function rgaLinearizeIds(seq) {
|
|
|
85
155
|
if (cached && cached.version === ver) return cached.ids;
|
|
86
156
|
const idx = rgaChildrenIndex(seq);
|
|
87
157
|
const out = [];
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
158
|
+
const stack = [];
|
|
159
|
+
const rootChildren = idx.get(HEAD);
|
|
160
|
+
if (rootChildren) stack.push({
|
|
161
|
+
children: rootChildren,
|
|
162
|
+
index: 0
|
|
163
|
+
});
|
|
164
|
+
while (stack.length > 0) {
|
|
165
|
+
const frame = stack[stack.length - 1];
|
|
166
|
+
if (frame.index >= frame.children.length) {
|
|
167
|
+
stack.pop();
|
|
168
|
+
continue;
|
|
94
169
|
}
|
|
170
|
+
const child = frame.children[frame.index++];
|
|
171
|
+
if (!child.tombstone) out.push(child.id);
|
|
172
|
+
const grandchildren = idx.get(child.id);
|
|
173
|
+
if (grandchildren) stack.push({
|
|
174
|
+
children: grandchildren,
|
|
175
|
+
index: 0
|
|
176
|
+
});
|
|
95
177
|
}
|
|
96
|
-
walk(HEAD);
|
|
97
178
|
linearCache.set(seq, {
|
|
98
179
|
version: ver,
|
|
99
180
|
ids: out
|
|
@@ -118,6 +199,61 @@ function rgaDelete(seq, id) {
|
|
|
118
199
|
e.tombstone = true;
|
|
119
200
|
bumpVersion(seq);
|
|
120
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* Prune tombstoned elements that are causally stable and have no live descendants
|
|
204
|
+
* depending on them for sequence traversal.
|
|
205
|
+
*
|
|
206
|
+
* Returns the number of removed elements.
|
|
207
|
+
*/
|
|
208
|
+
function rgaCompactTombstones(seq, isStable) {
|
|
209
|
+
if (seq.elems.size === 0) return 0;
|
|
210
|
+
const children = /* @__PURE__ */ new Map();
|
|
211
|
+
const roots = [];
|
|
212
|
+
for (const elem of seq.elems.values()) {
|
|
213
|
+
const byPrev = children.get(elem.prev);
|
|
214
|
+
if (byPrev) byPrev.push(elem.id);
|
|
215
|
+
else children.set(elem.prev, [elem.id]);
|
|
216
|
+
if (elem.prev === HEAD || !seq.elems.has(elem.prev)) roots.push(elem.id);
|
|
217
|
+
}
|
|
218
|
+
const removable = /* @__PURE__ */ new Set();
|
|
219
|
+
const visited = /* @__PURE__ */ new Set();
|
|
220
|
+
const stack = [];
|
|
221
|
+
const pushRoot = (id) => {
|
|
222
|
+
if (!visited.has(id)) stack.push({
|
|
223
|
+
id,
|
|
224
|
+
expanded: false
|
|
225
|
+
});
|
|
226
|
+
};
|
|
227
|
+
for (const id of roots) pushRoot(id);
|
|
228
|
+
for (const id of seq.elems.keys()) pushRoot(id);
|
|
229
|
+
while (stack.length > 0) {
|
|
230
|
+
const frame = stack.pop();
|
|
231
|
+
if (!frame.expanded) {
|
|
232
|
+
if (visited.has(frame.id)) continue;
|
|
233
|
+
visited.add(frame.id);
|
|
234
|
+
stack.push({
|
|
235
|
+
id: frame.id,
|
|
236
|
+
expanded: true
|
|
237
|
+
});
|
|
238
|
+
const childIds = children.get(frame.id);
|
|
239
|
+
if (childIds) {
|
|
240
|
+
for (const childId of childIds) if (!visited.has(childId)) stack.push({
|
|
241
|
+
id: childId,
|
|
242
|
+
expanded: false
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const elem = seq.elems.get(frame.id);
|
|
248
|
+
if (!elem || !elem.tombstone || !isStable(elem.insDot)) continue;
|
|
249
|
+
const childIds = children.get(frame.id);
|
|
250
|
+
if (!childIds || childIds.every((childId) => removable.has(childId))) removable.add(frame.id);
|
|
251
|
+
}
|
|
252
|
+
if (removable.size === 0) return 0;
|
|
253
|
+
for (const id of removable) seq.elems.delete(id);
|
|
254
|
+
bumpVersion(seq);
|
|
255
|
+
return removable.size;
|
|
256
|
+
}
|
|
121
257
|
function rgaIdAtIndex(seq, index) {
|
|
122
258
|
return rgaLinearizeIds(seq)[index];
|
|
123
259
|
}
|
|
@@ -129,17 +265,111 @@ function rgaPrevForInsertAtIndex(seq, index) {
|
|
|
129
265
|
|
|
130
266
|
//#endregion
|
|
131
267
|
//#region src/materialize.ts
|
|
132
|
-
|
|
268
|
+
function createMaterializedObject() {
|
|
269
|
+
return Object.create(null);
|
|
270
|
+
}
|
|
271
|
+
function setMaterializedProperty(out, key, value) {
|
|
272
|
+
Object.defineProperty(out, key, {
|
|
273
|
+
configurable: true,
|
|
274
|
+
enumerable: true,
|
|
275
|
+
value,
|
|
276
|
+
writable: true
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
/** Convert a CRDT node graph into a plain JSON value using an explicit stack. */
|
|
133
280
|
function materialize(node) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
281
|
+
if (node.kind === "lww") return node.value;
|
|
282
|
+
const root = node.kind === "obj" ? createMaterializedObject() : [];
|
|
283
|
+
const stack = [];
|
|
284
|
+
if (node.kind === "obj") stack.push({
|
|
285
|
+
kind: "obj",
|
|
286
|
+
depth: 0,
|
|
287
|
+
entries: Array.from(node.entries.entries(), ([key, value]) => [key, value.node]),
|
|
288
|
+
index: 0,
|
|
289
|
+
out: root
|
|
290
|
+
});
|
|
291
|
+
else stack.push({
|
|
292
|
+
kind: "seq",
|
|
293
|
+
depth: 0,
|
|
294
|
+
ids: rgaLinearizeIds(node),
|
|
295
|
+
index: 0,
|
|
296
|
+
seq: node,
|
|
297
|
+
out: root
|
|
298
|
+
});
|
|
299
|
+
while (stack.length > 0) {
|
|
300
|
+
const frame = stack[stack.length - 1];
|
|
301
|
+
if (frame.kind === "obj") {
|
|
302
|
+
if (frame.index >= frame.entries.length) {
|
|
303
|
+
stack.pop();
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const [key, child] = frame.entries[frame.index++];
|
|
307
|
+
const childDepth = frame.depth + 1;
|
|
308
|
+
assertTraversalDepth(childDepth);
|
|
309
|
+
if (child.kind === "lww") {
|
|
310
|
+
setMaterializedProperty(frame.out, key, child.value);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (child.kind === "obj") {
|
|
314
|
+
const outObj = createMaterializedObject();
|
|
315
|
+
setMaterializedProperty(frame.out, key, outObj);
|
|
316
|
+
stack.push({
|
|
317
|
+
kind: "obj",
|
|
318
|
+
depth: childDepth,
|
|
319
|
+
entries: Array.from(child.entries.entries(), ([childKey, value]) => [childKey, value.node]),
|
|
320
|
+
index: 0,
|
|
321
|
+
out: outObj
|
|
322
|
+
});
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const outArr = [];
|
|
326
|
+
setMaterializedProperty(frame.out, key, outArr);
|
|
327
|
+
stack.push({
|
|
328
|
+
kind: "seq",
|
|
329
|
+
depth: childDepth,
|
|
330
|
+
ids: rgaLinearizeIds(child),
|
|
331
|
+
index: 0,
|
|
332
|
+
seq: child,
|
|
333
|
+
out: outArr
|
|
334
|
+
});
|
|
335
|
+
continue;
|
|
140
336
|
}
|
|
141
|
-
|
|
337
|
+
if (frame.index >= frame.ids.length) {
|
|
338
|
+
stack.pop();
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const id = frame.ids[frame.index++];
|
|
342
|
+
const child = frame.seq.elems.get(id).value;
|
|
343
|
+
const childDepth = frame.depth + 1;
|
|
344
|
+
assertTraversalDepth(childDepth);
|
|
345
|
+
if (child.kind === "lww") {
|
|
346
|
+
frame.out.push(child.value);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (child.kind === "obj") {
|
|
350
|
+
const outObj = createMaterializedObject();
|
|
351
|
+
frame.out.push(outObj);
|
|
352
|
+
stack.push({
|
|
353
|
+
kind: "obj",
|
|
354
|
+
depth: childDepth,
|
|
355
|
+
entries: Array.from(child.entries.entries(), ([key, value]) => [key, value.node]),
|
|
356
|
+
index: 0,
|
|
357
|
+
out: outObj
|
|
358
|
+
});
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
const outArr = [];
|
|
362
|
+
frame.out.push(outArr);
|
|
363
|
+
stack.push({
|
|
364
|
+
kind: "seq",
|
|
365
|
+
depth: childDepth,
|
|
366
|
+
ids: rgaLinearizeIds(child),
|
|
367
|
+
index: 0,
|
|
368
|
+
seq: child,
|
|
369
|
+
out: outArr
|
|
370
|
+
});
|
|
142
371
|
}
|
|
372
|
+
return root;
|
|
143
373
|
}
|
|
144
374
|
|
|
145
375
|
//#endregion
|
|
@@ -184,6 +414,168 @@ function objRemove(obj, key, dot) {
|
|
|
184
414
|
if (!curDel || compareDot(curDel, dot) <= 0) obj.tombstone.set(key, dot);
|
|
185
415
|
obj.entries.delete(key);
|
|
186
416
|
}
|
|
417
|
+
/**
|
|
418
|
+
* Prune object tombstones that satisfy a caller-provided stability predicate.
|
|
419
|
+
* Returns the number of removed tombstone records.
|
|
420
|
+
*/
|
|
421
|
+
function objCompactTombstones(obj, isStable) {
|
|
422
|
+
let removed = 0;
|
|
423
|
+
for (const [key, dot] of obj.tombstone.entries()) {
|
|
424
|
+
if (!isStable(dot)) continue;
|
|
425
|
+
obj.tombstone.delete(key);
|
|
426
|
+
removed += 1;
|
|
427
|
+
}
|
|
428
|
+
return removed;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
//#endregion
|
|
432
|
+
//#region src/json-value.ts
|
|
433
|
+
/**
|
|
434
|
+
* Runtime validation error for values that are not JSON-compatible.
|
|
435
|
+
* `path` is an RFC 6901 pointer relative to the validated root.
|
|
436
|
+
*/
|
|
437
|
+
var JsonValueValidationError = class extends TypeError {
|
|
438
|
+
path;
|
|
439
|
+
detail;
|
|
440
|
+
constructor(path, detail) {
|
|
441
|
+
super(`invalid JSON value at ${path === "" ? "<root>" : path}: ${detail}`);
|
|
442
|
+
this.name = "JsonValueValidationError";
|
|
443
|
+
this.path = path;
|
|
444
|
+
this.detail = detail;
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
/** Assert that a runtime value is JSON-compatible (including finite numbers only). */
|
|
448
|
+
function assertRuntimeJsonValue(value) {
|
|
449
|
+
const stack = [{
|
|
450
|
+
value,
|
|
451
|
+
path: "",
|
|
452
|
+
depth: 0
|
|
453
|
+
}];
|
|
454
|
+
while (stack.length > 0) {
|
|
455
|
+
const frame = stack.pop();
|
|
456
|
+
assertTraversalDepth(frame.depth);
|
|
457
|
+
if (isJsonPrimitive$1(frame.value)) continue;
|
|
458
|
+
if (Array.isArray(frame.value)) {
|
|
459
|
+
for (const [index, child] of frame.value.entries()) stack.push({
|
|
460
|
+
value: child,
|
|
461
|
+
path: appendPointerSegment(frame.path, String(index)),
|
|
462
|
+
depth: frame.depth + 1
|
|
463
|
+
});
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (isJsonObject(frame.value)) {
|
|
467
|
+
for (const [key, child] of Object.entries(frame.value)) stack.push({
|
|
468
|
+
value: child,
|
|
469
|
+
path: appendPointerSegment(frame.path, key),
|
|
470
|
+
depth: frame.depth + 1
|
|
471
|
+
});
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
throw new JsonValueValidationError(frame.path, describeInvalidValue(frame.value));
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Normalize a runtime value to JSON-compatible data.
|
|
479
|
+
* - non-finite numbers -> null
|
|
480
|
+
* - invalid object-property values -> key omitted
|
|
481
|
+
* - invalid root / array values -> null
|
|
482
|
+
*/
|
|
483
|
+
function normalizeRuntimeJsonValue(value) {
|
|
484
|
+
const rootHolder = {};
|
|
485
|
+
const stack = [{
|
|
486
|
+
value,
|
|
487
|
+
path: "",
|
|
488
|
+
depth: 0,
|
|
489
|
+
slot: { kind: "root" }
|
|
490
|
+
}];
|
|
491
|
+
while (stack.length > 0) {
|
|
492
|
+
const frame = stack.pop();
|
|
493
|
+
assertTraversalDepth(frame.depth);
|
|
494
|
+
if (isJsonPrimitive$1(frame.value)) {
|
|
495
|
+
assignSlot(frame.slot, frame.value, rootHolder);
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
if (Array.isArray(frame.value)) {
|
|
499
|
+
const out = [];
|
|
500
|
+
assignSlot(frame.slot, out, rootHolder);
|
|
501
|
+
for (const [index, child] of frame.value.entries()) stack.push({
|
|
502
|
+
value: child,
|
|
503
|
+
path: appendPointerSegment(frame.path, String(index)),
|
|
504
|
+
depth: frame.depth + 1,
|
|
505
|
+
slot: {
|
|
506
|
+
kind: "array",
|
|
507
|
+
out,
|
|
508
|
+
index
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
if (isJsonObject(frame.value)) {
|
|
514
|
+
const out = Object.create(null);
|
|
515
|
+
assignSlot(frame.slot, out, rootHolder);
|
|
516
|
+
for (const [key, child] of Object.entries(frame.value)) stack.push({
|
|
517
|
+
value: child,
|
|
518
|
+
path: appendPointerSegment(frame.path, key),
|
|
519
|
+
depth: frame.depth + 1,
|
|
520
|
+
slot: {
|
|
521
|
+
kind: "object",
|
|
522
|
+
out,
|
|
523
|
+
key
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (isNonFiniteNumber(frame.value)) {
|
|
529
|
+
assignSlot(frame.slot, null, rootHolder);
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
if (frame.slot.kind !== "object") assignSlot(frame.slot, null, rootHolder);
|
|
533
|
+
}
|
|
534
|
+
return rootHolder.value ?? null;
|
|
535
|
+
}
|
|
536
|
+
/** Runtime JSON guardrail helper shared by create/apply/diff paths. */
|
|
537
|
+
function coerceRuntimeJsonValue(value, mode) {
|
|
538
|
+
if (mode === "none") return value;
|
|
539
|
+
if (mode === "strict") {
|
|
540
|
+
assertRuntimeJsonValue(value);
|
|
541
|
+
return value;
|
|
542
|
+
}
|
|
543
|
+
return normalizeRuntimeJsonValue(value);
|
|
544
|
+
}
|
|
545
|
+
function assignSlot(slot, value, rootHolder) {
|
|
546
|
+
if (slot.kind === "root") {
|
|
547
|
+
rootHolder.value = value;
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (slot.kind === "array") {
|
|
551
|
+
slot.out[slot.index] = value;
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
slot.out[slot.key] = value;
|
|
555
|
+
}
|
|
556
|
+
function appendPointerSegment(path, segment) {
|
|
557
|
+
const escaped = segment.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
558
|
+
if (path === "") return `/${escaped}`;
|
|
559
|
+
return `${path}/${escaped}`;
|
|
560
|
+
}
|
|
561
|
+
function isJsonPrimitive$1(value) {
|
|
562
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
563
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
564
|
+
}
|
|
565
|
+
function isJsonObject(value) {
|
|
566
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
567
|
+
}
|
|
568
|
+
function isNonFiniteNumber(value) {
|
|
569
|
+
return typeof value === "number" && !Number.isFinite(value);
|
|
570
|
+
}
|
|
571
|
+
function describeInvalidValue(value) {
|
|
572
|
+
if (typeof value === "number") return `non-finite number (${String(value)})`;
|
|
573
|
+
if (value === void 0) return "undefined is not valid JSON";
|
|
574
|
+
if (typeof value === "bigint") return "bigint is not valid JSON";
|
|
575
|
+
if (typeof value === "symbol") return "symbol is not valid JSON";
|
|
576
|
+
if (typeof value === "function") return "function is not valid JSON";
|
|
577
|
+
return `unsupported value type (${typeof value})`;
|
|
578
|
+
}
|
|
187
579
|
|
|
188
580
|
//#endregion
|
|
189
581
|
//#region src/types.ts
|
|
@@ -195,6 +587,7 @@ const ROOT_KEY = "@@crdt/root";
|
|
|
195
587
|
|
|
196
588
|
//#endregion
|
|
197
589
|
//#region src/patch.ts
|
|
590
|
+
const DEFAULT_LCS_MAX_CELLS = 25e4;
|
|
198
591
|
/** Structured compile error used to map patch validation failures to typed reasons. */
|
|
199
592
|
var PatchCompileError = class extends Error {
|
|
200
593
|
reason;
|
|
@@ -208,6 +601,17 @@ var PatchCompileError = class extends Error {
|
|
|
208
601
|
this.opIndex = opIndex;
|
|
209
602
|
}
|
|
210
603
|
};
|
|
604
|
+
/** Structured lookup error thrown by `getAtJson`. */
|
|
605
|
+
var JsonLookupError = class extends Error {
|
|
606
|
+
code;
|
|
607
|
+
segment;
|
|
608
|
+
constructor(code, segment, message) {
|
|
609
|
+
super(message);
|
|
610
|
+
this.name = "JsonLookupError";
|
|
611
|
+
this.code = code;
|
|
612
|
+
this.segment = segment;
|
|
613
|
+
}
|
|
614
|
+
};
|
|
211
615
|
/**
|
|
212
616
|
* Parse an RFC 6901 JSON Pointer into a path array, unescaping `~1` and `~0`.
|
|
213
617
|
* @param ptr - A JSON Pointer string (e.g. `"/a/b"` or `""`).
|
|
@@ -254,14 +658,15 @@ function unescapeJsonPointerToken(token) {
|
|
|
254
658
|
function getAtJson(base, path) {
|
|
255
659
|
let cur = base;
|
|
256
660
|
for (const seg of path) if (Array.isArray(cur)) {
|
|
257
|
-
if (!ARRAY_INDEX_TOKEN_PATTERN.test(seg)) throw new
|
|
661
|
+
if (!ARRAY_INDEX_TOKEN_PATTERN.test(seg)) throw new JsonLookupError("EXPECTED_ARRAY_INDEX", seg, `Expected array index, got '${seg}'`);
|
|
258
662
|
const idx = Number(seg);
|
|
259
|
-
if (idx < 0 || idx >= cur.length) throw new
|
|
663
|
+
if (idx < 0 || idx >= cur.length) throw new JsonLookupError("INDEX_OUT_OF_BOUNDS", seg, `Index out of bounds at '${seg}'`);
|
|
260
664
|
cur = cur[idx];
|
|
261
665
|
} else if (cur && typeof cur === "object") {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
666
|
+
const obj = cur;
|
|
667
|
+
if (!hasOwn(obj, seg)) throw new JsonLookupError("MISSING_KEY", seg, `Missing key '${seg}'`);
|
|
668
|
+
cur = obj[seg];
|
|
669
|
+
} else throw new JsonLookupError("NON_CONTAINER", seg, `Cannot traverse into non-container at '${seg}'`);
|
|
265
670
|
return cur;
|
|
266
671
|
}
|
|
267
672
|
/**
|
|
@@ -275,12 +680,13 @@ function getAtJson(base, path) {
|
|
|
275
680
|
function compileJsonPatchToIntent(baseJson, patch, options = {}) {
|
|
276
681
|
const semantics = options.semantics ?? "sequential";
|
|
277
682
|
let workingBase = semantics === "sequential" ? structuredClone(baseJson) : baseJson;
|
|
683
|
+
const pointerCache = /* @__PURE__ */ new Map();
|
|
278
684
|
const intents = [];
|
|
279
685
|
for (let opIndex = 0; opIndex < patch.length; opIndex++) {
|
|
280
686
|
const op = patch[opIndex];
|
|
281
687
|
const compileBase = semantics === "sequential" ? workingBase : baseJson;
|
|
282
|
-
intents.push(...compileSingleOp(compileBase, op, opIndex, semantics));
|
|
283
|
-
if (semantics === "sequential") workingBase =
|
|
688
|
+
intents.push(...compileSingleOp(compileBase, op, opIndex, semantics, pointerCache));
|
|
689
|
+
if (semantics === "sequential") workingBase = applyPatchOpToJsonInPlace(workingBase, op, opIndex, pointerCache);
|
|
284
690
|
}
|
|
285
691
|
return intents;
|
|
286
692
|
}
|
|
@@ -294,15 +700,26 @@ function compileJsonPatchToIntent(baseJson, patch, options = {}) {
|
|
|
294
700
|
* @returns An array of JSON Patch operations that transform `base` into `next`.
|
|
295
701
|
*/
|
|
296
702
|
function diffJsonPatch(base, next, options = {}) {
|
|
703
|
+
const runtimeMode = options.jsonValidation ?? "none";
|
|
704
|
+
const runtimeBase = coerceRuntimeJsonValue(base, runtimeMode);
|
|
705
|
+
const runtimeNext = coerceRuntimeJsonValue(next, runtimeMode);
|
|
297
706
|
const ops = [];
|
|
298
|
-
diffValue([],
|
|
707
|
+
diffValue([], runtimeBase, runtimeNext, ops, options);
|
|
299
708
|
return ops;
|
|
300
709
|
}
|
|
301
710
|
function diffValue(path, base, next, ops, options) {
|
|
302
711
|
if (jsonEquals(base, next)) return;
|
|
303
712
|
if (Array.isArray(base) || Array.isArray(next)) {
|
|
304
713
|
if ((options.arrayStrategy ?? "lcs") === "lcs" && Array.isArray(base) && Array.isArray(next)) {
|
|
305
|
-
|
|
714
|
+
if (!shouldUseLcsDiff(base.length, next.length, options.lcsMaxCells)) {
|
|
715
|
+
ops.push({
|
|
716
|
+
op: "replace",
|
|
717
|
+
path: stringifyJsonPointer(path),
|
|
718
|
+
value: next
|
|
719
|
+
});
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
diffArray(path, base, next, ops);
|
|
306
723
|
return;
|
|
307
724
|
}
|
|
308
725
|
ops.push({
|
|
@@ -338,7 +755,7 @@ function diffValue(path, base, next, ops, options) {
|
|
|
338
755
|
}
|
|
339
756
|
for (const key of baseKeys) if (nextSet.has(key)) diffValue([...path, key], base[key], next[key], ops, options);
|
|
340
757
|
}
|
|
341
|
-
function diffArray(path, base, next, ops
|
|
758
|
+
function diffArray(path, base, next, ops) {
|
|
342
759
|
const n = base.length;
|
|
343
760
|
const m = next.length;
|
|
344
761
|
const lcs = Array.from({ length: n + 1 }, () => Array(m + 1).fill(0));
|
|
@@ -378,6 +795,12 @@ function diffArray(path, base, next, ops, _options) {
|
|
|
378
795
|
}
|
|
379
796
|
ops.push(...compactArrayOps(localOps));
|
|
380
797
|
}
|
|
798
|
+
function shouldUseLcsDiff(baseLength, nextLength, lcsMaxCells) {
|
|
799
|
+
if (lcsMaxCells === Number.POSITIVE_INFINITY) return true;
|
|
800
|
+
const cap = lcsMaxCells ?? DEFAULT_LCS_MAX_CELLS;
|
|
801
|
+
if (!Number.isFinite(cap) || cap < 1) return false;
|
|
802
|
+
return (baseLength + 1) * (nextLength + 1) <= cap;
|
|
803
|
+
}
|
|
381
804
|
function compactArrayOps(ops) {
|
|
382
805
|
const out = [];
|
|
383
806
|
for (let i = 0; i < ops.length; i++) {
|
|
@@ -414,7 +837,7 @@ function jsonEquals(a, b) {
|
|
|
414
837
|
const bKeys = Object.keys(b);
|
|
415
838
|
if (aKeys.length !== bKeys.length) return false;
|
|
416
839
|
for (const key of aKeys) {
|
|
417
|
-
if (!(key
|
|
840
|
+
if (!hasOwn(b, key)) return false;
|
|
418
841
|
if (!jsonEquals(a[key], b[key])) return false;
|
|
419
842
|
}
|
|
420
843
|
return true;
|
|
@@ -426,6 +849,9 @@ const ARRAY_INDEX_TOKEN_PATTERN = /^(0|[1-9][0-9]*)$/;
|
|
|
426
849
|
function hasOwn(value, key) {
|
|
427
850
|
return Object.prototype.hasOwnProperty.call(value, key);
|
|
428
851
|
}
|
|
852
|
+
function isUnsafeObjectKey(key) {
|
|
853
|
+
return key === "__proto__";
|
|
854
|
+
}
|
|
429
855
|
function pathValueAt(base, path) {
|
|
430
856
|
if (path.length === 0) return base;
|
|
431
857
|
return getAtJson(base, path);
|
|
@@ -433,17 +859,17 @@ function pathValueAt(base, path) {
|
|
|
433
859
|
function assertNever$1(_value, message) {
|
|
434
860
|
throw new Error(message);
|
|
435
861
|
}
|
|
436
|
-
function compileSingleOp(baseJson, op, opIndex, semantics) {
|
|
862
|
+
function compileSingleOp(baseJson, op, opIndex, semantics, pointerCache) {
|
|
437
863
|
if (op.op === "test") return [{
|
|
438
864
|
t: "Test",
|
|
439
|
-
path: parsePointerOrThrow(op.path, op.path, opIndex),
|
|
865
|
+
path: parsePointerOrThrow(op.path, op.path, opIndex, pointerCache),
|
|
440
866
|
value: op.value
|
|
441
867
|
}];
|
|
442
868
|
if (op.op === "copy" || op.op === "move") {
|
|
443
|
-
const fromPath = parsePointerOrThrow(op.from, op.from, opIndex);
|
|
444
|
-
const toPath = parsePointerOrThrow(op.path, op.path, opIndex);
|
|
869
|
+
const fromPath = parsePointerOrThrow(op.from, op.from, opIndex, pointerCache);
|
|
870
|
+
const toPath = parsePointerOrThrow(op.path, op.path, opIndex, pointerCache);
|
|
445
871
|
if (op.op === "move" && isStrictDescendantPath(fromPath, toPath)) throw compileError("INVALID_MOVE", `cannot move a value into one of its descendants at ${op.path}`, op.path, opIndex);
|
|
446
|
-
const val = lookupValueOrThrow(baseJson, fromPath, op.from, opIndex);
|
|
872
|
+
const val = structuredClone(lookupValueOrThrow(baseJson, fromPath, op.from, opIndex));
|
|
447
873
|
if (op.op === "move" && isSamePath(fromPath, toPath)) return [];
|
|
448
874
|
if (op.op === "move" && semantics === "sequential") {
|
|
449
875
|
const removeOp = {
|
|
@@ -455,21 +881,21 @@ function compileSingleOp(baseJson, op, opIndex, semantics) {
|
|
|
455
881
|
path: op.path,
|
|
456
882
|
value: val
|
|
457
883
|
};
|
|
458
|
-
const baseAfterRemove = applyPatchOpToJson(baseJson, removeOp, opIndex);
|
|
459
|
-
return [...compileSingleOp(baseJson, removeOp, opIndex, semantics), ...compileSingleOp(baseAfterRemove, addOp, opIndex, semantics)];
|
|
884
|
+
const baseAfterRemove = applyPatchOpToJson(baseJson, removeOp, opIndex, pointerCache);
|
|
885
|
+
return [...compileSingleOp(baseJson, removeOp, opIndex, semantics, pointerCache), ...compileSingleOp(baseAfterRemove, addOp, opIndex, semantics, pointerCache)];
|
|
460
886
|
}
|
|
461
887
|
const out = compileSingleOp(baseJson, {
|
|
462
888
|
op: "add",
|
|
463
889
|
path: op.path,
|
|
464
890
|
value: val
|
|
465
|
-
}, opIndex, semantics);
|
|
891
|
+
}, opIndex, semantics, pointerCache);
|
|
466
892
|
if (op.op === "move") out.push(...compileSingleOp(baseJson, {
|
|
467
893
|
op: "remove",
|
|
468
894
|
path: op.from
|
|
469
|
-
}, opIndex, semantics));
|
|
895
|
+
}, opIndex, semantics, pointerCache));
|
|
470
896
|
return out;
|
|
471
897
|
}
|
|
472
|
-
const path = parsePointerOrThrow(op.path, op.path, opIndex);
|
|
898
|
+
const path = parsePointerOrThrow(op.path, op.path, opIndex, pointerCache);
|
|
473
899
|
if (path.length === 0) {
|
|
474
900
|
if (op.op === "replace" || op.op === "add") return [{
|
|
475
901
|
t: "ObjSet",
|
|
@@ -505,6 +931,7 @@ function compileSingleOp(baseJson, op, opIndex, semantics) {
|
|
|
505
931
|
return assertNever$1(op, "Unsupported op at array path");
|
|
506
932
|
}
|
|
507
933
|
if (!isPlainObject(parentValue)) throw compileError("INVALID_TARGET", `expected object or array parent at ${parentPath}`, parentPath, opIndex);
|
|
934
|
+
if (isUnsafeObjectKey(token)) throw compileError("INVALID_POINTER", `unsafe object key at ${op.path}`, op.path, opIndex);
|
|
508
935
|
if ((op.op === "replace" || op.op === "remove") && !hasOwn(parentValue, token)) throw compileError("MISSING_TARGET", `missing key ${token} at ${parentPath}`, op.path, opIndex);
|
|
509
936
|
if (op.op === "add") return [{
|
|
510
937
|
t: "ObjSet",
|
|
@@ -527,32 +954,31 @@ function compileSingleOp(baseJson, op, opIndex, semantics) {
|
|
|
527
954
|
}];
|
|
528
955
|
return assertNever$1(op, "Unsupported op");
|
|
529
956
|
}
|
|
530
|
-
function applyPatchOpToJson(baseJson, op, opIndex) {
|
|
531
|
-
|
|
957
|
+
function applyPatchOpToJson(baseJson, op, opIndex, pointerCache) {
|
|
958
|
+
return applyPatchOpToJsonInPlace(structuredClone(baseJson), op, opIndex, pointerCache);
|
|
959
|
+
}
|
|
960
|
+
function applyPatchOpToJsonInPlace(doc, op, opIndex, pointerCache) {
|
|
532
961
|
if (op.op === "test") return doc;
|
|
533
962
|
if (op.op === "copy" || op.op === "move") {
|
|
534
|
-
const fromPath = parsePointerOrThrow(op.from, op.from, opIndex);
|
|
963
|
+
const fromPath = parsePointerOrThrow(op.from, op.from, opIndex, pointerCache);
|
|
535
964
|
const value = structuredClone(lookupValueOrThrow(doc, fromPath, op.from, opIndex));
|
|
536
|
-
|
|
965
|
+
return applyPatchOpToJsonInPlace(op.op === "move" ? applyPatchOpToJsonInPlace(doc, {
|
|
537
966
|
op: "remove",
|
|
538
967
|
path: op.from
|
|
539
|
-
}, opIndex)
|
|
540
|
-
return applyPatchOpToJson(doc, {
|
|
968
|
+
}, opIndex, pointerCache) : doc, {
|
|
541
969
|
op: "add",
|
|
542
970
|
path: op.path,
|
|
543
971
|
value
|
|
544
|
-
}, opIndex);
|
|
972
|
+
}, opIndex, pointerCache);
|
|
545
973
|
}
|
|
546
|
-
const path = parsePointerOrThrow(op.path, op.path, opIndex);
|
|
974
|
+
const path = parsePointerOrThrow(op.path, op.path, opIndex, pointerCache);
|
|
547
975
|
if (path.length === 0) {
|
|
548
976
|
if (op.op === "add" || op.op === "replace") return structuredClone(op.value);
|
|
549
977
|
throw compileError("INVALID_TARGET", "remove at root path is not supported in RFC-compliant mode", op.path, opIndex);
|
|
550
978
|
}
|
|
551
979
|
const parentPath = path.slice(0, -1);
|
|
552
980
|
const token = path[path.length - 1];
|
|
553
|
-
|
|
554
|
-
if (parentPath.length === 0) parent = doc;
|
|
555
|
-
else parent = lookupValueOrThrow(doc, parentPath, op.path, opIndex);
|
|
981
|
+
const parent = parentPath.length === 0 ? doc : lookupValueOrThrow(doc, parentPath, op.path, opIndex);
|
|
556
982
|
if (Array.isArray(parent)) {
|
|
557
983
|
const index = parseArrayIndexToken(token, op.op, parent.length, op.path, opIndex);
|
|
558
984
|
if (op.op === "add") {
|
|
@@ -568,6 +994,7 @@ function applyPatchOpToJson(baseJson, op, opIndex) {
|
|
|
568
994
|
return doc;
|
|
569
995
|
}
|
|
570
996
|
if (!isPlainObject(parent)) throw compileError("INVALID_TARGET", `expected object or array parent at ${stringifyJsonPointer(parentPath)}`, op.path, opIndex);
|
|
997
|
+
if (isUnsafeObjectKey(token)) throw compileError("INVALID_POINTER", `unsafe object key at ${op.path}`, op.path, opIndex);
|
|
571
998
|
if (op.op === "add" || op.op === "replace") {
|
|
572
999
|
parent[token] = structuredClone(op.value);
|
|
573
1000
|
return doc;
|
|
@@ -575,9 +1002,13 @@ function applyPatchOpToJson(baseJson, op, opIndex) {
|
|
|
575
1002
|
delete parent[token];
|
|
576
1003
|
return doc;
|
|
577
1004
|
}
|
|
578
|
-
function parsePointerOrThrow(ptr, path, opIndex) {
|
|
1005
|
+
function parsePointerOrThrow(ptr, path, opIndex, pointerCache) {
|
|
1006
|
+
const cached = pointerCache.get(ptr);
|
|
1007
|
+
if (cached) return cached.slice();
|
|
579
1008
|
try {
|
|
580
|
-
|
|
1009
|
+
const parsed = parseJsonPointer(ptr);
|
|
1010
|
+
pointerCache.set(ptr, parsed);
|
|
1011
|
+
return parsed.slice();
|
|
581
1012
|
} catch (error) {
|
|
582
1013
|
throw compileError("INVALID_POINTER", error instanceof Error ? error.message : "invalid pointer", path, opIndex);
|
|
583
1014
|
}
|
|
@@ -615,26 +1046,31 @@ function compileErrorFromLookup(error, path, opIndex) {
|
|
|
615
1046
|
return compileError(mapped.reason, mapped.message, path, opIndex);
|
|
616
1047
|
}
|
|
617
1048
|
function mapLookupErrorToPatchReason(error) {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
1049
|
+
if (error instanceof JsonLookupError) switch (error.code) {
|
|
1050
|
+
case "EXPECTED_ARRAY_INDEX": return {
|
|
1051
|
+
reason: "INVALID_POINTER",
|
|
1052
|
+
message: error.message
|
|
1053
|
+
};
|
|
1054
|
+
case "INDEX_OUT_OF_BOUNDS": return {
|
|
1055
|
+
reason: "OUT_OF_BOUNDS",
|
|
1056
|
+
message: error.message
|
|
1057
|
+
};
|
|
1058
|
+
case "MISSING_KEY": return {
|
|
1059
|
+
reason: "MISSING_PARENT",
|
|
1060
|
+
message: error.message
|
|
1061
|
+
};
|
|
1062
|
+
case "NON_CONTAINER": return {
|
|
1063
|
+
reason: "INVALID_TARGET",
|
|
1064
|
+
message: error.message
|
|
1065
|
+
};
|
|
1066
|
+
default: return {
|
|
1067
|
+
reason: "INVALID_PATCH",
|
|
1068
|
+
message: error.message
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
635
1071
|
return {
|
|
636
1072
|
reason: "INVALID_PATCH",
|
|
637
|
-
message
|
|
1073
|
+
message: error instanceof Error ? error.message : "invalid path"
|
|
638
1074
|
};
|
|
639
1075
|
}
|
|
640
1076
|
function compileError(reason, message, path, opIndex) {
|
|
@@ -751,7 +1187,43 @@ function ensureSeqAtPath(head, path, dotForCreate) {
|
|
|
751
1187
|
if (head.root.kind !== "seq") head.root = newSeq();
|
|
752
1188
|
return head.root;
|
|
753
1189
|
}
|
|
1190
|
+
function getNodeAtPath(doc, path) {
|
|
1191
|
+
let cur = doc.root;
|
|
1192
|
+
for (const seg of path) {
|
|
1193
|
+
if (cur.kind !== "obj") return;
|
|
1194
|
+
const ent = cur.entries.get(seg);
|
|
1195
|
+
if (!ent) return;
|
|
1196
|
+
cur = ent.node;
|
|
1197
|
+
}
|
|
1198
|
+
return cur;
|
|
1199
|
+
}
|
|
1200
|
+
function getHeadSeqForBaseArrayIntent(head, path) {
|
|
1201
|
+
const pointer = `/${path.join("/")}`;
|
|
1202
|
+
const headNode = getNodeAtPath(head, path);
|
|
1203
|
+
if (!headNode) return {
|
|
1204
|
+
ok: false,
|
|
1205
|
+
code: 409,
|
|
1206
|
+
reason: "MISSING_PARENT",
|
|
1207
|
+
message: `head array missing at ${pointer}`,
|
|
1208
|
+
path: pointer
|
|
1209
|
+
};
|
|
1210
|
+
if (headNode.kind !== "seq") return {
|
|
1211
|
+
ok: false,
|
|
1212
|
+
code: 409,
|
|
1213
|
+
reason: "INVALID_TARGET",
|
|
1214
|
+
message: `expected array at ${pointer}`,
|
|
1215
|
+
path: pointer
|
|
1216
|
+
};
|
|
1217
|
+
return {
|
|
1218
|
+
ok: true,
|
|
1219
|
+
seq: headNode
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
754
1222
|
function deepNodeFromJson(value, dot) {
|
|
1223
|
+
return deepNodeFromJsonWithDepth(value, dot, 0);
|
|
1224
|
+
}
|
|
1225
|
+
function deepNodeFromJsonWithDepth(value, dot, depth) {
|
|
1226
|
+
assertTraversalDepth(depth);
|
|
755
1227
|
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return newReg(value, dot);
|
|
756
1228
|
if (Array.isArray(value)) {
|
|
757
1229
|
const seq = newSeq();
|
|
@@ -763,40 +1235,123 @@ function deepNodeFromJson(value, dot) {
|
|
|
763
1235
|
ctr: ++ctr
|
|
764
1236
|
};
|
|
765
1237
|
const id = dotToElemId(childDot);
|
|
766
|
-
rgaInsertAfter(seq, prev, id, childDot,
|
|
1238
|
+
rgaInsertAfter(seq, prev, id, childDot, deepNodeFromJsonWithDepth(v, childDot, depth + 1));
|
|
767
1239
|
prev = id;
|
|
768
1240
|
}
|
|
769
1241
|
return seq;
|
|
770
1242
|
}
|
|
771
1243
|
const obj = newObj();
|
|
772
|
-
for (const [k, v] of Object.entries(value)) objSet(obj, k,
|
|
1244
|
+
for (const [k, v] of Object.entries(value)) objSet(obj, k, deepNodeFromJsonWithDepth(v, dot, depth + 1), dot);
|
|
773
1245
|
return obj;
|
|
774
1246
|
}
|
|
775
1247
|
function nodeFromJson(value, nextDot) {
|
|
776
|
-
if (value
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
1248
|
+
if (isJsonPrimitive(value)) return newReg(value, nextDot());
|
|
1249
|
+
const root = Array.isArray(value) ? newSeq() : newObj();
|
|
1250
|
+
const stack = [];
|
|
1251
|
+
if (Array.isArray(value)) stack.push({
|
|
1252
|
+
kind: "seq",
|
|
1253
|
+
depth: 0,
|
|
1254
|
+
values: value,
|
|
1255
|
+
index: 0,
|
|
1256
|
+
prev: HEAD,
|
|
1257
|
+
target: root
|
|
1258
|
+
});
|
|
1259
|
+
else stack.push({
|
|
1260
|
+
kind: "obj",
|
|
1261
|
+
depth: 0,
|
|
1262
|
+
entries: Object.entries(value),
|
|
1263
|
+
index: 0,
|
|
1264
|
+
target: root
|
|
1265
|
+
});
|
|
1266
|
+
while (stack.length > 0) {
|
|
1267
|
+
const frame = stack[stack.length - 1];
|
|
1268
|
+
if (frame.kind === "obj") {
|
|
1269
|
+
if (frame.index >= frame.entries.length) {
|
|
1270
|
+
stack.pop();
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
const [key, childValue] = frame.entries[frame.index++];
|
|
1274
|
+
const childDepth = frame.depth + 1;
|
|
1275
|
+
assertTraversalDepth(childDepth);
|
|
1276
|
+
const entryDot = nextDot();
|
|
1277
|
+
if (isJsonPrimitive(childValue)) {
|
|
1278
|
+
objSet(frame.target, key, newReg(childValue, nextDot()), entryDot);
|
|
1279
|
+
continue;
|
|
1280
|
+
}
|
|
1281
|
+
if (Array.isArray(childValue)) {
|
|
1282
|
+
const childSeq = newSeq();
|
|
1283
|
+
objSet(frame.target, key, childSeq, entryDot);
|
|
1284
|
+
stack.push({
|
|
1285
|
+
kind: "seq",
|
|
1286
|
+
depth: childDepth,
|
|
1287
|
+
values: childValue,
|
|
1288
|
+
index: 0,
|
|
1289
|
+
prev: HEAD,
|
|
1290
|
+
target: childSeq
|
|
1291
|
+
});
|
|
1292
|
+
continue;
|
|
1293
|
+
}
|
|
1294
|
+
const childObj = newObj();
|
|
1295
|
+
objSet(frame.target, key, childObj, entryDot);
|
|
1296
|
+
stack.push({
|
|
1297
|
+
kind: "obj",
|
|
1298
|
+
depth: childDepth,
|
|
1299
|
+
entries: Object.entries(childValue),
|
|
1300
|
+
index: 0,
|
|
1301
|
+
target: childObj
|
|
1302
|
+
});
|
|
1303
|
+
continue;
|
|
785
1304
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
const
|
|
791
|
-
|
|
1305
|
+
if (frame.index >= frame.values.length) {
|
|
1306
|
+
stack.pop();
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1309
|
+
const childValue = frame.values[frame.index++];
|
|
1310
|
+
const childDepth = frame.depth + 1;
|
|
1311
|
+
assertTraversalDepth(childDepth);
|
|
1312
|
+
const insDot = nextDot();
|
|
1313
|
+
const id = dotToElemId(insDot);
|
|
1314
|
+
if (isJsonPrimitive(childValue)) {
|
|
1315
|
+
rgaInsertAfter(frame.target, frame.prev, id, insDot, newReg(childValue, nextDot()));
|
|
1316
|
+
frame.prev = id;
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
if (Array.isArray(childValue)) {
|
|
1320
|
+
const childSeq = newSeq();
|
|
1321
|
+
rgaInsertAfter(frame.target, frame.prev, id, insDot, childSeq);
|
|
1322
|
+
frame.prev = id;
|
|
1323
|
+
stack.push({
|
|
1324
|
+
kind: "seq",
|
|
1325
|
+
depth: childDepth,
|
|
1326
|
+
values: childValue,
|
|
1327
|
+
index: 0,
|
|
1328
|
+
prev: HEAD,
|
|
1329
|
+
target: childSeq
|
|
1330
|
+
});
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
const childObj = newObj();
|
|
1334
|
+
rgaInsertAfter(frame.target, frame.prev, id, insDot, childObj);
|
|
1335
|
+
frame.prev = id;
|
|
1336
|
+
stack.push({
|
|
1337
|
+
kind: "obj",
|
|
1338
|
+
depth: childDepth,
|
|
1339
|
+
entries: Object.entries(childValue),
|
|
1340
|
+
index: 0,
|
|
1341
|
+
target: childObj
|
|
1342
|
+
});
|
|
792
1343
|
}
|
|
793
|
-
return
|
|
1344
|
+
return root;
|
|
794
1345
|
}
|
|
795
1346
|
/** Deep-clone a CRDT document. The clone is fully independent of the original. */
|
|
796
1347
|
function cloneDoc(doc) {
|
|
797
1348
|
return { root: cloneNode(doc.root) };
|
|
798
1349
|
}
|
|
799
1350
|
function cloneNode(node) {
|
|
1351
|
+
return cloneNodeAtDepth(node, 0);
|
|
1352
|
+
}
|
|
1353
|
+
function cloneNodeAtDepth(node, depth) {
|
|
1354
|
+
assertTraversalDepth(depth);
|
|
800
1355
|
if (node.kind === "lww") return {
|
|
801
1356
|
kind: "lww",
|
|
802
1357
|
value: structuredClone(node.value),
|
|
@@ -808,7 +1363,7 @@ function cloneNode(node) {
|
|
|
808
1363
|
if (node.kind === "obj") {
|
|
809
1364
|
const entries = /* @__PURE__ */ new Map();
|
|
810
1365
|
for (const [k, v] of node.entries.entries()) entries.set(k, {
|
|
811
|
-
node:
|
|
1366
|
+
node: cloneNodeAtDepth(v.node, depth + 1),
|
|
812
1367
|
dot: {
|
|
813
1368
|
actor: v.dot.actor,
|
|
814
1369
|
ctr: v.dot.ctr
|
|
@@ -830,7 +1385,7 @@ function cloneNode(node) {
|
|
|
830
1385
|
id: e.id,
|
|
831
1386
|
prev: e.prev,
|
|
832
1387
|
tombstone: e.tombstone,
|
|
833
|
-
value:
|
|
1388
|
+
value: cloneNodeAtDepth(e.value, depth + 1),
|
|
834
1389
|
insDot: {
|
|
835
1390
|
actor: e.insDot.actor,
|
|
836
1391
|
ctr: e.insDot.ctr
|
|
@@ -841,6 +1396,9 @@ function cloneNode(node) {
|
|
|
841
1396
|
elems
|
|
842
1397
|
};
|
|
843
1398
|
}
|
|
1399
|
+
function isJsonPrimitive(value) {
|
|
1400
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
1401
|
+
}
|
|
844
1402
|
function applyTest(base, head, it, evalTestAgainst) {
|
|
845
1403
|
const snapshot = evalTestAgainst === "head" ? materialize(head.root) : materialize(base.root);
|
|
846
1404
|
let got;
|
|
@@ -910,13 +1468,23 @@ function applyObjRemove(head, it, newDot) {
|
|
|
910
1468
|
objRemove(parentObj, it.key, d);
|
|
911
1469
|
return null;
|
|
912
1470
|
}
|
|
913
|
-
function applyArrInsert(base, head, it, newDot, bumpCounterAbove) {
|
|
1471
|
+
function applyArrInsert(base, head, it, newDot, bumpCounterAbove, strictParents = false) {
|
|
1472
|
+
const pointer = `/${it.path.join("/")}`;
|
|
914
1473
|
const baseSeq = getSeqAtPath(base, it.path);
|
|
915
1474
|
if (!baseSeq) {
|
|
1475
|
+
if (strictParents) return {
|
|
1476
|
+
ok: false,
|
|
1477
|
+
code: 409,
|
|
1478
|
+
reason: "MISSING_PARENT",
|
|
1479
|
+
message: `base array missing at /${it.path.join("/")}`,
|
|
1480
|
+
path: pointer
|
|
1481
|
+
};
|
|
916
1482
|
if (it.index === 0 || it.index === Number.POSITIVE_INFINITY) {
|
|
917
1483
|
const headSeq = ensureSeqAtPath(head, it.path, newDot());
|
|
918
1484
|
const prev = it.index === 0 ? HEAD : rgaPrevForInsertAtIndex(headSeq, Number.MAX_SAFE_INTEGER);
|
|
919
|
-
const
|
|
1485
|
+
const dotRes = nextInsertDotForPrev(headSeq, prev, newDot, pointer, bumpCounterAbove);
|
|
1486
|
+
if (!dotRes.ok) return dotRes;
|
|
1487
|
+
const d = dotRes.dot;
|
|
920
1488
|
rgaInsertAfter(headSeq, prev, dotToElemId(d), d, nodeFromJson(it.value, newDot));
|
|
921
1489
|
return null;
|
|
922
1490
|
}
|
|
@@ -925,10 +1493,13 @@ function applyArrInsert(base, head, it, newDot, bumpCounterAbove) {
|
|
|
925
1493
|
code: 409,
|
|
926
1494
|
reason: "MISSING_PARENT",
|
|
927
1495
|
message: `base array missing at /${it.path.join("/")}`,
|
|
928
|
-
path:
|
|
1496
|
+
path: pointer
|
|
929
1497
|
};
|
|
930
1498
|
}
|
|
931
|
-
|
|
1499
|
+
newDot();
|
|
1500
|
+
const headSeqRes = getHeadSeqForBaseArrayIntent(head, it.path);
|
|
1501
|
+
if (!headSeqRes.ok) return headSeqRes;
|
|
1502
|
+
const headSeq = headSeqRes.seq;
|
|
932
1503
|
const idx = it.index === Number.POSITIVE_INFINITY ? rgaLinearizeIds(baseSeq).length : it.index;
|
|
933
1504
|
const baseLen = rgaLinearizeIds(baseSeq).length;
|
|
934
1505
|
if (idx < 0 || idx > baseLen) return {
|
|
@@ -939,23 +1510,41 @@ function applyArrInsert(base, head, it, newDot, bumpCounterAbove) {
|
|
|
939
1510
|
path: `/${it.path.join("/")}/${it.index}`
|
|
940
1511
|
};
|
|
941
1512
|
const prev = idx === 0 ? HEAD : rgaIdAtIndex(baseSeq, idx - 1) ?? HEAD;
|
|
942
|
-
const
|
|
1513
|
+
const dotRes = nextInsertDotForPrev(headSeq, prev, newDot, pointer, bumpCounterAbove);
|
|
1514
|
+
if (!dotRes.ok) return dotRes;
|
|
1515
|
+
const d = dotRes.dot;
|
|
943
1516
|
rgaInsertAfter(headSeq, prev, dotToElemId(d), d, nodeFromJson(it.value, newDot));
|
|
944
1517
|
return null;
|
|
945
1518
|
}
|
|
946
|
-
function nextInsertDotForPrev(seq, prev, newDot, bumpCounterAbove) {
|
|
1519
|
+
function nextInsertDotForPrev(seq, prev, newDot, path, bumpCounterAbove) {
|
|
1520
|
+
const MAX_INSERT_DOT_ATTEMPTS = 1024;
|
|
947
1521
|
let maxSiblingDot = null;
|
|
948
1522
|
for (const elem of seq.elems.values()) {
|
|
949
1523
|
if (elem.prev !== prev) continue;
|
|
950
1524
|
if (!maxSiblingDot || compareDot(elem.insDot, maxSiblingDot) > 0) maxSiblingDot = elem.insDot;
|
|
951
1525
|
}
|
|
952
1526
|
if (maxSiblingDot) bumpCounterAbove?.(maxSiblingDot.ctr);
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1527
|
+
if (!maxSiblingDot) return {
|
|
1528
|
+
ok: true,
|
|
1529
|
+
dot: newDot()
|
|
1530
|
+
};
|
|
1531
|
+
for (let attempts = 0; attempts < MAX_INSERT_DOT_ATTEMPTS; attempts++) {
|
|
1532
|
+
const candidate = newDot();
|
|
1533
|
+
if (compareDot(candidate, maxSiblingDot) > 0) return {
|
|
1534
|
+
ok: true,
|
|
1535
|
+
dot: candidate
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
return {
|
|
1539
|
+
ok: false,
|
|
1540
|
+
code: 409,
|
|
1541
|
+
reason: "DOT_GENERATION_EXHAUSTED",
|
|
1542
|
+
message: `failed to generate insert dot within ${MAX_INSERT_DOT_ATTEMPTS} attempts`,
|
|
1543
|
+
path
|
|
1544
|
+
};
|
|
956
1545
|
}
|
|
957
1546
|
function applyArrDelete(base, head, it, newDot) {
|
|
958
|
-
|
|
1547
|
+
newDot();
|
|
959
1548
|
const baseSeq = getSeqAtPath(base, it.path);
|
|
960
1549
|
if (!baseSeq) return {
|
|
961
1550
|
ok: false,
|
|
@@ -964,7 +1553,9 @@ function applyArrDelete(base, head, it, newDot) {
|
|
|
964
1553
|
message: `base array missing at /${it.path.join("/")}`,
|
|
965
1554
|
path: `/${it.path.join("/")}`
|
|
966
1555
|
};
|
|
967
|
-
const
|
|
1556
|
+
const headSeqRes = getHeadSeqForBaseArrayIntent(head, it.path);
|
|
1557
|
+
if (!headSeqRes.ok) return headSeqRes;
|
|
1558
|
+
const headSeq = headSeqRes.seq;
|
|
968
1559
|
const baseId = rgaIdAtIndex(baseSeq, it.index);
|
|
969
1560
|
if (!baseId) return {
|
|
970
1561
|
ok: false,
|
|
@@ -973,11 +1564,18 @@ function applyArrDelete(base, head, it, newDot) {
|
|
|
973
1564
|
message: `no base element at index ${it.index}`,
|
|
974
1565
|
path: `/${it.path.join("/")}/${it.index}`
|
|
975
1566
|
};
|
|
1567
|
+
if (!headSeq.elems.get(baseId)) return {
|
|
1568
|
+
ok: false,
|
|
1569
|
+
code: 409,
|
|
1570
|
+
reason: "MISSING_TARGET",
|
|
1571
|
+
message: `element missing in head lineage at index ${it.index}`,
|
|
1572
|
+
path: `/${it.path.join("/")}/${it.index}`
|
|
1573
|
+
};
|
|
976
1574
|
rgaDelete(headSeq, baseId);
|
|
977
1575
|
return null;
|
|
978
1576
|
}
|
|
979
1577
|
function applyArrReplace(base, head, it, newDot) {
|
|
980
|
-
|
|
1578
|
+
newDot();
|
|
981
1579
|
const baseSeq = getSeqAtPath(base, it.path);
|
|
982
1580
|
if (!baseSeq) return {
|
|
983
1581
|
ok: false,
|
|
@@ -986,7 +1584,9 @@ function applyArrReplace(base, head, it, newDot) {
|
|
|
986
1584
|
message: `base array missing at /${it.path.join("/")}`,
|
|
987
1585
|
path: `/${it.path.join("/")}`
|
|
988
1586
|
};
|
|
989
|
-
const
|
|
1587
|
+
const headSeqRes = getHeadSeqForBaseArrayIntent(head, it.path);
|
|
1588
|
+
if (!headSeqRes.ok) return headSeqRes;
|
|
1589
|
+
const headSeq = headSeqRes.seq;
|
|
990
1590
|
const baseId = rgaIdAtIndex(baseSeq, it.index);
|
|
991
1591
|
if (!baseId) return {
|
|
992
1592
|
ok: false,
|
|
@@ -1015,9 +1615,11 @@ function applyArrReplace(base, head, it, newDot) {
|
|
|
1015
1615
|
* @param newDot - A function that generates a unique `Dot` per mutation.
|
|
1016
1616
|
* @param evalTestAgainst - Whether `test` ops are evaluated against `"head"` or `"base"`.
|
|
1017
1617
|
* @param bumpCounterAbove - Optional hook that can fast-forward the underlying counter before inserts.
|
|
1618
|
+
* @param options - Optional behavior toggles.
|
|
1619
|
+
* @param options.strictParents - When `true`, reject array inserts whose base parent path is missing.
|
|
1018
1620
|
* @returns `{ ok: true }` on success, or `{ ok: false, code: 409, message }` on conflict.
|
|
1019
1621
|
*/
|
|
1020
|
-
function applyIntentsToCrdt(base, head, intents, newDot, evalTestAgainst = "head", bumpCounterAbove) {
|
|
1622
|
+
function applyIntentsToCrdt(base, head, intents, newDot, evalTestAgainst = "head", bumpCounterAbove, options = {}) {
|
|
1021
1623
|
for (const it of intents) {
|
|
1022
1624
|
let fail = null;
|
|
1023
1625
|
switch (it.t) {
|
|
@@ -1031,7 +1633,7 @@ function applyIntentsToCrdt(base, head, intents, newDot, evalTestAgainst = "head
|
|
|
1031
1633
|
fail = applyObjRemove(head, it, newDot);
|
|
1032
1634
|
break;
|
|
1033
1635
|
case "ArrInsert":
|
|
1034
|
-
fail = applyArrInsert(base, head, it, newDot, bumpCounterAbove);
|
|
1636
|
+
fail = applyArrInsert(base, head, it, newDot, bumpCounterAbove, options.strictParents ?? false);
|
|
1035
1637
|
break;
|
|
1036
1638
|
case "ArrDelete":
|
|
1037
1639
|
fail = applyArrDelete(base, head, it, newDot);
|
|
@@ -1045,7 +1647,7 @@ function applyIntentsToCrdt(base, head, intents, newDot, evalTestAgainst = "head
|
|
|
1045
1647
|
}
|
|
1046
1648
|
return { ok: true };
|
|
1047
1649
|
}
|
|
1048
|
-
function jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove) {
|
|
1650
|
+
function jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove, strictParents = false) {
|
|
1049
1651
|
if (isJsonPatchToCrdtOptions(baseOrOptions)) return jsonPatchToCrdtInternal(baseOrOptions);
|
|
1050
1652
|
if (!head || !patch || !newDot) return {
|
|
1051
1653
|
ok: false,
|
|
@@ -1059,10 +1661,11 @@ function jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst = "
|
|
|
1059
1661
|
patch,
|
|
1060
1662
|
newDot,
|
|
1061
1663
|
evalTestAgainst,
|
|
1062
|
-
bumpCounterAbove
|
|
1664
|
+
bumpCounterAbove,
|
|
1665
|
+
strictParents
|
|
1063
1666
|
});
|
|
1064
1667
|
}
|
|
1065
|
-
function jsonPatchToCrdtSafe(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove) {
|
|
1668
|
+
function jsonPatchToCrdtSafe(baseOrOptions, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove, strictParents = false) {
|
|
1066
1669
|
try {
|
|
1067
1670
|
if (isJsonPatchToCrdtOptions(baseOrOptions)) return jsonPatchToCrdt(baseOrOptions);
|
|
1068
1671
|
if (!head || !patch || !newDot) return {
|
|
@@ -1071,7 +1674,7 @@ function jsonPatchToCrdtSafe(baseOrOptions, head, patch, newDot, evalTestAgainst
|
|
|
1071
1674
|
reason: "INVALID_PATCH",
|
|
1072
1675
|
message: "invalid jsonPatchToCrdtSafe call signature"
|
|
1073
1676
|
};
|
|
1074
|
-
return jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst, bumpCounterAbove);
|
|
1677
|
+
return jsonPatchToCrdt(baseOrOptions, head, patch, newDot, evalTestAgainst, bumpCounterAbove, strictParents);
|
|
1075
1678
|
} catch (error) {
|
|
1076
1679
|
return toApplyError$1(error);
|
|
1077
1680
|
}
|
|
@@ -1109,7 +1712,7 @@ function jsonPatchToCrdtInternal(options) {
|
|
|
1109
1712
|
} catch (error) {
|
|
1110
1713
|
return toApplyError$1(error);
|
|
1111
1714
|
}
|
|
1112
|
-
return applyIntentsToCrdt(options.base, options.head, intents, options.newDot, evalTestAgainst, options.bumpCounterAbove);
|
|
1715
|
+
return applyIntentsToCrdt(options.base, options.head, intents, options.newDot, evalTestAgainst, options.bumpCounterAbove, { strictParents: options.strictParents });
|
|
1113
1716
|
}
|
|
1114
1717
|
let shadowBase = cloneDoc(evalTestAgainst === "base" ? options.base : options.head);
|
|
1115
1718
|
let shadowCtr = 0;
|
|
@@ -1128,10 +1731,10 @@ function jsonPatchToCrdtInternal(options) {
|
|
|
1128
1731
|
} catch (error) {
|
|
1129
1732
|
return withOpIndex(toApplyError$1(error), opIndex);
|
|
1130
1733
|
}
|
|
1131
|
-
const headStep = applyIntentsToCrdt(shadowBase, options.head, intents, options.newDot, evalTestAgainst, options.bumpCounterAbove);
|
|
1734
|
+
const headStep = applyIntentsToCrdt(shadowBase, options.head, intents, options.newDot, evalTestAgainst, options.bumpCounterAbove, { strictParents: options.strictParents });
|
|
1132
1735
|
if (!headStep.ok) return withOpIndex(headStep, opIndex);
|
|
1133
1736
|
if (evalTestAgainst === "base") {
|
|
1134
|
-
const shadowStep = applyIntentsToCrdt(shadowBase, shadowBase, intents, shadowDot, "base", shadowBump);
|
|
1737
|
+
const shadowStep = applyIntentsToCrdt(shadowBase, shadowBase, intents, shadowDot, "base", shadowBump, { strictParents: options.strictParents });
|
|
1135
1738
|
if (!shadowStep.ok) return withOpIndex(shadowStep, opIndex);
|
|
1136
1739
|
} else shadowBase = cloneDoc(options.head);
|
|
1137
1740
|
return { ok: true };
|
|
@@ -1184,6 +1787,7 @@ function isJsonPatchToCrdtOptions(value) {
|
|
|
1184
1787
|
return typeof value === "object" && value !== null && "base" in value && "head" in value && "patch" in value && "newDot" in value;
|
|
1185
1788
|
}
|
|
1186
1789
|
function toApplyError$1(error) {
|
|
1790
|
+
if (error instanceof TraversalDepthError) return toDepthApplyError(error);
|
|
1187
1791
|
if (error instanceof PatchCompileError) return {
|
|
1188
1792
|
ok: false,
|
|
1189
1793
|
code: 409,
|
|
@@ -1234,7 +1838,7 @@ var PatchError = class extends Error {
|
|
|
1234
1838
|
function createState(initial, options) {
|
|
1235
1839
|
const clock = createClock(options.actor, options.start ?? 0);
|
|
1236
1840
|
return {
|
|
1237
|
-
doc: docFromJson(initial, clock.next),
|
|
1841
|
+
doc: docFromJson(coerceRuntimeJsonValue(initial, options.jsonValidation ?? "none"), clock.next),
|
|
1238
1842
|
clock
|
|
1239
1843
|
};
|
|
1240
1844
|
}
|
|
@@ -1289,11 +1893,18 @@ function tryApplyPatch(state, patch, options = {}) {
|
|
|
1289
1893
|
doc: cloneDoc(state.doc),
|
|
1290
1894
|
clock: cloneClock(state.clock)
|
|
1291
1895
|
};
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
ok
|
|
1295
|
-
|
|
1296
|
-
|
|
1896
|
+
try {
|
|
1897
|
+
const result = applyPatchInternal(nextState, patch, options, "batch");
|
|
1898
|
+
if (!result.ok) return {
|
|
1899
|
+
ok: false,
|
|
1900
|
+
error: result
|
|
1901
|
+
};
|
|
1902
|
+
} catch (error) {
|
|
1903
|
+
return {
|
|
1904
|
+
ok: false,
|
|
1905
|
+
error: toApplyError(error)
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1297
1908
|
return {
|
|
1298
1909
|
ok: true,
|
|
1299
1910
|
state: nextState
|
|
@@ -1309,11 +1920,18 @@ function tryApplyPatchInPlace(state, patch, options = {}) {
|
|
|
1309
1920
|
state.clock = next.state.clock;
|
|
1310
1921
|
return { ok: true };
|
|
1311
1922
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
ok
|
|
1315
|
-
|
|
1316
|
-
|
|
1923
|
+
try {
|
|
1924
|
+
const result = applyPatchInternal(state, patch, applyOptions, "step");
|
|
1925
|
+
if (!result.ok) return {
|
|
1926
|
+
ok: false,
|
|
1927
|
+
error: result
|
|
1928
|
+
};
|
|
1929
|
+
} catch (error) {
|
|
1930
|
+
return {
|
|
1931
|
+
ok: false,
|
|
1932
|
+
error: toApplyError(error)
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1317
1935
|
return { ok: true };
|
|
1318
1936
|
}
|
|
1319
1937
|
/**
|
|
@@ -1321,7 +1939,10 @@ function tryApplyPatchInPlace(state, patch, options = {}) {
|
|
|
1321
1939
|
* Does not mutate caller-provided values.
|
|
1322
1940
|
*/
|
|
1323
1941
|
function validateJsonPatch(base, patch, options = {}) {
|
|
1324
|
-
const result = tryApplyPatch(createState(base, {
|
|
1942
|
+
const result = tryApplyPatch(createState(base, {
|
|
1943
|
+
actor: "__validate__",
|
|
1944
|
+
jsonValidation: options.jsonValidation
|
|
1945
|
+
}), patch, options);
|
|
1325
1946
|
if (!result.ok) return {
|
|
1326
1947
|
ok: false,
|
|
1327
1948
|
error: result.error
|
|
@@ -1350,35 +1971,43 @@ function toApplyPatchOptionsForActor(options) {
|
|
|
1350
1971
|
return {
|
|
1351
1972
|
semantics: options.semantics,
|
|
1352
1973
|
testAgainst: options.testAgainst,
|
|
1974
|
+
strictParents: options.strictParents,
|
|
1975
|
+
jsonValidation: options.jsonValidation,
|
|
1353
1976
|
base: options.base ? {
|
|
1354
1977
|
doc: options.base,
|
|
1355
1978
|
clock: createClock("__base__", 0)
|
|
1356
1979
|
} : void 0
|
|
1357
1980
|
};
|
|
1358
1981
|
}
|
|
1359
|
-
function applyPatchInternal(state, patch, options) {
|
|
1982
|
+
function applyPatchInternal(state, patch, options, execution) {
|
|
1360
1983
|
if ((options.semantics ?? "sequential") === "sequential") {
|
|
1984
|
+
if (!options.base && execution === "batch") {
|
|
1985
|
+
const compiled = compileIntents(materialize(state.doc.root), patch, "sequential", options.jsonValidation ?? "none");
|
|
1986
|
+
if (!compiled.ok) return compiled;
|
|
1987
|
+
return applyIntentsToCrdt(state.doc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
|
|
1988
|
+
}
|
|
1361
1989
|
const explicitBaseState = options.base ? {
|
|
1362
1990
|
doc: cloneDoc(options.base.doc),
|
|
1363
1991
|
clock: createClock("__base__", 0)
|
|
1364
1992
|
} : null;
|
|
1365
1993
|
for (const [opIndex, op] of patch.entries()) {
|
|
1366
|
-
const step = applyPatchOpSequential(state, op, options, explicitBaseState ? explicitBaseState.doc :
|
|
1994
|
+
const step = applyPatchOpSequential(state, op, options, explicitBaseState ? explicitBaseState.doc : state.doc, opIndex);
|
|
1367
1995
|
if (!step.ok) return step;
|
|
1368
1996
|
if (explicitBaseState && op.op !== "test") {
|
|
1369
1997
|
const baseStep = applyPatchInternal(explicitBaseState, [op], {
|
|
1370
1998
|
semantics: "sequential",
|
|
1371
|
-
testAgainst: "base"
|
|
1372
|
-
|
|
1999
|
+
testAgainst: "base",
|
|
2000
|
+
strictParents: options.strictParents
|
|
2001
|
+
}, "step");
|
|
1373
2002
|
if (!baseStep.ok) return baseStep;
|
|
1374
2003
|
}
|
|
1375
2004
|
}
|
|
1376
2005
|
return { ok: true };
|
|
1377
2006
|
}
|
|
1378
2007
|
const baseDoc = options.base ? options.base.doc : cloneDoc(state.doc);
|
|
1379
|
-
const compiled = compileIntents(materialize(baseDoc.root), patch, "base");
|
|
2008
|
+
const compiled = compileIntents(materialize(baseDoc.root), patch, "base", options.jsonValidation ?? "none");
|
|
1380
2009
|
if (!compiled.ok) return compiled;
|
|
1381
|
-
return applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr));
|
|
2010
|
+
return applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
|
|
1382
2011
|
}
|
|
1383
2012
|
function applyPatchOpSequential(state, op, options, baseDoc, opIndex) {
|
|
1384
2013
|
const baseJson = materialize(baseDoc.root);
|
|
@@ -1386,12 +2015,13 @@ function applyPatchOpSequential(state, op, options, baseDoc, opIndex) {
|
|
|
1386
2015
|
const fromResolved = resolveValueAtPointer(baseJson, op.from, opIndex);
|
|
1387
2016
|
if (!fromResolved.ok) return fromResolved;
|
|
1388
2017
|
const fromValue = fromResolved.value;
|
|
1389
|
-
const removeRes = applySinglePatchOp(state, baseDoc, {
|
|
2018
|
+
const removeRes = applySinglePatchOp(state, baseDoc, baseJson, {
|
|
1390
2019
|
op: "remove",
|
|
1391
2020
|
path: op.from
|
|
1392
2021
|
}, options);
|
|
1393
2022
|
if (!removeRes.ok) return removeRes;
|
|
1394
|
-
|
|
2023
|
+
const addBase = state.doc;
|
|
2024
|
+
return applySinglePatchOp(state, addBase, materialize(addBase.root), {
|
|
1395
2025
|
op: "add",
|
|
1396
2026
|
path: op.path,
|
|
1397
2027
|
value: fromValue
|
|
@@ -1401,13 +2031,13 @@ function applyPatchOpSequential(state, op, options, baseDoc, opIndex) {
|
|
|
1401
2031
|
const fromResolved = resolveValueAtPointer(baseJson, op.from, opIndex);
|
|
1402
2032
|
if (!fromResolved.ok) return fromResolved;
|
|
1403
2033
|
const fromValue = fromResolved.value;
|
|
1404
|
-
return applySinglePatchOp(state, baseDoc, {
|
|
2034
|
+
return applySinglePatchOp(state, baseDoc, baseJson, {
|
|
1405
2035
|
op: "add",
|
|
1406
2036
|
path: op.path,
|
|
1407
2037
|
value: fromValue
|
|
1408
2038
|
}, options);
|
|
1409
2039
|
}
|
|
1410
|
-
return applySinglePatchOp(state, baseDoc, op, options);
|
|
2040
|
+
return applySinglePatchOp(state, baseDoc, baseJson, op, options);
|
|
1411
2041
|
}
|
|
1412
2042
|
function resolveValueAtPointer(baseJson, pointer, opIndex) {
|
|
1413
2043
|
let path;
|
|
@@ -1425,49 +2055,94 @@ function resolveValueAtPointer(baseJson, pointer, opIndex) {
|
|
|
1425
2055
|
return toPointerLookupApplyError(error, pointer, opIndex);
|
|
1426
2056
|
}
|
|
1427
2057
|
}
|
|
1428
|
-
function applySinglePatchOp(state, baseDoc, op, options) {
|
|
1429
|
-
const compiled = compileIntents(
|
|
2058
|
+
function applySinglePatchOp(state, baseDoc, baseJson, op, options) {
|
|
2059
|
+
const compiled = compileIntents(baseJson, [op], "sequential", options.jsonValidation ?? "none");
|
|
1430
2060
|
if (!compiled.ok) return compiled;
|
|
1431
|
-
return applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr));
|
|
2061
|
+
return applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
|
|
1432
2062
|
}
|
|
1433
2063
|
function bumpClockCounter(state, ctr) {
|
|
1434
2064
|
if (state.clock.ctr < ctr) state.clock.ctr = ctr;
|
|
1435
2065
|
}
|
|
1436
|
-
function compileIntents(baseJson, patch, semantics = "sequential") {
|
|
2066
|
+
function compileIntents(baseJson, patch, semantics = "sequential", jsonValidation = "none") {
|
|
1437
2067
|
try {
|
|
1438
2068
|
return {
|
|
1439
2069
|
ok: true,
|
|
1440
|
-
intents: compileJsonPatchToIntent(baseJson, patch, { semantics })
|
|
2070
|
+
intents: compileJsonPatchToIntent(baseJson, preparePatchPayloads(patch, jsonValidation), { semantics })
|
|
1441
2071
|
};
|
|
1442
2072
|
} catch (error) {
|
|
1443
2073
|
return toApplyError(error);
|
|
1444
2074
|
}
|
|
1445
2075
|
}
|
|
2076
|
+
function preparePatchPayloads(patch, mode) {
|
|
2077
|
+
if (mode === "none") return patch;
|
|
2078
|
+
const out = [];
|
|
2079
|
+
for (const [opIndex, op] of patch.entries()) {
|
|
2080
|
+
if (op.op === "move" || op.op === "copy" || op.op === "remove") {
|
|
2081
|
+
out.push(op);
|
|
2082
|
+
continue;
|
|
2083
|
+
}
|
|
2084
|
+
if (mode === "strict") {
|
|
2085
|
+
try {
|
|
2086
|
+
assertRuntimeJsonValue(op.value);
|
|
2087
|
+
} catch (error) {
|
|
2088
|
+
if (error instanceof JsonValueValidationError) throw patchPayloadCompileError(op, opIndex, error);
|
|
2089
|
+
throw error;
|
|
2090
|
+
}
|
|
2091
|
+
out.push(op);
|
|
2092
|
+
continue;
|
|
2093
|
+
}
|
|
2094
|
+
out.push({
|
|
2095
|
+
...op,
|
|
2096
|
+
value: coerceRuntimeJsonValue(op.value, mode)
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
return out;
|
|
2100
|
+
}
|
|
2101
|
+
function patchPayloadCompileError(op, opIndex, error) {
|
|
2102
|
+
const path = mergePointerPaths(op.path, error.path);
|
|
2103
|
+
return new PatchCompileError("INVALID_PATCH", `invalid JSON value for '${op.op}' at ${path === "" ? "<root>" : path}: ${error.detail}`, path, opIndex);
|
|
2104
|
+
}
|
|
2105
|
+
function mergePointerPaths(basePointer, nestedPointer) {
|
|
2106
|
+
if (nestedPointer === "") return basePointer;
|
|
2107
|
+
if (basePointer === "") return nestedPointer;
|
|
2108
|
+
return `${basePointer}${nestedPointer}`;
|
|
2109
|
+
}
|
|
1446
2110
|
function maxCtrInNodeForActor$1(node, actor) {
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
2111
|
+
let best = 0;
|
|
2112
|
+
const stack = [{
|
|
2113
|
+
node,
|
|
2114
|
+
depth: 0
|
|
2115
|
+
}];
|
|
2116
|
+
while (stack.length > 0) {
|
|
2117
|
+
const frame = stack.pop();
|
|
2118
|
+
assertTraversalDepth(frame.depth);
|
|
2119
|
+
if (frame.node.kind === "lww") {
|
|
2120
|
+
if (frame.node.dot.actor === actor && frame.node.dot.ctr > best) best = frame.node.dot.ctr;
|
|
2121
|
+
continue;
|
|
2122
|
+
}
|
|
2123
|
+
if (frame.node.kind === "obj") {
|
|
2124
|
+
for (const entry of frame.node.entries.values()) {
|
|
1452
2125
|
if (entry.dot.actor === actor && entry.dot.ctr > best) best = entry.dot.ctr;
|
|
1453
|
-
|
|
1454
|
-
|
|
2126
|
+
stack.push({
|
|
2127
|
+
node: entry.node,
|
|
2128
|
+
depth: frame.depth + 1
|
|
2129
|
+
});
|
|
1455
2130
|
}
|
|
1456
|
-
for (const tomb of node.tombstone.values()) if (tomb.actor === actor && tomb.ctr > best) best = tomb.ctr;
|
|
1457
|
-
|
|
2131
|
+
for (const tomb of frame.node.tombstone.values()) if (tomb.actor === actor && tomb.ctr > best) best = tomb.ctr;
|
|
2132
|
+
continue;
|
|
1458
2133
|
}
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
}
|
|
1466
|
-
return best;
|
|
2134
|
+
for (const elem of frame.node.elems.values()) {
|
|
2135
|
+
if (elem.insDot.actor === actor && elem.insDot.ctr > best) best = elem.insDot.ctr;
|
|
2136
|
+
stack.push({
|
|
2137
|
+
node: elem.value,
|
|
2138
|
+
depth: frame.depth + 1
|
|
2139
|
+
});
|
|
1467
2140
|
}
|
|
1468
2141
|
}
|
|
2142
|
+
return best;
|
|
1469
2143
|
}
|
|
1470
2144
|
function toApplyError(error) {
|
|
2145
|
+
if (error instanceof TraversalDepthError) return toDepthApplyError(error);
|
|
1471
2146
|
if (error instanceof PatchCompileError) return {
|
|
1472
2147
|
ok: false,
|
|
1473
2148
|
code: 409,
|
|
@@ -1507,13 +2182,38 @@ function toPointerLookupApplyError(error, pointer, opIndex) {
|
|
|
1507
2182
|
|
|
1508
2183
|
//#endregion
|
|
1509
2184
|
//#region src/serialize.ts
|
|
2185
|
+
const HEAD_ELEM_ID = "HEAD";
|
|
2186
|
+
function createSerializedRecord() {
|
|
2187
|
+
return Object.create(null);
|
|
2188
|
+
}
|
|
2189
|
+
function setSerializedRecordValue(out, key, value) {
|
|
2190
|
+
Object.defineProperty(out, key, {
|
|
2191
|
+
configurable: true,
|
|
2192
|
+
enumerable: true,
|
|
2193
|
+
value,
|
|
2194
|
+
writable: true
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
2197
|
+
var DeserializeError = class extends Error {
|
|
2198
|
+
code = 409;
|
|
2199
|
+
reason;
|
|
2200
|
+
path;
|
|
2201
|
+
constructor(reason, path, message) {
|
|
2202
|
+
super(message);
|
|
2203
|
+
this.name = "DeserializeError";
|
|
2204
|
+
this.reason = reason;
|
|
2205
|
+
this.path = path;
|
|
2206
|
+
}
|
|
2207
|
+
};
|
|
1510
2208
|
/** Serialize a CRDT document to a JSON-safe representation (Maps become plain objects). */
|
|
1511
2209
|
function serializeDoc(doc) {
|
|
1512
2210
|
return { root: serializeNode(doc.root) };
|
|
1513
2211
|
}
|
|
1514
2212
|
/** Reconstruct a CRDT document from its serialized form. */
|
|
1515
2213
|
function deserializeDoc(data) {
|
|
1516
|
-
|
|
2214
|
+
if (!isRecord(data)) fail("INVALID_SERIALIZED_SHAPE", "/", "serialized doc must be an object");
|
|
2215
|
+
if (!("root" in data)) fail("INVALID_SERIALIZED_SHAPE", "/root", "serialized doc is missing root");
|
|
2216
|
+
return { root: deserializeNode(data.root, "/root", 0) };
|
|
1517
2217
|
}
|
|
1518
2218
|
/** Serialize a full CRDT state (document + clock) to a JSON-safe representation. */
|
|
1519
2219
|
function serializeState(state) {
|
|
@@ -1527,7 +2227,11 @@ function serializeState(state) {
|
|
|
1527
2227
|
}
|
|
1528
2228
|
/** Reconstruct a full CRDT state from its serialized form, restoring the clock. */
|
|
1529
2229
|
function deserializeState(data) {
|
|
1530
|
-
|
|
2230
|
+
if (!isRecord(data)) fail("INVALID_SERIALIZED_SHAPE", "/", "serialized state must be an object");
|
|
2231
|
+
if (!("doc" in data)) fail("INVALID_SERIALIZED_SHAPE", "/doc", "serialized state is missing doc");
|
|
2232
|
+
if (!("clock" in data)) fail("INVALID_SERIALIZED_SHAPE", "/clock", "serialized state is missing clock");
|
|
2233
|
+
const clockRaw = asRecord(data.clock, "/clock");
|
|
2234
|
+
const clock = createClock(readActor(clockRaw.actor, "/clock/actor"), readCounter(clockRaw.ctr, "/clock/ctr"));
|
|
1531
2235
|
return {
|
|
1532
2236
|
doc: deserializeDoc(data.doc),
|
|
1533
2237
|
clock
|
|
@@ -1543,27 +2247,27 @@ function serializeNode(node) {
|
|
|
1543
2247
|
}
|
|
1544
2248
|
};
|
|
1545
2249
|
if (node.kind === "obj") {
|
|
1546
|
-
const entries =
|
|
1547
|
-
for (const [k, v] of node.entries.entries()) entries
|
|
2250
|
+
const entries = createSerializedRecord();
|
|
2251
|
+
for (const [k, v] of node.entries.entries()) setSerializedRecordValue(entries, k, {
|
|
1548
2252
|
node: serializeNode(v.node),
|
|
1549
2253
|
dot: {
|
|
1550
2254
|
actor: v.dot.actor,
|
|
1551
2255
|
ctr: v.dot.ctr
|
|
1552
2256
|
}
|
|
1553
|
-
};
|
|
1554
|
-
const tombstone =
|
|
1555
|
-
for (const [k, d] of node.tombstone.entries()) tombstone
|
|
2257
|
+
});
|
|
2258
|
+
const tombstone = createSerializedRecord();
|
|
2259
|
+
for (const [k, d] of node.tombstone.entries()) setSerializedRecordValue(tombstone, k, {
|
|
1556
2260
|
actor: d.actor,
|
|
1557
2261
|
ctr: d.ctr
|
|
1558
|
-
};
|
|
2262
|
+
});
|
|
1559
2263
|
return {
|
|
1560
2264
|
kind: "obj",
|
|
1561
2265
|
entries,
|
|
1562
2266
|
tombstone
|
|
1563
2267
|
};
|
|
1564
2268
|
}
|
|
1565
|
-
const elems =
|
|
1566
|
-
for (const [id, e] of node.elems.entries()) elems
|
|
2269
|
+
const elems = createSerializedRecord();
|
|
2270
|
+
for (const [id, e] of node.elems.entries()) setSerializedRecordValue(elems, id, {
|
|
1567
2271
|
id: e.id,
|
|
1568
2272
|
prev: e.prev,
|
|
1569
2273
|
tombstone: e.tombstone,
|
|
@@ -1572,60 +2276,138 @@ function serializeNode(node) {
|
|
|
1572
2276
|
actor: e.insDot.actor,
|
|
1573
2277
|
ctr: e.insDot.ctr
|
|
1574
2278
|
}
|
|
1575
|
-
};
|
|
2279
|
+
});
|
|
1576
2280
|
return {
|
|
1577
2281
|
kind: "seq",
|
|
1578
2282
|
elems
|
|
1579
2283
|
};
|
|
1580
2284
|
}
|
|
1581
|
-
function deserializeNode(node) {
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
2285
|
+
function deserializeNode(node, path, depth) {
|
|
2286
|
+
assertTraversalDepth(depth);
|
|
2287
|
+
const raw = asRecord(node, path);
|
|
2288
|
+
const kind = readString(raw.kind, `${path}/kind`);
|
|
2289
|
+
if (kind === "lww") {
|
|
2290
|
+
if (!("value" in raw)) fail("INVALID_SERIALIZED_SHAPE", `${path}/value`, "lww node is missing value");
|
|
2291
|
+
if (!("dot" in raw)) fail("INVALID_SERIALIZED_SHAPE", `${path}/dot`, "lww node is missing dot");
|
|
2292
|
+
return {
|
|
2293
|
+
kind: "lww",
|
|
2294
|
+
value: structuredClone(readJsonValue(raw.value, `${path}/value`, depth + 1)),
|
|
2295
|
+
dot: readDot(raw.dot, `${path}/dot`)
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
if (kind === "obj") {
|
|
2299
|
+
const entriesRaw = asRecord(raw.entries, `${path}/entries`);
|
|
2300
|
+
const tombstoneRaw = asRecord(raw.tombstone, `${path}/tombstone`);
|
|
1591
2301
|
const entries = /* @__PURE__ */ new Map();
|
|
1592
|
-
for (const [k, v] of Object.entries(
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
2302
|
+
for (const [k, v] of Object.entries(entriesRaw)) {
|
|
2303
|
+
const entryPath = `${path}/entries/${k}`;
|
|
2304
|
+
const entryRaw = asRecord(v, entryPath);
|
|
2305
|
+
entries.set(k, {
|
|
2306
|
+
node: deserializeNode(entryRaw.node, `${entryPath}/node`, depth + 1),
|
|
2307
|
+
dot: readDot(entryRaw.dot, `${entryPath}/dot`)
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
1599
2310
|
const tombstone = /* @__PURE__ */ new Map();
|
|
1600
|
-
for (const [k, d] of Object.entries(
|
|
1601
|
-
actor: d.actor,
|
|
1602
|
-
ctr: d.ctr
|
|
1603
|
-
});
|
|
2311
|
+
for (const [k, d] of Object.entries(tombstoneRaw)) tombstone.set(k, readDot(d, `${path}/tombstone/${k}`));
|
|
1604
2312
|
return {
|
|
1605
2313
|
kind: "obj",
|
|
1606
2314
|
entries,
|
|
1607
2315
|
tombstone
|
|
1608
2316
|
};
|
|
1609
2317
|
}
|
|
2318
|
+
if (kind !== "seq") fail("INVALID_SERIALIZED_SHAPE", `${path}/kind`, `unsupported node kind '${kind}'`);
|
|
2319
|
+
const elemsRaw = asRecord(raw.elems, `${path}/elems`);
|
|
1610
2320
|
const elems = /* @__PURE__ */ new Map();
|
|
1611
|
-
for (const [id,
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
}
|
|
1620
|
-
|
|
2321
|
+
for (const [id, rawElem] of Object.entries(elemsRaw)) {
|
|
2322
|
+
const elemPath = `${path}/elems/${id}`;
|
|
2323
|
+
const elem = asRecord(rawElem, elemPath);
|
|
2324
|
+
const elemId = readString(elem.id, `${elemPath}/id`);
|
|
2325
|
+
if (elemId !== id) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/id`, `sequence element id '${elemId}' does not match key '${id}'`);
|
|
2326
|
+
const prev = readString(elem.prev, `${elemPath}/prev`);
|
|
2327
|
+
const tombstone = readBoolean(elem.tombstone, `${elemPath}/tombstone`);
|
|
2328
|
+
const value = deserializeNode(elem.value, `${elemPath}/value`, depth + 1);
|
|
2329
|
+
const insDot = readDot(elem.insDot, `${elemPath}/insDot`);
|
|
2330
|
+
if (dotToElemId(insDot) !== id) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/insDot`, "sequence element id must match its insertion dot");
|
|
2331
|
+
elems.set(id, {
|
|
2332
|
+
id,
|
|
2333
|
+
prev,
|
|
2334
|
+
tombstone,
|
|
2335
|
+
value,
|
|
2336
|
+
insDot
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
2339
|
+
for (const elem of elems.values()) {
|
|
2340
|
+
if (elem.prev === elem.id) fail("INVALID_SERIALIZED_INVARIANT", `${path}/elems/${elem.id}/prev`, "sequence element cannot reference itself as predecessor");
|
|
2341
|
+
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`);
|
|
2342
|
+
}
|
|
1621
2343
|
return {
|
|
1622
2344
|
kind: "seq",
|
|
1623
2345
|
elems
|
|
1624
2346
|
};
|
|
1625
2347
|
}
|
|
2348
|
+
function asRecord(value, path) {
|
|
2349
|
+
if (!isRecord(value)) fail("INVALID_SERIALIZED_SHAPE", path, "expected object");
|
|
2350
|
+
return value;
|
|
2351
|
+
}
|
|
2352
|
+
function readDot(value, path) {
|
|
2353
|
+
const raw = asRecord(value, path);
|
|
2354
|
+
return {
|
|
2355
|
+
actor: readActor(raw.actor, `${path}/actor`),
|
|
2356
|
+
ctr: readCounter(raw.ctr, `${path}/ctr`)
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
function readActor(value, path) {
|
|
2360
|
+
const actor = readString(value, path);
|
|
2361
|
+
if (actor.length === 0) fail("INVALID_SERIALIZED_SHAPE", path, "actor must not be empty");
|
|
2362
|
+
return actor;
|
|
2363
|
+
}
|
|
2364
|
+
function readCounter(value, path) {
|
|
2365
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) fail("INVALID_SERIALIZED_SHAPE", path, "counter must be a non-negative safe integer");
|
|
2366
|
+
return value;
|
|
2367
|
+
}
|
|
2368
|
+
function readString(value, path) {
|
|
2369
|
+
if (typeof value !== "string") fail("INVALID_SERIALIZED_SHAPE", path, "expected string");
|
|
2370
|
+
return value;
|
|
2371
|
+
}
|
|
2372
|
+
function readBoolean(value, path) {
|
|
2373
|
+
if (typeof value !== "boolean") fail("INVALID_SERIALIZED_SHAPE", path, "expected boolean");
|
|
2374
|
+
return value;
|
|
2375
|
+
}
|
|
2376
|
+
function readJsonValue(value, path, depth) {
|
|
2377
|
+
assertJsonValue(value, path, depth);
|
|
2378
|
+
return value;
|
|
2379
|
+
}
|
|
2380
|
+
function assertJsonValue(value, path, depth) {
|
|
2381
|
+
assertTraversalDepth(depth);
|
|
2382
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return;
|
|
2383
|
+
if (typeof value === "number") {
|
|
2384
|
+
if (!Number.isFinite(value)) fail("INVALID_SERIALIZED_SHAPE", path, "json number must be finite");
|
|
2385
|
+
return;
|
|
2386
|
+
}
|
|
2387
|
+
if (Array.isArray(value)) {
|
|
2388
|
+
for (const [index, item] of value.entries()) assertJsonValue(item, `${path}/${index}`, depth + 1);
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2391
|
+
if (!isRecord(value)) fail("INVALID_SERIALIZED_SHAPE", path, "expected JSON value");
|
|
2392
|
+
for (const [key, child] of Object.entries(value)) assertJsonValue(child, `${path}/${key}`, depth + 1);
|
|
2393
|
+
}
|
|
2394
|
+
function fail(reason, path, message) {
|
|
2395
|
+
throw new DeserializeError(reason, path, message);
|
|
2396
|
+
}
|
|
2397
|
+
function isRecord(value) {
|
|
2398
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2399
|
+
}
|
|
1626
2400
|
|
|
1627
2401
|
//#endregion
|
|
1628
2402
|
//#region src/merge.ts
|
|
2403
|
+
var SharedElementMetadataMismatchError = class extends Error {
|
|
2404
|
+
path;
|
|
2405
|
+
constructor(path, id, field) {
|
|
2406
|
+
super(`shared RGA element '${id}' has conflicting ${field} metadata`);
|
|
2407
|
+
this.name = "SharedElementMetadataMismatchError";
|
|
2408
|
+
this.path = path;
|
|
2409
|
+
}
|
|
2410
|
+
};
|
|
1629
2411
|
/** Error thrown by throwing merge helpers (`mergeDoc` / `mergeState`). */
|
|
1630
2412
|
var MergeError = class extends Error {
|
|
1631
2413
|
code;
|
|
@@ -1635,7 +2417,7 @@ var MergeError = class extends Error {
|
|
|
1635
2417
|
super(error.message);
|
|
1636
2418
|
this.name = "MergeError";
|
|
1637
2419
|
this.code = error.code;
|
|
1638
|
-
this.reason =
|
|
2420
|
+
this.reason = error.reason;
|
|
1639
2421
|
this.path = error.path;
|
|
1640
2422
|
}
|
|
1641
2423
|
};
|
|
@@ -1659,21 +2441,39 @@ function mergeDoc(a, b, options = {}) {
|
|
|
1659
2441
|
}
|
|
1660
2442
|
/** Non-throwing `mergeDoc` variant with structured conflict details. */
|
|
1661
2443
|
function tryMergeDoc(a, b, options = {}) {
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
error: {
|
|
2444
|
+
try {
|
|
2445
|
+
const mismatchPath = options.requireSharedOrigin ?? true ? findSeqLineageMismatch(a.root, b.root, []) : null;
|
|
2446
|
+
if (mismatchPath) return {
|
|
1666
2447
|
ok: false,
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
2448
|
+
error: {
|
|
2449
|
+
ok: false,
|
|
2450
|
+
code: 409,
|
|
2451
|
+
reason: "LINEAGE_MISMATCH",
|
|
2452
|
+
message: `merge requires shared array origin at ${mismatchPath}`,
|
|
2453
|
+
path: mismatchPath
|
|
2454
|
+
}
|
|
2455
|
+
};
|
|
2456
|
+
return {
|
|
2457
|
+
ok: true,
|
|
2458
|
+
doc: { root: mergeNode(a.root, b.root) }
|
|
2459
|
+
};
|
|
2460
|
+
} catch (error) {
|
|
2461
|
+
if (error instanceof SharedElementMetadataMismatchError) return {
|
|
2462
|
+
ok: false,
|
|
2463
|
+
error: {
|
|
2464
|
+
ok: false,
|
|
2465
|
+
code: 409,
|
|
2466
|
+
reason: "LINEAGE_MISMATCH",
|
|
2467
|
+
message: error.message,
|
|
2468
|
+
path: error.path
|
|
2469
|
+
}
|
|
2470
|
+
};
|
|
2471
|
+
if (error instanceof TraversalDepthError) return {
|
|
2472
|
+
ok: false,
|
|
2473
|
+
error: toDepthApplyError(error)
|
|
2474
|
+
};
|
|
2475
|
+
throw error;
|
|
2476
|
+
}
|
|
1677
2477
|
}
|
|
1678
2478
|
/**
|
|
1679
2479
|
* Merge two CRDT states.
|
|
@@ -1706,25 +2506,42 @@ function tryMergeState(a, b, options = {}) {
|
|
|
1706
2506
|
};
|
|
1707
2507
|
}
|
|
1708
2508
|
function findSeqLineageMismatch(a, b, path) {
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
2509
|
+
const stack = [{
|
|
2510
|
+
a,
|
|
2511
|
+
b,
|
|
2512
|
+
path,
|
|
2513
|
+
depth: path.length
|
|
2514
|
+
}];
|
|
2515
|
+
while (stack.length > 0) {
|
|
2516
|
+
const frame = stack.pop();
|
|
2517
|
+
assertTraversalDepth(frame.depth);
|
|
2518
|
+
if (frame.a.kind === "seq" && frame.b.kind === "seq") {
|
|
2519
|
+
const hasElemsA = frame.a.elems.size > 0;
|
|
2520
|
+
const hasElemsB = frame.b.elems.size > 0;
|
|
2521
|
+
if (hasElemsA && hasElemsB) {
|
|
2522
|
+
let shared = false;
|
|
2523
|
+
for (const id of frame.a.elems.keys()) if (frame.b.elems.has(id)) {
|
|
2524
|
+
shared = true;
|
|
2525
|
+
break;
|
|
2526
|
+
}
|
|
2527
|
+
if (!shared) return `/${frame.path.join("/")}`;
|
|
1717
2528
|
}
|
|
1718
|
-
if (!shared) return `/${path.join("/")}`;
|
|
1719
2529
|
}
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
2530
|
+
if (frame.a.kind === "obj" && frame.b.kind === "obj") {
|
|
2531
|
+
const left = frame.a;
|
|
2532
|
+
const right = frame.b;
|
|
2533
|
+
const sharedKeys = [...left.entries.keys()].filter((key) => right.entries.has(key));
|
|
2534
|
+
for (let i = sharedKeys.length - 1; i >= 0; i--) {
|
|
2535
|
+
const key = sharedKeys[i];
|
|
2536
|
+
const nextA = left.entries.get(key).node;
|
|
2537
|
+
const nextB = right.entries.get(key).node;
|
|
2538
|
+
stack.push({
|
|
2539
|
+
a: nextA,
|
|
2540
|
+
b: nextB,
|
|
2541
|
+
path: [...frame.path, key],
|
|
2542
|
+
depth: frame.depth + 1
|
|
2543
|
+
});
|
|
2544
|
+
}
|
|
1728
2545
|
}
|
|
1729
2546
|
}
|
|
1730
2547
|
return null;
|
|
@@ -1736,28 +2553,38 @@ function maxObservedCtrForActor(doc, actor, a, b) {
|
|
|
1736
2553
|
return best;
|
|
1737
2554
|
}
|
|
1738
2555
|
function maxCtrInNodeForActor(node, actor) {
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
2556
|
+
let best = 0;
|
|
2557
|
+
const stack = [{
|
|
2558
|
+
node,
|
|
2559
|
+
depth: 0
|
|
2560
|
+
}];
|
|
2561
|
+
while (stack.length > 0) {
|
|
2562
|
+
const frame = stack.pop();
|
|
2563
|
+
assertTraversalDepth(frame.depth);
|
|
2564
|
+
if (frame.node.kind === "lww") {
|
|
2565
|
+
if (frame.node.dot.actor === actor && frame.node.dot.ctr > best) best = frame.node.dot.ctr;
|
|
2566
|
+
continue;
|
|
2567
|
+
}
|
|
2568
|
+
if (frame.node.kind === "obj") {
|
|
2569
|
+
for (const entry of frame.node.entries.values()) {
|
|
1744
2570
|
if (entry.dot.actor === actor && entry.dot.ctr > best) best = entry.dot.ctr;
|
|
1745
|
-
|
|
1746
|
-
|
|
2571
|
+
stack.push({
|
|
2572
|
+
node: entry.node,
|
|
2573
|
+
depth: frame.depth + 1
|
|
2574
|
+
});
|
|
1747
2575
|
}
|
|
1748
|
-
for (const tomb of node.tombstone.values()) if (tomb.actor === actor && tomb.ctr > best) best = tomb.ctr;
|
|
1749
|
-
|
|
2576
|
+
for (const tomb of frame.node.tombstone.values()) if (tomb.actor === actor && tomb.ctr > best) best = tomb.ctr;
|
|
2577
|
+
continue;
|
|
1750
2578
|
}
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
}
|
|
1758
|
-
return best;
|
|
2579
|
+
for (const elem of frame.node.elems.values()) {
|
|
2580
|
+
if (elem.insDot.actor === actor && elem.insDot.ctr > best) best = elem.insDot.ctr;
|
|
2581
|
+
stack.push({
|
|
2582
|
+
node: elem.value,
|
|
2583
|
+
depth: frame.depth + 1
|
|
2584
|
+
});
|
|
1759
2585
|
}
|
|
1760
2586
|
}
|
|
2587
|
+
return best;
|
|
1761
2588
|
}
|
|
1762
2589
|
function repDot(node) {
|
|
1763
2590
|
switch (node.kind) {
|
|
@@ -1782,11 +2609,15 @@ function repDot(node) {
|
|
|
1782
2609
|
}
|
|
1783
2610
|
}
|
|
1784
2611
|
function mergeNode(a, b) {
|
|
2612
|
+
return mergeNodeAtDepth(a, b, 0, []);
|
|
2613
|
+
}
|
|
2614
|
+
function mergeNodeAtDepth(a, b, depth, path) {
|
|
2615
|
+
assertTraversalDepth(depth);
|
|
1785
2616
|
if (a.kind === "lww" && b.kind === "lww") return mergeLww(a, b);
|
|
1786
|
-
if (a.kind === "obj" && b.kind === "obj") return mergeObj(a, b);
|
|
1787
|
-
if (a.kind === "seq" && b.kind === "seq") return mergeSeq(a, b);
|
|
1788
|
-
if (compareDot(repDot(a), repDot(b)) >= 0) return cloneNodeShallow(a);
|
|
1789
|
-
return cloneNodeShallow(b);
|
|
2617
|
+
if (a.kind === "obj" && b.kind === "obj") return mergeObj(a, b, depth + 1, path);
|
|
2618
|
+
if (a.kind === "seq" && b.kind === "seq") return mergeSeq(a, b, depth + 1, path);
|
|
2619
|
+
if (compareDot(repDot(a), repDot(b)) >= 0) return cloneNodeShallow(a, depth + 1);
|
|
2620
|
+
return cloneNodeShallow(b, depth + 1);
|
|
1790
2621
|
}
|
|
1791
2622
|
function mergeLww(a, b) {
|
|
1792
2623
|
if (compareDot(a.dot, b.dot) >= 0) return {
|
|
@@ -1800,7 +2631,8 @@ function mergeLww(a, b) {
|
|
|
1800
2631
|
dot: { ...b.dot }
|
|
1801
2632
|
};
|
|
1802
2633
|
}
|
|
1803
|
-
function mergeObj(a, b) {
|
|
2634
|
+
function mergeObj(a, b, depth, path) {
|
|
2635
|
+
assertTraversalDepth(depth);
|
|
1804
2636
|
const entries = /* @__PURE__ */ new Map();
|
|
1805
2637
|
const tombstone = /* @__PURE__ */ new Map();
|
|
1806
2638
|
const allTombKeys = new Set([...a.tombstone.keys(), ...b.tombstone.keys()]);
|
|
@@ -1817,15 +2649,15 @@ function mergeObj(a, b) {
|
|
|
1817
2649
|
const eb = b.entries.get(key);
|
|
1818
2650
|
let merged;
|
|
1819
2651
|
if (ea && eb) merged = {
|
|
1820
|
-
node:
|
|
2652
|
+
node: mergeNodeAtDepth(ea.node, eb.node, depth + 1, [...path, key]),
|
|
1821
2653
|
dot: compareDot(ea.dot, eb.dot) >= 0 ? { ...ea.dot } : { ...eb.dot }
|
|
1822
2654
|
};
|
|
1823
2655
|
else if (ea) merged = {
|
|
1824
|
-
node: cloneNodeShallow(ea.node),
|
|
2656
|
+
node: cloneNodeShallow(ea.node, depth + 1),
|
|
1825
2657
|
dot: { ...ea.dot }
|
|
1826
2658
|
};
|
|
1827
2659
|
else merged = {
|
|
1828
|
-
node: cloneNodeShallow(eb.node),
|
|
2660
|
+
node: cloneNodeShallow(eb.node, depth + 1),
|
|
1829
2661
|
dot: { ...eb.dot }
|
|
1830
2662
|
};
|
|
1831
2663
|
const td = tombstone.get(key);
|
|
@@ -1838,14 +2670,17 @@ function mergeObj(a, b) {
|
|
|
1838
2670
|
tombstone
|
|
1839
2671
|
};
|
|
1840
2672
|
}
|
|
1841
|
-
function mergeSeq(a, b) {
|
|
2673
|
+
function mergeSeq(a, b, depth, path) {
|
|
2674
|
+
assertTraversalDepth(depth);
|
|
1842
2675
|
const elems = /* @__PURE__ */ new Map();
|
|
1843
2676
|
const allIds = new Set([...a.elems.keys(), ...b.elems.keys()]);
|
|
1844
2677
|
for (const id of allIds) {
|
|
1845
2678
|
const ea = a.elems.get(id);
|
|
1846
2679
|
const eb = b.elems.get(id);
|
|
1847
2680
|
if (ea && eb) {
|
|
1848
|
-
|
|
2681
|
+
if (ea.prev !== eb.prev) throw new SharedElementMetadataMismatchError(toPointer(path), id, "prev");
|
|
2682
|
+
if (!sameDot(ea.insDot, eb.insDot)) throw new SharedElementMetadataMismatchError(toPointer(path), id, "insDot");
|
|
2683
|
+
const mergedValue = mergeNodeAtDepth(ea.value, eb.value, depth + 1, [...path, id]);
|
|
1849
2684
|
elems.set(id, {
|
|
1850
2685
|
id,
|
|
1851
2686
|
prev: ea.prev,
|
|
@@ -1853,24 +2688,33 @@ function mergeSeq(a, b) {
|
|
|
1853
2688
|
value: mergedValue,
|
|
1854
2689
|
insDot: { ...ea.insDot }
|
|
1855
2690
|
});
|
|
1856
|
-
} else if (ea) elems.set(id, cloneElem(ea));
|
|
1857
|
-
else elems.set(id, cloneElem(eb));
|
|
2691
|
+
} else if (ea) elems.set(id, cloneElem(ea, depth + 1));
|
|
2692
|
+
else elems.set(id, cloneElem(eb, depth + 1));
|
|
1858
2693
|
}
|
|
1859
2694
|
return {
|
|
1860
2695
|
kind: "seq",
|
|
1861
2696
|
elems
|
|
1862
2697
|
};
|
|
1863
2698
|
}
|
|
1864
|
-
function
|
|
2699
|
+
function sameDot(a, b) {
|
|
2700
|
+
return a.actor === b.actor && a.ctr === b.ctr;
|
|
2701
|
+
}
|
|
2702
|
+
function toPointer(path) {
|
|
2703
|
+
if (path.length === 0) return "/";
|
|
2704
|
+
return `/${path.join("/")}`;
|
|
2705
|
+
}
|
|
2706
|
+
function cloneElem(e, depth) {
|
|
2707
|
+
assertTraversalDepth(depth);
|
|
1865
2708
|
return {
|
|
1866
2709
|
id: e.id,
|
|
1867
2710
|
prev: e.prev,
|
|
1868
2711
|
tombstone: e.tombstone,
|
|
1869
|
-
value: cloneNodeShallow(e.value),
|
|
2712
|
+
value: cloneNodeShallow(e.value, depth + 1),
|
|
1870
2713
|
insDot: { ...e.insDot }
|
|
1871
2714
|
};
|
|
1872
2715
|
}
|
|
1873
|
-
function cloneNodeShallow(node) {
|
|
2716
|
+
function cloneNodeShallow(node, depth) {
|
|
2717
|
+
assertTraversalDepth(depth);
|
|
1874
2718
|
switch (node.kind) {
|
|
1875
2719
|
case "lww": return {
|
|
1876
2720
|
kind: "lww",
|
|
@@ -1880,7 +2724,7 @@ function cloneNodeShallow(node) {
|
|
|
1880
2724
|
case "obj": {
|
|
1881
2725
|
const entries = /* @__PURE__ */ new Map();
|
|
1882
2726
|
for (const [k, v] of node.entries) entries.set(k, {
|
|
1883
|
-
node: cloneNodeShallow(v.node),
|
|
2727
|
+
node: cloneNodeShallow(v.node, depth + 1),
|
|
1884
2728
|
dot: { ...v.dot }
|
|
1885
2729
|
});
|
|
1886
2730
|
const tombstone = /* @__PURE__ */ new Map();
|
|
@@ -1893,7 +2737,7 @@ function cloneNodeShallow(node) {
|
|
|
1893
2737
|
}
|
|
1894
2738
|
case "seq": {
|
|
1895
2739
|
const elems = /* @__PURE__ */ new Map();
|
|
1896
|
-
for (const [id, e] of node.elems) elems.set(id, cloneElem(e));
|
|
2740
|
+
for (const [id, e] of node.elems) elems.set(id, cloneElem(e, depth + 1));
|
|
1897
2741
|
return {
|
|
1898
2742
|
kind: "seq",
|
|
1899
2743
|
elems
|
|
@@ -1903,12 +2747,115 @@ function cloneNodeShallow(node) {
|
|
|
1903
2747
|
}
|
|
1904
2748
|
|
|
1905
2749
|
//#endregion
|
|
2750
|
+
//#region src/compact.ts
|
|
2751
|
+
function isDotStable(stable, dot) {
|
|
2752
|
+
return (stable[dot.actor] ?? 0) >= dot.ctr;
|
|
2753
|
+
}
|
|
2754
|
+
/**
|
|
2755
|
+
* Compact causally-stable tombstones in a document.
|
|
2756
|
+
*
|
|
2757
|
+
* Safety note:
|
|
2758
|
+
* - Only compact at checkpoints that are causally stable across all peers you
|
|
2759
|
+
* may still merge with.
|
|
2760
|
+
* - Do not merge this compacted document with replicas that might be behind
|
|
2761
|
+
* the provided checkpoint.
|
|
2762
|
+
*/
|
|
2763
|
+
function compactDocTombstones(doc, options) {
|
|
2764
|
+
const targetDoc = options.mutate ? doc : cloneDoc(doc);
|
|
2765
|
+
const stats = {
|
|
2766
|
+
objectTombstonesRemoved: 0,
|
|
2767
|
+
sequenceTombstonesRemoved: 0
|
|
2768
|
+
};
|
|
2769
|
+
const stable = options.stable;
|
|
2770
|
+
const stack = [{
|
|
2771
|
+
node: targetDoc.root,
|
|
2772
|
+
depth: 0
|
|
2773
|
+
}];
|
|
2774
|
+
while (stack.length > 0) {
|
|
2775
|
+
const frame = stack.pop();
|
|
2776
|
+
assertTraversalDepth(frame.depth);
|
|
2777
|
+
if (frame.node.kind === "obj") {
|
|
2778
|
+
stats.objectTombstonesRemoved += objCompactTombstones(frame.node, (dot) => isDotStable(stable, dot));
|
|
2779
|
+
for (const entry of frame.node.entries.values()) stack.push({
|
|
2780
|
+
node: entry.node,
|
|
2781
|
+
depth: frame.depth + 1
|
|
2782
|
+
});
|
|
2783
|
+
continue;
|
|
2784
|
+
}
|
|
2785
|
+
if (frame.node.kind === "seq") {
|
|
2786
|
+
stats.sequenceTombstonesRemoved += rgaCompactTombstones(frame.node, (dot) => isDotStable(stable, dot));
|
|
2787
|
+
for (const elem of frame.node.elems.values()) stack.push({
|
|
2788
|
+
node: elem.value,
|
|
2789
|
+
depth: frame.depth + 1
|
|
2790
|
+
});
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
return {
|
|
2794
|
+
doc: targetDoc,
|
|
2795
|
+
stats
|
|
2796
|
+
};
|
|
2797
|
+
}
|
|
2798
|
+
/**
|
|
2799
|
+
* Compact causally-stable tombstones in a state document.
|
|
2800
|
+
*
|
|
2801
|
+
* Safety note:
|
|
2802
|
+
* - Only compact at checkpoints that are causally stable across all peers you
|
|
2803
|
+
* may still merge with.
|
|
2804
|
+
* - Do not merge this compacted state with replicas that might be behind the
|
|
2805
|
+
* provided checkpoint.
|
|
2806
|
+
*/
|
|
2807
|
+
function compactStateTombstones(state, options) {
|
|
2808
|
+
if (options.mutate) return {
|
|
2809
|
+
state,
|
|
2810
|
+
stats: compactDocTombstones(state.doc, {
|
|
2811
|
+
stable: options.stable,
|
|
2812
|
+
mutate: true
|
|
2813
|
+
}).stats
|
|
2814
|
+
};
|
|
2815
|
+
const nextState = {
|
|
2816
|
+
doc: cloneDoc(state.doc),
|
|
2817
|
+
clock: cloneClock(state.clock)
|
|
2818
|
+
};
|
|
2819
|
+
return {
|
|
2820
|
+
state: nextState,
|
|
2821
|
+
stats: compactDocTombstones(nextState.doc, {
|
|
2822
|
+
stable: options.stable,
|
|
2823
|
+
mutate: true
|
|
2824
|
+
}).stats
|
|
2825
|
+
};
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
//#endregion
|
|
2829
|
+
Object.defineProperty(exports, 'ClockValidationError', {
|
|
2830
|
+
enumerable: true,
|
|
2831
|
+
get: function () {
|
|
2832
|
+
return ClockValidationError;
|
|
2833
|
+
}
|
|
2834
|
+
});
|
|
2835
|
+
Object.defineProperty(exports, 'DeserializeError', {
|
|
2836
|
+
enumerable: true,
|
|
2837
|
+
get: function () {
|
|
2838
|
+
return DeserializeError;
|
|
2839
|
+
}
|
|
2840
|
+
});
|
|
1906
2841
|
Object.defineProperty(exports, 'HEAD', {
|
|
1907
2842
|
enumerable: true,
|
|
1908
2843
|
get: function () {
|
|
1909
2844
|
return HEAD;
|
|
1910
2845
|
}
|
|
1911
2846
|
});
|
|
2847
|
+
Object.defineProperty(exports, 'JsonValueValidationError', {
|
|
2848
|
+
enumerable: true,
|
|
2849
|
+
get: function () {
|
|
2850
|
+
return JsonValueValidationError;
|
|
2851
|
+
}
|
|
2852
|
+
});
|
|
2853
|
+
Object.defineProperty(exports, 'MAX_TRAVERSAL_DEPTH', {
|
|
2854
|
+
enumerable: true,
|
|
2855
|
+
get: function () {
|
|
2856
|
+
return MAX_TRAVERSAL_DEPTH;
|
|
2857
|
+
}
|
|
2858
|
+
});
|
|
1912
2859
|
Object.defineProperty(exports, 'MergeError', {
|
|
1913
2860
|
enumerable: true,
|
|
1914
2861
|
get: function () {
|
|
@@ -1933,6 +2880,12 @@ Object.defineProperty(exports, 'ROOT_KEY', {
|
|
|
1933
2880
|
return ROOT_KEY;
|
|
1934
2881
|
}
|
|
1935
2882
|
});
|
|
2883
|
+
Object.defineProperty(exports, 'TraversalDepthError', {
|
|
2884
|
+
enumerable: true,
|
|
2885
|
+
get: function () {
|
|
2886
|
+
return TraversalDepthError;
|
|
2887
|
+
}
|
|
2888
|
+
});
|
|
1936
2889
|
Object.defineProperty(exports, 'applyIntentsToCrdt', {
|
|
1937
2890
|
enumerable: true,
|
|
1938
2891
|
get: function () {
|
|
@@ -1969,6 +2922,18 @@ Object.defineProperty(exports, 'cloneDoc', {
|
|
|
1969
2922
|
return cloneDoc;
|
|
1970
2923
|
}
|
|
1971
2924
|
});
|
|
2925
|
+
Object.defineProperty(exports, 'compactDocTombstones', {
|
|
2926
|
+
enumerable: true,
|
|
2927
|
+
get: function () {
|
|
2928
|
+
return compactDocTombstones;
|
|
2929
|
+
}
|
|
2930
|
+
});
|
|
2931
|
+
Object.defineProperty(exports, 'compactStateTombstones', {
|
|
2932
|
+
enumerable: true,
|
|
2933
|
+
get: function () {
|
|
2934
|
+
return compactStateTombstones;
|
|
2935
|
+
}
|
|
2936
|
+
});
|
|
1972
2937
|
Object.defineProperty(exports, 'compareDot', {
|
|
1973
2938
|
enumerable: true,
|
|
1974
2939
|
get: function () {
|
|
@@ -2119,6 +3084,12 @@ Object.defineProperty(exports, 'nextDotForActor', {
|
|
|
2119
3084
|
return nextDotForActor;
|
|
2120
3085
|
}
|
|
2121
3086
|
});
|
|
3087
|
+
Object.defineProperty(exports, 'objCompactTombstones', {
|
|
3088
|
+
enumerable: true,
|
|
3089
|
+
get: function () {
|
|
3090
|
+
return objCompactTombstones;
|
|
3091
|
+
}
|
|
3092
|
+
});
|
|
2122
3093
|
Object.defineProperty(exports, 'objRemove', {
|
|
2123
3094
|
enumerable: true,
|
|
2124
3095
|
get: function () {
|
|
@@ -2143,6 +3114,12 @@ Object.defineProperty(exports, 'parseJsonPointer', {
|
|
|
2143
3114
|
return parseJsonPointer;
|
|
2144
3115
|
}
|
|
2145
3116
|
});
|
|
3117
|
+
Object.defineProperty(exports, 'rgaCompactTombstones', {
|
|
3118
|
+
enumerable: true,
|
|
3119
|
+
get: function () {
|
|
3120
|
+
return rgaCompactTombstones;
|
|
3121
|
+
}
|
|
3122
|
+
});
|
|
2146
3123
|
Object.defineProperty(exports, 'rgaDelete', {
|
|
2147
3124
|
enumerable: true,
|
|
2148
3125
|
get: function () {
|