@whatwg-node/server 0.10.0-alpha-20240717150008-1474b9d9b679a0e8f6225f44f11b95a6f4bf24ea → 0.10.0-alpha-20241123133536-975c9068dde45574fcfa26567e4bab96f45d1f85
Sign up to get free protection for your applications and to get access to all the features.
- package/cjs/createServerAdapter.js +62 -24
- package/cjs/index.js +1 -0
- package/cjs/plugins/useContentEncoding.js +133 -0
- package/cjs/plugins/useErrorHandling.js +5 -1
- package/cjs/utils.js +174 -96
- package/cjs/uwebsockets.js +192 -27
- package/esm/createServerAdapter.js +63 -25
- package/esm/index.js +1 -0
- package/esm/plugins/useContentEncoding.js +130 -0
- package/esm/plugins/useErrorHandling.js +5 -1
- package/esm/utils.js +170 -95
- package/esm/uwebsockets.js +190 -27
- package/package.json +4 -3
- package/typings/index.d.cts +1 -0
- package/typings/index.d.ts +1 -0
- package/typings/plugins/types.d.cts +3 -0
- package/typings/plugins/types.d.ts +3 -0
- package/typings/plugins/useContentEncoding.d.cts +2 -0
- package/typings/plugins/useContentEncoding.d.ts +2 -0
- package/typings/plugins/useCors.d.cts +1 -1
- package/typings/plugins/useCors.d.ts +1 -1
- package/typings/plugins/useErrorHandling.d.cts +3 -3
- package/typings/plugins/useErrorHandling.d.ts +3 -3
- package/typings/types.d.cts +10 -2
- package/typings/types.d.ts +10 -2
- package/typings/utils.d.cts +8 -3
- package/typings/utils.d.ts +8 -3
- package/typings/uwebsockets.d.cts +3 -1
- package/typings/uwebsockets.d.ts +3 -1
@@ -1,10 +1,8 @@
|
|
1
1
|
/* eslint-disable @typescript-eslint/ban-types */
|
2
|
+
import { AsyncDisposableStack, DisposableSymbols } from '@whatwg-node/disposablestack';
|
2
3
|
import * as DefaultFetchAPI from '@whatwg-node/fetch';
|
3
|
-
import { completeAssign, handleAbortSignalAndPromiseResponse, handleErrorFromRequestHandler, isFetchEvent, isNodeRequest, isolateObject, isPromise, isRequestInit, isServerResponse, iterateAsyncVoid, normalizeNodeRequest, sendNodeResponse, ServerAdapterRequestAbortSignal, } from './utils.js';
|
4
|
+
import { completeAssign, ensureDisposableStackRegisteredForTerminateEvents, handleAbortSignalAndPromiseResponse, handleErrorFromRequestHandler, isFetchEvent, isNodeRequest, isolateObject, isPromise, isRequestInit, isServerResponse, iterateAsyncVoid, nodeRequestResponseMap, normalizeNodeRequest, sendNodeResponse, ServerAdapterRequestAbortSignal, } from './utils.js';
|
4
5
|
import { getRequestFromUWSRequest, isUWSResponse, sendResponseToUwsOpts, } from './uwebsockets.js';
|
5
|
-
async function handleWaitUntils(waitUntilPromises) {
|
6
|
-
await Promise.allSettled(waitUntilPromises);
|
7
|
-
}
|
8
6
|
// Required for envs like nextjs edge runtime
|
9
7
|
function isRequestAccessible(serverContext) {
|
10
8
|
try {
|
@@ -25,6 +23,36 @@ function createServerAdapter(serverAdapterBaseObject, options) {
|
|
25
23
|
: serverAdapterBaseObject.handle;
|
26
24
|
const onRequestHooks = [];
|
27
25
|
const onResponseHooks = [];
|
26
|
+
const waitUntilPromises = new Set();
|
27
|
+
const disposableStack = new AsyncDisposableStack();
|
28
|
+
const signals = new Set();
|
29
|
+
function registerSignal(signal) {
|
30
|
+
signals.add(signal);
|
31
|
+
signal.addEventListener('abort', () => {
|
32
|
+
signals.delete(signal);
|
33
|
+
});
|
34
|
+
}
|
35
|
+
disposableStack.defer(() => {
|
36
|
+
for (const signal of signals) {
|
37
|
+
signal.sendAbort();
|
38
|
+
}
|
39
|
+
});
|
40
|
+
function handleWaitUntils() {
|
41
|
+
return Promise.allSettled(waitUntilPromises).then(() => { }, () => { });
|
42
|
+
}
|
43
|
+
disposableStack.defer(handleWaitUntils);
|
44
|
+
function waitUntil(promiseLike) {
|
45
|
+
// If it is a Node.js environment, we should register the disposable stack to handle process termination events
|
46
|
+
if (globalThis.process) {
|
47
|
+
ensureDisposableStackRegisteredForTerminateEvents(disposableStack);
|
48
|
+
}
|
49
|
+
waitUntilPromises.add(promiseLike.then(() => {
|
50
|
+
waitUntilPromises.delete(promiseLike);
|
51
|
+
}, err => {
|
52
|
+
console.error(`Unexpected error while waiting: ${err.message || err}`);
|
53
|
+
waitUntilPromises.delete(promiseLike);
|
54
|
+
}));
|
55
|
+
}
|
28
56
|
if (options?.plugins != null) {
|
29
57
|
for (const plugin of options.plugins) {
|
30
58
|
if (plugin.onRequest) {
|
@@ -50,6 +78,9 @@ function createServerAdapter(serverAdapterBaseObject, options) {
|
|
50
78
|
});
|
51
79
|
const onRequestHooksIteration$ = iterateAsyncVoid(onRequestHooks, (onRequestHook, stopEarly) => onRequestHook({
|
52
80
|
request,
|
81
|
+
setRequest(newRequest) {
|
82
|
+
request = newRequest;
|
83
|
+
},
|
53
84
|
serverContext,
|
54
85
|
fetchAPI,
|
55
86
|
url,
|
@@ -65,13 +96,17 @@ function createServerAdapter(serverAdapterBaseObject, options) {
|
|
65
96
|
},
|
66
97
|
}));
|
67
98
|
function handleResponse(response) {
|
68
|
-
if (
|
99
|
+
if (onResponseHooks.length === 0) {
|
69
100
|
return response;
|
70
101
|
}
|
71
102
|
const onResponseHookPayload = {
|
72
103
|
request,
|
73
104
|
response,
|
74
105
|
serverContext,
|
106
|
+
setResponse(newResponse) {
|
107
|
+
response = newResponse;
|
108
|
+
},
|
109
|
+
fetchAPI,
|
75
110
|
};
|
76
111
|
const onResponseHooksIteration$ = iterateAsyncVoid(onResponseHooks, onResponseHook => onResponseHook(onResponseHookPayload));
|
77
112
|
if (isPromise(onResponseHooksIteration$)) {
|
@@ -95,20 +130,22 @@ function createServerAdapter(serverAdapterBaseObject, options) {
|
|
95
130
|
return handleEarlyResponse();
|
96
131
|
}
|
97
132
|
: givenHandleRequest;
|
98
|
-
|
99
|
-
|
133
|
+
// TODO: Remove this on the next major version
|
134
|
+
function handleNodeRequest(nodeRequest, ...ctx) {
|
100
135
|
const serverContext = ctx.length > 1 ? completeAssign(...ctx) : ctx[0] || {};
|
101
|
-
const request = normalizeNodeRequest(nodeRequest,
|
136
|
+
const request = normalizeNodeRequest(nodeRequest, fetchAPI, registerSignal);
|
102
137
|
return handleRequest(request, serverContext);
|
103
138
|
}
|
139
|
+
function handleNodeRequestAndResponse(nodeRequest, nodeResponseOrContainer, ...ctx) {
|
140
|
+
const nodeResponse = nodeResponseOrContainer.raw || nodeResponseOrContainer;
|
141
|
+
nodeRequestResponseMap.set(nodeRequest, nodeResponse);
|
142
|
+
return handleNodeRequest(nodeRequest, ...ctx);
|
143
|
+
}
|
104
144
|
function requestListener(nodeRequest, nodeResponse, ...ctx) {
|
105
|
-
const waitUntilPromises = [];
|
106
145
|
const defaultServerContext = {
|
107
146
|
req: nodeRequest,
|
108
147
|
res: nodeResponse,
|
109
|
-
waitUntil
|
110
|
-
waitUntilPromises.push(cb.catch(err => console.error(err)));
|
111
|
-
},
|
148
|
+
waitUntil,
|
112
149
|
};
|
113
150
|
let response$;
|
114
151
|
try {
|
@@ -133,19 +170,17 @@ function createServerAdapter(serverAdapterBaseObject, options) {
|
|
133
170
|
}
|
134
171
|
}
|
135
172
|
function handleUWS(res, req, ...ctx) {
|
136
|
-
const waitUntilPromises = [];
|
137
173
|
const defaultServerContext = {
|
138
174
|
res,
|
139
175
|
req,
|
140
|
-
waitUntil
|
141
|
-
waitUntilPromises.push(cb.catch(err => console.error(err)));
|
142
|
-
},
|
176
|
+
waitUntil,
|
143
177
|
};
|
144
178
|
const filteredCtxParts = ctx.filter(partCtx => partCtx != null);
|
145
179
|
const serverContext = filteredCtxParts.length > 0
|
146
180
|
? completeAssign(defaultServerContext, ...ctx)
|
147
181
|
: defaultServerContext;
|
148
182
|
const signal = new ServerAdapterRequestAbortSignal();
|
183
|
+
registerSignal(signal);
|
149
184
|
const originalResEnd = res.end.bind(res);
|
150
185
|
let resEnded = false;
|
151
186
|
res.end = function (data) {
|
@@ -177,7 +212,7 @@ function createServerAdapter(serverAdapterBaseObject, options) {
|
|
177
212
|
.catch((e) => handleErrorFromRequestHandler(e, fetchAPI.Response))
|
178
213
|
.then(response => {
|
179
214
|
if (!signal.aborted && !resEnded) {
|
180
|
-
return sendResponseToUwsOpts(res, response, signal);
|
215
|
+
return sendResponseToUwsOpts(res, response, signal, fetchAPI);
|
181
216
|
}
|
182
217
|
})
|
183
218
|
.catch(err => {
|
@@ -186,7 +221,7 @@ function createServerAdapter(serverAdapterBaseObject, options) {
|
|
186
221
|
}
|
187
222
|
try {
|
188
223
|
if (!signal.aborted && !resEnded) {
|
189
|
-
return sendResponseToUwsOpts(res, response$, signal);
|
224
|
+
return sendResponseToUwsOpts(res, response$, signal, fetchAPI);
|
190
225
|
}
|
191
226
|
}
|
192
227
|
catch (err) {
|
@@ -206,17 +241,12 @@ function createServerAdapter(serverAdapterBaseObject, options) {
|
|
206
241
|
}
|
207
242
|
function handleRequestWithWaitUntil(request, ...ctx) {
|
208
243
|
const filteredCtxParts = ctx.filter(partCtx => partCtx != null);
|
209
|
-
let waitUntilPromises;
|
210
244
|
const serverContext = filteredCtxParts.length > 1
|
211
245
|
? completeAssign({}, ...filteredCtxParts)
|
212
246
|
: isolateObject(filteredCtxParts[0], filteredCtxParts[0] == null || filteredCtxParts[0].waitUntil == null
|
213
|
-
?
|
247
|
+
? waitUntil
|
214
248
|
: undefined);
|
215
|
-
|
216
|
-
if (waitUntilPromises?.length) {
|
217
|
-
return handleWaitUntils(waitUntilPromises).then(() => response$);
|
218
|
-
}
|
219
|
-
return response$;
|
249
|
+
return handleRequest(request, serverContext);
|
220
250
|
}
|
221
251
|
const fetchFn = (input, ...maybeCtx) => {
|
222
252
|
if (typeof input === 'string' || 'href' in input) {
|
@@ -263,11 +293,19 @@ function createServerAdapter(serverAdapterBaseObject, options) {
|
|
263
293
|
const adapterObj = {
|
264
294
|
handleRequest: handleRequestWithWaitUntil,
|
265
295
|
fetch: fetchFn,
|
296
|
+
handleNodeRequest,
|
266
297
|
handleNodeRequestAndResponse,
|
267
298
|
requestListener,
|
268
299
|
handleEvent,
|
269
300
|
handleUWS,
|
270
301
|
handle: genericRequestHandler,
|
302
|
+
disposableStack,
|
303
|
+
[DisposableSymbols.asyncDispose]() {
|
304
|
+
return disposableStack.disposeAsync();
|
305
|
+
},
|
306
|
+
dispose() {
|
307
|
+
return disposableStack.disposeAsync();
|
308
|
+
},
|
271
309
|
};
|
272
310
|
const serverAdapter = new Proxy(genericRequestHandler, {
|
273
311
|
// It should have all the attributes of the handler function and the server instance
|
package/esm/index.js
CHANGED
@@ -4,5 +4,6 @@ export * from './utils.js';
|
|
4
4
|
export * from './plugins/types.js';
|
5
5
|
export * from './plugins/useCors.js';
|
6
6
|
export * from './plugins/useErrorHandling.js';
|
7
|
+
export * from './plugins/useContentEncoding.js';
|
7
8
|
export * from './uwebsockets.js';
|
8
9
|
export { Response } from '@whatwg-node/fetch';
|
@@ -0,0 +1,130 @@
|
|
1
|
+
import { decompressedResponseMap, getSupportedEncodings, isAsyncIterable, isReadable, } from '../utils.js';
|
2
|
+
export function useContentEncoding() {
|
3
|
+
const encodingMap = new WeakMap();
|
4
|
+
return {
|
5
|
+
onRequest({ request, setRequest, fetchAPI, endResponse }) {
|
6
|
+
if (request.body) {
|
7
|
+
const contentEncodingHeader = request.headers.get('content-encoding');
|
8
|
+
if (contentEncodingHeader && contentEncodingHeader !== 'none') {
|
9
|
+
const contentEncodings = contentEncodingHeader?.split(',');
|
10
|
+
if (!contentEncodings.every(encoding => getSupportedEncodings(fetchAPI).includes(encoding))) {
|
11
|
+
endResponse(new fetchAPI.Response(`Unsupported 'Content-Encoding': ${contentEncodingHeader}`, {
|
12
|
+
status: 415,
|
13
|
+
statusText: 'Unsupported Media Type',
|
14
|
+
}));
|
15
|
+
return;
|
16
|
+
}
|
17
|
+
let newBody = request.body;
|
18
|
+
for (const contentEncoding of contentEncodings) {
|
19
|
+
newBody = newBody.pipeThrough(new fetchAPI.DecompressionStream(contentEncoding));
|
20
|
+
}
|
21
|
+
request = new fetchAPI.Request(request.url, {
|
22
|
+
body: newBody,
|
23
|
+
cache: request.cache,
|
24
|
+
credentials: request.credentials,
|
25
|
+
headers: request.headers,
|
26
|
+
integrity: request.integrity,
|
27
|
+
keepalive: request.keepalive,
|
28
|
+
method: request.method,
|
29
|
+
mode: request.mode,
|
30
|
+
redirect: request.redirect,
|
31
|
+
referrer: request.referrer,
|
32
|
+
referrerPolicy: request.referrerPolicy,
|
33
|
+
signal: request.signal,
|
34
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
35
|
+
// @ts-ignore - not in the TS types yet
|
36
|
+
duplex: 'half',
|
37
|
+
});
|
38
|
+
setRequest(request);
|
39
|
+
}
|
40
|
+
}
|
41
|
+
const acceptEncoding = request.headers.get('accept-encoding');
|
42
|
+
if (acceptEncoding) {
|
43
|
+
encodingMap.set(request, acceptEncoding.split(','));
|
44
|
+
}
|
45
|
+
},
|
46
|
+
onResponse({ request, response, setResponse, fetchAPI, serverContext }) {
|
47
|
+
const waitUntil = serverContext.waitUntil?.bind(serverContext) || (() => { });
|
48
|
+
// Hack for avoiding to create whatwg-node to create a readable stream until it's needed
|
49
|
+
if (response['bodyInit'] || response.body) {
|
50
|
+
const encodings = encodingMap.get(request);
|
51
|
+
if (encodings) {
|
52
|
+
const supportedEncoding = encodings.find(encoding => getSupportedEncodings(fetchAPI).includes(encoding));
|
53
|
+
if (supportedEncoding) {
|
54
|
+
const compressionStream = new fetchAPI.CompressionStream(supportedEncoding);
|
55
|
+
// To calculate final content-length
|
56
|
+
const contentLength = response.headers.get('content-length');
|
57
|
+
if (contentLength) {
|
58
|
+
const bufOfRes = response._buffer;
|
59
|
+
if (bufOfRes) {
|
60
|
+
const writer = compressionStream.writable.getWriter();
|
61
|
+
waitUntil(writer.write(bufOfRes));
|
62
|
+
waitUntil(writer.close());
|
63
|
+
const uint8Arrays$ = isReadable(compressionStream.readable['readable'])
|
64
|
+
? collectReadableValues(compressionStream.readable['readable'])
|
65
|
+
: isAsyncIterable(compressionStream.readable)
|
66
|
+
? collectAsyncIterableValues(compressionStream.readable)
|
67
|
+
: collectReadableStreamValues(compressionStream.readable);
|
68
|
+
return uint8Arrays$.then(uint8Arrays => {
|
69
|
+
const chunks = uint8Arrays.flatMap(uint8Array => [...uint8Array]);
|
70
|
+
const uint8Array = new Uint8Array(chunks);
|
71
|
+
const newHeaders = new fetchAPI.Headers(response.headers);
|
72
|
+
newHeaders.set('content-encoding', supportedEncoding);
|
73
|
+
newHeaders.set('content-length', uint8Array.byteLength.toString());
|
74
|
+
const compressedResponse = new fetchAPI.Response(uint8Array, {
|
75
|
+
...response,
|
76
|
+
headers: newHeaders,
|
77
|
+
});
|
78
|
+
decompressedResponseMap.set(compressedResponse, response);
|
79
|
+
setResponse(compressedResponse);
|
80
|
+
waitUntil(compressionStream.writable.close());
|
81
|
+
});
|
82
|
+
}
|
83
|
+
}
|
84
|
+
const newHeaders = new fetchAPI.Headers(response.headers);
|
85
|
+
newHeaders.set('content-encoding', supportedEncoding);
|
86
|
+
newHeaders.delete('content-length');
|
87
|
+
const compressedBody = response.body.pipeThrough(compressionStream);
|
88
|
+
const compressedResponse = new fetchAPI.Response(compressedBody, {
|
89
|
+
status: response.status,
|
90
|
+
statusText: response.statusText,
|
91
|
+
headers: newHeaders,
|
92
|
+
});
|
93
|
+
decompressedResponseMap.set(compressedResponse, response);
|
94
|
+
setResponse(compressedResponse);
|
95
|
+
}
|
96
|
+
}
|
97
|
+
}
|
98
|
+
},
|
99
|
+
};
|
100
|
+
}
|
101
|
+
function collectReadableValues(readable) {
|
102
|
+
const values = [];
|
103
|
+
readable.on('data', value => values.push(value));
|
104
|
+
return new Promise((resolve, reject) => {
|
105
|
+
readable.once('end', () => resolve(values));
|
106
|
+
readable.once('error', reject);
|
107
|
+
});
|
108
|
+
}
|
109
|
+
async function collectAsyncIterableValues(asyncIterable) {
|
110
|
+
const values = [];
|
111
|
+
for await (const value of asyncIterable) {
|
112
|
+
values.push(value);
|
113
|
+
}
|
114
|
+
return values;
|
115
|
+
}
|
116
|
+
async function collectReadableStreamValues(readableStream) {
|
117
|
+
const reader = readableStream.getReader();
|
118
|
+
const values = [];
|
119
|
+
while (true) {
|
120
|
+
const { done, value } = await reader.read();
|
121
|
+
if (done) {
|
122
|
+
reader.releaseLock();
|
123
|
+
break;
|
124
|
+
}
|
125
|
+
else if (value) {
|
126
|
+
values.push(value);
|
127
|
+
}
|
128
|
+
}
|
129
|
+
return values;
|
130
|
+
}
|
@@ -19,13 +19,17 @@ function createDefaultErrorResponse(ResponseCtor) {
|
|
19
19
|
return new ResponseCtor(null, { status: 500 });
|
20
20
|
}
|
21
21
|
export class HTTPError extends Error {
|
22
|
+
status;
|
23
|
+
message;
|
24
|
+
headers;
|
25
|
+
details;
|
26
|
+
name = 'HTTPError';
|
22
27
|
constructor(status = 500, message, headers = {}, details) {
|
23
28
|
super(message);
|
24
29
|
this.status = status;
|
25
30
|
this.message = message;
|
26
31
|
this.headers = headers;
|
27
32
|
this.details = details;
|
28
|
-
this.name = 'HTTPError';
|
29
33
|
Error.captureStackTrace(this, HTTPError);
|
30
34
|
}
|
31
35
|
}
|