Skip to content

Commit 0c47340

Browse files
authored
Merge pull request #965 from streamich/json-crdt-diff
JSON CRDT diff improvements
2 parents 2ae4ed6 + 45b6f38 commit 0c47340

File tree

13 files changed

+430
-87
lines changed

13 files changed

+430
-87
lines changed

packages/json-joy/src/json-crdt-diff/JsonCrdtDiff.ts

Lines changed: 99 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
import {deepEqual} from '@jsonjoy.com/util/lib/json-equal/deepEqual';
22
import {cmpUint8Array} from '@jsonjoy.com/buffers/lib/cmpUint8Array';
3-
import {type ITimespanStruct, type ITimestampStruct, type Patch, PatchBuilder} from '../json-crdt-patch';
3+
import {
4+
type ITimespanStruct,
5+
type ITimestampStruct,
6+
NodeBuilder,
7+
nodes,
8+
type Patch,
9+
PatchBuilder,
10+
Timestamp,
11+
tss,
12+
} from '../json-crdt-patch';
413
import {ArrNode, BinNode, ConNode, ObjNode, StrNode, ValNode, VecNode, type JsonNode} from '../json-crdt/nodes';
514
import * as str from '../util/diff/str';
615
import * as bin from '../util/diff/bin';
716
import * as line from '../util/diff/line';
817
import {structHashCrdt} from '../json-hash/structHashCrdt';
9-
import {structHash} from '../json-hash';
18+
import {structHashSchema} from '../json-hash/structHashSchema';
1019
import type {Model} from '../json-crdt/model';
1120

