fix(runtime): avoid leaking detached cleanup promises

This commit is contained in:
Vincent Koc
2026-04-13 16:42:23 +01:00
parent 74f2c4a56b
commit e157c83c65
4 changed files with 155 additions and 3 deletions

View File

@@ -0,0 +1,37 @@
import { afterEach, describe, expect, it } from "vitest";
import { trackBackgroundTask } from "./last-route.js";
const waitForAsyncCallbacks = async () => {
await Promise.resolve();
await new Promise((resolve) => setTimeout(resolve, 0));
};
describe("trackBackgroundTask", () => {
const unhandledRejections: unknown[] = [];
const onUnhandledRejection = (reason: unknown) => {
unhandledRejections.push(reason);
};
afterEach(() => {
process.off("unhandledRejection", onUnhandledRejection);
unhandledRejections.length = 0;
});
it("does not leak unhandled rejections when a tracked task fails", async () => {
process.on("unhandledRejection", onUnhandledRejection);
const backgroundTasks = new Set<Promise<unknown>>();
let rejectTask!: (reason?: unknown) => void;
const task = new Promise<void>((_resolve, reject) => {
rejectTask = reject;
});
trackBackgroundTask(backgroundTasks, task);
expect(backgroundTasks.size).toBe(1);
rejectTask(new Error("boom"));
await waitForAsyncCallbacks();
expect(backgroundTasks.size).toBe(0);
expect(unhandledRejections).toEqual([]);
});
});

View File

@@ -9,9 +9,10 @@ export function trackBackgroundTask(
task: Promise<unknown>,
) {
backgroundTasks.add(task);
void task.finally(() => {
const cleanup = () => {
backgroundTasks.delete(task);
});
};
task.then(cleanup, cleanup);
}
export function updateLastRouteInBackground(params: {