Allow durable imports and deep copies in certified loading scopes

This commit is contained in:
2026-11-09 13:39:39 +08:00
parent 945aaea504
commit a66933775e
23 changed files with 234 additions and 62 deletions
+3 -3
View File
@@ -10,7 +10,7 @@ const counts=new WeakMap<Document,ReadonlyMap<Id,number>>();
function membership(document:Document){let result=counts.get(document);if(!result){const next=new Map<Id,number>();for(const element of document.elements.values())next.set(element.ownerCanvasId,(next.get(element.ownerCanvasId)??0)+1);result=next;counts.set(document,result);}return result;}
export function sourceClosed(state:LoadingState,source:Capture){return source.document.elements.size===state.totalElements&&source.document.canvases.size===state.declaredCanvasIds.length;}
export function ownerReady(state:LoadingState,source:Capture,document:Document,id:Id){return document.canvases.has(id)&&(!source.versions.has(id)||state.version!==1&&(membership(source.document).get(id)??0)===state.canvasElementCounts[id]);}
function born(state:LoadingState,id:Id){return state.version===4&&!Object.hasOwn(state.elementOwners,id);}
function born(state:LoadingState,id:Id){return (state.version===4||state.version===5)&&!Object.hasOwn(state.elementOwners,id);}
function safeData(data:ElementRecord['data'],registry:Registry,component:Id,document:Document,source?:Document,prior?:ElementRecord['data']){
const definition=registry.components.get(component);if(!definition?.copy)return false;
for(const key of ['ai','taskId','runId'])if(data[key]!==undefined&&data[key]!==null&&!equal(data[key],prior?.[key]))return false;
@@ -21,7 +21,7 @@ function safeData(data:ElementRecord['data'],registry:Registry,component:Id,docu
}
function references(document:Document,registry:Registry){for(const element of document.elements.values())if(registry.resolve(element)==='known'){const schema=registry.components.get(element.componentId)?.copy;for(const reference of schema?.references??[])for(const id of copyReferenceIds(element.data,reference))if(!(reference.target==='element'?document.elements:document.canvases).has(id))throw fault('loading_reference_incomplete',[element.id,id]);}}
export function structuralCommand(command:Command,document:Document,registry:Registry,state:LoadingState,source:Capture):Availability|undefined {
if(state.version!==4)return;
if(state.version<4)return;
const deny:Availability={enabled:false,code:'model_loading',targets:[]},yes:Availability={enabled:true};
if(command.type==='element.create'){const element=command.element;return born(state,element.id)&&!source.document.tombstones.has(element.id)&&ownerReady(state,source,document,element.ownerCanvasId)&&registry.resolve(element)==='known'&&safeData(element.data,registry,element.componentId,document,source.document)?yes:deny;}
if(command.type==='canvas.create'){const canvas=command.canvas,parent=canvas.parent;return !source.versions.has(canvas.id)&&!!parent&&born(state,parent.entryElementId)&&ownerReady(state,source,document,parent.canvasId)?yes:deny;}
@@ -32,7 +32,7 @@ export function structuralCommand(command:Command,document:Document,registry:Reg
}
/** Actual changes, not a command name, establish the mutation's required closure. */
export function assertStructuralChanges(changes:readonly Change[],document:Document,registry:Registry,state:LoadingState,source:Capture):Document {
if(state.version!==4)throw fault('model_loading');
if(state.version<4)throw fault('model_loading');
let cursor=document;const closed=sourceClosed(state,source);
for(const change of changes){
if(change.kind==='canvas'){