Budget growing history and import recovery encoding
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import {dataRecord} from '../canvas-core/data-containers.ts';
|
||||
import {createImportReadProof,IMPORT_PROOF_BLOCK_BYTES} from './import-read-proof.ts';
|
||||
import {isImportByteFailure} from './import-byte-reader.ts';
|
||||
import {assertImportEvidence,importSeals,importsMaySend} from './import-verification.ts';
|
||||
@@ -674,7 +675,7 @@ export async function openProjectEngine(options: EngineOptions) {
|
||||
const context=proofReaders.get(job.id),current=editor.getSnapshot().runtime.imports?.get(job.id)?.source;
|
||||
if(!context||context.fileId!==job.source.fileId)throw fault('import_source_unavailable');
|
||||
const {sha256:_,integrityError:__,...source}=job.source;
|
||||
return {...job,source:{...source,readProof:context.reader.getProof(),...(current?.sha256?{sha256:current.sha256}:{}),...(current?.integrityError?{integrityError:current.integrityError}:{})}};
|
||||
return dataRecord(freeze({...job,source:{...source,readProof:context.reader.getProof(),...(current?.sha256?{sha256:current.sha256}:{}),...(current?.integrityError?{integrityError:current.integrityError}:{})}}));
|
||||
}
|
||||
function failImportVerification(job:ImportJob,error:unknown){
|
||||
const code=(error as {code?:string}).code??'import_source_verification_failed';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {dataRecord} from '../canvas-core/data-containers.ts';
|
||||
import type {Id} from '../canvas-core/types.ts';
|
||||
import {fault} from '../canvas-core/values.ts';
|
||||
type Ids=Readonly<Record<string,Id>>;
|
||||
@@ -20,7 +21,7 @@ export function createImportIdentities(){
|
||||
(ids as Record<string,Id>)[key]=id;used.add(id);return id;
|
||||
},
|
||||
has:(id:Id)=>used.has(id),
|
||||
finish:():Ids=>owned?{...ids}:ids,
|
||||
finish:():Ids=>owned?dataRecord(ids):ids,
|
||||
});
|
||||
};
|
||||
return Object.freeze(Object.assign(capture,{clear(){cached=undefined;}}));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {dataArray,dataRecord} from '../canvas-core/data-containers.ts';
|
||||
import {createImportBatches} from './import-batches.ts';
|
||||
import {createImportIdentities} from './import-identities.ts';
|
||||
import {validateImportReadProof,type ImportReadProof} from './import-read-proof.ts';
|
||||
@@ -146,7 +147,7 @@ export function createImportRunner(ports:ImportRunnerPorts){
|
||||
else if(!equal(job.source,value.source))throw fault('invalid_import_checkpoint');
|
||||
return value;
|
||||
}
|
||||
function next(job:ImportJob,patch:Partial<ImportJob>):ImportJob{const value={...job,...patch,version:3 as const,generation:job.generation+1};if(value.error===undefined)delete value.error;return freeze(checkpointEvidence(value));}
|
||||
function next(job:ImportJob,patch:Partial<ImportJob>):ImportJob{const value={...job,...patch,version:3 as const,generation:job.generation+1};if(value.error===undefined)delete value.error;const checked=checkpointEvidence(value);return checked===value?dataRecord(freeze(value)):freeze(checked);}
|
||||
function publish(job:ImportJob,patch:Partial<ImportJob>){const value=next(job,patch);ports.publish(value,job.generation);if(!equal(current(),value))throw fault('invalid_import_publish');return value;}
|
||||
function prepare(line:ImportLine,job:ImportJob){
|
||||
const ids=identities(job.stableIds,allocateIdentity);
|
||||
@@ -161,7 +162,7 @@ export function createImportRunner(ports:ImportRunnerPorts){
|
||||
function completed(job:ImportJob,units:readonly ImportUnit[],pending:readonly ImportLocator[]){
|
||||
const bindings:Record<Id,ImportBinding>=Object.assign(Object.create(null),job.bindings??{});
|
||||
for(const unit of units)for(const change of unit.changes)if(change.kind==='element'&&change.before===null&&change.after)bindings[change.id]={kind:'element',incarnation:change.after.incarnation};else if(change.kind==='canvas'&&change.before===null&&change.after)bindings[change.id]={kind:'canvas',...(change.after.parent?{entryElementId:change.after.parent.entryElementId}:{})};
|
||||
return next(job,{pending,committedSourceKeys:[...job.committedSourceKeys,...units.map(unit=>unit.key)],bindings:{...bindings}});
|
||||
return next(job,{pending,committedSourceKeys:dataArray(freeze([...job.committedSourceKeys,...units.map(unit=>unit.key)])),bindings:dataRecord(freeze({...bindings}))});
|
||||
}
|
||||
function commit(units:readonly ImportUnit[],job:ImportJob,value:ImportJob){const base=current();if(base.generation!==job.generation)throw fault('stale_import_job');value=freeze(checkpointEvidence(value));const start=now(),result=ports.commit(units,value,job.generation),elapsed=Math.max(0,now()-start);if(!['applied','rejected'].includes(result?.status)||!equal(current(),result.status==='applied'?value:base))throw fault('invalid_import_commit');if(result.status==='applied')batches.record(units.length,elapsed);return result;}
|
||||
async function pump(signal:AbortSignal){
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user