62 lines
4.0 KiB
TypeScript
62 lines
4.0 KiB
TypeScript
import { createDuplicatePlanner, type DuplicateCheckpoint } from '../canvas-core/duplicate-checkpoint.ts';
|
|
import type { Document, Registry, Target } from '../canvas-core/types.ts';
|
|
import { planDuplicateSteps, type DuplicateOptions, type DuplicatePlan } from '../canvas-core/duplicate.ts';
|
|
import { clone, fault, freeze } from '../canvas-core/values.ts';
|
|
|
|
export type DuplicateScheduling = {
|
|
readonly signal?: AbortSignal;
|
|
readonly checkpoint?: (steps:number) => Promise<void>;
|
|
readonly frameBudgetMs?: number;
|
|
readonly maxStepsPerSlice?: number;
|
|
readonly now?: () => number;
|
|
readonly yieldTask?: () => Promise<void>;
|
|
};
|
|
|
|
/**
|
|
* Cooperative planning from a captured immutable StateRoot document, not a live editor getter.
|
|
* No document publication or network effects. The caller must persist source/mapping and
|
|
* revalidate external identities before applying a plan; this is not the import executor.
|
|
*/
|
|
export async function planDuplicateAsync(document: Document, registry: Registry, selection: readonly Target[], options: DuplicateOptions, scheduling: DuplicateScheduling = {}): Promise<DuplicatePlan> {
|
|
const budget = scheduling.frameBudgetMs ?? 6, limit = scheduling.maxStepsPerSlice ?? 512;
|
|
if (!Number.isFinite(budget) || budget <= 0 || !Number.isSafeInteger(limit) || limit < 1) throw fault('invalid_copy_budget');
|
|
const signal = scheduling.signal, now = scheduling.now ?? (() => performance.now()), yieldTask = scheduling.yieldTask ?? (() => new Promise<void>(resolve => setTimeout(resolve, 0)));
|
|
const check = () => { if (signal?.aborted) throw fault('copy_aborted'); };
|
|
check();
|
|
// Capture caller-owned small inputs before the first await. Document/registry are immutable core snapshots.
|
|
const targets = freeze(clone(selection)), settings: DuplicateOptions = Object.freeze({ createId: options.createId, ...(options.worldOffset ? {worldOffset: freeze(clone(options.worldOffset))} : {}), ...(options.mapping ? {mapping: freeze(clone(options.mapping))} : {}) });
|
|
const steps = planDuplicateSteps(document, registry, targets, settings);
|
|
let completedSteps=0;
|
|
try {
|
|
while (true) {
|
|
check();
|
|
const start = now(); let count = 0;
|
|
do {
|
|
check();
|
|
const step = steps.next();
|
|
check();
|
|
if (step.done) return step.value;
|
|
count++;completedSteps++;
|
|
} while (count < limit && now() - start < budget);
|
|
if(scheduling.checkpoint)await scheduling.checkpoint(completedSteps);
|
|
check();await yieldTask();
|
|
}
|
|
} finally {
|
|
// Release snapshot references after cancellation or host scheduling failures.
|
|
steps.return(undefined as never);
|
|
}
|
|
}
|
|
|
|
/** Resume semantic planning cursors. Source/output validation and derived output caches are rebuilt. */
|
|
export async function planDuplicateResumable(document:Document,registry:Registry,selection:readonly Target[],options:DuplicateOptions,binding:{sourceToken:string;schemaToken:string;checkpoint?:DuplicateCheckpoint},scheduling:Omit<DuplicateScheduling,'checkpoint'>&{checkpoint?:(state:DuplicateCheckpoint)=>Promise<void>}={}):Promise<DuplicatePlan>{
|
|
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 settings=Object.freeze({createId:options.createId,...(options.worldOffset?{worldOffset:freeze(clone(options.worldOffset))}:{}),...(options.mapping?{mapping:freeze(clone(options.mapping))}:{})}),planner=createDuplicatePlanner(document,registry,freeze(clone(selection)),settings,binding);
|
|
while(true){
|
|
const start=now();let count=0;
|
|
do{check();const done=planner.step();check();if(done)return planner.result();count++;}while(count<limit&&now()-start<budget);
|
|
if(scheduling.checkpoint)await scheduling.checkpoint(planner.checkpoint());check();await yieldTask();
|
|
}
|
|
}
|