From a945605b3c958ed449775f71b7a2ed48ea7f0309 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Mon, 13 Apr 2026 16:24:16 +0100 Subject: [PATCH] fix(plugin-sdk): avoid leaking queue rejection cleanup --- src/plugin-sdk/keyed-async-queue.test.ts | 26 ++++++++++++++++++++++++ src/plugin-sdk/keyed-async-queue.ts | 5 +++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/plugin-sdk/keyed-async-queue.test.ts b/src/plugin-sdk/keyed-async-queue.test.ts index 65549175fbc..d68cc22a8b9 100644 --- a/src/plugin-sdk/keyed-async-queue.test.ts +++ b/src/plugin-sdk/keyed-async-queue.test.ts @@ -78,6 +78,32 @@ describe("enqueueKeyedTask", () => { await expect(runs[1]()).resolves.toBe("ok"); }); + it("does not leak unhandled rejections when a task failure is already awaited", async () => { + const tails = new Map>(); + const unhandled: unknown[] = []; + const onUnhandledRejection = (reason: unknown) => { + unhandled.push(reason); + }; + process.on("unhandledRejection", onUnhandledRejection); + + try { + await expect( + enqueueKeyedTask({ + tails, + key: "a", + task: async () => { + throw new Error("boom"); + }, + }), + ).rejects.toThrow("boom"); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(unhandled).toEqual([]); + } finally { + process.off("unhandledRejection", onUnhandledRejection); + } + }); + it("runs enqueue/settle hooks once per task", async () => { const tails = new Map>(); const onEnqueue = vi.fn(); diff --git a/src/plugin-sdk/keyed-async-queue.ts b/src/plugin-sdk/keyed-async-queue.ts index 0f07f3c8462..ea33e6f03ce 100644 --- a/src/plugin-sdk/keyed-async-queue.ts +++ b/src/plugin-sdk/keyed-async-queue.ts @@ -23,11 +23,12 @@ export function enqueueKeyedTask(params: { () => undefined, ); params.tails.set(params.key, tail); - void tail.finally(() => { + const cleanup = () => { if (params.tails.get(params.key) === tail) { params.tails.delete(params.key); } - }); + }; + tail.then(cleanup, cleanup); return current; }