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 }); } }