53 lines
6.2 KiB
TypeScript
53 lines
6.2 KiB
TypeScript
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<Document,{members:Map<Id,Id[]>;children:Map<Id,Id[]>}>();
|
|
function index(document:Document){let value=relations.get(document);if(!value){const members=new Map<Id,Id[]>(),children=new Map<Id,Id[]>();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<Id>([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<Id>(),canvases=new Map<Id,CanvasRecord>(document.canvases);
|
|
// Missing captured children cannot be mistaken for an empty/leaf selection.
|
|
const missingChildren=new Map<Id,Id[]>();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<Id>();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]);
|
|
}
|
|
}
|