import { processImageOnCanvas, type ProcessingStage } from "./canvas-pipeline"; import { ImageToolError, type ImageErrorCode } from "./errors"; import { IMAGE_LIMITS } from "./limits"; import type { ProcessImageRequest, ProcessImageResult } from "./types"; let requestSequence = 0; export async function processImage( request: ProcessImageRequest, options: { readonly signal?: AbortSignal; readonly onStage?: (stage: ProcessingStage) => void; } = {}, ): Promise { if (supportsWorkerPipeline()) return processInWorker(request, options); return processImageOnCanvas(request, options); } function processInWorker( request: ProcessImageRequest, options: { readonly signal?: AbortSignal; readonly onStage?: (stage: ProcessingStage) => void; }, ): Promise { const id = ++requestSequence; return new Promise((resolve, reject) => { const worker = new Worker( new URL("../workers/image.worker.ts", import.meta.url), { type: "module", name: `image-tools-${id}`, }, ); let settled = false; const finish = (callback: () => void) => { if (settled) return; settled = true; window.clearTimeout(timeout); options.signal?.removeEventListener("abort", abort); worker.terminate(); callback(); }; const abort = () => { finish(() => reject( new ImageToolError("ABORTED", "The image operation was cancelled."), ), ); }; const timeout = window.setTimeout(() => { finish(() => reject( new ImageToolError( "PROCESSING_TIMEOUT", `Image processing exceeded the ${IMAGE_LIMITS.processingTimeoutMs / 1000}-second safety limit.`, ), ), ); }, IMAGE_LIMITS.processingTimeoutMs); options.signal?.addEventListener("abort", abort, { once: true }); worker.onerror = () => { finish(() => reject( new ImageToolError( "DECODE_FAILED", "The image worker stopped unexpectedly.", ), ), ); }; worker.onmessage = ( event: MessageEvent< | { id: number; type: "progress"; stage: ProcessingStage } | { id: number; type: "result"; result: ProcessImageResult } | { id: number; type: "error"; code: ImageErrorCode; message: string } >, ) => { const data = event.data; if (data.id !== id || settled) return; if (data.type === "progress") { options.onStage?.(data.stage); } else if (data.type === "result") { finish(() => resolve(data.result)); } else { finish(() => reject(new ImageToolError(data.code, data.message))); } }; if (options.signal?.aborted) abort(); else worker.postMessage({ id, request }); }); } function supportsWorkerPipeline(): boolean { return ( typeof Worker !== "undefined" && typeof OffscreenCanvas !== "undefined" ); }