Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better integration with uWS #212

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/fets-212-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"fets": patch
---
dependencies updates:
- Updated dependency [`@whatwg-node/fetch@^0.9.3` ↗︎](https://www.npmjs.com/package/@whatwg-node/fetch/v/0.9.3) (from `^0.9.0`, in `dependencies`)
- Updated dependency [`@whatwg-node/server@^0.8.4` ↗︎](https://www.npmjs.com/package/@whatwg-node/server/v/0.8.4) (from `^0.8.1`, in `dependencies`)
3 changes: 2 additions & 1 deletion e2e/deno/import-map.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"fs": "https://deno.land/[email protected]/node/fs.ts",
"util": "https://deno.land/[email protected]/node/util.ts",
"zod-to-json-schema": "npm:zod-to-json-schema",
"zod": "npm:zod"
"zod": "npm:zod",
"@repeaterjs/repeater": "npm:@repeaterjs/repeater"
}
}
106 changes: 65 additions & 41 deletions examples/todolist/__integration_tests__/todolist.spec.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,79 @@
/* eslint-disable camelcase */
import { globalAgent } from 'http';
import { createServer, globalAgent, Server } from 'http';
import { AddressInfo } from 'net';
import {
type us_listen_socket,
us_listen_socket_close,
us_socket_local_port,
} from 'uWebSockets.js';
import { fetch } from '@whatwg-node/fetch';
import { app } from '../src/app';
import { app, router } from '../src/router';

