docs(canvas): explain import execution and lazy copy source recovery
This commit is contained in:
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* 由原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';
|
||||
@@ -8,22 +13,30 @@ 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签名及有效绑定,ID工厂在恢复重校验中禁止意外再分配。 */
|
||||
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.
|
||||
/** 给边计算提供新端点/owner视图,get按需转换并只保留32条缓存,不批量生成所有副本对象。 */
|
||||
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;
|
||||
/** read按ordinal选择共享tree/edge转换器;单记录错误返回error,源指纹/完整性与调度另由宿主约束。 */
|
||||
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');
|
||||
|
||||
Reference in New Issue
Block a user