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