describe('uWebSockets', () => {
const nodeMajor = parseInt(process.versions.node.split('.')[0], 10);
if (nodeMajor < 16) {
it('should be skipped', () => {});
return;
}
let listenSocket: us_listen_socket;
describe('TodoList', () => {
let port: number;
beforeAll(done => {
app.listen(0, newListenSocket => {
listenSocket = newListenSocket;
if (!listenSocket) {
done.fail('Failed to start the server');
return;
}
port = us_socket_local_port(listenSocket);
done();

describe('Node', () => {
let server: Server;
beforeAll(done => {
server = createServer(router);
server.listen(0, () => {
port = (server.address() as AddressInfo).port;
done();
});
});
afterAll(() => {
server.close();
globalAgent.destroy();
});
runTests();
});
afterAll(() => {
if (listenSocket) {
us_listen_socket_close(listenSocket);

describe('uWebSockets', () => {
const nodeMajor = parseInt(process.versions.node.split('.')[0], 10);
if (nodeMajor < 16) {
it('should be skipped', () => {});
return;
}
globalAgent.destroy();
});
it('should work', async () => {
const response = await fetch(`http://localhost:${port}/todos`);
expect(response.status).toBe(200);
expect(await response.json()).toEqual([]);
});
it('should show Swagger UI', async () => {
const response = await fetch(`http://localhost:${port}/docs`);
expect(response.status).toBe(200);
expect(response.headers.get('content-type')).toBe('text/html');
const html = await response.text();
expect(html).toContain('<title>SwaggerUI</title>');
let listenSocket: us_listen_socket;
beforeAll(done => {
app.listen(0, newListenSocket => {
listenSocket = newListenSocket;
if (!listenSocket) {
done.fail('Failed to start the server');
return;
}
port = us_socket_local_port(listenSocket);
done();
});
});
afterAll(() => {
if (listenSocket) {
us_listen_socket_close(listenSocket);
}
globalAgent.destroy();
});
runTests();
});
it('should expose OpenAPI document', async () => {
const response = await fetch(`http://localhost:${port}/openapi.json`);
expect(response.status).toBe(200);
expect(response.headers.get('content-type')).toBe('application/json');
const json = await response.json();
expect(json).toMatchInlineSnapshot(`

function runTests() {
it('should work', async () => {
const response = await fetch(`http://localhost:${port}/todos`);
expect(response.status).toBe(200);
expect(await response.json()).toEqual([]);
});
it('should show Swagger UI', async () => {
const response = await fetch(`http://localhost:${port}/docs`);
expect(response.status).toBe(200);
expect(response.headers.get('content-type')).toBe('text/html');
const html = await response.text();
expect(html).toContain('<title>SwaggerUI</title>');
});
it('should expose OpenAPI document', async () => {
const response = await fetch(`http://localhost:${port}/openapi.json`);
expect(response.status).toBe(200);
expect(response.headers.get('content-type')).toBe('application/json');
const json = await response.json();
expect(json).toMatchInlineSnapshot(`
{
"components": {
"schemas": {
Expand Down Expand Up @@ -297,5 +320,6 @@ describe('uWebSockets', () => {
},
}
`);
});
});
}
});
4 changes: 0 additions & 4 deletions examples/todolist/src/app.ts

This file was deleted.

2 changes: 1 addition & 1 deletion examples/todolist/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { app } from './app';
import { app } from './router';

app.listen(3000, () => {
console.log('SwaggerUI is served at http://localhost:3000/docs');
Expand Down
4 changes: 4 additions & 0 deletions examples/todolist/src/router.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { promises as fsPromises } from 'fs';
import { join } from 'path';
import { createRouter, FromSchema, Response } from 'fets';
import { App } from 'uWebSockets.js';

const TodoSchema = {
type: 'object',
Expand All @@ -16,6 +17,8 @@ type Todo = FromSchema<typeof TodoSchema>;

const todos: Todo[] = [];

export const app = App();

export const router = createRouter({
openAPI: {
info: {
Expand All @@ -29,6 +32,7 @@ export const router = createRouter({
},
} as const,
},
app,
})
.route({
description: 'Get all todos',
Expand Down
4 changes: 2 additions & 2 deletions packages/fets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@
"dependencies": {
"@ardatan/fast-json-stringify": "^0.0.6",
"@whatwg-node/cookie-store": "^0.1.0",
"@whatwg-node/fetch": "^0.9.0",
"@whatwg-node/server": "^0.8.1",
"@whatwg-node/fetch": "^0.9.3",
"@whatwg-node/server": "^0.8.4",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"hotscript": "^1.0.11",
Expand Down
12 changes: 9 additions & 3 deletions packages/fets/src/createRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { OpenAPIV3_1 } from 'openapi-types';
import * as DefaultFetchAPI from '@whatwg-node/fetch';
import { createServerAdapter } from '@whatwg-node/server';
import { useOpenAPI } from './plugins/openapi.js';
import { useUWS } from './plugins/uws.js';
import { isLazySerializedResponse } from './Response.js';
import { HTTPMethod, TypedRequest, TypedResponse } from './typed-fetch.js';
import type {
Expand Down Expand Up @@ -45,7 +46,7 @@ export function createRouterBase(
};
const __onRouterInitHooks: OnRouterInitHook<any>[] = [];
const onRouteHooks: OnRouteHook<any>[] = [];
const onSerializeResponseHooks: OnSerializeResponseHook<any>[] = [];
const __onSerializeResponseHooks: OnSerializeResponseHook<any>[] = [];
for (const plugin of plugins) {
if (plugin.onRouterInit) {
__onRouterInitHooks.push(plugin.onRouterInit);
Expand All @@ -54,7 +55,7 @@ export function createRouterBase(
onRouteHooks.push(plugin.onRoute);
}
if (plugin.onSerializeResponse) {
onSerializeResponseHooks.push(plugin.onSerializeResponse);
__onSerializeResponseHooks.push(plugin.onSerializeResponse);
}
}
const routesByMethod = new Map<
Expand Down Expand Up @@ -170,7 +171,7 @@ export function createRouterBase(
for (const handler of handlers) {
const handlerResult = await handler(routerRequest, context);
if (isLazySerializedResponse(handlerResult)) {
for (const onSerializeResponseHook of onSerializeResponseHooks) {
for (const onSerializeResponseHook of __onSerializeResponseHooks) {
onSerializeResponseHook({
request: routerRequest,
lazyResponse: handlerResult,
Expand Down Expand Up @@ -230,6 +231,9 @@ export function createRouterBase(
},
__client: {},
__onRouterInitHooks,
__onSerializeResponseHooks,
plugins,
fetchAPI,
};
}

Expand All @@ -244,6 +248,7 @@ export function createRouter<
swaggerUI: { endpoint: swaggerUIEndpoint = '/docs', ...swaggerUIOpts } = {},
plugins: userPlugins = [],
base = '/',
app,
...options
}: RouterOptions<TServerContext, TComponents> = {}): Router<
TServerContext,
Expand Down Expand Up @@ -277,6 +282,7 @@ export function createRouter<
}),
]
: []),
...(app ? [useUWS(app)] : []),
useZod(),
...userPlugins,
];
Expand Down
1 change: 1 addition & 0 deletions packages/fets/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export { FromSchema } from 'json-schema-to-ts';
export * from './client/index.js';
export * from './Response.js';
export { useAjv } from './plugins/ajv.js';
export { useUWS } from './plugins/uws.js';
140 changes: 140 additions & 0 deletions packages/fets/src/plugins/uws.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import {
getRequestFromUWSRequest,
handleOnRequestHook,
OnRequestHook,
OnResponseHook,
sendResponseToUwsOpts,
UWSRequest,
UWSResponse,
} from '@whatwg-node/server';
import { isLazySerializedResponse } from '../Response.js';
import { Router, RouterPlugin, RouterRequest, UWSApp } from '../types.js';

async function handleOnResponse({
request,
response,
res,
serverContext,
onResponseHooks,
}: {
request: RouterRequest;
response: Response;
res: UWSResponse;
serverContext: any;
onResponseHooks: OnResponseHook<any>[];
}) {
for (const onResponseHook of onResponseHooks) {
await onResponseHook({
request: request as Request,
response,
serverContext,
});
}
await sendResponseToUwsOpts({
response,
res,
});
}

export function useUWS(app: UWSApp): RouterPlugin<any> {
const onRequestHooks: OnRequestHook<any>[] = [];
const onResponseHooks: OnResponseHook<any>[] = [];
const requestMap = new WeakMap<UWSRequest, RouterRequest>();
const serverContextMap = new WeakMap<UWSRequest, any>();
let router: Router<any, {}, {}>;
return {
onRouterInit(_router) {
router = _router;
for (const plugin of router.plugins) {
if (plugin.onRequest) {
onRequestHooks.push(plugin.onRequest);
}
if (plugin.onResponse) {
onResponseHooks.push(plugin.onResponse);
}
}
app.any('/*', async function (res, req: any) {
// Create a Request and attach it to the uWS request
const request = getRequestFromUWSRequest({
req,
res,
fetchAPI: router.fetchAPI,
}) as RouterRequest;
requestMap.set(req, request);
const serverContext = {
req,
res,
};
serverContextMap.set(req, serverContext);
const { response } = await handleOnRequestHook({
fetchAPI: router.fetchAPI,
request: request as Request,
givenHandleRequest: (() => {}) as any,
onRequestHooks,
serverContext,
});
if (!response) {
req.setYield(true);
} else {
await handleOnResponse({
request,
response,
res,
serverContext,
onResponseHooks,
});
}
});
},
onRoute({ method, path, handlers }) {
let appMethod = method.toLowerCase();
let normalizedPath = path;
if (!path.startsWith('/')) {
normalizedPath = '/' + path;
}
for (const handler of handlers) {
if (!(appMethod in app)) {
appMethod = 'any';
}
app[appMethod as 'post'](normalizedPath, async function (res, req: any) {
if (appMethod === 'any') {
if (req.getMethod().toLowerCase() !== method.toLowerCase()) {
req.setYield(true);
return;
}
}
const request = requestMap.get(req)!;
const serverContext = requestMap.get(req);
let response = (await handler(request!, serverContext)) as Response;
if (isLazySerializedResponse(response)) {
for (const onSerializeResponseHook of router.__onSerializeResponseHooks) {
onSerializeResponseHook({
request,
lazyResponse: response,
serverContext,
});
}
if (!response.serializerSet) {
response = router.fetchAPI.Response.json(response.jsonObj, response.init);
} else {
response = await response.responsePromise;
}
}
if (!response) {
req.setYield(true);
} else {
if (response) {
await handleOnResponse({
request,
response,
res,
serverContext,
onResponseHooks,
});
}
}
});
}
},
};
}
Loading