Negotiate and decode snapshot-bound canonical model streams

This commit is contained in:
2026-11-09 02:06:07 +08:00
parent 0205f24904
commit a92ae72c8b
9 changed files with 172 additions and 15 deletions
+4 -4
View File
@@ -2,13 +2,13 @@ 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 { CanonicalReadResult } from './contracts.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<CanonicalReadResult>; onError?: (error: unknown) => void }) {
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<CanonicalReadUpdate>; onError?: (error: unknown) => void }) {
let accountId: Id | null = null, authEpoch = 0, disposed = false;
const workspaces = new Map<Id, WorkspaceRuntime>(), retired = new Set<WorkspaceRuntime>();
const readers = new Map<Id, ScopedLoader<CanonicalReadResult>>();
const readers = new Map<Id, ScopedLoader<CanonicalReadUpdate>>();
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] : []);
@@ -34,7 +34,7 @@ export function createRuntimeHost(options: { registry: Registry; workspace(accou
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 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});
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)throw fault('incomplete_load');return last.value;}finally{handle.release();}
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;}finally{handle.release();}
},
workspace(account: Id, workspace: Id) {
activate(account);