40 lines
5.6 KiB
TypeScript
40 lines
5.6 KiB
TypeScript
/**
|
||
* 可恢复解码状态合同与只读图片计算池。Blob 属于文件仓库,解码仅产生尺寸,不重编码/替换原字节。
|
||
* 等待/执行都在内存调度,结果由 engine 校验生命周期后写入资源事实;不能直接在回调中改元素。
|
||
*/
|
||
import type {Json} from '../canvas-core/types.ts';
|
||
import {fault,freeze} from '../canvas-core/values.ts';
|
||
/** 解码器返回的正整数宽高,不包含临时 bitmap 或浏览器对象。 */
|
||
export type ImageDimensions={readonly width:number;readonly height:number};
|
||
/** 当前 v1 的 pending/ready/failed;ready 记录尺寸来源为 decode 或 server。 */
|
||
type KnownImageDecodeState={readonly version:1;readonly status:'pending'}|{readonly version:1;readonly status:'ready';readonly width:number;readonly height:number;readonly source:'decode'|'server'}|{readonly version:1;readonly status:'failed';readonly code:string};
|
||
/** 保留未来版本记录,恢复时不能因不识别就过滤;实际解释/回收要检查版本。 */
|
||
export type ImageDecodeState=KnownImageDecodeState|Readonly<{version:number;[key:string]:Json|undefined}>;
|
||
/** 宿主可注入 Worker 或其它解码执行器,close 属于计算池生命周期,不属于 view。 */
|
||
export type ImageDecoderPort={decode(bytes:Blob,signal?:AbortSignal):Promise<ImageDimensions>;close():void};
|
||
/** 这里只验证正安全整数和乘积可安全表示,不等同于已应用产品的像素预算。 */
|
||
export function validImageDimensions(value:unknown):value is ImageDimensions{const size=value as ImageDimensions;return !!size&&Number.isSafeInteger(size.width)&&size.width>0&&Number.isSafeInteger(size.height)&&size.height>0&&Number.isSafeInteger(size.width*size.height);}
|
||
/** Bounded read-only compute. Cancellation rejects the caller; a slot remains occupied until execution settles. */
|
||
/** 默认 2 并发/64 等待,最多 8 并发;waiting/active 各用按插入顺序的 Map,不是持久任务队列。
|
||
* maxQueued 只限制尚未进入 active 的任务;active 包括已取消但执行器仍未退出的任务。
|
||
* 池只检查 Blob 的 image/ MIME 前缀和尺寸数值,不限制文件字节/实际像素预算,也不缓存解码结果。 */
|
||
export function createImageDecodePool(options:{execute:(bytes:Blob,signal:AbortSignal)=>Promise<ImageDimensions>;concurrency?:number;maxQueued?:number}):ImageDecoderPort{
|
||
const concurrency=options.concurrency??2,maxQueued=options.maxQueued??64;
|
||
if(!Number.isSafeInteger(concurrency)||concurrency<1||concurrency>8||!Number.isSafeInteger(maxQueued)||maxQueued<1)throw fault('invalid_image_decode_budget');
|
||
/** 每个解码请求独立控制器/完成标记;cleanup 解除外部取消监听,迟到执行结果不再采纳。 */
|
||
type Task={id:number;bytes:Blob;controller:AbortController;resolve:(size:ImageDimensions)=>void;reject:(error:unknown)=>void;done:boolean;cleanup():void};
|
||
const waiting=new Map<number,Task>(),active=new Map<number,Task>();let next=0,closed=false;
|
||
/** 幂等 settle 调用者并清理等待项;成功浅复制/冻结执行器结果,扩展属性不会被过滤。
|
||
* error 用真值区分成败,执行器应以 Error 等真值拒绝;falsy rejection 不保证按失败解释。
|
||
* 不删除 active,取消后的名额仍要等 execute 链的 finally 才归还。 */
|
||
function finish(task:Task,error?:unknown,size?:ImageDimensions){if(task.done)return;task.done=true;task.cleanup();waiting.delete(task.id);if(error)task.reject(error);else task.resolve(freeze({...size!}));}
|
||
/** 取 waiting 首项立即占 active,再在微任务调用 execute;启动前复查内部 abort。
|
||
* 成功尺寸先校验再 finish;正常错误交 finish,finally 负责释放 active 并继续 FIFO。
|
||
* execute 若不响应取消且永不 settle,名额会持续占用;池没有超时或强制终止通用执行器的能力。 */
|
||
function pump(){while(!closed&&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('image_decode_aborted');return options.execute(task.bytes,task.controller.signal);}).then(size=>{if(!validImageDimensions(size))throw fault('invalid_image_dimensions');finish(task,undefined,size);}).catch(error=>finish(task,error)).finally(()=>{active.delete(task.id);pump();});}}
|
||
/** decode 拒绝关闭/预先取消/非图片 Blob/等待容量已满,随后创建任务、绑定外部 abort 并入 waiting。
|
||
* abort 转发给内部 controller,立即拒绝调用者并移除 waiting;运行中的 active 留待 finally。
|
||
* close 幂等取消全部调用结果并禁止继续泵,不返回 drain Promise,也不证明所有 execute 已停止。 */
|
||
return Object.freeze({decode(bytes:Blob,signal?:AbortSignal){if(closed)return Promise.reject(fault('image_decoder_closed'));if(signal?.aborted)return Promise.reject(fault('image_decode_aborted'));if(!(bytes instanceof Blob)||!bytes.type.startsWith('image/'))return Promise.reject(fault('invalid_image_input'));if(active.size>=concurrency&&waiting.size>=maxQueued)return Promise.reject(fault('image_decode_capacity'));return new Promise<ImageDimensions>((resolve,reject)=>{const abort=()=>{task.controller.abort();finish(task,fault('image_decode_aborted'));pump();},task:Task={id:++next,bytes,controller:new AbortController(),resolve,reject,done:false,cleanup:()=>signal?.removeEventListener('abort',abort)};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('image_decode_aborted'));}}});
|
||
}
|