Budget growing history and import recovery encoding

This commit is contained in:
2026-11-11 04:16:21 +08:00
parent b4c40d3087
commit 210bc14074
11 changed files with 120 additions and 22 deletions
+18 -7
View File
@@ -71,21 +71,30 @@ export function sealRecoveryImage(formatVersion:ImageVersion,rootRevision:number
return finishImage(formatVersion,rootRevision,localRevision,encodeRecoveryValue(payload));
}
type Prepared = Encoded | {source:object;depth:number;children:Prepared[];keys?:string[]};
type Scalar = null|string|number|boolean|undefined|symbol;
type Prepared = Scalar | Encoded | {source:object;depth:number;children:Prepared[];keys?:string[]};
// Read unknown objects synchronously, before the first suspension. Shape-owned
// containers can include external values; each such occurrence is captured now.
// Only our fresh container shape and previously proven encoded values defer.
function prepareEncoding(input:unknown) {
const normalize=createEncoder(),visiting=new Set<object>();
function prepare(value:unknown,depth:number,key:string):Prepared {
if(!value||typeof value!=='object')return normalize(value,depth,key);
const kind=typeof value;
if(value===null||kind==='string'||kind==='number'||kind==='boolean'||kind==='undefined'||kind==='symbol'){
if(depth>64&&value!==undefined&&typeof value!=='symbol')throw fault('invalid_recovery_payload');
// Primitives cannot change while suspended. Defer normalization/UTF-8 work
// as well as allocation of their Encoded facts to the budgeted traversal.
return value as Scalar;
}
if(typeof value!=='object')return normalize(value,depth,key);
const cached=immutable.get(value);
if(cached){if(depth+cached.height>64)throw fault('invalid_recovery_payload');return cached;}
if(!isDataContainer(value))return normalize(value,depth,key);
// Prototype toJSON changes must retain ordinary JSON semantics, including
// accessors. Inspect descriptors without evaluating them an extra time.
let prototype:object|null=value,hasToJSON=false;
while(prototype){if(Object.getOwnPropertyDescriptor(prototype,'toJSON')){hasToJSON=true;break;}prototype=Object.getPrototypeOf(prototype);}
if(!isDataContainer(value)||hasToJSON)return normalize(value,depth,key);
if(hasToJSON)return normalize(value,depth,key);
if(depth>64||visiting.has(value))throw fault('invalid_recovery_payload');
visiting.add(value);
try {
@@ -95,7 +104,7 @@ function prepareEncoding(input:unknown) {
return {source:value,depth,children,keys};
}finally{visiting.delete(value);}
}
return prepare(input,0,'');
return {plan:prepare(input,0,''),normalize};
}
/** Capture external data immediately; cooperatively encode stable owned tables.
@@ -103,15 +112,17 @@ function prepareEncoding(input:unknown) {
* The returned image has exactly the synchronous codec's wire representation. */
export async function sealRecoveryImageAsync(formatVersion:ImageVersion,rootRevision:number,localRevision:number,payload:unknown,options:ImportScheduling={}):Promise<RecoveryImage> {
validateHeader(formatVersion,rootRevision,localRevision);
const plan=prepareEncoding(payload);
const {plan,normalize}=prepareEncoding(payload);
const scheduler=(globalThis as typeof globalThis & {scheduler?:{yield?:()=>Promise<void>}}).scheduler;
const budget=createImportBudget(new AbortController().signal,{...options,yieldTask:options.yieldTask??(typeof scheduler?.yield==='function'?()=>scheduler.yield!():undefined)});
function* encode(plan:Prepared):Generator<Promise<void>,Encoded,void>{
function* encode(plan:Prepared,depth=0,key=''):Generator<Promise<void>,Encoded,void>{
if(plan===null||typeof plan!=='object')return normalize(plan,depth,key);
if('height' in plan)return plan;
let height=0,bytes=2,reusable=true,properties=0;
const array=plan.keys===undefined,result:Json[]|Record<string,Json>=array?[]:{};
for(let index=0;index<plan.children.length;index++){
const entry=plan.children[index],child='height' in entry?entry:yield* encode(entry);
const entry=plan.children[index],name=plan.keys?.[index]??String(index);
const child=entry===null||typeof entry!=='object'?normalize(entry,plan.depth+1,name):'height' in entry?entry:yield* encode(entry,plan.depth+1,name);
reusable&&=child.reusable;
if(array){
if(child.value===undefined&&plan.depth+1>64)throw fault('invalid_recovery_payload');