Files
Endoleap/src/lib/canvas-runtime/orphan-files.ts
T

44 lines
4.1 KiB
TypeScript

import type {Id,Registry} from '../canvas-core/types.ts';
import {equal,fault,freeze,readMap} from '../canvas-core/values.ts';
import type {EngineSnapshot,RuntimeState} from './contracts.ts';
import {importFiles,terminalImport,validFileRetirement,type FileRetirement} from './import-file-retention.ts';
import {resourceRetentionPins} from './resource-retention.ts';
export type OrphanFile={readonly version:1;readonly id:Id;readonly byteLength:number;readonly mimeType:string;readonly digest:string;readonly retirement?:FileRetirement};
export function validateOrphanFile(value:unknown,id:Id):asserts value is OrphanFile {
const file=value as OrphanFile;
if(!file||file.version!==1||file.id!==id||!id||!Number.isSafeInteger(file.byteLength)||file.byteLength<0||typeof file.mimeType!=='string'||typeof file.digest!=='string'||!/^[a-f0-9]{64}$/.test(file.digest)||file.retirement!==undefined&&!validFileRetirement(file.retirement))throw fault('invalid_orphan_file');
}
export function orphanCoverage(snapshot:EngineSnapshot,registry:Registry,displays:ReadonlyMap<Id,number>,temporary:ReadonlyMap<Id,readonly Id[]>) {
const pins=resourceRetentionPins(snapshot,registry,displays,temporary),blockers:string[]=[];
if(!pins.complete)blockers.push('opaque_references');
if(snapshot.runtime.runs.size)blockers.push('opaque_run_records');
if(snapshot.runtime.recoveries.size)blockers.push('recovery_audits');
if([...(snapshot.runtime.imports?.values()??[])].some(job=>!terminalImport(job.status)))blockers.push('unfinished_imports');
if(temporary.has('copy-admission'))blockers.push('copy_admission');
if([...snapshot.runtime.ledger.operations.values()].some(record=>record.phase!=='resolved'&&!['document.change','asset.upload'].includes(record.intent.operationType)))blockers.push('opaque_pending_actions');
return Object.freeze({...pins,complete:blockers.length===0,blockers:Object.freeze(blockers)});
}
/** Registered orphan identities survive repinning. Purged identities cannot be forgotten or reassigned. */
export function prepareOrphanFiles(previous:RuntimeState,next:RuntimeState,coverage:()=>{complete:boolean;pins:ReadonlyMap<Id,unknown>},reserved:ReadonlySet<Id>,referencesChanged:boolean):RuntimeState {
if(!previous.orphanFiles?.size&&!next.orphanFiles?.size)return next;
const hasSoft=[...(next.orphanFiles?.values()??[])].some(file=>file.retirement&&file.retirement.purgedAt===undefined);
if(previous.orphanFiles===next.orphanFiles&&previous.resources===next.resources&&previous.imports===next.imports&&!reserved.size&&!referencesChanged&&!hasSoft)return next;
const sources=importFiles(next.imports);
for(const [id,file]of next.orphanFiles??[]){validateOrphanFile(file,id);if(next.resources.has(id)||sources.has(id))throw fault('local_file_identity_conflict',[id]);}
for(const [id,file]of previous.orphanFiles??[]){const after=next.orphanFiles?.get(id);if(!after||file.retirement?.purgedAt!==undefined&&!equal(file,after))throw fault('local_file_retired',[id]);}
const pins=coverage(),clear:Id[]=[];
for(const [id,file]of next.orphanFiles??[]){
if(reserved.has(id)&&(!pins.complete||pins.pins.has(id)))throw fault('resource_collection_in_progress',[id]);
if(file.retirement?.purgedAt!==undefined){if(pins.pins.has(id))throw fault('local_file_retired',[id]);}
else if(file.retirement&&(!pins.complete||pins.pins.has(id)))clear.push(id);
}
if(!clear.length)return next;
const files=new Map(next.orphanFiles);for(const id of clear){const {retirement:_,...file}=files.get(id)!;files.set(id,freeze(file));}return Object.freeze({...next,orphanFiles:readMap(files)});
}
/** Read-only I/O may finish late; cancellation detaches its result without publishing it. */
export function abortableFileRead<T>(task:Promise<T>,signal:AbortSignal):Promise<T>{
return new Promise((resolve,reject)=>{const abort=()=>{signal.removeEventListener('abort',abort);reject(fault('file_inventory_aborted'));};signal.addEventListener('abort',abort,{once:true});if(signal.aborted)abort();task.then(value=>{signal.removeEventListener('abort',abort);if(!signal.aborted)resolve(value);},error=>{signal.removeEventListener('abort',abort);if(!signal.aborted)reject(error);});});
}