Skip to content

Commit fa08c52

Browse files
authored
Merge pull request #471 from streamich/debug-file
Debug file
2 parents c24cea3 + 653dd80 commit fa08c52

File tree

11 files changed

+476
-62
lines changed

11 files changed

+476
-62
lines changed

src/json-crdt-patch/clock/clock.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,10 +196,10 @@ export class ClockVector extends LogicalClock implements IClockVector {
196196
}
197197

198198
/**
199-
* Returns a human-readable string representation of the vector clock.
199+
* Returns a human-readable string representation of the clock vector.
200200
*
201201
* @param tab String to use for indentation.
202-
* @returns Human-readable string representation of the vector clock.
202+
* @returns Human-readable string representation of the clock vector.
203203
*/
204204
public toString(tab: string = ''): string {
205205
const last = this.peers.size;
@@ -236,4 +236,13 @@ export class ServerClockVector extends LogicalClock implements IClockVector {
236236
public fork(): ServerClockVector {
237237
return new ServerClockVector(SESSION.SERVER, this.time);
238238
}
239+
240+
/**
241+
* Returns a human-readable string representation of the clock vector.
242+
*
243+
* @returns Human-readable string representation of the clock vector.
244+
*/
245+
public toString(): string {
246+
return `clock ${this.sid}.${this.time}`;
247+
}
239248
}

src/json-crdt/file/File.ts

Lines changed: 128 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,101 @@
1-
import {Model} from "../model";
2-
import {PatchLog} from "./PatchLog";
3-
import {FileModelEncoding} from "./constants";
1+
import {Model} from '../model';
2+
import {PatchLog} from './PatchLog';
3+
import {FileModelEncoding} from './constants';
44
import {Encoder as SidecarEncoder} from '../codec/sidecar/binary/Encoder';
5+
import {Decoder as SidecarDecoder} from '../codec/sidecar/binary/Decoder';
56
import {Encoder as StructuralEncoderCompact} from '../codec/structural/compact/Encoder';
67
import {Encoder as StructuralEncoderVerbose} from '../codec/structural/verbose/Encoder';
78
import {encode as encodeCompact} from '../../json-crdt-patch/codec/compact/encode';
89
import {encode as encodeVerbose} from '../../json-crdt-patch/codec/verbose/encode';
9-
import type * as types from "./types";
10+
import {Writer} from '../../util/buffers/Writer';
11+
import {CborEncoder} from '../../json-pack/cbor/CborEncoder';
12+
import {JsonEncoder} from '../../json-pack/json/JsonEncoder';
13+
import {printTree} from '../../util/print/printTree';
14+
import {decodeModel, decodeNdjsonComponents, decodePatch, decodeSeqCborComponents} from './util';
15+
import {Patch} from '../../json-crdt-patch';
16+
import type * as types from './types';
17+
import type {Printable} from '../../util/print/types';
1018

11-
export class File {
12-
public static fromModel(model: Model): File {
19+
export class File implements Printable {
20+
public static unserialize(components: types.FileReadSequence): File {
21+
const [view, metadata, model, history, ...frontier] = components;
22+
const modelFormat = metadata[1];
23+
let decodedModel: Model<any> | null = null;
24+
if (model) {
25+
const isSidecar = modelFormat === FileModelEncoding.SidecarBinary;
26+
if (isSidecar) {
27+
const decoder = new SidecarDecoder();
28+
if (!(model instanceof Uint8Array)) throw new Error('NOT_BLOB');
29+
decodedModel = decoder.decode(view, model);
30+
} else {
31+
decodedModel = decodeModel(model);
32+
}
33+
}
34+
let log: PatchLog | null = null;
35+
if (history) {
36+
const [start, patches] = history;
37+
if (start) {
38+
const startModel = decodeModel(start);
39+
log = new PatchLog(startModel);
40+
for (const patch of patches) log.push(decodePatch(patch));
41+
}
42+
}
43+
if (!log) throw new Error('NO_HISTORY');
44+
if (!decodedModel) decodedModel = log.replayToEnd();
45+
if (frontier.length) {
46+
for (const patch of frontier) {
47+
const patchDecoded = decodePatch(patch);
48+
decodedModel.applyPatch(patchDecoded);
49+
log.push(patchDecoded);
50+
}
51+
}
52+
const file = new File(decodedModel, log);
53+
return file;
54+
}
55+
56+
public static fromNdjson(blob: Uint8Array): File {
57+
const components = decodeNdjsonComponents(blob);
58+
return File.unserialize(components as types.FileReadSequence);
59+
}
60+
61+
public static fromSeqCbor(blob: Uint8Array): File {
62+
const components = decodeSeqCborComponents(blob);
63+
return File.unserialize(components as types.FileReadSequence);
64+
}
65+
66+
public static fromModel(model: Model<any>): File {
1367
return new File(model, PatchLog.fromModel(model));
1468
}
1569

16-
constructor(
17-
public readonly model: Model,
18-
public readonly history: PatchLog,
19-
) {}
70+
constructor(public readonly model: Model, public readonly log: PatchLog) {}
71+
72+
public apply(patch: Patch): void {
73+
const id = patch.getId();
74+
if (!id) return;
75+
this.model.applyPatch(patch);
76+
this.log.push(patch);
77+
}
78+
79+
public sync(): () => void {
80+
const {model, log} = this;
81+
const api = model.api;
82+
const autoflushUnsubscribe = api.autoFlush();
83+
const onPatchUnsubscribe = api.onPatch.listen((patch) => {
84+
log.push(patch);
85+
});
86+
const onFlushUnsubscribe = api.onFlush.listen((patch) => {
87+
log.push(patch);
88+
});
89+
return () => {
90+
autoflushUnsubscribe();
91+
onPatchUnsubscribe();
92+
onFlushUnsubscribe();
93+
};
94+
}
2095

2196
public serialize(params: types.FileSerializeParams = {}): types.FileWriteSequence {
22-
const view = this.model.view();
23-
const metadata: types.FileMetadata = [
24-
{},
25-
FileModelEncoding.SidecarBinary,
26-
];
97+
if (params.noView && params.model === 'sidecar') throw new Error('SIDECAR_MODEL_WITHOUT_VIEW');
98+
const metadata: types.FileMetadata = [{}, FileModelEncoding.Auto];
2799
let model: Uint8Array | unknown | null = null;
28100
const modelFormat = params.model ?? 'sidecar';
29101
switch (modelFormat) {
@@ -35,58 +107,80 @@ export class File {
35107
break;
36108
}
37109
case 'binary': {
38-
metadata[1] = FileModelEncoding.StructuralBinary;
39110
model = this.model.toBinary();
40111
break;
41112
}
42113
case 'compact': {
43-
metadata[1] = FileModelEncoding.StructuralCompact;
44114
model = new StructuralEncoderCompact().encode(this.model);
45115
break;
46116
}
47117
case 'verbose': {
48-
metadata[1] = FileModelEncoding.StructuralVerbose;
49118
model = new StructuralEncoderVerbose().encode(this.model);
50119
break;
51120
}
121+
case 'none': {
122+
model = null;
123+
break;
124+
}
52125
default:
53126
throw new Error(`Invalid model format: ${modelFormat}`);
54127
}
55-
const history: types.FileWriteSequenceHistory = [
56-
null,
57-
[],
58-
];
128+
const history: types.FileWriteSequenceHistory = [null, []];
59129
const patchFormat = params.history ?? 'binary';
60130
switch (patchFormat) {
61131
case 'binary': {
62-
history[0] = this.history.start.toBinary();
63-
this.history.patches.forEach(({v}) => {
132+
history[0] = this.log.start.toBinary();
133+
this.log.patches.forEach(({v}) => {
64134
history[1].push(v.toBinary());
65135
});
66136
break;
67137
}
68138
case 'compact': {
69-
history[0] = new StructuralEncoderCompact().encode(this.history.start);
70-
this.history.patches.forEach(({v}) => {
139+
history[0] = new StructuralEncoderCompact().encode(this.log.start);
140+
this.log.patches.forEach(({v}) => {
71141
history[1].push(encodeCompact(v));
72142
});
73143
break;
74144
}
75145
case 'verbose': {
76-
history[0] = new StructuralEncoderVerbose().encode(this.history.start);
77-
this.history.patches.forEach(({v}) => {
146+
history[0] = new StructuralEncoderVerbose().encode(this.log.start);
147+
this.log.patches.forEach(({v}) => {
78148
history[1].push(encodeVerbose(v));
79149
});
80150
break;
81151
}
152+
case 'none': {
153+
break;
154+
}
82155
default:
83156
throw new Error(`Invalid history format: ${patchFormat}`);
84157
}
85-
return [
86-
view,
87-
metadata,
88-
model,
89-
history,
90-
];
158+
return [params.noView ? null : this.model.view(), metadata, model, history];
159+
}
160+
161+
public toBinary(params: types.FileEncodingParams): Uint8Array {
162+
const sequence = this.serialize(params);
163+
const writer = new Writer(16 * 1024);
164+
switch (params.format) {
165+
case 'ndjson': {
166+
const json = new JsonEncoder(writer);
167+
for (const component of sequence) {
168+
json.writeAny(component);
169+
json.writer.u8('\n'.charCodeAt(0));
170+
}
171+
return json.writer.flush();
172+
}
173+
case 'seq.cbor': {
174+
const cbor = new CborEncoder(writer);
175+
for (const component of sequence) cbor.writeAny(component);
176+
return cbor.writer.flush();
177+
}
178+
}
179+
}
180+
181+
// ---------------------------------------------------------------- Printable
182+
183+
public toString(tab?: string) {
184+
return `file` + printTree(tab, [(tab) => this.model.toString(tab), () => '', (tab) => this.log.toString(tab)]);
91185
}
92186
}

src/json-crdt/file/PatchLog.ts

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
import {ITimestampStruct, Patch, ServerClockVector, compare} from "../../json-crdt-patch";
2-
import {AvlMap} from "../../util/trees/avl/AvlMap";
3-
import {Model} from "../model";
1+
import {ITimestampStruct, Patch, compare} from '../../json-crdt-patch';
2+
import {printTree} from '../../util/print/printTree';
3+
import {AvlMap} from '../../util/trees/avl/AvlMap';
4+
import {Model} from '../model';
5+
import type {Printable} from '../../util/print/types';
6+
import {first, next} from '../../util/trees/util';
47

5-
export class PatchLog {
6-
public static fromModel (model: Model): PatchLog {
8+
export class PatchLog implements Printable {
9+
public static fromModel(model: Model<any>): PatchLog {
710
const start = new Model(model.clock.clone());
811
const log = new PatchLog(start);
912
if (model.api.builder.patch.ops.length) {
@@ -15,11 +18,44 @@ export class PatchLog {
1518

1619
public readonly patches = new AvlMap<ITimestampStruct, Patch>(compare);
1720

18-
constructor (public readonly start: Model) {}
21+
constructor(public readonly start: Model) {}
1922

2023
public push(patch: Patch): void {
2124
const id = patch.getId();
2225
if (!id) return;
2326
this.patches.set(id, patch);
2427
}
28+
29+
public replayToEnd(): Model {
30+
const clone = this.start.clone();
31+
for (let node = first(this.patches.root); node; node = next(node)) clone.applyPatch(node.v);
32+
return clone;
33+
}
34+
35+
public replayTo(ts: ITimestampStruct): Model {
36+
const clone = this.start.clone();
37+
for (let node = first(this.patches.root); node && compare(ts, node.k) >= 0; node = next(node))
38+
clone.applyPatch(node.v);
39+
return clone;
40+
}
41+
42+
// ---------------------------------------------------------------- Printable
43+
44+
public toString(tab?: string) {
45+
const log: Patch[] = [];
46+
this.patches.forEach(({v}) => log.push(v));
47+
return (
48+
`log` +
49+
printTree(tab, [
50+
(tab) => this.start.toString(tab),
51+
() => '',
52+
(tab) =>
53+
'history' +
54+
printTree(
55+
tab,
56+
log.map((patch, i) => (tab) => `${i}: ${patch.toString(tab)}`),
57+
),
58+
])
59+
);
60+
}
2561
}

0 commit comments

Comments
 (0)