/** * 复制规划器的合作调度适配,不读live editor getter、不发布文档、不访问网络。 * 完整同步生成器、可恢复语义状态机、lazy输出准备分别提供显式入口,不能把准备完成当成内容已加入。 * 所有时间片都是软目标,持久源/映射/检查点与提交前外部身份复核由调用者负责。 */ 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'; /** 可注入取消、每片时间/步数和让出端口;checkpoint在完整规划适配中只报告累计语义步数。 */ export type DuplicateScheduling = { readonly signal?: AbortSignal; readonly checkpoint?: (steps:number) => Promise; readonly frameBudgetMs?: number; readonly maxStepsPerSlice?: number; readonly now?: () => number; readonly yieldTask?: () => Promise; }; /** * 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. */ /** 分片消费完整 plan 生成器,默认每片 6ms/512 个语义步,完成后返回全量计划而非逐条发布。 * checkpoint 仅接收累计步数,不含映射/生成器状态,不能用该数字恢复规划;最终返回前不保证再调用一次。 * 每次 next 本身可包含同步固定成本,软预算不能抢占它;now/yieldTask 决定实际调度,默认 timer 让出。 * 异常/取消关闭生成器,不回滚已调用 createId 或 checkpoint 的外部副作用;调用方负责可靠源/映射保存。 */ export async function planDuplicateAsync(document: Document, registry: Registry, selection: readonly Target[], options: DuplicateOptions, scheduling: DuplicateScheduling = {}): Promise { 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(resolve => setTimeout(resolve, 0))); /** 在迭代、checkpoint和yield边界检查取消,避免迟到准备结果被宿主继续采用。 */ 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. // 首次await前捕获选区/偏移/预留映射,源document与registry按不可变快照合同使用。 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. */ /** 从完整 DuplicateCheckpoint 恢复语义游标/已分配 ID,源验证和派生输出缓存仍由 planner 重建。 * 每个未完成时间片可将不可变 checkpoint 交宿主 await;回调成功只代表宿主 Promise 成功,不独自证明落盘。 * planner.step 完成后直接 result,不额外回调最终状态;需要最终可靠切点时由调用方保存返回计划/状态。 * selection/偏移/映射在入口捕获,document/registry 沿不可变快照合同使用;调度选项应保持稳定。 * 与完整生成器入口一样,不发布业务文档、不补偿已分配 ID 或宿主 checkpoint 副作用。 */ export async function planDuplicateResumable(document:Document,registry:Registry,selection:readonly Target[],options:DuplicateOptions,binding:{sourceToken:string;schemaToken:string;checkpoint?:DuplicateCheckpoint},scheduling:Omit&{checkpoint?:(state:DuplicateCheckpoint)=>Promise}={}):Promise{ const budget=scheduling.frameBudgetMs??6,limit=scheduling.maxStepsPerSlice??512,now=scheduling.now??(()=>performance.now()),yieldTask=scheduling.yieldTask??(()=>new Promise(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(count0 时按阶段/首次分配/时间/步数门槛节奏化保存。 * 阈值只在切片末检查,maxStepsPerCheckpoint 可能多跨一片;不是逐语义步的硬持久间隔。 * ready 时只有 interval>0 且有回调才补一次保存;默认路径可直接返回,宿主仍须持久化返回状态。 * topologyOnly 仅选择源拓扑检查,不证明全部组件 payload 可复制;懒源逐条转换时仍可报记录失败。 */ export async function prepareDuplicateStream(document:Document,registry:Registry,selection:readonly Target[],options:DuplicateOptions,binding:{sourceToken:string;schemaToken:string;checkpoint?:DuplicateCheckpoint;topologyOnly?:boolean},scheduling:Omit&{checkpoint?:(state:DuplicateCheckpoint)=>Promise;checkpointIntervalMs?:number;maxStepsPerCheckpoint?:number}={}):Promise{ const budget=scheduling.frameBudgetMs??6,limit=scheduling.maxStepsPerSlice??512,now=scheduling.now??(()=>performance.now()),yieldTask=scheduling.yieldTask??(()=>new Promise(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(); // 检查点保存频率与UI时间片分开:阶段变化、首次分配、工作量/时间阈值或时钟异常均触发保存。 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); // saved* 是调度内存,不是另一份持久 checkpoint;首次 savedPhase 未知会在首片触发回调。 // savedAt 只在回调成功后重取,保存耗时不算新增规划工作,也不会单因慢存储强迫下一片立即再存。 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(count0||!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(); } }