import type { Document, Id, Registry } from '../canvas-core/types.ts'; import { fault, readMap } from '../canvas-core/values.ts'; import { openProjectEngine, type EngineOptions } from './engine.ts'; import type { StoragePort } from './storage.ts'; export type ProjectEngine = Awaited>; export type WorkspaceOptions = { accountId: Id; workspaceId: Id; authEpoch: number; registry: Registry; storage: StoragePort; isCurrentIdentity(): boolean; adapters(projectId: Id, current: () => boolean): Pick; onError?: (error: unknown) => void; }; /** Workspace owns persistent projects; a view only borrows one and never closes it. */ export function createWorkspaceRuntime(options: WorkspaceOptions) { const projects = new Map>(); let accepting = true, closed = false, closing: Promise | undefined; const current = () => accepting && !closed && options.isCurrentIdentity(); return Object.freeze({ accountId: options.accountId, workspaceId: options.workspaceId, authEpoch: options.authEpoch, registry: options.registry, isCurrentIdentity: current, getProjects: () => readMap(projects), open(document: Document, versions: ReadonlyMap) { if (!current()) return Promise.reject(fault('identity_changed')); const existing = projects.get(document.projectId); if (existing) return existing.then(engine => { if (!current()) throw fault('identity_changed'); engine.refresh(document, versions); return engine; }); const opening = openProjectEngine({ scope: { accountId: options.accountId, workspaceId: options.workspaceId, projectId: document.projectId }, registry: options.registry, document, versions, storage: options.storage, ...options.adapters(document.projectId, current), onError: options.onError, traversalPolicy: () => current() ? { enabled: true } : { enabled: false, code: 'identity_changed', targets: [] }, policy: () => current() ? { enabled: true } : { enabled: false, code: 'identity_changed', targets: [] }, }); projects.set(document.projectId, opening); void opening.catch(() => { if (projects.get(document.projectId) === opening) projects.delete(document.projectId); }); return opening.then(engine => { if (!current()) throw fault('identity_changed'); engine.refresh(document, versions); return engine; }); }, close() { accepting = false; // Fence commands/transports before awaiting pending opens or storage. if (closed) return Promise.resolve(); if (closing) return closing; const task = (async () => { const results = await Promise.allSettled([...projects.values()].map(async pending => { let engine: ProjectEngine; try { engine = await pending; } catch { return; } await engine.close(); })); const errors = results.flatMap(result => result.status === 'rejected' ? [result.reason] : []); if (errors.length) throw new AggregateError(errors, 'workspace_close_failed'); options.storage.close(); projects.clear(); closed = true; })(); closing = task.finally(() => { closing = undefined; }); return closing; }, }); } export type WorkspaceRuntime = ReturnType;