Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 99 additions & 49 deletions packages/json-joy/src/json-crdt-diff/JsonCrdtDiff.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import {deepEqual} from '@jsonjoy.com/util/lib/json-equal/deepEqual';
import {cmpUint8Array} from '@jsonjoy.com/buffers/lib/cmpUint8Array';
import {type ITimespanStruct, type ITimestampStruct, type Patch, PatchBuilder} from '../json-crdt-patch';
import {
type ITimespanStruct,
type ITimestampStruct,
NodeBuilder,
nodes,
type Patch,
PatchBuilder,
Timestamp,
tss,
} from '../json-crdt-patch';
import {ArrNode, BinNode, ConNode, ObjNode, StrNode, ValNode, VecNode, type JsonNode} from '../json-crdt/nodes';
import * as str from '../util/diff/str';
import * as bin from '../util/diff/bin';
import * as line from '../util/diff/line';
import {structHashCrdt} from '../json-hash/structHashCrdt';
import {structHash} from '../json-hash';
import {structHashSchema} from '../json-hash/structHashSchema';
import type {Model} from '../json-crdt/model';

export class DiffError extends Error {
Expand All @@ -16,7 +25,7 @@ export class DiffError extends Error {
}

export class JsonCrdtDiff {
protected builder: PatchBuilder;
public builder: PatchBuilder;

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

protected diffArr(src: ArrNode, dst: unknown[]): void {
if (src.size() === 0) {
const length = dst.length;
if (length === 0) return;
let after: ITimestampStruct = src.id;
for (let i = 0; i < length; i++) after = this.builder.insArr(src.id, after, [this.buildView(dst[i])]);
return;
} else if (dst.length === 0) {
const spans: ITimespanStruct[] = [];
for (const chunk of src.chunks()) {
if (chunk.del) continue;
const id = chunk.id;
spans.push(tss(id.sid, id.time, chunk.span));
}
if (spans.length) this.builder.del(src.id, spans);
return;
}
const srcLines: string[] = [];
src.children((node) => {
srcLines.push(structHashCrdt(node));
});
src.children((node) => srcLines.push(structHashCrdt(node)));
const dstLines: string[] = [];
const dstLength = dst.length;
for (let i = 0; i < dstLength; i++) dstLines.push(structHash(dst[i]));
for (let i = 0; i < dstLength; i++) dstLines.push(structHashSchema(dst[i]));
const linePatch = line.diff(srcLines, dstLines);
if (!linePatch.length) return;
const inserts: [after: ITimestampStruct, views: unknown[]][] = [];
const deletes: ITimespanStruct[] = [];
const patchLength = linePatch.length;
for (let i = patchLength - 1; i >= 0; i--) {
const [type, posSrc, posDst] = linePatch[i];
switch (type) {
case line.LINE_PATCH_OP_TYPE.EQL:
break;
case line.LINE_PATCH_OP_TYPE.INS: {
const view = dst[posDst];
const after = posSrc >= 0 ? src.find(posSrc) : src.id;
if (!after) throw new DiffError();
inserts.push([after, [view]]);
break;
}
case line.LINE_PATCH_OP_TYPE.DEL: {
const span = src.findInterval(posSrc, 1);
if (!span || !span.length) throw new DiffError();
deletes.push(...span);
break;
}
case line.LINE_PATCH_OP_TYPE.MIX: {
const view = dst[posDst];
try {
this.diffAny(src.getNode(posSrc)!, view);
} catch (error) {
if (error instanceof DiffError) {
const span = src.findInterval(posSrc, 1)!;
deletes.push(...span);
const after = posSrc ? src.find(posSrc - 1) : src.id;
if (!after) throw new DiffError();
inserts.push([after, [view]]);
} else throw error;
}
line.apply(
linePatch,
(posSrc) => {
const span = src.findInterval(posSrc, 1);
if (!span || !span.length) throw new DiffError();
deletes.push(...span);
},
(posSrc, posDst) => {
const view = dst[posDst];
const after = posSrc >= 0 ? src.find(posSrc) : src.id;
if (!after) throw new DiffError();
inserts.push([after, [view]]);
},
(posSrc, posDst) => {
const view = dst[posDst];
try {
this.diffAny(src.getNode(posSrc)!, view);
} catch (error) {
if (error instanceof DiffError) {
const span = src.findInterval(posSrc, 1)!;
deletes.push(...span);
const after = posSrc ? src.find(posSrc - 1) : src.id;
if (!after) throw new DiffError();
inserts.push([after, [view]]);
} else throw error;
}
}
}
},
);
const builder = this.builder;
const length = inserts.length;
for (let i = 0; i < length; i++) {
const [after, views] = inserts[i];
builder.insArr(
src.id,
after,
views.map((view) => builder.json(view)),
views.map((view) => this.buildView(view)),
);
}
if (deletes.length) builder.del(src.id, deletes);
Expand Down Expand Up @@ -131,7 +147,8 @@ export class JsonCrdtDiff {
}
}
}
inserts.push([key, src.get(key) instanceof ConNode ? builder.con(dstValue) : builder.constOrJson(dstValue)]);
inserts.push([key, this.buildConView(dstValue)]);
// inserts.push([key, src.get(key) instanceof ConNode ? builder.con(dstValue) : this.buildConView(dstValue)]);
}
if (inserts.length) builder.insObj(src.id, inserts);
}
Expand All @@ -149,11 +166,11 @@ export class JsonCrdtDiff {
if (id) {
const child = index.get(id);
const isDeleted = !child || (child instanceof ConNode && child.val === void 0);
if (isDeleted) return;
if (isDeleted) continue;
edits.push([i, builder.con(void 0)]);
}
}
for (let i = 0; i < min; i++) {
CHILDREN: for (let i = 0; i < min; i++) {
const value = dst[i];
const child = src.get(i);
if (child) {
Expand All @@ -163,10 +180,15 @@ export class JsonCrdtDiff {
} catch (error) {
if (!(error instanceof DiffError)) throw error;
}
if (child instanceof ConNode && typeof value !== 'object') {
const valueId = builder.con(value);
edits.push([i, valueId]);
continue CHILDREN;
}
}
edits.push([i, builder.constOrJson(value)]);
edits.push([i, this.buildConView(value)]);
}
for (let i = srcLength; i < dstLength; i++) edits.push([i, builder.constOrJson(dst[i])]);
for (let i = srcLength; i < dstLength; i++) edits.push([i, this.buildConView(dst[i])]);
if (edits.length) builder.insVec(src.id, edits);
}

