trpc-uwebsockets 0.9.4 → 0.10.0-proxy-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -1,161 +1,56 @@
1
- import {
2
- AnyRouter,
3
- inferRouterContext,
4
- resolveHTTPResponse,
5
- } from '@trpc/server';
6
- import { HTTPRequest } from '@trpc/server/dist/declarations/src/http/internals/types';
7
- import { CookieSerializeOptions } from 'cookie';
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { AnyRouter } from '@trpc/server';
3
+ // import { HTTPRequest } from '@trpc/server/dist/index';
8
4
  import type { HttpRequest, HttpResponse, TemplatedApp } from 'uWebSockets.js';
5
+ import { uWsHTTPRequestHandler } from './requestHandler';
6
+
9
7
  import {
10
- UWebSocketsCreateHandlerOptions,
11
- UWebSocketsRequestObject,
12
- UWebSocketsResponseObject,
8
+ uHTTPHandlerOptions,
9
+ WrappedHTTPRequest,
10
+ WrappedHTTPResponse,
13
11
  } from './types';
14
- import { getCookieFn, readPostBody } from './utils';
15
- import cookie from 'cookie';
16
- export * from './types';
12
+
13
+ // export * from './types';
17
14
 
18
15
  /**
19
16
  * @param uWsApp uWebsockets server instance
20
- * @param pathPrefix The path to trpc without trailing slash (ex: "/trpc")
17
+ * @param prefix The path to trpc without trailing slash (ex: "/trpc")
21
18
  * @param opts handler options
22
19
  */
