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