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