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/README.md +43 -63
- package/dist/index.js +18 -123
- package/dist/index.js.map +1 -1
- package/dist/requestHandler.js +86 -0
- package/dist/requestHandler.js.map +1 -0
- package/dist/utils.js +16 -17
- package/dist/utils.js.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +28 -133
- package/src/requestHandler.ts +99 -0
- package/src/types.ts +30 -27
- package/src/utils.ts +26 -14
- package/test/index.spec.ts +110 -172
- package/types/index.d.ts +3 -4
- package/types/requestHandler.d.ts +3 -0
- package/types/types.d.ts +17 -19
- package/types/utils.d.ts +4 -4
package/src/index.ts
CHANGED
|
@@ -1,161 +1,56 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
uHTTPHandlerOptions,
|
|
9
|
+
WrappedHTTPRequest,
|
|
10
|
+
WrappedHTTPResponse,
|
|
13
11
|
} from './types';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export * from './types';
|
|
12
|
+
|
|
13
|
+
// export * from './types';
|
|
17
14
|
|
|
18
15
|
/**
|
|
19
16
|
* @param uWsApp uWebsockets server instance
|
|
20
|
-
* @param
|
|
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
|
-
|
|
26
|
-
opts:
|
|
22
|
+
prefix: string,
|
|
23
|
+
opts: uHTTPHandlerOptions<TRouter>
|
|
27
24
|
) {
|
|
28
|
-
const prefixTrimLength =
|
|
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
|
-
|
|
40
|
-
const
|
|
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
|
|
40
|
+
const wrappedReq: WrappedHTTPRequest = {
|
|
50
41
|
headers,
|
|
51
42
|
method,
|
|
52
43
|
query,
|
|
53
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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.
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
10
|
+
export type WrappedHTTPRequest = {
|
|
20
11
|
headers: Record<string, string>;
|
|
21
12
|
method: 'POST' | 'GET';
|
|
22
|
-
query:
|
|
23
|
-
|
|
24
|
-
getCookies: (opts?: CookieParseOptions) => Record<string, string>;
|
|
13
|
+
query: string;
|
|
14
|
+
url: string;
|
|
25
15
|
};
|
|
26
16
|
|
|
27
|
-
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
import {
|
|
4
|
+
WrappedHTTPRequest,
|
|
5
|
+
// uHTTPRequestHandlerOptions,
|
|
6
|
+
WrappedHTTPResponse,
|
|
7
|
+
} from './types';
|
|
8
|
+
import { AnyRouter, TRPCError } from '@trpc/server';
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
+
}
|