1221
export class DiffError extends Error {
@@ -16,7 +25,7 @@ export class DiffError extends Error {
1625
}
1726

1827
export class JsonCrdtDiff {
19-
protected builder: PatchBuilder;
28+
public builder: PatchBuilder;
2029

2130
public constructor(protected readonly model: Model<any>) {
2231
this.builder = new PatchBuilder(model.clock.clone());
@@ -47,60 +56,67 @@ export class JsonCrdtDiff {
4756
}
4857

4958
protected diffArr(src: ArrNode, dst: unknown[]): void {
59+
if (src.size() === 0) {
60+
const length = dst.length;
61+
if (length === 0) return;
62+
let after: ITimestampStruct = src.id;
63+
for (let i = 0; i < length; i++) after = this.builder.insArr(src.id, after, [this.buildView(dst[i])]);
64+
return;
65+
} else if (dst.length === 0) {
66+
const spans: ITimespanStruct[] = [];
67+
for (const chunk of src.chunks()) {
68+
if (chunk.del) continue;
69+
const id = chunk.id;
70+
spans.push(tss(id.sid, id.time, chunk.span));
71+
}
72+
if (spans.length) this.builder.del(src.id, spans);
73+
return;
74+
}
5075
const srcLines: string[] = [];
51-
src.children((node) => {
52-
srcLines.push(structHashCrdt(node));
53-
});
76+
src.children((node) => srcLines.push(structHashCrdt(node)));
5477
const dstLines: string[] = [];
5578
const dstLength = dst.length;
56-
for (let i = 0; i < dstLength; i++) dstLines.push(structHash(dst[i]));
79+
for (let i = 0; i < dstLength; i++) dstLines.push(structHashSchema(dst[i]));
5780
const linePatch = line.diff(srcLines, dstLines);
5881
if (!linePatch.length) return;
5982
const inserts: [after: ITimestampStruct, views: unknown[]][] = [];
6083
const deletes: ITimespanStruct[] = [];
61-
const patchLength = linePatch.length;
62-
for (let i = patchLength - 1; i >= 0; i--) {
63-
const [type, posSrc, posDst] = linePatch[i];
64-
switch (type) {
65-
case line.LINE_PATCH_OP_TYPE.EQL:
66-
break;
67-
case line.LINE_PATCH_OP_TYPE.INS: {
68-
const view = dst[posDst];
69-
const after = posSrc >= 0 ? src.find(posSrc) : src.id;
70-
if (!after) throw new DiffError();
71-
inserts.push([after, [view]]);
72-
break;
73-
}
74-
case line.LINE_PATCH_OP_TYPE.DEL: {
75-
const span = src.findInterval(posSrc, 1);
76-
if (!span || !span.length) throw new DiffError();
77-
deletes.push(...span);
78-
break;
79-
}
80-
case line.LINE_PATCH_OP_TYPE.MIX: {
81-
const view = dst[posDst];
82-
try {
83-
this.diffAny(src.getNode(posSrc)!, view);
84-
} catch (error) {
85-
if (error instanceof DiffError) {
86-
const span = src.findInterval(posSrc, 1)!;
87-
deletes.push(...span);
88-
const after = posSrc ? src.find(posSrc - 1) : src.id;
89-
if (!after) throw new DiffError();
90-
inserts.push([after, [view]]);
91-
} else throw error;
92-
}
84+
line.apply(
85+
linePatch,
86+
(posSrc) => {
87+
const span = src.findInterval(posSrc, 1);
88+
if (!span || !span.length) throw new DiffError();
89+
deletes.push(...span);
90+
},
91+
(posSrc, posDst) => {
92+
const view = dst[posDst];
93+
const after = posSrc >= 0 ? src.find(posSrc) : src.id;
94+
if (!after) throw new DiffError();
95+
inserts.push([after, [view]]);
96+
},
97+
(posSrc, posDst) => {
98+
const view = dst[posDst];
99+
try {
100+
this.diffAny(src.getNode(posSrc)!, view);
101+
} catch (error) {
102+
if (error instanceof DiffError) {
103+
const span = src.findInterval(posSrc, 1)!;
104+
deletes.push(...span);
105+
const after = posSrc ? src.find(posSrc - 1) : src.id;
106+
if (!after) throw new DiffError();
107+
inserts.push([after, [view]]);
108+
} else throw error;
93109
}
94-
}
95-
}
110+
},
111+
);
96112
const builder = this.builder;
97113
const length = inserts.length;
98114
for (let i = 0; i < length; i++) {
99115
const [after, views] = inserts[i];
100116
builder.insArr(
101117
src.id,
102118
after,
103-
views.map((view) => builder.json(view)),
119+
views.map((view) => this.buildView(view)),
104120
);
105121
}
106122
if (deletes.length) builder.del(src.id, deletes);
@@ -131,7 +147,8 @@ export class JsonCrdtDiff {
131147
}
132148
}
133149
}
134-
inserts.push([key, src.get(key) instanceof ConNode ? builder.con(dstValue) : builder.constOrJson(dstValue)]);
150+
inserts.push([key, this.buildConView(dstValue)]);
151+
// inserts.push([key, src.get(key) instanceof ConNode ? builder.con(dstValue) : this.buildConView(dstValue)]);
135152
}
136153
if (inserts.length) builder.insObj(src.id, inserts);
137154
}
@@ -149,11 +166,11 @@ export class JsonCrdtDiff {
149166
if (id) {
150167
const child = index.get(id);
151168
const isDeleted = !child || (child instanceof ConNode && child.val === void 0);
152-
if (isDeleted) return;
169+
if (isDeleted) continue;
153170
edits.push([i, builder.con(void 0)]);
154171
}
155172
}
156-
for (let i = 0; i < min; i++) {
173+
CHILDREN: for (let i = 0; i < min; i++) {
157174
const value = dst[i];
158175
const child = src.get(i);
159176
if (child) {
@@ -163,10 +180,15 @@ export class JsonCrdtDiff {
163180
} catch (error) {
164181
if (!(error instanceof DiffError)) throw error;
165182
}
183+
if (child instanceof ConNode && typeof value !== 'object') {
184+
const valueId = builder.con(value);
185+
edits.push([i, valueId]);
186+
continue CHILDREN;
187+
}
166188
}
167-
edits.push([i, builder.constOrJson(value)]);
189+
edits.push([i, this.buildConView(value)]);
168190
}
169-
for (let i = srcLength; i < dstLength; i++) edits.push([i, builder.constOrJson(dst[i])]);
191+
for (let i = srcLength; i < dstLength; i++) edits.push([i, this.buildConView(dst[i])]);
170192
if (edits.length) builder.insVec(src.id, edits);
171193
}
172194

@@ -176,30 +198,44 @@ export class JsonCrdtDiff {
176198
} catch (error) {
177199
if (error instanceof DiffError) {
178200
const builder = this.builder;
179-
builder.setVal(src.id, builder.constOrJson(dst));
201+
builder.setVal(src.id, this.buildConView(dst));
180202
} else throw error;
181203
}
182204
}
183205

