115 lines
9.6 KiB
TypeScript
115 lines
9.6 KiB
TypeScript
/**
|
|
* 复制规划器的合作调度适配,不读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<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.
|
|
*/
|
|
/** 分片消费完整 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<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)));
|
|
/** 在迭代、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<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();
|
|
}
|
|
}
|
|
|
|
|
|
/** Stop before record conversion: the stable order/mappings are sufficient for seekable lazy output. */
|
|
/** 仅推进至 ready/emit 起点,返回顺序/映射已固定的 checkpoint;尚未完成所有 payload 转换与输出校验。
|
|
* planner 捕获小输入与私有语义 state;此层只调度 step/checkpoint,不用另一个 live 文档替换源。
|
|
* checkpointIntervalMs=0 默认每个未完成切片保存;>0 时按阶段/首次分配/时间/步数门槛节奏化保存。
|
|
* 阈值只在切片末检查,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<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();
|
|
// 检查点保存频率与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(count<limit&&now()-start<budget);
|
|
if(scheduling.checkpoint){
|
|
// 避免每次UI让出都必须编码/落盘一个大检查点,同时为长准备保留明确的恢复边界。
|
|
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();
|
|
}
|
|
}
|