55 lines
3.3 KiB
TypeScript
55 lines
3.3 KiB
TypeScript
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<ReturnType<typeof openProjectEngine>>;
|
|
export type WorkspaceOptions = {
|
|
accountId: Id; workspaceId: Id; authEpoch: number; registry: Registry; storage: StoragePort;
|
|
isCurrentIdentity(): boolean;
|
|
adapters(projectId: Id, current: () => boolean): Pick<EngineOptions, 'persistence' | 'assets' | 'runs'>;
|
|
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<Id, Promise<ProjectEngine>>();
|
|
let accepting = true, closed = false, closing: Promise<void> | 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<Id, number>) {
|
|
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<typeof createWorkspaceRuntime>;
|