/** Promise-based wrapper around the filter Web Worker. */ import type { FilterRequest, FilterResponse, FilterSuccess, } from "./filterTypes.ts"; export interface ImageInput { width: number; height: number; rgba: Uint8ClampedArray; } export interface FilterClient { run(script: string, image: ImageInput): Promise; terminate(): void; } export function createFilterClient(): FilterClient { const worker = new Worker(new URL("./filter.worker.ts", import.meta.url), { type: "module", }); let nextId = 1; const pending = new Map< number, { resolve: (r: FilterSuccess) => void; reject: (e: Error) => void } >(); worker.onmessage = (e: MessageEvent) => { const res = e.data; const entry = pending.get(res.id); if (!entry) return; pending.delete(res.id); if (res.ok) { entry.resolve(res); } else { const err = new Error(res.error) as Error & { logs?: string[] }; err.logs = res.logs; entry.reject(err); } }; worker.onerror = (e) => { // A worker-level error rejects everything in flight. const err = new Error(e.message || "Filter worker crashed"); for (const { reject } of pending.values()) reject(err); pending.clear(); }; return { run(script, image) { return new Promise((resolve, reject) => { const id = nextId++; pending.set(id, { resolve, reject }); // Copy the bytes so the caller's source image stays intact after the // buffer is transferred to the worker. const rgba = image.rgba.slice(); const req: FilterRequest = { id, script, width: image.width, height: image.height, rgba, }; worker.postMessage(req, [rgba.buffer]); }); }, terminate() { worker.terminate(); pending.clear(); }, }; }