Yield recovery container encoding without changing durable cuts
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import type { Json } from '../canvas-core/types.ts';
|
||||
import { fault, freeze, validJson } from '../canvas-core/values.ts';
|
||||
import {isDataContainer} from '../canvas-core/data-containers.ts';
|
||||
import {createImportBudget, type ImportScheduling} from './import-budget.ts';
|
||||
import type { RecoveryImage } from './storage-port.ts';
|
||||
|
||||
type Encoded = {value:Json|undefined;height:number;reusable:boolean;bytes:number};
|
||||
@@ -8,7 +10,7 @@ function utf8Length(text:string){let bytes=0;for(let i=0;i<text.length;i++){cons
|
||||
const scalarBytes=(value:unknown)=>utf8Length(JSON.stringify(value));
|
||||
|
||||
/** JSON value semantics, with reuse only for transitively frozen plain data properties. */
|
||||
export function encodeRecoveryValue(input:unknown):Json {
|
||||
function createEncoder() {
|
||||
const visiting=new Set<object>();
|
||||
function visit(value:unknown,depth:number,key:string):Encoded {
|
||||
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)};}
|
||||
@@ -50,13 +52,81 @@ export function encodeRecoveryValue(input:unknown):Json {
|
||||
const encoded={value:Object.freeze(result) as Json,height,reusable,bytes};encodedSizes.set(result,bytes);if(reusable)immutable.set(object,encoded);return encoded;
|
||||
}finally{visiting.delete(object);}
|
||||
}
|
||||
const result=visit(input,0,'');if(result.value===undefined)throw fault('invalid_recovery_payload');return result.value;
|
||||
return visit;
|
||||
}
|
||||
export function encodeRecoveryValue(input:unknown):Json {
|
||||
const result=createEncoder()(input,0,'');if(result.value===undefined)throw fault('invalid_recovery_payload');return result.value;
|
||||
}
|
||||
|
||||
/** Only codec-owned, validated immutable images qualify for the storage copy fast path. */
|
||||
export function sealRecoveryImage(formatVersion:1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17,rootRevision:number,localRevision:number,payload:unknown):RecoveryImage {
|
||||
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');
|
||||
const encoded=encodeRecoveryValue(payload),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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
/** Only codec-owned, validated immutable images qualify for the storage copy fast path. */
|
||||
export function sealRecoveryImage(formatVersion:ImageVersion,rootRevision:number,localRevision:number,payload:unknown):RecoveryImage {
|
||||
validateHeader(formatVersion,rootRevision,localRevision);
|
||||
return finishImage(formatVersion,rootRevision,localRevision,encodeRecoveryValue(payload));
|
||||
}
|
||||
|
||||
type Prepared = 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 cached=immutable.get(value);
|
||||
if(cached){if(depth+cached.height>64)throw fault('invalid_recovery_payload');return cached;}
|
||||
// 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(depth>64||visiting.has(value))throw fault('invalid_recovery_payload');
|
||||
visiting.add(value);
|
||||
try {
|
||||
const keys=Array.isArray(value)?undefined:Object.keys(value),children:Prepared[]=[];
|
||||
if(keys)for(const name of keys)children.push(prepare((value as Record<string,unknown>)[name],depth+1,name));
|
||||
else for(let index=0;index<(value as unknown[]).length;index++)children.push(prepare((value as unknown[])[index],depth+1,String(index)));
|
||||
return {source:value,depth,children,keys};
|
||||
}finally{visiting.delete(value);}
|
||||
}
|
||||
return prepare(input,0,'');
|
||||
}
|
||||
|
||||
/** 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. */
|
||||
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 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>{
|
||||
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);
|
||||
reusable&&=child.reusable;
|
||||
if(array){
|
||||
if(child.value===undefined&&plan.depth+1>64)throw fault('invalid_recovery_payload');
|
||||
(result as Json[]).push(child.value??null);bytes+=(index?1:0)+(child.value===undefined?4:child.bytes);height=Math.max(height,1+child.height);
|
||||
}else if(child.value!==undefined){
|
||||
const name=plan.keys![index];bytes+=(properties++?1:0)+scalarBytes(name)+1+child.bytes;
|
||||
Object.defineProperty(result,name,{value:child.value,enumerable:true});height=Math.max(height,1+child.height);
|
||||
}
|
||||
const pause=budget.step();if(pause)yield pause;
|
||||
}
|
||||
const encoded={value:Object.freeze(result) as Json,height,reusable,bytes};encodedSizes.set(result,bytes);if(reusable)immutable.set(plan.source,encoded);return encoded;
|
||||
}
|
||||
const iterator=encode(plan);let step=iterator.next();while(!step.done){await step.value;step=iterator.next();}
|
||||
if(step.value.value===undefined)throw fault('invalid_recovery_payload');
|
||||
return finishImage(formatVersion,rootRevision,localRevision,step.value.value);
|
||||
}
|
||||
export function isSealedRecoveryImage(image:RecoveryImage):boolean{return sealed.has(image);}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user