Several people edit one model at once through a server you run yourself (archify-vscode-ext-sync); the files stay ordinary files on each disk. Sign-in through the browser, offline-first with later merge, teammates' cursors in the Source view and text editors, and a Shared section on the model page with people, versions, Compare and Restore. Without a server nothing changes: the sync code is a separate bundle that loads only when a model is shared, after a sign-in, or when a sync command or invite link is used.
69 lines
2.1 KiB
TypeScript
69 lines
2.1 KiB
TypeScript
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
|
|
/**
|
|
* The offline copy of one shared model: the last compacted Yjs state and a
|
|
* log of every update since, each written before it is sent. Lets people
|
|
* keep working while the server is away and catch up afterwards.
|
|
*/
|
|
export class OfflineStore {
|
|
private readonly statePath: string;
|
|
private readonly logPath: string;
|
|
private logged = 0;
|
|
|
|
constructor(dir: string, id: string) {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
this.statePath = path.join(dir, `${id}.state`);
|
|
this.logPath = path.join(dir, `${id}.log`);
|
|
}
|
|
|
|
exists(): boolean {
|
|
return fs.existsSync(this.statePath) || fs.existsSync(this.logPath);
|
|
}
|
|
|
|
/** Updates to apply in order: the state, then the log. A torn last entry is skipped. */
|
|
load(): Uint8Array[] {
|
|
const out: Uint8Array[] = [];
|
|
if (fs.existsSync(this.statePath)) out.push(new Uint8Array(fs.readFileSync(this.statePath)));
|
|
if (fs.existsSync(this.logPath)) {
|
|
const log = fs.readFileSync(this.logPath);
|
|
let at = 0;
|
|
while (at + 4 <= log.length) {
|
|
const length = log.readUInt32LE(at);
|
|
if (at + 4 + length > log.length) break;
|
|
out.push(new Uint8Array(log.subarray(at + 4, at + 4 + length)));
|
|
at += 4 + length;
|
|
this.logged++;
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** Appends one update; synchronous, so it is on disk before the caller sends it. */
|
|
append(update: Uint8Array): void {
|
|
const head = Buffer.alloc(4);
|
|
head.writeUInt32LE(update.length);
|
|
fs.appendFileSync(this.logPath, Buffer.concat([head, update]));
|
|
this.logged++;
|
|
}
|
|
|
|
/** Updates in the log since the last compaction. */
|
|
get pending(): number {
|
|
return this.logged;
|
|
}
|
|
|
|
/** Replaces state and log with one encoded state. */
|
|
compact(state: Uint8Array): void {
|
|
const tmp = `${this.statePath}.tmp`;
|
|
fs.writeFileSync(tmp, state);
|
|
fs.renameSync(tmp, this.statePath);
|
|
fs.rmSync(this.logPath, { force: true });
|
|
this.logged = 0;
|
|
}
|
|
|
|
remove(): void {
|
|
fs.rmSync(this.statePath, { force: true });
|
|
fs.rmSync(this.logPath, { force: true });
|
|
}
|
|
}
|