json-patch-to-crdt 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +416 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +33 -0
- package/dist/index.mjs +3 -0
- package/dist/internals.d.mts +25 -0
- package/dist/internals.d.ts +25 -0
- package/dist/internals.js +51 -0
- package/dist/internals.mjs +3 -0
- package/dist/merge-B1BFMhJJ.js +1723 -0
- package/dist/merge-BpAUNaPe.d.mts +419 -0
- package/dist/merge-DikOFBWc.mjs +1435 -0
- package/dist/merge-QmPXxE6_.d.ts +419 -0
- package/package.json +58 -0
|
@@ -0,0 +1,1723 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/clock.ts
|
|
3
|
+
/**
|
|
4
|
+
* Create a new clock for the given actor. Each call to `clock.next()` yields a fresh `Dot`.
|
|
5
|
+
* @param actor - Unique identifier for this peer.
|
|
6
|
+
* @param start - Initial counter value (defaults to 0).
|
|
7
|
+
*/
|
|
8
|
+
function createClock(actor, start = 0) {
|
|
9
|
+
const clock = {
|
|
10
|
+
actor,
|
|
11
|
+
ctr: start,
|
|
12
|
+
next() {
|
|
13
|
+
clock.ctr += 1;
|
|
14
|
+
return {
|
|
15
|
+
actor: clock.actor,
|
|
16
|
+
ctr: clock.ctr
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
return clock;
|
|
21
|
+
}
|
|
22
|
+
/** Create an independent copy of a clock at the same counter position. */
|
|
23
|
+
function cloneClock(clock) {
|
|
24
|
+
return createClock(clock.actor, clock.ctr);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Generate the next per-actor dot from a mutable version vector.
|
|
28
|
+
* Useful when a server needs to mint dots for many actors.
|
|
29
|
+
*/
|
|
30
|
+
function nextDotForActor(vv, actor) {
|
|
31
|
+
const ctr = (vv[actor] ?? 0) + 1;
|
|
32
|
+
vv[actor] = ctr;
|
|
33
|
+
return {
|
|
34
|
+
actor,
|
|
35
|
+
ctr
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Record an observed dot in a version vector. */
|
|
39
|
+
function observeDot(vv, dot) {
|
|
40
|
+
if ((vv[dot.actor] ?? 0) < dot.ctr) vv[dot.actor] = dot.ctr;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/dot.ts
|
|
45
|
+
function compareDot(a, b) {
|
|
46
|
+
if (a.ctr !== b.ctr) return a.ctr - b.ctr;
|
|
47
|
+
return a.actor < b.actor ? -1 : a.actor > b.actor ? 1 : 0;
|
|
48
|
+
}
|
|
49
|
+
function vvHasDot(vv, d) {
|
|
50
|
+
return (vv[d.actor] ?? 0) >= d.ctr;
|
|
51
|
+
}
|
|
52
|
+
function vvMerge(a, b) {
|
|
53
|
+
const out = { ...a };
|
|
54
|
+
for (const [actor, ctr] of Object.entries(b)) out[actor] = Math.max(out[actor] ?? 0, ctr);
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
function dotToElemId(d) {
|
|
58
|
+
return `${d.actor}:${d.ctr}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/rga.ts
|
|
63
|
+
const HEAD = "HEAD";
|
|
64
|
+
const linearCache = /* @__PURE__ */ new WeakMap();
|
|
65
|
+
const seqVersion = /* @__PURE__ */ new WeakMap();
|
|
66
|
+
function getVersion(seq) {
|
|
67
|
+
return seqVersion.get(seq) ?? 0;
|
|
68
|
+
}
|
|
69
|
+
function bumpVersion(seq) {
|
|
70
|
+
seqVersion.set(seq, getVersion(seq) + 1);
|
|
71
|
+
}
|
|
72
|
+
function rgaChildrenIndex(seq) {
|
|
73
|
+
const idx = /* @__PURE__ */ new Map();
|
|
74
|
+
for (const e of seq.elems.values()) {
|
|
75
|
+
const arr = idx.get(e.prev) ?? [];
|
|
76
|
+
arr.push(e);
|
|
77
|
+
idx.set(e.prev, arr);
|
|
78
|
+
}
|
|
79
|
+
for (const arr of idx.values()) arr.sort((a, b) => compareDot(b.insDot, a.insDot));
|
|
80
|
+
return idx;
|
|
81
|
+
}
|
|
82
|
+
function rgaLinearizeIds(seq) {
|
|
83
|
+
const ver = getVersion(seq);
|
|
84
|
+
const cached = linearCache.get(seq);
|
|
85
|
+
if (cached && cached.version === ver) return cached.ids;
|
|
86
|
+
const idx = rgaChildrenIndex(seq);
|
|
87
|
+
const out = [];
|
|
88
|
+
function walk(prev) {
|
|
89
|
+
const children = idx.get(prev);
|
|
90
|
+
if (!children) return;
|
|
91
|
+
for (const c of children) {
|
|
92
|
+
if (!c.tombstone) out.push(c.id);
|
|
93
|
+
walk(c.id);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
walk(HEAD);
|
|
97
|
+
linearCache.set(seq, {
|
|
98
|
+
version: ver,
|
|
99
|
+
ids: out
|
|
100
|
+
});
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
function rgaInsertAfter(seq, prev, id, insDot, value) {
|
|
104
|
+
if (seq.elems.has(id)) return;
|
|
105
|
+
seq.elems.set(id, {
|
|
106
|
+
id,
|
|
107
|
+
prev,
|
|
108
|
+
tombstone: false,
|
|
109
|
+
value,
|
|
110
|
+
insDot
|
|
111
|
+
});
|
|
112
|
+
bumpVersion(seq);
|
|
113
|
+
}
|
|
114
|
+
function rgaDelete(seq, id) {
|
|
115
|
+
const e = seq.elems.get(id);
|
|
116
|
+
if (!e) return;
|
|
117
|
+
if (e.tombstone) return;
|
|
118
|
+
e.tombstone = true;
|
|
119
|
+
bumpVersion(seq);
|
|
120
|
+
}
|
|
121
|
+
function rgaIdAtIndex(seq, index) {
|
|
122
|
+
return rgaLinearizeIds(seq)[index];
|
|
123
|
+
}
|
|
124
|
+
function rgaPrevForInsertAtIndex(seq, index) {
|
|
125
|
+
if (index <= 0) return HEAD;
|
|
126
|
+
const ids = rgaLinearizeIds(seq);
|
|
127
|
+
return ids[index - 1] ?? (ids.length ? ids[ids.length - 1] : HEAD);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region src/materialize.ts
|
|
132
|
+
/** Recursively convert a CRDT node graph into a plain JSON value. */
|
|
133
|
+
function materialize(node) {
|
|
134
|
+
switch (node.kind) {
|
|
135
|
+
case "lww": return node.value;
|
|
136
|
+
case "obj": {
|
|
137
|
+
const out = {};
|
|
138
|
+
for (const [k, { node: child }] of node.entries.entries()) out[k] = materialize(child);
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
case "seq": return rgaLinearizeIds(node).map((id) => materialize(node.elems.get(id).value));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region src/nodes.ts
|
|
147
|
+
function newObj() {
|
|
148
|
+
return {
|
|
149
|
+
kind: "obj",
|
|
150
|
+
entries: /* @__PURE__ */ new Map(),
|
|
151
|
+
tombstone: /* @__PURE__ */ new Map()
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function newSeq() {
|
|
155
|
+
return {
|
|
156
|
+
kind: "seq",
|
|
157
|
+
elems: /* @__PURE__ */ new Map()
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function newReg(value, dot) {
|
|
161
|
+
return {
|
|
162
|
+
kind: "lww",
|
|
163
|
+
value,
|
|
164
|
+
dot
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function lwwSet(reg, value, dot) {
|
|
168
|
+
if (compareDot(reg.dot, dot) <= 0) {
|
|
169
|
+
reg.value = value;
|
|
170
|
+
reg.dot = dot;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function objSet(obj, key, node, dot) {
|
|
174
|
+
const delDot = obj.tombstone.get(key);
|
|
175
|
+
if (delDot && compareDot(delDot, dot) >= 0) return;
|
|
176
|
+
const cur = obj.entries.get(key);
|
|
177
|
+
if (!cur || compareDot(cur.dot, dot) <= 0) obj.entries.set(key, {
|
|
178
|
+
node,
|
|
179
|
+
dot
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function objRemove(obj, key, dot) {
|
|
183
|
+
const curDel = obj.tombstone.get(key);
|
|
184
|
+
if (!curDel || compareDot(curDel, dot) <= 0) obj.tombstone.set(key, dot);
|
|
185
|
+
obj.entries.delete(key);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region src/types.ts
|
|
190
|
+
/**
|
|
191
|
+
* Internal sentinel key used in `IntentOp` to represent root-level operations.
|
|
192
|
+
* Namespaced to avoid collision with user data keys.
|
|
193
|
+
*/
|
|
194
|
+
const ROOT_KEY = "@@crdt/root";
|
|
195
|
+
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region src/patch.ts
|
|
198
|
+
/**
|
|
199
|
+
* Parse an RFC 6901 JSON Pointer into a path array, unescaping `~1` and `~0`.
|
|
200
|
+
* @param ptr - A JSON Pointer string (e.g. `"/a/b"` or `""`).
|
|
201
|
+
* @returns An array of path segments.
|
|
202
|
+
*/
|
|
203
|
+
function parseJsonPointer(ptr) {
|
|
204
|
+
if (ptr === "") return [];
|
|
205
|
+
if (!ptr.startsWith("/")) throw new Error(`Invalid pointer: ${ptr}`);
|
|
206
|
+
return ptr.slice(1).split("/").map((s) => s.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
207
|
+
}
|
|
208
|
+
/** Convert a path array back to an RFC 6901 JSON Pointer string. */
|
|
209
|
+
function stringifyJsonPointer(path) {
|
|
210
|
+
if (path.length === 0) return "";
|
|
211
|
+
return `/${path.map(escapeJsonPointer).join("/")}`;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Navigate a JSON value by path and return the value at that location.
|
|
215
|
+
* Throws if the path is invalid, out of bounds, or traverses a non-container.
|
|
216
|
+
*/
|
|
217
|
+
function getAtJson(base, path) {
|
|
218
|
+
let cur = base;
|
|
219
|
+
for (const seg of path) if (Array.isArray(cur)) {
|
|
220
|
+
const idx = seg === "-" ? cur.length : Number(seg);
|
|
221
|
+
if (!Number.isInteger(idx)) throw new Error(`Expected array index, got ${seg}`);
|
|
222
|
+
if (idx < 0 || idx >= cur.length) throw new Error(`Index out of bounds at ${seg}`);
|
|
223
|
+
cur = cur[idx];
|
|
224
|
+
} else if (cur && typeof cur === "object") {
|
|
225
|
+
if (!(seg in cur)) throw new Error(`Missing key ${seg}`);
|
|
226
|
+
cur = cur[seg];
|
|
227
|
+
} else throw new Error(`Cannot traverse into non-container at ${seg}`);
|
|
228
|
+
return cur;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Compile RFC 6902 JSON Patch operations into CRDT intent operations.
|
|
232
|
+
* `move`/`copy` are expanded to `add` + optional `remove`. Array indices
|
|
233
|
+
* and the `"-"` append token are resolved against the base JSON.
|
|
234
|
+
* @param baseJson - The base JSON value for resolving paths.
|
|
235
|
+
* @param patch - Array of JSON Patch operations.
|
|
236
|
+
* @returns An array of `IntentOp` ready for `applyIntentsToCrdt`.
|
|
237
|
+
*/
|
|
238
|
+
function compileJsonPatchToIntent(baseJson, patch) {
|
|
239
|
+
const intents = [];
|
|
240
|
+
for (const op of patch) {
|
|
241
|
+
if (op.op === "test") {
|
|
242
|
+
intents.push({
|
|
243
|
+
t: "Test",
|
|
244
|
+
path: parseJsonPointer(op.path),
|
|
245
|
+
value: op.value
|
|
246
|
+
});
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (op.op === "copy" || op.op === "move") {
|
|
250
|
+
const val = getAtJson(baseJson, parseJsonPointer(op.from));
|
|
251
|
+
intents.push(...compileJsonPatchToIntent(baseJson, [{
|
|
252
|
+
op: "add",
|
|
253
|
+
path: op.path,
|
|
254
|
+
value: val
|
|
255
|
+
}]));
|
|
256
|
+
if (op.op === "move") intents.push(...compileJsonPatchToIntent(baseJson, [{
|
|
257
|
+
op: "remove",
|
|
258
|
+
path: op.from
|
|
259
|
+
}]));
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const path = parseJsonPointer(op.path);
|
|
263
|
+
const parent = path.slice(0, -1);
|
|
264
|
+
const last = path[path.length - 1];
|
|
265
|
+
if (path.length === 0) {
|
|
266
|
+
if (op.op === "replace" || op.op === "add") intents.push({
|
|
267
|
+
t: "ObjSet",
|
|
268
|
+
path: [],
|
|
269
|
+
key: ROOT_KEY,
|
|
270
|
+
value: op.value
|
|
271
|
+
});
|
|
272
|
+
else if (op.op === "remove") intents.push({
|
|
273
|
+
t: "ObjSet",
|
|
274
|
+
path: [],
|
|
275
|
+
key: ROOT_KEY,
|
|
276
|
+
value: null
|
|
277
|
+
});
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const isIndexLike = (s) => s === "-" || /^[0-9]+$/.test(s);
|
|
281
|
+
if (isIndexLike(last)) {
|
|
282
|
+
const index = last === "-" ? Number.POSITIVE_INFINITY : Number(last);
|
|
283
|
+
if (op.op === "add") intents.push({
|
|
284
|
+
t: "ArrInsert",
|
|
285
|
+
path: parent,
|
|
286
|
+
index,
|
|
287
|
+
value: op.value
|
|
288
|
+
});
|
|
289
|
+
else if (op.op === "remove") intents.push({
|
|
290
|
+
t: "ArrDelete",
|
|
291
|
+
path: parent,
|
|
292
|
+
index
|
|
293
|
+
});
|
|
294
|
+
else if (op.op === "replace") intents.push({
|
|
295
|
+
t: "ArrReplace",
|
|
296
|
+
path: parent,
|
|
297
|
+
index,
|
|
298
|
+
value: op.value
|
|
299
|
+
});
|
|
300
|
+
else assertNever$1(op, "Unsupported op at array index path");
|
|
301
|
+
} else {
|
|
302
|
+
const parentValue = pathValueAt(baseJson, parent);
|
|
303
|
+
if (!isPlainObject(parentValue)) throw new Error(`Expected object parent at ${stringifyJsonPointer(parent)}`);
|
|
304
|
+
if ((op.op === "replace" || op.op === "remove") && !hasOwn(parentValue, last)) throw new Error(`Missing key ${last} at ${stringifyJsonPointer(parent)}`);
|
|
305
|
+
if (op.op === "add") intents.push({
|
|
306
|
+
t: "ObjSet",
|
|
307
|
+
path: parent,
|
|
308
|
+
key: last,
|
|
309
|
+
value: op.value,
|
|
310
|
+
mode: "add"
|
|
311
|
+
});
|
|
312
|
+
else if (op.op === "replace") intents.push({
|
|
313
|
+
t: "ObjSet",
|
|
314
|
+
path: parent,
|
|
315
|
+
key: last,
|
|
316
|
+
value: op.value,
|
|
317
|
+
mode: "replace"
|
|
318
|
+
});
|
|
319
|
+
else if (op.op === "remove") intents.push({
|
|
320
|
+
t: "ObjRemove",
|
|
321
|
+
path: parent,
|
|
322
|
+
key: last
|
|
323
|
+
});
|
|
324
|
+
else assertNever$1(op, "Unsupported op");
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return intents;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Compute a JSON Patch delta between two JSON values.
|
|
331
|
+
* By default arrays use a deterministic LCS strategy.
|
|
332
|
+
* Pass `{ arrayStrategy: "atomic" }` for single-op array replacement.
|
|
333
|
+
* @param base - The original JSON value.
|
|
334
|
+
* @param next - The target JSON value.
|
|
335
|
+
* @param options - Diff options.
|
|
336
|
+
* @returns An array of JSON Patch operations that transform `base` into `next`.
|
|
337
|
+
*/
|
|
338
|
+
function diffJsonPatch(base, next, options = {}) {
|
|
339
|
+
const ops = [];
|
|
340
|
+
diffValue([], base, next, ops, options);
|
|
341
|
+
return ops;
|
|
342
|
+
}
|
|
343
|
+
function diffValue(path, base, next, ops, options) {
|
|
344
|
+
if (jsonEquals(base, next)) return;
|
|
345
|
+
if (Array.isArray(base) || Array.isArray(next)) {
|
|
346
|
+
if ((options.arrayStrategy ?? "lcs") === "lcs" && Array.isArray(base) && Array.isArray(next)) {
|
|
347
|
+
diffArray(path, base, next, ops, options);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
ops.push({
|
|
351
|
+
op: "replace",
|
|
352
|
+
path: stringifyJsonPointer(path),
|
|
353
|
+
value: next
|
|
354
|
+
});
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (!isPlainObject(base) || !isPlainObject(next)) {
|
|
358
|
+
ops.push({
|
|
359
|
+
op: "replace",
|
|
360
|
+
path: stringifyJsonPointer(path),
|
|
361
|
+
value: next
|
|
362
|
+
});
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const baseKeys = Object.keys(base).sort();
|
|
366
|
+
const nextKeys = Object.keys(next).sort();
|
|
367
|
+
const baseSet = new Set(baseKeys);
|
|
368
|
+
const nextSet = new Set(nextKeys);
|
|
369
|
+
for (const key of baseKeys) if (!nextSet.has(key)) ops.push({
|
|
370
|
+
op: "remove",
|
|
371
|
+
path: stringifyJsonPointer([...path, key])
|
|
372
|
+
});
|
|
373
|
+
for (const key of nextKeys) if (!baseSet.has(key)) {
|
|
374
|
+
const nextValue = next[key];
|
|
375
|
+
ops.push({
|
|
376
|
+
op: "add",
|
|
377
|
+
path: stringifyJsonPointer([...path, key]),
|
|
378
|
+
value: nextValue
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
for (const key of baseKeys) if (nextSet.has(key)) diffValue([...path, key], base[key], next[key], ops, options);
|
|
382
|
+
}
|
|
383
|
+
function diffArray(path, base, next, ops, _options) {
|
|
384
|
+
const n = base.length;
|
|
385
|
+
const m = next.length;
|
|
386
|
+
const lcs = Array.from({ length: n + 1 }, () => Array(m + 1).fill(0));
|
|
387
|
+
for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--) if (jsonEquals(base[i], next[j])) lcs[i][j] = 1 + lcs[i + 1][j + 1];
|
|
388
|
+
else lcs[i][j] = Math.max(lcs[i + 1][j], lcs[i][j + 1]);
|
|
389
|
+
const localOps = [];
|
|
390
|
+
let i = 0;
|
|
391
|
+
let j = 0;
|
|
392
|
+
let index = 0;
|
|
393
|
+
while (i < n || j < m) {
|
|
394
|
+
if (i < n && j < m && jsonEquals(base[i], next[j])) {
|
|
395
|
+
i += 1;
|
|
396
|
+
j += 1;
|
|
397
|
+
index += 1;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
const lcsDown = i < n ? lcs[i + 1][j] : -1;
|
|
401
|
+
const lcsRight = j < m ? lcs[i][j + 1] : -1;
|
|
402
|
+
if (j < m && (i === n || lcsRight > lcsDown)) {
|
|
403
|
+
localOps.push({
|
|
404
|
+
op: "add",
|
|
405
|
+
path: stringifyJsonPointer([...path, String(index)]),
|
|
406
|
+
value: next[j]
|
|
407
|
+
});
|
|
408
|
+
j += 1;
|
|
409
|
+
index += 1;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (i < n) {
|
|
413
|
+
localOps.push({
|
|
414
|
+
op: "remove",
|
|
415
|
+
path: stringifyJsonPointer([...path, String(index)])
|
|
416
|
+
});
|
|
417
|
+
i += 1;
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
ops.push(...compactArrayOps(localOps));
|
|
422
|
+
}
|
|
423
|
+
function compactArrayOps(ops) {
|
|
424
|
+
const out = [];
|
|
425
|
+
for (let i = 0; i < ops.length; i++) {
|
|
426
|
+
const op = ops[i];
|
|
427
|
+
const next = ops[i + 1];
|
|
428
|
+
if (op.op === "remove" && next && next.op === "add" && op.path === next.path) {
|
|
429
|
+
out.push({
|
|
430
|
+
op: "replace",
|
|
431
|
+
path: op.path,
|
|
432
|
+
value: next.value
|
|
433
|
+
});
|
|
434
|
+
i += 1;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
out.push(op);
|
|
438
|
+
}
|
|
439
|
+
return out;
|
|
440
|
+
}
|
|
441
|
+
function escapeJsonPointer(token) {
|
|
442
|
+
return token.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
443
|
+
}
|
|
444
|
+
/** Deep equality check for JSON values (null-safe, handles arrays and objects). */
|
|
445
|
+
function jsonEquals(a, b) {
|
|
446
|
+
if (a === b) return true;
|
|
447
|
+
if (a === null || b === null) return false;
|
|
448
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
449
|
+
if (!Array.isArray(a) || !Array.isArray(b)) return false;
|
|
450
|
+
if (a.length !== b.length) return false;
|
|
451
|
+
for (let i = 0; i < a.length; i++) if (!jsonEquals(a[i], b[i])) return false;
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
if (!isPlainObject(a) || !isPlainObject(b)) return false;
|
|
455
|
+
const aKeys = Object.keys(a);
|
|
456
|
+
const bKeys = Object.keys(b);
|
|
457
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
458
|
+
for (const key of aKeys) {
|
|
459
|
+
if (!(key in b)) return false;
|
|
460
|
+
if (!jsonEquals(a[key], b[key])) return false;
|
|
461
|
+
}
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
function isPlainObject(value) {
|
|
465
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
466
|
+
}
|
|
467
|
+
function hasOwn(value, key) {
|
|
468
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
469
|
+
}
|
|
470
|
+
function pathValueAt(base, path) {
|
|
471
|
+
if (path.length === 0) return base;
|
|
472
|
+
return getAtJson(base, path);
|
|
473
|
+
}
|
|
474
|
+
function assertNever$1(_value, message) {
|
|
475
|
+
throw new Error(message);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
//#endregion
|
|
479
|
+
//#region src/doc.ts
|
|
480
|
+
/**
|
|
481
|
+
* Create a CRDT document from a JSON value, using fresh dots for each node.
|
|
482
|
+
* @param value - The JSON value to convert.
|
|
483
|
+
* @param nextDot - A function that generates a unique `Dot` on each call.
|
|
484
|
+
* @returns A new CRDT `Doc`.
|
|
485
|
+
*/
|
|
486
|
+
function docFromJson(value, nextDot) {
|
|
487
|
+
return { root: nodeFromJson(value, nextDot) };
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Legacy: create a doc using a single dot with counter offsets for array children.
|
|
491
|
+
* Prefer `docFromJson(value, nextDot)` to ensure unique dots per node.
|
|
492
|
+
*/
|
|
493
|
+
function docFromJsonWithDot(value, dot) {
|
|
494
|
+
return { root: deepNodeFromJson(value, dot) };
|
|
495
|
+
}
|
|
496
|
+
function getSeqAtPath(doc, path) {
|
|
497
|
+
let cur = doc.root;
|
|
498
|
+
for (const seg of path) {
|
|
499
|
+
if (cur.kind !== "obj") return;
|
|
500
|
+
const ent = cur.entries.get(seg);
|
|
501
|
+
if (!ent) return;
|
|
502
|
+
cur = ent.node;
|
|
503
|
+
}
|
|
504
|
+
return cur.kind === "seq" ? cur : void 0;
|
|
505
|
+
}
|
|
506
|
+
function getObjAtPathStrict(doc, path) {
|
|
507
|
+
let cur = doc.root;
|
|
508
|
+
const seen = [];
|
|
509
|
+
if (path.length === 0) {
|
|
510
|
+
if (cur.kind !== "obj") return {
|
|
511
|
+
ok: false,
|
|
512
|
+
message: "expected object at /"
|
|
513
|
+
};
|
|
514
|
+
return {
|
|
515
|
+
ok: true,
|
|
516
|
+
obj: cur
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
for (const seg of path) {
|
|
520
|
+
if (cur.kind !== "obj") return {
|
|
521
|
+
ok: false,
|
|
522
|
+
message: `expected object at /${seen.join("/")}`
|
|
523
|
+
};
|
|
524
|
+
const entry = cur.entries.get(seg);
|
|
525
|
+
seen.push(seg);
|
|
526
|
+
if (!entry || entry.node.kind !== "obj") return {
|
|
527
|
+
ok: false,
|
|
528
|
+
message: `expected object at /${seen.join("/")}`
|
|
529
|
+
};
|
|
530
|
+
cur = entry.node;
|
|
531
|
+
}
|
|
532
|
+
return {
|
|
533
|
+
ok: true,
|
|
534
|
+
obj: cur
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
function ensureSeqAtPath(head, path, dotForCreate) {
|
|
538
|
+
let cur = head.root;
|
|
539
|
+
let parent = null;
|
|
540
|
+
let parentKey = null;
|
|
541
|
+
if (path.length === 0) {
|
|
542
|
+
if (head.root.kind !== "seq") head.root = newSeq();
|
|
543
|
+
return head.root;
|
|
544
|
+
}
|
|
545
|
+
for (let i = 0; i < path.length; i++) {
|
|
546
|
+
const seg = path[i];
|
|
547
|
+
if (cur.kind !== "obj") {
|
|
548
|
+
const replacement = newObj();
|
|
549
|
+
if (parent && parentKey !== null) objSet(parent, parentKey, replacement, dotForCreate);
|
|
550
|
+
else head.root = replacement;
|
|
551
|
+
cur = replacement;
|
|
552
|
+
}
|
|
553
|
+
const obj = cur;
|
|
554
|
+
const ent = obj.entries.get(seg);
|
|
555
|
+
if (i === path.length - 1) {
|
|
556
|
+
if (!ent || ent.node.kind !== "seq") {
|
|
557
|
+
const seq = newSeq();
|
|
558
|
+
objSet(obj, seg, seq, dotForCreate);
|
|
559
|
+
return seq;
|
|
560
|
+
}
|
|
561
|
+
return ent.node;
|
|
562
|
+
}
|
|
563
|
+
if (!ent || ent.node.kind !== "obj") {
|
|
564
|
+
const child = newObj();
|
|
565
|
+
objSet(obj, seg, child, dotForCreate);
|
|
566
|
+
parent = obj;
|
|
567
|
+
parentKey = seg;
|
|
568
|
+
cur = child;
|
|
569
|
+
} else {
|
|
570
|
+
parent = obj;
|
|
571
|
+
parentKey = seg;
|
|
572
|
+
cur = ent.node;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (head.root.kind !== "seq") head.root = newSeq();
|
|
576
|
+
return head.root;
|
|
577
|
+
}
|
|
578
|
+
function deepNodeFromJson(value, dot) {
|
|
579
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return newReg(value, dot);
|
|
580
|
+
if (Array.isArray(value)) {
|
|
581
|
+
const seq = newSeq();
|
|
582
|
+
let prev = HEAD;
|
|
583
|
+
let ctr = dot.ctr;
|
|
584
|
+
for (const v of value) {
|
|
585
|
+
const childDot = {
|
|
586
|
+
actor: dot.actor,
|
|
587
|
+
ctr: ++ctr
|
|
588
|
+
};
|
|
589
|
+
const id = dotToElemId(childDot);
|
|
590
|
+
rgaInsertAfter(seq, prev, id, childDot, deepNodeFromJson(v, childDot));
|
|
591
|
+
prev = id;
|
|
592
|
+
}
|
|
593
|
+
return seq;
|
|
594
|
+
}
|
|
595
|
+
const obj = newObj();
|
|
596
|
+
for (const [k, v] of Object.entries(value)) objSet(obj, k, deepNodeFromJson(v, dot), dot);
|
|
597
|
+
return obj;
|
|
598
|
+
}
|
|
599
|
+
function nodeFromJson(value, nextDot) {
|
|
600
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return newReg(value, nextDot());
|
|
601
|
+
if (Array.isArray(value)) {
|
|
602
|
+
const seq = newSeq();
|
|
603
|
+
let prev = HEAD;
|
|
604
|
+
for (const v of value) {
|
|
605
|
+
const insDot = nextDot();
|
|
606
|
+
const id = dotToElemId(insDot);
|
|
607
|
+
rgaInsertAfter(seq, prev, id, insDot, nodeFromJson(v, nextDot));
|
|
608
|
+
prev = id;
|
|
609
|
+
}
|
|
610
|
+
return seq;
|
|
611
|
+
}
|
|
612
|
+
const obj = newObj();
|
|
613
|
+
for (const [k, v] of Object.entries(value)) {
|
|
614
|
+
const entryDot = nextDot();
|
|
615
|
+
objSet(obj, k, nodeFromJson(v, nextDot), entryDot);
|
|
616
|
+
}
|
|
617
|
+
return obj;
|
|
618
|
+
}
|
|
619
|
+
/** Deep-clone a CRDT document. The clone is fully independent of the original. */
|
|
620
|
+
function cloneDoc(doc) {
|
|
621
|
+
return { root: cloneNode(doc.root) };
|
|
622
|
+
}
|
|
623
|
+
function cloneNode(node) {
|
|
624
|
+
if (node.kind === "lww") return {
|
|
625
|
+
kind: "lww",
|
|
626
|
+
value: structuredClone(node.value),
|
|
627
|
+
dot: {
|
|
628
|
+
actor: node.dot.actor,
|
|
629
|
+
ctr: node.dot.ctr
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
if (node.kind === "obj") {
|
|
633
|
+
const entries = /* @__PURE__ */ new Map();
|
|
634
|
+
for (const [k, v] of node.entries.entries()) entries.set(k, {
|
|
635
|
+
node: cloneNode(v.node),
|
|
636
|
+
dot: {
|
|
637
|
+
actor: v.dot.actor,
|
|
638
|
+
ctr: v.dot.ctr
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
const tombstone = /* @__PURE__ */ new Map();
|
|
642
|
+
for (const [k, d] of node.tombstone.entries()) tombstone.set(k, {
|
|
643
|
+
actor: d.actor,
|
|
644
|
+
ctr: d.ctr
|
|
645
|
+
});
|
|
646
|
+
return {
|
|
647
|
+
kind: "obj",
|
|
648
|
+
entries,
|
|
649
|
+
tombstone
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
const elems = /* @__PURE__ */ new Map();
|
|
653
|
+
for (const [id, e] of node.elems.entries()) elems.set(id, {
|
|
654
|
+
id: e.id,
|
|
655
|
+
prev: e.prev,
|
|
656
|
+
tombstone: e.tombstone,
|
|
657
|
+
value: cloneNode(e.value),
|
|
658
|
+
insDot: {
|
|
659
|
+
actor: e.insDot.actor,
|
|
660
|
+
ctr: e.insDot.ctr
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
return {
|
|
664
|
+
kind: "seq",
|
|
665
|
+
elems
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
function applyTest(base, head, it, evalTestAgainst) {
|
|
669
|
+
const snapshot = evalTestAgainst === "head" ? materialize(head.root) : materialize(base.root);
|
|
670
|
+
let got;
|
|
671
|
+
try {
|
|
672
|
+
got = getAtJson(snapshot, it.path);
|
|
673
|
+
} catch {
|
|
674
|
+
return {
|
|
675
|
+
ok: false,
|
|
676
|
+
code: 409,
|
|
677
|
+
message: `test path missing at /${it.path.join("/")}`
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
if (!jsonEquals(got, it.value)) return {
|
|
681
|
+
ok: false,
|
|
682
|
+
code: 409,
|
|
683
|
+
message: `test failed at /${it.path.join("/")}`
|
|
684
|
+
};
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
function applyObjSet(head, it, newDot) {
|
|
688
|
+
if (it.path.length === 0 && it.key === ROOT_KEY) {
|
|
689
|
+
head.root = nodeFromJson(it.value, newDot);
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
const parentRes = getObjAtPathStrict(head, it.path);
|
|
693
|
+
if (!parentRes.ok) return {
|
|
694
|
+
ok: false,
|
|
695
|
+
code: 409,
|
|
696
|
+
message: parentRes.message
|
|
697
|
+
};
|
|
698
|
+
if (it.mode === "replace" && !parentRes.obj.entries.has(it.key)) return {
|
|
699
|
+
ok: false,
|
|
700
|
+
code: 409,
|
|
701
|
+
message: `no value at /${[...it.path, it.key].join("/")}`
|
|
702
|
+
};
|
|
703
|
+
const d = newDot();
|
|
704
|
+
const parentObj = parentRes.obj;
|
|
705
|
+
objSet(parentObj, it.key, nodeFromJson(it.value, newDot), d);
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
function applyObjRemove(head, it, newDot) {
|
|
709
|
+
const parentRes = getObjAtPathStrict(head, it.path);
|
|
710
|
+
if (!parentRes.ok) return {
|
|
711
|
+
ok: false,
|
|
712
|
+
code: 409,
|
|
713
|
+
message: parentRes.message
|
|
714
|
+
};
|
|
715
|
+
if (!parentRes.obj.entries.has(it.key)) return {
|
|
716
|
+
ok: false,
|
|
717
|
+
code: 409,
|
|
718
|
+
message: `no value at /${[...it.path, it.key].join("/")}`
|
|
719
|
+
};
|
|
720
|
+
const d = newDot();
|
|
721
|
+
const parentObj = parentRes.obj;
|
|
722
|
+
objRemove(parentObj, it.key, d);
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
function applyArrInsert(base, head, it, newDot, bumpCounterAbove) {
|
|
726
|
+
const baseSeq = getSeqAtPath(base, it.path);
|
|
727
|
+
if (!baseSeq) {
|
|
728
|
+
if (it.index === 0 || it.index === Number.POSITIVE_INFINITY) {
|
|
729
|
+
const headSeq = ensureSeqAtPath(head, it.path, newDot());
|
|
730
|
+
const prev = it.index === 0 ? HEAD : rgaPrevForInsertAtIndex(headSeq, Number.MAX_SAFE_INTEGER);
|
|
731
|
+
const d = nextInsertDotForPrev(headSeq, prev, newDot, bumpCounterAbove);
|
|
732
|
+
rgaInsertAfter(headSeq, prev, dotToElemId(d), d, nodeFromJson(it.value, newDot));
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
return {
|
|
736
|
+
ok: false,
|
|
737
|
+
code: 409,
|
|
738
|
+
message: `base array missing at /${it.path.join("/")}`
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
const headSeq = ensureSeqAtPath(head, it.path, newDot());
|
|
742
|
+
const idx = it.index === Number.POSITIVE_INFINITY ? rgaLinearizeIds(baseSeq).length : it.index;
|
|
743
|
+
const baseLen = rgaLinearizeIds(baseSeq).length;
|
|
744
|
+
if (idx < 0 || idx > baseLen) return {
|
|
745
|
+
ok: false,
|
|
746
|
+
code: 409,
|
|
747
|
+
message: `index out of bounds at /${it.path.join("/")}/${it.index}`
|
|
748
|
+
};
|
|
749
|
+
const prev = idx === 0 ? HEAD : rgaIdAtIndex(baseSeq, idx - 1) ?? HEAD;
|
|
750
|
+
const d = nextInsertDotForPrev(headSeq, prev, newDot, bumpCounterAbove);
|
|
751
|
+
rgaInsertAfter(headSeq, prev, dotToElemId(d), d, nodeFromJson(it.value, newDot));
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
function nextInsertDotForPrev(seq, prev, newDot, bumpCounterAbove) {
|
|
755
|
+
let maxSiblingDot = null;
|
|
756
|
+
for (const elem of seq.elems.values()) {
|
|
757
|
+
if (elem.prev !== prev) continue;
|
|
758
|
+
if (!maxSiblingDot || compareDot(elem.insDot, maxSiblingDot) > 0) maxSiblingDot = elem.insDot;
|
|
759
|
+
}
|
|
760
|
+
if (maxSiblingDot) bumpCounterAbove?.(maxSiblingDot.ctr);
|
|
761
|
+
let candidate = newDot();
|
|
762
|
+
while (maxSiblingDot && compareDot(candidate, maxSiblingDot) <= 0) candidate = newDot();
|
|
763
|
+
return candidate;
|
|
764
|
+
}
|
|
765
|
+
function applyArrDelete(base, head, it, newDot) {
|
|
766
|
+
const d = newDot();
|
|
767
|
+
const baseSeq = getSeqAtPath(base, it.path);
|
|
768
|
+
if (!baseSeq) return {
|
|
769
|
+
ok: false,
|
|
770
|
+
code: 409,
|
|
771
|
+
message: `base array missing at /${it.path.join("/")}`
|
|
772
|
+
};
|
|
773
|
+
const headSeq = ensureSeqAtPath(head, it.path, d);
|
|
774
|
+
const baseId = rgaIdAtIndex(baseSeq, it.index);
|
|
775
|
+
if (!baseId) return {
|
|
776
|
+
ok: false,
|
|
777
|
+
code: 409,
|
|
778
|
+
message: `no base element at index ${it.index}`
|
|
779
|
+
};
|
|
780
|
+
rgaDelete(headSeq, baseId);
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
function applyArrReplace(base, head, it, newDot) {
|
|
784
|
+
const d = newDot();
|
|
785
|
+
const baseSeq = getSeqAtPath(base, it.path);
|
|
786
|
+
if (!baseSeq) return {
|
|
787
|
+
ok: false,
|
|
788
|
+
code: 409,
|
|
789
|
+
message: `base array missing at /${it.path.join("/")}`
|
|
790
|
+
};
|
|
791
|
+
const headSeq = ensureSeqAtPath(head, it.path, d);
|
|
792
|
+
const baseId = rgaIdAtIndex(baseSeq, it.index);
|
|
793
|
+
if (!baseId) return {
|
|
794
|
+
ok: false,
|
|
795
|
+
code: 409,
|
|
796
|
+
message: `no base element at index ${it.index}`
|
|
797
|
+
};
|
|
798
|
+
const e = headSeq.elems.get(baseId);
|
|
799
|
+
if (!e || e.tombstone) return {
|
|
800
|
+
ok: false,
|
|
801
|
+
code: 409,
|
|
802
|
+
message: `element already deleted at index ${it.index}`
|
|
803
|
+
};
|
|
804
|
+
e.value = nodeFromJson(it.value, newDot);
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Apply compiled intent operations to a CRDT document.
|
|
809
|
+
* Array indices are resolved against the base document.
|
|
810
|
+
* @param base - The base document snapshot used for index mapping and test evaluation.
|
|
811
|
+
* @param head - The target document to mutate.
|
|
812
|
+
* @param intents - Compiled intent operations from `compileJsonPatchToIntent`.
|
|
813
|
+
* @param newDot - A function that generates a unique `Dot` per mutation.
|
|
814
|
+
* @param evalTestAgainst - Whether `test` ops are evaluated against `"head"` or `"base"`.
|
|
815
|
+
* @param bumpCounterAbove - Optional hook that can fast-forward the underlying counter before inserts.
|
|
816
|
+
* @returns `{ ok: true }` on success, or `{ ok: false, code: 409, message }` on conflict.
|
|
817
|
+
*/
|
|
818
|
+
function applyIntentsToCrdt(base, head, intents, newDot, evalTestAgainst = "head", bumpCounterAbove) {
|
|
819
|
+
for (const it of intents) {
|
|
820
|
+
let fail = null;
|
|
821
|
+
switch (it.t) {
|
|
822
|
+
case "Test":
|
|
823
|
+
fail = applyTest(base, head, it, evalTestAgainst);
|
|
824
|
+
break;
|
|
825
|
+
case "ObjSet":
|
|
826
|
+
fail = applyObjSet(head, it, newDot);
|
|
827
|
+
break;
|
|
828
|
+
case "ObjRemove":
|
|
829
|
+
fail = applyObjRemove(head, it, newDot);
|
|
830
|
+
break;
|
|
831
|
+
case "ArrInsert":
|
|
832
|
+
fail = applyArrInsert(base, head, it, newDot, bumpCounterAbove);
|
|
833
|
+
break;
|
|
834
|
+
case "ArrDelete":
|
|
835
|
+
fail = applyArrDelete(base, head, it, newDot);
|
|
836
|
+
break;
|
|
837
|
+
case "ArrReplace":
|
|
838
|
+
fail = applyArrReplace(base, head, it, newDot);
|
|
839
|
+
break;
|
|
840
|
+
default: assertNever(it, "Unhandled intent type");
|
|
841
|
+
}
|
|
842
|
+
if (fail) return fail;
|
|
843
|
+
}
|
|
844
|
+
return { ok: true };
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Convenience wrapper: compile a JSON Patch and apply it to a CRDT document.
|
|
848
|
+
* @param base - The base document for index resolution.
|
|
849
|
+
* @param head - The target document to mutate.
|
|
850
|
+
* @param patch - Array of RFC 6902 JSON Patch operations.
|
|
851
|
+
* @param newDot - A function that generates a unique `Dot` per mutation.
|
|
852
|
+
* @param evalTestAgainst - Whether `test` ops evaluate against `"head"` or `"base"`.
|
|
853
|
+
* @param bumpCounterAbove - Optional hook that can fast-forward the underlying counter before inserts.
|
|
854
|
+
* @returns `{ ok: true }` on success, or `{ ok: false, code: 409, message }` on conflict.
|
|
855
|
+
*/
|
|
856
|
+
function jsonPatchToCrdt(base, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove) {
|
|
857
|
+
return applyIntentsToCrdt(base, head, compileJsonPatchToIntent(materialize(base.root), patch), newDot, evalTestAgainst, bumpCounterAbove);
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Safe wrapper around `jsonPatchToCrdt` that converts compile-time errors into `409` results.
|
|
861
|
+
* This function never throws for malformed/invalid patch paths.
|
|
862
|
+
*/
|
|
863
|
+
function jsonPatchToCrdtSafe(base, head, patch, newDot, evalTestAgainst = "head", bumpCounterAbove) {
|
|
864
|
+
try {
|
|
865
|
+
return jsonPatchToCrdt(base, head, patch, newDot, evalTestAgainst, bumpCounterAbove);
|
|
866
|
+
} catch (error) {
|
|
867
|
+
return {
|
|
868
|
+
ok: false,
|
|
869
|
+
code: 409,
|
|
870
|
+
message: error instanceof Error ? error.message : "failed to compile patch"
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Generate a JSON Patch delta between two CRDT documents.
|
|
876
|
+
* @param base - The base document snapshot.
|
|
877
|
+
* @param head - The current document state.
|
|
878
|
+
* @param options - Diff options (e.g. `{ arrayStrategy: "lcs" }`).
|
|
879
|
+
* @returns An array of JSON Patch operations that transform base into head.
|
|
880
|
+
*/
|
|
881
|
+
function crdtToJsonPatch(base, head, options) {
|
|
882
|
+
return diffJsonPatch(materialize(base.root), materialize(head.root), options);
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Emit a single root `replace` patch representing the full document state.
|
|
886
|
+
* Use `crdtToJsonPatch(base, head)` for delta patches instead.
|
|
887
|
+
*/
|
|
888
|
+
function crdtToFullReplace(doc) {
|
|
889
|
+
return [{
|
|
890
|
+
op: "replace",
|
|
891
|
+
path: "",
|
|
892
|
+
value: materialize(doc.root)
|
|
893
|
+
}];
|
|
894
|
+
}
|
|
895
|
+
function assertNever(_value, message) {
|
|
896
|
+
throw new Error(message);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
//#endregion
|
|
900
|
+
//#region src/state.ts
|
|
901
|
+
/** Error thrown when a JSON Patch cannot be applied. Includes a numeric `.code` (409 for conflicts). */
|
|
902
|
+
var PatchError = class extends Error {
|
|
903
|
+
code;
|
|
904
|
+
constructor(message, code = 409) {
|
|
905
|
+
super(message);
|
|
906
|
+
this.name = "PatchError";
|
|
907
|
+
this.code = code;
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
/**
|
|
911
|
+
* Create a new CRDT state from an initial JSON value.
|
|
912
|
+
* @param initial - The initial JSON document.
|
|
913
|
+
* @param options - Actor ID and optional starting counter.
|
|
914
|
+
* @returns A new `CrdtState` containing the document and clock.
|
|
915
|
+
*/
|
|
916
|
+
function createState(initial, options) {
|
|
917
|
+
const clock = createClock(options.actor, options.start ?? 0);
|
|
918
|
+
return {
|
|
919
|
+
doc: docFromJson(initial, clock.next),
|
|
920
|
+
clock
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Materialize a CRDT document or state back to a plain JSON value.
|
|
925
|
+
* @param target - A `Doc` or `CrdtState` to materialize.
|
|
926
|
+
* @returns The JSON representation of the current document.
|
|
927
|
+
*/
|
|
928
|
+
function toJson(target) {
|
|
929
|
+
if ("doc" in target) return materialize(target.doc.root);
|
|
930
|
+
return materialize(target.root);
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Apply a JSON Patch to the state, returning a new immutable state.
|
|
934
|
+
* Throws `PatchError` on conflict (e.g. out-of-bounds index, failed test op).
|
|
935
|
+
* @param state - The current CRDT state.
|
|
936
|
+
* @param patch - Array of RFC 6902 JSON Patch operations.
|
|
937
|
+
* @param options - Optional base document and test evaluation mode.
|
|
938
|
+
* @returns A new `CrdtState` with the patch applied.
|
|
939
|
+
*/
|
|
940
|
+
function applyPatch(state, patch, options = {}) {
|
|
941
|
+
const nextState = {
|
|
942
|
+
doc: cloneDoc(state.doc),
|
|
943
|
+
clock: cloneClock(state.clock)
|
|
944
|
+
};
|
|
945
|
+
const result = applyPatchInternal(nextState, patch, options);
|
|
946
|
+
if (!result.ok) throw new PatchError(result.message, result.code);
|
|
947
|
+
return nextState;
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Apply a JSON Patch to the state in place, mutating the existing state.
|
|
951
|
+
* Throws `PatchError` on conflict.
|
|
952
|
+
* @param state - The CRDT state to mutate.
|
|
953
|
+
* @param patch - Array of RFC 6902 JSON Patch operations.
|
|
954
|
+
* @param options - Optional base document and test evaluation mode.
|
|
955
|
+
*/
|
|
956
|
+
function applyPatchInPlace(state, patch, options = {}) {
|
|
957
|
+
if (options.atomic ?? true) {
|
|
958
|
+
const next = applyPatch(state, patch, options);
|
|
959
|
+
state.doc = next.doc;
|
|
960
|
+
state.clock = next.clock;
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
const result = applyPatchInternal(state, patch, options);
|
|
964
|
+
if (!result.ok) throw new PatchError(result.message, result.code);
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Apply a JSON Patch as a specific actor while maintaining an external version vector.
|
|
968
|
+
* Returns the updated state and a new version vector snapshot.
|
|
969
|
+
*/
|
|
970
|
+
function applyPatchAsActor(doc, vv, actor, patch, options = {}) {
|
|
971
|
+
const observedCtr = maxCtrInNodeForActor$1(doc.root, actor);
|
|
972
|
+
const state = applyPatch({
|
|
973
|
+
doc,
|
|
974
|
+
clock: createClock(actor, Math.max(vv[actor] ?? 0, observedCtr))
|
|
975
|
+
}, patch, options);
|
|
976
|
+
return {
|
|
977
|
+
state,
|
|
978
|
+
vv: {
|
|
979
|
+
...vv,
|
|
980
|
+
[actor]: Math.max(vv[actor] ?? 0, state.clock.ctr)
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
function applyPatchInternal(state, patch, options) {
|
|
985
|
+
if ((options.semantics ?? "base") === "sequential") {
|
|
986
|
+
const explicitBaseState = options.base ? {
|
|
987
|
+
doc: cloneDoc(options.base),
|
|
988
|
+
clock: createClock("__base__", 0)
|
|
989
|
+
} : null;
|
|
990
|
+
for (const op of patch) {
|
|
991
|
+
const step = applyPatchOpSequential(state, op, options, explicitBaseState ? explicitBaseState.doc : cloneDoc(state.doc));
|
|
992
|
+
if (!step.ok) return step;
|
|
993
|
+
if (explicitBaseState) {
|
|
994
|
+
const baseStep = applyPatchInternal(explicitBaseState, [op], {
|
|
995
|
+
semantics: "sequential",
|
|
996
|
+
testAgainst: options.testAgainst
|
|
997
|
+
});
|
|
998
|
+
if (!baseStep.ok) return baseStep;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return { ok: true };
|
|
1002
|
+
}
|
|
1003
|
+
const baseDoc = options.base ? options.base : cloneDoc(state.doc);
|
|
1004
|
+
const compiled = compileIntents(materialize(baseDoc.root), patch);
|
|
1005
|
+
if (!compiled.ok) return compiled;
|
|
1006
|
+
return applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr));
|
|
1007
|
+
}
|
|
1008
|
+
function applyPatchOpSequential(state, op, options, baseDoc) {
|
|
1009
|
+
const baseJson = materialize(baseDoc.root);
|
|
1010
|
+
if (op.op === "move") {
|
|
1011
|
+
const fromValue = getAtJson(baseJson, parseJsonPointer(op.from));
|
|
1012
|
+
const removeRes = applySinglePatchOp(state, baseDoc, {
|
|
1013
|
+
op: "remove",
|
|
1014
|
+
path: op.from
|
|
1015
|
+
}, options);
|
|
1016
|
+
if (!removeRes.ok) return removeRes;
|
|
1017
|
+
return applySinglePatchOp(state, cloneDoc(state.doc), {
|
|
1018
|
+
op: "add",
|
|
1019
|
+
path: op.path,
|
|
1020
|
+
value: fromValue
|
|
1021
|
+
}, options);
|
|
1022
|
+
}
|
|
1023
|
+
if (op.op === "copy") {
|
|
1024
|
+
const fromValue = getAtJson(baseJson, parseJsonPointer(op.from));
|
|
1025
|
+
return applySinglePatchOp(state, baseDoc, {
|
|
1026
|
+
op: "add",
|
|
1027
|
+
path: op.path,
|
|
1028
|
+
value: fromValue
|
|
1029
|
+
}, options);
|
|
1030
|
+
}
|
|
1031
|
+
return applySinglePatchOp(state, baseDoc, op, options);
|
|
1032
|
+
}
|
|
1033
|
+
function applySinglePatchOp(state, baseDoc, op, options) {
|
|
1034
|
+
const compiled = compileIntents(materialize(baseDoc.root), [op]);
|
|
1035
|
+
if (!compiled.ok) return compiled;
|
|
1036
|
+
return applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr));
|
|
1037
|
+
}
|
|
1038
|
+
function bumpClockCounter(state, ctr) {
|
|
1039
|
+
if (state.clock.ctr < ctr) state.clock.ctr = ctr;
|
|
1040
|
+
}
|
|
1041
|
+
function compileIntents(baseJson, patch) {
|
|
1042
|
+
try {
|
|
1043
|
+
return {
|
|
1044
|
+
ok: true,
|
|
1045
|
+
intents: compileJsonPatchToIntent(baseJson, patch)
|
|
1046
|
+
};
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
return {
|
|
1049
|
+
ok: false,
|
|
1050
|
+
code: 409,
|
|
1051
|
+
message: error instanceof Error ? error.message : "failed to compile patch"
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
function maxCtrInNodeForActor$1(node, actor) {
|
|
1056
|
+
switch (node.kind) {
|
|
1057
|
+
case "lww": return node.dot.actor === actor ? node.dot.ctr : 0;
|
|
1058
|
+
case "obj": {
|
|
1059
|
+
let best = 0;
|
|
1060
|
+
for (const entry of node.entries.values()) {
|
|
1061
|
+
if (entry.dot.actor === actor && entry.dot.ctr > best) best = entry.dot.ctr;
|
|
1062
|
+
const childBest = maxCtrInNodeForActor$1(entry.node, actor);
|
|
1063
|
+
if (childBest > best) best = childBest;
|
|
1064
|
+
}
|
|
1065
|
+
for (const tomb of node.tombstone.values()) if (tomb.actor === actor && tomb.ctr > best) best = tomb.ctr;
|
|
1066
|
+
return best;
|
|
1067
|
+
}
|
|
1068
|
+
case "seq": {
|
|
1069
|
+
let best = 0;
|
|
1070
|
+
for (const elem of node.elems.values()) {
|
|
1071
|
+
if (elem.insDot.actor === actor && elem.insDot.ctr > best) best = elem.insDot.ctr;
|
|
1072
|
+
const childBest = maxCtrInNodeForActor$1(elem.value, actor);
|
|
1073
|
+
if (childBest > best) best = childBest;
|
|
1074
|
+
}
|
|
1075
|
+
return best;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
//#endregion
|
|
1081
|
+
//#region src/serialize.ts
|
|
1082
|
+
/** Serialize a CRDT document to a JSON-safe representation (Maps become plain objects). */
|
|
1083
|
+
function serializeDoc(doc) {
|
|
1084
|
+
return { root: serializeNode(doc.root) };
|
|
1085
|
+
}
|
|
1086
|
+
/** Reconstruct a CRDT document from its serialized form. */
|
|
1087
|
+
function deserializeDoc(data) {
|
|
1088
|
+
return { root: deserializeNode(data.root) };
|
|
1089
|
+
}
|
|
1090
|
+
/** Serialize a full CRDT state (document + clock) to a JSON-safe representation. */
|
|
1091
|
+
function serializeState(state) {
|
|
1092
|
+
return {
|
|
1093
|
+
doc: serializeDoc(state.doc),
|
|
1094
|
+
clock: {
|
|
1095
|
+
actor: state.clock.actor,
|
|
1096
|
+
ctr: state.clock.ctr
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
/** Reconstruct a full CRDT state from its serialized form, restoring the clock. */
|
|
1101
|
+
function deserializeState(data) {
|
|
1102
|
+
const clock = createClock(data.clock.actor, data.clock.ctr);
|
|
1103
|
+
return {
|
|
1104
|
+
doc: deserializeDoc(data.doc),
|
|
1105
|
+
clock
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
function serializeNode(node) {
|
|
1109
|
+
if (node.kind === "lww") return {
|
|
1110
|
+
kind: "lww",
|
|
1111
|
+
value: structuredClone(node.value),
|
|
1112
|
+
dot: {
|
|
1113
|
+
actor: node.dot.actor,
|
|
1114
|
+
ctr: node.dot.ctr
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
if (node.kind === "obj") {
|
|
1118
|
+
const entries = {};
|
|
1119
|
+
for (const [k, v] of node.entries.entries()) entries[k] = {
|
|
1120
|
+
node: serializeNode(v.node),
|
|
1121
|
+
dot: {
|
|
1122
|
+
actor: v.dot.actor,
|
|
1123
|
+
ctr: v.dot.ctr
|
|
1124
|
+
}
|
|
1125
|
+
};
|
|
1126
|
+
const tombstone = {};
|
|
1127
|
+
for (const [k, d] of node.tombstone.entries()) tombstone[k] = {
|
|
1128
|
+
actor: d.actor,
|
|
1129
|
+
ctr: d.ctr
|
|
1130
|
+
};
|
|
1131
|
+
return {
|
|
1132
|
+
kind: "obj",
|
|
1133
|
+
entries,
|
|
1134
|
+
tombstone
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
const elems = {};
|
|
1138
|
+
for (const [id, e] of node.elems.entries()) elems[id] = {
|
|
1139
|
+
id: e.id,
|
|
1140
|
+
prev: e.prev,
|
|
1141
|
+
tombstone: e.tombstone,
|
|
1142
|
+
value: serializeNode(e.value),
|
|
1143
|
+
insDot: {
|
|
1144
|
+
actor: e.insDot.actor,
|
|
1145
|
+
ctr: e.insDot.ctr
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1148
|
+
return {
|
|
1149
|
+
kind: "seq",
|
|
1150
|
+
elems
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
function deserializeNode(node) {
|
|
1154
|
+
if (node.kind === "lww") return {
|
|
1155
|
+
kind: "lww",
|
|
1156
|
+
value: structuredClone(node.value),
|
|
1157
|
+
dot: {
|
|
1158
|
+
actor: node.dot.actor,
|
|
1159
|
+
ctr: node.dot.ctr
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
if (node.kind === "obj") {
|
|
1163
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1164
|
+
for (const [k, v] of Object.entries(node.entries)) entries.set(k, {
|
|
1165
|
+
node: deserializeNode(v.node),
|
|
1166
|
+
dot: {
|
|
1167
|
+
actor: v.dot.actor,
|
|
1168
|
+
ctr: v.dot.ctr
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
const tombstone = /* @__PURE__ */ new Map();
|
|
1172
|
+
for (const [k, d] of Object.entries(node.tombstone)) tombstone.set(k, {
|
|
1173
|
+
actor: d.actor,
|
|
1174
|
+
ctr: d.ctr
|
|
1175
|
+
});
|
|
1176
|
+
return {
|
|
1177
|
+
kind: "obj",
|
|
1178
|
+
entries,
|
|
1179
|
+
tombstone
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
const elems = /* @__PURE__ */ new Map();
|
|
1183
|
+
for (const [id, e] of Object.entries(node.elems)) elems.set(id, {
|
|
1184
|
+
id: e.id,
|
|
1185
|
+
prev: e.prev,
|
|
1186
|
+
tombstone: e.tombstone,
|
|
1187
|
+
value: deserializeNode(e.value),
|
|
1188
|
+
insDot: {
|
|
1189
|
+
actor: e.insDot.actor,
|
|
1190
|
+
ctr: e.insDot.ctr
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1193
|
+
return {
|
|
1194
|
+
kind: "seq",
|
|
1195
|
+
elems
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
//#endregion
|
|
1200
|
+
//#region src/merge.ts
|
|
1201
|
+
/**
|
|
1202
|
+
* Merge two CRDT documents from different peers into one.
|
|
1203
|
+
* By default this requires shared array lineage for non-empty sequences.
|
|
1204
|
+
*
|
|
1205
|
+
* Resolution rules:
|
|
1206
|
+
* - **LwwReg**: the register with the higher dot wins (total order by counter then actor).
|
|
1207
|
+
* - **ObjNode**: entries are merged key-by-key; tombstones use max-dot-per-key.
|
|
1208
|
+
* If both sides have a live entry for the same key, the entry nodes are merged recursively.
|
|
1209
|
+
* Delete-wins: if a tombstone dot >= an entry dot, the entry is removed.
|
|
1210
|
+
* - **RgaSeq**: elements from both sides are unioned by element ID.
|
|
1211
|
+
* If both sides have the same element, tombstone wins (delete bias) and values are merged recursively.
|
|
1212
|
+
* - **Kind mismatch**: the node with the higher "representative dot" wins and replaces the other entirely.
|
|
1213
|
+
*/
|
|
1214
|
+
function mergeDoc(a, b, options = {}) {
|
|
1215
|
+
const mismatchPath = options.requireSharedOrigin ?? true ? findSeqLineageMismatch(a.root, b.root, []) : null;
|
|
1216
|
+
if (mismatchPath) throw new Error(`merge requires shared array origin at ${mismatchPath}`);
|
|
1217
|
+
return { root: mergeNode(a.root, b.root) };
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Merge two CRDT states.
|
|
1221
|
+
*
|
|
1222
|
+
* The merged clock keeps a stable actor identity:
|
|
1223
|
+
* - defaults to the actor from the first argument (`a`)
|
|
1224
|
+
* - can be overridden via `options.actor`
|
|
1225
|
+
* - optional `options.requireSharedOrigin` controls merge lineage checks
|
|
1226
|
+
*
|
|
1227
|
+
* The merged counter is lifted to the highest counter already observed for
|
|
1228
|
+
* that actor across both input clocks and the merged document dots.
|
|
1229
|
+
*/
|
|
1230
|
+
function mergeState(a, b, options = {}) {
|
|
1231
|
+
const doc = mergeDoc(a.doc, b.doc, { requireSharedOrigin: options.requireSharedOrigin });
|
|
1232
|
+
const actor = options.actor ?? a.clock.actor;
|
|
1233
|
+
return {
|
|
1234
|
+
doc,
|
|
1235
|
+
clock: createClock(actor, maxObservedCtrForActor(doc, actor, a, b))
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
function findSeqLineageMismatch(a, b, path) {
|
|
1239
|
+
if (a.kind === "seq" && b.kind === "seq") {
|
|
1240
|
+
const hasElemsA = a.elems.size > 0;
|
|
1241
|
+
const hasElemsB = b.elems.size > 0;
|
|
1242
|
+
if (hasElemsA && hasElemsB) {
|
|
1243
|
+
let shared = false;
|
|
1244
|
+
for (const id of a.elems.keys()) if (b.elems.has(id)) {
|
|
1245
|
+
shared = true;
|
|
1246
|
+
break;
|
|
1247
|
+
}
|
|
1248
|
+
if (!shared) return `/${path.join("/")}`;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
if (a.kind === "obj" && b.kind === "obj") {
|
|
1252
|
+
const sharedKeys = new Set([...a.entries.keys()].filter((key) => b.entries.has(key)));
|
|
1253
|
+
for (const key of sharedKeys) {
|
|
1254
|
+
const nextA = a.entries.get(key).node;
|
|
1255
|
+
const nextB = b.entries.get(key).node;
|
|
1256
|
+
const mismatch = findSeqLineageMismatch(nextA, nextB, [...path, key]);
|
|
1257
|
+
if (mismatch) return mismatch;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return null;
|
|
1261
|
+
}
|
|
1262
|
+
function maxObservedCtrForActor(doc, actor, a, b) {
|
|
1263
|
+
let best = maxCtrInNodeForActor(doc.root, actor);
|
|
1264
|
+
if (a.clock.actor === actor && a.clock.ctr > best) best = a.clock.ctr;
|
|
1265
|
+
if (b.clock.actor === actor && b.clock.ctr > best) best = b.clock.ctr;
|
|
1266
|
+
return best;
|
|
1267
|
+
}
|
|
1268
|
+
function maxCtrInNodeForActor(node, actor) {
|
|
1269
|
+
switch (node.kind) {
|
|
1270
|
+
case "lww": return node.dot.actor === actor ? node.dot.ctr : 0;
|
|
1271
|
+
case "obj": {
|
|
1272
|
+
let best = 0;
|
|
1273
|
+
for (const entry of node.entries.values()) {
|
|
1274
|
+
if (entry.dot.actor === actor && entry.dot.ctr > best) best = entry.dot.ctr;
|
|
1275
|
+
const childBest = maxCtrInNodeForActor(entry.node, actor);
|
|
1276
|
+
if (childBest > best) best = childBest;
|
|
1277
|
+
}
|
|
1278
|
+
for (const tomb of node.tombstone.values()) if (tomb.actor === actor && tomb.ctr > best) best = tomb.ctr;
|
|
1279
|
+
return best;
|
|
1280
|
+
}
|
|
1281
|
+
case "seq": {
|
|
1282
|
+
let best = 0;
|
|
1283
|
+
for (const elem of node.elems.values()) {
|
|
1284
|
+
if (elem.insDot.actor === actor && elem.insDot.ctr > best) best = elem.insDot.ctr;
|
|
1285
|
+
const childBest = maxCtrInNodeForActor(elem.value, actor);
|
|
1286
|
+
if (childBest > best) best = childBest;
|
|
1287
|
+
}
|
|
1288
|
+
return best;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
function repDot(node) {
|
|
1293
|
+
switch (node.kind) {
|
|
1294
|
+
case "lww": return node.dot;
|
|
1295
|
+
case "obj": {
|
|
1296
|
+
let best = {
|
|
1297
|
+
actor: "",
|
|
1298
|
+
ctr: 0
|
|
1299
|
+
};
|
|
1300
|
+
for (const entry of node.entries.values()) if (compareDot(entry.dot, best) > 0) best = entry.dot;
|
|
1301
|
+
for (const d of node.tombstone.values()) if (compareDot(d, best) > 0) best = d;
|
|
1302
|
+
return best;
|
|
1303
|
+
}
|
|
1304
|
+
case "seq": {
|
|
1305
|
+
let best = {
|
|
1306
|
+
actor: "",
|
|
1307
|
+
ctr: 0
|
|
1308
|
+
};
|
|
1309
|
+
for (const e of node.elems.values()) if (compareDot(e.insDot, best) > 0) best = e.insDot;
|
|
1310
|
+
return best;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
function mergeNode(a, b) {
|
|
1315
|
+
if (a.kind === "lww" && b.kind === "lww") return mergeLww(a, b);
|
|
1316
|
+
if (a.kind === "obj" && b.kind === "obj") return mergeObj(a, b);
|
|
1317
|
+
if (a.kind === "seq" && b.kind === "seq") return mergeSeq(a, b);
|
|
1318
|
+
if (compareDot(repDot(a), repDot(b)) >= 0) return cloneNodeShallow(a);
|
|
1319
|
+
return cloneNodeShallow(b);
|
|
1320
|
+
}
|
|
1321
|
+
function mergeLww(a, b) {
|
|
1322
|
+
if (compareDot(a.dot, b.dot) >= 0) return {
|
|
1323
|
+
kind: "lww",
|
|
1324
|
+
value: structuredClone(a.value),
|
|
1325
|
+
dot: { ...a.dot }
|
|
1326
|
+
};
|
|
1327
|
+
return {
|
|
1328
|
+
kind: "lww",
|
|
1329
|
+
value: structuredClone(b.value),
|
|
1330
|
+
dot: { ...b.dot }
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
function mergeObj(a, b) {
|
|
1334
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1335
|
+
const tombstone = /* @__PURE__ */ new Map();
|
|
1336
|
+
const allTombKeys = new Set([...a.tombstone.keys(), ...b.tombstone.keys()]);
|
|
1337
|
+
for (const key of allTombKeys) {
|
|
1338
|
+
const da = a.tombstone.get(key);
|
|
1339
|
+
const db = b.tombstone.get(key);
|
|
1340
|
+
if (da && db) tombstone.set(key, compareDot(da, db) >= 0 ? { ...da } : { ...db });
|
|
1341
|
+
else if (da) tombstone.set(key, { ...da });
|
|
1342
|
+
else tombstone.set(key, { ...db });
|
|
1343
|
+
}
|
|
1344
|
+
const allKeys = new Set([...a.entries.keys(), ...b.entries.keys()]);
|
|
1345
|
+
for (const key of allKeys) {
|
|
1346
|
+
const ea = a.entries.get(key);
|
|
1347
|
+
const eb = b.entries.get(key);
|
|
1348
|
+
let merged;
|
|
1349
|
+
if (ea && eb) merged = {
|
|
1350
|
+
node: mergeNode(ea.node, eb.node),
|
|
1351
|
+
dot: compareDot(ea.dot, eb.dot) >= 0 ? { ...ea.dot } : { ...eb.dot }
|
|
1352
|
+
};
|
|
1353
|
+
else if (ea) merged = {
|
|
1354
|
+
node: cloneNodeShallow(ea.node),
|
|
1355
|
+
dot: { ...ea.dot }
|
|
1356
|
+
};
|
|
1357
|
+
else merged = {
|
|
1358
|
+
node: cloneNodeShallow(eb.node),
|
|
1359
|
+
dot: { ...eb.dot }
|
|
1360
|
+
};
|
|
1361
|
+
const td = tombstone.get(key);
|
|
1362
|
+
if (td && compareDot(td, merged.dot) >= 0) continue;
|
|
1363
|
+
entries.set(key, merged);
|
|
1364
|
+
}
|
|
1365
|
+
return {
|
|
1366
|
+
kind: "obj",
|
|
1367
|
+
entries,
|
|
1368
|
+
tombstone
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
function mergeSeq(a, b) {
|
|
1372
|
+
const elems = /* @__PURE__ */ new Map();
|
|
1373
|
+
const allIds = new Set([...a.elems.keys(), ...b.elems.keys()]);
|
|
1374
|
+
for (const id of allIds) {
|
|
1375
|
+
const ea = a.elems.get(id);
|
|
1376
|
+
const eb = b.elems.get(id);
|
|
1377
|
+
if (ea && eb) {
|
|
1378
|
+
const mergedValue = mergeNode(ea.value, eb.value);
|
|
1379
|
+
elems.set(id, {
|
|
1380
|
+
id,
|
|
1381
|
+
prev: ea.prev,
|
|
1382
|
+
tombstone: ea.tombstone || eb.tombstone,
|
|
1383
|
+
value: mergedValue,
|
|
1384
|
+
insDot: { ...ea.insDot }
|
|
1385
|
+
});
|
|
1386
|
+
} else if (ea) elems.set(id, cloneElem(ea));
|
|
1387
|
+
else elems.set(id, cloneElem(eb));
|
|
1388
|
+
}
|
|
1389
|
+
return {
|
|
1390
|
+
kind: "seq",
|
|
1391
|
+
elems
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
function cloneElem(e) {
|
|
1395
|
+
return {
|
|
1396
|
+
id: e.id,
|
|
1397
|
+
prev: e.prev,
|
|
1398
|
+
tombstone: e.tombstone,
|
|
1399
|
+
value: cloneNodeShallow(e.value),
|
|
1400
|
+
insDot: { ...e.insDot }
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
function cloneNodeShallow(node) {
|
|
1404
|
+
switch (node.kind) {
|
|
1405
|
+
case "lww": return {
|
|
1406
|
+
kind: "lww",
|
|
1407
|
+
value: structuredClone(node.value),
|
|
1408
|
+
dot: { ...node.dot }
|
|
1409
|
+
};
|
|
1410
|
+
case "obj": {
|
|
1411
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1412
|
+
for (const [k, v] of node.entries) entries.set(k, {
|
|
1413
|
+
node: cloneNodeShallow(v.node),
|
|
1414
|
+
dot: { ...v.dot }
|
|
1415
|
+
});
|
|
1416
|
+
const tombstone = /* @__PURE__ */ new Map();
|
|
1417
|
+
for (const [k, d] of node.tombstone) tombstone.set(k, { ...d });
|
|
1418
|
+
return {
|
|
1419
|
+
kind: "obj",
|
|
1420
|
+
entries,
|
|
1421
|
+
tombstone
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
case "seq": {
|
|
1425
|
+
const elems = /* @__PURE__ */ new Map();
|
|
1426
|
+
for (const [id, e] of node.elems) elems.set(id, cloneElem(e));
|
|
1427
|
+
return {
|
|
1428
|
+
kind: "seq",
|
|
1429
|
+
elems
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
//#endregion
|
|
1436
|
+
Object.defineProperty(exports, 'HEAD', {
|
|
1437
|
+
enumerable: true,
|
|
1438
|
+
get: function () {
|
|
1439
|
+
return HEAD;
|
|
1440
|
+
}
|
|
1441
|
+
});
|
|
1442
|
+
Object.defineProperty(exports, 'PatchError', {
|
|
1443
|
+
enumerable: true,
|
|
1444
|
+
get: function () {
|
|
1445
|
+
return PatchError;
|
|
1446
|
+
}
|
|
1447
|
+
});
|
|
1448
|
+
Object.defineProperty(exports, 'ROOT_KEY', {
|
|
1449
|
+
enumerable: true,
|
|
1450
|
+
get: function () {
|
|
1451
|
+
return ROOT_KEY;
|
|
1452
|
+
}
|
|
1453
|
+
});
|
|
1454
|
+
Object.defineProperty(exports, 'applyIntentsToCrdt', {
|
|
1455
|
+
enumerable: true,
|
|
1456
|
+
get: function () {
|
|
1457
|
+
return applyIntentsToCrdt;
|
|
1458
|
+
}
|
|
1459
|
+
});
|
|
1460
|
+
Object.defineProperty(exports, 'applyPatch', {
|
|
1461
|
+
enumerable: true,
|
|
1462
|
+
get: function () {
|
|
1463
|
+
return applyPatch;
|
|
1464
|
+
}
|
|
1465
|
+
});
|
|
1466
|
+
Object.defineProperty(exports, 'applyPatchAsActor', {
|
|
1467
|
+
enumerable: true,
|
|
1468
|
+
get: function () {
|
|
1469
|
+
return applyPatchAsActor;
|
|
1470
|
+
}
|
|
1471
|
+
});
|
|
1472
|
+
Object.defineProperty(exports, 'applyPatchInPlace', {
|
|
1473
|
+
enumerable: true,
|
|
1474
|
+
get: function () {
|
|
1475
|
+
return applyPatchInPlace;
|
|
1476
|
+
}
|
|
1477
|
+
});
|
|
1478
|
+
Object.defineProperty(exports, 'cloneClock', {
|
|
1479
|
+
enumerable: true,
|
|
1480
|
+
get: function () {
|
|
1481
|
+
return cloneClock;
|
|
1482
|
+
}
|
|
1483
|
+
});
|
|
1484
|
+
Object.defineProperty(exports, 'cloneDoc', {
|
|
1485
|
+
enumerable: true,
|
|
1486
|
+
get: function () {
|
|
1487
|
+
return cloneDoc;
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1490
|
+
Object.defineProperty(exports, 'compareDot', {
|
|
1491
|
+
enumerable: true,
|
|
1492
|
+
get: function () {
|
|
1493
|
+
return compareDot;
|
|
1494
|
+
}
|
|
1495
|
+
});
|
|
1496
|
+
Object.defineProperty(exports, 'compileJsonPatchToIntent', {
|
|
1497
|
+
enumerable: true,
|
|
1498
|
+
get: function () {
|
|
1499
|
+
return compileJsonPatchToIntent;
|
|
1500
|
+
}
|
|
1501
|
+
});
|
|
1502
|
+
Object.defineProperty(exports, 'crdtToFullReplace', {
|
|
1503
|
+
enumerable: true,
|
|
1504
|
+
get: function () {
|
|
1505
|
+
return crdtToFullReplace;
|
|
1506
|
+
}
|
|
1507
|
+
});
|
|
1508
|
+
Object.defineProperty(exports, 'crdtToJsonPatch', {
|
|
1509
|
+
enumerable: true,
|
|
1510
|
+
get: function () {
|
|
1511
|
+
return crdtToJsonPatch;
|
|
1512
|
+
}
|
|
1513
|
+
});
|
|
1514
|
+
Object.defineProperty(exports, 'createClock', {
|
|
1515
|
+
enumerable: true,
|
|
1516
|
+
get: function () {
|
|
1517
|
+
return createClock;
|
|
1518
|
+
}
|
|
1519
|
+
});
|
|
1520
|
+
Object.defineProperty(exports, 'createState', {
|
|
1521
|
+
enumerable: true,
|
|
1522
|
+
get: function () {
|
|
1523
|
+
return createState;
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
Object.defineProperty(exports, 'deserializeDoc', {
|
|
1527
|
+
enumerable: true,
|
|
1528
|
+
get: function () {
|
|
1529
|
+
return deserializeDoc;
|
|
1530
|
+
}
|
|
1531
|
+
});
|
|
1532
|
+
Object.defineProperty(exports, 'deserializeState', {
|
|
1533
|
+
enumerable: true,
|
|
1534
|
+
get: function () {
|
|
1535
|
+
return deserializeState;
|
|
1536
|
+
}
|
|
1537
|
+
});
|
|
1538
|
+
Object.defineProperty(exports, 'diffJsonPatch', {
|
|
1539
|
+
enumerable: true,
|
|
1540
|
+
get: function () {
|
|
1541
|
+
return diffJsonPatch;
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
Object.defineProperty(exports, 'docFromJson', {
|
|
1545
|
+
enumerable: true,
|
|
1546
|
+
get: function () {
|
|
1547
|
+
return docFromJson;
|
|
1548
|
+
}
|
|
1549
|
+
});
|
|
1550
|
+
Object.defineProperty(exports, 'docFromJsonWithDot', {
|
|
1551
|
+
enumerable: true,
|
|
1552
|
+
get: function () {
|
|
1553
|
+
return docFromJsonWithDot;
|
|
1554
|
+
}
|
|
1555
|
+
});
|
|
1556
|
+
Object.defineProperty(exports, 'dotToElemId', {
|
|
1557
|
+
enumerable: true,
|
|
1558
|
+
get: function () {
|
|
1559
|
+
return dotToElemId;
|
|
1560
|
+
}
|
|
1561
|
+
});
|
|
1562
|
+
Object.defineProperty(exports, 'getAtJson', {
|
|
1563
|
+
enumerable: true,
|
|
1564
|
+
get: function () {
|
|
1565
|
+
return getAtJson;
|
|
1566
|
+
}
|
|
1567
|
+
});
|
|
1568
|
+
Object.defineProperty(exports, 'jsonEquals', {
|
|
1569
|
+
enumerable: true,
|
|
1570
|
+
get: function () {
|
|
1571
|
+
return jsonEquals;
|
|
1572
|
+
}
|
|
1573
|
+
});
|
|
1574
|
+
Object.defineProperty(exports, 'jsonPatchToCrdt', {
|
|
1575
|
+
enumerable: true,
|
|
1576
|
+
get: function () {
|
|
1577
|
+
return jsonPatchToCrdt;
|
|
1578
|
+
}
|
|
1579
|
+
});
|
|
1580
|
+
Object.defineProperty(exports, 'jsonPatchToCrdtSafe', {
|
|
1581
|
+
enumerable: true,
|
|
1582
|
+
get: function () {
|
|
1583
|
+
return jsonPatchToCrdtSafe;
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
1586
|
+
Object.defineProperty(exports, 'lwwSet', {
|
|
1587
|
+
enumerable: true,
|
|
1588
|
+
get: function () {
|
|
1589
|
+
return lwwSet;
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1592
|
+
Object.defineProperty(exports, 'materialize', {
|
|
1593
|
+
enumerable: true,
|
|
1594
|
+
get: function () {
|
|
1595
|
+
return materialize;
|
|
1596
|
+
}
|
|
1597
|
+
});
|
|
1598
|
+
Object.defineProperty(exports, 'mergeDoc', {
|
|
1599
|
+
enumerable: true,
|
|
1600
|
+
get: function () {
|
|
1601
|
+
return mergeDoc;
|
|
1602
|
+
}
|
|
1603
|
+
});
|
|
1604
|
+
Object.defineProperty(exports, 'mergeState', {
|
|
1605
|
+
enumerable: true,
|
|
1606
|
+
get: function () {
|
|
1607
|
+
return mergeState;
|
|
1608
|
+
}
|
|
1609
|
+
});
|
|
1610
|
+
Object.defineProperty(exports, 'newObj', {
|
|
1611
|
+
enumerable: true,
|
|
1612
|
+
get: function () {
|
|
1613
|
+
return newObj;
|
|
1614
|
+
}
|
|
1615
|
+
});
|
|
1616
|
+
Object.defineProperty(exports, 'newReg', {
|
|
1617
|
+
enumerable: true,
|
|
1618
|
+
get: function () {
|
|
1619
|
+
return newReg;
|
|
1620
|
+
}
|
|
1621
|
+
});
|
|
1622
|
+
Object.defineProperty(exports, 'newSeq', {
|
|
1623
|
+
enumerable: true,
|
|
1624
|
+
get: function () {
|
|
1625
|
+
return newSeq;
|
|
1626
|
+
}
|
|
1627
|
+
});
|
|
1628
|
+
Object.defineProperty(exports, 'nextDotForActor', {
|
|
1629
|
+
enumerable: true,
|
|
1630
|
+
get: function () {
|
|
1631
|
+
return nextDotForActor;
|
|
1632
|
+
}
|
|
1633
|
+
});
|
|
1634
|
+
Object.defineProperty(exports, 'objRemove', {
|
|
1635
|
+
enumerable: true,
|
|
1636
|
+
get: function () {
|
|
1637
|
+
return objRemove;
|
|
1638
|
+
}
|
|
1639
|
+
});
|
|
1640
|
+
Object.defineProperty(exports, 'objSet', {
|
|
1641
|
+
enumerable: true,
|
|
1642
|
+
get: function () {
|
|
1643
|
+
return objSet;
|
|
1644
|
+
}
|
|
1645
|
+
});
|
|
1646
|
+
Object.defineProperty(exports, 'observeDot', {
|
|
1647
|
+
enumerable: true,
|
|
1648
|
+
get: function () {
|
|
1649
|
+
return observeDot;
|
|
1650
|
+
}
|
|
1651
|
+
});
|
|
1652
|
+
Object.defineProperty(exports, 'parseJsonPointer', {
|
|
1653
|
+
enumerable: true,
|
|
1654
|
+
get: function () {
|
|
1655
|
+
return parseJsonPointer;
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
Object.defineProperty(exports, 'rgaDelete', {
|
|
1659
|
+
enumerable: true,
|
|
1660
|
+
get: function () {
|
|
1661
|
+
return rgaDelete;
|
|
1662
|
+
}
|
|
1663
|
+
});
|
|
1664
|
+
Object.defineProperty(exports, 'rgaIdAtIndex', {
|
|
1665
|
+
enumerable: true,
|
|
1666
|
+
get: function () {
|
|
1667
|
+
return rgaIdAtIndex;
|
|
1668
|
+
}
|
|
1669
|
+
});
|
|
1670
|
+
Object.defineProperty(exports, 'rgaInsertAfter', {
|
|
1671
|
+
enumerable: true,
|
|
1672
|
+
get: function () {
|
|
1673
|
+
return rgaInsertAfter;
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1676
|
+
Object.defineProperty(exports, 'rgaLinearizeIds', {
|
|
1677
|
+
enumerable: true,
|
|
1678
|
+
get: function () {
|
|
1679
|
+
return rgaLinearizeIds;
|
|
1680
|
+
}
|
|
1681
|
+
});
|
|
1682
|
+
Object.defineProperty(exports, 'rgaPrevForInsertAtIndex', {
|
|
1683
|
+
enumerable: true,
|
|
1684
|
+
get: function () {
|
|
1685
|
+
return rgaPrevForInsertAtIndex;
|
|
1686
|
+
}
|
|
1687
|
+
});
|
|
1688
|
+
Object.defineProperty(exports, 'serializeDoc', {
|
|
1689
|
+
enumerable: true,
|
|
1690
|
+
get: function () {
|
|
1691
|
+
return serializeDoc;
|
|
1692
|
+
}
|
|
1693
|
+
});
|
|
1694
|
+
Object.defineProperty(exports, 'serializeState', {
|
|
1695
|
+
enumerable: true,
|
|
1696
|
+
get: function () {
|
|
1697
|
+
return serializeState;
|
|
1698
|
+
}
|
|
1699
|
+
});
|
|
1700
|
+
Object.defineProperty(exports, 'stringifyJsonPointer', {
|
|
1701
|
+
enumerable: true,
|
|
1702
|
+
get: function () {
|
|
1703
|
+
return stringifyJsonPointer;
|
|
1704
|
+
}
|
|
1705
|
+
});
|
|
1706
|
+
Object.defineProperty(exports, 'toJson', {
|
|
1707
|
+
enumerable: true,
|
|
1708
|
+
get: function () {
|
|
1709
|
+
return toJson;
|
|
1710
|
+
}
|
|
1711
|
+
});
|
|
1712
|
+
Object.defineProperty(exports, 'vvHasDot', {
|
|
1713
|
+
enumerable: true,
|
|
1714
|
+
get: function () {
|
|
1715
|
+
return vvHasDot;
|
|
1716
|
+
}
|
|
1717
|
+
});
|
|
1718
|
+
Object.defineProperty(exports, 'vvMerge', {
|
|
1719
|
+
enumerable: true,
|
|
1720
|
+
get: function () {
|
|
1721
|
+
return vvMerge;
|
|
1722
|
+
}
|
|
1723
|
+
});
|