Reuse preallocated import identities and stable recovery document views
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import type {Id} from '../canvas-core/types.ts';
|
||||
import {fault} from '../canvas-core/values.ts';
|
||||
type Ids=Readonly<Record<string,Id>>;
|
||||
/** One runner's last immutable ID index. Copies are deferred until allocation. */
|
||||
export function createImportIdentities(){
|
||||
let cached:{ids:Ids;used:Set<Id>}|undefined;
|
||||
const capture=(input:Ids,createId:()=>Id)=>{
|
||||
let identity=cached?.ids===input?cached:undefined;
|
||||
if(!identity){
|
||||
const reusable=Object.isFrozen(input)&&Object.values(Object.getOwnPropertyDescriptors(input)).every(property=>Object.hasOwn(property,'value')&&typeof property.value==='string');
|
||||
const ids=reusable?input:Object.assign(Object.create(null),input),used=new Set<Id>(Object.values(ids));identity={ids,used};if(reusable)cached=identity;
|
||||
}
|
||||
let ids=identity.ids,used=identity.used,owned=ids!==input;
|
||||
return Object.freeze({
|
||||
reserve(key:string){
|
||||
if(typeof key!=='string'||!key)throw fault('invalid_import_key');
|
||||
if(Object.hasOwn(ids,key))return ids[key];
|
||||
const id=createId();if(typeof id!=='string'||!id||used.has(id))throw fault('duplicate_import_identity');
|
||||
if(!owned){ids=Object.assign(Object.create(null),ids);used=new Set(used);owned=true;}
|
||||
(ids as Record<string,Id>)[key]=id;used.add(id);return id;
|
||||
},
|
||||
has:(id:Id)=>used.has(id),
|
||||
finish:():Ids=>owned?{...ids}:ids,
|
||||
});
|
||||
};
|
||||
return Object.freeze(Object.assign(capture,{clear(){cached=undefined;}}));
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import {createImportIdentities} from './import-identities.ts';
|
||||
import {validateImportReadProof,type ImportReadProof} from './import-read-proof.ts';
|
||||
import {readImportRange,type ImportByteReader} from './import-byte-reader.ts';
|
||||
import {createImportBudget,type ImportScheduling} from './import-budget.ts';
|
||||
@@ -130,6 +131,7 @@ export function createImportRunner(ports:ImportRunnerPorts){
|
||||
if(initial.source?.kind==='duplicate-snapshot')throw fault('import_interpreter_unavailable');
|
||||
if(initial.source&&(initial.source.integrityVersion===2?!ports.checkpointSource:!initial.source.sha256||initial.source.integrityVersion!==1))throw fault('import_source_integrity_unavailable');
|
||||
if(!Number.isFinite(budget)||budget<=0||![maxPending,maxBytes,maxBatch].every(value=>Number.isSafeInteger(value)&&value>0))throw fault('invalid_import_budget');
|
||||
const identities=createImportIdentities(),allocateIdentity=()=>ports.createId();
|
||||
let costPerUnit:number|undefined;
|
||||
let active:Promise<void>|undefined,controller:AbortController|undefined,closed=false,restart=false;const holds=new Set<symbol>();
|
||||
const report=(error:unknown)=>{try{ports.onError(error);}catch{/* Reporting cannot change imported content. */}};
|
||||
@@ -146,13 +148,12 @@ export function createImportRunner(ports:ImportRunnerPorts){
|
||||
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 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:Record<string,Id>=Object.assign(Object.create(null),job.stableIds),used=new Set(Object.values(ids));
|
||||
const reserve=(key:string)=>{if(typeof key!=='string'||!key)throw fault('invalid_import_key');if(Object.hasOwn(ids,key))return ids[key];const id=ports.createId();if(typeof id!=='string'||!id||used.has(id))throw fault('duplicate_import_identity');ids[key]=id;used.add(id);return id;};
|
||||
const unit=ports.prepare(line,job,reserve);
|
||||
const ids=identities(job.stableIds,allocateIdentity);
|
||||
const unit=ports.prepare(line,job,ids.reserve);
|
||||
if(!validJson(unit)||typeof unit.key!=='string'||!unit.key||!Array.isArray(unit.changes)||!Array.isArray(unit.dependencies)||!unit.changes.length||unit.changes.length>10000||new Set(unit.dependencies).size!==unit.dependencies.length||unit.dependencies.some(key=>typeof key!=='string'||!key))throw fault('invalid_import_unit');
|
||||
for(const change of unit.changes)if(change.kind!=='field'&&change.before===null&&change.after&&!used.has(change.id))throw fault('unstable_import_identity');
|
||||
for(const change of unit.changes)if(change.kind!=='field'&&change.before===null&&change.after&&!ids.has(change.id))throw fault('unstable_import_identity');
|
||||
if(new TextEncoder().encode(JSON.stringify(unit)).byteLength>maxBytes)throw fault('import_unit_capacity');
|
||||
return{unit:freeze(clone(unit)),ids:{...ids}};
|
||||
return{unit:freeze(clone(unit)),ids:ids.finish()};
|
||||
}
|
||||
const checkpoint=async(stage:'parsed'|'committed'='committed')=>{if(ports.optimisticLocal&&ports.awaitDurabilityBudget)await ports.awaitDurabilityBudget(stage);else await ports.persist();};
|
||||
const digest=async(unit:ImportUnit)=>[...new Uint8Array(await crypto.subtle.digest('SHA-256',new TextEncoder().encode(JSON.stringify(unit))))].map(value=>value.toString(16).padStart(2,'0')).join('');
|
||||
@@ -273,7 +274,7 @@ export function createImportRunner(ports:ImportRunnerPorts){
|
||||
if(closed||controller?.signal.aborted)return;
|
||||
try{const job=current();if(['running','completed','partial'].includes(job.status)){publish(job,{status:'paused',error:(error as {code?:string}).code??'import_failed'});try{await ports.persist();}catch{/* Storage may still be unavailable. */}}}catch{/* A replaced identity cannot receive an old error. */}
|
||||
report(error);
|
||||
}).finally(()=>{active=undefined;controller=undefined;if(restart&&!closed&&!holds.size){try{if(current().status==='running')void launch();}catch{/* Replaced scope stays untouched. */}}});
|
||||
}).finally(()=>{identities.clear();active=undefined;controller=undefined;if(restart&&!closed&&!holds.size){try{if(current().status==='running')void launch();}catch{/* Replaced scope stays untouched. */}}});
|
||||
return active;
|
||||
}
|
||||
return Object.freeze({
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { Change, CoreSnapshot, Document, HistoryEntry, HistoryState, Id, Js
|
||||
import { validateHistoryReplay, replayHistoryEvidence, sameHistory } from './history-replay.ts';
|
||||
import type { RecoveryAudit } from './conflicts.ts';
|
||||
import { applyHistoryTransition, validateHistoryTransition, type HistoryTransition } from '../canvas-core/history-transition.ts';
|
||||
import { createDocument, validateDocument } from '../canvas-core/document.ts';
|
||||
import { createDocument, validateDocument, isFactoryDocument } from '../canvas-core/document.ts';
|
||||
import { fault, freeze, readMap, validJson, equal } from '../canvas-core/values.ts';
|
||||
import type { EngineSnapshot, RuntimeState } from './contracts.ts';
|
||||
import type { FrozenRequest, LedgerSnapshot, OperationRecord } from './ledger.ts';
|
||||
@@ -21,7 +21,16 @@ import { validateRunInput, type RunRecord } from './runs.ts';
|
||||
import type { ResourceRecord } from './resources.ts';
|
||||
import type { RecoveryImage } from './storage-port.ts';
|
||||
|
||||
const documentValue = (document: Document) => ({ projectId: document.projectId, canvases: [...document.canvases.values()], elements: [...document.elements.values()], tombstones: [...document.tombstones] });
|
||||
function encodeDocumentValue(document:Document){
|
||||
// Freeze our own containers only. The codec still independently proves whether
|
||||
// each nested value can be reused, including external mutable records.
|
||||
return Object.freeze({projectId:document.projectId,canvases:Object.freeze([...document.canvases.values()]),elements:Object.freeze([...document.elements.values()]),tombstones:Object.freeze([...document.tombstones].map(pair=>Object.freeze(pair)))});
|
||||
}
|
||||
const documentViews=new WeakMap<Document,ReturnType<typeof encodeDocumentValue>>();
|
||||
function documentValue(document:Document){
|
||||
if(!isFactoryDocument(document))return encodeDocumentValue(document);
|
||||
let value=documentViews.get(document);if(!value){value=encodeDocumentValue(document);documentViews.set(document,value);}return value;
|
||||
}
|
||||
/** Plain records only: functions, credentials, renderer objects, URLs and Map methods never serialize. */
|
||||
export function encodeRecovery(snapshot: EngineSnapshot): RecoveryImage {
|
||||
const payload = {
|
||||
|
||||
Reference in New Issue
Block a user