docs(canvas): 完善画布模块与函数的逻辑注释 #12
@@ -13,7 +13,10 @@ import { duplicateImportLine } from './import-duplicate.ts';
|
||||
import type { ImportSource } from './imports.ts';
|
||||
|
||||
/** A seekable record cursor over the original SHA-bound snapshot, not a second mapped Blob. */
|
||||
/** 要求未输出的emit起点、当前registry签名及有效绑定,ID工厂在恢复重校验中禁止意外再分配。 */
|
||||
/** 要求 emit 起点/零输出计数与当前 registry 签名,调用 planner 复核 checkpoint 结构绑定且禁止再分配 ID。
|
||||
* 映射复制为私有 Map,但 document/registry/state 及 readElement 仍沿用调用方引用,须保持不可变/稳定。
|
||||
* sourceToken/fingerprint 的真实字节绑定由上层验证;本函数不读取文件或重新计算 SHA。
|
||||
* 构造后可按 ordinal 任意重读,read 不推进 state.emitted;持久进度由 runner 的 job.cursor 保存。 */
|
||||
export function createDuplicatePlanSource(document:Document,registry:Registry,state:DuplicateCheckpoint,fingerprint:string,readElement?:(id:Id,signal:AbortSignal)=>Promise<ElementRecord>):ImportSource{
|
||||
if(state.phase!=='emit'||state.emitted!==0||state.emittedEdges!==0||state.schemaToken!==copyRegistrySignature(document,registry))throw fault('copy_interpreter_changed');
|
||||
// Structural/source binding checks; the decoded source has already passed document validation.
|
||||
@@ -22,7 +25,9 @@ export function createDuplicatePlanSource(document:Document,registry:Registry,st
|
||||
const elements=new Map(state.elementIds),canvases=new Map(state.canvasIds),copiedCanvases=new Set(state.canvases),keys=new Map<Id,string>(),reverseElements=new Map(state.elementIds.map(([id,mapped])=>[mapped,id])),reverseCanvases=new Map(state.canvasIds.map(([id,mapped])=>[mapped,id]));
|
||||
for(const[id,mapped]of elements)keys.set(mapped,`element:${id}`);for(const[id,mapped]of canvases)keys.set(mapped,`canvas:${id}`);
|
||||
// Edge geometry needs mapped endpoint/owner records. Compute these on demand; retain a bounded cache.
|
||||
/** 给边计算提供新端点/owner视图,get按需转换并只保留32条缓存,不批量生成所有副本对象。 */
|
||||
/** 给边计算提供原表+映射表的只读投影,get 按需转换映射 ID,每个投影最多缓存 32 项。
|
||||
* 淘汰按插入顺序,缓存命中不刷新顺序,因此是 FIFO 而非 LRU;不缓存 readElement 的完整 payload。
|
||||
* size/迭代将两组成员拼接,依赖映射不与原 ID 碰撞的 planner 合同,不能当可发布的规范文档表。 */
|
||||
function projection<T extends ElementRecord|CanvasRecord>(base:ReadonlyMap<Id,T>,reverse:ReadonlyMap<Id,Id>,kind:'element'|'canvas'):ReadonlyMap<Id,T>{
|
||||
// 未映射ID沿用原表,新ID转换源记录;缓存淘汰只影响计算成本,不改变稳定映射/源数据。
|
||||
const cache=new Map<Id,T>();const get=(id:Id):T|undefined=>{const old=reverse.get(id);if(!old)return base.get(id);const found=cache.get(id);if(found)return found;const original=base.get(old);if(!original)throw fault('invalid_copy_stream_checkpoint');const record=duplicateTreeChange(document,registry,kind,original,elements,canvases,copiedCanvases,state.offset).after as T;cache.set(id,record);if(cache.size>32)cache.delete(cache.keys().next().value!);return record;};
|
||||
@@ -32,7 +37,11 @@ export function createDuplicatePlanSource(document:Document,registry:Registry,st
|
||||
}
|
||||
// projected只服务边端点和公共owner的坐标计算;length是tree order+edges的固定ordinal总数。
|
||||
const projected:Document={...document,elements:projection(document.elements,reverseElements,'element'),canvases:projection(document.canvases,reverseCanvases,'canvas')},length=state.order.length+state.edges.length;
|
||||
/** read按ordinal选择共享tree/edge转换器;单记录错误返回error,源指纹/完整性与调度另由宿主约束。 */
|
||||
/** 前 state.order.length 项用树转换器,其余用边转换器,生成同一预映射 CopyLine。
|
||||
* readElement 可按需异步加载元素;其身份/源一致性由注入端口保证,canvas 直接从已捕获文档取。
|
||||
* 预先取消/非法 ordinal 在 try 外拒绝;try 内失败(含读取后取消)转成 ImportLine.error,runner 必须复查信号。
|
||||
* 成功检查单行 JSON 的 UTF-8 长度 ≤256KiB(不含 NDJSON 换行),不限制完整输出总字节或在单条内让出。
|
||||
* 失败不会推进此源的全局游标;是否保留失败、跳过或停止由 runner 管理。 */
|
||||
return Object.freeze({fingerprint,parserVersion:2,async read(cursor:number,signal:AbortSignal){
|
||||
if(signal.aborted)throw fault('import_aborted');if(!Number.isSafeInteger(cursor)||cursor<0||cursor>length)throw fault('invalid_import_cursor');if(cursor===length)return null;
|
||||
const task=state.order[cursor],key=task?`${task.kind}:${task.id}`:`element:${state.edges[cursor-state.order.length]}`;
|
||||
|
||||
@@ -20,7 +20,10 @@ type ExternalCanvas = {id: Id; parent: CanvasRecord['parent']; entryIncarnation?
|
||||
type CopyLine = {key: string; change: Exclude<Change,{kind:'field'}>; dependencies: string[]; externalElements: ExternalElement[]; externalCanvases: ExternalCanvas[]};
|
||||
|
||||
/** Internal source: already mapped records, with explicit guards for every retained external reference. */
|
||||
/** 兼容完整计划路径:逐change生成有限记录NDJSON,单条≤256KiB、全源≤64MiB;仍返回完整Blob。 */
|
||||
/** 完整计划兼容路径:先同步建立新 ID→源 key Map,再逐 change 编码 NDJSON,保留所有字符串直到生成 Blob。
|
||||
* 单条含换行的 UTF-8 字节须 ≤256KiB,全源 ≤64MiB;这些是编码成本限制,不是进程峰值内存保证。
|
||||
* 每条完成后按约每 KiB 一个工作单位调用 copy budget;单次映射建表/JSON.stringify 不被抢占。
|
||||
* 成功前复查取消,不输出部分 Blob;不验证整个 plan 的 ID 唯一性/图合法性或计算源摘要。 */
|
||||
export async function encodeDuplicateSource(document: Document, registry: Registry, plan: DuplicatePlan, signal: AbortSignal, scheduling:ImportScheduling={}): Promise<Blob> {
|
||||
const keys = new Map<Id,string>(),parts: BlobPart[] = [];
|
||||
for (const [id,mapped] of Object.entries(plan.elementIds)) keys.set(mapped,`element:${id}`);
|
||||
@@ -36,11 +39,15 @@ export async function encodeDuplicateSource(document: Document, registry: Regist
|
||||
if(signal.aborted)throw fault('copy_aborted');return new Blob(parts,{type:'application/x-ndjson'});
|
||||
}
|
||||
|
||||
/** 共享逐记录转换器:将新映射ID关联到source key,收集owner/端点/声明引用的必要外部闭包。 */
|
||||
/** 将一条已映射创建变化封装成 CopyLine:keys 区分副本内部依赖与仍引用原图的外部身份。
|
||||
* 收集 owner/端点/声明数据引用及外部 canvas 父链入口,去掉自身依赖;不重新映射字段或分配 ID。
|
||||
* 返回的 change 沿用输入引用,数组/guard 未整体冻结;输入文档/映射须来自已验证的不可变计划。
|
||||
* 这里只收集已知 copy.references,不独立调用 resolve 验组件版本,也不替代最终整图校验。 */
|
||||
export function duplicateImportLine(document:Document,registry:Registry,change:Exclude<Change,{kind:'field'}>,keys:ReadonlyMap<Id,string>):CopyLine {
|
||||
if(!change.after||change.before!==null)throw fault('invalid_duplicate_plan');
|
||||
const dependencies=new Set<string>(),elements=new Map<Id,ExternalElement>(),canvases=new Map<Id,ExternalCanvas>();
|
||||
/** 内部canvas转成依赖;外部canvas沿父链收集guard和入口节点身份,不猜其数据字段。 */
|
||||
/** keys 命中即登记内部依赖;否则沿原 canvas 父链收集 parent 与存在的入口代次。
|
||||
* canvases Map 同时去重/止循环,不在这里证明父链无环或每个入口都存在,依赖合法源文档合同。 */
|
||||
const canvas=(id:Id)=>{
|
||||
const key=keys.get(id);if(key){dependencies.add(key);return;}
|
||||
let next:Id|undefined=id;
|
||||
@@ -65,7 +72,9 @@ export function duplicateImportLine(document:Document,registry:Registry,change:E
|
||||
return {key,change,dependencies:[...dependencies],externalElements:[...elements.values()],externalCanvases:[...canvases.values()]};
|
||||
}
|
||||
|
||||
/** 校验旧durable预映射行与当前job.stableIds一致,元素新incarnation必须为1,不重新分配ID。 */
|
||||
/** 旧持久源的基础封装校验:允许源种类、JSON 值域、数组形状、创建 ID 与 stableIds/key 前缀对应。
|
||||
* element 还要求 incarnation=1;不逐字段验证 guard/dependency schema,也不证明外部 guard 集合完整。
|
||||
* 返回原行引用,不捕获/修复源,不重新分配 ID;完整源绑定、变化及图校验仍由 runner/发布边界承担。 */
|
||||
function copyLine(line:ImportLine,job:ImportJob):CopyLine {
|
||||
const value=line.value as unknown as CopyLine;
|
||||
if(!['duplicate-ndjson','duplicate-plan'].includes(job.source?.kind??'')||!value||!validJson(value)||typeof value.key!=='string'||!Array.isArray(value.dependencies)||!Array.isArray(value.externalElements)||!Array.isArray(value.externalCanvases)||!value.change||value.change.kind!=='element'&&value.change.kind!=='canvas'||value.change.before!==null||!value.change.after||!Object.hasOwn(job.stableIds,value.key)||job.stableIds[value.key]!==value.change.id||value.change.after.id!==value.change.id||!value.key.startsWith(`${value.change.kind}:`))throw fault('invalid_duplicate_source');
|
||||
@@ -73,7 +82,9 @@ function copyLine(line:ImportLine,job:ImportJob):CopyLine {
|
||||
return value;
|
||||
}
|
||||
/** Re-check the current reference contract when decoding old durable prepared sources. */
|
||||
/** 对当前复制schema重验引用:内部映射须声明依赖,外部引用须explicit preserve且有对应guard。 */
|
||||
/** 对 element 的当前 copy.references 重验:内部映射(自引用除外)须列依赖,外部须 preserve 且列 guard ID。
|
||||
* 每个引用线性扫描 stableIds 找对应命名空间;这里只查声明数据引用,不重建 owner/端点的完整 guard 闭包。
|
||||
* canvas 直接跳过;此步骤不是全部 CopyLine schema 校验或外部对象当前生命周期校验。 */
|
||||
function assertCopyReferences(value:CopyLine,job:ImportJob,registry:Registry){
|
||||
if(value.change.kind!=='element')return;
|
||||
const record=value.change.after!,schema=registry.components.get(record.componentId)?.copy;
|
||||
@@ -84,14 +95,18 @@ function assertCopyReferences(value:CopyLine,job:ImportJob,registry:Registry){
|
||||
else if(ref.external!=='preserve'||!(ref.target==='element'?value.externalElements:value.externalCanvases).some(guard=>guard.id===id))throw fault('copy_external_reference',[record.id,id]);
|
||||
}
|
||||
}
|
||||
/** 形成runner的纯ImportUnit并保留原行作为guard,reserve只核对已有映射,不修改原图。 */
|
||||
/** 先校验预映射行/声明引用,再用 reserve 核对自身 ID,要求依赖键都在 job.stableIds 中。
|
||||
* reserve 由调用方提供,本函数不回滚它的副作用;合法已准备源应命中现有映射而非再分配身份。
|
||||
* dependencies/change 沿用原行引用,guard 用 JSON 克隆捕获;不检查依赖已提交,也不发布/持久化内容。 */
|
||||
export function prepareDuplicateImport(line:ImportLine,job:ImportJob,reserve:(key:string)=>Id,registry:Registry):ImportUnit {
|
||||
const value=copyLine(line,job);assertCopyReferences(value,job,registry);if(reserve(value.key)!==value.change.id)throw fault('invalid_duplicate_source');
|
||||
for(const key of value.dependencies)if(!Object.hasOwn(job.stableIds,key))throw fault('invalid_duplicate_source');
|
||||
return{key:value.key,dependencies:value.dependencies,changes:[value.change],guard:clone(line.value!)};
|
||||
}
|
||||
/** Check at the synchronous commit boundary, never only when the source was first parsed. */
|
||||
/** 提交前按当前文档核对外部节点代次/owner、canvas parent/入口代次;不符即拒绝这一单元/组。 */
|
||||
/** 同步提交前重新解释行与声明引用,再逐个核对提供的外部 element 代次/owner、canvas parent/入口代次。
|
||||
* 内容/样式可变化而通过;组件版本等未在此额外比较,guard 是否完整依赖受信源生成与恢复校验。
|
||||
* 不锁住对象或发布 changes;应与真正提交处于同一同步边界,中间 await 会让该检查过期。 */
|
||||
export function assertDuplicateExternal(line:ImportLine,job:ImportJob,document:Document,registry:Registry){
|
||||
const value=copyLine(line,job);assertCopyReferences(value,job,registry);
|
||||
for(const guard of value.externalElements){const record=document.elements.get(guard.id);if(!record||record.incarnation!==guard.incarnation||record.ownerCanvasId!==guard.ownerCanvasId)throw fault('copy_external_changed',[guard.id]);}
|
||||
|
||||
Reference in New Issue
Block a user