feat(canvas): schedule deep copy planning cooperatively

This commit is contained in:
2026-11-08 15:47:57 +08:00
parent ce02e43958
commit 379db2fe14
5 changed files with 178 additions and 21 deletions
+44
View File
@@ -0,0 +1,44 @@
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 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);
try {
while (true) {
check();
const start = now(); let count = 0;
do {
check();
const step = steps.next();
check();
if (step.done) return step.value;
count++;
} while (count < limit && now() - start < budget);
await yieldTask();
}
} finally {
// Release snapshot references after cancellation or host scheduling failures.
steps.return(undefined as never);
}
}