From 77f873657947fba8f654a75ade4841abe72c9329 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Apr 2026 11:32:59 +0200 Subject: [PATCH] fix(image-pipeline): skip pre-aborted source bitmap loads --- lib/image-pipeline/source-loader.ts | 2 ++ tests/image-pipeline/source-loader.test.ts | 30 ++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/lib/image-pipeline/source-loader.ts b/lib/image-pipeline/source-loader.ts index 6eb3dbb..dbb5164 100644 --- a/lib/image-pipeline/source-loader.ts +++ b/lib/image-pipeline/source-loader.ts @@ -90,6 +90,8 @@ export async function loadSourceBitmap( throw new Error("ImageBitmap is not available in this environment."); } + throwIfAborted(options.signal); + const promise = getOrCreateSourceBitmapPromise(sourceUrl); return await awaitWithLocalAbort(promise, options.signal); } diff --git a/tests/image-pipeline/source-loader.test.ts b/tests/image-pipeline/source-loader.test.ts index 835629f..cdf0837 100644 --- a/tests/image-pipeline/source-loader.test.ts +++ b/tests/image-pipeline/source-loader.test.ts @@ -64,6 +64,36 @@ describe("loadSourceBitmap", () => { expect(createImageBitmap).toHaveBeenCalledTimes(1); }); + it("does not start fetch/decode work or cache when the signal is already aborted", async () => { + const response = { + ok: true, + status: 200, + blob: vi.fn().mockResolvedValue(blob), + }; + + vi.stubGlobal("fetch", vi.fn().mockResolvedValue(response)); + + const { loadSourceBitmap } = await importSubject(); + const controller = new AbortController(); + controller.abort(); + + await expect( + loadSourceBitmap("https://cdn.example.com/source.png", { + signal: controller.signal, + }), + ).rejects.toMatchObject({ name: "AbortError" }); + + expect(fetch).not.toHaveBeenCalled(); + expect(response.blob).not.toHaveBeenCalled(); + expect(createImageBitmap).not.toHaveBeenCalled(); + + await expect(loadSourceBitmap("https://cdn.example.com/source.png")).resolves.toBe(bitmap); + + expect(fetch).toHaveBeenCalledTimes(1); + expect(response.blob).toHaveBeenCalledTimes(1); + expect(createImageBitmap).toHaveBeenCalledTimes(1); + }); + it("lets a later consumer succeed after an earlier caller aborts", async () => { const responseDeferred = createDeferred<{ ok: boolean;