import type {CanvasRecord,Document,Id,Target} from '../canvas-core/types.ts'; import {fault,freeze,equal} from '../canvas-core/values.ts'; import type {EngineSnapshot} from './contracts.ts'; import {validateImportJob,type LoadingImportAdmission,type ImportJob} from './imports.ts'; import {applyChanges} from '../canvas-core/document.ts'; import {copyTopology} from './copy-snapshot-index.ts'; import {structuralRequestScope} from './loading.ts'; import {ownerReady} from './loading-structure.ts'; const relations=new WeakMap;children:Map}>(); function index(document:Document){let value=relations.get(document);if(!value){const members=new Map(),children=new Map();for(const element of document.elements.values()){const ids=members.get(element.ownerCanvasId)??[];ids.push(element.id);members.set(element.ownerCanvasId,ids);}for(const canvas of document.canvases.values())if(canvas.parent){const ids=children.get(canvas.parent.entryElementId)??[];ids.push(canvas.id);children.set(canvas.parent.entryElementId,ids);}value={members,children};relations.set(document,value);}return value;} /** The caller captures this proof with the same document that becomes source bytes. */ export function loadingImportAdmission(snapshot:EngineSnapshot,target:Id,selection?:readonly Target[]):LoadingImportAdmission|undefined { const state=snapshot.runtime.loading;if(!state)return; if((state.version!==5&&state.version!==6)||!snapshot.runtime.loadingRead)throw fault('model_loading'); const source=snapshot.runtime.loadingRead,document=snapshot.document,required=new Set([target]); const requireScope=(id:Id)=>{required.add(id);if(!ownerReady(state,source,document,id))throw fault('loading_scope_incomplete',[id]);};requireScope(target); if(selection){ if(!selection.length)throw fault('empty_copy_selection');const tables=index(document),pending=selection.map(target=>{const element=document.elements.get(target.id);if(!element||element.incarnation!==target.incarnation)throw fault('stale_incarnation',[target.id]);return target.id;}),copied=new Set(),canvases=new Map(document.canvases); // Missing captured children cannot be mistaken for an empty/leaf selection. const missingChildren=new Map();for(const canvas of state.sourceCanvases)if(canvas.parent&&!source.document.canvases.has(canvas.id)&&!document.canvases.has(canvas.id)){canvases.set(canvas.id,canvas);const ids=missingChildren.get(canvas.parent.entryElementId)??[];ids.push(canvas.id);missingChildren.set(canvas.parent.entryElementId,ids);} while(pending.length){const id=pending.pop()!;if(copied.has(id))continue;copied.add(id);const element=document.elements.get(id);if(!element)throw fault('loading_scope_incomplete',[id]);requireScope(element.ownerCanvasId); for(const child of [...(tables.children.get(id)??[]),...(missingChildren.get(id)??[])]){requireScope(child);pending.push(...(tables.members.get(child)??[]));} } for(const id of copied){const element=document.elements.get(id)!;if(Object.hasOwn(state.elementOwners,id)){ let canvas=canvases.get(element.ownerCanvasId);const seen=new Set();while(canvas){if(seen.has(canvas.id))throw fault('invalid_model_topology');seen.add(canvas.id);requireScope(canvas.id);canvas=canvas.parent?canvases.get(canvas.parent.canvasId):undefined;} }} // Edges involving born objects are entirely local to this captured namespace. for(const element of document.elements.values())if(element.structure==='connection'&&copied.has(element.target.elementId)&&!copied.has(element.source.elementId))requireScope(element.ownerCanvasId); } let operationSequence=0;for(const record of snapshot.runtime.ledger.operations.values())operationSequence=Math.max(operationSequence,record.intent.sequence); return freeze({version:1 as const,sourceToken:state.sourceToken,operationSequence,requiredCanvasIds:[...required].sort()}); } export function validateLoadingImportAdmissions(runtime:EngineSnapshot['runtime']):void { const state=runtime.loading;if(!state||(state.version!==5&&state.version!==6))return; let maxSequence=0;for(const record of runtime.ledger.operations.values())maxSequence=Math.max(maxSequence,record.intent.sequence); for(const job of runtime.imports?.values()??[]){validateImportJob(job);if(job.projectId!==runtime.acknowledged.projectId)throw fault('invalid_loading_import',[job.id]);const proof=job.source?.loadingAdmission;if(!proof||proof.version!==1||proof.sourceToken!==state.sourceToken||proof.operationSequence>maxSequence||!proof.requiredCanvasIds.includes(job.source!.targetCanvasId)||proof.requiredCanvasIds.some(id=>!structuralRequestScope(runtime,runtime.loadingRead!,id)))throw fault('invalid_loading_import',[job.id]);} } /** Revalidate the frozen copy topology before planning or resuming its source. * Later source chunks may add unrelated owners, so compare only the closed cut. */ export function validateLoadingCopySource(snapshot:EngineSnapshot,job:ImportJob,document:Document):void { const state=snapshot.runtime.loading,proof=job.source?.loadingAdmission;if(!state||(state.version!==5&&state.version!==6)||!proof)return; const source=snapshot.runtime.loadingRead!;let expected=source.document; for(const record of [...snapshot.runtime.ledger.operations.values()].sort((a,b)=>a.intent.sequence-b.intent.sequence))if(record.intent.sequence<=proof.operationSequence)expected=applyChanges(expected,record.intent.changes,false); const selection=job.preparation?.selection??job.source?.plan?.selection;if(!selection)throw fault('invalid_loading_import',[job.id]); const required=loadingImportAdmission({...snapshot,document:expected},job.source!.targetCanvasId,selection)!; if(!equal(required.requiredCanvasIds,proof.requiredCanvasIds))throw fault('loading_copy_scope_changed',[job.id]); for(const id of proof.requiredCanvasIds){if(!equal(expected.canvases.get(id),document.canvases.get(id)))throw fault('loading_copy_scope_changed',[id]); const wanted=Object.fromEntries([...expected.elements.values()].filter(element=>element.ownerCanvasId===id).map(element=>[element.id,copyTopology(element)])),actual=Object.fromEntries([...document.elements.values()].filter(element=>element.ownerCanvasId===id).map(element=>[element.id,copyTopology(element)]));if(!equal(wanted,actual))throw fault('loading_copy_scope_changed',[id]); } }