From c3131a71bd406e8bbae0694a5683e1198ede7722 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Thu, 12 Nov 2026 14:15:02 +0800 Subject: [PATCH] docs(runtime): explain durable storage and recovery JSON contracts --- src/lib/canvas-runtime/recovery-json.ts | 17 +++++++++++++---- src/lib/canvas-runtime/storage-port.ts | 20 ++++++++++++++------ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/lib/canvas-runtime/recovery-json.ts b/src/lib/canvas-runtime/recovery-json.ts index c03180f..3170511 100644 --- a/src/lib/canvas-runtime/recovery-json.ts +++ b/src/lib/canvas-runtime/recovery-json.ts @@ -8,7 +8,9 @@ import {isSealedRecoveryImage,sealedRecoveryByteLength,encodedRecoveryByteLength import {createImportBudget,type ImportScheduling} from './import-budget.ts'; import {fault} from '../canvas-core/values.ts'; -/** 长字符串每段至多4096 UTF-16单元;避免截断代理对,再分别转义并拼接为一个合法 JSON 字符串。 */ +/** 按最多 4096 UTF-16 单元切原字符串,跨边界代理对一起留到下段;孤立代理由原生 JSON 转义。 + * 长串只输出一对外层引号,各段去掉 stringify 引号后拼接,保持完整字符串的转义/字节语义。 + * 段上限不是转义后字符/UTF-8 字节上限,单段 stringify 仍是同步工作。 */ function* stringTokens(value:string):Generator{ if(value.length<=4096){yield JSON.stringify(value);return;} yield '"'; @@ -16,7 +18,9 @@ function* stringTokens(value:string):Generator{ yield '"'; } /** Only traverse codec-owned normalized JSON. Small owned subtrees use native stringify. */ -/** 保留数组/对象原顺序,显式输出分隔符;≤16KiB 已知子树直接 stringify,降低 token 分配开销。 */ +/** 只遍历已封存的规范 JSON:数组按索引、对象按 Object.keys 顺序输出,显式生成分隔符。 + * codec 有精确 UTF-8 大小事实且 ≤16KiB 的对象/数组直接原生 stringify;没有大小事实不猜其成本。 + * 不自行解释 toJSON/getter、稀疏/undefined 或执行深度校验;这些应在封存捕获阶段已规范化/约束。 */ function* tokens(value:unknown):Generator{ if(value&&typeof value==='object'){ const size=encodedRecoveryByteLength(value);if(size!==undefined&&size<=16384){yield JSON.stringify(value);return;} @@ -26,12 +30,17 @@ function* tokens(value:unknown):Generator{ if(typeof value==='string')yield* stringTokens(value);else yield JSON.stringify(value); } /** Preserve exact JSON.stringify bytes; scheduling never exposes a partial recovery image. */ -/** 输出完成的 Blob;check 在工作/await 边界复查 leader/生命周期,失败不返回半成品。 */ +/** 返回完整 application/json Blob,不发布 Root、不写存储或持续产出可消费的字节流。 + * 私有 sealed 品牌才走 token 分片并核对精确字节;外部 image 同步原生 stringify,不因 Object.freeze 获得快速路径。 + * check 由宿主实现 leader/身份围栏,在入口、聚合片段及 await 前后/末尾调用;内部 signal 从不主动 abort。 + * 所有 BlobPart 累积到结束,合作调度只限制部分同步工作,不是全文件零缓冲或整体内存硬预算。 + * 任一错误不返回部分 Blob;此处不再 validate RecoveryImage/schema,输入有效性由 codec/存储边界保证。 */ export async function serializeRecoveryImage(image:RecoveryImage,options:ImportScheduling&{check?:()=>void}={}):Promise{ options.check?.(); // 外部输入不信任所有权/尺寸缓存,继续走原生同步 stringify,不能冒称这一路也全部可分片。 if(!isSealedRecoveryImage(image)){const bytes=new Blob([JSON.stringify(image)],{type:'application/json'});options.check?.();return bytes;} - // 小 token 先聚合成约8K字符的片段,控制 BlobPart 数量;不是每个标点都创建独立 Blob。 + // token 先聚合到至少 8192 个输出字符串 UTF-16 单元再 flush,单 token 可使片段超过目标;不是 8KiB 字节上限。 + // 每次 flush 给预算记 128 单位,粗时钟也能按累积工作让出;不对每个标点单独创建 BlobPart/读取时钟。 const budget=createImportBudget(new AbortController().signal,options),parts:BlobPart[]=[];let pending:string[]=[],length=0; for(const token of tokens(image)){ pending.push(token);length+=token.length; diff --git a/src/lib/canvas-runtime/storage-port.ts b/src/lib/canvas-runtime/storage-port.ts index 0429cd9..41e4d28 100644 --- a/src/lib/canvas-runtime/storage-port.ts +++ b/src/lib/canvas-runtime/storage-port.ts @@ -6,19 +6,25 @@ import type { Json } from '../canvas-core/types.ts'; /** Portable durable-store contract. Browser database types belong to the adapter. */ -/** 账号/工作区/项目共同隔离持久事实;同 projectId 不得串用其它账号的存储。 */ +/** 持久键由账号/工作区/项目组成,不含 view/navigation/authEpoch;身份退役另由宿主与 leader 围栏处理。 + * 类型声明不捕获字符串、不验证权限,adapter 应按此作用域隔离事实,不能仅用 projectId 作全局键。 */ export type StorageScope = { readonly accountId: string; readonly workspaceId: string; readonly projectId: string }; /** 可恢复单 Root 图像;formatVersion 是解释器版本,rootRevision 与文档 localRevision 分开。 */ export type RecoveryImage = { readonly formatVersion: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17; readonly rootRevision: number; readonly localRevision: number; readonly payload: Json }; -/** 同一提交携带恢复图像、文件写入/删除及可选共享 depot cut,adapter 保证合同中的原子性。 */ +/** 恢复图像与可选文件增删/depot 清单的同次原子提交合同;不表示对象构造后已保存。 + * files 是 Blob 原字节,image/depot 保存逻辑引用/证明;adapter 需在异步前捕获输入并核对身份/删除资格。 + * 需要删除时还须显式 fileDeletion 能力,不能将独立异步删文件视为与 Root 同一事务。 */ export type DurableBatch = { readonly image: RecoveryImage; readonly deleteFiles?:readonly string[]; readonly depot?:import('./asset-depot.ts').DepotCut; readonly files?: readonly { readonly id: string; readonly bytes: Blob }[] }; -/** 排他写者句柄;fence 区分代次,active 检查加实际存储校验阻止旧 writer 在新 leader 后继续写。 */ +/** 排他写者句柄,fence 标识持有代次,active 可随 release/失效改变;只读类型不代表 active 永远不变。 + * 宿主准入检查不能替代 adapter 在真实 I/O/提交处复核 scope/fence/有效性,接口自身不实现跨进程锁。 */ export type StorageLeader = { readonly scope: StorageScope; readonly fence: number; readonly active: boolean; release(): Promise }; /** 文件盘点只返回元信息,不为扫描加载全部 Blob 字节。 */ export type StoredFileInfo={readonly id:string;readonly byteLength:number;readonly mimeType:string}; /** 分页盘点绑定 rootRevision,nextAfter 是后续页游标,不能跨变更拼接成完整清理依据。 */ export type FileInventoryPage={readonly files:readonly StoredFileInfo[];readonly rootRevision:number;readonly nextAfter?:string}; -/** 宿主实现的存储能力;可选字段必须显式探测,缺能力不能假装已完成扫描或原子删除。 */ +/** 宿主实现的持久协议,声明本身不执行校验/深冻结、获取锁或操作数据库。 + * 可选能力须显式探测;引擎的 ledger generation/Root 发布只是内存事实,commit 成功确认后才推进可靠水位。 + * close 为同步本地句柄收尾,异步 leader.release 独立;失败/取消的 I/O 不自动说明数据从未提交。 */ export type StoragePort = { /** 可选工作区共享资源仓库,文件/引用保留通过 depot 合同处理。 */ readonly assetDepot?:import('./asset-depot.ts').AssetDepotPort; @@ -28,9 +34,11 @@ export type StoragePort = { readonly fileDeletion?: 'atomic-v1'; /** 取得作用域 leader,已有有效写者时拒绝,而不是悄悄允许两个 writer。 */ claim(scope: StorageScope): Promise; - /** 在有效 leader 下恢复最后可靠图像;null 表示没有已提交快照。 */ + /** 恢复最后已提交图像,不直接把任意 payload 当合法 Root;codec/engine 仍需版本、结构与身份校验。 + * null 只表示该作用域没有已提交快照,不证明所有本地文件/共享 depot 记录也不存在。 */ recover(leader: StorageLeader): Promise; - /** 原子提交 DurableBatch 并返回真实完成的 rootRevision,不能提前在请求入队时 resolve。 */ + /** adapter 捕获/验证并原子提交批次,返回实际完成的 durableRootRevision;不能在入队/单条写请求成功时提前 resolve。 + * 回执不直接修改 editor 或放行 scheduler,调用方还需核对水位并按同切点恢复调度。 */ commit(leader: StorageLeader, batch: DurableBatch): Promise<{ readonly durableRootRevision: number }>; /** 读取指定文件的 Blob,缺失用 undefined;不从文档资源引用猜字节已经存在。 */ readFile(leader: StorageLeader, id: string): Promise;