diff --git a/apps/bulldozer-js/src/index.ts b/apps/bulldozer-js/src/index.ts index 01bbf6036..04a3e92d1 100644 --- a/apps/bulldozer-js/src/index.ts +++ b/apps/bulldozer-js/src/index.ts @@ -1088,7 +1088,9 @@ runAsynchronously(async () => { memory: serviceMemoryUsage(), }, { suppressInNodeEnvDevelopment: true }); } - await wait(1000); + // The HTTP server owns this periodic loop's lifetime; after it closes, + // waiting for the next tick must not keep the process alive. + await wait(1000, { unref: true }); }); } }); diff --git a/packages/shared/src/utils/promises.tsx b/packages/shared/src/utils/promises.tsx index 36f7c77c1..88776203e 100644 --- a/packages/shared/src/utils/promises.tsx +++ b/packages/shared/src/utils/promises.tsx @@ -259,7 +259,7 @@ export function concatStacktracesIfRejected(promise: Promise): void { }); } -export async function wait(ms: number) { +export async function wait(ms: number, options?: { unref?: boolean }) { if (!Number.isFinite(ms) || ms < 0) { throw new HexclaveAssertionError(`wait() requires a non-negative integer number of milliseconds to wait. (found: ${ms}ms)`); } @@ -267,7 +267,10 @@ export async function wait(ms: number) { throw new HexclaveAssertionError("The maximum timeout for wait() is 2147483647ms (2**31 - 1). (found: ${ms}ms)"); } return await traceSpan({ description: 'wait(...)', attributes: { 'stack.wait.ms': ms } }, async (span) => { - return await new Promise(resolve => setTimeout(resolve, ms)); + return await new Promise((resolve) => { + const timeout = setTimeout(resolve, ms); + if (options?.unref === true && typeof timeout === "object") timeout.unref(); + }); }); } import.meta.vitest?.test("wait", async ({ expect }) => { @@ -280,6 +283,10 @@ import.meta.vitest?.test("wait", async ({ expect }) => { // Test with zero await expect(wait(0)).resolves.toBeUndefined(); + // Node's unref option must preserve the normal wait behavior while another + // referenced handle (the test runner) owns the process lifetime. + await expect(wait(0, { unref: true })).resolves.toBeUndefined(); + // Test with negative number await expect(wait(-10)).rejects.toThrow("wait() requires a non-negative integer"); diff --git a/scripts/reseed-bulldozer.sh b/scripts/reseed-bulldozer.sh index 544d8f88d..f990543d1 100755 --- a/scripts/reseed-bulldozer.sh +++ b/scripts/reseed-bulldozer.sh @@ -23,7 +23,7 @@ echo "Starting temporary bulldozer-js on port $BULLDOZER_PORT ..." # Run the server directly (single node process, so we can reliably kill it) via # tsx's node loader — the same entrypoint the package's `start` script uses. ( - cd "$BULLDOZER_DIR" && exec node --import tsx --expose-gc src/index.ts + cd "$BULLDOZER_DIR" && NODE_ENV=development exec node --import tsx --expose-gc src/index.ts ) & BULLDOZER_PID=$!