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
+3 -1
View File
@@ -1,6 +1,6 @@
import type {OrphanFile} from './orphan-files.ts';
import type { ImportJob } from './imports.ts';
import type { Document, Id, Json, OperationIntent, Snapshot } from '../canvas-core/types.ts';
import type { CanvasRecord, ElementRecord, Document, Id, Json, OperationIntent, Snapshot } from '../canvas-core/types.ts';
import type { LedgerSnapshot, FrozenRequest } from './ledger.ts';
import type { CompiledRequest, OperationStatus } from './scheduler.ts';
import type { RunRecord } from './runs.ts';
@@ -22,6 +22,8 @@ export type RuntimeState = {
};
export type EngineSnapshot = Snapshot<RuntimeState>;
export type CanonicalProposal = { readonly document: Document; readonly versions: ReadonlyMap<Id, number> };
/** Receiving records are read deltas, not a validated Document or an ACK base. */
export type CanonicalReadUpdate = CanonicalReadResult | { readonly status:'receiving'; readonly projectId:Id; readonly canvases:readonly CanvasRecord[]; readonly elements:readonly ElementRecord[]; readonly tombstones:ReadonlyMap<Id,number>; readonly versions:ReadonlyMap<Id,number>; readonly receivedElements:number; readonly totalElements:number };
export type CanonicalReadResult = ({ readonly status: 'ready' } & CanonicalProposal) | { readonly status: 'migration-required'; readonly rootId: Id; readonly sourceVersions: ReadonlyMap<Id, number> };
/** A validated historical execution receipt; its old model is never published over a fresh read. */
export type AppliedModelProof = { readonly versions:ReadonlyMap<Id,number>; assertCanonical(proposal:CanonicalProposal):void };
+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);