From 722ffda607106cb07378766a6ecc4a10a527eb2c Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Mon, 12 Sep 2022 16:18:47 +0300 Subject: [PATCH] Wait until (#116) * feat(server): waitUntil * Better tests * Remove get-port * Node 14 --- .changeset/twenty-papayas-invite.md | 5 +++ jest.config.js | 7 ++++ packages/server/src/index.ts | 44 ++++++++++++++++++--- packages/server/test/node.spec.ts | 59 +++++++++++++++++++++++++++++ 4 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 .changeset/twenty-papayas-invite.md create mode 100644 packages/server/test/node.spec.ts diff --git a/.changeset/twenty-papayas-invite.md b/.changeset/twenty-papayas-invite.md new file mode 100644 index 0000000000..41954ab644 --- /dev/null +++ b/.changeset/twenty-papayas-invite.md @@ -0,0 +1,5 @@ +--- +'@whatwg-node/server': minor +--- + +Implement `waitUntil` diff --git a/jest.config.js b/jest.config.js index 4a527c9d6e..05fd2c7721 100644 --- a/jest.config.js +++ b/jest.config.js @@ -5,6 +5,7 @@ const CI = !!process.env.CI; const ROOT_DIR = __dirname; const TSCONFIG = resolve(ROOT_DIR, 'tsconfig.json'); const tsconfig = require(TSCONFIG); +const ESM_PACKAGES = []; module.exports = { testEnvironment: 'node', @@ -15,6 +16,12 @@ module.exports = { moduleNameMapper: pathsToModuleNameMapper(tsconfig.compilerOptions.paths, { prefix: `${ROOT_DIR}/`, }), + transformIgnorePatterns: [`node_modules/(?!(${ESM_PACKAGES.join('|')})/)`], + transform: { + '^.+\\.mjs?$': 'babel-jest', + '^.+\\.ts?$': 'babel-jest', + '^.+\\.js$': 'babel-jest', + }, collectCoverage: false, cacheDirectory: resolve(ROOT_DIR, `${CI ? '' : 'node_modules/'}.cache/jest`), }; diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 8cb8320615..13842347ce 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -53,10 +53,21 @@ export type ServerAdapter = TBaseObject & ServerAdapterObject['fetch'] & ServerAdapterObject; +function handleWaitUntils(waitUntilPromises: Promise[]) { + return Promise.allSettled(waitUntilPromises).then(waitUntils => + waitUntils.forEach(waitUntil => { + if (waitUntil.status === 'rejected') { + console.error(waitUntil.reason); + } + }) + ); +} + export function createServerAdapter< TServerContext = { req: NodeRequest; res: ServerResponse; + waitUntil(promise: Promise): void; }, TBaseObject = unknown >({ @@ -77,18 +88,23 @@ export function createServerAdapter< } async function requestListener(nodeRequest: NodeRequest, serverResponse: ServerResponse) { + const waitUntilPromises: Promise[] = []; const response = await handleNodeRequest(nodeRequest, { req: nodeRequest, res: serverResponse, - waitUntil(p: Promise) { - p.catch(err => console.error(err)); + waitUntil(p: Promise) { + waitUntilPromises.push(p); }, } as any); if (response) { - return sendNodeResponse(response, serverResponse); + await sendNodeResponse(response, serverResponse); } else { - return new Promise(resolve => serverResponse.end(resolve)); + await new Promise(resolve => { + serverResponse.statusCode = 404; + serverResponse.end(resolve); + }); } + await handleWaitUntils(waitUntilPromises); } function handleEvent(event: FetchEvent) { @@ -108,7 +124,7 @@ export function createServerAdapter< handle: requestListener, }; - function genericRequestHandler(input: any, ctx: any) { + function genericRequestHandler(input: any, ctx: any, ...rest: any[]) { if ('process' in globalThis && process.versions?.['bun'] != null) { // This is required for bun input.text(); @@ -128,6 +144,24 @@ export function createServerAdapter< } // Or is it Request itself? // Then ctx is present and it is the context + if (rest?.length > 0) { + ctx = Object.assign({}, ctx, ...rest); + } + if (!ctx.waitUntil) { + const waitUntilPromises: Promise[] = []; + ctx.waitUntil = (p: Promise) => { + waitUntilPromises.push(p); + }; + const response$ = handleRequest(input, { + ...ctx, + waitUntil(p: Promise) { + waitUntilPromises.push(p); + }, + }); + if (waitUntilPromises.length > 0) { + return handleWaitUntils(waitUntilPromises).then(() => response$); + } + } return handleRequest(input, ctx); } diff --git a/packages/server/test/node.spec.ts b/packages/server/test/node.spec.ts new file mode 100644 index 0000000000..f5333ecd88 --- /dev/null +++ b/packages/server/test/node.spec.ts @@ -0,0 +1,59 @@ +import { createServerAdapter } from '@whatwg-node/server'; +import { createServer, Server } from 'http'; +import { fetch } from '@whatwg-node/fetch'; + +describe('Node Specific Cases', () => { + let port = 9876; + let server: Server | undefined; + afterEach(done => { + if (server) { + server.close(err => { + if (err) { + throw err; + } + server = undefined; + done(); + }); + } else { + done(); + } + port = Math.floor(Math.random() * 1000) + 9800; + }); + it('should handle empty responses', async () => { + const serverAdapter = createServerAdapter({ + async handleRequest() { + return undefined as any; + }, + }); + server = createServer(serverAdapter); + await new Promise(resolve => server!.listen(port, resolve)); + const response = await fetch(`http://localhost:${port}`); + await response.text(); + expect(response.status).toBe(404); + }); + it('should handle waitUntil properly', async () => { + let flag = false; + const serverAdapter = createServerAdapter({ + handleRequest(_request, { waitUntil }) { + waitUntil( + Promise.resolve().then(() => { + flag = true; + }) + ); + return Promise.resolve( + new Response(null, { + status: 204, + }) + ); + }, + }); + server = createServer(serverAdapter); + await new Promise(resolve => server!.listen(port, resolve)); + const response$ = fetch(`http://localhost:${port}`); + expect(flag).toBe(false); + const response = await response$; + await response.text(); + expect(flag).toBe(true); + expect.assertions(2); + }); +});