78 lines
8.2 KiB
TypeScript
78 lines
8.2 KiB
TypeScript
/**
|
||
* 原生导入记录到纯ImportUnit的转换:源ID经reserve映射为新身份,依赖保留为source key等待闭合。
|
||
* $target或文档源根映射到显式选定的现有canvas;副本incarnation从1开始,不继承旧墓碑。
|
||
* 任务字段只按组件copy.omit声明去除;转换不创建/启动生成任务,不能据此假定所有未知执行字段都已过滤。
|
||
* 本模块只规划changes,不写文档、不上传/授权;资源地址和授权事实由宿主/engine另行提供与校验。
|
||
*/
|
||
import type { Document, ElementRecord, Id, Json, Registry } from '../canvas-core/types.ts';
|
||
import { copyReferenceIds } from '../canvas-core/copy-paths.ts';
|
||
import { remapCopyData } from '../canvas-core/duplicate.ts';
|
||
import { clone, fault } from '../canvas-core/values.ts';
|
||
import type { ImportJob, ImportLine, ImportUnit } from './imports.ts';
|
||
|
||
/** 按 source.targetCanvasId 定位当前 canvas;有 targetEntry 时核对入口 ID 与元素代次,否则要求根。
|
||
* 不比较整个祖先链/父 canvas、入口组件或 job.projectId;同入口移动后的目标仍可通过此局部身份检查。
|
||
* 返回原 source 引用,不生成新任务或锁住目标,后续提交前还需再检查当前文档。 */
|
||
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;
|
||
}
|
||
/** 仅检查已在 committedSourceKeys 的依赖:stableIds 定位目标,再以 job.bindings 核对生命周期。
|
||
* element 检查代次,canvas 检查仍存在、入口 ID 及已记录入口代次;不比较正文、几何、组件或全部 owner 链。
|
||
* 未提交/SCC 内部依赖不在这里猜成存在,留给分组选取与原子创建后的文档校验;本函数不改变绑定。 */
|
||
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. */
|
||
/** 校验封装/源 ID 后克隆 record,reserve 自身与引用键,生成一条 before=null 的创建变化和依赖列表。
|
||
* reserve 可能先于后续类型校验调用;本函数不回滚自定义 reserve 的副作用,runner 应使用私有捕获上下文。
|
||
* registry.resolve 只确认已知类型/版本与 copy 声明,不等同完整数据/几何/引用校验;最终提交仍需验证文档。
|
||
* 原几何/其它字段按记录携带,不在此统一转换世界坐标,也不检查所有目标 ID 的文档/墓碑冲突。
|
||
* 返回的 ImportUnit/changes envelope 未整体冻结,交 runner 捕获、计成本、持久化和发布。 */
|
||
export function prepareNativeImport(line:ImportLine,job:ImportJob,reserve:(key:string)=>Id,document:Document,registry:Registry,resourceURL?:(id:Id)=>string):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>();
|
||
/** 元素引用先登记依赖再reserve,可先遇到引用后遇到定义,恢复仍复用同一个新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;};
|
||
/** 根/$target直接重定向目标,子canvas则分配稳定新ID并登记canvas依赖。 */
|
||
const owner=(id:Json):Id=>{if(id==='$target'||source.kind==='native-document'&&id===raw.sourceRootId){canvasIds.set(id as string,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);
|
||
// 只接受带 parent 的子 canvas;源根由读取器排除,再通过 sourceRootId/$target alias 接到现有目标。
|
||
// parent/record 其它属性沿用克隆数据,额外 schema/树约束不会在此分支自动补齐。
|
||
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}]};
|
||
}
|
||
// record是脱离源的副本,original是其类型视图;未知组件/缺复制声明不猜字段含义。
|
||
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');
|
||
// native-document 的非空资源字段要求宿主 resourceURL:值须为非空 ID,assetId 还重建 data.src。
|
||
// callback 只是地址提供者,此处不读取字节/校验权限;其它资源字段不自动改名,NDJSON 也不走此文档专用分支。
|
||
if(source.kind==='native-document')for(const key of definition.resourceFields??[]){const value=original.data[key];if(value===undefined||value===null)continue;if(!resourceURL)throw fault('import_resource_authorization_required');if(typeof value!=='string'||!value)throw fault('invalid_import_reference');if(key==='assetId')record.data={...original.data,src:resourceURL(value)};}
|
||
record.data=original.data;
|
||
// 只遍历schema声明引用,避免重写普通文字里的ID;omit字段由共享remapCopyData处理。
|
||
for(const ref of definition.copy.references)for(const item of copyReferenceIds(original.data,ref)){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)};
|
||
// 连线端点同样进入依赖/映射;不能以源owner或字符串形似就猜端点关系。
|
||
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');
|
||
// Set 保留引用首次遇到的顺序并去重;删除自身依赖避免单节点自引用被误当外部等待。
|
||
// 尚未出现的定义仍有稳定 reserve ID/依赖键,缺失定义由 runner 后续报告,不直接连回源项目。
|
||
dependencies.delete(key);return{key,dependencies:[...dependencies],changes:[{kind:'element',id,before:null,after:after as ElementRecord}]};
|
||
}
|