Separate cooperative copy planning from durable checkpoint cadence
This commit is contained in:
@@ -41,6 +41,7 @@ export function createDuplicatePlanner(document:Document,registry:Registry,selec
|
||||
function allocate(id:Id,kind:'elements'|'canvases'){const map=suppliedMapping?.[kind],value=map&&Object.hasOwn(map,id)?map[id]:idFactory();if(typeof value!=='string'||!value||used.has(value))throw fault('duplicate_identity',[value]);used.add(value);return value;}
|
||||
return Object.freeze({
|
||||
ready():boolean{return !sourceChecks&&state.phase==='emit'&&state.emitted===0;},
|
||||
progress(){return Object.freeze({phase:state.phase,steps:state.steps,allocated:state.elementIds.length+state.canvasIds.length});},
|
||||
checkpoint():DuplicateCheckpoint{return freeze(clone(state));},
|
||||
step():boolean{
|
||||
if(sourceChecks){if(sourceChecks.next().done)sourceChecks=undefined;return false;}
|
||||
|
||||
@@ -62,10 +62,25 @@ export async function planDuplicateResumable(document:Document,registry:Registry
|
||||
|
||||
|
||||
/** Stop before record conversion: the stable order/mappings are sufficient for seekable lazy output. */
|
||||
export async function prepareDuplicateStream(document:Document,registry:Registry,selection:readonly Target[],options:DuplicateOptions,binding:{sourceToken:string;schemaToken:string;checkpoint?:DuplicateCheckpoint;topologyOnly?:boolean},scheduling:Omit<DuplicateScheduling,'checkpoint'>&{checkpoint?:(state:DuplicateCheckpoint)=>Promise<void>}={}):Promise<DuplicateCheckpoint>{
|
||||
export async function prepareDuplicateStream(document:Document,registry:Registry,selection:readonly Target[],options:DuplicateOptions,binding:{sourceToken:string;schemaToken:string;checkpoint?:DuplicateCheckpoint;topologyOnly?:boolean},scheduling:Omit<DuplicateScheduling,'checkpoint'>&{checkpoint?:(state:DuplicateCheckpoint)=>Promise<void>;checkpointIntervalMs?:number;maxStepsPerCheckpoint?:number}={}):Promise<DuplicateCheckpoint>{
|
||||
const budget=scheduling.frameBudgetMs??6,limit=scheduling.maxStepsPerSlice??512,now=scheduling.now??(()=>performance.now()),yieldTask=scheduling.yieldTask??(()=>new Promise<void>(resolve=>setTimeout(resolve,0)));
|
||||
if(!Number.isFinite(budget)||budget<=0||!Number.isSafeInteger(limit)||limit<1)throw fault('invalid_copy_budget');
|
||||
const check=()=>{if(scheduling.signal?.aborted)throw fault('copy_aborted');};check();
|
||||
const interval=scheduling.checkpointIntervalMs??0,maxCheckpointSteps=scheduling.maxStepsPerCheckpoint??8192;
|
||||
if(!Number.isFinite(interval)||interval<0||!Number.isSafeInteger(maxCheckpointSteps)||maxCheckpointSteps<1)throw fault('invalid_copy_budget');
|
||||
const planner=createDuplicatePlanner(document,registry,selection,options,binding);
|
||||
while(true){const start=now();let count=0;do{check();if(planner.ready())return planner.checkpoint();if(planner.step())throw fault('invalid_copy_stream_checkpoint');count++;}while(count<limit&&now()-start<budget);if(scheduling.checkpoint)await scheduling.checkpoint(planner.checkpoint());check();await yieldTask();}
|
||||
let savedPhase:DuplicateCheckpoint['phase']|undefined,savedSteps=binding.checkpoint?.steps??0,savedAllocated=(binding.checkpoint?.elementIds.length??0)+(binding.checkpoint?.canvasIds.length??0),savedAt=now();
|
||||
while(true){
|
||||
const start=now();let count=0;
|
||||
do{check();if(planner.ready()){const state=planner.checkpoint();if(interval&&scheduling.checkpoint){await scheduling.checkpoint(state);check();}return state;}if(planner.step())throw fault('invalid_copy_stream_checkpoint');count++;}while(count<limit&&now()-start<budget);
|
||||
if(scheduling.checkpoint){
|
||||
const progress=planner.progress(),at=now(),elapsed=at-savedAt;
|
||||
if(!interval||progress.phase!==savedPhase||!savedAllocated&&progress.allocated>0||!Number.isFinite(elapsed)||elapsed<0||elapsed>=interval||progress.steps-savedSteps>=maxCheckpointSteps){
|
||||
await scheduling.checkpoint(planner.checkpoint());
|
||||
// Persistence latency is not more private planning work.
|
||||
savedPhase=progress.phase;savedSteps=progress.steps;savedAllocated=progress.allocated;savedAt=now();
|
||||
}
|
||||
}
|
||||
check();await yieldTask();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -710,7 +710,7 @@ export async function openProjectEngine(options: EngineOptions) {
|
||||
validateLoadingCopySource(editor.getSnapshot(),admitted,document);
|
||||
if(copyRegistrySignature(document,options.registry)!==preparation.registrySignature)throw fault('copy_interpreter_changed');
|
||||
if(preparation.version===3||preparation.version===4){
|
||||
const state=await prepareDuplicateStream(document,options.registry,preparation.selection,{worldOffset:preparation.worldOffset,createId},{sourceToken:admitted.source.sha256!,schemaToken:preparation.registrySignature,checkpoint:preparation.planner,topologyOnly:!!indexed},{signal:controller.signal,checkpoint:async state=>{
|
||||
const state=await prepareDuplicateStream(document,options.registry,preparation.selection,{worldOffset:preparation.worldOffset,createId},{sourceToken:admitted.source.sha256!,schemaToken:preparation.registrySignature,checkpoint:preparation.planner,topologyOnly:!!indexed},{signal:controller.signal,checkpointIntervalMs:50,maxStepsPerCheckpoint:8192,checkpoint:async state=>{
|
||||
const job=check(),previous=job.preparation!;if(previous.planner&&state.steps<=previous.steps)return;
|
||||
publishImport(freeze({...job,generation:job.generation+1,preparation:{...previous,steps:state.steps,allocationIds:[...state.elementIds,...state.canvasIds].map(pair=>pair[1]),planner:state}}),job.generation);await persist();check();
|
||||
}}),job=check();assertNativeImportTarget(job,editor.getSnapshot().document);
|
||||
|
||||
Reference in New Issue
Block a user