23
20
  export function createUWebSocketsHandler<TRouter extends AnyRouter>(
24
21
  uWsApp: TemplatedApp,
25
- pathPrefix: string,
26
- opts: UWebSocketsCreateHandlerOptions<TRouter>
22
+ prefix: string,
23
+ opts: uHTTPHandlerOptions<TRouter>
27
24
  ) {
28
- const prefixTrimLength = pathPrefix.length + 1; // remove /* from url
29
-
30
- const handler = async (res: HttpResponse, req: HttpRequest) => {
31
- const method = req.getMethod().toUpperCase();
32
- if (method !== 'GET' && method !== 'POST') {
33
- // handle only get and post requests, while the rest
34
- // will not be captured and propagated further
35
- req.setYield(true);
36
- return;
37
- }
25
+ const prefixTrimLength = prefix.length + 1; // remove /* from url
38
26
 
39
- const path = req.getUrl().substring(prefixTrimLength);
40
- const query = new URLSearchParams(decodeURIComponent(req.getQuery()));
27
+ const handler = (res: HttpResponse, req: HttpRequest) => {
28
+ const method = req.getMethod().toUpperCase() as 'GET' | 'POST';
29
+ const url = req.getUrl().substring(prefixTrimLength);
30
+ const query = req.getQuery();
41
31
 
42
32
  const headers: Record<string, string> = {};
43
33
  req.forEach((key, value) => {
34
+ // TODO handle headers with the same key, potential issue
44
35
  headers[key] = value;
45
36
  });
46
37
 
47
38
  // new request object needs to be created, because socket
48
39
  // can only be accessed synchronously, after await it cannot be accessed
49
- const request: UWebSocketsRequestObject = {
40
+ const wrappedReq: WrappedHTTPRequest = {
50
41
  headers,
51
42
  method,
52
43
  query,
53
- path,
54
- getCookies: getCookieFn(headers),
55
- };
56
-
57
- const resOverride = {
58
- headers: new Map<string, string>(),
59
- cookies: [] as string[],
60
- status: 0,
61
- };
62
-
63
- const response: UWebSocketsResponseObject = {
64
- setCookie: (
65
- name: string,
66
- value: string,
67
- opts?: CookieSerializeOptions
68
- ) => {
69
- const serialized = cookie.serialize(name, value, opts); //.substring(12); //remove the "Set-Cookie: "
70
- resOverride.cookies.push(serialized);
71
- },
72
- setStatus: (status: number) => {
73
- resOverride.status = status;
74
- },
75
- setHeader: (key: string, value: string) => {
76
- resOverride.headers.set(key, value);
77
- },
44
+ url,
78
45
  };
79
46
 
80
- const bodyResult = await readPostBody(method, res);
81
-
82
- // req is no longer available!
83
-
84
- const createContext = async function _(): Promise<
85
- inferRouterContext<TRouter>
86
- > {
87
- //res could be proxied here
88
- return await opts.createContext?.({
89
- uWs: uWsApp,
90
- req: request,
91
- res: response,
92
- });
93
- };
94
-
95
- const internalReqObj: HTTPRequest = {
96
- method,
97
- headers,
98
- query,
99
- body: bodyResult.ok ? bodyResult.data : undefined,
100
- };
101
-
102
- // TODO batching, onError options need implementation.
103
- const result = await resolveHTTPResponse({
104
- path,
105
- createContext,
106
- router: opts.router,
107
- req: internalReqObj,
108
- error: bodyResult.ok ? null : bodyResult.error,
109
- });
110
-
111
- // user returned already, do nothing
112
- res.cork(() => {
113
- if (resOverride.status != 0) {
114
- res.writeStatus(resOverride.status.toString());
115
- } else if ('status' in result) {
116
- res.writeStatus(result.status.toString());
117
- } else {
118
- // assume something went bad, should never happen?
119
- throw new Error('No status to send');
120
-
121
- // res.writeStatus('500 INTERNAL SERVER ERROR');
122
- // res.end();
123
- // return;
124
- }
125
-
126
- if (opts.onRequest) opts.onRequest(request, response);
127
-
128
- //send all cookies
129
- resOverride.cookies.forEach((value) => {
130
- res.writeHeader('Set-Cookie', value);
131
- });
132
-
133
- resOverride.headers.forEach((value, key) => {
134
- res.writeHeader(key, value);
135
- });
136
-
137
- for (const [key, value] of Object.entries(result.headers ?? {})) {
138
- if (typeof value === 'undefined') {
139
- continue;
140
- }
141
- // make sure to never override user defined headers
142
- // cookies are an exception
143
- if (resOverride.headers.has(key)) continue;
144
-
145
- // not sure why it could be an array. This code path is not tested
146
- // maybe its duplicates for the same key? like multiple "Set-Cookie"
147
- if (Array.isArray(value))
148
- value.forEach((header) => {
149
- res.writeHeader(key, header);
150
- });
151
- else res.writeHeader(key, value);
152
- }
153
-
154
- //now send user headers
155
- if (result.body) res.write(result.body);
156
- res.end();
47
+ uWsHTTPRequestHandler({
48
+ req: wrappedReq,
49
+ uRes: res,
50
+ path: url,
51
+ ...opts,
157
52
  });
158
53
  };
159
-
160
- uWsApp.any(pathPrefix + '/*', handler);
54
+ uWsApp.get(prefix + '/*', handler);
55
+ uWsApp.post(prefix + '/*', handler);
161
56
  }
@@ -0,0 +1,99 @@
1
+ import { AnyRouter, inferRouterDef, resolveHTTPResponse } from '@trpc/server';
2
+ import { HTTPRequest } from '@trpc/server/dist/http/internals/types';
3
+ import { getPostBody, sendResponse } from './utils';
4
+ import { uHTTPRequestHandlerOptions, WrappedHTTPResponse } from './types';
5
+
6
+ type HeaderSet = { name: string; value: string }; //in order to allow multiple of the same header (Set-Cookie) for example
7
+
8
+ export async function uWsHTTPRequestHandler<TRouter extends AnyRouter>(
9
+ opts: uHTTPRequestHandlerOptions<TRouter>
10
+ ) {
11
+ const resOverride = {
12
+ headers: [] as HeaderSet[],
13
+ status: 0,
14
+ };
15
+
16
+ const wrappedRes: WrappedHTTPResponse = {
17
+ setStatus: (status: number) => {
18
+ resOverride.status = status;
19
+ },
20
+ setHeader: (name: string, value: string) => {
21
+ resOverride.headers.push({ name, value });
22
+ // resOverride.headers.set(key, value);
23
+ },
24
+ };
25
+
26
+ const createContext = async function _createContext(): Promise<
27
+ inferRouterDef<TRouter>['_ctx']
28
+ > {
29
+ return await opts.createContext?.({
30
+ req: opts.req,
31
+ res: wrappedRes,
32
+ });
33
+ };
34
+ const { path, router, uRes, req } = opts;
35
+ let aborted = false;
36
+ uRes.onAborted(() => {
37
+ // console.log('request was aborted');
38
+ aborted = true;
39
+ });
40
+
41
+ const bodyResult = await getPostBody(req.method, uRes, opts.maxBodySize);
42
+
43
+ const query = new URLSearchParams(opts.req.query);
44
+ const requestObj: HTTPRequest = {
45
+ method: opts.req.method,
46
+ headers: opts.req.headers,
47
+ query,
48
+ body: bodyResult.ok ? bodyResult.data : undefined,
49
+ };
50
+
51
+ const result = await resolveHTTPResponse({
52
+ batching: opts.batching,
53
+ responseMeta: opts.responseMeta,
54
+ path,
55
+ createContext,
56
+ router,
57
+ req: requestObj,
58
+ error: bodyResult.ok ? null : bodyResult.error,
59
+ onError(o) {
60
+ opts?.onError?.({
61
+ ...o,
62
+ req: opts.req,
63
+ });
64
+ },
65
+ });
66
+
67
+ if (aborted) {
68
+ // TODO check this behavior
69
+ return;
70
+ }
71
+
72
+ uRes.cork(() => {
73
+ // if ('status' in result && (!res.statusCode || res.statusCode === 200)) {
74
+ if (resOverride.status > 0) {
75
+ uRes.writeStatus(resOverride.status.toString()); // TODO convert code to actual message
76
+ }
77
+ if ('status' in result) {
78
+ uRes.writeStatus(result.status.toString());
79
+ }
80
+
81
+ //send our manual headers
82
+ resOverride.headers.forEach((h) => {
83
+ uRes.writeHeader(h.name, h.value);
84
+ });
85
+
86
+ for (const [key, value] of Object.entries(result.headers ?? {})) {
87
+ if (typeof value === 'undefined') {
88
+ continue;
89
+ }
90
+ if (Array.isArray(value))
91
+ value.forEach((v) => {
92
+ uRes.writeHeader(key, v);
93
+ });
94
+ else uRes.writeHeader(key, value);
95
+ }
96
+
97
+ sendResponse(uRes, result.body);
98
+ });
99
+ }
package/src/types.ts CHANGED
@@ -1,38 +1,41 @@
1
- import { AnyRouter, inferRouterContext } from '@trpc/server';
2
1
  import { HttpRequest, HttpResponse, TemplatedApp } from 'uWebSockets.js';
3
- import { CookieParseOptions, CookieSerializeOptions } from 'cookie';
4
2
 
5
- export type UWebSocketsCreateHandlerOptions<TRouter extends AnyRouter> = {
6
- /* trpc router */
7
- router: TRouter;
8
- /* optional create context */
9
- createContext?: (
10
- opts: UWebSocketsCreateContextOptions
11
- ) => Promise<inferRouterContext<TRouter>> | inferRouterContext<TRouter>;
12
- /* optional pre-request handler. Useful for dealing with CORS */
13
- onRequest?: (
14
- req: UWebSocketsRequestObject,
15
- res: UWebSocketsResponseObject
16
- ) => void;
17
- };
3
+ import { AnyRouter } from '@trpc/server';
4
+ import {
5
+ NodeHTTPCreateContextFnOptions,
6
+ NodeHTTPCreateContextOption,
7
+ } from '@trpc/server/adapters/node-http';
8
+ import { HTTPBaseHandlerOptions } from '@trpc/server/dist/http/internals/types';
18
9
 
19
- export type UWebSocketsRequestObject = {
10
+ export type WrappedHTTPRequest = {
20
11
  headers: Record<string, string>;
21
12
  method: 'POST' | 'GET';
22
- query: URLSearchParams;
23
- path: string;
24
- getCookies: (opts?: CookieParseOptions) => Record<string, string>;
13
+ query: string;
14
+ url: string;
25
15
  };
26
16
 
27
- // if this to be used, it needs to be proxied
28
- export type UWebSocketsResponseObject = {
29
- setCookie(key: string, value: string, opts?: CookieSerializeOptions): void;
17
+ export type WrappedHTTPResponse = {
30
18
  setStatus(status: number): void;
31
19
  setHeader(key: string, value: string): void;
32
20
  };
33
21
 
34
- export type UWebSocketsCreateContextOptions = {
35
- req: UWebSocketsRequestObject;
36
- uWs: TemplatedApp;
37
- res: UWebSocketsResponseObject;
38
- };
22
+ export type uHTTPHandlerOptions<TRouter extends AnyRouter> =
23
+ HTTPBaseHandlerOptions<TRouter, WrappedHTTPRequest> & {
24
+ maxBodySize?: number;
25
+ } & NodeHTTPCreateContextOption<
26
+ TRouter,
27
+ WrappedHTTPRequest,
28
+ WrappedHTTPResponse
29
+ >;
30
+
31
+ export type uHTTPRequestHandlerOptions<TRouter extends AnyRouter> = {
32
+ req: WrappedHTTPRequest;
33
+ // res: WrappedHTTPResponse;
34
+ uRes: HttpResponse;
35
+ path: string;
36
+ } & uHTTPHandlerOptions<TRouter>;
37
+
38
+ export type CreateContextOptions = NodeHTTPCreateContextFnOptions<
39
+ WrappedHTTPRequest,
40
+ WrappedHTTPResponse
41
+ >;
package/src/utils.ts CHANGED
@@ -1,18 +1,17 @@
1
- import { TRPCError } from '@trpc/server';
2
1
  import { HttpResponse } from 'uWebSockets.js';
3
- import cookie, { CookieParseOptions } from 'cookie';
4
- /*
5
- cookie: 'cookie1=abc; cookie2=d.e'
6
- */
7
2
 
8
- export const getCookieFn =
9
- (headers: Record<string, string>) => (opts?: CookieParseOptions) => {
10
- if (!('cookie' in headers)) return {};
3
+ import {
4
+ WrappedHTTPRequest,
5
+ // uHTTPRequestHandlerOptions,
6
+ WrappedHTTPResponse,
7
+ } from './types';
8
+ import { AnyRouter, TRPCError } from '@trpc/server';
11
9
 
12
- return cookie.parse(headers.cookie, opts);
13
- };
14
-
15
- export function readPostBody(method: string, res: HttpResponse) {
10
+ export function getPostBody<
11
+ TRouter extends AnyRouter,
12
+ TRequest extends WrappedHTTPRequest,
13
+ TResponse extends WrappedHTTPResponse
14
+ >(method, res: HttpResponse, maxBodySize?: number) {
16
15
  return new Promise<
17
16
  { ok: true; data: unknown } | { ok: false; error: TRPCError }
18
17
  >((resolve) => {
@@ -38,8 +37,15 @@ export function readPostBody(method: string, res: HttpResponse) {
38
37
 
39
38
  const chunk = Buffer.from(ab);
40
39
 
41
- //else accumulate
42
- if (buffer) buffer = Buffer.concat([buffer, chunk]);
40
+ if (maxBodySize && buffer.length >= maxBodySize) {
41
+ resolve({
42
+ ok: false,
43
+ error: new TRPCError({ code: 'PAYLOAD_TOO_LARGE' }),
44
+ });
45
+ }
46
+ if (buffer)
47
+ //else accumulate
48
+ buffer = Buffer.concat([buffer, chunk]);
43
49
  else buffer = Buffer.concat([chunk]);
44
50
 
45
51
  if (isLast) {
@@ -58,3 +64,9 @@ export function readPostBody(method: string, res: HttpResponse) {
58
64
  });
59
65
  });
60
66
  }
67
+
68
+ // FIXME buffer the output with tryEnd instead
69
+ // https://github.com/uNetworking/uWebSockets.js/blob/master/examples/VideoStreamer.js
70
+ export function sendResponse(res: HttpResponse, payload?: string) {
71
+ res.end(payload);
72
+ }