diff --git a/application.test.ts b/application.test.ts index 6e628175..f404d956 100644 --- a/application.test.ts +++ b/application.test.ts @@ -1105,10 +1105,8 @@ Deno.test({ }); Deno.test({ - name: "Application.listen() - no options", + name: "Application.listen() - no options - aborted before onListen", // ignore: isNode(), - ignore: true, // there is a challenge with serve and the abort controller that - // needs to be isolated async fn() { const controller = new AbortController(); const app = new Application(); @@ -1118,11 +1116,47 @@ Deno.test({ const { signal } = controller; const p = app.listen({ signal }); controller.abort(); - await p; + assertRejects( + async () => await p, + "aborted prematurely before 'listen' event", + ); teardown(); }, }); +Deno.test({ + name: "Application.listen() - no options - aborted after onListen", + async fn() { + const controller = new AbortController(); + const app = new Application(); + app.use((ctx) => { + ctx.response.body = "hello world"; + }); + const { signal } = controller; + const p = app.listen({ signal }); + app.addEventListener("listen", async () => controller.abort()); + const GRACEFUL_TIME = 1000; + let timer: number | undefined; + const raceResult = await Promise.race([ + new Promise(async (resolve) => { + await p; + clearTimeout(timer); + resolve("resolved cleanly"); + }), + new Promise((resolve) => + timer = setTimeout( + () => resolve("likely forever pending"), + GRACEFUL_TIME, + ) + ), + ]); + assert( + raceResult === "resolved cleanly", + `'listen promise' should resolve before ${GRACEFUL_TIME} ms`, + ); + }, +}); + Deno.test({ name: "Application load correct default server", ignore: isNode(), // this just hangs on node, because we can't close down diff --git a/http_server_native.ts b/http_server_native.ts index 1b3dddfa..61c114e2 100644 --- a/http_server_native.ts +++ b/http_server_native.ts @@ -79,6 +79,12 @@ export class Server> const { signal } = this.#options; const { onListen, ...options } = this.#options; const { promise, resolve } = createPromiseWithResolvers(); + if (signal?.aborted) { + // if user somehow aborted before `listen` is invoked, we throw + return Promise.reject( + new Error("aborted prematurely before 'listen' event"), + ); + } this.#stream = new ReadableStream({ start: (controller) => { this.#httpServer = serve?.({ @@ -96,6 +102,11 @@ export class Server> signal, ...options, }); + // closinng stream, so that the Application listen promise can resolve itself + // https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultController/close + signal?.addEventListener("abort", () => controller.close(), { + once: true, + }); }, });