Expand All @@ -176,30 +198,44 @@ export class JsonCrdtDiff {
} catch (error) {
if (error instanceof DiffError) {
const builder = this.builder;
builder.setVal(src.id, builder.constOrJson(dst));
builder.setVal(src.id, this.buildConView(dst));
} else throw error;
}
}

protected diffAny(src: JsonNode, dst: unknown): void {
if (src instanceof ConNode) {
if (dst instanceof nodes.con) dst = dst.raw;
const val = src.val;
if (val !== dst && !deepEqual(src.val, dst)) throw new DiffError();
if (
val !== dst &&
((val instanceof Timestamp && !(dst instanceof Timestamp)) ||
(!(val instanceof Timestamp) && dst instanceof Timestamp) ||
!deepEqual(src.val, dst))
)
throw new DiffError();
} else if (src instanceof StrNode) {
if (dst instanceof nodes.str) dst = dst.raw;
if (typeof dst !== 'string') throw new DiffError();
this.diffStr(src, dst);
} else if (src instanceof ObjNode) {
if (dst instanceof nodes.obj) dst = dst.opt ? {...dst.obj, ...dst.opt} : dst.obj;
if (dst instanceof NodeBuilder) throw new DiffError();
if (!dst || typeof dst !== 'object' || Array.isArray(dst)) throw new DiffError();
this.diffObj(src, dst as Record<string, unknown>);
} else if (src instanceof ValNode) {
if (dst instanceof nodes.val) dst = dst.value;
this.diffVal(src, dst);
} else if (src instanceof ArrNode) {
if (dst instanceof nodes.arr) dst = dst.arr;
if (!Array.isArray(dst)) throw new DiffError();
this.diffArr(src, dst as unknown[]);
} else if (src instanceof VecNode) {
if (dst instanceof nodes.vec) dst = dst.value;
if (!Array.isArray(dst)) throw new DiffError();
this.diffVec(src, dst as unknown[]);
} else if (src instanceof BinNode) {
if (dst instanceof nodes.bin) dst = dst.raw;
if (!(dst instanceof Uint8Array)) throw new DiffError();
this.diffBin(src, dst);
} else {
Expand All @@ -211,4 +247,18 @@ export class JsonCrdtDiff {
this.diffAny(src, dst);
return this.builder.flush();
}

protected buildView(dst: unknown): ITimestampStruct {
const builder = this.builder;
if (dst instanceof Timestamp) return builder.con(dst);
if (dst instanceof nodes.con) return builder.con(dst.raw);
return builder.json(dst);
}

protected buildConView(dst: unknown): ITimestampStruct {
const builder = this.builder;
if (dst instanceof Timestamp) return builder.con(dst);
if (dst instanceof nodes.con) return builder.con(dst.raw);
return builder.constOrJson(dst);
}
}
Loading