From a927f36d96b3a03a7682781c77c6b6765a8f8f24 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Thu, 12 Nov 2026 14:20:03 +0800 Subject: [PATCH] docs(runtime): explain recovery capture and sealing lifecycle --- src/lib/canvas-runtime/recovery-encoding.ts | 41 +++++++++++++++------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/src/lib/canvas-runtime/recovery-encoding.ts b/src/lib/canvas-runtime/recovery-encoding.ts index d6d91bd..05369d5 100644 --- a/src/lib/canvas-runtime/recovery-encoding.ts +++ b/src/lib/canvas-runtime/recovery-encoding.ts @@ -12,7 +12,8 @@ import type { RecoveryImage } from './storage-port.ts'; /** 归一后的值、子树高度、可复用证明和精确 JSON UTF-8 字节数;undefined 表示 JSON 省略值。 */ type Encoded = {value:Json|undefined;height:number;reusable:boolean;bytes:number}; -// WeakMap 仅按被证明安全的源身份缓存;sealed/imageSizes 是本 codec 的来源事实,外部 freeze 不能伪造。 +// immutable: 源对象→归一值/高度/字节;encodedSizes: 输出子树→字节;sealed/imageSizes: 本 codec 图像身份/总字节。 +// 都是进程内弱来源事实,不持久化、不检查权限/schema;JSON 克隆/重开对象不会继承 sealed 品牌。 const immutable=new WeakMap(),sealed=new WeakSet(),encodedSizes=new WeakMap(),imageSizes=new WeakMap(); /** 计算 UTF-8 字节数(代理对计4字节);调用时多为 JSON.stringify 后的已转义字符串。 */ function utf8Length(text:string){let bytes=0;for(let i=0;i=0xd800&&c<=0xdbff&&i+1=0xdc00&&text.charCodeAt(i+1)<=0xdfff){bytes+=4;i++;}else bytes+=3;}return bytes;} @@ -20,7 +21,9 @@ function utf8Length(text:string){let bytes=0;for(let i=0;iutf8Length(JSON.stringify(value)); /** JSON value semantics, with reuse only for transitively frozen plain data properties. */ -/** 创建一次编码的递归访问器,visiting 检测当前路径循环;共享不可变节点可从全局弱缓存复用。 */ +/** 一次同步访问器拥有 visiting 路径 Set,共用模块 immutable 弱缓存;共享节点不等同循环,退出路径即移除。 + * 普通数据逐属性捕获,toJSON/装箱/自定义原型退回原生 JSON 语义;此 codec 不是一般保身份深克隆工具。 + * 会求值外部 getter/toJSON,也可能传播原生 JSON 异常;不修改业务 Root,但不能回滚用户回调副作用。 */ function createEncoder() { const visiting=new Set(); /** 按原 JSON 语义捕获值并检查最大深度64,返回携带不可变性/大小证明的 Encoded。 */ @@ -28,7 +31,8 @@ function createEncoder() { if(value===null||typeof value==='string'||typeof value==='boolean'){if(depth>64)throw fault('invalid_recovery_payload');return{value,height:0,reusable:true,bytes:scalarBytes(value)};} if(typeof value==='number'){if(depth>64)throw fault('invalid_recovery_payload');const normalized=Number.isFinite(value)?(Object.is(value,-0)?0:value):null;return{value:normalized,height:0,reusable:true,bytes:scalarBytes(normalized)};} if(value===undefined||typeof value==='symbol')return{value:undefined,height:0,reusable:true,bytes:0}; - /** 对 toJSON/装箱值/自定义原型使用原生 JSON 兼容路径,每次重新求值,不缓存隐藏可变行为。 */ + /** 用 {[key]:value} 包裹保留原生 toJSON 的属性 key 上下文,stringify/parse 后再检查 JSON 深度并冻结结果。 + * undefined 省略、BigInt/循环/用户回调错误保持该原生路径行为;返回 reusable=false,避免缓存隐藏可变状态。 */ const fallback=()=>{ // Preserve the previous JSON codec for toJSON, boxed values and custom prototypes. // These can depend on hidden mutable state and are deliberately never cached. @@ -40,6 +44,7 @@ function createEncoder() { if(typeof value==='bigint'||typeof value==='function')return fallback(); if(typeof value!=='object')throw fault('invalid_recovery_payload'); const object=value as object,cached=immutable.get(object); + // 命中先前完整证明可跳过重读,但仍以当前位置+子树高度检查深度;它不是重新验证任意原型行为的入口。 if(cached){if(depth+cached.height>64)throw fault('invalid_recovery_payload');return cached;} const prototype=Object.getPrototypeOf(object); // A callable toJSON getter must also be evaluated afresh on every encode. @@ -59,7 +64,8 @@ function createEncoder() { } const encoded={value:Object.freeze(result) as unknown as Json,height,reusable,bytes};encodedSizes.set(result,bytes);if(reusable)immutable.set(object,encoded);return encoded; } - // 对象只遍历可枚举自有字符串键;defineProperty 安全写入特殊键,并保留原属性次序。 + // 对象只遍历可枚举自有字符串键;省略值不计输出属性/高度,数组同类槽则归一成 null 并受深度上限约束。 + // defineProperty 将 __proto__ 等保留为普通自有数据键,字节累计包含名字/冒号/逗号,不改变属性次序。 const result:Record={};let properties=0; for(const name of Object.keys(object)){ const descriptor=Object.getOwnPropertyDescriptor(object,name);if(!descriptor||!Object.hasOwn(descriptor,'value'))reusable=false; @@ -82,7 +88,9 @@ type ImageVersion = RecoveryImage['formatVersion']; function validateHeader(formatVersion:ImageVersion,rootRevision:number,localRevision:number){ if(![1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17].includes(formatVersion)||!Number.isSafeInteger(rootRevision)||rootRevision<0||!Number.isSafeInteger(localRevision)||localRevision<0||localRevision>rootRevision)throw fault('corrupt_recovery_image'); } -/** 组装本 codec 拥有的不可变信封,并登记精确字节成本;只登记真正由本模块构造的对象。 */ +/** 对已归一 payload 组装冻结图像并登记私有 sealed/大小;公开入口在此前验证 header。 + * header 仅数字/ASCII 字段,以 payload:null 信封长度减 4 再加 payload UTF-8 成本;无子树事实时用原生计算。 + * sealed 只表示 codec 捕获完成,不证明业务 schema、存储完成或服务端已确认。 */ function finishImage(formatVersion:ImageVersion,rootRevision:number,localRevision:number,encoded:Json):RecoveryImage { const image=Object.freeze({formatVersion,rootRevision,localRevision,payload:encoded});sealed.add(image);imageSizes.set(image,JSON.stringify({...image,payload:null}).length-4+(encoded&&typeof encoded==='object'?encodedSizes.get(encoded)??scalarBytes(encoded):scalarBytes(encoded)));return image; } @@ -100,11 +108,14 @@ type Prepared = Scalar | Encoded | {source:object;depth:number;children?:Prepare // Read unknown objects synchronously, before the first suspension. Shape-owned // containers can include external values; each such occurrence is captured now. // Detached JSON can also defer after checking its prototype environment. -/** 在第一次让出前固定不安全输入;shape-owned 外壳仍逐个捕获外部子值,不能只信 Object.freeze。 */ +/** 同步准备 Prepared 树:原子值可延后,已证明 Encoded 可复用,陌生子树按出现次数立即规范化。 + * shape-owned 容器只证明外壳,仍逐个捕获外部子值;detached JSON 在原型环境允许时可整棵延后。 + * 此准备阶段本身可遍历大量结构/运行回调,未必常数时间;不是 async 后才首次读取所有输入。 */ function prepareEncoding(input:unknown) { const normalize=createEncoder(),visiting=new Set(); let plainEnvironment:boolean|undefined; - /** 同步规范化陌生对象;任何用户行为可能改原型环境,结束即使失败也使环境缓存失效。 */ + /** 同步规范化陌生值;回调可能改全局原型,finally 只清 plainEnvironment 检查缓存,失败也清。 + * 不清模块已有 immutable 编码事实,也不撤销用户环境修改;后续 detached 准入需重新读描述符。 */ const capture=(value:unknown,depth:number,key:string)=>{ try{return normalize(value,depth,key);}finally{plainEnvironment=undefined;} }; @@ -151,15 +162,20 @@ function prepareEncoding(input:unknown) { /** Capture external data immediately; cooperatively encode stable owned tables. * Time slicing is soft: synchronous capture of an unknown subtree remains indivisible. * The returned image has exactly the synchronous codec's wire representation. */ -/** 异步入口:先同步固定输入,再按预算遍历安全部分;结果表示与同步 codec 相同。 - * 同步 capture、单个原生操作仍不可抢占,不能由 async 名称推导整个编码无长任务。 */ +/** 验 header 并同步 prepare 后,驱动编码生成器按 ImportBudget 合作让出,最终才 finishImage。 + * 默认优先 scheduler.yield,否则 timer;无 signal/check 参数,宿主需通过 yieldTask 或外层采纳围栏处理生命周期。 + * 逐子项计工作,原子归一化/字符串转义/UTF-8 计量及未知子树捕获仍为同步步骤,不保证每项都不超时间片。 + * 失败不返回部分图像/授予可用 sealed 结果,但已建的不可变子树缓存与回调副作用不会整体回滚。 + * 结果沿用同步 codec 的规范 JSON 表示,不包含文件 Blob 或恢复业务状态的执行动作。 */ export async function sealRecoveryImageAsync(formatVersion:ImageVersion,rootRevision:number,localRevision:number,payload:unknown,options:ImportScheduling={}):Promise { validateHeader(formatVersion,rootRevision,localRevision); const {plan,normalize}=prepareEncoding(payload); const scheduler=(globalThis as typeof globalThis & {scheduler?:{yield?:()=>Promise}}).scheduler; // 默认使用可用 scheduler.yield;没有时由预算适配器回退,所有语义仍由同一个 normalize 决定。 const budget=createImportBudget(new AbortController().signal,{...options,yieldTask:options.yieldTask??(typeof scheduler?.yield==='function'?()=>scheduler.yield!():undefined)}); - /** 生成器只在预算要求时 yield Promise;逐项累加深度/字节并冻结结果,尚未完成的图像不外发。 */ + /** Prepared 的判别:原子值走 normalize,带 height 是已捕获 Encoded,其余为结构计划。 + * 有 children 就用准备阶段捕获值;无 children 只允许已准入 detached 源的不可变数据属性延后读取。 + * 每个输出子项完成后才 budget.step/yield;组装容器时累计高度/精确字节并登记弱缓存,不外发半成品。 */ function* encode(plan:Prepared,depth=0,key=''):Generator,Encoded,void>{ if(plan===null||typeof plan!=='object')return normalize(plan,depth,key); if('height' in plan)return plan; @@ -198,7 +214,7 @@ export async function sealRecoveryImageAsync(formatVersion:ImageVersion,rootRevi if(step.value.value===undefined)throw fault('invalid_recovery_payload'); return finishImage(formatVersion,rootRevision,localRevision,step.value.value); } -/** 查询私有 sealed 来源,供存储跳过重复防御复制;不接受外部伪造品牌字段。 */ +/** 仅查询本进程私有 WeakSet 身份,不重新验 header/payload/持久性;外部同形对象或反序列化副本不命中。 */ export function isSealedRecoveryImage(image:RecoveryImage):boolean{return sealed.has(image);} /** Exact UTF-8 JSON size, available only for codec-owned immutable images. */ @@ -206,5 +222,6 @@ export function isSealedRecoveryImage(image:RecoveryImage):boolean{return sealed export function sealedRecoveryByteLength(image:RecoveryImage){return imageSizes.get(image);} /** Internal codec ownership/size fact, never inferred from external Object.freeze. */ -/** 已编码 JSON 子树的精确大小事实,供 serializer 选择小子树原生 stringify 快路径。 */ +/** 查询本 codec 输出子树的大小事实,未知身份返回 undefined,不能用外部 freeze/自报长度冒充。 + * 只提供 serializer 的成本依据,不证明任何加载/上传任务已完成。 */ export function encodedRecoveryByteLength(value:object){return encodedSizes.get(value);}