collaborative-ui
Version:
React component library for building real-time collaborative editing applications.
164 lines (163 loc) • 7.24 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.JsonCrdtExplorerState = void 0;
const json_crdt_1 = require("json-joy/lib/json-crdt");
const json_crdt_extensions_1 = require("json-joy/lib/json-crdt-extensions");
const Log_1 = require("json-joy/lib/json-crdt/log/Log");
const LogDecoder_1 = require("json-joy/lib/json-crdt/log/codec/LogDecoder");
const CborDecoder_1 = require("@jsonjoy.com/json-pack/lib/cbor/CborDecoder");
const rxjs_1 = require("rxjs");
const gzip_1 = require("@jsonjoy.com/util/lib/compression/gzip");
const util_1 = require("./util");
const JsonCrdtLogState_1 = require("../JsonCrdtLog/JsonCrdtLogState");
class JsonCrdtExplorerState {
constructor() {
this.files$ = new rxjs_1.BehaviorSubject([]);
this.selected$ = new rxjs_1.BehaviorSubject('');
this.file$ = new rxjs_1.BehaviorSubject(null);
this.sid = json_crdt_1.Model.sid();
this.select = (id) => {
this.selected$.next(id);
};
this.openFile = (log, name = 'JSON CRDT document' + (this.newCnt > 1 ? ` (${this.newCnt})` : '')) => {
const now = Date.now();
const logState = new JsonCrdtLogState_1.JsonCrdtLogState(log, { view: 'model' });
const file = {
id: Math.random().toString(36).slice(2) + '.' + now,
openTime: now,
log,
name,
logState,
};
this.files$.next([...this.files$.getValue(), file]);
this.select(file.id);
return file;
};
this.close = (id) => {
const list = this.files$.getValue().filter((m) => m.id !== id);
this.files$.next(list);
const files = this.files$.getValue();
if (files.length && !this.file$.getValue())
this.select(files[0].id);
};
this.rename = (id, name) => {
const files = this.files$.getValue();
const file = files.find((m) => m.id === id);
if (!file)
return;
file.name = name;
this.files$.next([...files]);
};
this.addFile = async (file) => {
if (!file)
return;
let uint8 = new Uint8Array(await file.arrayBuffer());
const name = file.name ? (0, util_1.stripExtensions)(file.name) : 'model';
if (file.name.endsWith('patches.bin')) {
const cborDecoder = new CborDecoder_1.CborDecoder();
const array = cborDecoder.decode(uint8);
if (!Array.isArray(array))
throw new Error('Incompatible JSON CRDT log file.');
const patches = array.map((patch) => json_crdt_1.Patch.fromBinary(patch));
const lastPatch = patches[patches.length - 1];
if (!lastPatch)
throw new Error('Incompatible JSON CRDT log file.');
const id = lastPatch.getId();
if (!id)
throw new Error('Incompatible JSON CRDT log file.');
const model = json_crdt_1.Model.create(undefined, id.sid);
const log = new Log_1.Log(() => model.clone());
log.end.applyBatch(patches);
log.end.api.autoFlush();
log.end.setSid(this.sid);
this.openFile(log, name);
}
if (file.name.endsWith('.crdt')) {
try {
uint8 = (await (0, gzip_1.ungzip)(uint8));
}
catch { }
const model = json_crdt_1.Model.load(uint8, this.sid);
const log = new Log_1.Log(() => model);
log.end.api.autoFlush();
log.end.setSid(this.sid);
this.openFile(log, name);
}
else if (file.name.endsWith('.cbor.gz') ||
file.name.endsWith('.seq.cbor') ||
file.name.endsWith('.seq.cbor.gz')) {
await this.addLog(uint8, name);
}
};
this.addLog = async (uint8, name, display) => {
try {
uint8 = await (0, gzip_1.ungzip)(uint8);
}
catch { }
const cborDecoder = new CborDecoder_1.CborDecoder();
const decoder = new LogDecoder_1.LogDecoder({ cborDecoder });
const { history: log } = decoder.decode(uint8, { history: true, frontier: true, format: 'seq.cbor' });
if (!log)
throw new Error('Incompatible JSON CRDT log file.');
const start = log.start;
log.start = () => {
const model = start();
model.ext.register(json_crdt_extensions_1.ext.quill);
return model;
};
log.end.ext.register(json_crdt_extensions_1.ext.quill);
log.end.api.autoFlush();
log.end.setSid(this.sid);
const file = this.openFile(log, name);
file.display = display;
if (file.display === 'text') {
const logState = file.logState;
logState.patchState.show$.next(false);
logState.modelState.showModel$.next(false);
logState.modelState.showView$.next(false);
logState.modelState.showDisplay$.next(true);
}
else if (file.display === 'blogpost' || file.display === 'todo') {
const logState = file.logState;
logState.patchState.show$.next(false);
logState.modelState.showModel$.next(false);
logState.modelState.showView$.next(true);
logState.modelState.showDisplay$.next(true);
}
else if (file.display === 'quill') {
const logState = file.logState;
logState.patchState.show$.next(false);
logState.modelState.showModel$.next(false);
logState.modelState.showView$.next(true);
logState.modelState.showDisplay$.next(true);
}
};
this.addTrace = async (uint8, trace) => {
return await this.addLog(uint8, trace.name, trace.display);
};
this.addFiles = async (files) => {
files.map((file) => this.addFile(file).catch(() => { }));
};
this.newCnt = 0;
this.createNew = () => {
const schema = json_crdt_1.s.obj({});
const model = json_crdt_1.Model.create(schema, this.sid);
this.createFromModel(model);
};
this.createFromModel = (model) => {
this.newCnt++;
const log = Log_1.Log.fromNewModel(model);
log.end.api.autoFlush();
this.openFile(log);
};
this.files$
.pipe((0, rxjs_1.switchMap)(() => this.selected$), (0, rxjs_1.map)((selected) => {
if (!selected)
return null;
const files = this.files$.getValue();
return files.find((file) => file.id === selected) ?? null;
}))
.subscribe(this.file$);
}
}
exports.JsonCrdtExplorerState = JsonCrdtExplorerState;