diff --git a/src/lib/canvas-runtime/duplicate.ts b/src/lib/canvas-runtime/duplicate.ts index 8f9d339..6033b97 100644 --- a/src/lib/canvas-runtime/duplicate.ts +++ b/src/lib/canvas-runtime/duplicate.ts @@ -23,7 +23,10 @@ export type DuplicateScheduling = { * 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步;返回前全部记录已生成并校验,但不执行导入。 */ +/** 分片消费完整 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'); @@ -58,7 +61,11 @@ export async function planDuplicateAsync(document: Document, registry: Registry, } /** Resume semantic planning cursors. Source/output validation and derived output caches are rebuilt. */ -/** 以完整checkpoint恢复语义游标,仍重做源验证/派生输出缓存,不声称从任意字节零重算。 */ +/** 从完整 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'); @@ -73,7 +80,12 @@ export async function planDuplicateResumable(document:Document,registry:Registry /** Stop before record conversion: the stable order/mappings are sufficient for seekable lazy output. */ -/** 只推进到emit起点:范围/顺序/全部新ID已固定,但尚未转换全部payload,可交lazy source逐条输出。 */ +/** 仅推进至 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&{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'); @@ -82,7 +94,8 @@ export async function prepareDuplicateStream(document:Document,registry:Registry 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); - // savedSteps等只记录上次已等待checkpoint回调成功的位置,保存耗时不冒充新增规划工作。 + // 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; diff --git a/src/lib/canvas-runtime/scope-counts.ts b/src/lib/canvas-runtime/scope-counts.ts index a163c98..d960cb2 100644 --- a/src/lib/canvas-runtime/scope-counts.ts +++ b/src/lib/canvas-runtime/scope-counts.ts @@ -8,7 +8,9 @@ import {fault} from '../canvas-core/values.ts'; /** Snapshot-bound direct membership counts, including nodes and connections. */ /** 当前捕获快照的直属数量,包含节点和边;不是递归子树大小或 DOM 已挂载数量。 */ export type CanvasElementCounts=Readonly>; -/** 要求 counts 精确覆盖 canvasIds、每项非负安全整数,且总和等于声明 total。 */ +/** 检查自有可枚举键数量及 canvasIds 的每个键存在,计数为非负安全整数,总和安全且等于 total。 + * canvasIds 须由调用方保证唯一;此函数不单独去重,重复声明会破坏“键数量相同即覆盖相同”的前提。 + * 只验证声明内部一致,不读取真实元素、认证服务或证明后续 payload 已齐;输入也不在此捕获/冻结。 */ export function validateCanvasElementCounts(value:unknown,canvasIds:readonly Id[],total:number):asserts value is CanvasElementCounts { if(!value||typeof value!=='object'||Array.isArray(value))throw fault('invalid_model_coverage'); const counts=value as Record,keys=Object.keys(counts);let sum=0; @@ -20,7 +22,9 @@ export function validateCanvasElementCounts(value:unknown,canvasIds:readonly Id[ /** All active IDs in the capture, including records not yet received. */ /** 完整活 ID 命名空间,即使部分记录尚未传到客户端,也预先知道其直接 owner。 */ export type ElementOwners=Readonly>; -/** 核验每个活 ID 的 owner 合法,并按 owner 反算数量与 counts 一致,不靠已读前缀猜完整性。 */ +/** 要求声明恰有 total 个自有可枚举活 ID,每个非空 ID 的 owner 是 counts 中已有键,再反算数量。 + * counts 应先经计数校验;此处不独立验证其值域,也不与已读 document 比较 ID/owner 是否对应。 + * 只做同步声明一致性检查,不捕获/冻结输入;实际 payload 到达后还需匹配这份源快照身份表。 */ export function validateElementOwners(value:unknown,counts:CanvasElementCounts,total:number):asserts value is ElementOwners { if(!value||typeof value!=='object'||Array.isArray(value)||Object.keys(value).length!==total)throw fault('invalid_model_identity'); // 无原型计数表避免特殊 owner 键污染计数;空 canvas 由缺省 0 与 counts 比较。 @@ -30,8 +34,10 @@ export function validateElementOwners(value:unknown,counts:CanvasElementCounts,t } /** Full captured topology, independent of which entry payloads have arrived. */ -/** 用完整 topology+owner 证明验证单根、父存在、入口唯一/归属正确和无环。 - * 此时入口记录可能尚未收到,入口是否确为 node 仍需 payload 到达后的文档校验。 */ +/** 用源 canvas 表和已验证 owners 声明检查唯一根=projectId、父存在、入口唯一/归属与无环。 + * 不是完整 CanvasRecord/payload schema 校验;未收到入口时不能证明其为 node、组件可用或实际拥有关系一致。 + * 每个子 canvas 重新走父链,深树会反复访问共享祖先;同步检查无预算/取消,应由捕获规模约束成本。 + * 不捕获输入或标记文档已完整;加载器另需核对已收到树/元素与此声明,再建立 owner 的覆盖状态。 */ export function validateCanvasTopology(canvases:readonly import('../canvas-core/types.ts').CanvasRecord[],owners:ElementOwners,projectId:Id):void { if(!Array.isArray(canvases)||!canvases.length)throw fault('invalid_model_topology'); const table=new Map(canvases.map(canvas=>[canvas?.id,canvas]));if(table.size!==canvases.length||!table.has(projectId))throw fault('invalid_model_topology');