/** * 可恢复解码状态合同与只读图片计算池。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;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并发;取消结果与执行容量分开,防止无视 abort 的任务挤爆池。 */ export function createImageDecodePool(options:{execute:(bytes:Blob,signal:AbortSignal)=>Promise;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(),active=new Map();let next=0,closed=false; /** 幂等 settle 调用者,成功捕获冻结尺寸;不提前归还仍在执行的 active 槽。 */ 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!}));} /** FIFO 取任务并在微任务执行,校验返回尺寸,最后 finally 删除 active 再继续泵;异常不毒化后续队列。 */ function pump(){while(!closed&&active.size{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 校验图片 MIME/容量并建立任务;close 取消所有调用结果,active 实际结束仍由 finally 收尾。 */ 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((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'));}}}); }