Files
vscode-cowriting-plugin/src/store.ts
T
2026-06-10 10:01:03 -07:00

69 lines
2.8 KiB
TypeScript

/**
* CoauthorStore — load/save the per-document `.threads/` sidecar (spec §6.4).
* Git-native, serverless, plain pretty JSON (INV-2). vscode-free (Node fs only),
* so it is unit-testable; the SHARED FileSystemWatcher that triggers
* re-anchoring on external change is wired in the vscode layer (extension.ts),
* with self-write suppression centralized here because two controllers
* (ThreadController, AttributionController) co-own the sidecar (F3 §6.2).
*/
import * as fs from "node:fs";
import * as path from "node:path";
import { emptyArtifact, serializeArtifact, type Artifact } from "./model";
export class CoauthorStore {
/** sidecar paths this store just wrote, so the shared watcher can ignore them. */
private readonly selfWrites = new Set<string>();
/** @param rootDir absolute workspace-folder root that owns the `.threads/` tree. */
constructor(private readonly rootDir: string) {}
/**
* Delete-and-report: true iff `fsPath` was a sidecar we just wrote via
* `update`. The shared watcher (extension.ts) calls this to suppress
* self-write events before fanning out to the controllers.
*/
consumeSelfWrite(fsPath: string): boolean {
return this.selfWrites.delete(fsPath);
}
/** `.threads/<repo-relative-docPath>.json` (spec §6.3). */
sidecarPath(docPath: string): string {
return path.join(this.rootDir, ".threads", `${docPath}.json`);
}
load(docPath: string): Artifact | null {
const p = this.sidecarPath(docPath);
if (!fs.existsSync(p)) return null;
return JSON.parse(fs.readFileSync(p, "utf8")) as Artifact;
}
save(docPath: string, artifact: Artifact): void {
const p = this.sidecarPath(docPath);
fs.mkdirSync(path.dirname(p), { recursive: true });
fs.writeFileSync(p, serializeArtifact(artifact), "utf8");
}
/**
* Read-modify-write for sidecar co-ownership (spec F3 §6.2): each controller
* mutates only its own section against a FRESH load, so ThreadController and
* AttributionController never clobber each other. After the mutation, anchors
* referenced by neither threads nor attributions are pruned (proposals are
* still empty in F3 — revisit in F4). `mutate` MUST be synchronous: the
* read-modify-write (and the self-write mark) completes within this call.
*/
update(docPath: string, mutate: (artifact: Artifact) => void): Artifact {
const artifact = this.load(docPath) ?? emptyArtifact(docPath);
mutate(artifact);
const referenced = new Set<string>([
...artifact.threads.map((t) => t.anchorId),
...artifact.attributions.map((a) => a.anchorId),
]);
for (const id of Object.keys(artifact.anchors)) {
if (!referenced.has(id)) delete artifact.anchors[id];
}
this.selfWrites.add(this.sidecarPath(docPath));
this.save(docPath, artifact);
return artifact;
}
}