fix(ssr): clear the budget timer once a raced read answers

The mood and dashboard prefetches and the sign-in location lookup raced
their work against a bare setTimeout that was never cleared, leaving
one live timer behind per render or sign-in after the work had already
answered. A shared helper races the work and clears the timer.
This commit is contained in:
Marc-André Bombeck committed 2026-10-08 22:57:47 +02:00
1 parent dd76b434c4
commit b4e3e22d43
5 files changed
+64 -17

No files matched your search

@@ -0,0 +1,34 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { settleWithin } from "../settle-within";
afterEach(() => {
vi.useRealTimers();
});
describe("settleWithin", () => {
it("answers with the work when it lands inside the budget, and leaves no timer", async () => {
vi.useFakeTimers();
await expect(
settleWithin(Promise.resolve("done"), 1_500, null),
).resolves.toBe("done");
// A bare race left the budget timer pending after the work answered.
expect(vi.getTimerCount()).toBe(0);
});
it("answers with the fallback once the budget runs out", async () => {
vi.useFakeTimers();
const pending = settleWithin(new Promise<string>(() => {}), 1_500, null);
await vi.advanceTimersByTimeAsync(1_500);
await expect(pending).resolves.toBeNull();
expect(vi.getTimerCount()).toBe(0);
});
it("passes a rejection inside the budget through, and clears the timer", async () => {
vi.useFakeTimers();
await expect(
settleWithin(Promise.reject(new Error("boom")), 1_500, null),
).rejects.toThrow("boom");
expect(vi.getTimerCount()).toBe(0);
});
});
+24
View File
@@ -0,0 +1,24 @@
/**
* Wait for `work` at most `ms`, then settle with `fallback` instead.
*
* The budget timer is cleared as soon as either side settles. A bare
* `Promise.race` against a `setTimeout` left the timer running after the
* work had long answered: one live timer per server render or per audited
* sign-in, each holding its closure until it fired. `work` is not
* cancelled; a rejection of `work` inside the budget rejects here too.
*/
export async function settleWithin<T, F>(
work: Promise<T>,
ms: number,
fallback: F,
): Promise<T | F> {
let timer: ReturnType<typeof setTimeout> | undefined;
const budget = new Promise<F>((resolve) => {
timer = setTimeout(() => resolve(fallback), ms);
});
try {
return await Promise.race([work, budget]);
} finally {
clearTimeout(timer);
}
}