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 {createDigestPool,type DigestPort} from './digest-pool.ts'; import type { StoragePort } from './storage.ts'; import { createDisplayCache } from './display-cache.ts'; export type ProjectEngine = Awaited>; export type WorkspaceOptions = { accountId: Id; workspaceId: Id; authEpoch: number; registry: Registry; storage: StoragePort; digests?:DigestPort; images?:import('./image-decoder.ts').ImageDecoderPort; queryBudget?:EngineOptions['queryBudget']; 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 digests=options.digests??createDigestPool(); const displays=createDisplayCache({onError:options.onError}); 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, displays, digests, images:options.images, queryBudget:options.queryBudget, ...options.adapters(document.projectId, current), onError: options.onError,isCurrentIdentity:current, 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'); displays.close();digests.close();options.images?.close();options.storage.close(); projects.clear(); closed = true; })(); closing = task.finally(() => { closing = undefined; }); return closing; }, }); } export type WorkspaceRuntime = ReturnType;