json-patch-to-crdt 0.1.1 → 0.1.2
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 +71 -1
- package/dist/{merge-CKcP1ZPt.mjs → compact-BJBGW9tC.mjs} +795 -200
- package/dist/{merge-BAfuC6bf.js → compact-CkLd4Yh5.js} +836 -199
- package/dist/{merge-B8nmGV-o.d.ts → depth-p6fX9Ak7.d.ts} +83 -3
- package/dist/{merge-DQ_KDtnE.d.mts → depth-wDeQ1hO1.d.mts} +83 -3
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +20 -16
- package/dist/index.mjs +2 -2
- package/dist/internals.d.mts +15 -3
- package/dist/internals.d.ts +15 -3
- package/dist/internals.js +65 -58
- package/dist/internals.mjs +2 -2
- package/package.json +12 -2
|
@@ -39,6 +39,33 @@ function observeDot(vv, dot) {
|
|
|
39
39
|
if ((vv[dot.actor] ?? 0) < dot.ctr) vv[dot.actor] = dot.ctr;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/depth.ts
|
|
44
|
+
const MAX_TRAVERSAL_DEPTH = 16384;
|
|
45
|
+
var TraversalDepthError = class extends Error {
|
|
46
|
+
code = 409;
|
|
47
|
+
reason = "MAX_DEPTH_EXCEEDED";
|
|
48
|
+
depth;
|
|
49
|
+
maxDepth;
|
|
50
|
+
constructor(depth, maxDepth = MAX_TRAVERSAL_DEPTH) {
|
|
51
|
+
super(`maximum nesting depth ${maxDepth} exceeded at depth ${depth}`);
|
|
52
|
+
this.name = "TraversalDepthError";
|
|
53
|
+
this.depth = depth;
|
|
54
|
+
this.maxDepth = maxDepth;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
function assertTraversalDepth(depth, maxDepth = MAX_TRAVERSAL_DEPTH) {
|
|
58
|
+
if (depth > maxDepth) throw new TraversalDepthError(depth, maxDepth);
|
|
59
|
+
}
|
|
60
|
+
function toDepthApplyError(error) {
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
code: error.code,
|
|
64
|
+
reason: error.reason,
|
|
65
|
+
message: error.message
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
42
69
|
//#endregion
|
|
43
70
|
//#region src/dot.ts
|
|
44
71
|
function compareDot(a, b) {
|
|
@@ -84,15 +111,26 @@ function rgaLinearizeIds(seq) {
|
|
|
84
111
|
if (cached && cached.version === ver) return cached.ids;
|
|
85
112
|
const idx = rgaChildrenIndex(seq);
|
|
86
113
|
const out = [];
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
114
|
+
const stack = [];
|
|
115
|
+
const rootChildren = idx.get(HEAD);
|
|
116
|
+
if (rootChildren) stack.push({
|
|
117
|
+
children: rootChildren,
|
|
118
|
+
index: 0
|
|
119
|
+
});
|
|
120
|
+
while (stack.length > 0) {
|
|
121
|
+
const frame = stack[stack.length - 1];
|
|
122
|
+
if (frame.index >= frame.children.length) {
|
|
123
|
+
stack.pop();
|
|
124
|
+
continue;
|
|
93
125
|
}
|
|
126
|
+
const child = frame.children[frame.index++];
|
|
127
|
+
if (!child.tombstone) out.push(child.id);
|
|
128
|
+
const grandchildren = idx.get(child.id);
|
|
129
|
+
if (grandchildren) stack.push({
|
|
130
|
+
children: grandchildren,
|
|
131
|
+
index: 0
|
|
132
|
+
});
|
|
94
133
|
}
|
|
95
|
-
walk(HEAD);
|
|
96
134
|
linearCache.set(seq, {
|
|
97
135
|
version: ver,
|
|
98
136
|
ids: out
|
|
@@ -117,6 +155,61 @@ function rgaDelete(seq, id) {
|
|
|
117
155
|
e.tombstone = true;
|
|
118
156
|
bumpVersion(seq);
|
|
119
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Prune tombstoned elements that are causally stable and have no live descendants
|
|
160
|
+
* depending on them for sequence traversal.
|
|
161
|
+
*
|
|
162
|
+
* Returns the number of removed elements.
|
|
163
|
+
*/
|
|
164
|
+
function rgaCompactTombstones(seq, isStable) {
|
|
165
|
+
if (seq.elems.size === 0) return 0;
|
|
166
|
+
const children = /* @__PURE__ */ new Map();
|
|
167
|
+
const roots = [];
|
|
168
|
+
for (const elem of seq.elems.values()) {
|
|
169
|
+
const byPrev = children.get(elem.prev);
|
|
170
|
+
if (byPrev) byPrev.push(elem.id);
|
|
171
|
+
else children.set(elem.prev, [elem.id]);
|
|
172
|
+
if (elem.prev === HEAD || !seq.elems.has(elem.prev)) roots.push(elem.id);
|
|
173
|
+
}
|
|
174
|
+
const removable = /* @__PURE__ */ new Set();
|
|
175
|
+
const visited = /* @__PURE__ */ new Set();
|
|
176
|
+
const stack = [];
|
|
177
|
+
const pushRoot = (id) => {
|
|
178
|
+
if (!visited.has(id)) stack.push({
|
|
179
|
+
id,
|
|
180
|
+
expanded: false
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
for (const id of roots) pushRoot(id);
|
|
184
|
+
for (const id of seq.elems.keys()) pushRoot(id);
|
|
185
|
+
while (stack.length > 0) {
|
|
186
|
+
const frame = stack.pop();
|
|
187
|
+
if (!frame.expanded) {
|
|
188
|
+
if (visited.has(frame.id)) continue;
|
|
189
|
+
visited.add(frame.id);
|
|
190
|
+
stack.push({
|
|
191
|
+
id: frame.id,
|
|
192
|
+
expanded: true
|
|
193
|
+
});
|
|
194
|
+
const childIds = children.get(frame.id);
|
|
195
|
+
if (childIds) {
|
|
196
|
+
for (const childId of childIds) if (!visited.has(childId)) stack.push({
|
|
197
|
+
id: childId,
|
|
198
|
+
expanded: false
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const elem = seq.elems.get(frame.id);
|
|
204
|
+
if (!elem || !elem.tombstone || !isStable(elem.insDot)) continue;
|
|
205
|
+
const childIds = children.get(frame.id);
|
|
206
|
+
if (!childIds || childIds.every((childId) => removable.has(childId))) removable.add(frame.id);
|
|
207
|
+
}
|
|
208
|
+
if (removable.size === 0) return 0;
|
|
209
|
+
for (const id of removable) seq.elems.delete(id);
|
|
210
|
+
bumpVersion(seq);
|
|
211
|
+
return removable.size;
|
|
212
|
+
}
|
|
120
213
|
function rgaIdAtIndex(seq, index) {
|
|
121
214
|
return rgaLinearizeIds(seq)[index];
|
|
122
215
|
}
|
|
@@ -128,17 +221,100 @@ function rgaPrevForInsertAtIndex(seq, index) {
|
|
|
128
221
|
|
|
129
222
|
//#endregion
|
|
130
223
|
//#region src/materialize.ts
|
|
131
|
-
/**
|
|
224
|
+
/** Convert a CRDT node graph into a plain JSON value using an explicit stack. */
|
|
132
225
|
function materialize(node) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
226
|
+
if (node.kind === "lww") return node.value;
|
|
227
|
+
const root = node.kind === "obj" ? {} : [];
|
|
228
|
+
const stack = [];
|
|
229
|
+
if (node.kind === "obj") stack.push({
|
|
230
|
+
kind: "obj",
|
|
231
|
+
depth: 0,
|
|
232
|
+
entries: Array.from(node.entries.entries(), ([key, value]) => [key, value.node]),
|
|
233
|
+
index: 0,
|
|
234
|
+
out: root
|
|
235
|
+
});
|
|
236
|
+
else stack.push({
|
|
237
|
+
kind: "seq",
|
|
238
|
+
depth: 0,
|
|
239
|
+
ids: rgaLinearizeIds(node),
|
|
240
|
+
index: 0,
|
|
241
|
+
seq: node,
|
|
242
|
+
out: root
|
|
243
|
+
});
|
|
244
|
+
while (stack.length > 0) {
|
|
245
|
+
const frame = stack[stack.length - 1];
|
|
246
|
+
if (frame.kind === "obj") {
|
|
247
|
+
if (frame.index >= frame.entries.length) {
|
|
248
|
+
stack.pop();
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const [key, child] = frame.entries[frame.index++];
|
|
252
|
+
const childDepth = frame.depth + 1;
|
|
253
|
+
assertTraversalDepth(childDepth);
|
|
254
|
+
if (child.kind === "lww") {
|
|
255
|
+
frame.out[key] = child.value;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (child.kind === "obj") {
|
|
259
|
+
const outObj = {};
|
|
260
|
+
frame.out[key] = outObj;
|
|
261
|
+
stack.push({
|
|
262
|
+
kind: "obj",
|
|
263
|
+
depth: childDepth,
|
|
264
|
+
entries: Array.from(child.entries.entries(), ([childKey, value]) => [childKey, value.node]),
|
|
265
|
+
index: 0,
|
|
266
|
+
out: outObj
|
|
267
|
+
});
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const outArr = [];
|
|
271
|
+
frame.out[key] = outArr;
|
|
272
|
+
stack.push({
|
|
273
|
+
kind: "seq",
|
|
274
|
+
depth: childDepth,
|
|
275
|
+
ids: rgaLinearizeIds(child),
|
|
276
|
+
index: 0,
|
|
277
|
+
seq: child,
|
|
278
|
+
out: outArr
|
|
279
|
+
});
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (frame.index >= frame.ids.length) {
|
|
283
|
+
stack.pop();
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const id = frame.ids[frame.index++];
|
|
287
|
+
const child = frame.seq.elems.get(id).value;
|
|
288
|
+
const childDepth = frame.depth + 1;
|
|
289
|
+
assertTraversalDepth(childDepth);
|
|
290
|
+
if (child.kind === "lww") {
|
|
291
|
+
frame.out.push(child.value);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (child.kind === "obj") {
|
|
295
|
+
const outObj = {};
|
|
296
|
+
frame.out.push(outObj);
|
|
297
|
+
stack.push({
|
|
298
|
+
kind: "obj",
|
|
299
|
+
depth: childDepth,
|
|
300
|
+
entries: Array.from(child.entries.entries(), ([key, value]) => [key, value.node]),
|
|
301
|
+
index: 0,
|
|
302
|
+
out: outObj
|
|
303
|
+
});
|
|
304
|
+
continue;
|
|
139
305
|
}
|
|
140
|
-
|
|
306
|
+
const outArr = [];
|
|
307
|
+
frame.out.push(outArr);
|
|
308
|
+
stack.push({
|
|
309
|
+
kind: "seq",
|
|
310
|
+
depth: childDepth,
|
|
311
|
+
ids: rgaLinearizeIds(child),
|
|
312
|
+
index: 0,
|
|
313
|
+
seq: child,
|
|
314
|
+
out: outArr
|
|
315
|
+
});
|
|
141
316
|
}
|
|
317
|
+
return root;
|
|
142
318
|
}
|
|
143
319
|
|
|
144
320
|
//#endregion
|
|
@@ -183,6 +359,19 @@ function objRemove(obj, key, dot) {
|
|
|
183
359
|
if (!curDel || compareDot(curDel, dot) <= 0) obj.tombstone.set(key, dot);
|
|
184
360
|
obj.entries.delete(key);
|
|
185
361
|
}
|
|
362
|
+
/**
|
|
363
|
+
* Prune object tombstones that satisfy a caller-provided stability predicate.
|
|
364
|
+
* Returns the number of removed tombstone records.
|
|
365
|
+
*/
|
|
366
|
+
function objCompactTombstones(obj, isStable) {
|
|
367
|
+
let removed = 0;
|
|
368
|
+
for (const [key, dot] of obj.tombstone.entries()) {
|
|
369
|
+
if (!isStable(dot)) continue;
|
|
370
|
+
obj.tombstone.delete(key);
|
|
371
|
+
removed += 1;
|
|
372
|
+
}
|
|
373
|
+
return removed;
|
|
374
|
+
}
|
|
186
375
|
|
|
187
376
|
//#endregion
|
|
188
377
|
//#region src/types.ts
|
|
@@ -194,6 +383,7 @@ const ROOT_KEY = "@@crdt/root";
|
|
|
194
383
|
|
|
195
384
|
//#endregion
|
|
196
385
|
//#region src/patch.ts
|
|
386
|
+
const DEFAULT_LCS_MAX_CELLS = 25e4;
|
|
197
387
|
/** Structured compile error used to map patch validation failures to typed reasons. */
|
|
198
388
|
var PatchCompileError = class extends Error {
|
|
199
389
|
reason;
|
|
@@ -207,6 +397,17 @@ var PatchCompileError = class extends Error {
|
|
|
207
397
|
this.opIndex = opIndex;
|
|
208
398
|
}
|
|
209
399
|
};
|
|
400
|
+
/** Structured lookup error thrown by `getAtJson`. */
|
|
401
|
+
var JsonLookupError = class extends Error {
|
|
402
|
+
code;
|
|
403
|
+
segment;
|
|
404
|
+
constructor(code, segment, message) {
|
|
405
|
+
super(message);
|
|
406
|
+
this.name = "JsonLookupError";
|
|
407
|
+
this.code = code;
|
|
408
|
+
this.segment = segment;
|
|
409
|
+
}
|
|
410
|
+
};
|
|
210
411
|
/**
|
|
211
412
|
* Parse an RFC 6901 JSON Pointer into a path array, unescaping `~1` and `~0`.
|
|
212
413
|
* @param ptr - A JSON Pointer string (e.g. `"/a/b"` or `""`).
|
|
@@ -253,14 +454,14 @@ function unescapeJsonPointerToken(token) {
|
|
|
253
454
|
function getAtJson(base, path) {
|
|
254
455
|
let cur = base;
|
|
255
456
|
for (const seg of path) if (Array.isArray(cur)) {
|
|
256
|
-
if (!ARRAY_INDEX_TOKEN_PATTERN.test(seg)) throw new
|
|
457
|
+
if (!ARRAY_INDEX_TOKEN_PATTERN.test(seg)) throw new JsonLookupError("EXPECTED_ARRAY_INDEX", seg, `Expected array index, got '${seg}'`);
|
|
257
458
|
const idx = Number(seg);
|
|
258
|
-
if (idx < 0 || idx >= cur.length) throw new
|
|
459
|
+
if (idx < 0 || idx >= cur.length) throw new JsonLookupError("INDEX_OUT_OF_BOUNDS", seg, `Index out of bounds at '${seg}'`);
|
|
259
460
|
cur = cur[idx];
|
|
260
461
|
} else if (cur && typeof cur === "object") {
|
|
261
|
-
if (!(seg in cur)) throw new
|
|
462
|
+
if (!(seg in cur)) throw new JsonLookupError("MISSING_KEY", seg, `Missing key '${seg}'`);
|
|
262
463
|
cur = cur[seg];
|
|
263
|
-
} else throw new
|
|
464
|
+
} else throw new JsonLookupError("NON_CONTAINER", seg, `Cannot traverse into non-container at '${seg}'`);
|
|
264
465
|
return cur;
|
|
265
466
|
}
|
|
266
467
|
/**
|
|
@@ -301,7 +502,15 @@ function diffValue(path, base, next, ops, options) {
|
|
|
301
502
|
if (jsonEquals(base, next)) return;
|
|
302
503
|
if (Array.isArray(base) || Array.isArray(next)) {
|
|
303
504
|
if ((options.arrayStrategy ?? "lcs") === "lcs" && Array.isArray(base) && Array.isArray(next)) {
|
|
304
|
-
|
|
505
|
+
if (!shouldUseLcsDiff(base.length, next.length, options.lcsMaxCells)) {
|
|
506
|
+
ops.push({
|
|
507
|
+
op: "replace",
|
|
508
|
+
path: stringifyJsonPointer(path),
|
|
509
|
+
value: next
|
|
510
|
+
});
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
diffArray(path, base, next, ops);
|
|
305
514
|
return;
|
|
306
515
|
}
|
|
307
516
|
ops.push({
|
|
@@ -337,7 +546,7 @@ function diffValue(path, base, next, ops, options) {
|
|
|
337
546
|
}
|
|
338
547
|
for (const key of baseKeys) if (nextSet.has(key)) diffValue([...path, key], base[key], next[key], ops, options);
|
|
339
548
|
}
|
|
340
|
-
function diffArray(path, base, next, ops
|
|
549
|
+
function diffArray(path, base, next, ops) {
|
|
341
550
|
const n = base.length;
|
|
342
551
|
const m = next.length;
|
|
343
552
|
const lcs = Array.from({ length: n + 1 }, () => Array(m + 1).fill(0));
|
|
@@ -377,6 +586,12 @@ function diffArray(path, base, next, ops, _options) {
|
|
|
377
586
|
}
|
|
378
587
|
ops.push(...compactArrayOps(localOps));
|
|
379
588
|
}
|
|
589
|
+
function shouldUseLcsDiff(baseLength, nextLength, lcsMaxCells) {
|
|
590
|
+
if (lcsMaxCells === Number.POSITIVE_INFINITY) return true;
|
|
591
|
+
const cap = lcsMaxCells ?? DEFAULT_LCS_MAX_CELLS;
|
|
592
|
+
if (!Number.isFinite(cap) || cap < 1) return false;
|
|
593
|
+
return (baseLength + 1) * (nextLength + 1) <= cap;
|
|
594
|
+
}
|
|
380
595
|
function compactArrayOps(ops) {
|
|
381
596
|
const out = [];
|
|
382
597
|
for (let i = 0; i < ops.length; i++) {
|
|
@@ -614,26 +829,31 @@ function compileErrorFromLookup(error, path, opIndex) {
|
|
|
614
829
|
return compileError(mapped.reason, mapped.message, path, opIndex);
|
|
615
830
|
}
|
|
616
831
|
function mapLookupErrorToPatchReason(error) {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
832
|
+
if (error instanceof JsonLookupError) switch (error.code) {
|
|
833
|
+
case "EXPECTED_ARRAY_INDEX": return {
|
|
834
|
+
reason: "INVALID_POINTER",
|
|
835
|
+
message: error.message
|
|
836
|
+
};
|
|
837
|
+
case "INDEX_OUT_OF_BOUNDS": return {
|
|
838
|
+
reason: "OUT_OF_BOUNDS",
|
|
839
|
+
message: error.message
|
|
840
|
+
};
|
|
841
|
+
case "MISSING_KEY": return {
|
|
842
|
+
reason: "MISSING_PARENT",
|
|
843
|
+
message: error.message
|
|
844
|
+
};
|
|
845
|
+
case "NON_CONTAINER": return {
|
|
846
|
+
reason: "INVALID_TARGET",
|
|
847
|
+
message: error.message
|
|
848
|
+
};
|
|
849
|
+
default: return {
|
|
850
|
+
reason: "INVALID_PATCH",
|
|
851
|
+
message: error.message
|
|
852
|
+
};
|
|
853
|
+
}
|
|
634
854
|
return {
|
|
635
855
|
reason: "INVALID_PATCH",
|
|
636
|
-
message
|
|
856
|
+
message: error instanceof Error ? error.message : "invalid path"
|
|
637
857
|
};
|
|
638
858
|
}
|
|
639
859
|
function compileError(reason, message, path, opIndex) {
|
|
@@ -751,6 +971,10 @@ function ensureSeqAtPath(head, path, dotForCreate) {
|
|
|
751
971
|
return head.root;
|
|
752
972
|
}
|
|
753
973
|
function deepNodeFromJson(value, dot) {
|
|
974
|
+
return deepNodeFromJsonWithDepth(value, dot, 0);
|
|
975
|
+
}
|
|
976
|
+
function deepNodeFromJsonWithDepth(value, dot, depth) {
|
|
977
|
+
assertTraversalDepth(depth);
|
|
754
978
|
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return newReg(value, dot);
|
|
755
979
|
if (Array.isArray(value)) {
|
|
756
980
|
const seq = newSeq();
|
|
@@ -762,40 +986,123 @@ function deepNodeFromJson(value, dot) {
|
|
|
762
986
|
ctr: ++ctr
|
|
763
987
|
};
|
|
764
988
|
const id = dotToElemId(childDot);
|
|
765
|
-
rgaInsertAfter(seq, prev, id, childDot,
|
|
989
|
+
rgaInsertAfter(seq, prev, id, childDot, deepNodeFromJsonWithDepth(v, childDot, depth + 1));
|
|
766
990
|
prev = id;
|
|
767
991
|
}
|
|
768
992
|
return seq;
|
|
769
993
|
}
|
|
770
994
|
const obj = newObj();
|
|
771
|
-
for (const [k, v] of Object.entries(value)) objSet(obj, k,
|
|
995
|
+
for (const [k, v] of Object.entries(value)) objSet(obj, k, deepNodeFromJsonWithDepth(v, dot, depth + 1), dot);
|
|
772
996
|
return obj;
|
|
773
997
|
}
|
|
774
998
|
function nodeFromJson(value, nextDot) {
|
|
775
|
-
if (value
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
999
|
+
if (isJsonPrimitive(value)) return newReg(value, nextDot());
|
|
1000
|
+
const root = Array.isArray(value) ? newSeq() : newObj();
|
|
1001
|
+
const stack = [];
|
|
1002
|
+
if (Array.isArray(value)) stack.push({
|
|
1003
|
+
kind: "seq",
|
|
1004
|
+
depth: 0,
|
|
1005
|
+
values: value,
|
|
1006
|
+
index: 0,
|
|
1007
|
+
prev: HEAD,
|
|
1008
|
+
target: root
|
|
1009
|
+
});
|
|
1010
|
+
else stack.push({
|
|
1011
|
+
kind: "obj",
|
|
1012
|
+
depth: 0,
|
|
1013
|
+
entries: Object.entries(value),
|
|
1014
|
+
index: 0,
|
|
1015
|
+
target: root
|
|
1016
|
+
});
|
|
1017
|
+
while (stack.length > 0) {
|
|
1018
|
+
const frame = stack[stack.length - 1];
|
|
1019
|
+
if (frame.kind === "obj") {
|
|
1020
|
+
if (frame.index >= frame.entries.length) {
|
|
1021
|
+
stack.pop();
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
const [key, childValue] = frame.entries[frame.index++];
|
|
1025
|
+
const childDepth = frame.depth + 1;
|
|
1026
|
+
assertTraversalDepth(childDepth);
|
|
1027
|
+
const entryDot = nextDot();
|
|
1028
|
+
if (isJsonPrimitive(childValue)) {
|
|
1029
|
+
objSet(frame.target, key, newReg(childValue, nextDot()), entryDot);
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1032
|
+
if (Array.isArray(childValue)) {
|
|
1033
|
+
const childSeq = newSeq();
|
|
1034
|
+
objSet(frame.target, key, childSeq, entryDot);
|
|
1035
|
+
stack.push({
|
|
1036
|
+
kind: "seq",
|
|
1037
|
+
depth: childDepth,
|
|
1038
|
+
values: childValue,
|
|
1039
|
+
index: 0,
|
|
1040
|
+
prev: HEAD,
|
|
1041
|
+
target: childSeq
|
|
1042
|
+
});
|
|
1043
|
+
continue;
|
|
1044
|
+
}
|
|
1045
|
+
const childObj = newObj();
|
|
1046
|
+
objSet(frame.target, key, childObj, entryDot);
|
|
1047
|
+
stack.push({
|
|
1048
|
+
kind: "obj",
|
|
1049
|
+
depth: childDepth,
|
|
1050
|
+
entries: Object.entries(childValue),
|
|
1051
|
+
index: 0,
|
|
1052
|
+
target: childObj
|
|
1053
|
+
});
|
|
1054
|
+
continue;
|
|
784
1055
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
const
|
|
790
|
-
|
|
1056
|
+
if (frame.index >= frame.values.length) {
|
|
1057
|
+
stack.pop();
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
const childValue = frame.values[frame.index++];
|
|
1061
|
+
const childDepth = frame.depth + 1;
|
|
1062
|
+
assertTraversalDepth(childDepth);
|
|
1063
|
+
const insDot = nextDot();
|
|
1064
|
+
const id = dotToElemId(insDot);
|
|
1065
|
+
if (isJsonPrimitive(childValue)) {
|
|
1066
|
+
rgaInsertAfter(frame.target, frame.prev, id, insDot, newReg(childValue, nextDot()));
|
|
1067
|
+
frame.prev = id;
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
1070
|
+
if (Array.isArray(childValue)) {
|
|
1071
|
+
const childSeq = newSeq();
|
|
1072
|
+
rgaInsertAfter(frame.target, frame.prev, id, insDot, childSeq);
|
|
1073
|
+
frame.prev = id;
|
|
1074
|
+
stack.push({
|
|
1075
|
+
kind: "seq",
|
|
1076
|
+
depth: childDepth,
|
|
1077
|
+
values: childValue,
|
|
1078
|
+
index: 0,
|
|
1079
|
+
prev: HEAD,
|
|
1080
|
+
target: childSeq
|
|
1081
|
+
});
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
const childObj = newObj();
|
|
1085
|
+
rgaInsertAfter(frame.target, frame.prev, id, insDot, childObj);
|
|
1086
|
+
frame.prev = id;
|
|
1087
|
+
stack.push({
|
|
1088
|
+
kind: "obj",
|
|
1089
|
+
depth: childDepth,
|
|
1090
|
+
entries: Object.entries(childValue),
|
|
1091
|
+
index: 0,
|
|
1092
|
+
target: childObj
|
|
1093
|
+
});
|
|
791
1094
|
}
|
|
792
|
-
return
|
|
1095
|
+
return root;
|
|
793
1096
|
}
|
|
794
1097
|
/** Deep-clone a CRDT document. The clone is fully independent of the original. */
|
|
795
1098
|
function cloneDoc(doc) {
|
|
796
1099
|
return { root: cloneNode(doc.root) };
|
|
797
1100
|
}
|
|
798
1101
|
function cloneNode(node) {
|
|
1102
|
+
return cloneNodeAtDepth(node, 0);
|
|
1103
|
+
}
|
|
1104
|
+
function cloneNodeAtDepth(node, depth) {
|
|
1105
|
+
assertTraversalDepth(depth);
|
|
799
1106
|
if (node.kind === "lww") return {
|
|
800
1107
|
kind: "lww",
|
|
801
1108
|
value: structuredClone(node.value),
|
|
@@ -807,7 +1114,7 @@ function cloneNode(node) {
|
|
|
807
1114
|
if (node.kind === "obj") {
|
|
808
1115
|
const entries = /* @__PURE__ */ new Map();
|
|
809
1116
|
for (const [k, v] of node.entries.entries()) entries.set(k, {
|
|
810
|
-
node:
|
|
1117
|
+
node: cloneNodeAtDepth(v.node, depth + 1),
|
|
811
1118
|
dot: {
|
|
812
1119
|
actor: v.dot.actor,
|
|
813
1120
|
ctr: v.dot.ctr
|
|
@@ -829,7 +1136,7 @@ function cloneNode(node) {
|
|
|
829
1136
|
id: e.id,
|
|
830
1137
|
prev: e.prev,
|
|
831
1138
|
tombstone: e.tombstone,
|
|
832
|
-
value:
|
|
1139
|
+
value: cloneNodeAtDepth(e.value, depth + 1),
|
|
833
1140
|
insDot: {
|
|
834
1141
|
actor: e.insDot.actor,
|
|
835
1142
|
ctr: e.insDot.ctr
|
|
@@ -840,6 +1147,9 @@ function cloneNode(node) {
|
|
|
840
1147
|
elems
|
|
841
1148
|
};
|
|
842
1149
|
}
|
|
1150
|
+
function isJsonPrimitive(value) {
|
|
1151
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
1152
|
+
}
|
|
843
1153
|
function applyTest(base, head, it, evalTestAgainst) {
|
|
844
1154
|
const snapshot = evalTestAgainst === "head" ? materialize(head.root) : materialize(base.root);
|
|
845
1155
|
let got;
|
|
@@ -910,12 +1220,15 @@ function applyObjRemove(head, it, newDot) {
|
|
|
910
1220
|
return null;
|
|
911
1221
|
}
|
|
912
1222
|
function applyArrInsert(base, head, it, newDot, bumpCounterAbove) {
|
|
1223
|
+
const pointer = `/${it.path.join("/")}`;
|
|
913
1224
|
const baseSeq = getSeqAtPath(base, it.path);
|
|
914
1225
|
if (!baseSeq) {
|
|
915
1226
|
if (it.index === 0 || it.index === Number.POSITIVE_INFINITY) {
|
|
916
1227
|
const headSeq = ensureSeqAtPath(head, it.path, newDot());
|
|
917
1228
|
const prev = it.index === 0 ? HEAD : rgaPrevForInsertAtIndex(headSeq, Number.MAX_SAFE_INTEGER);
|
|
918
|
-
const
|
|
1229
|
+
const dotRes = nextInsertDotForPrev(headSeq, prev, newDot, pointer, bumpCounterAbove);
|
|
1230
|
+
if (!dotRes.ok) return dotRes;
|
|
1231
|
+
const d = dotRes.dot;
|
|
919
1232
|
rgaInsertAfter(headSeq, prev, dotToElemId(d), d, nodeFromJson(it.value, newDot));
|
|
920
1233
|
return null;
|
|
921
1234
|
}
|
|
@@ -924,7 +1237,7 @@ function applyArrInsert(base, head, it, newDot, bumpCounterAbove) {
|
|
|
924
1237
|
code: 409,
|
|
925
1238
|
reason: "MISSING_PARENT",
|
|
926
1239
|
message: `base array missing at /${it.path.join("/")}`,
|
|
927
|
-
path:
|
|
1240
|
+
path: pointer
|
|
928
1241
|
};
|
|
929
1242
|
}
|
|
930
1243
|
const headSeq = ensureSeqAtPath(head, it.path, newDot());
|
|
@@ -938,20 +1251,38 @@ function applyArrInsert(base, head, it, newDot, bumpCounterAbove) {
|
|
|
938
1251
|
path: `/${it.path.join("/")}/${it.index}`
|
|
939
1252
|
};
|
|
940
1253
|
const prev = idx === 0 ? HEAD : rgaIdAtIndex(baseSeq, idx - 1) ?? HEAD;
|
|
941
|
-
const
|
|
1254
|
+
const dotRes = nextInsertDotForPrev(headSeq, prev, newDot, pointer, bumpCounterAbove);
|
|
1255
|
+
if (!dotRes.ok) return dotRes;
|
|
1256
|
+
const d = dotRes.dot;
|
|
942
1257
|
rgaInsertAfter(headSeq, prev, dotToElemId(d), d, nodeFromJson(it.value, newDot));
|
|
943
1258
|
return null;
|
|
944
1259
|
}
|
|
945
|
-
function nextInsertDotForPrev(seq, prev, newDot, bumpCounterAbove) {
|
|
1260
|
+
function nextInsertDotForPrev(seq, prev, newDot, path, bumpCounterAbove) {
|
|
1261
|
+
const MAX_INSERT_DOT_ATTEMPTS = 1024;
|
|
946
1262
|
let maxSiblingDot = null;
|
|
947
1263
|
for (const elem of seq.elems.values()) {
|
|
948
1264
|
if (elem.prev !== prev) continue;
|
|
949
1265
|
if (!maxSiblingDot || compareDot(elem.insDot, maxSiblingDot) > 0) maxSiblingDot = elem.insDot;
|
|
950
1266
|
}
|
|
951
1267
|
if (maxSiblingDot) bumpCounterAbove?.(maxSiblingDot.ctr);
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1268
|
+
if (!maxSiblingDot) return {
|
|
1269
|
+
ok: true,
|
|
1270
|
+
dot: newDot()
|
|
1271
|
+
};
|
|
1272
|
+
for (let attempts = 0; attempts < MAX_INSERT_DOT_ATTEMPTS; attempts++) {
|
|
1273
|
+
const candidate = newDot();
|
|
1274
|
+
if (compareDot(candidate, maxSiblingDot) > 0) return {
|
|
1275
|
+
ok: true,
|
|
1276
|
+
dot: candidate
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
return {
|
|
1280
|
+
ok: false,
|
|
1281
|
+
code: 409,
|
|
1282
|
+
reason: "DOT_GENERATION_EXHAUSTED",
|
|
1283
|
+
message: `failed to generate insert dot within ${MAX_INSERT_DOT_ATTEMPTS} attempts`,
|
|
1284
|
+
path
|
|
1285
|
+
};
|
|
955
1286
|
}
|
|
956
1287
|
function applyArrDelete(base, head, it, newDot) {
|
|
957
1288
|
const d = newDot();
|
|
@@ -1183,6 +1514,7 @@ function isJsonPatchToCrdtOptions(value) {
|
|
|
1183
1514
|
return typeof value === "object" && value !== null && "base" in value && "head" in value && "patch" in value && "newDot" in value;
|
|
1184
1515
|
}
|
|
1185
1516
|
function toApplyError$1(error) {
|
|
1517
|
+
if (error instanceof TraversalDepthError) return toDepthApplyError(error);
|
|
1186
1518
|
if (error instanceof PatchCompileError) return {
|
|
1187
1519
|
ok: false,
|
|
1188
1520
|
code: 409,
|
|
@@ -1288,11 +1620,18 @@ function tryApplyPatch(state, patch, options = {}) {
|
|
|
1288
1620
|
doc: cloneDoc(state.doc),
|
|
1289
1621
|
clock: cloneClock(state.clock)
|
|
1290
1622
|
};
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
ok
|
|
1294
|
-
|
|
1295
|
-
|
|
1623
|
+
try {
|
|
1624
|
+
const result = applyPatchInternal(nextState, patch, options);
|
|
1625
|
+
if (!result.ok) return {
|
|
1626
|
+
ok: false,
|
|
1627
|
+
error: result
|
|
1628
|
+
};
|
|
1629
|
+
} catch (error) {
|
|
1630
|
+
return {
|
|
1631
|
+
ok: false,
|
|
1632
|
+
error: toApplyError(error)
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1296
1635
|
return {
|
|
1297
1636
|
ok: true,
|
|
1298
1637
|
state: nextState
|
|
@@ -1308,11 +1647,18 @@ function tryApplyPatchInPlace(state, patch, options = {}) {
|
|
|
1308
1647
|
state.clock = next.state.clock;
|
|
1309
1648
|
return { ok: true };
|
|
1310
1649
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
ok
|
|
1314
|
-
|
|
1315
|
-
|
|
1650
|
+
try {
|
|
1651
|
+
const result = applyPatchInternal(state, patch, applyOptions);
|
|
1652
|
+
if (!result.ok) return {
|
|
1653
|
+
ok: false,
|
|
1654
|
+
error: result
|
|
1655
|
+
};
|
|
1656
|
+
} catch (error) {
|
|
1657
|
+
return {
|
|
1658
|
+
ok: false,
|
|
1659
|
+
error: toApplyError(error)
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1316
1662
|
return { ok: true };
|
|
1317
1663
|
}
|
|
1318
1664
|
/**
|
|
@@ -1362,7 +1708,7 @@ function applyPatchInternal(state, patch, options) {
|
|
|
1362
1708
|
clock: createClock("__base__", 0)
|
|
1363
1709
|
} : null;
|
|
1364
1710
|
for (const [opIndex, op] of patch.entries()) {
|
|
1365
|
-
const step = applyPatchOpSequential(state, op, options, explicitBaseState ? explicitBaseState.doc :
|
|
1711
|
+
const step = applyPatchOpSequential(state, op, options, explicitBaseState ? explicitBaseState.doc : state.doc, opIndex);
|
|
1366
1712
|
if (!step.ok) return step;
|
|
1367
1713
|
if (explicitBaseState && op.op !== "test") {
|
|
1368
1714
|
const baseStep = applyPatchInternal(explicitBaseState, [op], {
|
|
@@ -1385,12 +1731,13 @@ function applyPatchOpSequential(state, op, options, baseDoc, opIndex) {
|
|
|
1385
1731
|
const fromResolved = resolveValueAtPointer(baseJson, op.from, opIndex);
|
|
1386
1732
|
if (!fromResolved.ok) return fromResolved;
|
|
1387
1733
|
const fromValue = fromResolved.value;
|
|
1388
|
-
const removeRes = applySinglePatchOp(state, baseDoc, {
|
|
1734
|
+
const removeRes = applySinglePatchOp(state, baseDoc, baseJson, {
|
|
1389
1735
|
op: "remove",
|
|
1390
1736
|
path: op.from
|
|
1391
1737
|
}, options);
|
|
1392
1738
|
if (!removeRes.ok) return removeRes;
|
|
1393
|
-
|
|
1739
|
+
const addBase = state.doc;
|
|
1740
|
+
return applySinglePatchOp(state, addBase, materialize(addBase.root), {
|
|
1394
1741
|
op: "add",
|
|
1395
1742
|
path: op.path,
|
|
1396
1743
|
value: fromValue
|
|
@@ -1400,13 +1747,13 @@ function applyPatchOpSequential(state, op, options, baseDoc, opIndex) {
|
|
|
1400
1747
|
const fromResolved = resolveValueAtPointer(baseJson, op.from, opIndex);
|
|
1401
1748
|
if (!fromResolved.ok) return fromResolved;
|
|
1402
1749
|
const fromValue = fromResolved.value;
|
|
1403
|
-
return applySinglePatchOp(state, baseDoc, {
|
|
1750
|
+
return applySinglePatchOp(state, baseDoc, baseJson, {
|
|
1404
1751
|
op: "add",
|
|
1405
1752
|
path: op.path,
|
|
1406
1753
|
value: fromValue
|
|
1407
1754
|
}, options);
|
|
1408
1755
|
}
|
|
1409
|
-
return applySinglePatchOp(state, baseDoc, op, options);
|
|
1756
|
+
return applySinglePatchOp(state, baseDoc, baseJson, op, options);
|
|
1410
1757
|
}
|
|
1411
1758
|
function resolveValueAtPointer(baseJson, pointer, opIndex) {
|
|
1412
1759
|
let path;
|
|
@@ -1424,8 +1771,8 @@ function resolveValueAtPointer(baseJson, pointer, opIndex) {
|
|
|
1424
1771
|
return toPointerLookupApplyError(error, pointer, opIndex);
|
|
1425
1772
|
}
|
|
1426
1773
|
}
|
|
1427
|
-
function applySinglePatchOp(state, baseDoc, op, options) {
|
|
1428
|
-
const compiled = compileIntents(
|
|
1774
|
+
function applySinglePatchOp(state, baseDoc, baseJson, op, options) {
|
|
1775
|
+
const compiled = compileIntents(baseJson, [op], "sequential");
|
|
1429
1776
|
if (!compiled.ok) return compiled;
|
|
1430
1777
|
return applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr));
|
|
1431
1778
|
}
|
|
@@ -1443,30 +1790,41 @@ function compileIntents(baseJson, patch, semantics = "sequential") {
|
|
|
1443
1790
|
}
|
|
1444
1791
|
}
|
|
1445
1792
|
function maxCtrInNodeForActor$1(node, actor) {
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1793
|
+
let best = 0;
|
|
1794
|
+
const stack = [{
|
|
1795
|
+
node,
|
|
1796
|
+
depth: 0
|
|
1797
|
+
}];
|
|
1798
|
+
while (stack.length > 0) {
|
|
1799
|
+
const frame = stack.pop();
|
|
1800
|
+
assertTraversalDepth(frame.depth);
|
|
1801
|
+
if (frame.node.kind === "lww") {
|
|
1802
|
+
if (frame.node.dot.actor === actor && frame.node.dot.ctr > best) best = frame.node.dot.ctr;
|
|
1803
|
+
continue;
|
|
1804
|
+
}
|
|
1805
|
+
if (frame.node.kind === "obj") {
|
|
1806
|
+
for (const entry of frame.node.entries.values()) {
|
|
1451
1807
|
if (entry.dot.actor === actor && entry.dot.ctr > best) best = entry.dot.ctr;
|
|
1452
|
-
|
|
1453
|
-
|
|
1808
|
+
stack.push({
|
|
1809
|
+
node: entry.node,
|
|
1810
|
+
depth: frame.depth + 1
|
|
1811
|
+
});
|
|
1454
1812
|
}
|
|
1455
|
-
for (const tomb of node.tombstone.values()) if (tomb.actor === actor && tomb.ctr > best) best = tomb.ctr;
|
|
1456
|
-
|
|
1813
|
+
for (const tomb of frame.node.tombstone.values()) if (tomb.actor === actor && tomb.ctr > best) best = tomb.ctr;
|
|
1814
|
+
continue;
|
|
1457
1815
|
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
}
|
|
1465
|
-
return best;
|
|
1816
|
+
for (const elem of frame.node.elems.values()) {
|
|
1817
|
+
if (elem.insDot.actor === actor && elem.insDot.ctr > best) best = elem.insDot.ctr;
|
|
1818
|
+
stack.push({
|
|
1819
|
+
node: elem.value,
|
|
1820
|
+
depth: frame.depth + 1
|
|
1821
|
+
});
|
|
1466
1822
|
}
|
|
1467
1823
|
}
|
|
1824
|
+
return best;
|
|
1468
1825
|
}
|
|
1469
1826
|
function toApplyError(error) {
|
|
1827
|
+
if (error instanceof TraversalDepthError) return toDepthApplyError(error);
|
|
1470
1828
|
if (error instanceof PatchCompileError) return {
|
|
1471
1829
|
ok: false,
|
|
1472
1830
|
code: 409,
|
|
@@ -1506,13 +1864,27 @@ function toPointerLookupApplyError(error, pointer, opIndex) {
|
|
|
1506
1864
|
|
|
1507
1865
|
//#endregion
|
|
1508
1866
|
//#region src/serialize.ts
|
|
1867
|
+
const HEAD_ELEM_ID = "HEAD";
|
|
1868
|
+
var DeserializeError = class extends Error {
|
|
1869
|
+
code = 409;
|
|
1870
|
+
reason;
|
|
1871
|
+
path;
|
|
1872
|
+
constructor(reason, path, message) {
|
|
1873
|
+
super(message);
|
|
1874
|
+
this.name = "DeserializeError";
|
|
1875
|
+
this.reason = reason;
|
|
1876
|
+
this.path = path;
|
|
1877
|
+
}
|
|
1878
|
+
};
|
|
1509
1879
|
/** Serialize a CRDT document to a JSON-safe representation (Maps become plain objects). */
|
|
1510
1880
|
function serializeDoc(doc) {
|
|
1511
1881
|
return { root: serializeNode(doc.root) };
|
|
1512
1882
|
}
|
|
1513
1883
|
/** Reconstruct a CRDT document from its serialized form. */
|
|
1514
1884
|
function deserializeDoc(data) {
|
|
1515
|
-
|
|
1885
|
+
if (!isRecord(data)) fail("INVALID_SERIALIZED_SHAPE", "/", "serialized doc must be an object");
|
|
1886
|
+
if (!("root" in data)) fail("INVALID_SERIALIZED_SHAPE", "/root", "serialized doc is missing root");
|
|
1887
|
+
return { root: deserializeNode(data.root, "/root", 0) };
|
|
1516
1888
|
}
|
|
1517
1889
|
/** Serialize a full CRDT state (document + clock) to a JSON-safe representation. */
|
|
1518
1890
|
function serializeState(state) {
|
|
@@ -1526,7 +1898,11 @@ function serializeState(state) {
|
|
|
1526
1898
|
}
|
|
1527
1899
|
/** Reconstruct a full CRDT state from its serialized form, restoring the clock. */
|
|
1528
1900
|
function deserializeState(data) {
|
|
1529
|
-
|
|
1901
|
+
if (!isRecord(data)) fail("INVALID_SERIALIZED_SHAPE", "/", "serialized state must be an object");
|
|
1902
|
+
if (!("doc" in data)) fail("INVALID_SERIALIZED_SHAPE", "/doc", "serialized state is missing doc");
|
|
1903
|
+
if (!("clock" in data)) fail("INVALID_SERIALIZED_SHAPE", "/clock", "serialized state is missing clock");
|
|
1904
|
+
const clockRaw = asRecord(data.clock, "/clock");
|
|
1905
|
+
const clock = createClock(readActor(clockRaw.actor, "/clock/actor"), readCounter(clockRaw.ctr, "/clock/ctr"));
|
|
1530
1906
|
return {
|
|
1531
1907
|
doc: deserializeDoc(data.doc),
|
|
1532
1908
|
clock
|
|
@@ -1577,54 +1953,132 @@ function serializeNode(node) {
|
|
|
1577
1953
|
elems
|
|
1578
1954
|
};
|
|
1579
1955
|
}
|
|
1580
|
-
function deserializeNode(node) {
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1956
|
+
function deserializeNode(node, path, depth) {
|
|
1957
|
+
assertTraversalDepth(depth);
|
|
1958
|
+
const raw = asRecord(node, path);
|
|
1959
|
+
const kind = readString(raw.kind, `${path}/kind`);
|
|
1960
|
+
if (kind === "lww") {
|
|
1961
|
+
if (!("value" in raw)) fail("INVALID_SERIALIZED_SHAPE", `${path}/value`, "lww node is missing value");
|
|
1962
|
+
if (!("dot" in raw)) fail("INVALID_SERIALIZED_SHAPE", `${path}/dot`, "lww node is missing dot");
|
|
1963
|
+
return {
|
|
1964
|
+
kind: "lww",
|
|
1965
|
+
value: structuredClone(readJsonValue(raw.value, `${path}/value`, depth + 1)),
|
|
1966
|
+
dot: readDot(raw.dot, `${path}/dot`)
|
|
1967
|
+
};
|
|
1968
|
+
}
|
|
1969
|
+
if (kind === "obj") {
|
|
1970
|
+
const entriesRaw = asRecord(raw.entries, `${path}/entries`);
|
|
1971
|
+
const tombstoneRaw = asRecord(raw.tombstone, `${path}/tombstone`);
|
|
1590
1972
|
const entries = /* @__PURE__ */ new Map();
|
|
1591
|
-
for (const [k, v] of Object.entries(
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1973
|
+
for (const [k, v] of Object.entries(entriesRaw)) {
|
|
1974
|
+
const entryPath = `${path}/entries/${k}`;
|
|
1975
|
+
const entryRaw = asRecord(v, entryPath);
|
|
1976
|
+
entries.set(k, {
|
|
1977
|
+
node: deserializeNode(entryRaw.node, `${entryPath}/node`, depth + 1),
|
|
1978
|
+
dot: readDot(entryRaw.dot, `${entryPath}/dot`)
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1598
1981
|
const tombstone = /* @__PURE__ */ new Map();
|
|
1599
|
-
for (const [k, d] of Object.entries(
|
|
1600
|
-
actor: d.actor,
|
|
1601
|
-
ctr: d.ctr
|
|
1602
|
-
});
|
|
1982
|
+
for (const [k, d] of Object.entries(tombstoneRaw)) tombstone.set(k, readDot(d, `${path}/tombstone/${k}`));
|
|
1603
1983
|
return {
|
|
1604
1984
|
kind: "obj",
|
|
1605
1985
|
entries,
|
|
1606
1986
|
tombstone
|
|
1607
1987
|
};
|
|
1608
1988
|
}
|
|
1989
|
+
if (kind !== "seq") fail("INVALID_SERIALIZED_SHAPE", `${path}/kind`, `unsupported node kind '${kind}'`);
|
|
1990
|
+
const elemsRaw = asRecord(raw.elems, `${path}/elems`);
|
|
1609
1991
|
const elems = /* @__PURE__ */ new Map();
|
|
1610
|
-
for (const [id,
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1992
|
+
for (const [id, rawElem] of Object.entries(elemsRaw)) {
|
|
1993
|
+
const elemPath = `${path}/elems/${id}`;
|
|
1994
|
+
const elem = asRecord(rawElem, elemPath);
|
|
1995
|
+
const elemId = readString(elem.id, `${elemPath}/id`);
|
|
1996
|
+
if (elemId !== id) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/id`, `sequence element id '${elemId}' does not match key '${id}'`);
|
|
1997
|
+
const prev = readString(elem.prev, `${elemPath}/prev`);
|
|
1998
|
+
const tombstone = readBoolean(elem.tombstone, `${elemPath}/tombstone`);
|
|
1999
|
+
const value = deserializeNode(elem.value, `${elemPath}/value`, depth + 1);
|
|
2000
|
+
const insDot = readDot(elem.insDot, `${elemPath}/insDot`);
|
|
2001
|
+
if (dotToElemId(insDot) !== id) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/insDot`, "sequence element id must match its insertion dot");
|
|
2002
|
+
elems.set(id, {
|
|
2003
|
+
id,
|
|
2004
|
+
prev,
|
|
2005
|
+
tombstone,
|
|
2006
|
+
value,
|
|
2007
|
+
insDot
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
for (const elem of elems.values()) {
|
|
2011
|
+
if (elem.prev === elem.id) fail("INVALID_SERIALIZED_INVARIANT", `${path}/elems/${elem.id}/prev`, "sequence element cannot reference itself as predecessor");
|
|
2012
|
+
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`);
|
|
2013
|
+
}
|
|
1620
2014
|
return {
|
|
1621
2015
|
kind: "seq",
|
|
1622
2016
|
elems
|
|
1623
2017
|
};
|
|
1624
2018
|
}
|
|
2019
|
+
function asRecord(value, path) {
|
|
2020
|
+
if (!isRecord(value)) fail("INVALID_SERIALIZED_SHAPE", path, "expected object");
|
|
2021
|
+
return value;
|
|
2022
|
+
}
|
|
2023
|
+
function readDot(value, path) {
|
|
2024
|
+
const raw = asRecord(value, path);
|
|
2025
|
+
return {
|
|
2026
|
+
actor: readActor(raw.actor, `${path}/actor`),
|
|
2027
|
+
ctr: readCounter(raw.ctr, `${path}/ctr`)
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
function readActor(value, path) {
|
|
2031
|
+
const actor = readString(value, path);
|
|
2032
|
+
if (actor.length === 0) fail("INVALID_SERIALIZED_SHAPE", path, "actor must not be empty");
|
|
2033
|
+
return actor;
|
|
2034
|
+
}
|
|
2035
|
+
function readCounter(value, path) {
|
|
2036
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) fail("INVALID_SERIALIZED_SHAPE", path, "counter must be a non-negative safe integer");
|
|
2037
|
+
return value;
|
|
2038
|
+
}
|
|
2039
|
+
function readString(value, path) {
|
|
2040
|
+
if (typeof value !== "string") fail("INVALID_SERIALIZED_SHAPE", path, "expected string");
|
|
2041
|
+
return value;
|
|
2042
|
+
}
|
|
2043
|
+
function readBoolean(value, path) {
|
|
2044
|
+
if (typeof value !== "boolean") fail("INVALID_SERIALIZED_SHAPE", path, "expected boolean");
|
|
2045
|
+
return value;
|
|
2046
|
+
}
|
|
2047
|
+
function readJsonValue(value, path, depth) {
|
|
2048
|
+
assertJsonValue(value, path, depth);
|
|
2049
|
+
return value;
|
|
2050
|
+
}
|
|
2051
|
+
function assertJsonValue(value, path, depth) {
|
|
2052
|
+
assertTraversalDepth(depth);
|
|
2053
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return;
|
|
2054
|
+
if (typeof value === "number") {
|
|
2055
|
+
if (!Number.isFinite(value)) fail("INVALID_SERIALIZED_SHAPE", path, "json number must be finite");
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
if (Array.isArray(value)) {
|
|
2059
|
+
for (const [index, item] of value.entries()) assertJsonValue(item, `${path}/${index}`, depth + 1);
|
|
2060
|
+
return;
|
|
2061
|
+
}
|
|
2062
|
+
if (!isRecord(value)) fail("INVALID_SERIALIZED_SHAPE", path, "expected JSON value");
|
|
2063
|
+
for (const [key, child] of Object.entries(value)) assertJsonValue(child, `${path}/${key}`, depth + 1);
|
|
2064
|
+
}
|
|
2065
|
+
function fail(reason, path, message) {
|
|
2066
|
+
throw new DeserializeError(reason, path, message);
|
|
2067
|
+
}
|
|
2068
|
+
function isRecord(value) {
|
|
2069
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2070
|
+
}
|
|
1625
2071
|
|
|
1626
2072
|
//#endregion
|
|
1627
2073
|
//#region src/merge.ts
|
|
2074
|
+
var SharedElementMetadataMismatchError = class extends Error {
|
|
2075
|
+
path;
|
|
2076
|
+
constructor(path, id, field) {
|
|
2077
|
+
super(`shared RGA element '${id}' has conflicting ${field} metadata`);
|
|
2078
|
+
this.name = "SharedElementMetadataMismatchError";
|
|
2079
|
+
this.path = path;
|
|
2080
|
+
}
|
|
2081
|
+
};
|
|
1628
2082
|
/** Error thrown by throwing merge helpers (`mergeDoc` / `mergeState`). */
|
|
1629
2083
|
var MergeError = class extends Error {
|
|
1630
2084
|
code;
|
|
@@ -1634,7 +2088,7 @@ var MergeError = class extends Error {
|
|
|
1634
2088
|
super(error.message);
|
|
1635
2089
|
this.name = "MergeError";
|
|
1636
2090
|
this.code = error.code;
|
|
1637
|
-
this.reason =
|
|
2091
|
+
this.reason = error.reason;
|
|
1638
2092
|
this.path = error.path;
|
|
1639
2093
|
}
|
|
1640
2094
|
};
|
|
@@ -1658,21 +2112,39 @@ function mergeDoc(a, b, options = {}) {
|
|
|
1658
2112
|
}
|
|
1659
2113
|
/** Non-throwing `mergeDoc` variant with structured conflict details. */
|
|
1660
2114
|
function tryMergeDoc(a, b, options = {}) {
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
error: {
|
|
2115
|
+
try {
|
|
2116
|
+
const mismatchPath = options.requireSharedOrigin ?? true ? findSeqLineageMismatch(a.root, b.root, []) : null;
|
|
2117
|
+
if (mismatchPath) return {
|
|
1665
2118
|
ok: false,
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
2119
|
+
error: {
|
|
2120
|
+
ok: false,
|
|
2121
|
+
code: 409,
|
|
2122
|
+
reason: "LINEAGE_MISMATCH",
|
|
2123
|
+
message: `merge requires shared array origin at ${mismatchPath}`,
|
|
2124
|
+
path: mismatchPath
|
|
2125
|
+
}
|
|
2126
|
+
};
|
|
2127
|
+
return {
|
|
2128
|
+
ok: true,
|
|
2129
|
+
doc: { root: mergeNode(a.root, b.root) }
|
|
2130
|
+
};
|
|
2131
|
+
} catch (error) {
|
|
2132
|
+
if (error instanceof SharedElementMetadataMismatchError) return {
|
|
2133
|
+
ok: false,
|
|
2134
|
+
error: {
|
|
2135
|
+
ok: false,
|
|
2136
|
+
code: 409,
|
|
2137
|
+
reason: "LINEAGE_MISMATCH",
|
|
2138
|
+
message: error.message,
|
|
2139
|
+
path: error.path
|
|
2140
|
+
}
|
|
2141
|
+
};
|
|
2142
|
+
if (error instanceof TraversalDepthError) return {
|
|
2143
|
+
ok: false,
|
|
2144
|
+
error: toDepthApplyError(error)
|
|
2145
|
+
};
|
|
2146
|
+
throw error;
|
|
2147
|
+
}
|
|
1676
2148
|
}
|
|
1677
2149
|
/**
|
|
1678
2150
|
* Merge two CRDT states.
|
|
@@ -1705,25 +2177,42 @@ function tryMergeState(a, b, options = {}) {
|
|
|
1705
2177
|
};
|
|
1706
2178
|
}
|
|
1707
2179
|
function findSeqLineageMismatch(a, b, path) {
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
2180
|
+
const stack = [{
|
|
2181
|
+
a,
|
|
2182
|
+
b,
|
|
2183
|
+
path,
|
|
2184
|
+
depth: path.length
|
|
2185
|
+
}];
|
|
2186
|
+
while (stack.length > 0) {
|
|
2187
|
+
const frame = stack.pop();
|
|
2188
|
+
assertTraversalDepth(frame.depth);
|
|
2189
|
+
if (frame.a.kind === "seq" && frame.b.kind === "seq") {
|
|
2190
|
+
const hasElemsA = frame.a.elems.size > 0;
|
|
2191
|
+
const hasElemsB = frame.b.elems.size > 0;
|
|
2192
|
+
if (hasElemsA && hasElemsB) {
|
|
2193
|
+
let shared = false;
|
|
2194
|
+
for (const id of frame.a.elems.keys()) if (frame.b.elems.has(id)) {
|
|
2195
|
+
shared = true;
|
|
2196
|
+
break;
|
|
2197
|
+
}
|
|
2198
|
+
if (!shared) return `/${frame.path.join("/")}`;
|
|
1716
2199
|
}
|
|
1717
|
-
if (!shared) return `/${path.join("/")}`;
|
|
1718
2200
|
}
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
2201
|
+
if (frame.a.kind === "obj" && frame.b.kind === "obj") {
|
|
2202
|
+
const left = frame.a;
|
|
2203
|
+
const right = frame.b;
|
|
2204
|
+
const sharedKeys = [...left.entries.keys()].filter((key) => right.entries.has(key));
|
|
2205
|
+
for (let i = sharedKeys.length - 1; i >= 0; i--) {
|
|
2206
|
+
const key = sharedKeys[i];
|
|
2207
|
+
const nextA = left.entries.get(key).node;
|
|
2208
|
+
const nextB = right.entries.get(key).node;
|
|
2209
|
+
stack.push({
|
|
2210
|
+
a: nextA,
|
|
2211
|
+
b: nextB,
|
|
2212
|
+
path: [...frame.path, key],
|
|
2213
|
+
depth: frame.depth + 1
|
|
2214
|
+
});
|
|
2215
|
+
}
|
|
1727
2216
|
}
|
|
1728
2217
|
}
|
|
1729
2218
|
return null;
|
|
@@ -1735,28 +2224,38 @@ function maxObservedCtrForActor(doc, actor, a, b) {
|
|
|
1735
2224
|
return best;
|
|
1736
2225
|
}
|
|
1737
2226
|
function maxCtrInNodeForActor(node, actor) {
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
2227
|
+
let best = 0;
|
|
2228
|
+
const stack = [{
|
|
2229
|
+
node,
|
|
2230
|
+
depth: 0
|
|
2231
|
+
}];
|
|
2232
|
+
while (stack.length > 0) {
|
|
2233
|
+
const frame = stack.pop();
|
|
2234
|
+
assertTraversalDepth(frame.depth);
|
|
2235
|
+
if (frame.node.kind === "lww") {
|
|
2236
|
+
if (frame.node.dot.actor === actor && frame.node.dot.ctr > best) best = frame.node.dot.ctr;
|
|
2237
|
+
continue;
|
|
2238
|
+
}
|
|
2239
|
+
if (frame.node.kind === "obj") {
|
|
2240
|
+
for (const entry of frame.node.entries.values()) {
|
|
1743
2241
|
if (entry.dot.actor === actor && entry.dot.ctr > best) best = entry.dot.ctr;
|
|
1744
|
-
|
|
1745
|
-
|
|
2242
|
+
stack.push({
|
|
2243
|
+
node: entry.node,
|
|
2244
|
+
depth: frame.depth + 1
|
|
2245
|
+
});
|
|
1746
2246
|
}
|
|
1747
|
-
for (const tomb of node.tombstone.values()) if (tomb.actor === actor && tomb.ctr > best) best = tomb.ctr;
|
|
1748
|
-
|
|
2247
|
+
for (const tomb of frame.node.tombstone.values()) if (tomb.actor === actor && tomb.ctr > best) best = tomb.ctr;
|
|
2248
|
+
continue;
|
|
1749
2249
|
}
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
}
|
|
1757
|
-
return best;
|
|
2250
|
+
for (const elem of frame.node.elems.values()) {
|
|
2251
|
+
if (elem.insDot.actor === actor && elem.insDot.ctr > best) best = elem.insDot.ctr;
|
|
2252
|
+
stack.push({
|
|
2253
|
+
node: elem.value,
|
|
2254
|
+
depth: frame.depth + 1
|
|
2255
|
+
});
|
|
1758
2256
|
}
|
|
1759
2257
|
}
|
|
2258
|
+
return best;
|
|
1760
2259
|
}
|
|
1761
2260
|
function repDot(node) {
|
|
1762
2261
|
switch (node.kind) {
|
|
@@ -1781,11 +2280,15 @@ function repDot(node) {
|
|
|
1781
2280
|
}
|
|
1782
2281
|
}
|
|
1783
2282
|
function mergeNode(a, b) {
|
|
2283
|
+
return mergeNodeAtDepth(a, b, 0, []);
|
|
2284
|
+
}
|
|
2285
|
+
function mergeNodeAtDepth(a, b, depth, path) {
|
|
2286
|
+
assertTraversalDepth(depth);
|
|
1784
2287
|
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);
|
|
2288
|
+
if (a.kind === "obj" && b.kind === "obj") return mergeObj(a, b, depth + 1, path);
|
|
2289
|
+
if (a.kind === "seq" && b.kind === "seq") return mergeSeq(a, b, depth + 1, path);
|
|
2290
|
+
if (compareDot(repDot(a), repDot(b)) >= 0) return cloneNodeShallow(a, depth + 1);
|
|
2291
|
+
return cloneNodeShallow(b, depth + 1);
|
|
1789
2292
|
}
|
|
1790
2293
|
function mergeLww(a, b) {
|
|
1791
2294
|
if (compareDot(a.dot, b.dot) >= 0) return {
|
|
@@ -1799,7 +2302,8 @@ function mergeLww(a, b) {
|
|
|
1799
2302
|
dot: { ...b.dot }
|
|
1800
2303
|
};
|
|
1801
2304
|
}
|
|
1802
|
-
function mergeObj(a, b) {
|
|
2305
|
+
function mergeObj(a, b, depth, path) {
|
|
2306
|
+
assertTraversalDepth(depth);
|
|
1803
2307
|
const entries = /* @__PURE__ */ new Map();
|
|
1804
2308
|
const tombstone = /* @__PURE__ */ new Map();
|
|
1805
2309
|
const allTombKeys = new Set([...a.tombstone.keys(), ...b.tombstone.keys()]);
|
|
@@ -1816,15 +2320,15 @@ function mergeObj(a, b) {
|
|
|
1816
2320
|
const eb = b.entries.get(key);
|
|
1817
2321
|
let merged;
|
|
1818
2322
|
if (ea && eb) merged = {
|
|
1819
|
-
node:
|
|
2323
|
+
node: mergeNodeAtDepth(ea.node, eb.node, depth + 1, [...path, key]),
|
|
1820
2324
|
dot: compareDot(ea.dot, eb.dot) >= 0 ? { ...ea.dot } : { ...eb.dot }
|
|
1821
2325
|
};
|
|
1822
2326
|
else if (ea) merged = {
|
|
1823
|
-
node: cloneNodeShallow(ea.node),
|
|
2327
|
+
node: cloneNodeShallow(ea.node, depth + 1),
|
|
1824
2328
|
dot: { ...ea.dot }
|
|
1825
2329
|
};
|
|
1826
2330
|
else merged = {
|
|
1827
|
-
node: cloneNodeShallow(eb.node),
|
|
2331
|
+
node: cloneNodeShallow(eb.node, depth + 1),
|
|
1828
2332
|
dot: { ...eb.dot }
|
|
1829
2333
|
};
|
|
1830
2334
|
const td = tombstone.get(key);
|
|
@@ -1837,14 +2341,17 @@ function mergeObj(a, b) {
|
|
|
1837
2341
|
tombstone
|
|
1838
2342
|
};
|
|
1839
2343
|
}
|
|
1840
|
-
function mergeSeq(a, b) {
|
|
2344
|
+
function mergeSeq(a, b, depth, path) {
|
|
2345
|
+
assertTraversalDepth(depth);
|
|
1841
2346
|
const elems = /* @__PURE__ */ new Map();
|
|
1842
2347
|
const allIds = new Set([...a.elems.keys(), ...b.elems.keys()]);
|
|
1843
2348
|
for (const id of allIds) {
|
|
1844
2349
|
const ea = a.elems.get(id);
|
|
1845
2350
|
const eb = b.elems.get(id);
|
|
1846
2351
|
if (ea && eb) {
|
|
1847
|
-
|
|
2352
|
+
if (ea.prev !== eb.prev) throw new SharedElementMetadataMismatchError(toPointer(path), id, "prev");
|
|
2353
|
+
if (!sameDot(ea.insDot, eb.insDot)) throw new SharedElementMetadataMismatchError(toPointer(path), id, "insDot");
|
|
2354
|
+
const mergedValue = mergeNodeAtDepth(ea.value, eb.value, depth + 1, [...path, id]);
|
|
1848
2355
|
elems.set(id, {
|
|
1849
2356
|
id,
|
|
1850
2357
|
prev: ea.prev,
|
|
@@ -1852,24 +2359,33 @@ function mergeSeq(a, b) {
|
|
|
1852
2359
|
value: mergedValue,
|
|
1853
2360
|
insDot: { ...ea.insDot }
|
|
1854
2361
|
});
|
|
1855
|
-
} else if (ea) elems.set(id, cloneElem(ea));
|
|
1856
|
-
else elems.set(id, cloneElem(eb));
|
|
2362
|
+
} else if (ea) elems.set(id, cloneElem(ea, depth + 1));
|
|
2363
|
+
else elems.set(id, cloneElem(eb, depth + 1));
|
|
1857
2364
|
}
|
|
1858
2365
|
return {
|
|
1859
2366
|
kind: "seq",
|
|
1860
2367
|
elems
|
|
1861
2368
|
};
|
|
1862
2369
|
}
|
|
1863
|
-
function
|
|
2370
|
+
function sameDot(a, b) {
|
|
2371
|
+
return a.actor === b.actor && a.ctr === b.ctr;
|
|
2372
|
+
}
|
|
2373
|
+
function toPointer(path) {
|
|
2374
|
+
if (path.length === 0) return "/";
|
|
2375
|
+
return `/${path.join("/")}`;
|
|
2376
|
+
}
|
|
2377
|
+
function cloneElem(e, depth) {
|
|
2378
|
+
assertTraversalDepth(depth);
|
|
1864
2379
|
return {
|
|
1865
2380
|
id: e.id,
|
|
1866
2381
|
prev: e.prev,
|
|
1867
2382
|
tombstone: e.tombstone,
|
|
1868
|
-
value: cloneNodeShallow(e.value),
|
|
2383
|
+
value: cloneNodeShallow(e.value, depth + 1),
|
|
1869
2384
|
insDot: { ...e.insDot }
|
|
1870
2385
|
};
|
|
1871
2386
|
}
|
|
1872
|
-
function cloneNodeShallow(node) {
|
|
2387
|
+
function cloneNodeShallow(node, depth) {
|
|
2388
|
+
assertTraversalDepth(depth);
|
|
1873
2389
|
switch (node.kind) {
|
|
1874
2390
|
case "lww": return {
|
|
1875
2391
|
kind: "lww",
|
|
@@ -1879,7 +2395,7 @@ function cloneNodeShallow(node) {
|
|
|
1879
2395
|
case "obj": {
|
|
1880
2396
|
const entries = /* @__PURE__ */ new Map();
|
|
1881
2397
|
for (const [k, v] of node.entries) entries.set(k, {
|
|
1882
|
-
node: cloneNodeShallow(v.node),
|
|
2398
|
+
node: cloneNodeShallow(v.node, depth + 1),
|
|
1883
2399
|
dot: { ...v.dot }
|
|
1884
2400
|
});
|
|
1885
2401
|
const tombstone = /* @__PURE__ */ new Map();
|
|
@@ -1892,7 +2408,7 @@ function cloneNodeShallow(node) {
|
|
|
1892
2408
|
}
|
|
1893
2409
|
case "seq": {
|
|
1894
2410
|
const elems = /* @__PURE__ */ new Map();
|
|
1895
|
-
for (const [id, e] of node.elems) elems.set(id, cloneElem(e));
|
|
2411
|
+
for (const [id, e] of node.elems) elems.set(id, cloneElem(e, depth + 1));
|
|
1896
2412
|
return {
|
|
1897
2413
|
kind: "seq",
|
|
1898
2414
|
elems
|
|
@@ -1902,4 +2418,83 @@ function cloneNodeShallow(node) {
|
|
|
1902
2418
|
}
|
|
1903
2419
|
|
|
1904
2420
|
//#endregion
|
|
1905
|
-
|
|
2421
|
+
//#region src/compact.ts
|
|
2422
|
+
function isDotStable(stable, dot) {
|
|
2423
|
+
return (stable[dot.actor] ?? 0) >= dot.ctr;
|
|
2424
|
+
}
|
|
2425
|
+
/**
|
|
2426
|
+
* Compact causally-stable tombstones in a document.
|
|
2427
|
+
*
|
|
2428
|
+
* Safety note:
|
|
2429
|
+
* - Only compact at checkpoints that are causally stable across all peers you
|
|
2430
|
+
* may still merge with.
|
|
2431
|
+
* - Do not merge this compacted document with replicas that might be behind
|
|
2432
|
+
* the provided checkpoint.
|
|
2433
|
+
*/
|
|
2434
|
+
function compactDocTombstones(doc, options) {
|
|
2435
|
+
const targetDoc = options.mutate ? doc : cloneDoc(doc);
|
|
2436
|
+
const stats = {
|
|
2437
|
+
objectTombstonesRemoved: 0,
|
|
2438
|
+
sequenceTombstonesRemoved: 0
|
|
2439
|
+
};
|
|
2440
|
+
const stable = options.stable;
|
|
2441
|
+
const stack = [{
|
|
2442
|
+
node: targetDoc.root,
|
|
2443
|
+
depth: 0
|
|
2444
|
+
}];
|
|
2445
|
+
while (stack.length > 0) {
|
|
2446
|
+
const frame = stack.pop();
|
|
2447
|
+
assertTraversalDepth(frame.depth);
|
|
2448
|
+
if (frame.node.kind === "obj") {
|
|
2449
|
+
stats.objectTombstonesRemoved += objCompactTombstones(frame.node, (dot) => isDotStable(stable, dot));
|
|
2450
|
+
for (const entry of frame.node.entries.values()) stack.push({
|
|
2451
|
+
node: entry.node,
|
|
2452
|
+
depth: frame.depth + 1
|
|
2453
|
+
});
|
|
2454
|
+
continue;
|
|
2455
|
+
}
|
|
2456
|
+
if (frame.node.kind === "seq") {
|
|
2457
|
+
stats.sequenceTombstonesRemoved += rgaCompactTombstones(frame.node, (dot) => isDotStable(stable, dot));
|
|
2458
|
+
for (const elem of frame.node.elems.values()) stack.push({
|
|
2459
|
+
node: elem.value,
|
|
2460
|
+
depth: frame.depth + 1
|
|
2461
|
+
});
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
return {
|
|
2465
|
+
doc: targetDoc,
|
|
2466
|
+
stats
|
|
2467
|
+
};
|
|
2468
|
+
}
|
|
2469
|
+
/**
|
|
2470
|
+
* Compact causally-stable tombstones in a state document.
|
|
2471
|
+
*
|
|
2472
|
+
* Safety note:
|
|
2473
|
+
* - Only compact at checkpoints that are causally stable across all peers you
|
|
2474
|
+
* may still merge with.
|
|
2475
|
+
* - Do not merge this compacted state with replicas that might be behind the
|
|
2476
|
+
* provided checkpoint.
|
|
2477
|
+
*/
|
|
2478
|
+
function compactStateTombstones(state, options) {
|
|
2479
|
+
if (options.mutate) return {
|
|
2480
|
+
state,
|
|
2481
|
+
stats: compactDocTombstones(state.doc, {
|
|
2482
|
+
stable: options.stable,
|
|
2483
|
+
mutate: true
|
|
2484
|
+
}).stats
|
|
2485
|
+
};
|
|
2486
|
+
const nextState = {
|
|
2487
|
+
doc: cloneDoc(state.doc),
|
|
2488
|
+
clock: cloneClock(state.clock)
|
|
2489
|
+
};
|
|
2490
|
+
return {
|
|
2491
|
+
state: nextState,
|
|
2492
|
+
stats: compactDocTombstones(nextState.doc, {
|
|
2493
|
+
stable: options.stable,
|
|
2494
|
+
mutate: true
|
|
2495
|
+
}).stats
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
//#endregion
|
|
2500
|
+
export { rgaLinearizeIds as $, jsonPatchToCrdtSafe as A, lwwSet as B, applyIntentsToCrdt as C, docFromJson as D, crdtToJsonPatch as E, getAtJson as F, objRemove as G, newReg as H, jsonEquals as I, HEAD as J, objSet as K, parseJsonPointer as L, PatchCompileError as M, compileJsonPatchToIntent as N, docFromJsonWithDot as O, diffJsonPatch as P, rgaInsertAfter as Q, stringifyJsonPointer as R, validateJsonPatch as S, crdtToFullReplace as T, newSeq as U, newObj as V, objCompactTombstones as W, rgaDelete as X, rgaCompactTombstones as Y, rgaIdAtIndex as Z, createState as _, mergeState as a, MAX_TRAVERSAL_DEPTH as at, tryApplyPatch as b, DeserializeError as c, createClock as ct, serializeDoc as d, rgaPrevForInsertAtIndex as et, serializeState as f, applyPatchInPlace as g, applyPatchAsActor as h, mergeDoc as i, vvMerge as it, tryJsonPatchToCrdt as j, jsonPatchToCrdt as k, deserializeDoc as l, nextDotForActor as lt, applyPatch as m, compactStateTombstones as n, dotToElemId as nt, tryMergeDoc as o, TraversalDepthError as ot, PatchError as p, materialize as q, MergeError as r, vvHasDot as rt, tryMergeState as s, cloneClock as st, compactDocTombstones as t, compareDot as tt, deserializeState as u, observeDot as ut, forkState as v, cloneDoc as w, tryApplyPatchInPlace as x, toJson as y, ROOT_KEY as z };
|