From 030e9fccde7657ff66bc5e306e9453a4a6c93539 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Thu, 1 Jun 2023 14:37:29 +0200 Subject: [PATCH 1/9] use uwebsockets --- package.json | 3 +- src/use/uWebSockets.ts | 86 ++++++++++++++++++++++++++++++++++++++++++ yarn.lock | 8 ++++ 3 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 src/use/uWebSockets.ts diff --git a/package.json b/package.json index 1bee6163..c4ab51a0 100644 --- a/package.json +++ b/package.json @@ -147,7 +147,8 @@ "tslib": "^2.5.2", "typedoc": "^0.24.7", "typedoc-plugin-markdown": "^3.15.3", - "typescript": "^5.0.4" + "typescript": "^5.0.4", + "uWebSockets.js": "uNetworking/uWebSockets.js#v20.30.0" }, "resolutions": { "npm/libnpmversion": "^3.0.6" diff --git a/src/use/uWebSockets.ts b/src/use/uWebSockets.ts new file mode 100644 index 00000000..9a226a90 --- /dev/null +++ b/src/use/uWebSockets.ts @@ -0,0 +1,86 @@ +import type { HttpRequest, HttpResponse } from 'uWebSockets.js'; +import { + createHandler as createRawHandler, + HandlerOptions as RawHandlerOptions, + OperationContext, +} from '../handler'; + +/** + * The context in the request for the handler. + * + * @category Server/uWebSockets + */ +export interface RequestContext { + res: HttpResponse; +} + +/** + * Handler options when using the http adapter. + * + * @category Server/uWebSockets + */ +export type HandlerOptions = + RawHandlerOptions; + +/** + * Create a GraphQL over HTTP spec compliant request handler for + * the Node environment [uWebSockets.js module](https://github.com/uNetworking/uWebSockets.js/). + * + * ```js + * import uWS from 'uWebSockets.js'; // yarn add uWebSockets.js@uNetworking/uWebSockets.js#v20.30.0 + * import { createHandler } from 'graphql-http/lib/use/uWebSockets'; + * import { schema } from './my-graphql-step'; + * + * uWS + * .App() + * .any('/graphql', createHandler({ schema })) + * .listen(4000, () => { + * console.log('Listening to port 4000'); + * }); + * ``` + * + * @category Server/uWebSockets + */ +export function createHandler( + options: HandlerOptions, +): (res: HttpResponse, req: HttpRequest) => Promise { + const handle = createRawHandler(options); + return async function requestListener(res, req) { + try { + const [body, init] = await handle({ + url: req.getUrl(), + method: req.getMethod(), + headers: { get: req.getHeader }, + body: () => + new Promise((resolve) => { + let body = ''; + res.onData((chunk, isLast) => { + body += chunk; + if (isLast) { + resolve(body); + } + }); + }), + raw: req, + context: { res }, + }); + + res.writeStatus(`${init.status} ${init.statusText}`); + for (const [key, val] of Object.entries(init.headers || {})) { + res.writeHeader(key, val); + } + if (body) { + res.end(body); + } + } catch (err) { + // The handler shouldnt throw errors. + // If you wish to handle them differently, consider implementing your own request handler. + console.error( + 'Internal error occurred during request handling. ' + + 'Please check your implementation.', + err, + ); + res.writeStatus('500 Internal Server Error').endWithoutBody(); + } + }; +} diff --git a/yarn.lock b/yarn.lock index 6f8a4afb..56b5793e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7650,6 +7650,7 @@ __metadata: typedoc: ^0.24.7 typedoc-plugin-markdown: ^3.15.3 typescript: ^5.0.4 + uWebSockets.js: "uNetworking/uWebSockets.js#v20.30.0" peerDependencies: graphql: ">=0.11 <=16" languageName: unknown @@ -13246,6 +13247,13 @@ __metadata: languageName: node linkType: hard +"uWebSockets.js@uNetworking/uWebSockets.js#v20.30.0": + version: 20.30.0 + resolution: "uWebSockets.js@https://github.com/uNetworking/uWebSockets.js.git#commit=d39d4181daf5b670d44cbc1b18f8c28c85fd4142" + checksum: e8584a9aa00ea378647fe4530631ad10240d61bb7fa70aff4f44cab3b066432e404358d4d80a77c5c56e25029ae6dc6f4de13673da89c38cb4d8228acde74187 + languageName: node + linkType: hard + "ua-parser-js@npm:^0.7.30": version: 0.7.35 resolution: "ua-parser-js@npm:0.7.35" From 5c6b472ce818c5840314bbc6ad065a75b1c12d62 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Thu, 1 Jun 2023 14:37:45 +0200 Subject: [PATCH 2/9] in pkg --- package.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/package.json b/package.json index c4ab51a0..f77bacc0 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,11 @@ "require": "./lib/use/koa.js", "import": "./lib/use/koa.mjs" }, + "./lib/use/uWebSockets": { + "types": "./lib/use/uWebSockets.d.ts", + "require": "./lib/use/uWebSockets.js", + "import": "./lib/use/uWebSockets.mjs" + }, "./package.json": "./package.json" }, "types": "lib/index.d.ts", From 835d1b6b7f30533f38248494786b7b43940501e7 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Thu, 1 Jun 2023 14:39:02 +0200 Subject: [PATCH 3/9] use version --- src/use/uWebSockets.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/use/uWebSockets.ts b/src/use/uWebSockets.ts index 9a226a90..1a822e50 100644 --- a/src/use/uWebSockets.ts +++ b/src/use/uWebSockets.ts @@ -27,7 +27,7 @@ export type HandlerOptions = * the Node environment [uWebSockets.js module](https://github.com/uNetworking/uWebSockets.js/). * * ```js - * import uWS from 'uWebSockets.js'; // yarn add uWebSockets.js@uNetworking/uWebSockets.js#v20.30.0 + * import uWS from 'uWebSockets.js'; // yarn add uWebSockets.js@uNetworking/uWebSockets.js# * import { createHandler } from 'graphql-http/lib/use/uWebSockets'; * import { schema } from './my-graphql-step'; * From 77b713540b51da0209ab54bc273bcb77af668b50 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Thu, 1 Jun 2023 14:39:23 +0200 Subject: [PATCH 4/9] readme --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 0a87f83d..b678512b 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,21 @@ app.listen({ port: 4000 }); console.log('Listening to port 4000'); ``` +##### With [`uWebSockets.js`](https://github.com/uNetworking/uWebSockets.js) + +```js +import uWS from 'uWebSockets.js'; // yarn add uWebSockets.js@uNetworking/uWebSockets.js# +import { createHandler } from 'graphql-http/lib/use/uWebSockets'; +import { schema } from './previous-step'; + +uWS + .App() + .any('/graphql', createHandler({ schema })) + .listen(4000, () => { + console.log('Listening to port 4000'); + }); +``` + ##### With [`Deno`](https://deno.land/) ```ts From 12981dd904e8ef08ac0dc9a8b1354050e8a547f8 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Thu, 1 Jun 2023 15:05:20 +0200 Subject: [PATCH 5/9] endwithoutbody --- src/use/uWebSockets.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/use/uWebSockets.ts b/src/use/uWebSockets.ts index 1a822e50..f7cb886e 100644 --- a/src/use/uWebSockets.ts +++ b/src/use/uWebSockets.ts @@ -71,6 +71,8 @@ export function createHandler( } if (body) { res.end(body); + } else { + res.endWithoutBody(); } } catch (err) { // The handler shouldnt throw errors. From 205def8340bc44eeb85e0fc186eb51516ec5e8b8 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Thu, 1 Jun 2023 15:30:53 +0200 Subject: [PATCH 6/9] fixes and tests --- src/__tests__/use.ts | 38 ++++++++++++++++++++++++++++++ src/use/uWebSockets.ts | 52 +++++++++++++++++++++++++++--------------- 2 files changed, 71 insertions(+), 19 deletions(-) diff --git a/src/__tests__/use.ts b/src/__tests__/use.ts index 70cf5e07..c207def8 100644 --- a/src/__tests__/use.ts +++ b/src/__tests__/use.ts @@ -5,6 +5,7 @@ import fastify from 'fastify'; import Koa from 'koa'; import mount from 'koa-mount'; import { createServerAdapter } from '@whatwg-node/server'; +import uWS from 'uWebSockets.js'; import { startDisposableServer } from './utils/tserver'; import { serverAudits } from '../audits'; import { schema } from './fixtures/simple'; @@ -14,6 +15,7 @@ import { createHandler as createExpressHandler } from '../use/express'; import { createHandler as createFastifyHandler } from '../use/fastify'; import { createHandler as createFetchHandler } from '../use/fetch'; import { createHandler as createKoaHandler } from '../use/koa'; +import { createHandler as createUWSHandler } from '../use/uWebSockets'; describe('http', () => { const [url, , dispose] = startDisposableServer( @@ -219,3 +221,39 @@ describe('koa', () => { await dispose(); }); }); + +describe('uWebSockets.js', () => { + let url = ''; + beforeAll(async () => { + // get available port by starting a temporary server + const [availableUrl, availablePort, dispose] = startDisposableServer( + http.createServer(), + ); + await dispose(); + url = availableUrl; + + new Promise((resolve, reject) => { + uWS + .App() + .any('/', createUWSHandler({ schema })) + .listen(availablePort, (listenSocket) => { + if (!listenSocket) { + reject(new Error('Unavailable uWS listen socket')); + } else { + resolve(); + } + }); + }); + }); + + // TODO: dispose of app afterAll + + for (const audit of serverAudits({ url: () => url, fetchFn: fetch })) { + it(audit.name, async () => { + const result = await audit.fn(); + if (result.status !== 'ok') { + throw result.reason; + } + }); + } +}); diff --git a/src/use/uWebSockets.ts b/src/use/uWebSockets.ts index f7cb886e..984c362a 100644 --- a/src/use/uWebSockets.ts +++ b/src/use/uWebSockets.ts @@ -46,33 +46,45 @@ export function createHandler( ): (res: HttpResponse, req: HttpRequest) => Promise { const handle = createRawHandler(options); return async function requestListener(res, req) { + let aborted = false; + res.onAborted(() => (aborted = true)); try { + let url = req.getUrl(); + const query = req.getQuery(); + if (query) { + url += '?' + query; + } const [body, init] = await handle({ - url: req.getUrl(), - method: req.getMethod(), - headers: { get: req.getHeader }, + url, + method: req.getMethod().toUpperCase(), + headers: { get: (key) => req.getHeader(key) }, body: () => new Promise((resolve) => { let body = ''; - res.onData((chunk, isLast) => { - body += chunk; - if (isLast) { - resolve(body); - } - }); + if (aborted) { + resolve(body); + } else { + res.onData((chunk, isLast) => { + body += Buffer.from(chunk, 0, chunk.byteLength).toString(); + if (isLast) { + resolve(body); + } + }); + } }), raw: req, context: { res }, }); - - res.writeStatus(`${init.status} ${init.statusText}`); - for (const [key, val] of Object.entries(init.headers || {})) { - res.writeHeader(key, val); - } - if (body) { - res.end(body); - } else { - res.endWithoutBody(); + if (!aborted) { + res.writeStatus(`${init.status} ${init.statusText}`); + for (const [key, val] of Object.entries(init.headers || {})) { + res.writeHeader(key, val); + } + if (body) { + res.end(body); + } else { + res.endWithoutBody(); + } } } catch (err) { // The handler shouldnt throw errors. @@ -82,7 +94,9 @@ export function createHandler( 'Please check your implementation.', err, ); - res.writeStatus('500 Internal Server Error').endWithoutBody(); + if (!aborted) { + res.writeStatus('500 Internal Server Error').endWithoutBody(); + } } }; } From b489ad990f7d9f09550f5db4da0c61019fbf8eee Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Thu, 1 Jun 2023 15:37:48 +0200 Subject: [PATCH 7/9] uws listen socket close --- src/__tests__/use.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/__tests__/use.ts b/src/__tests__/use.ts index c207def8..75022394 100644 --- a/src/__tests__/use.ts +++ b/src/__tests__/use.ts @@ -224,6 +224,7 @@ describe('koa', () => { describe('uWebSockets.js', () => { let url = ''; + let appListenSocket: uWS.us_listen_socket; beforeAll(async () => { // get available port by starting a temporary server const [availableUrl, availablePort, dispose] = startDisposableServer( @@ -240,13 +241,14 @@ describe('uWebSockets.js', () => { if (!listenSocket) { reject(new Error('Unavailable uWS listen socket')); } else { + appListenSocket = listenSocket; resolve(); } }); }); }); - // TODO: dispose of app afterAll + afterAll(() => uWS.us_listen_socket_close(appListenSocket)); for (const audit of serverAudits({ url: () => url, fetchFn: fetch })) { it(audit.name, async () => { From cce1eb05fb953c74bb1764c0987974a7dc666ae0 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Thu, 1 Jun 2023 15:51:19 +0200 Subject: [PATCH 8/9] my schema --- src/use/uWebSockets.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/use/uWebSockets.ts b/src/use/uWebSockets.ts index 984c362a..6fefda15 100644 --- a/src/use/uWebSockets.ts +++ b/src/use/uWebSockets.ts @@ -29,7 +29,7 @@ export type HandlerOptions = * ```js * import uWS from 'uWebSockets.js'; // yarn add uWebSockets.js@uNetworking/uWebSockets.js# * import { createHandler } from 'graphql-http/lib/use/uWebSockets'; - * import { schema } from './my-graphql-step'; + * import { schema } from './my-graphql-schema'; * * uWS * .App() From cc96413073c744ca8f589a31f659b7182e69117d Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Thu, 1 Jun 2023 15:54:09 +0200 Subject: [PATCH 9/9] docs --- docs/README.md | 1 + .../use_uWebSockets.RequestContext.md | 19 +++++ docs/modules/use_uWebSockets.md | 82 +++++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 docs/interfaces/use_uWebSockets.RequestContext.md create mode 100644 docs/modules/use_uWebSockets.md diff --git a/docs/README.md b/docs/README.md index 9df702bd..6eabc3ba 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,3 +19,4 @@ graphql-http - [use/http2](modules/use_http2.md) - [use/koa](modules/use_koa.md) - [use/node](modules/use_node.md) +- [use/uWebSockets](modules/use_uWebSockets.md) diff --git a/docs/interfaces/use_uWebSockets.RequestContext.md b/docs/interfaces/use_uWebSockets.RequestContext.md new file mode 100644 index 00000000..2098d09d --- /dev/null +++ b/docs/interfaces/use_uWebSockets.RequestContext.md @@ -0,0 +1,19 @@ +[graphql-http](../README.md) / [use/uWebSockets](../modules/use_uWebSockets.md) / RequestContext + +# Interface: RequestContext + +[use/uWebSockets](../modules/use_uWebSockets.md).RequestContext + +The context in the request for the handler. + +## Table of contents + +### Properties + +- [res](use_uWebSockets.RequestContext.md#res) + +## Properties + +### res + +• **res**: `HttpResponse` diff --git a/docs/modules/use_uWebSockets.md b/docs/modules/use_uWebSockets.md new file mode 100644 index 00000000..fb312080 --- /dev/null +++ b/docs/modules/use_uWebSockets.md @@ -0,0 +1,82 @@ +[graphql-http](../README.md) / use/uWebSockets + +# Module: use/uWebSockets + +## Table of contents + +### Interfaces + +- [RequestContext](../interfaces/use_uWebSockets.RequestContext.md) + +### Type Aliases + +- [HandlerOptions](use_uWebSockets.md#handleroptions) + +### Functions + +- [createHandler](use_uWebSockets.md#createhandler) + +## Server/uWebSockets + +### HandlerOptions + +Ƭ **HandlerOptions**<`Context`\>: [`HandlerOptions`](../interfaces/handler.HandlerOptions.md)<`HttpRequest`, [`RequestContext`](../interfaces/use_uWebSockets.RequestContext.md), `Context`\> + +Handler options when using the http adapter. + +#### Type parameters + +| Name | Type | +| :------ | :------ | +| `Context` | extends [`OperationContext`](handler.md#operationcontext) = `undefined` | + +___ + +### createHandler + +▸ **createHandler**<`Context`\>(`options`): (`res`: `HttpResponse`, `req`: `HttpRequest`) => `Promise`<`void`\> + +Create a GraphQL over HTTP spec compliant request handler for +the Node environment [uWebSockets.js module](https://github.com/uNetworking/uWebSockets.js/). + +```js +import uWS from 'uWebSockets.js'; // yarn add uWebSockets.js@uNetworking/uWebSockets.js# +import { createHandler } from 'graphql-http/lib/use/uWebSockets'; +import { schema } from './my-graphql-schema'; + +uWS + .App() + .any('/graphql', createHandler({ schema })) + .listen(4000, () => { + console.log('Listening to port 4000'); + }); +``` + +#### Type parameters + +| Name | Type | +| :------ | :------ | +| `Context` | extends [`OperationContext`](handler.md#operationcontext) = `undefined` | + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `options` | [`HandlerOptions`](use_uWebSockets.md#handleroptions)<`Context`\> | + +#### Returns + +`fn` + +▸ (`res`, `req`): `Promise`<`void`\> + +##### Parameters + +| Name | Type | +| :------ | :------ | +| `res` | `HttpResponse` | +| `req` | `HttpRequest` | + +##### Returns + +`Promise`<`void`\>