55 lines
7.0 KiB
TypeScript
55 lines
7.0 KiB
TypeScript
/**
|
||
* 由原SHA绑定快照与稳定emit检查点提供可seek的lazy复制记录流(parserVersion=2)。
|
||
* 没有第二份完整映射Blob;节点/引用/曲线在read某ordinal时才转换,可用readElement从索引源按需解码payload。
|
||
* 这只是ImportSource,真正的记录摘要、依赖等待、提交和失败报告由统一runner负责。
|
||
*/
|
||
import type { Document, ElementRecord, CanvasRecord, Id, Registry, Json } from '../canvas-core/types.ts';
|
||
import type { DuplicateCheckpoint } from '../canvas-core/duplicate-checkpoint.ts';
|
||
import { createDuplicatePlanner } from '../canvas-core/duplicate-checkpoint.ts';
|
||
import { duplicateTreeChange, duplicateEdgeChange } from '../canvas-core/duplicate.ts';
|
||
import { fault } from '../canvas-core/values.ts';
|
||
import { copyRegistrySignature } from './copy-snapshot.ts';
|
||
import { duplicateImportLine } from './import-duplicate.ts';
|
||
import type { ImportSource } from './imports.ts';
|
||
|
||
/** A seekable record cursor over the original SHA-bound snapshot, not a second mapped Blob. */
|
||
/** 要求 emit 起点/零输出计数与当前 registry 签名,调用 planner 复核 checkpoint 结构绑定且禁止再分配 ID。
|
||
* 映射复制为私有 Map,但 document/registry/state 及 readElement 仍沿用调用方引用,须保持不可变/稳定。
|
||
* sourceToken/fingerprint 的真实字节绑定由上层验证;本函数不读取文件或重新计算 SHA。
|
||
* 构造后可按 ordinal 任意重读,read 不推进 state.emitted;持久进度由 runner 的 job.cursor 保存。 */
|
||
export function createDuplicatePlanSource(document:Document,registry:Registry,state:DuplicateCheckpoint,fingerprint:string,readElement?:(id:Id,signal:AbortSignal)=>Promise<ElementRecord>):ImportSource{
|
||
if(state.phase!=='emit'||state.emitted!==0||state.emittedEdges!==0||state.schemaToken!==copyRegistrySignature(document,registry))throw fault('copy_interpreter_changed');
|
||
// Structural/source binding checks; the decoded source has already passed document validation.
|
||
createDuplicatePlanner(document,registry,state.selection,{createId:()=>{throw fault('unstable_import_identity');},worldOffset:state.offset,...(state.mapping?{mapping:state.mapping}:{})},{sourceToken:state.sourceToken,schemaToken:state.schemaToken,checkpoint:state,topologyOnly:!!readElement});
|
||
// 固定旧→新和新→旧映射;source key仍用原ID,输出change使用已经预留的新ID。
|
||
const elements=new Map(state.elementIds),canvases=new Map(state.canvasIds),copiedCanvases=new Set(state.canvases),keys=new Map<Id,string>(),reverseElements=new Map(state.elementIds.map(([id,mapped])=>[mapped,id])),reverseCanvases=new Map(state.canvasIds.map(([id,mapped])=>[mapped,id]));
|
||
for(const[id,mapped]of elements)keys.set(mapped,`element:${id}`);for(const[id,mapped]of canvases)keys.set(mapped,`canvas:${id}`);
|
||
// Edge geometry needs mapped endpoint/owner records. Compute these on demand; retain a bounded cache.
|
||
/** 给边计算提供原表+映射表的只读投影,get 按需转换映射 ID,每个投影最多缓存 32 项。
|
||
* 淘汰按插入顺序,缓存命中不刷新顺序,因此是 FIFO 而非 LRU;不缓存 readElement 的完整 payload。
|
||
* size/迭代将两组成员拼接,依赖映射不与原 ID 碰撞的 planner 合同,不能当可发布的规范文档表。 */
|
||
function projection<T extends ElementRecord|CanvasRecord>(base:ReadonlyMap<Id,T>,reverse:ReadonlyMap<Id,Id>,kind:'element'|'canvas'):ReadonlyMap<Id,T>{
|
||
// 未映射ID沿用原表,新ID转换源记录;缓存淘汰只影响计算成本,不改变稳定映射/源数据。
|
||
const cache=new Map<Id,T>();const get=(id:Id):T|undefined=>{const old=reverse.get(id);if(!old)return base.get(id);const found=cache.get(id);if(found)return found;const original=base.get(old);if(!original)throw fault('invalid_copy_stream_checkpoint');const record=duplicateTreeChange(document,registry,kind,original,elements,canvases,copiedCanvases,state.offset).after as T;cache.set(id,record);if(cache.size>32)cache.delete(cache.keys().next().value!);return record;};
|
||
/** 迭代先原记录再映射记录,keys/values/forEach均复用同一逻辑投影,不能作为发布文档表。 */
|
||
const entries=function*():Generator<[Id,T],undefined>{yield* base;for(const id of reverse.keys())yield[id,get(id)!];return undefined;};
|
||
const view:ReadonlyMap<Id,T>={get,has:id=>base.has(id)||reverse.has(id),size:base.size+reverse.size,entries,keys:function*():Generator<Id,undefined>{for(const[id]of entries())yield id;return undefined;},values:function*():Generator<T,undefined>{for(const[,value]of entries())yield value;return undefined;},[Symbol.iterator]:entries,forEach(fn,thisArg){for(const[id,value]of entries())fn.call(thisArg,value,id,view);}};return Object.freeze(view);
|
||
}
|
||
// projected只服务边端点和公共owner的坐标计算;length是tree order+edges的固定ordinal总数。
|
||
const projected:Document={...document,elements:projection(document.elements,reverseElements,'element'),canvases:projection(document.canvases,reverseCanvases,'canvas')},length=state.order.length+state.edges.length;
|
||
/** 前 state.order.length 项用树转换器,其余用边转换器,生成同一预映射 CopyLine。
|
||
* readElement 可按需异步加载元素;其身份/源一致性由注入端口保证,canvas 直接从已捕获文档取。
|
||
* 预先取消/非法 ordinal 在 try 外拒绝;try 内失败(含读取后取消)转成 ImportLine.error,runner 必须复查信号。
|
||
* 成功检查单行 JSON 的 UTF-8 长度 ≤256KiB(不含 NDJSON 换行),不限制完整输出总字节或在单条内让出。
|
||
* 失败不会推进此源的全局游标;是否保留失败、跳过或停止由 runner 管理。 */
|
||
return Object.freeze({fingerprint,parserVersion:2,async read(cursor:number,signal:AbortSignal){
|
||
if(signal.aborted)throw fault('import_aborted');if(!Number.isSafeInteger(cursor)||cursor<0||cursor>length)throw fault('invalid_import_cursor');if(cursor===length)return null;
|
||
const task=state.order[cursor],key=task?`${task.kind}:${task.id}`:`element:${state.edges[cursor-state.order.length]}`;
|
||
// 索引源只在此请求完整元素payload;cancel后重查signal,再生成映射/guard行并检查单条大小。
|
||
try{const original=task?.kind==='canvas'?document.canvases.get(task.id)!:readElement?await readElement(task?.id??state.edges[cursor-state.order.length],signal):document.elements.get(task?.id??state.edges[cursor-state.order.length])!;
|
||
if(signal.aborted)throw fault('import_aborted');const change=cursor<state.order.length?duplicateTreeChange(document,registry,task.kind,original,elements,canvases,copiedCanvases,state.offset):duplicateEdgeChange(document,registry,original as ElementRecord,elements,canvases,projected),line=duplicateImportLine(document,registry,change,keys);
|
||
if(new TextEncoder().encode(JSON.stringify(line)).byteLength>256*1024)throw fault('record_too_large');
|
||
return{cursor,nextCursor:cursor+1,key:line.key,value:line as unknown as Json};}catch(error){return{cursor,nextCursor:cursor+1,key,error:(error as {code?:string}).code??'copy_record_failed'};}
|
||
}});
|
||
}
|