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. */ export function createDuplicatePlanSource(document:Document,registry:Registry,state:DuplicateCheckpoint,fingerprint:string,readElement?:(id:Id,signal:AbortSignal)=>Promise):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}); const elements=new Map(state.elementIds),canvases=new Map(state.canvasIds),copiedCanvases=new Set(state.canvases),keys=new Map(),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. function projection(base:ReadonlyMap,reverse:ReadonlyMap,kind:'element'|'canvas'):ReadonlyMap{ const cache=new Map();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;}; const entries=function*():Generator<[Id,T],undefined>{yield* base;for(const id of reverse.keys())yield[id,get(id)!];return undefined;}; const view:ReadonlyMap={get,has:id=>base.has(id)||reverse.has(id),size:base.size+reverse.size,entries,keys:function*():Generator{for(const[id]of entries())yield id;return undefined;},values:function*():Generator{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); } const projected:Document={...document,elements:projection(document.elements,reverseElements,'element'),canvases:projection(document.canvases,reverseCanvases,'canvas')},length=state.order.length+state.edges.length; 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]}`; 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=cursor256*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'};} }}); }