import {createModelPreview,type LiveModelPreview} from './model-preview.ts'; import type { Id, Registry } from '../canvas-core/types.ts'; import { fault, readMap } from '../canvas-core/values.ts'; import type { WorkspaceRuntime } from './workspace.ts'; import { createScopedLoader, type LoadPort, type ScopedLoader } from './loader.ts'; import type { CanonicalReadUpdate } from './contracts.ts'; /** Host lifetime matches the signed-in application, independently of navigation. */ export function createRuntimeHost(options: { registry: Registry; workspace(accountId: Id, workspaceId: Id, authEpoch: number, current: () => boolean): WorkspaceRuntime; modelLoader?(accountId:Id,workspaceId:Id,authEpoch:number,current:()=>boolean):LoadPort; onError?: (error: unknown) => void; onObserverError?: (error: unknown) => void }) { let accountId: Id | null = null, authEpoch = 0, disposed = false; const workspaces = new Map(), retired = new Set(); const readers = new Map>(); const closeRetired = async () => { const results = await Promise.allSettled([...retired].map(async runtime => { await runtime.close(); retired.delete(runtime); })); const errors = results.flatMap(result => result.status === 'rejected' ? [result.reason] : []); if (errors.length) throw new AggregateError(errors, 'host_close_failed'); }; function retire() { authEpoch++; accountId = null; for (const reader of readers.values()) reader.close();readers.clear();for (const runtime of workspaces.values()) retired.add(runtime); workspaces.clear(); } function activate(account: Id) { if (disposed) throw fault('host_disposed'); if (accountId !== account) { retire(); accountId = account; void closeRetired().catch(error => { try { options.onError?.(error); } catch { /* Reporting cannot change scope. */ } }); } } return Object.freeze({ registry: options.registry, bindAccount(account: Id) { activate(account); const epoch = authEpoch; return () => !disposed && accountId === account && authEpoch === epoch; }, identity: () => Object.freeze({ accountId, authEpoch }), getWorkspaces: () => readMap(workspaces), async loadModel(account:Id,workspace:Id,canvas:Id,settings:{signal?:AbortSignal;navigationGeneration?:number;onPreview?:(preview:LiveModelPreview)=>void}={}) { activate(account); const readEpoch=authEpoch; if(!options.modelLoader)throw fault('model_loader_unavailable'); let reader=readers.get(workspace); if(!reader){const epoch=authEpoch,current=()=>!disposed&&accountId===account&&authEpoch===epoch;reader=createScopedLoader({load:options.modelLoader(account,workspace,epoch,current),current:()=>current(),maxCacheEntries:0});readers.set(workspace,reader);} const preview=settings.onPreview?createModelPreview(options.registry):undefined;let seen=0,previewFailed=false;let declaredCanvasIds:readonly Id[]=[]; const onUpdate=(state:import('./loader.ts').LoadSnapshot)=>{ if(!preview||previewFailed||disposed||accountId!==account||authEpoch!==readEpoch||settings.signal?.aborted)return; try{for(;seencanvas.id));settings.onPreview?.(Object.freeze({...value,versions:readMap(new Map(Object.entries(chunk.serverVersions))),loading:Object.freeze({...(value.canvasElementCounts?value.elementOwners?{version:6 as const,canvasElementCounts:value.canvasElementCounts,elementOwners:value.elementOwners,sourceCanvases:value.sourceCanvases!}:{version:3 as const,canvasElementCounts:value.canvasElementCounts}:{version:1 as const}),sourceToken:chunk.snapshotToken,declaredCanvasIds,receivedElements:value.receivedElements,totalElements:value.totalElements})}));}}} catch(error){previewFailed=true;try{options.onObserverError?.(error);}catch{}} }; const handle=reader.acquire({accountId:account,workspaceId:workspace,projectId:null,canvasId:canvas,viewKey:'canonical-project',format:'canonical-model-v1',queryFingerprint:'full-project',source:'foundry',authEpoch,navigationGeneration:settings.navigationGeneration??0},{signal:settings.signal,onUpdate}); try{const snapshot=await handle.ready;if(disposed||accountId!==account||authEpoch!==readEpoch)throw fault('identity_changed');const last=snapshot.chunks.at(-1);if(!last||!last.complete||last.value.status==='receiving')throw fault('incomplete_load');return last.value.status==='ready'?Object.freeze({...last.value,sourceToken:last.snapshotToken}):last.value;}finally{handle.release();preview?.close();} }, workspace(account: Id, workspace: Id) { activate(account); let runtime = workspaces.get(workspace); if (!runtime) { const epoch = authEpoch; runtime = options.workspace(account, workspace, epoch, () => !disposed && accountId === account && authEpoch === epoch); workspaces.set(workspace, runtime); } return runtime; }, retireIdentity() { retire(); return closeRetired(); }, close() { disposed = true; retire(); return closeRetired(); }, }); } export type RuntimeHost = ReturnType;