feat(canvas): add bounded resumable import coordination
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import type { Document, ElementRecord, Id, Json, Registry } from '../canvas-core/types.ts';
|
||||
import { remapCopyData } from '../canvas-core/duplicate.ts';
|
||||
import { clone, fault } from '../canvas-core/values.ts';
|
||||
import type { ImportJob, ImportLine, ImportUnit } from './imports.ts';
|
||||
|
||||
export function assertNativeImportTarget(job:ImportJob,document:Document){
|
||||
const source=job.source,canvas=source?document.canvases.get(source.targetCanvasId):undefined;
|
||||
if(!source||!canvas)throw fault('import_target_missing');
|
||||
if(source.targetEntry?(canvas.parent?.entryElementId!==source.targetEntry.id||document.elements.get(source.targetEntry.id)?.incarnation!==source.targetEntry.incarnation):canvas.parent!==null)throw fault('import_target_changed');
|
||||
return source;
|
||||
}
|
||||
export function assertNativeImportDependencies(job:ImportJob,document:Document,dependencies:readonly string[]){
|
||||
for(const key of dependencies)if(job.committedSourceKeys.includes(key)){
|
||||
const id=job.stableIds[key],binding=job.bindings?.[id];
|
||||
if(!binding)throw fault('import_dependency_changed');
|
||||
if(binding.kind==='element'){if(document.elements.get(id)?.incarnation!==binding.incarnation)throw fault('import_dependency_changed',[id]);}
|
||||
else{const canvas=document.canvases.get(id),entry=binding.entryElementId,entryBinding=entry?job.bindings?.[entry]:undefined;if(!canvas||canvas.parent?.entryElementId!==entry||entry&&(entryBinding?.kind!=='element'||document.elements.get(entry)?.incarnation!==entryBinding.incarnation))throw fault('import_dependency_changed',[id]);}
|
||||
}
|
||||
}
|
||||
|
||||
/** Native NDJSON records use source IDs; $target names the explicitly chosen existing canvas. */
|
||||
export function prepareNativeImport(line:ImportLine,job:ImportJob,reserve:(key:string)=>Id,document:Document,registry:Registry):ImportUnit {
|
||||
const source=assertNativeImportTarget(job,document);
|
||||
const raw=line.value as Record<string,Json>|undefined;
|
||||
if(!raw||Array.isArray(raw)||!['element','canvas'].includes(raw.kind as string)||typeof raw.key!=='string'||!raw.key||raw.key==='$target'||!raw.record||typeof raw.record!=='object'||Array.isArray(raw.record))throw fault('invalid_native_import_record');
|
||||
const record=clone(raw.record) as Record<string,Json>;if(record.id!==raw.key)throw fault('import_source_identity_mismatch');
|
||||
const dependencies=new Set<string>(),elementIds=new Map<Id,Id>(),canvasIds=new Map<Id,Id>();
|
||||
const element=(id:Json):Id=>{if(typeof id!=='string'||!id)throw fault('invalid_import_reference');const key=`element:${id}`;dependencies.add(key);const mapped=reserve(key);elementIds.set(id,mapped);return mapped;};
|
||||
const owner=(id:Json):Id=>{if(id==='$target'){canvasIds.set('$target',source.targetCanvasId);return source.targetCanvasId;}if(typeof id!=='string'||!id)throw fault('invalid_import_reference');const key=`canvas:${id}`;dependencies.add(key);const mapped=reserve(key);canvasIds.set(id,mapped);return mapped;};
|
||||
const key=`${raw.kind}:${raw.key}`,id=reserve(key);
|
||||
if(raw.kind==='canvas'){
|
||||
const parent=record.parent as Record<string,Json>|null;
|
||||
if(!parent||typeof parent!=='object'||Array.isArray(parent))throw fault('invalid_import_canvas');
|
||||
const after={...record,id,parent:{...parent,canvasId:owner(parent.canvasId),entryElementId:element(parent.entryElementId)}};
|
||||
dependencies.delete(key);return{key,dependencies:[...dependencies],changes:[{kind:'canvas',id,before:null,after:after as never}]};
|
||||
}
|
||||
const original=record as unknown as ElementRecord,definition=registry.components.get(original.componentId);
|
||||
if(registry.resolve(original)!=='known'||!definition?.copy||!original.data||typeof original.data!=='object'||Array.isArray(original.data))throw fault('import_component_unavailable');
|
||||
for(const ref of definition.copy.references){
|
||||
let value:Json|undefined=original.data;for(const part of ref.path){if(!value||typeof value!=='object'||!Object.hasOwn(value,part)){value=undefined;break;}value=(value as Record<string,Json>)[part];}
|
||||
if(value===undefined||value===null)continue;
|
||||
if(ref.many&&!Array.isArray(value))throw fault('invalid_import_reference');
|
||||
for(const item of ref.many?value as Json[]:[value]){if(typeof item!=='string'||!item)throw fault('invalid_import_reference');if(ref.target==='element')element(item);else owner(item);}
|
||||
}
|
||||
const after={...record,id,incarnation:1,ownerCanvasId:owner(original.ownerCanvasId),data:remapCopyData(original,registry,elementIds,canvasIds,document)};
|
||||
if(original.structure==='connection'){
|
||||
if(!original.source||!original.target)throw fault('invalid_import_endpoint');
|
||||
Object.assign(after,{source:{...original.source,elementId:element(original.source.elementId)},target:{...original.target,elementId:element(original.target.elementId)}});
|
||||
}else if(original.structure!=='node')throw fault('invalid_import_structure');
|
||||
dependencies.delete(key);return{key,dependencies:[...dependencies],changes:[{kind:'element',id,before:null,after:after as ElementRecord}]};
|
||||
}
|
||||
Reference in New Issue
Block a user