184206
protected diffAny(src: JsonNode, dst: unknown): void {
185207
if (src instanceof ConNode) {
208+
if (dst instanceof nodes.con) dst = dst.raw;
186209
const val = src.val;
187-
if (val !== dst && !deepEqual(src.val, dst)) throw new DiffError();
210+
if (
211+
val !== dst &&
212+
((val instanceof Timestamp && !(dst instanceof Timestamp)) ||
213+
(!(val instanceof Timestamp) && dst instanceof Timestamp) ||
214+
!deepEqual(src.val, dst))
215+
)
216+
throw new DiffError();
188217
} else if (src instanceof StrNode) {
218+
if (dst instanceof nodes.str) dst = dst.raw;
189219
if (typeof dst !== 'string') throw new DiffError();
190220
this.diffStr(src, dst);
191221
} else if (src instanceof ObjNode) {
222+
if (dst instanceof nodes.obj) dst = dst.opt ? {...dst.obj, ...dst.opt} : dst.obj;
223+
if (dst instanceof NodeBuilder) throw new DiffError();
192224
if (!dst || typeof dst !== 'object' || Array.isArray(dst)) throw new DiffError();
193225
this.diffObj(src, dst as Record<string, unknown>);
194226
} else if (src instanceof ValNode) {
227+
if (dst instanceof nodes.val) dst = dst.value;
195228
this.diffVal(src, dst);
196229
} else if (src instanceof ArrNode) {
230+
if (dst instanceof nodes.arr) dst = dst.arr;
197231
if (!Array.isArray(dst)) throw new DiffError();
198232
this.diffArr(src, dst as unknown[]);
199233
} else if (src instanceof VecNode) {
234+
if (dst instanceof nodes.vec) dst = dst.value;
200235
if (!Array.isArray(dst)) throw new DiffError();
201236
this.diffVec(src, dst as unknown[]);
202237
} else if (src instanceof BinNode) {
238+
if (dst instanceof nodes.bin) dst = dst.raw;
203239
if (!(dst instanceof Uint8Array)) throw new DiffError();
204240
this.diffBin(src, dst);
205241
} else {
@@ -211,4 +247,18 @@ export class JsonCrdtDiff {
211247
this.diffAny(src, dst);
212248
return this.builder.flush();
213249
}
250+
251+
protected buildView(dst: unknown): ITimestampStruct {
252+
const builder = this.builder;
253+
if (dst instanceof Timestamp) return builder.con(dst);
254+
if (dst instanceof nodes.con) return builder.con(dst.raw);
255+
return builder.json(dst);
256+
}
257+
258+
protected buildConView(dst: unknown): ITimestampStruct {
259+
const builder = this.builder;
260+
if (dst instanceof Timestamp) return builder.con(dst);
261+
if (dst instanceof nodes.con) return builder.con(dst.raw);
262+
return builder.constOrJson(dst);
263+
}
214264
}

0 commit comments

Comments
 (0)