/** * SidecarRouter — the per-document persistence façade the three authoring * controllers depend on (F8 spec §6.2/§6.4). Implements SidecarStore and OWNS * the routing: it computes the single document key (keyOf) and dispatches * load/save/update/consumeSelfWrite/sidecarPath to the right implementation: * - an in-workspace `file:` doc → CoauthorStore, keyed by its repo-relative * path → `.threads/.json` (committable, INV-2; the only home F5's * cross-rung ever sees). * - an out-of-workspace `file:` or `untitled:` doc → GlobalSidecarStore, keyed * by its URI string → global storage (INV-19/24/25; untitled in-memory). * The membership predicate is `#24`'s isUnderRoot — here a ROUTING input, never * an authoring gate (that became isAuthorable). vscode-free (takes the extracted * identity, not a vscode.TextDocument) so it unit-tests. */ import * as path from "node:path"; import { isUnderRoot } from "./workspacePath"; import type { Artifact } from "./model"; import type { SidecarStore } from "./sidecarStore"; /** The minimal document identity the router routes on (extracted from a TextDocument). */ export interface DocIdentity { /** `document.uri.toString()` — the URI string. */ uri: string; /** `document.uri.fsPath`. */ fsPath: string; /** `document.uri.scheme`. */ scheme: string; } /** A SidecarStore that can also report a sidecar's on-disk path (CoauthorStore, GlobalSidecarStore). */ type LocatableStore = SidecarStore & { sidecarPath(key: string): string | undefined }; /** Extract the routing identity from anything URI-shaped (a real vscode.TextDocument fits). */ export function docIdentity(doc: { uri: { toString(): string; fsPath: string; scheme: string } }): DocIdentity { return { uri: doc.uri.toString(), fsPath: doc.uri.fsPath, scheme: doc.uri.scheme }; } export class SidecarRouter implements SidecarStore { constructor( private readonly repo: LocatableStore | null, private readonly global: LocatableStore, private readonly root: string | undefined, ) {} /** * The single document key: an in-workspace `file:` doc → its repo-relative * path (cross-rung-meaningful, the existing sidecar key, INV-2); everything * else → its URI string (machine-local, self-consistent — INV-25). */ keyOf(id: DocIdentity): string { if (id.scheme === "file" && this.root !== undefined && isUnderRoot(id.fsPath, this.root)) { return path.relative(this.root, id.fsPath); } return id.uri; } /** A key is a global (URI-string) key iff it carries a URI scheme prefix. */ private isGlobalKey(key: string): boolean { return key.startsWith("file://") || key.startsWith("untitled:"); } private storeFor(key: string): LocatableStore { if (this.isGlobalKey(key) || this.repo === null) return this.global; return this.repo; } load(key: string): Artifact | null { return this.storeFor(key).load(key); } save(key: string, artifact: Artifact): void { this.storeFor(key).save(key, artifact); } update(key: string, mutate: (artifact: Artifact) => void): Artifact { return this.storeFor(key).update(key, mutate); } /** Only the repo store self-writes (the `.threads/` watcher); global is a no-op. */ consumeSelfWrite(fsPath: string): boolean { return this.repo?.consumeSelfWrite(fsPath) ?? false; } /** * The sidecar's on-disk path: `.threads/.json` for a repo key, the hashed * global path for an out-of-folder `file:` key, undefined for untitled. Used by * the controllers' external-change handler (only repo sidecars are watched) and * by the E2E to assert the storage home. */ sidecarPath(key: string): string | undefined { return this.storeFor(key).sidecarPath(key); } }