c2ebacaInitial commit: numbl image filter web appJeremy Magland 1/** Promise-based wrapper around the filter Web Worker. */
2import type {
3 FilterRequest,
4 FilterResponse,
5 FilterSuccess,
6} from "./filterTypes.ts";
8export interface ImageInput {
9 width: number;
10 height: number;
11 rgba: Uint8ClampedArray;
12}
14export interface FilterClient {
15 run(script: string, image: ImageInput): Promise<FilterSuccess>;
16 terminate(): void;
17}
19export function createFilterClient(): FilterClient {
20 const worker = new Worker(new URL("./filter.worker.ts", import.meta.url), {
21 type: "module",
22 });
24 let nextId = 1;
25 const pending = new Map<
26 number,
27 { resolve: (r: FilterSuccess) => void; reject: (e: Error) => void }
28 >();
30 worker.onmessage = (e: MessageEvent<FilterResponse>) => {
31 const res = e.data;
32 const entry = pending.get(res.id);
33 if (!entry) return;
34 pending.delete(res.id);
35 if (res.ok) {
36 entry.resolve(res);
37 } else {
38 const err = new Error(res.error) as Error & { logs?: string[] };
39 err.logs = res.logs;
40 entry.reject(err);
41 }
42 };
44 worker.onerror = (e) => {
45 // A worker-level error rejects everything in flight.
46 const err = new Error(e.message || "Filter worker crashed");
47 for (const { reject } of pending.values()) reject(err);
48 pending.clear();
49 };
51 return {
52 run(script, image) {
53 return new Promise<FilterSuccess>((resolve, reject) => {
54 const id = nextId++;
55 pending.set(id, { resolve, reject });
56 // Copy the bytes so the caller's source image stays intact after the
57 // buffer is transferred to the worker.
58 const rgba = image.rgba.slice();
59 const req: FilterRequest = {
60 id,
61 script,
62 width: image.width,
63 height: image.height,
64 rgba,
65 };
66 worker.postMessage(req, [rgba.buffer]);
67 });
68 },
69 terminate() {
70 worker.terminate();
71 pending.clear();
72 },
73 };
74}