0.10.0: Archify Sync, optional live collaboration on a model
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.
This commit is contained in:
1 parent
a9021312b0
commit
c99f3e5fe8
27 files changed
+2899
-14
No files matched your search
@@ -0,0 +1,68 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user