25 lines
2.7 KiB
TypeScript
25 lines
2.7 KiB
TypeScript
import {fault} from '../canvas-core/values.ts';
|
|
import {importSourceDigest} from './import-integrity.ts';
|
|
export type DigestPort={digest(bytes:Blob,signal?:AbortSignal):Promise<string>;close():void};
|
|
/** Read-only compute scheduling: bounded active slots + FIFO Map, never durable operation facts. */
|
|
export function createDigestPool(options:{execute?:(bytes:Blob,signal:AbortSignal)=>Promise<string>;concurrency?:number;maxQueued?:number}={}):DigestPort{
|
|
const concurrency=options.concurrency??2,maxQueued=options.maxQueued??64,execute=options.execute??importSourceDigest;
|
|
if(!Number.isSafeInteger(concurrency)||concurrency<1||concurrency>8||!Number.isSafeInteger(maxQueued)||maxQueued<1)throw fault('invalid_digest_budget');
|
|
type Task={id:number;bytes:Blob;controller:AbortController;resolve:(value:string)=>void;reject:(error:unknown)=>void;cleanup:()=>void;done:boolean};
|
|
const waiting=new Map<number,Task>(),active=new Map<number,Task>();let next=0,closed=false;
|
|
function finish(task:Task,error?:unknown,value?:string){if(task.done)return;task.done=true;task.cleanup();waiting.delete(task.id);if(error)task.reject(error);else task.resolve(value!);}
|
|
function pump(){
|
|
if(closed)return;
|
|
while(active.size<concurrency&&waiting.size){const task=waiting.values().next().value!;waiting.delete(task.id);active.set(task.id,task);
|
|
void Promise.resolve().then(()=>{if(task.controller.signal.aborted)throw fault('import_aborted');return execute(task.bytes,task.controller.signal);}).then(value=>{if(task.controller.signal.aborted)throw fault('import_aborted');if(!/^[a-f0-9]{64}$/.test(value))throw fault('invalid_digest_response');finish(task,undefined,value);},error=>finish(task,error)).catch(error=>finish(task,error)).finally(()=>{active.delete(task.id);pump();});
|
|
}
|
|
}
|
|
return Object.freeze({
|
|
digest(bytes:Blob,signal?:AbortSignal):Promise<string>{
|
|
if(closed)return Promise.reject(fault('digest_pool_closed'));if(signal?.aborted)return Promise.reject(fault('import_aborted'));if(!(bytes instanceof Blob))return Promise.reject(fault('invalid_digest_input'));if(active.size>=concurrency&&waiting.size>=maxQueued)return Promise.reject(fault('digest_capacity'));
|
|
return new Promise((resolve,reject)=>{const task:Task={id:++next,bytes,controller:new AbortController(),resolve,reject,cleanup:()=>signal?.removeEventListener('abort',abort),done:false},abort=()=>{task.controller.abort();finish(task,fault('import_aborted'));pump();};signal?.addEventListener('abort',abort,{once:true});waiting.set(task.id,task);pump();});
|
|
},
|
|
close(){if(closed)return;closed=true;for(const task of [...waiting.values(),...active.values()]){task.controller.abort();finish(task,fault('import_aborted'));}},
|
|
});
|
|
}
|