115 lines
11 KiB
TypeScript
115 lines
11 KiB
TypeScript
/**
|
||
* 已映射复制记录与导入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<Change,{kind:'field'}>; dependencies: string[]; externalElements: ExternalElement[]; externalCanvases: ExternalCanvas[]};
|
||
|
||
/** Internal source: already mapped records, with explicit guards for every retained external reference. */
|
||
/** 完整计划兼容路径:先同步建立新 ID→源 key Map,再逐 change 编码 NDJSON,保留所有字符串直到生成 Blob。
|
||
* 单条含换行的 UTF-8 字节须 ≤256KiB,全源 ≤64MiB;这些是编码成本限制,不是进程峰值内存保证。
|
||
* 每条完成后按约每 KiB 一个工作单位调用 copy budget;单次映射建表/JSON.stringify 不被抢占。
|
||
* 成功前复查取消,不输出部分 Blob;不验证整个 plan 的 ID 唯一性/图合法性或计算源摘要。 */
|
||
export async function encodeDuplicateSource(document: Document, registry: Registry, plan: DuplicatePlan, signal: AbortSignal, scheduling:ImportScheduling={}): Promise<Blob> {
|
||
const keys = new Map<Id,string>(),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'});
|
||
}
|
||
|
||
/** 将一条已映射创建变化封装成 CopyLine:keys 区分副本内部依赖与仍引用原图的外部身份。
|
||
* 收集 owner/端点/声明数据引用及外部 canvas 父链入口,去掉自身依赖;不重新映射字段或分配 ID。
|
||
* 返回的 change 沿用输入引用,数组/guard 未整体冻结;输入文档/映射须来自已验证的不可变计划。
|
||
* 这里只收集已知 copy.references,不独立调用 resolve 验组件版本,也不替代最终整图校验。 */
|
||
export function duplicateImportLine(document:Document,registry:Registry,change:Exclude<Change,{kind:'field'}>,keys:ReadonlyMap<Id,string>):CopyLine {
|
||
if(!change.after||change.before!==null)throw fault('invalid_duplicate_plan');
|
||
const dependencies=new Set<string>(),elements=new Map<Id,ExternalElement>(),canvases=new Map<Id,ExternalCanvas>();
|
||
/** keys 命中即登记内部依赖;否则沿原 canvas 父链收集 parent 与存在的入口代次。
|
||
* canvases Map 同时去重/止循环,不在这里证明父链无环或每个入口都存在,依赖合法源文档合同。 */
|
||
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()]};
|
||
}
|
||
|
||
/** 旧持久源的基础封装校验:允许源种类、JSON 值域、数组形状、创建 ID 与 stableIds/key 前缀对应。
|
||
* element 还要求 incarnation=1;不逐字段验证 guard/dependency schema,也不证明外部 guard 集合完整。
|
||
* 返回原行引用,不捕获/修复源,不重新分配 ID;完整源绑定、变化及图校验仍由 runner/发布边界承担。 */
|
||
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. */
|
||
/** 对 element 的当前 copy.references 重验:内部映射(自引用除外)须列依赖,外部须 preserve 且列 guard ID。
|
||
* 每个引用线性扫描 stableIds 找对应命名空间;这里只查声明数据引用,不重建 owner/端点的完整 guard 闭包。
|
||
* canvas 直接跳过;此步骤不是全部 CopyLine schema 校验或外部对象当前生命周期校验。 */
|
||
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]);
|
||
}
|
||
}
|
||
/** 先校验预映射行/声明引用,再用 reserve 核对自身 ID,要求依赖键都在 job.stableIds 中。
|
||
* reserve 由调用方提供,本函数不回滚它的副作用;合法已准备源应命中现有映射而非再分配身份。
|
||
* dependencies/change 沿用原行引用,guard 用 JSON 克隆捕获;不检查依赖已提交,也不发布/持久化内容。 */
|
||
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. */
|
||
/** 同步提交前重新解释行与声明引用,再逐个核对提供的外部 element 代次/owner、canvas parent/入口代次。
|
||
* 内容/样式可变化而通过;组件版本等未在此额外比较,guard 是否完整依赖受信源生成与恢复校验。
|
||
* 不锁住对象或发布 changes;应与真正提交处于同一同步边界,中间 await 会让该检查过期。 */
|
||
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]);}
|
||
}
|