53 lines
4.5 KiB
TypeScript
53 lines
4.5 KiB
TypeScript
/**
|
|
* 把已捕获的恢复图像编码成精确 JSON UTF-8 Blob,按 token/片段分批让出。
|
|
* 序列化不发布部分 Root;只能对 codec-owned 规范化 JSON 使用分片快速路径,外部图像保留原生防御路径。
|
|
* 最终 Blob 大小必须匹配编码器的精确证明,避免悄悄变更检查点字节表示。
|
|
*/
|
|
import type {RecoveryImage} from './storage-port.ts';
|
|
import {isSealedRecoveryImage,sealedRecoveryByteLength,encodedRecoveryByteLength} from './recovery-encoding.ts';
|
|
import {createImportBudget,type ImportScheduling} from './import-budget.ts';
|
|
import {fault} from '../canvas-core/values.ts';
|
|
|
|
/** 按最多 4096 UTF-16 单元切原字符串,跨边界代理对一起留到下段;孤立代理由原生 JSON 转义。
|
|
* 长串只输出一对外层引号,各段去掉 stringify 引号后拼接,保持完整字符串的转义/字节语义。
|
|
* 段上限不是转义后字符/UTF-8 字节上限,单段 stringify 仍是同步工作。 */
|
|
function* stringTokens(value:string):Generator<string>{
|
|
if(value.length<=4096){yield JSON.stringify(value);return;}
|
|
yield '"';
|
|
for(let start=0;start<value.length;){let end=Math.min(start+4096,value.length);const a=value.charCodeAt(end-1),b=value.charCodeAt(end);if(a>=0xd800&&a<=0xdbff&&b>=0xdc00&&b<=0xdfff)end--;yield JSON.stringify(value.slice(start,end)).slice(1,-1);start=end;}
|
|
yield '"';
|
|
}
|
|
/** Only traverse codec-owned normalized JSON. Small owned subtrees use native stringify. */
|
|
/** 只遍历已封存的规范 JSON:数组按索引、对象按 Object.keys 顺序输出,显式生成分隔符。
|
|
* codec 有精确 UTF-8 大小事实且 ≤16KiB 的对象/数组直接原生 stringify;没有大小事实不猜其成本。
|
|
* 不自行解释 toJSON/getter、稀疏/undefined 或执行深度校验;这些应在封存捕获阶段已规范化/约束。 */
|
|
function* tokens(value:unknown):Generator<string>{
|
|
if(value&&typeof value==='object'){
|
|
const size=encodedRecoveryByteLength(value);if(size!==undefined&&size<=16384){yield JSON.stringify(value);return;}
|
|
if(Array.isArray(value)){yield '[';for(let i=0;i<value.length;i++){if(i)yield ',';yield* tokens(value[i]);}yield ']';return;}
|
|
yield '{';let first=true;for(const key of Object.keys(value)){if(!first)yield ',';first=false;yield* stringTokens(key);yield ':';yield* tokens((value as Record<string,unknown>)[key]);}yield '}';return;
|
|
}
|
|
if(typeof value==='string')yield* stringTokens(value);else yield JSON.stringify(value);
|
|
}
|
|
/** Preserve exact JSON.stringify bytes; scheduling never exposes a partial recovery image. */
|
|
/** 返回完整 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<Blob>{
|
|
options.check?.();
|
|
// 外部输入不信任所有权/尺寸缓存,继续走原生同步 stringify,不能冒称这一路也全部可分片。
|
|
if(!isSealedRecoveryImage(image)){const bytes=new Blob([JSON.stringify(image)],{type:'application/json'});options.check?.();return bytes;}
|
|
// 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;
|
|
// 检查身份后让出,恢复执行再检查;避免异步期间已退役的 writer 继续采用结果。
|
|
if(length>=8192){parts.push(pending.join(''));pending=[];length=0;options.check?.();await budget.step(128);options.check?.();}
|
|
}
|
|
if(pending.length)parts.push(pending.join(''));options.check?.();
|
|
const bytes=new Blob(parts,{type:'application/json'});if(bytes.size!==sealedRecoveryByteLength(image))throw fault('recovery_encoding_size_mismatch');return bytes;
|
|
}
|