json-patch-to-crdt 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -4
- package/dist/{compact-BE9UsxEo.mjs → compact-BcwxBNx_.mjs} +1664 -418
- package/dist/{compact-DrmgKiVW.js → compact-CXfvMNCT.js} +1753 -459
- package/dist/{depth-Cd3nyHWy.d.mts → depth-CpJSyZE5.d.mts} +94 -15
- package/dist/{depth-tcJ8L1dj.d.ts → depth-D88VeWb-.d.ts} +94 -15
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +6 -2
- package/dist/index.mjs +2 -2
- package/dist/internals.d.mts +31 -4
- package/dist/internals.d.ts +31 -4
- package/dist/internals.js +9 -1
- package/dist/internals.mjs +2 -2
- package/package.json +4 -1
|
@@ -1,19 +1,38 @@
|
|
|
1
1
|
|
|
2
|
-
//#region src/
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
//#region src/depth.ts
|
|
3
|
+
const MAX_TRAVERSAL_DEPTH = 16384;
|
|
4
|
+
var TraversalDepthError = class extends Error {
|
|
5
|
+
code = 409;
|
|
6
|
+
reason = "MAX_DEPTH_EXCEEDED";
|
|
7
|
+
depth;
|
|
8
|
+
maxDepth;
|
|
9
|
+
constructor(depth, maxDepth = MAX_TRAVERSAL_DEPTH) {
|
|
10
|
+
super(`maximum nesting depth ${maxDepth} exceeded at depth ${depth}`);
|
|
11
|
+
this.name = "TraversalDepthError";
|
|
12
|
+
this.depth = depth;
|
|
13
|
+
this.maxDepth = maxDepth;
|
|
9
14
|
}
|
|
10
15
|
};
|
|
11
|
-
function
|
|
12
|
-
if (
|
|
16
|
+
function assertTraversalDepth(depth, maxDepth = MAX_TRAVERSAL_DEPTH) {
|
|
17
|
+
if (depth > maxDepth) throw new TraversalDepthError(depth, maxDepth);
|
|
18
|
+
}
|
|
19
|
+
function toDepthApplyError(error) {
|
|
20
|
+
return {
|
|
21
|
+
ok: false,
|
|
22
|
+
code: error.code,
|
|
23
|
+
reason: error.reason,
|
|
24
|
+
message: error.message
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/version-vector.ts
|
|
30
|
+
function readVersionVectorCounter(vv, actor) {
|
|
31
|
+
if (!Object.prototype.hasOwnProperty.call(vv, actor)) return;
|
|
13
32
|
const counter = vv[actor];
|
|
14
|
-
return typeof counter === "number" ? counter : 0;
|
|
33
|
+
return typeof counter === "number" ? counter : void 0;
|
|
15
34
|
}
|
|
16
|
-
function
|
|
35
|
+
function writeVersionVectorCounter(vv, actor, counter) {
|
|
17
36
|
Object.defineProperty(vv, actor, {
|
|
18
37
|
configurable: true,
|
|
19
38
|
enumerable: true,
|
|
@@ -21,6 +40,104 @@ function writeVvCounter$1(vv, actor, counter) {
|
|
|
21
40
|
writable: true
|
|
22
41
|
});
|
|
23
42
|
}
|
|
43
|
+
function observeVersionVectorDot(vv, dot) {
|
|
44
|
+
if ((readVersionVectorCounter(vv, dot.actor) ?? 0) < dot.ctr) writeVersionVectorCounter(vv, dot.actor, dot.ctr);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Inspect a document or state and return the highest observed counter per actor.
|
|
48
|
+
*
|
|
49
|
+
* When a `CrdtState` is provided, the returned vector is also seeded from the
|
|
50
|
+
* state's local clock so callers do not lose counters that have advanced ahead
|
|
51
|
+
* of the currently materialized document tree.
|
|
52
|
+
*/
|
|
53
|
+
function observedVersionVector(target) {
|
|
54
|
+
const doc = "doc" in target ? target.doc : target;
|
|
55
|
+
const vv = Object.create(null);
|
|
56
|
+
if ("clock" in target) observeVersionVectorDot(vv, {
|
|
57
|
+
actor: target.clock.actor,
|
|
58
|
+
ctr: target.clock.ctr
|
|
59
|
+
});
|
|
60
|
+
const stack = [{
|
|
61
|
+
node: doc.root,
|
|
62
|
+
depth: 0
|
|
63
|
+
}];
|
|
64
|
+
while (stack.length > 0) {
|
|
65
|
+
const frame = stack.pop();
|
|
66
|
+
assertTraversalDepth(frame.depth);
|
|
67
|
+
if (frame.node.kind === "lww") {
|
|
68
|
+
observeVersionVectorDot(vv, frame.node.dot);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (frame.node.kind === "obj") {
|
|
72
|
+
for (const entry of frame.node.entries.values()) {
|
|
73
|
+
observeVersionVectorDot(vv, entry.dot);
|
|
74
|
+
stack.push({
|
|
75
|
+
node: entry.node,
|
|
76
|
+
depth: frame.depth + 1
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
for (const tombstone of frame.node.tombstone.values()) observeVersionVectorDot(vv, tombstone);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
for (const elem of frame.node.elems.values()) {
|
|
83
|
+
observeVersionVectorDot(vv, elem.insDot);
|
|
84
|
+
if (elem.delDot) observeVersionVectorDot(vv, elem.delDot);
|
|
85
|
+
stack.push({
|
|
86
|
+
node: elem.value,
|
|
87
|
+
depth: frame.depth + 1
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return vv;
|
|
92
|
+
}
|
|
93
|
+
/** Combine version vectors using per-actor maxima. */
|
|
94
|
+
function mergeVersionVectors(...vectors) {
|
|
95
|
+
const merged = Object.create(null);
|
|
96
|
+
for (const vv of vectors) for (const actor of Object.keys(vv)) {
|
|
97
|
+
const counter = readVersionVectorCounter(vv, actor);
|
|
98
|
+
if (counter === void 0) continue;
|
|
99
|
+
writeVersionVectorCounter(merged, actor, Math.max(readVersionVectorCounter(merged, actor) ?? 0, counter));
|
|
100
|
+
}
|
|
101
|
+
return merged;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Derive a causally-stable checkpoint by taking the per-actor minimum.
|
|
105
|
+
*
|
|
106
|
+
* When called with a single vector the result equals that vector. In practice,
|
|
107
|
+
* a meaningful shared-stability checkpoint usually needs acknowledgements from
|
|
108
|
+
* at least two peers or from an explicit quorum.
|
|
109
|
+
*/
|
|
110
|
+
function intersectVersionVectors(...vectors) {
|
|
111
|
+
if (vectors.length === 0) return Object.create(null);
|
|
112
|
+
const actors = /* @__PURE__ */ new Set();
|
|
113
|
+
for (const vv of vectors) for (const actor of Object.keys(vv)) actors.add(actor);
|
|
114
|
+
const intersection = Object.create(null);
|
|
115
|
+
for (const actor of actors) {
|
|
116
|
+
const counters = vectors.map((vv) => readVersionVectorCounter(vv, actor) ?? 0);
|
|
117
|
+
const counter = Math.min(...counters);
|
|
118
|
+
if (counter > 0) writeVersionVectorCounter(intersection, actor, counter);
|
|
119
|
+
}
|
|
120
|
+
return intersection;
|
|
121
|
+
}
|
|
122
|
+
/** Check whether one version vector has observed every counter in another. */
|
|
123
|
+
function versionVectorCovers(observed, required) {
|
|
124
|
+
for (const actor of Object.keys(required)) {
|
|
125
|
+
const requiredCounter = readVersionVectorCounter(required, actor) ?? 0;
|
|
126
|
+
if ((readVersionVectorCounter(observed, actor) ?? 0) < requiredCounter) return false;
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/clock.ts
|
|
133
|
+
var ClockValidationError = class extends TypeError {
|
|
134
|
+
reason;
|
|
135
|
+
constructor(reason, message) {
|
|
136
|
+
super(message);
|
|
137
|
+
this.name = "ClockValidationError";
|
|
138
|
+
this.reason = reason;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
24
141
|
/**
|
|
25
142
|
* Create a new clock for the given actor. Each call to `clock.next()` yields a fresh `Dot`.
|
|
26
143
|
* @param actor - Unique identifier for this peer.
|
|
@@ -57,8 +174,8 @@ function cloneClock(clock) {
|
|
|
57
174
|
* Useful when a server needs to mint dots for many actors.
|
|
58
175
|
*/
|
|
59
176
|
function nextDotForActor(vv, actor) {
|
|
60
|
-
const ctr =
|
|
61
|
-
|
|
177
|
+
const ctr = (readVersionVectorCounter(vv, actor) ?? 0) + 1;
|
|
178
|
+
writeVersionVectorCounter(vv, actor, ctr);
|
|
62
179
|
return {
|
|
63
180
|
actor,
|
|
64
181
|
ctr
|
|
@@ -66,63 +183,20 @@ function nextDotForActor(vv, actor) {
|
|
|
66
183
|
}
|
|
67
184
|
/** Record an observed dot in a version vector. */
|
|
68
185
|
function observeDot(vv, dot) {
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
//#endregion
|
|
73
|
-
//#region src/depth.ts
|
|
74
|
-
const MAX_TRAVERSAL_DEPTH = 16384;
|
|
75
|
-
var TraversalDepthError = class extends Error {
|
|
76
|
-
code = 409;
|
|
77
|
-
reason = "MAX_DEPTH_EXCEEDED";
|
|
78
|
-
depth;
|
|
79
|
-
maxDepth;
|
|
80
|
-
constructor(depth, maxDepth = MAX_TRAVERSAL_DEPTH) {
|
|
81
|
-
super(`maximum nesting depth ${maxDepth} exceeded at depth ${depth}`);
|
|
82
|
-
this.name = "TraversalDepthError";
|
|
83
|
-
this.depth = depth;
|
|
84
|
-
this.maxDepth = maxDepth;
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
function assertTraversalDepth(depth, maxDepth = MAX_TRAVERSAL_DEPTH) {
|
|
88
|
-
if (depth > maxDepth) throw new TraversalDepthError(depth, maxDepth);
|
|
89
|
-
}
|
|
90
|
-
function toDepthApplyError(error) {
|
|
91
|
-
return {
|
|
92
|
-
ok: false,
|
|
93
|
-
code: error.code,
|
|
94
|
-
reason: error.reason,
|
|
95
|
-
message: error.message
|
|
96
|
-
};
|
|
186
|
+
observeVersionVectorDot(vv, dot);
|
|
97
187
|
}
|
|
98
188
|
|
|
99
189
|
//#endregion
|
|
100
190
|
//#region src/dot.ts
|
|
101
|
-
function readVvCounter(vv, actor) {
|
|
102
|
-
if (!Object.prototype.hasOwnProperty.call(vv, actor)) return;
|
|
103
|
-
const counter = vv[actor];
|
|
104
|
-
return typeof counter === "number" ? counter : void 0;
|
|
105
|
-
}
|
|
106
|
-
function writeVvCounter(vv, actor, counter) {
|
|
107
|
-
Object.defineProperty(vv, actor, {
|
|
108
|
-
configurable: true,
|
|
109
|
-
enumerable: true,
|
|
110
|
-
value: counter,
|
|
111
|
-
writable: true
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
191
|
function compareDot(a, b) {
|
|
115
192
|
if (a.ctr !== b.ctr) return a.ctr - b.ctr;
|
|
116
193
|
return a.actor < b.actor ? -1 : a.actor > b.actor ? 1 : 0;
|
|
117
194
|
}
|
|
118
195
|
function vvHasDot(vv, d) {
|
|
119
|
-
return (
|
|
196
|
+
return (readVersionVectorCounter(vv, d.actor) ?? 0) >= d.ctr;
|
|
120
197
|
}
|
|
121
198
|
function vvMerge(a, b) {
|
|
122
|
-
|
|
123
|
-
for (const [actor, ctr] of Object.entries(a)) writeVvCounter(out, actor, ctr);
|
|
124
|
-
for (const [actor, ctr] of Object.entries(b)) writeVvCounter(out, actor, Math.max(readVvCounter(out, actor) ?? 0, ctr));
|
|
125
|
-
return out;
|
|
199
|
+
return mergeVersionVectors(a, b);
|
|
126
200
|
}
|
|
127
201
|
function dotToElemId(d) {
|
|
128
202
|
return `${d.actor}:${d.ctr}`;
|
|
@@ -133,12 +207,31 @@ function dotToElemId(d) {
|
|
|
133
207
|
const HEAD = "HEAD";
|
|
134
208
|
const linearCache = /* @__PURE__ */ new WeakMap();
|
|
135
209
|
const seqVersion = /* @__PURE__ */ new WeakMap();
|
|
210
|
+
const maxSiblingInsDotByPrevCache = /* @__PURE__ */ new WeakMap();
|
|
136
211
|
function getVersion(seq) {
|
|
137
212
|
return seqVersion.get(seq) ?? 0;
|
|
138
213
|
}
|
|
139
214
|
function bumpVersion(seq) {
|
|
140
215
|
seqVersion.set(seq, getVersion(seq) + 1);
|
|
141
216
|
}
|
|
217
|
+
function buildMaxSiblingInsDotByPrevIndex(seq) {
|
|
218
|
+
const index = /* @__PURE__ */ new Map();
|
|
219
|
+
for (const elem of seq.elems.values()) {
|
|
220
|
+
const current = index.get(elem.prev);
|
|
221
|
+
if (!current || compareDot(elem.insDot, current) > 0) index.set(elem.prev, elem.insDot);
|
|
222
|
+
}
|
|
223
|
+
maxSiblingInsDotByPrevCache.set(seq, index);
|
|
224
|
+
return index;
|
|
225
|
+
}
|
|
226
|
+
function getMaxSiblingInsDotByPrevIndex(seq) {
|
|
227
|
+
return maxSiblingInsDotByPrevCache.get(seq) ?? buildMaxSiblingInsDotByPrevIndex(seq);
|
|
228
|
+
}
|
|
229
|
+
function trackInsertedSiblingDot(seq, prev, insDot) {
|
|
230
|
+
const index = maxSiblingInsDotByPrevCache.get(seq);
|
|
231
|
+
if (!index) return;
|
|
232
|
+
const current = index.get(prev);
|
|
233
|
+
if (!current || compareDot(insDot, current) > 0) index.set(prev, insDot);
|
|
234
|
+
}
|
|
142
235
|
function rgaChildrenIndex(seq) {
|
|
143
236
|
const idx = /* @__PURE__ */ new Map();
|
|
144
237
|
for (const e of seq.elems.values()) {
|
|
@@ -187,6 +280,36 @@ function rgaLinearizeIds(seq) {
|
|
|
187
280
|
});
|
|
188
281
|
return [...out];
|
|
189
282
|
}
|
|
283
|
+
function rgaLength(seq) {
|
|
284
|
+
const ver = getVersion(seq);
|
|
285
|
+
const cached = linearCache.get(seq);
|
|
286
|
+
if (cached && cached.version === ver) return cached.ids.length;
|
|
287
|
+
return rgaLinearizeIds(seq).length;
|
|
288
|
+
}
|
|
289
|
+
function rgaCreateIndexedIdSnapshot(seq) {
|
|
290
|
+
const ids = rgaLinearizeIds(seq);
|
|
291
|
+
return {
|
|
292
|
+
length() {
|
|
293
|
+
return ids.length;
|
|
294
|
+
},
|
|
295
|
+
idAt(index) {
|
|
296
|
+
return ids[index];
|
|
297
|
+
},
|
|
298
|
+
prevForInsertAt(index) {
|
|
299
|
+
if (index <= 0) return HEAD;
|
|
300
|
+
return ids[index - 1] ?? (ids.length > 0 ? ids[ids.length - 1] : HEAD);
|
|
301
|
+
},
|
|
302
|
+
insertAt(index, id) {
|
|
303
|
+
const at = Math.max(0, Math.min(index, ids.length));
|
|
304
|
+
ids.splice(at, 0, id);
|
|
305
|
+
},
|
|
306
|
+
deleteAt(index) {
|
|
307
|
+
if (index < 0 || index >= ids.length) return;
|
|
308
|
+
const [removed] = ids.splice(index, 1);
|
|
309
|
+
return removed;
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
190
313
|
function rgaInsertAfter(seq, prev, id, insDot, value) {
|
|
191
314
|
if (seq.elems.has(id)) return;
|
|
192
315
|
seq.elems.set(id, {
|
|
@@ -196,15 +319,110 @@ function rgaInsertAfter(seq, prev, id, insDot, value) {
|
|
|
196
319
|
value,
|
|
197
320
|
insDot
|
|
198
321
|
});
|
|
322
|
+
trackInsertedSiblingDot(seq, prev, insDot);
|
|
199
323
|
bumpVersion(seq);
|
|
200
324
|
}
|
|
201
|
-
function
|
|
325
|
+
function rgaInsertAfterChecked(seq, prev, id, insDot, value) {
|
|
326
|
+
if (seq.elems.has(id)) return;
|
|
327
|
+
if (prev !== HEAD && !seq.elems.has(prev)) throw new Error(`RGA predecessor '${prev}' does not exist`);
|
|
328
|
+
rgaInsertAfter(seq, prev, id, insDot, value);
|
|
329
|
+
}
|
|
330
|
+
function rgaDelete(seq, id, delDot) {
|
|
202
331
|
const e = seq.elems.get(id);
|
|
203
332
|
if (!e) return;
|
|
204
|
-
if (e.tombstone)
|
|
333
|
+
if (e.tombstone) {
|
|
334
|
+
if (delDot && (!e.delDot || compareDot(delDot, e.delDot) > 0)) {
|
|
335
|
+
e.delDot = {
|
|
336
|
+
actor: delDot.actor,
|
|
337
|
+
ctr: delDot.ctr
|
|
338
|
+
};
|
|
339
|
+
bumpVersion(seq);
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
205
343
|
e.tombstone = true;
|
|
344
|
+
if (delDot) e.delDot = {
|
|
345
|
+
actor: delDot.actor,
|
|
346
|
+
ctr: delDot.ctr
|
|
347
|
+
};
|
|
206
348
|
bumpVersion(seq);
|
|
207
349
|
}
|
|
350
|
+
function validateRgaSeq(seq) {
|
|
351
|
+
const issues = [];
|
|
352
|
+
for (const elem of seq.elems.values()) if (elem.prev !== HEAD && !seq.elems.has(elem.prev)) issues.push({
|
|
353
|
+
code: "MISSING_PREDECESSOR",
|
|
354
|
+
id: elem.id,
|
|
355
|
+
prev: elem.prev,
|
|
356
|
+
message: `RGA element '${elem.id}' references missing predecessor '${elem.prev}'`
|
|
357
|
+
});
|
|
358
|
+
const cycleIds = /* @__PURE__ */ new Set();
|
|
359
|
+
const visitState = /* @__PURE__ */ new Map();
|
|
360
|
+
const sortedIds = [...seq.elems.keys()].sort();
|
|
361
|
+
for (const startId of sortedIds) {
|
|
362
|
+
if (visitState.get(startId) === 2) continue;
|
|
363
|
+
const trail = [];
|
|
364
|
+
const trailIndex = /* @__PURE__ */ new Map();
|
|
365
|
+
let currentId = startId;
|
|
366
|
+
while (currentId !== void 0) {
|
|
367
|
+
const seenAt = trailIndex.get(currentId);
|
|
368
|
+
if (seenAt !== void 0) {
|
|
369
|
+
for (let i = seenAt; i < trail.length; i++) cycleIds.add(trail[i]);
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
if (visitState.get(currentId) === 2) break;
|
|
373
|
+
const elem = seq.elems.get(currentId);
|
|
374
|
+
if (!elem) break;
|
|
375
|
+
trailIndex.set(currentId, trail.length);
|
|
376
|
+
trail.push(currentId);
|
|
377
|
+
if (elem.prev === HEAD) break;
|
|
378
|
+
currentId = elem.prev;
|
|
379
|
+
}
|
|
380
|
+
for (const id of trail) visitState.set(id, 2);
|
|
381
|
+
}
|
|
382
|
+
for (const id of [...cycleIds].sort()) {
|
|
383
|
+
const elem = seq.elems.get(id);
|
|
384
|
+
issues.push({
|
|
385
|
+
code: "PREDECESSOR_CYCLE",
|
|
386
|
+
id,
|
|
387
|
+
prev: elem.prev,
|
|
388
|
+
message: `RGA predecessor cycle detected at '${id}'`
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
const children = rgaChildrenIndex(seq);
|
|
392
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
393
|
+
const stack = [...children.get(HEAD) ?? []];
|
|
394
|
+
while (stack.length > 0) {
|
|
395
|
+
const elem = stack.pop();
|
|
396
|
+
if (reachable.has(elem.id)) continue;
|
|
397
|
+
reachable.add(elem.id);
|
|
398
|
+
const descendants = children.get(elem.id);
|
|
399
|
+
if (descendants) stack.push(...descendants);
|
|
400
|
+
}
|
|
401
|
+
for (const id of sortedIds) {
|
|
402
|
+
if (reachable.has(id)) continue;
|
|
403
|
+
const elem = seq.elems.get(id);
|
|
404
|
+
issues.push({
|
|
405
|
+
code: "ORPHANED_ELEMENT",
|
|
406
|
+
id,
|
|
407
|
+
prev: elem.prev,
|
|
408
|
+
message: `RGA element '${id}' is unreachable from HEAD`
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
if (issues.length === 0) return {
|
|
412
|
+
ok: true,
|
|
413
|
+
issues: []
|
|
414
|
+
};
|
|
415
|
+
const issueOrder = {
|
|
416
|
+
MISSING_PREDECESSOR: 0,
|
|
417
|
+
PREDECESSOR_CYCLE: 1,
|
|
418
|
+
ORPHANED_ELEMENT: 2
|
|
419
|
+
};
|
|
420
|
+
issues.sort((a, b) => a.id.localeCompare(b.id) || issueOrder[a.code] - issueOrder[b.code] || a.prev.localeCompare(b.prev));
|
|
421
|
+
return {
|
|
422
|
+
ok: false,
|
|
423
|
+
issues
|
|
424
|
+
};
|
|
425
|
+
}
|
|
208
426
|
/**
|
|
209
427
|
* Prune tombstoned elements that are causally stable and have no live descendants
|
|
210
428
|
* depending on them for sequence traversal.
|
|
@@ -251,15 +469,19 @@ function rgaCompactTombstones(seq, isStable) {
|
|
|
251
469
|
continue;
|
|
252
470
|
}
|
|
253
471
|
const elem = seq.elems.get(frame.id);
|
|
254
|
-
if (!elem || !elem.tombstone || !isStable(elem.
|
|
472
|
+
if (!elem || !elem.tombstone || !elem.delDot || !isStable(elem.delDot)) continue;
|
|
255
473
|
const childIds = children.get(frame.id);
|
|
256
474
|
if (!childIds || childIds.every((childId) => removable.has(childId))) removable.add(frame.id);
|
|
257
475
|
}
|
|
258
476
|
if (removable.size === 0) return 0;
|
|
259
477
|
for (const id of removable) seq.elems.delete(id);
|
|
478
|
+
maxSiblingInsDotByPrevCache.delete(seq);
|
|
260
479
|
bumpVersion(seq);
|
|
261
480
|
return removable.size;
|
|
262
481
|
}
|
|
482
|
+
function rgaMaxInsertDotForPrev(seq, prev) {
|
|
483
|
+
return getMaxSiblingInsDotByPrevIndex(seq).get(prev) ?? null;
|
|
484
|
+
}
|
|
263
485
|
function rgaIdAtIndex(seq, index) {
|
|
264
486
|
return rgaLinearizeIds(seq)[index];
|
|
265
487
|
}
|
|
@@ -271,6 +493,7 @@ function rgaPrevForInsertAtIndex(seq, index) {
|
|
|
271
493
|
|
|
272
494
|
//#endregion
|
|
273
495
|
//#region src/materialize.ts
|
|
496
|
+
let materializeObserver = null;
|
|
274
497
|
function createMaterializedObject() {
|
|
275
498
|
return Object.create(null);
|
|
276
499
|
}
|
|
@@ -284,6 +507,8 @@ function setMaterializedProperty(out, key, value) {
|
|
|
284
507
|
}
|
|
285
508
|
/** Convert a CRDT node graph into a plain JSON value using an explicit stack. */
|
|
286
509
|
function materialize(node) {
|
|
510
|
+
const observer = materializeObserver;
|
|
511
|
+
observer?.([], node);
|
|
287
512
|
if (node.kind === "lww") return node.value;
|
|
288
513
|
const root = node.kind === "obj" ? createMaterializedObject() : [];
|
|
289
514
|
const stack = [];
|
|
@@ -291,13 +516,16 @@ function materialize(node) {
|
|
|
291
516
|
kind: "obj",
|
|
292
517
|
depth: 0,
|
|
293
518
|
entries: node.entries.entries(),
|
|
294
|
-
out: root
|
|
519
|
+
out: root,
|
|
520
|
+
path: []
|
|
295
521
|
});
|
|
296
522
|
else stack.push({
|
|
297
523
|
kind: "seq",
|
|
298
524
|
depth: 0,
|
|
299
525
|
cursor: rgaCreateLinearCursor(node),
|
|
300
|
-
out: root
|
|
526
|
+
out: root,
|
|
527
|
+
path: [],
|
|
528
|
+
nextIndex: 0
|
|
301
529
|
});
|
|
302
530
|
while (stack.length > 0) {
|
|
303
531
|
const frame = stack[stack.length - 1];
|
|
@@ -311,6 +539,8 @@ function materialize(node) {
|
|
|
311
539
|
const child = entry.node;
|
|
312
540
|
const childDepth = frame.depth + 1;
|
|
313
541
|
assertTraversalDepth(childDepth);
|
|
542
|
+
const childPath = [...frame.path, key];
|
|
543
|
+
observer?.(childPath, child);
|
|
314
544
|
if (child.kind === "lww") {
|
|
315
545
|
setMaterializedProperty(frame.out, key, child.value);
|
|
316
546
|
continue;
|
|
@@ -322,7 +552,8 @@ function materialize(node) {
|
|
|
322
552
|
kind: "obj",
|
|
323
553
|
depth: childDepth,
|
|
324
554
|
entries: child.entries.entries(),
|
|
325
|
-
out: outObj
|
|
555
|
+
out: outObj,
|
|
556
|
+
path: childPath
|
|
326
557
|
});
|
|
327
558
|
continue;
|
|
328
559
|
}
|
|
@@ -332,7 +563,9 @@ function materialize(node) {
|
|
|
332
563
|
kind: "seq",
|
|
333
564
|
depth: childDepth,
|
|
334
565
|
cursor: rgaCreateLinearCursor(child),
|
|
335
|
-
out: outArr
|
|
566
|
+
out: outArr,
|
|
567
|
+
path: childPath,
|
|
568
|
+
nextIndex: 0
|
|
336
569
|
});
|
|
337
570
|
continue;
|
|
338
571
|
}
|
|
@@ -344,6 +577,9 @@ function materialize(node) {
|
|
|
344
577
|
const child = elem.value;
|
|
345
578
|
const childDepth = frame.depth + 1;
|
|
346
579
|
assertTraversalDepth(childDepth);
|
|
580
|
+
const childPath = [...frame.path, String(frame.nextIndex)];
|
|
581
|
+
frame.nextIndex += 1;
|
|
582
|
+
observer?.(childPath, child);
|
|
347
583
|
if (child.kind === "lww") {
|
|
348
584
|
frame.out.push(child.value);
|
|
349
585
|
continue;
|
|
@@ -355,7 +591,8 @@ function materialize(node) {
|
|
|
355
591
|
kind: "obj",
|
|
356
592
|
depth: childDepth,
|
|
357
593
|
entries: child.entries.entries(),
|
|
358
|
-
out: outObj
|
|
594
|
+
out: outObj,
|
|
595
|
+
path: childPath
|
|
359
596
|
});
|
|
360
597
|
continue;
|
|
361
598
|
}
|
|
@@ -365,7 +602,9 @@ function materialize(node) {
|
|
|
365
602
|
kind: "seq",
|
|
366
603
|
depth: childDepth,
|
|
367
604
|
cursor: rgaCreateLinearCursor(child),
|
|
368
|
-
out: outArr
|
|
605
|
+
out: outArr,
|
|
606
|
+
path: childPath,
|
|
607
|
+
nextIndex: 0
|
|
369
608
|
});
|
|
370
609
|
}
|
|
371
610
|
return root;
|
|
@@ -476,6 +715,7 @@ function assertRuntimeJsonValue(value) {
|
|
|
476
715
|
/**
|
|
477
716
|
* Normalize a runtime value to JSON-compatible data.
|
|
478
717
|
* - non-finite numbers -> null
|
|
718
|
+
* - non-plain objects -> null at the root / in arrays, omitted from object properties
|
|
479
719
|
* - invalid object-property values -> key omitted
|
|
480
720
|
* - invalid root / array values -> null
|
|
481
721
|
*/
|
|
@@ -562,7 +802,10 @@ function isJsonPrimitive$1(value) {
|
|
|
562
802
|
return typeof value === "number" && Number.isFinite(value);
|
|
563
803
|
}
|
|
564
804
|
function isJsonObject(value) {
|
|
565
|
-
|
|
805
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
806
|
+
if (Object.prototype.toString.call(value) !== "[object Object]") return false;
|
|
807
|
+
const prototype = Object.getPrototypeOf(value);
|
|
808
|
+
return prototype === null || Object.getPrototypeOf(prototype) === null;
|
|
566
809
|
}
|
|
567
810
|
function isNonFiniteNumber(value) {
|
|
568
811
|
return typeof value === "number" && !Number.isFinite(value);
|
|
@@ -573,8 +816,16 @@ function describeInvalidValue(value) {
|
|
|
573
816
|
if (typeof value === "bigint") return "bigint is not valid JSON";
|
|
574
817
|
if (typeof value === "symbol") return "symbol is not valid JSON";
|
|
575
818
|
if (typeof value === "function") return "function is not valid JSON";
|
|
819
|
+
if (typeof value === "object" && value !== null) return `non-plain object (${describeObjectKind(value)}) is not valid JSON`;
|
|
576
820
|
return `unsupported value type (${typeof value})`;
|
|
577
821
|
}
|
|
822
|
+
function describeObjectKind(value) {
|
|
823
|
+
const tag = Object.prototype.toString.call(value).slice(8, -1);
|
|
824
|
+
if (tag !== "Object") return tag;
|
|
825
|
+
const constructor = value.constructor;
|
|
826
|
+
if (typeof constructor === "function" && constructor.name !== "" && constructor.name !== "Object") return constructor.name;
|
|
827
|
+
return "Object";
|
|
828
|
+
}
|
|
578
829
|
|
|
579
830
|
//#endregion
|
|
580
831
|
//#region src/types.ts
|
|
@@ -587,6 +838,7 @@ const ROOT_KEY = "@@crdt/root";
|
|
|
587
838
|
//#endregion
|
|
588
839
|
//#region src/patch.ts
|
|
589
840
|
const DEFAULT_LCS_MAX_CELLS = 25e4;
|
|
841
|
+
const LINEAR_LCS_MATRIX_BASE_CASE_MAX_CELLS = 4096;
|
|
590
842
|
/** Structured compile error used to map patch validation failures to typed reasons. */
|
|
591
843
|
var PatchCompileError = class extends Error {
|
|
592
844
|
reason;
|
|
@@ -677,22 +929,37 @@ function getAtJson(base, path) {
|
|
|
677
929
|
* @returns An array of `IntentOp` ready for `applyIntentsToCrdt`.
|
|
678
930
|
*/
|
|
679
931
|
function compileJsonPatchToIntent(baseJson, patch, options = {}) {
|
|
932
|
+
const internalOptions = options;
|
|
680
933
|
const semantics = options.semantics ?? "sequential";
|
|
934
|
+
const opIndexOffset = internalOptions.opIndexOffset ?? 0;
|
|
681
935
|
let workingBase = baseJson;
|
|
682
|
-
const pointerCache = /* @__PURE__ */ new Map();
|
|
936
|
+
const pointerCache = internalOptions.pointerCache ?? /* @__PURE__ */ new Map();
|
|
683
937
|
const intents = [];
|
|
684
938
|
for (let opIndex = 0; opIndex < patch.length; opIndex++) {
|
|
685
939
|
const op = patch[opIndex];
|
|
940
|
+
const absoluteOpIndex = opIndex + opIndexOffset;
|
|
686
941
|
const compileBase = semantics === "sequential" ? workingBase : baseJson;
|
|
687
|
-
intents.push(...compileSingleOp(compileBase, op,
|
|
688
|
-
if (semantics === "sequential") workingBase = applyPatchOpToJsonWithStructuralSharing(workingBase, op,
|
|
942
|
+
intents.push(...compileSingleOp(compileBase, op, absoluteOpIndex, semantics, pointerCache));
|
|
943
|
+
if (semantics === "sequential") workingBase = applyPatchOpToJsonWithStructuralSharing(workingBase, op, absoluteOpIndex, pointerCache);
|
|
689
944
|
}
|
|
690
945
|
return intents;
|
|
691
946
|
}
|
|
947
|
+
/** Compile a single JSON Patch operation into CRDT intents. */
|
|
948
|
+
function compileJsonPatchOpToIntent(baseJson, op, options = {}) {
|
|
949
|
+
const internalOptions = options;
|
|
950
|
+
const semantics = options.semantics ?? "sequential";
|
|
951
|
+
const pointerCache = internalOptions.pointerCache ?? /* @__PURE__ */ new Map();
|
|
952
|
+
return compileSingleOp(baseJson, op, internalOptions.opIndexOffset ?? 0, semantics, pointerCache);
|
|
953
|
+
}
|
|
692
954
|
/**
|
|
693
955
|
* Compute a JSON Patch delta between two JSON values.
|
|
694
956
|
* By default arrays use a deterministic LCS strategy.
|
|
695
957
|
* Pass `{ arrayStrategy: "atomic" }` for single-op array replacement.
|
|
958
|
+
* Pass `{ arrayStrategy: "lcs-linear" }` for a lower-memory LCS variant.
|
|
959
|
+
* Use `lcsLinearMaxCells` to optionally cap worst-case `lcs-linear` work and
|
|
960
|
+
* fall back to an atomic array replacement for very large unmatched windows.
|
|
961
|
+
* Pass `{ emitMoves: true }` or `{ emitCopies: true }` to opt into RFC 6902
|
|
962
|
+
* move/copy emission when a deterministic rewrite is available.
|
|
696
963
|
* @param base - The original JSON value.
|
|
697
964
|
* @param next - The target JSON value.
|
|
698
965
|
* @param options - Diff options.
|
|
@@ -707,175 +974,457 @@ function diffJsonPatch(base, next, options = {}) {
|
|
|
707
974
|
return ops;
|
|
708
975
|
}
|
|
709
976
|
function diffValue(path, base, next, ops, options) {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
977
|
+
const stack = [{
|
|
978
|
+
kind: "value",
|
|
979
|
+
base,
|
|
980
|
+
next
|
|
981
|
+
}];
|
|
982
|
+
while (stack.length > 0) {
|
|
983
|
+
const frame = stack.pop();
|
|
984
|
+
if (frame.kind === "path-pop") {
|
|
985
|
+
path.pop();
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
if (frame.kind === "object") {
|
|
989
|
+
if (frame.index >= frame.sharedKeys.length) continue;
|
|
990
|
+
const key = frame.sharedKeys[frame.index];
|
|
991
|
+
stack.push({
|
|
992
|
+
kind: "object",
|
|
993
|
+
base: frame.base,
|
|
994
|
+
next: frame.next,
|
|
995
|
+
sharedKeys: frame.sharedKeys,
|
|
996
|
+
index: frame.index + 1
|
|
997
|
+
});
|
|
998
|
+
path.push(key);
|
|
999
|
+
stack.push({ kind: "path-pop" });
|
|
1000
|
+
stack.push({
|
|
1001
|
+
kind: "value",
|
|
1002
|
+
base: frame.base[key],
|
|
1003
|
+
next: frame.next[key]
|
|
1004
|
+
});
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
assertTraversalDepth(path.length);
|
|
1008
|
+
if (frame.base === frame.next) continue;
|
|
1009
|
+
const baseIsArray = Array.isArray(frame.base);
|
|
1010
|
+
const nextIsArray = Array.isArray(frame.next);
|
|
1011
|
+
if (baseIsArray || nextIsArray) {
|
|
1012
|
+
if (!baseIsArray || !nextIsArray) {
|
|
1013
|
+
ops.push({
|
|
1014
|
+
op: "replace",
|
|
1015
|
+
path: stringifyJsonPointer(path),
|
|
1016
|
+
value: frame.next
|
|
1017
|
+
});
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
if (jsonEquals(frame.base, frame.next)) continue;
|
|
1021
|
+
const arrayStrategy = options.arrayStrategy ?? "lcs";
|
|
1022
|
+
if (arrayStrategy === "lcs") {
|
|
1023
|
+
if (!diffArrayWithLcsMatrix(path, frame.base, frame.next, ops, options)) ops.push({
|
|
1024
|
+
op: "replace",
|
|
1025
|
+
path: stringifyJsonPointer(path),
|
|
1026
|
+
value: frame.next
|
|
1027
|
+
});
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
if (arrayStrategy === "lcs-linear") {
|
|
1031
|
+
if (!diffArrayWithLinearLcs(path, frame.base, frame.next, ops, options)) ops.push({
|
|
1032
|
+
op: "replace",
|
|
1033
|
+
path: stringifyJsonPointer(path),
|
|
1034
|
+
value: frame.next
|
|
1035
|
+
});
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
ops.push({
|
|
714
1039
|
op: "replace",
|
|
715
1040
|
path: stringifyJsonPointer(path),
|
|
716
|
-
value: next
|
|
1041
|
+
value: frame.next
|
|
717
1042
|
});
|
|
718
|
-
|
|
1043
|
+
continue;
|
|
719
1044
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
1045
|
+
const baseIsObject = isPlainObject(frame.base);
|
|
1046
|
+
const nextIsObject = isPlainObject(frame.next);
|
|
1047
|
+
if (!baseIsObject || !nextIsObject) {
|
|
1048
|
+
ops.push({
|
|
1049
|
+
op: "replace",
|
|
1050
|
+
path: stringifyJsonPointer(path),
|
|
1051
|
+
value: frame.next
|
|
1052
|
+
});
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
const { sharedKeys, baseOnlyKeys, nextOnlyKeys } = collectObjectKeys(frame.base, frame.next);
|
|
1056
|
+
if (!(baseOnlyKeys.length > 0 || nextOnlyKeys.length > 0) && (path.length === 0 || sharedKeys.length > 1) && jsonEquals(frame.base, frame.next)) continue;
|
|
1057
|
+
emitObjectStructuralOps(path, frame.base, frame.next, sharedKeys, baseOnlyKeys, nextOnlyKeys, ops, options);
|
|
1058
|
+
if (sharedKeys.length > 0) stack.push({
|
|
1059
|
+
kind: "object",
|
|
1060
|
+
base: frame.base,
|
|
1061
|
+
next: frame.next,
|
|
1062
|
+
sharedKeys,
|
|
1063
|
+
index: 0
|
|
732
1064
|
});
|
|
733
|
-
return;
|
|
734
1065
|
}
|
|
1066
|
+
}
|
|
1067
|
+
function collectObjectKeys(base, next) {
|
|
735
1068
|
const baseKeys = Object.keys(base).sort();
|
|
736
1069
|
const nextKeys = Object.keys(next).sort();
|
|
1070
|
+
const baseOnlyKeys = [];
|
|
1071
|
+
const nextOnlyKeys = [];
|
|
1072
|
+
const sharedKeys = [];
|
|
737
1073
|
let baseIndex = 0;
|
|
738
1074
|
let nextIndex = 0;
|
|
739
1075
|
while (baseIndex < baseKeys.length && nextIndex < nextKeys.length) {
|
|
740
1076
|
const baseKey = baseKeys[baseIndex];
|
|
741
1077
|
const nextKey = nextKeys[nextIndex];
|
|
742
1078
|
if (baseKey === nextKey) {
|
|
1079
|
+
sharedKeys.push(baseKey);
|
|
743
1080
|
baseIndex += 1;
|
|
744
1081
|
nextIndex += 1;
|
|
745
1082
|
continue;
|
|
746
1083
|
}
|
|
747
1084
|
if (baseKey < nextKey) {
|
|
1085
|
+
baseOnlyKeys.push(baseKey);
|
|
1086
|
+
baseIndex += 1;
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
nextOnlyKeys.push(nextKey);
|
|
1090
|
+
nextIndex += 1;
|
|
1091
|
+
}
|
|
1092
|
+
while (baseIndex < baseKeys.length) {
|
|
1093
|
+
baseOnlyKeys.push(baseKeys[baseIndex]);
|
|
1094
|
+
baseIndex += 1;
|
|
1095
|
+
}
|
|
1096
|
+
while (nextIndex < nextKeys.length) {
|
|
1097
|
+
nextOnlyKeys.push(nextKeys[nextIndex]);
|
|
1098
|
+
nextIndex += 1;
|
|
1099
|
+
}
|
|
1100
|
+
return {
|
|
1101
|
+
sharedKeys,
|
|
1102
|
+
baseOnlyKeys,
|
|
1103
|
+
nextOnlyKeys
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
function emitObjectStructuralOps(path, base, next, sharedKeys, baseOnlyKeys, nextOnlyKeys, ops, options) {
|
|
1107
|
+
if (!options.emitMoves && !options.emitCopies) {
|
|
1108
|
+
for (const baseKey of baseOnlyKeys) {
|
|
748
1109
|
path.push(baseKey);
|
|
749
1110
|
ops.push({
|
|
750
1111
|
op: "remove",
|
|
751
1112
|
path: stringifyJsonPointer(path)
|
|
752
1113
|
});
|
|
753
1114
|
path.pop();
|
|
754
|
-
baseIndex += 1;
|
|
755
|
-
continue;
|
|
756
1115
|
}
|
|
757
|
-
|
|
1116
|
+
for (const nextKey of nextOnlyKeys) {
|
|
1117
|
+
path.push(nextKey);
|
|
1118
|
+
ops.push({
|
|
1119
|
+
op: "add",
|
|
1120
|
+
path: stringifyJsonPointer(path),
|
|
1121
|
+
value: next[nextKey]
|
|
1122
|
+
});
|
|
1123
|
+
path.pop();
|
|
1124
|
+
}
|
|
1125
|
+
return;
|
|
758
1126
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1127
|
+
const structuralKeyCache = /* @__PURE__ */ new WeakMap();
|
|
1128
|
+
const matchedMoveSources = /* @__PURE__ */ new Set();
|
|
1129
|
+
const moveTargets = /* @__PURE__ */ new Map();
|
|
1130
|
+
if (options.emitMoves) {
|
|
1131
|
+
const moveSourceBuckets = /* @__PURE__ */ new Map();
|
|
1132
|
+
for (const baseKey of baseOnlyKeys) insertObjectSourceBucket(moveSourceBuckets, baseKey, base[baseKey], structuralKeyCache);
|
|
1133
|
+
for (const nextKey of nextOnlyKeys) {
|
|
1134
|
+
const bucket = moveSourceBuckets.get(stableJsonValueKey(next[nextKey], structuralKeyCache));
|
|
1135
|
+
if (!bucket) continue;
|
|
1136
|
+
if (bucket.length > 0) {
|
|
1137
|
+
const candidate = bucket.shift();
|
|
1138
|
+
matchedMoveSources.add(candidate);
|
|
1139
|
+
moveTargets.set(nextKey, candidate);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
768
1142
|
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
1143
|
+
const copySourceBuckets = /* @__PURE__ */ new Map();
|
|
1144
|
+
for (const key of sharedKeys) {
|
|
1145
|
+
if (!jsonEquals(base[key], next[key])) continue;
|
|
1146
|
+
insertObjectSourceBucket(copySourceBuckets, key, base[key], structuralKeyCache);
|
|
1147
|
+
}
|
|
1148
|
+
for (const nextKey of nextOnlyKeys) {
|
|
1149
|
+
path.push(nextKey);
|
|
1150
|
+
const targetPath = stringifyJsonPointer(path);
|
|
1151
|
+
path.pop();
|
|
1152
|
+
const moveSource = moveTargets.get(nextKey);
|
|
1153
|
+
if (moveSource !== void 0) {
|
|
1154
|
+
path.push(moveSource);
|
|
1155
|
+
const fromPath = stringifyJsonPointer(path);
|
|
1156
|
+
path.pop();
|
|
1157
|
+
ops.push({
|
|
1158
|
+
op: "move",
|
|
1159
|
+
from: fromPath,
|
|
1160
|
+
path: targetPath
|
|
1161
|
+
});
|
|
1162
|
+
insertObjectSourceBucket(copySourceBuckets, nextKey, next[nextKey], structuralKeyCache);
|
|
777
1163
|
continue;
|
|
778
1164
|
}
|
|
779
|
-
if (
|
|
780
|
-
|
|
781
|
-
|
|
1165
|
+
if (options.emitCopies) {
|
|
1166
|
+
const copySource = findObjectCopySource(copySourceBuckets, next[nextKey], structuralKeyCache);
|
|
1167
|
+
if (copySource !== void 0) {
|
|
1168
|
+
path.push(copySource);
|
|
1169
|
+
const fromPath = stringifyJsonPointer(path);
|
|
1170
|
+
path.pop();
|
|
1171
|
+
ops.push({
|
|
1172
|
+
op: "copy",
|
|
1173
|
+
from: fromPath,
|
|
1174
|
+
path: targetPath
|
|
1175
|
+
});
|
|
1176
|
+
insertObjectSourceBucket(copySourceBuckets, nextKey, next[nextKey], structuralKeyCache);
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
782
1179
|
}
|
|
783
|
-
path.push(nextKey);
|
|
784
1180
|
ops.push({
|
|
785
1181
|
op: "add",
|
|
786
|
-
path:
|
|
1182
|
+
path: targetPath,
|
|
787
1183
|
value: next[nextKey]
|
|
788
1184
|
});
|
|
789
|
-
|
|
790
|
-
nextIndex += 1;
|
|
1185
|
+
insertObjectSourceBucket(copySourceBuckets, nextKey, next[nextKey], structuralKeyCache);
|
|
791
1186
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
path.push(
|
|
1187
|
+
for (const baseKey of baseOnlyKeys) {
|
|
1188
|
+
if (matchedMoveSources.has(baseKey)) continue;
|
|
1189
|
+
path.push(baseKey);
|
|
795
1190
|
ops.push({
|
|
796
|
-
op: "
|
|
797
|
-
path: stringifyJsonPointer(path)
|
|
798
|
-
value: next[nextKey]
|
|
1191
|
+
op: "remove",
|
|
1192
|
+
path: stringifyJsonPointer(path)
|
|
799
1193
|
});
|
|
800
1194
|
path.pop();
|
|
801
|
-
nextIndex += 1;
|
|
802
1195
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
1196
|
+
}
|
|
1197
|
+
function insertObjectSourceBucket(buckets, key, value, structuralKeyCache) {
|
|
1198
|
+
const bucketKey = stableJsonValueKey(value, structuralKeyCache);
|
|
1199
|
+
let bucket = buckets.get(bucketKey);
|
|
1200
|
+
if (!bucket) {
|
|
1201
|
+
bucket = [];
|
|
1202
|
+
buckets.set(bucketKey, bucket);
|
|
1203
|
+
}
|
|
1204
|
+
insertSortedKey(bucket, key);
|
|
1205
|
+
}
|
|
1206
|
+
function findObjectCopySource(copySourceBuckets, target, structuralKeyCache) {
|
|
1207
|
+
return copySourceBuckets.get(stableJsonValueKey(target, structuralKeyCache))?.[0];
|
|
1208
|
+
}
|
|
1209
|
+
function insertSortedKey(keys, key) {
|
|
1210
|
+
let low = 0;
|
|
1211
|
+
let high = keys.length;
|
|
1212
|
+
while (low < high) {
|
|
1213
|
+
const mid = Math.floor((low + high) / 2);
|
|
1214
|
+
if (keys[mid] < key) low = mid + 1;
|
|
1215
|
+
else high = mid;
|
|
1216
|
+
}
|
|
1217
|
+
keys.splice(low, 0, key);
|
|
1218
|
+
}
|
|
1219
|
+
function diffArrayWithLcsMatrix(path, base, next, ops, options) {
|
|
1220
|
+
const window = trimEqualArrayEdges(base, next);
|
|
1221
|
+
const baseStart = window.baseStart;
|
|
1222
|
+
const nextStart = window.nextStart;
|
|
1223
|
+
const n = window.unmatchedBaseLength;
|
|
1224
|
+
const m = window.unmatchedNextLength;
|
|
1225
|
+
if (!shouldUseLcsDiff(n, m, options.lcsMaxCells)) return false;
|
|
1226
|
+
if (n === 0 && m === 0) return true;
|
|
1227
|
+
const steps = [];
|
|
1228
|
+
buildArrayEditScriptWithMatrix(base, baseStart, baseStart + n, next, nextStart, nextStart + m, steps);
|
|
1229
|
+
pushArrayPatchOps(path, window.prefixLength, steps, ops, base, options);
|
|
1230
|
+
return true;
|
|
1231
|
+
}
|
|
1232
|
+
function diffArrayWithLinearLcs(path, base, next, ops, options) {
|
|
1233
|
+
const window = trimEqualArrayEdges(base, next);
|
|
1234
|
+
if (!shouldUseLinearLcsDiff(window.unmatchedBaseLength, window.unmatchedNextLength, options)) return false;
|
|
1235
|
+
const steps = [];
|
|
1236
|
+
buildArrayEditScriptLinearSpace(base, window.baseStart, window.baseStart + window.unmatchedBaseLength, next, window.nextStart, window.nextStart + window.unmatchedNextLength, steps);
|
|
1237
|
+
pushArrayPatchOps(path, window.prefixLength, steps, ops, base, options);
|
|
1238
|
+
return true;
|
|
1239
|
+
}
|
|
1240
|
+
function trimEqualArrayEdges(base, next) {
|
|
1241
|
+
const baseLength = base.length;
|
|
1242
|
+
const nextLength = next.length;
|
|
1243
|
+
let prefixLength = 0;
|
|
1244
|
+
while (prefixLength < baseLength && prefixLength < nextLength && jsonEquals(base[prefixLength], next[prefixLength])) prefixLength += 1;
|
|
1245
|
+
let suffixLength = 0;
|
|
1246
|
+
while (suffixLength < baseLength - prefixLength && suffixLength < nextLength - prefixLength && jsonEquals(base[baseLength - 1 - suffixLength], next[nextLength - 1 - suffixLength])) suffixLength += 1;
|
|
1247
|
+
return {
|
|
1248
|
+
baseStart: prefixLength,
|
|
1249
|
+
nextStart: prefixLength,
|
|
1250
|
+
prefixLength,
|
|
1251
|
+
unmatchedBaseLength: baseLength - prefixLength - suffixLength,
|
|
1252
|
+
unmatchedNextLength: nextLength - prefixLength - suffixLength
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
function buildArrayEditScriptLinearSpace(base, baseStart, baseEnd, next, nextStart, nextEnd, steps) {
|
|
1256
|
+
const unmatchedBaseLength = baseEnd - baseStart;
|
|
1257
|
+
const unmatchedNextLength = nextEnd - nextStart;
|
|
1258
|
+
if (unmatchedBaseLength === 0) {
|
|
1259
|
+
for (let nextIndex = nextStart; nextIndex < nextEnd; nextIndex++) steps.push({
|
|
1260
|
+
kind: "add",
|
|
1261
|
+
value: next[nextIndex]
|
|
1262
|
+
});
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
if (unmatchedNextLength === 0) {
|
|
1266
|
+
for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) steps.push({ kind: "remove" });
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
if (unmatchedBaseLength === 1) {
|
|
1270
|
+
pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, steps);
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
if (unmatchedNextLength === 1) {
|
|
1274
|
+
pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, steps);
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
if (shouldUseMatrixBaseCase(unmatchedBaseLength, unmatchedNextLength)) {
|
|
1278
|
+
buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStart, nextEnd, steps);
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
const baseMid = baseStart + Math.floor(unmatchedBaseLength / 2);
|
|
1282
|
+
const forwardScores = computeLcsPrefixLengths(base, baseStart, baseMid, next, nextStart, nextEnd);
|
|
1283
|
+
const reverseScores = computeLcsSuffixLengths(base, baseMid, baseEnd, next, nextStart, nextEnd);
|
|
1284
|
+
let bestOffset = 0;
|
|
1285
|
+
let bestScore = Number.NEGATIVE_INFINITY;
|
|
1286
|
+
for (let offset = 0; offset <= unmatchedNextLength; offset++) {
|
|
1287
|
+
const score = forwardScores[offset] + reverseScores[offset];
|
|
1288
|
+
if (score > bestScore) {
|
|
1289
|
+
bestScore = score;
|
|
1290
|
+
bestOffset = offset;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
const nextMid = nextStart + bestOffset;
|
|
1294
|
+
buildArrayEditScriptLinearSpace(base, baseStart, baseMid, next, nextStart, nextMid, steps);
|
|
1295
|
+
buildArrayEditScriptLinearSpace(base, baseMid, baseEnd, next, nextMid, nextEnd, steps);
|
|
1296
|
+
}
|
|
1297
|
+
function pushSingleBaseElementSteps(base, baseStart, next, nextStart, nextEnd, steps) {
|
|
1298
|
+
const matchIndex = findFirstMatchingIndexInNext(base[baseStart], next, nextStart, nextEnd);
|
|
1299
|
+
if (matchIndex === -1) {
|
|
1300
|
+
steps.push({ kind: "remove" });
|
|
1301
|
+
for (let nextIndex = nextStart; nextIndex < nextEnd; nextIndex++) steps.push({
|
|
1302
|
+
kind: "add",
|
|
1303
|
+
value: next[nextIndex]
|
|
1304
|
+
});
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
for (let nextIndex = nextStart; nextIndex < matchIndex; nextIndex++) steps.push({
|
|
1308
|
+
kind: "add",
|
|
1309
|
+
value: next[nextIndex]
|
|
1310
|
+
});
|
|
1311
|
+
steps.push({ kind: "equal" });
|
|
1312
|
+
for (let nextIndex = matchIndex + 1; nextIndex < nextEnd; nextIndex++) steps.push({
|
|
1313
|
+
kind: "add",
|
|
1314
|
+
value: next[nextIndex]
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
function pushSingleNextElementSteps(base, baseStart, baseEnd, next, nextStart, steps) {
|
|
1318
|
+
const matchIndex = findFirstMatchingIndexInBase(next[nextStart], base, baseStart, baseEnd);
|
|
1319
|
+
if (matchIndex === -1) {
|
|
1320
|
+
for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) steps.push({ kind: "remove" });
|
|
1321
|
+
steps.push({
|
|
1322
|
+
kind: "add",
|
|
1323
|
+
value: next[nextStart]
|
|
1324
|
+
});
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
for (let baseIndex = baseStart; baseIndex < matchIndex; baseIndex++) steps.push({ kind: "remove" });
|
|
1328
|
+
steps.push({ kind: "equal" });
|
|
1329
|
+
for (let baseIndex = matchIndex + 1; baseIndex < baseEnd; baseIndex++) steps.push({ kind: "remove" });
|
|
1330
|
+
}
|
|
1331
|
+
function findFirstMatchingIndexInNext(target, next, nextStart, nextEnd) {
|
|
1332
|
+
for (let nextIndex = nextStart; nextIndex < nextEnd; nextIndex++) if (jsonEquals(target, next[nextIndex])) return nextIndex;
|
|
1333
|
+
return -1;
|
|
1334
|
+
}
|
|
1335
|
+
function findFirstMatchingIndexInBase(target, base, baseStart, baseEnd) {
|
|
1336
|
+
for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) if (jsonEquals(target, base[baseIndex])) return baseIndex;
|
|
1337
|
+
return -1;
|
|
1338
|
+
}
|
|
1339
|
+
function shouldUseMatrixBaseCase(baseLength, nextLength) {
|
|
1340
|
+
return (baseLength + 1) * (nextLength + 1) <= LINEAR_LCS_MATRIX_BASE_CASE_MAX_CELLS;
|
|
1341
|
+
}
|
|
1342
|
+
function buildArrayEditScriptWithMatrix(base, baseStart, baseEnd, next, nextStart, nextEnd, steps) {
|
|
1343
|
+
const unmatchedBaseLength = baseEnd - baseStart;
|
|
1344
|
+
const unmatchedNextLength = nextEnd - nextStart;
|
|
1345
|
+
const lcs = Array.from({ length: unmatchedBaseLength + 1 }, () => Array(unmatchedNextLength + 1).fill(0));
|
|
1346
|
+
for (let baseOffset = unmatchedBaseLength - 1; baseOffset >= 0; baseOffset--) for (let nextOffset = unmatchedNextLength - 1; nextOffset >= 0; nextOffset--) if (jsonEquals(base[baseStart + baseOffset], next[nextStart + nextOffset])) lcs[baseOffset][nextOffset] = 1 + lcs[baseOffset + 1][nextOffset + 1];
|
|
1347
|
+
else lcs[baseOffset][nextOffset] = Math.max(lcs[baseOffset + 1][nextOffset], lcs[baseOffset][nextOffset + 1]);
|
|
1348
|
+
let baseOffset = 0;
|
|
1349
|
+
let nextOffset = 0;
|
|
1350
|
+
while (baseOffset < unmatchedBaseLength || nextOffset < unmatchedNextLength) {
|
|
1351
|
+
if (baseOffset < unmatchedBaseLength && nextOffset < unmatchedNextLength && jsonEquals(base[baseStart + baseOffset], next[nextStart + nextOffset])) {
|
|
1352
|
+
steps.push({ kind: "equal" });
|
|
1353
|
+
baseOffset += 1;
|
|
1354
|
+
nextOffset += 1;
|
|
814
1355
|
continue;
|
|
815
1356
|
}
|
|
816
|
-
|
|
817
|
-
|
|
1357
|
+
const lcsDown = baseOffset < unmatchedBaseLength ? lcs[baseOffset + 1][nextOffset] : -1;
|
|
1358
|
+
const lcsRight = nextOffset < unmatchedNextLength ? lcs[baseOffset][nextOffset + 1] : -1;
|
|
1359
|
+
if (nextOffset < unmatchedNextLength && (baseOffset === unmatchedBaseLength || lcsRight > lcsDown)) {
|
|
1360
|
+
steps.push({
|
|
1361
|
+
kind: "add",
|
|
1362
|
+
value: next[nextStart + nextOffset]
|
|
1363
|
+
});
|
|
1364
|
+
nextOffset += 1;
|
|
818
1365
|
continue;
|
|
819
1366
|
}
|
|
820
|
-
|
|
1367
|
+
if (baseOffset < unmatchedBaseLength) {
|
|
1368
|
+
steps.push({ kind: "remove" });
|
|
1369
|
+
baseOffset += 1;
|
|
1370
|
+
}
|
|
821
1371
|
}
|
|
822
1372
|
}
|
|
823
|
-
function
|
|
824
|
-
const
|
|
825
|
-
|
|
826
|
-
let
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
1373
|
+
function computeLcsPrefixLengths(base, baseStart, baseEnd, next, nextStart, nextEnd) {
|
|
1374
|
+
const unmatchedNextLength = nextEnd - nextStart;
|
|
1375
|
+
let previousRow = new Int32Array(unmatchedNextLength + 1);
|
|
1376
|
+
let currentRow = new Int32Array(unmatchedNextLength + 1);
|
|
1377
|
+
for (let baseIndex = baseStart; baseIndex < baseEnd; baseIndex++) {
|
|
1378
|
+
for (let nextOffset = 0; nextOffset < unmatchedNextLength; nextOffset++) if (jsonEquals(base[baseIndex], next[nextStart + nextOffset])) currentRow[nextOffset + 1] = previousRow[nextOffset] + 1;
|
|
1379
|
+
else currentRow[nextOffset + 1] = Math.max(previousRow[nextOffset + 1], currentRow[nextOffset]);
|
|
1380
|
+
const nextPreviousRow = currentRow;
|
|
1381
|
+
currentRow = previousRow;
|
|
1382
|
+
previousRow = nextPreviousRow;
|
|
1383
|
+
currentRow.fill(0);
|
|
1384
|
+
}
|
|
1385
|
+
return previousRow;
|
|
1386
|
+
}
|
|
1387
|
+
function computeLcsSuffixLengths(base, baseStart, baseEnd, next, nextStart, nextEnd) {
|
|
1388
|
+
const unmatchedNextLength = nextEnd - nextStart;
|
|
1389
|
+
let previousRow = new Int32Array(unmatchedNextLength + 1);
|
|
1390
|
+
let currentRow = new Int32Array(unmatchedNextLength + 1);
|
|
1391
|
+
for (let baseIndex = baseEnd - 1; baseIndex >= baseStart; baseIndex--) {
|
|
1392
|
+
for (let nextOffset = unmatchedNextLength - 1; nextOffset >= 0; nextOffset--) if (jsonEquals(base[baseIndex], next[nextStart + nextOffset])) currentRow[nextOffset] = previousRow[nextOffset + 1] + 1;
|
|
1393
|
+
else currentRow[nextOffset] = Math.max(previousRow[nextOffset], currentRow[nextOffset + 1]);
|
|
1394
|
+
const nextPreviousRow = currentRow;
|
|
1395
|
+
currentRow = previousRow;
|
|
1396
|
+
previousRow = nextPreviousRow;
|
|
1397
|
+
currentRow.fill(0);
|
|
1398
|
+
}
|
|
1399
|
+
return previousRow;
|
|
1400
|
+
}
|
|
1401
|
+
function pushArrayPatchOps(path, startIndex, steps, ops, base, options) {
|
|
839
1402
|
const localOps = [];
|
|
840
|
-
let
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
while (i < n || j < m) {
|
|
844
|
-
if (i < n && j < m && jsonEquals(base[baseStart + i], next[nextStart + j])) {
|
|
845
|
-
i += 1;
|
|
846
|
-
j += 1;
|
|
1403
|
+
let index = startIndex;
|
|
1404
|
+
for (const step of steps) {
|
|
1405
|
+
if (step.kind === "equal") {
|
|
847
1406
|
index += 1;
|
|
848
1407
|
continue;
|
|
849
1408
|
}
|
|
850
|
-
const
|
|
851
|
-
|
|
852
|
-
if (
|
|
853
|
-
const indexSegment = String(index);
|
|
854
|
-
path.push(indexSegment);
|
|
1409
|
+
const indexSegment = String(index);
|
|
1410
|
+
path.push(indexSegment);
|
|
1411
|
+
if (step.kind === "add") {
|
|
855
1412
|
localOps.push({
|
|
856
1413
|
op: "add",
|
|
857
1414
|
path: stringifyJsonPointer(path),
|
|
858
|
-
value:
|
|
1415
|
+
value: step.value
|
|
859
1416
|
});
|
|
860
|
-
path.pop();
|
|
861
|
-
j += 1;
|
|
862
1417
|
index += 1;
|
|
863
|
-
continue;
|
|
864
|
-
}
|
|
865
|
-
if (i < n) {
|
|
866
|
-
const indexSegment = String(index);
|
|
867
|
-
path.push(indexSegment);
|
|
868
|
-
localOps.push({
|
|
869
|
-
op: "remove",
|
|
870
|
-
path: stringifyJsonPointer(path)
|
|
871
|
-
});
|
|
872
1418
|
path.pop();
|
|
873
|
-
i += 1;
|
|
874
1419
|
continue;
|
|
875
1420
|
}
|
|
1421
|
+
localOps.push({
|
|
1422
|
+
op: "remove",
|
|
1423
|
+
path: stringifyJsonPointer(path)
|
|
1424
|
+
});
|
|
1425
|
+
path.pop();
|
|
876
1426
|
}
|
|
877
|
-
ops.push(...
|
|
878
|
-
return true;
|
|
1427
|
+
ops.push(...finalizeArrayOps(path, base, localOps, options));
|
|
879
1428
|
}
|
|
880
1429
|
function shouldUseLcsDiff(baseLength, nextLength, lcsMaxCells) {
|
|
881
1430
|
if (lcsMaxCells === Number.POSITIVE_INFINITY) return true;
|
|
@@ -883,6 +1432,169 @@ function shouldUseLcsDiff(baseLength, nextLength, lcsMaxCells) {
|
|
|
883
1432
|
if (!Number.isFinite(cap) || cap < 1) return false;
|
|
884
1433
|
return (baseLength + 1) * (nextLength + 1) <= cap;
|
|
885
1434
|
}
|
|
1435
|
+
function shouldUseLinearLcsDiff(baseLength, nextLength, options) {
|
|
1436
|
+
const cap = options.lcsLinearMaxCells;
|
|
1437
|
+
if (cap === void 0 || cap === Number.POSITIVE_INFINITY) return true;
|
|
1438
|
+
if (!Number.isFinite(cap) || cap < 1) return false;
|
|
1439
|
+
return (baseLength + 1) * (nextLength + 1) <= cap;
|
|
1440
|
+
}
|
|
1441
|
+
function finalizeArrayOps(arrayPath, base, ops, options) {
|
|
1442
|
+
if (ops.length === 0) return [];
|
|
1443
|
+
if (!options.emitMoves && !options.emitCopies) return compactArrayOps(ops);
|
|
1444
|
+
const out = [];
|
|
1445
|
+
const working = createArrayRewriteState(base);
|
|
1446
|
+
for (let i = 0; i < ops.length; i++) {
|
|
1447
|
+
const op = ops[i];
|
|
1448
|
+
const next = ops[i + 1];
|
|
1449
|
+
if (op.op === "remove" && next && next.op === "add") {
|
|
1450
|
+
const valuesMatch = working.entries[getArrayOpIndex(op.path, arrayPath)].key === getArrayRewriteValueKey(working, next.value);
|
|
1451
|
+
if (op.path === next.path) {
|
|
1452
|
+
const replaceOp = {
|
|
1453
|
+
op: "replace",
|
|
1454
|
+
path: op.path,
|
|
1455
|
+
value: next.value
|
|
1456
|
+
};
|
|
1457
|
+
out.push(replaceOp);
|
|
1458
|
+
applyArrayOptimizationOp(working, replaceOp, arrayPath);
|
|
1459
|
+
i += 1;
|
|
1460
|
+
continue;
|
|
1461
|
+
}
|
|
1462
|
+
if (options.emitMoves && valuesMatch) {
|
|
1463
|
+
const moveOp = {
|
|
1464
|
+
op: "move",
|
|
1465
|
+
from: op.path,
|
|
1466
|
+
path: next.path
|
|
1467
|
+
};
|
|
1468
|
+
out.push(moveOp);
|
|
1469
|
+
applyArrayOptimizationOp(working, moveOp, arrayPath);
|
|
1470
|
+
i += 1;
|
|
1471
|
+
continue;
|
|
1472
|
+
}
|
|
1473
|
+
if (valuesMatch) {
|
|
1474
|
+
out.push(op);
|
|
1475
|
+
applyArrayOptimizationOp(working, op, arrayPath);
|
|
1476
|
+
out.push(next);
|
|
1477
|
+
applyArrayOptimizationOp(working, next, arrayPath);
|
|
1478
|
+
i += 1;
|
|
1479
|
+
continue;
|
|
1480
|
+
}
|
|
1481
|
+
out.push(op);
|
|
1482
|
+
applyArrayOptimizationOp(working, op, arrayPath);
|
|
1483
|
+
continue;
|
|
1484
|
+
}
|
|
1485
|
+
if (op.op === "add" && next && next.op === "remove") {
|
|
1486
|
+
const targetIndex = getArrayOpIndex(op.path, arrayPath);
|
|
1487
|
+
const removeIndex = getArrayOpIndex(next.path, arrayPath);
|
|
1488
|
+
const sourceIndex = removeIndex - (targetIndex <= removeIndex ? 1 : 0);
|
|
1489
|
+
const matchesPendingRemove = sourceIndex >= 0 && sourceIndex < working.entries.length && working.entries[sourceIndex].key === getArrayRewriteValueKey(working, op.value);
|
|
1490
|
+
if (options.emitMoves && matchesPendingRemove) {
|
|
1491
|
+
const moveOp = {
|
|
1492
|
+
op: "move",
|
|
1493
|
+
from: stringifyJsonPointer([...arrayPath, String(sourceIndex)]),
|
|
1494
|
+
path: op.path
|
|
1495
|
+
};
|
|
1496
|
+
out.push(moveOp);
|
|
1497
|
+
applyArrayOptimizationOp(working, moveOp, arrayPath);
|
|
1498
|
+
i += 1;
|
|
1499
|
+
continue;
|
|
1500
|
+
}
|
|
1501
|
+
if (matchesPendingRemove) {
|
|
1502
|
+
out.push(op);
|
|
1503
|
+
applyArrayOptimizationOp(working, op, arrayPath);
|
|
1504
|
+
out.push(next);
|
|
1505
|
+
applyArrayOptimizationOp(working, next, arrayPath);
|
|
1506
|
+
i += 1;
|
|
1507
|
+
continue;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
if (op.op === "add" && options.emitCopies) {
|
|
1511
|
+
const copySourceIndex = findArrayCopySourceIndex(working, op.value);
|
|
1512
|
+
if (copySourceIndex !== -1) {
|
|
1513
|
+
const copyOp = {
|
|
1514
|
+
op: "copy",
|
|
1515
|
+
from: stringifyJsonPointer([...arrayPath, String(copySourceIndex)]),
|
|
1516
|
+
path: op.path
|
|
1517
|
+
};
|
|
1518
|
+
out.push(copyOp);
|
|
1519
|
+
applyArrayOptimizationOp(working, copyOp, arrayPath);
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
out.push(op);
|
|
1524
|
+
applyArrayOptimizationOp(working, op, arrayPath);
|
|
1525
|
+
}
|
|
1526
|
+
return out;
|
|
1527
|
+
}
|
|
1528
|
+
/** @internal Stable structural fingerprint used for deterministic diff rewrites. */
|
|
1529
|
+
function stableJsonValueKey(value, structuralKeyCache) {
|
|
1530
|
+
if (value !== null && typeof value === "object") {
|
|
1531
|
+
const cachedValue = structuralKeyCache?.get(value);
|
|
1532
|
+
if (cachedValue !== void 0) return cachedValue;
|
|
1533
|
+
}
|
|
1534
|
+
const stack = [{
|
|
1535
|
+
kind: "value",
|
|
1536
|
+
value,
|
|
1537
|
+
depth: 0
|
|
1538
|
+
}];
|
|
1539
|
+
const results = [];
|
|
1540
|
+
while (stack.length > 0) {
|
|
1541
|
+
const frame = stack.pop();
|
|
1542
|
+
if (frame.kind === "array") {
|
|
1543
|
+
const stableKey = `[${results.splice(frame.startIndex).join(",")}]`;
|
|
1544
|
+
structuralKeyCache?.set(frame.value, stableKey);
|
|
1545
|
+
results.push(stableKey);
|
|
1546
|
+
continue;
|
|
1547
|
+
}
|
|
1548
|
+
if (frame.kind === "object") {
|
|
1549
|
+
const childParts = results.splice(frame.startIndex);
|
|
1550
|
+
const stableKey = `{${frame.keys.map((key, index) => `${JSON.stringify(key)}:${childParts[index]}`).join(",")}}`;
|
|
1551
|
+
structuralKeyCache?.set(frame.value, stableKey);
|
|
1552
|
+
results.push(stableKey);
|
|
1553
|
+
continue;
|
|
1554
|
+
}
|
|
1555
|
+
assertTraversalDepth(frame.depth);
|
|
1556
|
+
if (frame.value === null || typeof frame.value !== "object") {
|
|
1557
|
+
results.push(JSON.stringify(frame.value));
|
|
1558
|
+
continue;
|
|
1559
|
+
}
|
|
1560
|
+
const cachedValue = structuralKeyCache?.get(frame.value);
|
|
1561
|
+
if (cachedValue !== void 0) {
|
|
1562
|
+
results.push(cachedValue);
|
|
1563
|
+
continue;
|
|
1564
|
+
}
|
|
1565
|
+
if (Array.isArray(frame.value)) {
|
|
1566
|
+
const startIndex = results.length;
|
|
1567
|
+
stack.push({
|
|
1568
|
+
kind: "array",
|
|
1569
|
+
value: frame.value,
|
|
1570
|
+
startIndex
|
|
1571
|
+
});
|
|
1572
|
+
for (let index = frame.value.length - 1; index >= 0; index--) stack.push({
|
|
1573
|
+
kind: "value",
|
|
1574
|
+
value: frame.value[index],
|
|
1575
|
+
depth: frame.depth + 1
|
|
1576
|
+
});
|
|
1577
|
+
continue;
|
|
1578
|
+
}
|
|
1579
|
+
const keys = Object.keys(frame.value).sort();
|
|
1580
|
+
const startIndex = results.length;
|
|
1581
|
+
stack.push({
|
|
1582
|
+
kind: "object",
|
|
1583
|
+
value: frame.value,
|
|
1584
|
+
keys,
|
|
1585
|
+
startIndex
|
|
1586
|
+
});
|
|
1587
|
+
for (let index = keys.length - 1; index >= 0; index--) {
|
|
1588
|
+
const key = keys[index];
|
|
1589
|
+
stack.push({
|
|
1590
|
+
kind: "value",
|
|
1591
|
+
value: frame.value[key],
|
|
1592
|
+
depth: frame.depth + 1
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
return results[0];
|
|
1597
|
+
}
|
|
886
1598
|
function compactArrayOps(ops) {
|
|
887
1599
|
const out = [];
|
|
888
1600
|
for (let i = 0; i < ops.length; i++) {
|
|
@@ -899,28 +1611,174 @@ function compactArrayOps(ops) {
|
|
|
899
1611
|
}
|
|
900
1612
|
out.push(op);
|
|
901
1613
|
}
|
|
902
|
-
return out;
|
|
1614
|
+
return out;
|
|
1615
|
+
}
|
|
1616
|
+
function createArrayRewriteState(base) {
|
|
1617
|
+
const structuralKeyCache = /* @__PURE__ */ new WeakMap();
|
|
1618
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
1619
|
+
return {
|
|
1620
|
+
entries: base.map((value, currentIndex) => {
|
|
1621
|
+
const entry = {
|
|
1622
|
+
value,
|
|
1623
|
+
key: stableJsonValueKey(value, structuralKeyCache),
|
|
1624
|
+
currentIndex,
|
|
1625
|
+
bucketIndex: -1
|
|
1626
|
+
};
|
|
1627
|
+
insertArrayRewriteBucketEntry(buckets, entry);
|
|
1628
|
+
return entry;
|
|
1629
|
+
}),
|
|
1630
|
+
buckets,
|
|
1631
|
+
structuralKeyCache
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
function getArrayRewriteValueKey(state, value) {
|
|
1635
|
+
return stableJsonValueKey(value, state.structuralKeyCache);
|
|
1636
|
+
}
|
|
1637
|
+
function findArrayCopySourceIndex(state, value) {
|
|
1638
|
+
return state.buckets.get(getArrayRewriteValueKey(state, value))?.[0]?.currentIndex ?? -1;
|
|
1639
|
+
}
|
|
1640
|
+
function insertArrayRewriteBucketEntry(buckets, entry) {
|
|
1641
|
+
let bucket = buckets.get(entry.key);
|
|
1642
|
+
if (!bucket) {
|
|
1643
|
+
bucket = [];
|
|
1644
|
+
buckets.set(entry.key, bucket);
|
|
1645
|
+
}
|
|
1646
|
+
let low = 0;
|
|
1647
|
+
let high = bucket.length;
|
|
1648
|
+
while (low < high) {
|
|
1649
|
+
const mid = Math.floor((low + high) / 2);
|
|
1650
|
+
if (bucket[mid].currentIndex < entry.currentIndex) low = mid + 1;
|
|
1651
|
+
else high = mid;
|
|
1652
|
+
}
|
|
1653
|
+
bucket.splice(low, 0, entry);
|
|
1654
|
+
reindexArrayRewriteBucketPositions(bucket, low);
|
|
1655
|
+
}
|
|
1656
|
+
function removeArrayRewriteBucketEntry(buckets, entry) {
|
|
1657
|
+
const bucket = buckets.get(entry.key);
|
|
1658
|
+
if (!bucket) return;
|
|
1659
|
+
const bucketIndex = entry.bucketIndex;
|
|
1660
|
+
if (bucketIndex < 0 || bucketIndex >= bucket.length || bucket[bucketIndex] !== entry) return;
|
|
1661
|
+
bucket.splice(bucketIndex, 1);
|
|
1662
|
+
if (bucket.length === 0) {
|
|
1663
|
+
buckets.delete(entry.key);
|
|
1664
|
+
entry.bucketIndex = -1;
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
entry.bucketIndex = -1;
|
|
1668
|
+
reindexArrayRewriteBucketPositions(bucket, bucketIndex);
|
|
1669
|
+
}
|
|
1670
|
+
function reindexArrayRewriteBucketPositions(bucket, startIndex) {
|
|
1671
|
+
for (let index = startIndex; index < bucket.length; index++) bucket[index].bucketIndex = index;
|
|
1672
|
+
}
|
|
1673
|
+
function reindexArrayRewriteEntries(entries, startIndex) {
|
|
1674
|
+
for (let index = startIndex; index < entries.length; index++) entries[index].currentIndex = index;
|
|
1675
|
+
}
|
|
1676
|
+
function getArrayOpIndex(ptr, arrayPath) {
|
|
1677
|
+
const parsed = parseJsonPointer(ptr);
|
|
1678
|
+
if (parsed.length !== arrayPath.length + 1) throw new Error(`Expected array operation under ${stringifyJsonPointer(arrayPath)}: ${ptr}`);
|
|
1679
|
+
for (let index = 0; index < arrayPath.length; index++) if (parsed[index] !== arrayPath[index]) throw new Error(`Expected array operation under ${stringifyJsonPointer(arrayPath)}: ${ptr}`);
|
|
1680
|
+
const token = parsed[arrayPath.length];
|
|
1681
|
+
if (!ARRAY_INDEX_TOKEN_PATTERN.test(token)) throw new Error(`Expected numeric array index at ${ptr}`);
|
|
1682
|
+
return Number(token);
|
|
1683
|
+
}
|
|
1684
|
+
function applyArrayOptimizationOp(working, op, arrayPath) {
|
|
1685
|
+
if (op.op === "add") {
|
|
1686
|
+
const index = getArrayOpIndex(op.path, arrayPath);
|
|
1687
|
+
const entry = {
|
|
1688
|
+
value: structuredClone(op.value),
|
|
1689
|
+
key: getArrayRewriteValueKey(working, op.value),
|
|
1690
|
+
currentIndex: index,
|
|
1691
|
+
bucketIndex: -1
|
|
1692
|
+
};
|
|
1693
|
+
working.entries.splice(index, 0, entry);
|
|
1694
|
+
reindexArrayRewriteEntries(working.entries, index + 1);
|
|
1695
|
+
insertArrayRewriteBucketEntry(working.buckets, entry);
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
if (op.op === "remove") {
|
|
1699
|
+
const index = getArrayOpIndex(op.path, arrayPath);
|
|
1700
|
+
const [removedEntry] = working.entries.splice(index, 1);
|
|
1701
|
+
if (removedEntry) removeArrayRewriteBucketEntry(working.buckets, removedEntry);
|
|
1702
|
+
reindexArrayRewriteEntries(working.entries, index);
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
if (op.op === "replace") {
|
|
1706
|
+
const index = getArrayOpIndex(op.path, arrayPath);
|
|
1707
|
+
const entry = working.entries[index];
|
|
1708
|
+
removeArrayRewriteBucketEntry(working.buckets, entry);
|
|
1709
|
+
entry.value = structuredClone(op.value);
|
|
1710
|
+
entry.key = getArrayRewriteValueKey(working, op.value);
|
|
1711
|
+
insertArrayRewriteBucketEntry(working.buckets, entry);
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
if (op.op === "copy") {
|
|
1715
|
+
const fromIndex = getArrayOpIndex(op.from, arrayPath);
|
|
1716
|
+
if (fromIndex < 0 || fromIndex >= working.entries.length) throw new Error(`applyArrayOptimizationOp: copy from index ${fromIndex} is out of bounds (length ${working.entries.length})`);
|
|
1717
|
+
const index = getArrayOpIndex(op.path, arrayPath);
|
|
1718
|
+
const source = working.entries[fromIndex];
|
|
1719
|
+
const entry = {
|
|
1720
|
+
value: structuredClone(source.value),
|
|
1721
|
+
key: source.key,
|
|
1722
|
+
currentIndex: index,
|
|
1723
|
+
bucketIndex: -1
|
|
1724
|
+
};
|
|
1725
|
+
working.entries.splice(index, 0, entry);
|
|
1726
|
+
reindexArrayRewriteEntries(working.entries, index + 1);
|
|
1727
|
+
insertArrayRewriteBucketEntry(working.buckets, entry);
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
if (op.op === "move") {
|
|
1731
|
+
const fromIndex = getArrayOpIndex(op.from, arrayPath);
|
|
1732
|
+
if (fromIndex < 0 || fromIndex >= working.entries.length) throw new Error(`applyArrayOptimizationOp: move from index ${fromIndex} is out of bounds (length ${working.entries.length})`);
|
|
1733
|
+
const [entry] = working.entries.splice(fromIndex, 1);
|
|
1734
|
+
if (!entry) throw new Error(`applyArrayOptimizationOp: move from index ${fromIndex} did not resolve`);
|
|
1735
|
+
removeArrayRewriteBucketEntry(working.buckets, entry);
|
|
1736
|
+
const index = getArrayOpIndex(op.path, arrayPath);
|
|
1737
|
+
working.entries.splice(index, 0, entry);
|
|
1738
|
+
reindexArrayRewriteEntries(working.entries, Math.min(fromIndex, index));
|
|
1739
|
+
insertArrayRewriteBucketEntry(working.buckets, entry);
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
throw new Error(`applyArrayOptimizationOp: unexpected op type "${op.op}"`);
|
|
903
1743
|
}
|
|
904
1744
|
function escapeJsonPointer(token) {
|
|
905
1745
|
return token.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
906
1746
|
}
|
|
907
1747
|
/** Deep equality check for JSON values (null-safe, handles arrays and objects). */
|
|
908
1748
|
function jsonEquals(a, b) {
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1749
|
+
const stack = [{
|
|
1750
|
+
left: a,
|
|
1751
|
+
right: b,
|
|
1752
|
+
depth: 0
|
|
1753
|
+
}];
|
|
1754
|
+
while (stack.length > 0) {
|
|
1755
|
+
const frame = stack.pop();
|
|
1756
|
+
assertTraversalDepth(frame.depth);
|
|
1757
|
+
if (frame.left === frame.right) continue;
|
|
1758
|
+
if (frame.left === null || frame.right === null) return false;
|
|
1759
|
+
if (Array.isArray(frame.left) || Array.isArray(frame.right)) {
|
|
1760
|
+
if (!Array.isArray(frame.left) || !Array.isArray(frame.right)) return false;
|
|
1761
|
+
if (frame.left.length !== frame.right.length) return false;
|
|
1762
|
+
for (let index = frame.left.length - 1; index >= 0; index--) stack.push({
|
|
1763
|
+
left: frame.left[index],
|
|
1764
|
+
right: frame.right[index],
|
|
1765
|
+
depth: frame.depth + 1
|
|
1766
|
+
});
|
|
1767
|
+
continue;
|
|
1768
|
+
}
|
|
1769
|
+
if (!isPlainObject(frame.left) || !isPlainObject(frame.right)) return false;
|
|
1770
|
+
const leftKeys = Object.keys(frame.left);
|
|
1771
|
+
const rightKeys = Object.keys(frame.right);
|
|
1772
|
+
if (leftKeys.length !== rightKeys.length) return false;
|
|
1773
|
+
for (let index = leftKeys.length - 1; index >= 0; index--) {
|
|
1774
|
+
const key = leftKeys[index];
|
|
1775
|
+
if (!hasOwn(frame.right, key)) return false;
|
|
1776
|
+
stack.push({
|
|
1777
|
+
left: frame.left[key],
|
|
1778
|
+
right: frame.right[key],
|
|
1779
|
+
depth: frame.depth + 1
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
924
1782
|
}
|
|
925
1783
|
return true;
|
|
926
1784
|
}
|
|
@@ -1497,6 +2355,10 @@ function cloneNodeAtDepth(node, depth) {
|
|
|
1497
2355
|
id: e.id,
|
|
1498
2356
|
prev: e.prev,
|
|
1499
2357
|
tombstone: e.tombstone,
|
|
2358
|
+
delDot: e.delDot ? {
|
|
2359
|
+
actor: e.delDot.actor,
|
|
2360
|
+
ctr: e.delDot.ctr
|
|
2361
|
+
} : void 0,
|
|
1500
2362
|
value: cloneNodeAtDepth(e.value, depth + 1),
|
|
1501
2363
|
insDot: {
|
|
1502
2364
|
actor: e.insDot.actor,
|
|
@@ -1601,7 +2463,17 @@ function applyObjRemove(head, it, newDot) {
|
|
|
1601
2463
|
objRemove(parentObj, it.key, d);
|
|
1602
2464
|
return null;
|
|
1603
2465
|
}
|
|
1604
|
-
function
|
|
2466
|
+
function createArrayIndexLookupSession() {
|
|
2467
|
+
const bySeq = /* @__PURE__ */ new WeakMap();
|
|
2468
|
+
return { get(seq) {
|
|
2469
|
+
const cached = bySeq.get(seq);
|
|
2470
|
+
if (cached) return cached;
|
|
2471
|
+
const created = rgaCreateIndexedIdSnapshot(seq);
|
|
2472
|
+
bySeq.set(seq, created);
|
|
2473
|
+
return created;
|
|
2474
|
+
} };
|
|
2475
|
+
}
|
|
2476
|
+
function applyArrInsert(base, head, it, newDot, indexSession, bumpCounterAbove, strictParents = false) {
|
|
1605
2477
|
const pointer = `/${it.path.join("/")}`;
|
|
1606
2478
|
const baseSeq = getSeqAtPath(base, it.path);
|
|
1607
2479
|
if (!baseSeq) {
|
|
@@ -1633,8 +2505,9 @@ function applyArrInsert(base, head, it, newDot, bumpCounterAbove, strictParents
|
|
|
1633
2505
|
const headSeqRes = getHeadSeqForBaseArrayIntent(head, it.path);
|
|
1634
2506
|
if (!headSeqRes.ok) return headSeqRes;
|
|
1635
2507
|
const headSeq = headSeqRes.seq;
|
|
1636
|
-
const
|
|
1637
|
-
const baseLen =
|
|
2508
|
+
const baseIndex = indexSession.get(baseSeq);
|
|
2509
|
+
const baseLen = baseIndex.length();
|
|
2510
|
+
const idx = it.index === Number.POSITIVE_INFINITY ? baseLen : it.index;
|
|
1638
2511
|
if (idx < 0 || idx > baseLen) return {
|
|
1639
2512
|
ok: false,
|
|
1640
2513
|
code: 409,
|
|
@@ -1642,20 +2515,18 @@ function applyArrInsert(base, head, it, newDot, bumpCounterAbove, strictParents
|
|
|
1642
2515
|
message: `index out of bounds at /${it.path.join("/")}/${it.index}`,
|
|
1643
2516
|
path: `/${it.path.join("/")}/${it.index}`
|
|
1644
2517
|
};
|
|
1645
|
-
const prev =
|
|
2518
|
+
const prev = baseIndex.prevForInsertAt(idx);
|
|
1646
2519
|
const dotRes = nextInsertDotForPrev(headSeq, prev, newDot, pointer, bumpCounterAbove);
|
|
1647
2520
|
if (!dotRes.ok) return dotRes;
|
|
1648
2521
|
const d = dotRes.dot;
|
|
1649
|
-
|
|
2522
|
+
const id = dotToElemId(d);
|
|
2523
|
+
rgaInsertAfter(headSeq, prev, id, d, nodeFromJson(it.value, newDot));
|
|
2524
|
+
if (baseSeq === headSeq) baseIndex.insertAt(idx, id);
|
|
1650
2525
|
return null;
|
|
1651
2526
|
}
|
|
1652
2527
|
function nextInsertDotForPrev(seq, prev, newDot, path, bumpCounterAbove) {
|
|
1653
2528
|
const MAX_INSERT_DOT_ATTEMPTS = 1024;
|
|
1654
|
-
|
|
1655
|
-
for (const elem of seq.elems.values()) {
|
|
1656
|
-
if (elem.prev !== prev) continue;
|
|
1657
|
-
if (!maxSiblingDot || compareDot(elem.insDot, maxSiblingDot) > 0) maxSiblingDot = elem.insDot;
|
|
1658
|
-
}
|
|
2529
|
+
const maxSiblingDot = rgaMaxInsertDotForPrev(seq, prev);
|
|
1659
2530
|
if (maxSiblingDot) bumpCounterAbove?.(maxSiblingDot.ctr);
|
|
1660
2531
|
if (!maxSiblingDot) return {
|
|
1661
2532
|
ok: true,
|
|
@@ -1676,8 +2547,8 @@ function nextInsertDotForPrev(seq, prev, newDot, path, bumpCounterAbove) {
|
|
|
1676
2547
|
path
|
|
1677
2548
|
};
|
|
1678
2549
|
}
|
|
1679
|
-
function applyArrDelete(base, head, it, newDot) {
|
|
1680
|
-
newDot();
|
|
2550
|
+
function applyArrDelete(base, head, it, newDot, indexSession) {
|
|
2551
|
+
const _d = newDot();
|
|
1681
2552
|
const baseSeq = getSeqAtPath(base, it.path);
|
|
1682
2553
|
if (!baseSeq) return {
|
|
1683
2554
|
ok: false,
|
|
@@ -1689,7 +2560,8 @@ function applyArrDelete(base, head, it, newDot) {
|
|
|
1689
2560
|
const headSeqRes = getHeadSeqForBaseArrayIntent(head, it.path);
|
|
1690
2561
|
if (!headSeqRes.ok) return headSeqRes;
|
|
1691
2562
|
const headSeq = headSeqRes.seq;
|
|
1692
|
-
const
|
|
2563
|
+
const baseIndex = indexSession.get(baseSeq);
|
|
2564
|
+
const baseId = baseIndex.idAt(it.index);
|
|
1693
2565
|
if (!baseId) return {
|
|
1694
2566
|
ok: false,
|
|
1695
2567
|
code: 409,
|
|
@@ -1704,10 +2576,11 @@ function applyArrDelete(base, head, it, newDot) {
|
|
|
1704
2576
|
message: `element missing in head lineage at index ${it.index}`,
|
|
1705
2577
|
path: `/${it.path.join("/")}/${it.index}`
|
|
1706
2578
|
};
|
|
1707
|
-
rgaDelete(headSeq, baseId);
|
|
2579
|
+
rgaDelete(headSeq, baseId, _d);
|
|
2580
|
+
if (baseSeq === headSeq) baseIndex.deleteAt(it.index);
|
|
1708
2581
|
return null;
|
|
1709
2582
|
}
|
|
1710
|
-
function applyArrReplace(base, head, it, newDot) {
|
|
2583
|
+
function applyArrReplace(base, head, it, newDot, indexSession) {
|
|
1711
2584
|
newDot();
|
|
1712
2585
|
const baseSeq = getSeqAtPath(base, it.path);
|
|
1713
2586
|
if (!baseSeq) return {
|
|
@@ -1720,7 +2593,7 @@ function applyArrReplace(base, head, it, newDot) {
|
|
|
1720
2593
|
const headSeqRes = getHeadSeqForBaseArrayIntent(head, it.path);
|
|
1721
2594
|
if (!headSeqRes.ok) return headSeqRes;
|
|
1722
2595
|
const headSeq = headSeqRes.seq;
|
|
1723
|
-
const baseId =
|
|
2596
|
+
const baseId = indexSession.get(baseSeq).idAt(it.index);
|
|
1724
2597
|
if (!baseId) return {
|
|
1725
2598
|
ok: false,
|
|
1726
2599
|
code: 409,
|
|
@@ -1753,6 +2626,7 @@ function applyArrReplace(base, head, it, newDot) {
|
|
|
1753
2626
|
* @returns `{ ok: true }` on success, or `{ ok: false, code: 409, message }` on conflict.
|
|
1754
2627
|
*/
|
|
1755
2628
|
function applyIntentsToCrdt(base, head, intents, newDot, evalTestAgainst = "head", bumpCounterAbove, options = {}) {
|
|
2629
|
+
const arrayIndexSession = createArrayIndexLookupSession();
|
|
1756
2630
|
for (const it of intents) {
|
|
1757
2631
|
let fail = null;
|
|
1758
2632
|
switch (it.t) {
|
|
@@ -1766,13 +2640,13 @@ function applyIntentsToCrdt(base, head, intents, newDot, evalTestAgainst = "head
|
|
|
1766
2640
|
fail = applyObjRemove(head, it, newDot);
|
|
1767
2641
|
break;
|
|
1768
2642
|
case "ArrInsert":
|
|
1769
|
-
fail = applyArrInsert(base, head, it, newDot, bumpCounterAbove, options.strictParents ?? false);
|
|
2643
|
+
fail = applyArrInsert(base, head, it, newDot, arrayIndexSession, bumpCounterAbove, options.strictParents ?? false);
|
|
1770
2644
|
break;
|
|
1771
2645
|
case "ArrDelete":
|
|
1772
|
-
fail = applyArrDelete(base, head, it, newDot);
|
|
2646
|
+
fail = applyArrDelete(base, head, it, newDot, arrayIndexSession);
|
|
1773
2647
|
break;
|
|
1774
2648
|
case "ArrReplace":
|
|
1775
|
-
fail = applyArrReplace(base, head, it, newDot);
|
|
2649
|
+
fail = applyArrReplace(base, head, it, newDot, arrayIndexSession);
|
|
1776
2650
|
break;
|
|
1777
2651
|
default: assertNever(it, "Unhandled intent type");
|
|
1778
2652
|
}
|
|
@@ -1814,15 +2688,199 @@ function jsonPatchToCrdtSafe(baseOrOptions, head, patch, newDot, evalTestAgainst
|
|
|
1814
2688
|
}
|
|
1815
2689
|
/** Alias for codebases that prefer `try*` naming for non-throwing APIs. */
|
|
1816
2690
|
const tryJsonPatchToCrdt = jsonPatchToCrdtSafe;
|
|
2691
|
+
function nodeToJsonForPatch(node) {
|
|
2692
|
+
return node.kind === "lww" ? node.value : materialize(node);
|
|
2693
|
+
}
|
|
2694
|
+
function rebaseDiffOps(path, nestedOps, out) {
|
|
2695
|
+
const prefix = stringifyJsonPointer(path);
|
|
2696
|
+
for (const op of nestedOps) {
|
|
2697
|
+
const rebasedPath = prefix === "" ? op.path : op.path === "" ? prefix : `${prefix}${op.path}`;
|
|
2698
|
+
if (op.op === "remove") {
|
|
2699
|
+
out.push({
|
|
2700
|
+
op: "remove",
|
|
2701
|
+
path: rebasedPath
|
|
2702
|
+
});
|
|
2703
|
+
continue;
|
|
2704
|
+
}
|
|
2705
|
+
if (op.op === "add" || op.op === "replace") {
|
|
2706
|
+
out.push({
|
|
2707
|
+
op: op.op,
|
|
2708
|
+
path: rebasedPath,
|
|
2709
|
+
value: op.value
|
|
2710
|
+
});
|
|
2711
|
+
continue;
|
|
2712
|
+
}
|
|
2713
|
+
throw new Error(`Unexpected op '${op.op}' from diffJsonPatch`);
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
function nodesJsonEqual(baseNode, headNode, depth) {
|
|
2717
|
+
assertTraversalDepth(depth);
|
|
2718
|
+
if (baseNode === headNode) return true;
|
|
2719
|
+
if (baseNode.kind !== headNode.kind) return false;
|
|
2720
|
+
if (baseNode.kind === "lww") {
|
|
2721
|
+
const headLww = headNode;
|
|
2722
|
+
return jsonEquals(baseNode.value, headLww.value);
|
|
2723
|
+
}
|
|
2724
|
+
if (baseNode.kind === "obj") {
|
|
2725
|
+
const headObj = headNode;
|
|
2726
|
+
if (baseNode.entries.size !== headObj.entries.size) return false;
|
|
2727
|
+
for (const [key, baseEntry] of baseNode.entries.entries()) {
|
|
2728
|
+
const headEntry = headObj.entries.get(key);
|
|
2729
|
+
if (!headEntry) return false;
|
|
2730
|
+
if (!nodesJsonEqual(baseEntry.node, headEntry.node, depth + 1)) return false;
|
|
2731
|
+
}
|
|
2732
|
+
return true;
|
|
2733
|
+
}
|
|
2734
|
+
const headSeq = headNode;
|
|
2735
|
+
const baseCursor = rgaCreateLinearCursor(baseNode);
|
|
2736
|
+
const headCursor = rgaCreateLinearCursor(headSeq);
|
|
2737
|
+
while (true) {
|
|
2738
|
+
const baseElem = baseCursor.next();
|
|
2739
|
+
const headElem = headCursor.next();
|
|
2740
|
+
if (baseElem === void 0 || headElem === void 0) return baseElem === void 0 && headElem === void 0;
|
|
2741
|
+
if (!nodesJsonEqual(baseElem.value, headElem.value, depth + 1)) return false;
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
function diffObjectNodes(path, baseNode, headNode, options, ops, depth) {
|
|
2745
|
+
assertTraversalDepth(depth);
|
|
2746
|
+
const baseKeys = [...baseNode.entries.keys()].sort();
|
|
2747
|
+
const headKeys = [...headNode.entries.keys()].sort();
|
|
2748
|
+
let baseIndex = 0;
|
|
2749
|
+
let headIndex = 0;
|
|
2750
|
+
while (baseIndex < baseKeys.length && headIndex < headKeys.length) {
|
|
2751
|
+
const baseKey = baseKeys[baseIndex];
|
|
2752
|
+
const headKey = headKeys[headIndex];
|
|
2753
|
+
if (baseKey === headKey) {
|
|
2754
|
+
baseIndex += 1;
|
|
2755
|
+
headIndex += 1;
|
|
2756
|
+
continue;
|
|
2757
|
+
}
|
|
2758
|
+
if (baseKey < headKey) {
|
|
2759
|
+
path.push(baseKey);
|
|
2760
|
+
ops.push({
|
|
2761
|
+
op: "remove",
|
|
2762
|
+
path: stringifyJsonPointer(path)
|
|
2763
|
+
});
|
|
2764
|
+
path.pop();
|
|
2765
|
+
baseIndex += 1;
|
|
2766
|
+
continue;
|
|
2767
|
+
}
|
|
2768
|
+
headIndex += 1;
|
|
2769
|
+
}
|
|
2770
|
+
while (baseIndex < baseKeys.length) {
|
|
2771
|
+
const baseKey = baseKeys[baseIndex];
|
|
2772
|
+
path.push(baseKey);
|
|
2773
|
+
ops.push({
|
|
2774
|
+
op: "remove",
|
|
2775
|
+
path: stringifyJsonPointer(path)
|
|
2776
|
+
});
|
|
2777
|
+
path.pop();
|
|
2778
|
+
baseIndex += 1;
|
|
2779
|
+
}
|
|
2780
|
+
baseIndex = 0;
|
|
2781
|
+
headIndex = 0;
|
|
2782
|
+
while (baseIndex < baseKeys.length && headIndex < headKeys.length) {
|
|
2783
|
+
const baseKey = baseKeys[baseIndex];
|
|
2784
|
+
const headKey = headKeys[headIndex];
|
|
2785
|
+
if (baseKey === headKey) {
|
|
2786
|
+
baseIndex += 1;
|
|
2787
|
+
headIndex += 1;
|
|
2788
|
+
continue;
|
|
2789
|
+
}
|
|
2790
|
+
if (baseKey < headKey) {
|
|
2791
|
+
baseIndex += 1;
|
|
2792
|
+
continue;
|
|
2793
|
+
}
|
|
2794
|
+
const headEntry = headNode.entries.get(headKey);
|
|
2795
|
+
path.push(headKey);
|
|
2796
|
+
ops.push({
|
|
2797
|
+
op: "add",
|
|
2798
|
+
path: stringifyJsonPointer(path),
|
|
2799
|
+
value: nodeToJsonForPatch(headEntry.node)
|
|
2800
|
+
});
|
|
2801
|
+
path.pop();
|
|
2802
|
+
headIndex += 1;
|
|
2803
|
+
}
|
|
2804
|
+
while (headIndex < headKeys.length) {
|
|
2805
|
+
const headKey = headKeys[headIndex];
|
|
2806
|
+
const headEntry = headNode.entries.get(headKey);
|
|
2807
|
+
path.push(headKey);
|
|
2808
|
+
ops.push({
|
|
2809
|
+
op: "add",
|
|
2810
|
+
path: stringifyJsonPointer(path),
|
|
2811
|
+
value: nodeToJsonForPatch(headEntry.node)
|
|
2812
|
+
});
|
|
2813
|
+
path.pop();
|
|
2814
|
+
headIndex += 1;
|
|
2815
|
+
}
|
|
2816
|
+
baseIndex = 0;
|
|
2817
|
+
headIndex = 0;
|
|
2818
|
+
while (baseIndex < baseKeys.length && headIndex < headKeys.length) {
|
|
2819
|
+
const baseKey = baseKeys[baseIndex];
|
|
2820
|
+
const headKey = headKeys[headIndex];
|
|
2821
|
+
if (baseKey === headKey) {
|
|
2822
|
+
const baseEntry = baseNode.entries.get(baseKey);
|
|
2823
|
+
const headEntry = headNode.entries.get(headKey);
|
|
2824
|
+
if (!nodesJsonEqual(baseEntry.node, headEntry.node, depth + 1)) {
|
|
2825
|
+
path.push(baseKey);
|
|
2826
|
+
diffNodeToPatch(path, baseEntry.node, headEntry.node, options, ops, depth + 1);
|
|
2827
|
+
path.pop();
|
|
2828
|
+
}
|
|
2829
|
+
baseIndex += 1;
|
|
2830
|
+
headIndex += 1;
|
|
2831
|
+
continue;
|
|
2832
|
+
}
|
|
2833
|
+
if (baseKey < headKey) {
|
|
2834
|
+
baseIndex += 1;
|
|
2835
|
+
continue;
|
|
2836
|
+
}
|
|
2837
|
+
headIndex += 1;
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
function diffNodeToPatch(path, baseNode, headNode, options, ops, depth) {
|
|
2841
|
+
assertTraversalDepth(depth);
|
|
2842
|
+
if (baseNode === headNode) return;
|
|
2843
|
+
if (baseNode.kind !== headNode.kind) {
|
|
2844
|
+
ops.push({
|
|
2845
|
+
op: "replace",
|
|
2846
|
+
path: stringifyJsonPointer(path),
|
|
2847
|
+
value: nodeToJsonForPatch(headNode)
|
|
2848
|
+
});
|
|
2849
|
+
return;
|
|
2850
|
+
}
|
|
2851
|
+
if (baseNode.kind === "lww") {
|
|
2852
|
+
const headLww = headNode;
|
|
2853
|
+
if (jsonEquals(baseNode.value, headLww.value)) return;
|
|
2854
|
+
ops.push({
|
|
2855
|
+
op: "replace",
|
|
2856
|
+
path: stringifyJsonPointer(path),
|
|
2857
|
+
value: headLww.value
|
|
2858
|
+
});
|
|
2859
|
+
return;
|
|
2860
|
+
}
|
|
2861
|
+
if (baseNode.kind === "obj") {
|
|
2862
|
+
diffObjectNodes(path, baseNode, headNode, options, ops, depth);
|
|
2863
|
+
return;
|
|
2864
|
+
}
|
|
2865
|
+
const headSeq = headNode;
|
|
2866
|
+
rebaseDiffOps(path, diffJsonPatch(materialize(baseNode), materialize(headSeq), options), ops);
|
|
2867
|
+
}
|
|
1817
2868
|
/**
|
|
1818
2869
|
* Generate a JSON Patch delta between two CRDT documents.
|
|
1819
2870
|
* @param base - The base document snapshot.
|
|
1820
2871
|
* @param head - The current document state.
|
|
1821
|
-
* @param options - Diff options (e.g. `{ arrayStrategy: "lcs" }`).
|
|
2872
|
+
* @param options - Diff options (e.g. `{ arrayStrategy: "lcs" }` or `{ arrayStrategy: "lcs-linear" }`).
|
|
1822
2873
|
* @returns An array of JSON Patch operations that transform base into head.
|
|
1823
2874
|
*/
|
|
1824
2875
|
function crdtToJsonPatch(base, head, options) {
|
|
1825
|
-
return diffJsonPatch(materialize(base.root), materialize(head.root), options);
|
|
2876
|
+
if ((options?.jsonValidation ?? "none") !== "none") return diffJsonPatch(materialize(base.root), materialize(head.root), options);
|
|
2877
|
+
return crdtNodesToJsonPatch(base.root, head.root, options);
|
|
2878
|
+
}
|
|
2879
|
+
/** Internals-only helper for diffing CRDT nodes from an existing traversal depth. */
|
|
2880
|
+
function crdtNodesToJsonPatch(baseNode, headNode, options, depth = 0) {
|
|
2881
|
+
const ops = [];
|
|
2882
|
+
diffNodeToPatch([], baseNode, headNode, options ?? {}, ops, depth);
|
|
2883
|
+
return ops;
|
|
1826
2884
|
}
|
|
1827
2885
|
/**
|
|
1828
2886
|
* Emit a single root `replace` patch representing the full document state.
|
|
@@ -2096,7 +3154,7 @@ function applyPatchAsActor(doc, vv, actor, patch, options = {}) {
|
|
|
2096
3154
|
}
|
|
2097
3155
|
/** Non-throwing `applyPatchAsActor` variant for internals sync flows. */
|
|
2098
3156
|
function tryApplyPatchAsActor(doc, vv, actor, patch, options = {}) {
|
|
2099
|
-
const observedCtr =
|
|
3157
|
+
const observedCtr = observedVersionVector(doc)[actor] ?? 0;
|
|
2100
3158
|
const applied = tryApplyPatch({
|
|
2101
3159
|
doc,
|
|
2102
3160
|
clock: createClock(actor, Math.max(vv[actor] ?? 0, observedCtr))
|
|
@@ -2124,166 +3182,387 @@ function toApplyPatchOptionsForActor(options) {
|
|
|
2124
3182
|
} : void 0
|
|
2125
3183
|
};
|
|
2126
3184
|
}
|
|
2127
|
-
function applyPatchInternal(state, patch, options, execution) {
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
3185
|
+
function applyPatchInternal(state, patch, options, execution) {
|
|
3186
|
+
const preparedPatch = preparePatchPayloadsSafe(patch, options.jsonValidation ?? "none");
|
|
3187
|
+
if (!preparedPatch.ok) return preparedPatch;
|
|
3188
|
+
const runtimePatch = preparedPatch.patch;
|
|
3189
|
+
if ((options.semantics ?? "sequential") === "sequential") {
|
|
3190
|
+
if (!options.base && execution === "batch") {
|
|
3191
|
+
const compiled = compilePreparedIntents(materialize(state.doc.root), runtimePatch, "sequential");
|
|
3192
|
+
if (!compiled.ok) return compiled;
|
|
3193
|
+
return applyIntentsToCrdt(state.doc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
|
|
3194
|
+
}
|
|
3195
|
+
const explicitBaseState = options.base ? {
|
|
3196
|
+
doc: cloneDoc(options.base.doc),
|
|
3197
|
+
clock: createClock("__base__", 0)
|
|
3198
|
+
} : null;
|
|
3199
|
+
const session = { pointerCache: /* @__PURE__ */ new Map() };
|
|
3200
|
+
for (const [opIndex, op] of runtimePatch.entries()) {
|
|
3201
|
+
const step = applyPatchOpSequential(state, op, options, explicitBaseState ? explicitBaseState.doc : state.doc, explicitBaseState, opIndex, session);
|
|
3202
|
+
if (!step.ok) return step;
|
|
3203
|
+
}
|
|
3204
|
+
return { ok: true };
|
|
3205
|
+
}
|
|
3206
|
+
const baseDoc = options.base ? options.base.doc : cloneDoc(state.doc);
|
|
3207
|
+
const compiled = compilePreparedIntents(materialize(baseDoc.root), runtimePatch, "base");
|
|
3208
|
+
if (!compiled.ok) return compiled;
|
|
3209
|
+
return applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
|
|
3210
|
+
}
|
|
3211
|
+
function applyPatchOpSequential(state, op, options, baseDoc, explicitBaseState, opIndex, session) {
|
|
3212
|
+
if (op.op === "move") {
|
|
3213
|
+
const fromResolved = resolveValueAtPointerInDoc(baseDoc, op.from, opIndex, session.pointerCache);
|
|
3214
|
+
if (!fromResolved.ok) return fromResolved;
|
|
3215
|
+
const fromValue = structuredClone(fromResolved.value);
|
|
3216
|
+
const removeRes = applySinglePatchOpSequentialStep(state, baseDoc, {
|
|
3217
|
+
op: "remove",
|
|
3218
|
+
path: op.from
|
|
3219
|
+
}, options, explicitBaseState, opIndex, session);
|
|
3220
|
+
if (!removeRes.ok) return removeRes;
|
|
3221
|
+
const addOp = {
|
|
3222
|
+
op: "add",
|
|
3223
|
+
path: op.path,
|
|
3224
|
+
value: fromValue
|
|
3225
|
+
};
|
|
3226
|
+
if (!explicitBaseState) return applySinglePatchOpSequentialStep(state, state.doc, addOp, options, null, opIndex, session);
|
|
3227
|
+
const headAddRes = applySinglePatchOpSequentialStep(state, state.doc, addOp, options, null, opIndex, session);
|
|
3228
|
+
if (!headAddRes.ok) return headAddRes;
|
|
3229
|
+
const shadowAddRes = applySinglePatchOpExplicitShadowStep(explicitBaseState, addOp, options, opIndex, session);
|
|
3230
|
+
if (!shadowAddRes.ok) return shadowAddRes;
|
|
3231
|
+
return { ok: true };
|
|
3232
|
+
}
|
|
3233
|
+
if (op.op === "copy") {
|
|
3234
|
+
const fromResolved = resolveValueAtPointerInDoc(baseDoc, op.from, opIndex, session.pointerCache);
|
|
3235
|
+
if (!fromResolved.ok) return fromResolved;
|
|
3236
|
+
return applySinglePatchOpSequentialStep(state, baseDoc, {
|
|
3237
|
+
op: "add",
|
|
3238
|
+
path: op.path,
|
|
3239
|
+
value: structuredClone(fromResolved.value)
|
|
3240
|
+
}, options, explicitBaseState, opIndex, session);
|
|
3241
|
+
}
|
|
3242
|
+
return applySinglePatchOpSequentialStep(state, baseDoc, op, options, explicitBaseState, opIndex, session);
|
|
3243
|
+
}
|
|
3244
|
+
function applySinglePatchOpSequentialStep(state, baseDoc, op, options, explicitBaseState, opIndex, session) {
|
|
3245
|
+
const compiled = compilePreparedSingleIntentFromDoc(baseDoc, op, session.pointerCache, opIndex);
|
|
3246
|
+
if (!compiled.ok) return compiled;
|
|
3247
|
+
const headStep = applyIntentsToCrdt(baseDoc, state.doc, compiled.intents, () => state.clock.next(), options.testAgainst ?? "head", (ctr) => bumpClockCounter(state, ctr), { strictParents: options.strictParents });
|
|
3248
|
+
if (!headStep.ok) return headStep;
|
|
3249
|
+
if (explicitBaseState && op.op !== "test") {
|
|
3250
|
+
const shadowStep = applyIntentsToCrdt(explicitBaseState.doc, explicitBaseState.doc, compiled.intents, () => explicitBaseState.clock.next(), "base", (ctr) => bumpClockCounter(explicitBaseState, ctr), { strictParents: options.strictParents });
|
|
3251
|
+
if (!shadowStep.ok) return shadowStep;
|
|
3252
|
+
}
|
|
3253
|
+
return { ok: true };
|
|
3254
|
+
}
|
|
3255
|
+
function applySinglePatchOpExplicitShadowStep(explicitBaseState, op, options, opIndex, session) {
|
|
3256
|
+
const compiled = compilePreparedSingleIntentFromDoc(explicitBaseState.doc, op, session.pointerCache, opIndex);
|
|
3257
|
+
if (!compiled.ok) return compiled;
|
|
3258
|
+
const shadowStep = applyIntentsToCrdt(explicitBaseState.doc, explicitBaseState.doc, compiled.intents, () => explicitBaseState.clock.next(), "base", (ctr) => bumpClockCounter(explicitBaseState, ctr), { strictParents: options.strictParents });
|
|
3259
|
+
if (!shadowStep.ok) return shadowStep;
|
|
3260
|
+
return { ok: true };
|
|
3261
|
+
}
|
|
3262
|
+
function resolveValueAtPointerInDoc(doc, pointer, opIndex, pointerCache) {
|
|
3263
|
+
let path;
|
|
3264
|
+
try {
|
|
3265
|
+
path = parsePointerWithCache(pointer, pointerCache);
|
|
3266
|
+
} catch (error) {
|
|
3267
|
+
return toPointerParseApplyError(error, pointer, opIndex);
|
|
3268
|
+
}
|
|
3269
|
+
const resolved = resolveNodeAtPath(doc.root, path);
|
|
3270
|
+
if (!resolved.ok) return {
|
|
3271
|
+
ok: false,
|
|
3272
|
+
...resolved.error,
|
|
3273
|
+
path: pointer,
|
|
3274
|
+
opIndex
|
|
3275
|
+
};
|
|
3276
|
+
return {
|
|
3277
|
+
ok: true,
|
|
3278
|
+
value: materialize(resolved.node)
|
|
3279
|
+
};
|
|
3280
|
+
}
|
|
3281
|
+
function compilePreparedSingleIntentFromDoc(baseDoc, op, pointerCache, opIndex) {
|
|
3282
|
+
let path;
|
|
3283
|
+
try {
|
|
3284
|
+
path = parsePointerWithCache(op.path, pointerCache);
|
|
3285
|
+
} catch (error) {
|
|
3286
|
+
return toPointerParseApplyError(error, op.path, opIndex);
|
|
3287
|
+
}
|
|
3288
|
+
if (op.op === "test") return {
|
|
3289
|
+
ok: true,
|
|
3290
|
+
intents: [{
|
|
3291
|
+
t: "Test",
|
|
3292
|
+
path,
|
|
3293
|
+
value: op.value
|
|
3294
|
+
}]
|
|
3295
|
+
};
|
|
3296
|
+
if (path.length === 0) {
|
|
3297
|
+
if (op.op === "remove") return {
|
|
3298
|
+
ok: false,
|
|
3299
|
+
code: 409,
|
|
3300
|
+
reason: "INVALID_TARGET",
|
|
3301
|
+
message: "remove at root path is not supported in RFC-compliant mode",
|
|
3302
|
+
path: op.path,
|
|
3303
|
+
opIndex
|
|
3304
|
+
};
|
|
3305
|
+
return {
|
|
3306
|
+
ok: true,
|
|
3307
|
+
intents: [{
|
|
3308
|
+
t: "ObjSet",
|
|
3309
|
+
path: [],
|
|
3310
|
+
key: ROOT_KEY,
|
|
3311
|
+
value: op.value
|
|
3312
|
+
}]
|
|
3313
|
+
};
|
|
3314
|
+
}
|
|
3315
|
+
const parentPath = path.slice(0, -1);
|
|
3316
|
+
const parentPointer = stringifyJsonPointer(parentPath);
|
|
3317
|
+
const key = path[path.length - 1];
|
|
3318
|
+
const resolvedParent = parentPath.length === 0 ? {
|
|
3319
|
+
ok: true,
|
|
3320
|
+
node: baseDoc.root
|
|
3321
|
+
} : resolveNodeAtPath(baseDoc.root, parentPath);
|
|
3322
|
+
if (!resolvedParent.ok) return {
|
|
3323
|
+
ok: false,
|
|
3324
|
+
...resolvedParent.error,
|
|
3325
|
+
path: parentPointer,
|
|
3326
|
+
opIndex
|
|
3327
|
+
};
|
|
3328
|
+
const parentNode = resolvedParent.node;
|
|
3329
|
+
if (parentNode.kind === "seq") {
|
|
3330
|
+
const parsedIndex = parseArrayIndexTokenForDoc(key, op.op, op.path, opIndex);
|
|
3331
|
+
if (!parsedIndex.ok) return parsedIndex;
|
|
3332
|
+
const boundedIndex = validateArrayIndexBounds(parsedIndex.index, op.op, rgaLength(parentNode), op.path, opIndex);
|
|
3333
|
+
if (!boundedIndex.ok) return boundedIndex;
|
|
3334
|
+
if (op.op === "add") return {
|
|
3335
|
+
ok: true,
|
|
3336
|
+
intents: [{
|
|
3337
|
+
t: "ArrInsert",
|
|
3338
|
+
path: parentPath,
|
|
3339
|
+
index: boundedIndex.index,
|
|
3340
|
+
value: op.value
|
|
3341
|
+
}]
|
|
3342
|
+
};
|
|
3343
|
+
if (op.op === "remove") return {
|
|
3344
|
+
ok: true,
|
|
3345
|
+
intents: [{
|
|
3346
|
+
t: "ArrDelete",
|
|
3347
|
+
path: parentPath,
|
|
3348
|
+
index: boundedIndex.index
|
|
3349
|
+
}]
|
|
3350
|
+
};
|
|
3351
|
+
return {
|
|
3352
|
+
ok: true,
|
|
3353
|
+
intents: [{
|
|
3354
|
+
t: "ArrReplace",
|
|
3355
|
+
path: parentPath,
|
|
3356
|
+
index: boundedIndex.index,
|
|
3357
|
+
value: op.value
|
|
3358
|
+
}]
|
|
3359
|
+
};
|
|
3360
|
+
}
|
|
3361
|
+
if (parentNode.kind !== "obj") return {
|
|
3362
|
+
ok: false,
|
|
3363
|
+
code: 409,
|
|
3364
|
+
reason: "INVALID_TARGET",
|
|
3365
|
+
message: `expected object or array parent at ${parentPointer}`,
|
|
3366
|
+
path: parentPointer,
|
|
3367
|
+
opIndex
|
|
3368
|
+
};
|
|
3369
|
+
if (key === "__proto__") return {
|
|
3370
|
+
ok: false,
|
|
3371
|
+
code: 409,
|
|
3372
|
+
reason: "INVALID_POINTER",
|
|
3373
|
+
message: `unsafe object key at ${op.path}`,
|
|
3374
|
+
path: op.path,
|
|
3375
|
+
opIndex
|
|
3376
|
+
};
|
|
3377
|
+
const entry = parentNode.entries.get(key);
|
|
3378
|
+
if ((op.op === "replace" || op.op === "remove") && !entry) return {
|
|
3379
|
+
ok: false,
|
|
3380
|
+
code: 409,
|
|
3381
|
+
reason: "MISSING_TARGET",
|
|
3382
|
+
message: `missing key ${key} at ${parentPointer}`,
|
|
3383
|
+
path: op.path,
|
|
3384
|
+
opIndex
|
|
3385
|
+
};
|
|
3386
|
+
if (op.op === "remove") return {
|
|
3387
|
+
ok: true,
|
|
3388
|
+
intents: [{
|
|
3389
|
+
t: "ObjRemove",
|
|
3390
|
+
path: parentPath,
|
|
3391
|
+
key
|
|
3392
|
+
}]
|
|
3393
|
+
};
|
|
3394
|
+
return {
|
|
3395
|
+
ok: true,
|
|
3396
|
+
intents: [{
|
|
3397
|
+
t: "ObjSet",
|
|
3398
|
+
path: parentPath,
|
|
3399
|
+
key,
|
|
3400
|
+
value: op.value,
|
|
3401
|
+
mode: op.op
|
|
3402
|
+
}]
|
|
3403
|
+
};
|
|
3404
|
+
}
|
|
3405
|
+
function parsePointerWithCache(pointer, pointerCache) {
|
|
3406
|
+
const cachedPath = pointerCache.get(pointer);
|
|
3407
|
+
if (cachedPath !== void 0) return cachedPath.slice();
|
|
3408
|
+
const parsedPath = parseJsonPointer(pointer);
|
|
3409
|
+
pointerCache.set(pointer, parsedPath);
|
|
3410
|
+
return parsedPath.slice();
|
|
3411
|
+
}
|
|
3412
|
+
function resolveNodeAtPath(root, path) {
|
|
3413
|
+
let current = root;
|
|
3414
|
+
for (const segment of path) {
|
|
3415
|
+
if (current.kind === "obj") {
|
|
3416
|
+
const entry = current.entries.get(segment);
|
|
3417
|
+
if (!entry) return {
|
|
3418
|
+
ok: false,
|
|
3419
|
+
error: {
|
|
3420
|
+
code: 409,
|
|
3421
|
+
reason: "MISSING_PARENT",
|
|
3422
|
+
message: `Missing key '${segment}'`
|
|
3423
|
+
}
|
|
3424
|
+
};
|
|
3425
|
+
current = entry.node;
|
|
3426
|
+
continue;
|
|
2133
3427
|
}
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
3428
|
+
if (current.kind === "seq") {
|
|
3429
|
+
if (!ARRAY_INDEX_TOKEN_PATTERN.test(segment)) return {
|
|
3430
|
+
ok: false,
|
|
3431
|
+
error: {
|
|
3432
|
+
code: 409,
|
|
3433
|
+
reason: "INVALID_POINTER",
|
|
3434
|
+
message: `Expected array index, got '${segment}'`
|
|
3435
|
+
}
|
|
3436
|
+
};
|
|
3437
|
+
const index = Number(segment);
|
|
3438
|
+
if (!Number.isSafeInteger(index)) return {
|
|
3439
|
+
ok: false,
|
|
3440
|
+
error: {
|
|
3441
|
+
code: 409,
|
|
3442
|
+
reason: "OUT_OF_BOUNDS",
|
|
3443
|
+
message: `Index out of bounds at '${segment}'`
|
|
3444
|
+
}
|
|
3445
|
+
};
|
|
3446
|
+
const elemId = rgaIdAtIndex(current, index);
|
|
3447
|
+
if (elemId === void 0) return {
|
|
3448
|
+
ok: false,
|
|
3449
|
+
error: {
|
|
3450
|
+
code: 409,
|
|
3451
|
+
reason: "OUT_OF_BOUNDS",
|
|
3452
|
+
message: `Index out of bounds at '${segment}'`
|
|
3453
|
+
}
|
|
3454
|
+
};
|
|
3455
|
+
current = current.elems.get(elemId).value;
|
|
3456
|
+
continue;
|
|
2145
3457
|
}
|
|
2146
|
-
return {
|
|
3458
|
+
return {
|
|
3459
|
+
ok: false,
|
|
3460
|
+
error: {
|
|
3461
|
+
code: 409,
|
|
3462
|
+
reason: "INVALID_TARGET",
|
|
3463
|
+
message: `Cannot traverse into non-container at '${segment}'`
|
|
3464
|
+
}
|
|
3465
|
+
};
|
|
2147
3466
|
}
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
3467
|
+
return {
|
|
3468
|
+
ok: true,
|
|
3469
|
+
node: current
|
|
3470
|
+
};
|
|
2152
3471
|
}
|
|
2153
|
-
function
|
|
2154
|
-
if (
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
path
|
|
2161
|
-
|
|
2162
|
-
if (!removeRes.ok) return removeRes;
|
|
2163
|
-
const addOp = {
|
|
2164
|
-
op: "add",
|
|
2165
|
-
path: op.path,
|
|
2166
|
-
value: fromValue
|
|
3472
|
+
function parseArrayIndexTokenForDoc(token, op, path, opIndex) {
|
|
3473
|
+
if (token === "-") {
|
|
3474
|
+
if (op !== "add") return {
|
|
3475
|
+
ok: false,
|
|
3476
|
+
code: 409,
|
|
3477
|
+
reason: "INVALID_POINTER",
|
|
3478
|
+
message: `'-' index is only valid for add at ${path}`,
|
|
3479
|
+
path,
|
|
3480
|
+
opIndex
|
|
2167
3481
|
};
|
|
2168
|
-
if (!explicitBaseState) return applySinglePatchOpSequentialStep(state, state.doc, removeRes.baseJson, removeRes.headJson, addOp, options, null);
|
|
2169
|
-
const headAddRes = applySinglePatchOpSequentialStep(state, state.doc, removeRes.headJson, removeRes.headJson, addOp, options, null);
|
|
2170
|
-
if (!headAddRes.ok) return headAddRes;
|
|
2171
|
-
const shadowAddRes = applySinglePatchOpExplicitShadowStep(explicitBaseState, removeRes.baseJson, addOp, options);
|
|
2172
|
-
if (!shadowAddRes.ok) return shadowAddRes;
|
|
2173
3482
|
return {
|
|
2174
3483
|
ok: true,
|
|
2175
|
-
|
|
2176
|
-
headJson: headAddRes.headJson
|
|
3484
|
+
index: Number.POSITIVE_INFINITY
|
|
2177
3485
|
};
|
|
2178
3486
|
}
|
|
2179
|
-
if (
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
if (explicitBaseState && op.op !== "test") {
|
|
2196
|
-
const shadowStep = applyIntentsToCrdt(explicitBaseState.doc, explicitBaseState.doc, compiled.intents, () => explicitBaseState.clock.next(), "base", (ctr) => bumpClockCounter(explicitBaseState, ctr), { strictParents: options.strictParents });
|
|
2197
|
-
if (!shadowStep.ok) return shadowStep;
|
|
2198
|
-
}
|
|
2199
|
-
if (op.op === "test") return {
|
|
2200
|
-
ok: true,
|
|
2201
|
-
baseJson,
|
|
2202
|
-
headJson
|
|
3487
|
+
if (!ARRAY_INDEX_TOKEN_PATTERN.test(token)) return {
|
|
3488
|
+
ok: false,
|
|
3489
|
+
code: 409,
|
|
3490
|
+
reason: "INVALID_POINTER",
|
|
3491
|
+
message: `expected array index at ${path}`,
|
|
3492
|
+
path,
|
|
3493
|
+
opIndex
|
|
3494
|
+
};
|
|
3495
|
+
const index = Number(token);
|
|
3496
|
+
if (!Number.isSafeInteger(index)) return {
|
|
3497
|
+
ok: false,
|
|
3498
|
+
code: 409,
|
|
3499
|
+
reason: "OUT_OF_BOUNDS",
|
|
3500
|
+
message: `array index is too large at ${path}`,
|
|
3501
|
+
path,
|
|
3502
|
+
opIndex
|
|
2203
3503
|
};
|
|
2204
|
-
const nextBaseJson = applyJsonPatchOpToShadow(baseJson, op);
|
|
2205
3504
|
return {
|
|
2206
3505
|
ok: true,
|
|
2207
|
-
|
|
2208
|
-
headJson: explicitBaseState ? applyJsonPatchOpToShadow(headJson, op) : nextBaseJson
|
|
3506
|
+
index
|
|
2209
3507
|
};
|
|
2210
3508
|
}
|
|
2211
|
-
function
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
3509
|
+
function validateArrayIndexBounds(index, op, arrLength, path, opIndex) {
|
|
3510
|
+
if (op === "add") {
|
|
3511
|
+
if (index === Number.POSITIVE_INFINITY) return {
|
|
3512
|
+
ok: true,
|
|
3513
|
+
index
|
|
3514
|
+
};
|
|
3515
|
+
if (index > arrLength) return {
|
|
3516
|
+
ok: false,
|
|
3517
|
+
code: 409,
|
|
3518
|
+
reason: "OUT_OF_BOUNDS",
|
|
3519
|
+
message: `index out of bounds at ${path}; expected 0..${arrLength}`,
|
|
3520
|
+
path,
|
|
3521
|
+
opIndex
|
|
3522
|
+
};
|
|
3523
|
+
} else if (index >= arrLength) return {
|
|
3524
|
+
ok: false,
|
|
3525
|
+
code: 409,
|
|
3526
|
+
reason: "OUT_OF_BOUNDS",
|
|
3527
|
+
message: `index out of bounds at ${path}; expected 0..${Math.max(arrLength - 1, 0)}`,
|
|
3528
|
+
path,
|
|
3529
|
+
opIndex
|
|
2219
3530
|
};
|
|
2220
3531
|
return {
|
|
2221
3532
|
ok: true,
|
|
2222
|
-
|
|
3533
|
+
index
|
|
2223
3534
|
};
|
|
2224
3535
|
}
|
|
2225
|
-
function
|
|
2226
|
-
|
|
2227
|
-
if (path.length === 0) {
|
|
2228
|
-
if (op.op === "test") return baseJson;
|
|
2229
|
-
if (op.op === "remove") return null;
|
|
2230
|
-
return structuredClone(op.value);
|
|
2231
|
-
}
|
|
2232
|
-
const parentPath = path.slice(0, -1);
|
|
2233
|
-
const key = path[path.length - 1];
|
|
2234
|
-
const parent = getAtJson(baseJson, parentPath);
|
|
2235
|
-
if (Array.isArray(parent)) {
|
|
2236
|
-
const idx = key === "-" ? parent.length : Number(key);
|
|
2237
|
-
if (!Number.isInteger(idx)) throw new Error(`Invalid array index ${key}`);
|
|
2238
|
-
if (op.op === "add") {
|
|
2239
|
-
parent.splice(idx, 0, structuredClone(op.value));
|
|
2240
|
-
return baseJson;
|
|
2241
|
-
}
|
|
2242
|
-
if (op.op === "remove") {
|
|
2243
|
-
parent.splice(idx, 1);
|
|
2244
|
-
return baseJson;
|
|
2245
|
-
}
|
|
2246
|
-
if (op.op === "replace") {
|
|
2247
|
-
parent[idx] = structuredClone(op.value);
|
|
2248
|
-
return baseJson;
|
|
2249
|
-
}
|
|
2250
|
-
return baseJson;
|
|
2251
|
-
}
|
|
2252
|
-
const obj = parent;
|
|
2253
|
-
if (op.op === "add" || op.op === "replace") {
|
|
2254
|
-
obj[key] = structuredClone(op.value);
|
|
2255
|
-
return baseJson;
|
|
2256
|
-
}
|
|
2257
|
-
if (op.op === "remove") {
|
|
2258
|
-
delete obj[key];
|
|
2259
|
-
return baseJson;
|
|
2260
|
-
}
|
|
2261
|
-
return baseJson;
|
|
3536
|
+
function bumpClockCounter(state, ctr) {
|
|
3537
|
+
if (state.clock.ctr < ctr) state.clock.ctr = ctr;
|
|
2262
3538
|
}
|
|
2263
|
-
function
|
|
2264
|
-
let path;
|
|
2265
|
-
try {
|
|
2266
|
-
path = parseJsonPointer(pointer);
|
|
2267
|
-
} catch (error) {
|
|
2268
|
-
return toPointerParseApplyError(error, pointer, opIndex);
|
|
2269
|
-
}
|
|
3539
|
+
function compilePreparedIntents(baseJson, patch, semantics = "sequential", pointerCache, opIndexOffset = 0) {
|
|
2270
3540
|
try {
|
|
3541
|
+
const compileOptions = toCompilePatchOptions(semantics, pointerCache, opIndexOffset);
|
|
3542
|
+
if (patch.length === 1) return {
|
|
3543
|
+
ok: true,
|
|
3544
|
+
intents: compileJsonPatchOpToIntent(baseJson, patch[0], compileOptions)
|
|
3545
|
+
};
|
|
2271
3546
|
return {
|
|
2272
3547
|
ok: true,
|
|
2273
|
-
|
|
3548
|
+
intents: compileJsonPatchToIntent(baseJson, patch, compileOptions)
|
|
2274
3549
|
};
|
|
2275
3550
|
} catch (error) {
|
|
2276
|
-
return
|
|
3551
|
+
return toApplyError(error);
|
|
2277
3552
|
}
|
|
2278
3553
|
}
|
|
2279
|
-
function
|
|
2280
|
-
|
|
3554
|
+
function toCompilePatchOptions(semantics, pointerCache, opIndexOffset = 0) {
|
|
3555
|
+
return {
|
|
3556
|
+
semantics,
|
|
3557
|
+
pointerCache,
|
|
3558
|
+
opIndexOffset
|
|
3559
|
+
};
|
|
2281
3560
|
}
|
|
2282
|
-
function
|
|
3561
|
+
function preparePatchPayloadsSafe(patch, mode) {
|
|
2283
3562
|
try {
|
|
2284
3563
|
return {
|
|
2285
3564
|
ok: true,
|
|
2286
|
-
|
|
3565
|
+
patch: preparePatchPayloads(patch, mode)
|
|
2287
3566
|
};
|
|
2288
3567
|
} catch (error) {
|
|
2289
3568
|
return toApplyError(error);
|
|
@@ -2323,40 +3602,6 @@ function mergePointerPaths(basePointer, nestedPointer) {
|
|
|
2323
3602
|
if (basePointer === "") return nestedPointer;
|
|
2324
3603
|
return `${basePointer}${nestedPointer}`;
|
|
2325
3604
|
}
|
|
2326
|
-
function maxCtrInNodeForActor$1(node, actor) {
|
|
2327
|
-
let best = 0;
|
|
2328
|
-
const stack = [{
|
|
2329
|
-
node,
|
|
2330
|
-
depth: 0
|
|
2331
|
-
}];
|
|
2332
|
-
while (stack.length > 0) {
|
|
2333
|
-
const frame = stack.pop();
|
|
2334
|
-
assertTraversalDepth(frame.depth);
|
|
2335
|
-
if (frame.node.kind === "lww") {
|
|
2336
|
-
if (frame.node.dot.actor === actor && frame.node.dot.ctr > best) best = frame.node.dot.ctr;
|
|
2337
|
-
continue;
|
|
2338
|
-
}
|
|
2339
|
-
if (frame.node.kind === "obj") {
|
|
2340
|
-
for (const entry of frame.node.entries.values()) {
|
|
2341
|
-
if (entry.dot.actor === actor && entry.dot.ctr > best) best = entry.dot.ctr;
|
|
2342
|
-
stack.push({
|
|
2343
|
-
node: entry.node,
|
|
2344
|
-
depth: frame.depth + 1
|
|
2345
|
-
});
|
|
2346
|
-
}
|
|
2347
|
-
for (const tomb of frame.node.tombstone.values()) if (tomb.actor === actor && tomb.ctr > best) best = tomb.ctr;
|
|
2348
|
-
continue;
|
|
2349
|
-
}
|
|
2350
|
-
for (const elem of frame.node.elems.values()) {
|
|
2351
|
-
if (elem.insDot.actor === actor && elem.insDot.ctr > best) best = elem.insDot.ctr;
|
|
2352
|
-
stack.push({
|
|
2353
|
-
node: elem.value,
|
|
2354
|
-
depth: frame.depth + 1
|
|
2355
|
-
});
|
|
2356
|
-
}
|
|
2357
|
-
}
|
|
2358
|
-
return best;
|
|
2359
|
-
}
|
|
2360
3605
|
function toApplyError(error) {
|
|
2361
3606
|
if (error instanceof TraversalDepthError) return toDepthApplyError(error);
|
|
2362
3607
|
if (error instanceof PatchCompileError) return {
|
|
@@ -2384,21 +3629,12 @@ function toPointerParseApplyError(error, pointer, opIndex) {
|
|
|
2384
3629
|
opIndex
|
|
2385
3630
|
};
|
|
2386
3631
|
}
|
|
2387
|
-
function toPointerLookupApplyError(error, pointer, opIndex) {
|
|
2388
|
-
const mapped = mapLookupErrorToPatchReason(error);
|
|
2389
|
-
return {
|
|
2390
|
-
ok: false,
|
|
2391
|
-
code: 409,
|
|
2392
|
-
reason: mapped.reason,
|
|
2393
|
-
message: mapped.message,
|
|
2394
|
-
path: pointer,
|
|
2395
|
-
opIndex
|
|
2396
|
-
};
|
|
2397
|
-
}
|
|
2398
3632
|
|
|
2399
3633
|
//#endregion
|
|
2400
3634
|
//#region src/serialize.ts
|
|
2401
3635
|
const HEAD_ELEM_ID = "HEAD";
|
|
3636
|
+
const SERIALIZED_DOC_VERSION = 1;
|
|
3637
|
+
const SERIALIZED_STATE_VERSION = 1;
|
|
2402
3638
|
function createSerializedRecord() {
|
|
2403
3639
|
return Object.create(null);
|
|
2404
3640
|
}
|
|
@@ -2423,13 +3659,16 @@ var DeserializeError = class extends Error {
|
|
|
2423
3659
|
};
|
|
2424
3660
|
/** Serialize a CRDT document to a JSON-safe representation (Maps become plain objects). */
|
|
2425
3661
|
function serializeDoc(doc) {
|
|
2426
|
-
return {
|
|
3662
|
+
return {
|
|
3663
|
+
version: SERIALIZED_DOC_VERSION,
|
|
3664
|
+
root: serializeNode(doc.root)
|
|
3665
|
+
};
|
|
2427
3666
|
}
|
|
2428
3667
|
/** Reconstruct a CRDT document from its serialized form. */
|
|
2429
3668
|
function deserializeDoc(data) {
|
|
2430
|
-
|
|
2431
|
-
if (!("root" in
|
|
2432
|
-
return { root: deserializeNode(
|
|
3669
|
+
const raw = readSerializedDocEnvelope(data);
|
|
3670
|
+
if (!("root" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/root", "serialized doc is missing root");
|
|
3671
|
+
return { root: deserializeNode(raw.root, "/root", 0) };
|
|
2433
3672
|
}
|
|
2434
3673
|
/** Non-throwing `deserializeDoc` variant with typed validation details. */
|
|
2435
3674
|
function tryDeserializeDoc(data) {
|
|
@@ -2450,6 +3689,7 @@ function tryDeserializeDoc(data) {
|
|
|
2450
3689
|
/** Serialize a full CRDT state (document + clock) to a JSON-safe representation. */
|
|
2451
3690
|
function serializeState(state) {
|
|
2452
3691
|
return {
|
|
3692
|
+
version: SERIALIZED_STATE_VERSION,
|
|
2453
3693
|
doc: serializeDoc(state.doc),
|
|
2454
3694
|
clock: {
|
|
2455
3695
|
actor: state.clock.actor,
|
|
@@ -2457,16 +3697,24 @@ function serializeState(state) {
|
|
|
2457
3697
|
}
|
|
2458
3698
|
};
|
|
2459
3699
|
}
|
|
2460
|
-
/**
|
|
3700
|
+
/**
|
|
3701
|
+
* Reconstruct a full CRDT state from its serialized form, restoring the clock.
|
|
3702
|
+
*
|
|
3703
|
+
* May throw `TraversalDepthError` when the payload exceeds the maximum
|
|
3704
|
+
* supported nesting depth.
|
|
3705
|
+
*/
|
|
2461
3706
|
function deserializeState(data) {
|
|
2462
|
-
|
|
2463
|
-
if (!("doc" in
|
|
2464
|
-
if (!("clock" in
|
|
2465
|
-
const clockRaw = asRecord(
|
|
2466
|
-
const
|
|
3707
|
+
const raw = readSerializedStateEnvelope(data);
|
|
3708
|
+
if (!("doc" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/doc", "serialized state is missing doc");
|
|
3709
|
+
if (!("clock" in raw)) fail("INVALID_SERIALIZED_SHAPE", "/clock", "serialized state is missing clock");
|
|
3710
|
+
const clockRaw = asRecord(raw.clock, "/clock");
|
|
3711
|
+
const actor = readActor(clockRaw.actor, "/clock/actor");
|
|
3712
|
+
const ctr = readCounter(clockRaw.ctr, "/clock/ctr");
|
|
3713
|
+
const doc = deserializeDoc(raw.doc);
|
|
3714
|
+
const observedCtr = observedVersionVector(doc)[actor] ?? 0;
|
|
2467
3715
|
return {
|
|
2468
|
-
doc
|
|
2469
|
-
clock
|
|
3716
|
+
doc,
|
|
3717
|
+
clock: createClock(actor, Math.max(ctr, observedCtr))
|
|
2470
3718
|
};
|
|
2471
3719
|
}
|
|
2472
3720
|
/** Non-throwing `deserializeState` variant with typed validation details. */
|
|
@@ -2515,21 +3763,38 @@ function serializeNode(node) {
|
|
|
2515
3763
|
};
|
|
2516
3764
|
}
|
|
2517
3765
|
const elems = createSerializedRecord();
|
|
2518
|
-
for (const [id, e] of node.elems.entries())
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
3766
|
+
for (const [id, e] of node.elems.entries()) {
|
|
3767
|
+
const serializedElem = {
|
|
3768
|
+
id: e.id,
|
|
3769
|
+
prev: e.prev,
|
|
3770
|
+
tombstone: e.tombstone,
|
|
3771
|
+
value: serializeNode(e.value),
|
|
3772
|
+
insDot: {
|
|
3773
|
+
actor: e.insDot.actor,
|
|
3774
|
+
ctr: e.insDot.ctr
|
|
3775
|
+
}
|
|
3776
|
+
};
|
|
3777
|
+
if (e.delDot) serializedElem.delDot = {
|
|
3778
|
+
actor: e.delDot.actor,
|
|
3779
|
+
ctr: e.delDot.ctr
|
|
3780
|
+
};
|
|
3781
|
+
setSerializedRecordValue(elems, id, serializedElem);
|
|
3782
|
+
}
|
|
2528
3783
|
return {
|
|
2529
3784
|
kind: "seq",
|
|
2530
3785
|
elems
|
|
2531
3786
|
};
|
|
2532
3787
|
}
|
|
3788
|
+
function readSerializedDocEnvelope(data) {
|
|
3789
|
+
const raw = asRecord(data, "/");
|
|
3790
|
+
assertSerializedEnvelopeVersion(raw, "/version", SERIALIZED_DOC_VERSION, "doc");
|
|
3791
|
+
return raw;
|
|
3792
|
+
}
|
|
3793
|
+
function readSerializedStateEnvelope(data) {
|
|
3794
|
+
const raw = asRecord(data, "/");
|
|
3795
|
+
assertSerializedEnvelopeVersion(raw, "/version", SERIALIZED_STATE_VERSION, "state");
|
|
3796
|
+
return raw;
|
|
3797
|
+
}
|
|
2533
3798
|
function deserializeNode(node, path, depth) {
|
|
2534
3799
|
assertTraversalDepth(depth);
|
|
2535
3800
|
const raw = asRecord(node, path);
|
|
@@ -2575,11 +3840,14 @@ function deserializeNode(node, path, depth) {
|
|
|
2575
3840
|
const tombstone = readBoolean(elem.tombstone, `${elemPath}/tombstone`);
|
|
2576
3841
|
const value = deserializeNode(elem.value, `${elemPath}/value`, depth + 1);
|
|
2577
3842
|
const insDot = readDot(elem.insDot, `${elemPath}/insDot`);
|
|
3843
|
+
const delDot = "delDot" in elem && elem.delDot !== void 0 ? readDot(elem.delDot, `${elemPath}/delDot`) : void 0;
|
|
2578
3844
|
if (dotToElemId(insDot) !== id) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/insDot`, "sequence element id must match its insertion dot");
|
|
3845
|
+
if (!tombstone && delDot) fail("INVALID_SERIALIZED_INVARIANT", `${elemPath}/delDot`, "live sequence elements must not include delete metadata");
|
|
2579
3846
|
elems.set(id, {
|
|
2580
3847
|
id,
|
|
2581
3848
|
prev,
|
|
2582
3849
|
tombstone,
|
|
3850
|
+
delDot,
|
|
2583
3851
|
value,
|
|
2584
3852
|
insDot
|
|
2585
3853
|
});
|
|
@@ -2618,6 +3886,15 @@ function asRecord(value, path) {
|
|
|
2618
3886
|
if (!isRecord(value)) fail("INVALID_SERIALIZED_SHAPE", path, "expected object");
|
|
2619
3887
|
return value;
|
|
2620
3888
|
}
|
|
3889
|
+
function assertSerializedEnvelopeVersion(raw, path, expectedVersion, label) {
|
|
3890
|
+
if (!("version" in raw)) return;
|
|
3891
|
+
const version = readVersion(raw.version, path);
|
|
3892
|
+
if (version !== expectedVersion) fail("INVALID_SERIALIZED_SHAPE", path, `unsupported serialized ${label} version '${version}'`);
|
|
3893
|
+
}
|
|
3894
|
+
function readVersion(value, path) {
|
|
3895
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) fail("INVALID_SERIALIZED_SHAPE", path, "envelope version must be a non-negative safe integer");
|
|
3896
|
+
return value;
|
|
3897
|
+
}
|
|
2621
3898
|
function readDot(value, path) {
|
|
2622
3899
|
const raw = asRecord(value, path);
|
|
2623
3900
|
return {
|
|
@@ -2716,7 +3993,7 @@ function mergeDoc(a, b, options = {}) {
|
|
|
2716
3993
|
function tryMergeDoc(a, b, options = {}) {
|
|
2717
3994
|
try {
|
|
2718
3995
|
const mismatchPath = options.requireSharedOrigin ?? true ? findSeqLineageMismatch(a.root, b.root, []) : null;
|
|
2719
|
-
if (mismatchPath) return {
|
|
3996
|
+
if (mismatchPath !== null) return {
|
|
2720
3997
|
ok: false,
|
|
2721
3998
|
error: {
|
|
2722
3999
|
ok: false,
|
|
@@ -2797,7 +4074,7 @@ function findSeqLineageMismatch(a, b, path) {
|
|
|
2797
4074
|
shared = true;
|
|
2798
4075
|
break;
|
|
2799
4076
|
}
|
|
2800
|
-
if (!shared) return
|
|
4077
|
+
if (!shared) return stringifyJsonPointer(frame.path);
|
|
2801
4078
|
}
|
|
2802
4079
|
}
|
|
2803
4080
|
if (frame.a.kind === "obj" && frame.b.kind === "obj") {
|
|
@@ -2820,45 +4097,11 @@ function findSeqLineageMismatch(a, b, path) {
|
|
|
2820
4097
|
return null;
|
|
2821
4098
|
}
|
|
2822
4099
|
function maxObservedCtrForActor(doc, actor, a, b) {
|
|
2823
|
-
let best =
|
|
4100
|
+
let best = observedVersionVector(doc)[actor] ?? 0;
|
|
2824
4101
|
if (a.clock.actor === actor && a.clock.ctr > best) best = a.clock.ctr;
|
|
2825
4102
|
if (b.clock.actor === actor && b.clock.ctr > best) best = b.clock.ctr;
|
|
2826
4103
|
return best;
|
|
2827
4104
|
}
|
|
2828
|
-
function maxCtrInNodeForActor(node, actor) {
|
|
2829
|
-
let best = 0;
|
|
2830
|
-
const stack = [{
|
|
2831
|
-
node,
|
|
2832
|
-
depth: 0
|
|
2833
|
-
}];
|
|
2834
|
-
while (stack.length > 0) {
|
|
2835
|
-
const frame = stack.pop();
|
|
2836
|
-
assertTraversalDepth(frame.depth);
|
|
2837
|
-
if (frame.node.kind === "lww") {
|
|
2838
|
-
if (frame.node.dot.actor === actor && frame.node.dot.ctr > best) best = frame.node.dot.ctr;
|
|
2839
|
-
continue;
|
|
2840
|
-
}
|
|
2841
|
-
if (frame.node.kind === "obj") {
|
|
2842
|
-
for (const entry of frame.node.entries.values()) {
|
|
2843
|
-
if (entry.dot.actor === actor && entry.dot.ctr > best) best = entry.dot.ctr;
|
|
2844
|
-
stack.push({
|
|
2845
|
-
node: entry.node,
|
|
2846
|
-
depth: frame.depth + 1
|
|
2847
|
-
});
|
|
2848
|
-
}
|
|
2849
|
-
for (const tomb of frame.node.tombstone.values()) if (tomb.actor === actor && tomb.ctr > best) best = tomb.ctr;
|
|
2850
|
-
continue;
|
|
2851
|
-
}
|
|
2852
|
-
for (const elem of frame.node.elems.values()) {
|
|
2853
|
-
if (elem.insDot.actor === actor && elem.insDot.ctr > best) best = elem.insDot.ctr;
|
|
2854
|
-
stack.push({
|
|
2855
|
-
node: elem.value,
|
|
2856
|
-
depth: frame.depth + 1
|
|
2857
|
-
});
|
|
2858
|
-
}
|
|
2859
|
-
}
|
|
2860
|
-
return best;
|
|
2861
|
-
}
|
|
2862
4105
|
function repDot(node) {
|
|
2863
4106
|
switch (node.kind) {
|
|
2864
4107
|
case "lww": return node.dot;
|
|
@@ -2951,13 +4194,14 @@ function mergeSeq(a, b, depth, path) {
|
|
|
2951
4194
|
const ea = a.elems.get(id);
|
|
2952
4195
|
const eb = b.elems.get(id);
|
|
2953
4196
|
if (ea && eb) {
|
|
2954
|
-
if (ea.prev !== eb.prev) throw new SharedElementMetadataMismatchError(
|
|
2955
|
-
if (!sameDot(ea.insDot, eb.insDot)) throw new SharedElementMetadataMismatchError(
|
|
4197
|
+
if (ea.prev !== eb.prev) throw new SharedElementMetadataMismatchError(stringifyJsonPointer(path), id, "prev");
|
|
4198
|
+
if (!sameDot(ea.insDot, eb.insDot)) throw new SharedElementMetadataMismatchError(stringifyJsonPointer(path), id, "insDot");
|
|
2956
4199
|
const mergedValue = mergeNodeAtDepth(ea.value, eb.value, depth + 1, [...path, id]);
|
|
2957
4200
|
elems.set(id, {
|
|
2958
4201
|
id,
|
|
2959
4202
|
prev: ea.prev,
|
|
2960
4203
|
tombstone: ea.tombstone || eb.tombstone,
|
|
4204
|
+
delDot: mergeDeleteDot(ea.delDot, eb.delDot),
|
|
2961
4205
|
value: mergedValue,
|
|
2962
4206
|
insDot: { ...ea.insDot }
|
|
2963
4207
|
});
|
|
@@ -2972,20 +4216,22 @@ function mergeSeq(a, b, depth, path) {
|
|
|
2972
4216
|
function sameDot(a, b) {
|
|
2973
4217
|
return a.actor === b.actor && a.ctr === b.ctr;
|
|
2974
4218
|
}
|
|
2975
|
-
function toPointer(path) {
|
|
2976
|
-
if (path.length === 0) return "/";
|
|
2977
|
-
return `/${path.join("/")}`;
|
|
2978
|
-
}
|
|
2979
4219
|
function cloneElem(e, depth) {
|
|
2980
4220
|
assertTraversalDepth(depth);
|
|
2981
4221
|
return {
|
|
2982
4222
|
id: e.id,
|
|
2983
4223
|
prev: e.prev,
|
|
2984
4224
|
tombstone: e.tombstone,
|
|
4225
|
+
delDot: e.delDot ? { ...e.delDot } : void 0,
|
|
2985
4226
|
value: cloneNodeShallow(e.value, depth + 1),
|
|
2986
4227
|
insDot: { ...e.insDot }
|
|
2987
4228
|
};
|
|
2988
4229
|
}
|
|
4230
|
+
function mergeDeleteDot(a, b) {
|
|
4231
|
+
if (a && b) return compareDot(a, b) >= 0 ? { ...a } : { ...b };
|
|
4232
|
+
if (a) return { ...a };
|
|
4233
|
+
if (b) return { ...b };
|
|
4234
|
+
}
|
|
2989
4235
|
function cloneNodeShallow(node, depth) {
|
|
2990
4236
|
assertTraversalDepth(depth);
|
|
2991
4237
|
switch (node.kind) {
|
|
@@ -3219,6 +4465,12 @@ Object.defineProperty(exports, 'compileJsonPatchToIntent', {
|
|
|
3219
4465
|
return compileJsonPatchToIntent;
|
|
3220
4466
|
}
|
|
3221
4467
|
});
|
|
4468
|
+
Object.defineProperty(exports, 'crdtNodesToJsonPatch', {
|
|
4469
|
+
enumerable: true,
|
|
4470
|
+
get: function () {
|
|
4471
|
+
return crdtNodesToJsonPatch;
|
|
4472
|
+
}
|
|
4473
|
+
});
|
|
3222
4474
|
Object.defineProperty(exports, 'crdtToFullReplace', {
|
|
3223
4475
|
enumerable: true,
|
|
3224
4476
|
get: function () {
|
|
@@ -3291,6 +4543,12 @@ Object.defineProperty(exports, 'getAtJson', {
|
|
|
3291
4543
|
return getAtJson;
|
|
3292
4544
|
}
|
|
3293
4545
|
});
|
|
4546
|
+
Object.defineProperty(exports, 'intersectVersionVectors', {
|
|
4547
|
+
enumerable: true,
|
|
4548
|
+
get: function () {
|
|
4549
|
+
return intersectVersionVectors;
|
|
4550
|
+
}
|
|
4551
|
+
});
|
|
3294
4552
|
Object.defineProperty(exports, 'jsonEquals', {
|
|
3295
4553
|
enumerable: true,
|
|
3296
4554
|
get: function () {
|
|
@@ -3333,6 +4591,12 @@ Object.defineProperty(exports, 'mergeState', {
|
|
|
3333
4591
|
return mergeState;
|
|
3334
4592
|
}
|
|
3335
4593
|
});
|
|
4594
|
+
Object.defineProperty(exports, 'mergeVersionVectors', {
|
|
4595
|
+
enumerable: true,
|
|
4596
|
+
get: function () {
|
|
4597
|
+
return mergeVersionVectors;
|
|
4598
|
+
}
|
|
4599
|
+
});
|
|
3336
4600
|
Object.defineProperty(exports, 'newObj', {
|
|
3337
4601
|
enumerable: true,
|
|
3338
4602
|
get: function () {
|
|
@@ -3381,6 +4645,12 @@ Object.defineProperty(exports, 'observeDot', {
|
|
|
3381
4645
|
return observeDot;
|
|
3382
4646
|
}
|
|
3383
4647
|
});
|
|
4648
|
+
Object.defineProperty(exports, 'observedVersionVector', {
|
|
4649
|
+
enumerable: true,
|
|
4650
|
+
get: function () {
|
|
4651
|
+
return observedVersionVector;
|
|
4652
|
+
}
|
|
4653
|
+
});
|
|
3384
4654
|
Object.defineProperty(exports, 'parseJsonPointer', {
|
|
3385
4655
|
enumerable: true,
|
|
3386
4656
|
get: function () {
|
|
@@ -3411,6 +4681,12 @@ Object.defineProperty(exports, 'rgaInsertAfter', {
|
|
|
3411
4681
|
return rgaInsertAfter;
|
|
3412
4682
|
}
|
|
3413
4683
|
});
|
|
4684
|
+
Object.defineProperty(exports, 'rgaInsertAfterChecked', {
|
|
4685
|
+
enumerable: true,
|
|
4686
|
+
get: function () {
|
|
4687
|
+
return rgaInsertAfterChecked;
|
|
4688
|
+
}
|
|
4689
|
+
});
|
|
3414
4690
|
Object.defineProperty(exports, 'rgaLinearizeIds', {
|
|
3415
4691
|
enumerable: true,
|
|
3416
4692
|
get: function () {
|
|
@@ -3435,6 +4711,12 @@ Object.defineProperty(exports, 'serializeState', {
|
|
|
3435
4711
|
return serializeState;
|
|
3436
4712
|
}
|
|
3437
4713
|
});
|
|
4714
|
+
Object.defineProperty(exports, 'stableJsonValueKey', {
|
|
4715
|
+
enumerable: true,
|
|
4716
|
+
get: function () {
|
|
4717
|
+
return stableJsonValueKey;
|
|
4718
|
+
}
|
|
4719
|
+
});
|
|
3438
4720
|
Object.defineProperty(exports, 'stringifyJsonPointer', {
|
|
3439
4721
|
enumerable: true,
|
|
3440
4722
|
get: function () {
|
|
@@ -3501,6 +4783,18 @@ Object.defineProperty(exports, 'validateJsonPatch', {
|
|
|
3501
4783
|
return validateJsonPatch;
|
|
3502
4784
|
}
|
|
3503
4785
|
});
|
|
4786
|
+
Object.defineProperty(exports, 'validateRgaSeq', {
|
|
4787
|
+
enumerable: true,
|
|
4788
|
+
get: function () {
|
|
4789
|
+
return validateRgaSeq;
|
|
4790
|
+
}
|
|
4791
|
+
});
|
|
4792
|
+
Object.defineProperty(exports, 'versionVectorCovers', {
|
|
4793
|
+
enumerable: true,
|
|
4794
|
+
get: function () {
|
|
4795
|
+
return versionVectorCovers;
|
|
4796
|
+
}
|
|
4797
|
+
});
|
|
3504
4798
|
Object.defineProperty(exports, 'vvHasDot', {
|
|
3505
4799
|
enumerable: true,
|
|
3506
4800
|
get: function () {
|