From 61728f9e525bd58820bd82098e8b5247004e365d Mon Sep 17 00:00:00 2001 From: Matthias Meister Date: Sat, 11 Apr 2026 11:23:02 +0200 Subject: [PATCH] fix(canvas): prevent AbortSignal cloning in render worker --- lib/image-pipeline/image-pipeline.worker.ts | 2 +- lib/image-pipeline/worker-client.ts | 10 +++-- tests/worker-client.test.ts | 42 +++++++++++++++++++++ 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/lib/image-pipeline/image-pipeline.worker.ts b/lib/image-pipeline/image-pipeline.worker.ts index 101cb06..8c7c7cf 100644 --- a/lib/image-pipeline/image-pipeline.worker.ts +++ b/lib/image-pipeline/image-pipeline.worker.ts @@ -21,7 +21,7 @@ type PreviewWorkerPayload = { featureFlags?: BackendFeatureFlags; }; -type FullWorkerPayload = RenderFullOptions & { +type FullWorkerPayload = Omit & { featureFlags?: BackendFeatureFlags; }; diff --git a/lib/image-pipeline/worker-client.ts b/lib/image-pipeline/worker-client.ts index a56b097..4f790db 100644 --- a/lib/image-pipeline/worker-client.ts +++ b/lib/image-pipeline/worker-client.ts @@ -32,7 +32,7 @@ type PreviewWorkerPayload = { featureFlags?: BackendFeatureFlags; }; -type FullWorkerPayload = RenderFullOptions & { +type FullWorkerPayload = Omit & { featureFlags?: BackendFeatureFlags; }; @@ -323,7 +323,7 @@ function runWorkerRequest { + const { signal, ...serializableOptions } = options; + try { return await runWorkerRequest({ kind: "full", payload: { - ...options, + ...serializableOptions, featureFlags: getWorkerFeatureFlagsSnapshot(), }, - signal: options.signal, + signal, }); } catch (error: unknown) { if (isAbortError(error)) { diff --git a/tests/worker-client.test.ts b/tests/worker-client.test.ts index 1aa674f..bc3f3d2 100644 --- a/tests/worker-client.test.ts +++ b/tests/worker-client.test.ts @@ -199,6 +199,48 @@ describe("worker-client fallbacks", () => { expect(bridgeMocks.renderFull).not.toHaveBeenCalled(); }); + it("does not include AbortSignal in full worker payload serialization", async () => { + const workerMessages: WorkerMessage[] = []; + FakeWorker.behavior = (worker, message) => { + workerMessages.push(message); + if (message.kind !== "full") { + return; + } + + queueMicrotask(() => { + worker.onmessage?.({ + data: { + kind: "full-result", + requestId: message.requestId, + payload: createFullResult(), + }, + } as MessageEvent); + }); + }; + vi.stubGlobal("Worker", FakeWorker as unknown as typeof Worker); + + const { renderFullWithWorkerFallback } = await import("@/lib/image-pipeline/worker-client"); + + await renderFullWithWorkerFallback({ + sourceUrl: "https://cdn.example.com/source.png", + steps: [], + render: { + resolution: "original", + format: "png", + }, + signal: new AbortController().signal, + }); + + const fullMessage = workerMessages.find((message) => message.kind === "full") as + | (WorkerMessage & { + payload?: Record; + }) + | undefined; + + expect(fullMessage).toBeDefined(); + expect(fullMessage?.payload).not.toHaveProperty("signal"); + }); + it("still falls back to the main thread when the Worker API is unavailable", async () => { vi.stubGlobal("Worker", undefined);