/** * 已映射复制记录与导入runner之间的桥:保留新ID,记录副本依赖及仍指向原图的外部身份guard。 * 完整plan可编码NDJSON,lazy source也复用同一逐记录转换;不在此上传图片或启动生成任务。 * 外部guard必须在同步commit边界重新查,不能只信解析时原图还没变。 */ import type { CanvasRecord, Change, Document, Id, Json, Registry } from '../canvas-core/types.ts'; import { copyReferenceIds } from '../canvas-core/copy-paths.ts'; import type { DuplicatePlan } from '../canvas-core/duplicate.ts'; import { clone, equal, fault, validJson } from '../canvas-core/values.ts'; import type { ImportJob, ImportLine, ImportUnit } from './imports.ts'; import {createCopyBudget} from './copy-budget.ts'; import type {ImportScheduling} from './import-budget.ts'; /** 保留外部端点/引用节点的ID、incarnation、owner,允许内容变化但不允许身份被替换。 */ type ExternalElement = {id: Id; incarnation: number; ownerCanvasId: Id}; /** 保留外部拥有链及入口代次,防重挂/删除恢复后把副本接到另一个分区关系。 */ type ExternalCanvas = {id: Id; parent: CanvasRecord['parent']; entryIncarnation?: number}; /** 一条预映射创建change,内部引用用source key依赖,外部引用用明确guard。 */ type CopyLine = {key: string; change: Exclude; dependencies: string[]; externalElements: ExternalElement[]; externalCanvases: ExternalCanvas[]}; /** Internal source: already mapped records, with explicit guards for every retained external reference. */ /** 兼容完整计划路径:逐change生成有限记录NDJSON,单条≤256KiB、全源≤64MiB;仍返回完整Blob。 */ export async function encodeDuplicateSource(document: Document, registry: Registry, plan: DuplicatePlan, signal: AbortSignal, scheduling:ImportScheduling={}): Promise { const keys = new Map(),parts: BlobPart[] = []; for (const [id,mapped] of Object.entries(plan.elementIds)) keys.set(mapped,`element:${id}`); for (const [id,mapped] of Object.entries(plan.canvasIds)) keys.set(mapped,`canvas:${id}`); const budget=createCopyBudget(signal,scheduling);let bytes=0; for (const change of plan.changes) { if(signal.aborted)throw fault('copy_aborted'); if(change.kind==='field'||!change.after||change.before!==null)throw fault('invalid_duplicate_plan'); const line=duplicateImportLine(document,registry,change,keys); const text=JSON.stringify(line)+'\n',size=new TextEncoder().encode(text).byteLength;if(size>256*1024)throw fault('record_too_large');bytes+=size;if(bytes>64*1024*1024)throw fault('invalid_import_source');parts.push(text); await budget.step(Math.max(1,Math.ceil(size/1024))); } if(signal.aborted)throw fault('copy_aborted');return new Blob(parts,{type:'application/x-ndjson'}); } /** 共享逐记录转换器:将新映射ID关联到source key,收集owner/端点/声明引用的必要外部闭包。 */ export function duplicateImportLine(document:Document,registry:Registry,change:Exclude,keys:ReadonlyMap):CopyLine { if(!change.after||change.before!==null)throw fault('invalid_duplicate_plan'); const dependencies=new Set(),elements=new Map(),canvases=new Map(); /** 内部canvas转成依赖;外部canvas沿父链收集guard和入口节点身份,不猜其数据字段。 */ const canvas=(id:Id)=>{ const key=keys.get(id);if(key){dependencies.add(key);return;} let next:Id|undefined=id; while(next&&!canvases.has(next)){ const original=document.canvases.get(next);if(!original)throw fault('copy_external_changed',[next]); const entry=original.parent?document.elements.get(original.parent.entryElementId):undefined; canvases.set(next,{id:next,parent:clone(original.parent),...(entry?{entryIncarnation:entry.incarnation}:{})}); if(entry)elements.set(entry.id,{id:entry.id,incarnation:entry.incarnation,ownerCanvasId:entry.ownerCanvasId}); next=original.parent?.canvasId; } }; /** 内部元素作为依赖,外部元素保留同生命周期与owner,同时补其外部canvas链。 */ const element=(id:Id)=>{const key=keys.get(id);if(key){dependencies.add(key);return;}const original=document.elements.get(id);if(!original)throw fault('copy_external_changed',[id]);elements.set(id,{id,incarnation:original.incarnation,ownerCanvasId:original.ownerCanvasId});canvas(original.ownerCanvasId);}; if(change.kind==='canvas'){if(!change.after.parent)throw fault('invalid_duplicate_plan');canvas(change.after.parent.canvasId);element(change.after.parent.entryElementId);} else { const record=change.after;canvas(record.ownerCanvasId); if(record.structure==='connection'){element(record.source.elementId);element(record.target.elementId);} const schema=registry.components.get(record.componentId)?.copy;if(!schema)throw fault('copy_schema_unavailable'); for(const ref of schema.references)for(const id of copyReferenceIds(record.data,ref)){if(ref.target==='element')element(id);else canvas(id);} } const key=keys.get(change.id);if(!key)throw fault('invalid_duplicate_plan');dependencies.delete(key); return {key,change,dependencies:[...dependencies],externalElements:[...elements.values()],externalCanvases:[...canvases.values()]}; } /** 校验旧durable预映射行与当前job.stableIds一致,元素新incarnation必须为1,不重新分配ID。 */ function copyLine(line:ImportLine,job:ImportJob):CopyLine { const value=line.value as unknown as CopyLine; if(!['duplicate-ndjson','duplicate-plan'].includes(job.source?.kind??'')||!value||!validJson(value)||typeof value.key!=='string'||!Array.isArray(value.dependencies)||!Array.isArray(value.externalElements)||!Array.isArray(value.externalCanvases)||!value.change||value.change.kind!=='element'&&value.change.kind!=='canvas'||value.change.before!==null||!value.change.after||!Object.hasOwn(job.stableIds,value.key)||job.stableIds[value.key]!==value.change.id||value.change.after.id!==value.change.id||!value.key.startsWith(`${value.change.kind}:`))throw fault('invalid_duplicate_source'); if(value.change.kind==='element'&&value.change.after?.incarnation!==1)throw fault('invalid_duplicate_source'); return value; } /** Re-check the current reference contract when decoding old durable prepared sources. */ /** 对当前复制schema重验引用:内部映射须声明依赖,外部引用须explicit preserve且有对应guard。 */ function assertCopyReferences(value:CopyLine,job:ImportJob,registry:Registry){ if(value.change.kind!=='element')return; const record=value.change.after!,schema=registry.components.get(record.componentId)?.copy; if(registry.resolve(record)!=='known'||!schema)throw fault('copy_schema_unavailable'); for(const ref of schema.references)for(const id of copyReferenceIds(record.data,ref)){ const key=Object.keys(job.stableIds).find(key=>job.stableIds[key]===id&&key.startsWith(`${ref.target}:`)); if(key){if(key!==value.key&&!value.dependencies.includes(key))throw fault('invalid_duplicate_source');} else if(ref.external!=='preserve'||!(ref.target==='element'?value.externalElements:value.externalCanvases).some(guard=>guard.id===id))throw fault('copy_external_reference',[record.id,id]); } } /** 形成runner的纯ImportUnit并保留原行作为guard,reserve只核对已有映射,不修改原图。 */ export function prepareDuplicateImport(line:ImportLine,job:ImportJob,reserve:(key:string)=>Id,registry:Registry):ImportUnit { const value=copyLine(line,job);assertCopyReferences(value,job,registry);if(reserve(value.key)!==value.change.id)throw fault('invalid_duplicate_source'); for(const key of value.dependencies)if(!Object.hasOwn(job.stableIds,key))throw fault('invalid_duplicate_source'); return{key:value.key,dependencies:value.dependencies,changes:[value.change],guard:clone(line.value!)}; } /** Check at the synchronous commit boundary, never only when the source was first parsed. */ /** 提交前按当前文档核对外部节点代次/owner、canvas parent/入口代次;不符即拒绝这一单元/组。 */ export function assertDuplicateExternal(line:ImportLine,job:ImportJob,document:Document,registry:Registry){ const value=copyLine(line,job);assertCopyReferences(value,job,registry); for(const guard of value.externalElements){const record=document.elements.get(guard.id);if(!record||record.incarnation!==guard.incarnation||record.ownerCanvasId!==guard.ownerCanvasId)throw fault('copy_external_changed',[guard.id]);} for(const guard of value.externalCanvases){const record=document.canvases.get(guard.id);if(!record||!equal(record.parent,guard.parent)||guard.parent&&document.elements.get(guard.parent.entryElementId)?.incarnation!==guard.entryIncarnation)throw fault('copy_external_changed',[guard.id]);} }