Files
image-tools/src/image/processor-client.ts
T
2026-09-01 02:47:11 +02:00

99 lines
2.9 KiB
TypeScript

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<ProcessImageResult> {
if (supportsWorkerPipeline()) return processInWorker(request, options);
return processImageOnCanvas(request, options);
}
function processInWorker(
request: ProcessImageRequest,
options: {
readonly signal?: AbortSignal;
readonly onStage?: (stage: ProcessingStage) => void;
},
): Promise<ProcessImageResult> {
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"
);
}