@whatwg-node/server 0.9.56 → 0.9.57-alpha-20241125145154-b2c748560e2a94065f9a25e55132e073f78bea40
Sign up to get free protection for your applications and to get access to all the features.
- package/cjs/createServerAdapter.js +74 -30
- package/cjs/plugins/useContentEncoding.js +6 -4
- package/cjs/utils.js +37 -10
- package/esm/createServerAdapter.js +76 -32
- package/esm/plugins/useContentEncoding.js +6 -4
- package/esm/utils.js +36 -10
- package/package.json +2 -1
- package/typings/plugins/types.d.cts +4 -2
- package/typings/plugins/types.d.ts +4 -2
- package/typings/types.d.cts +3 -1
- package/typings/types.d.ts +3 -1
- package/typings/utils.d.cts +3 -2
- package/typings/utils.d.ts +3 -2
@@ -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,13 +27,55 @@ 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
|
+
disposableStack.defer(() => {
|
45
|
+
if (waitUntilPromises.size > 0) {
|
46
|
+
return Promise.allSettled(waitUntilPromises).then(() => {
|
47
|
+
waitUntilPromises.clear();
|
48
|
+
}, () => {
|
49
|
+
waitUntilPromises.clear();
|
50
|
+
});
|
51
|
+
}
|
52
|
+
});
|
53
|
+
function waitUntil(promiseLike) {
|
54
|
+
// If it is a Node.js environment, we should register the disposable stack to handle process termination events
|
55
|
+
if (globalThis.process) {
|
56
|
+
(0, utils_js_1.ensureDisposableStackRegisteredForTerminateEvents)(disposableStack);
|
57
|
+
}
|
58
|
+
waitUntilPromises.add(promiseLike);
|
59
|
+
promiseLike.then(() => {
|
60
|
+
waitUntilPromises.delete(promiseLike);
|
61
|
+
}, err => {
|
62
|
+
console.error(`Unexpected error while waiting: ${err.message || err}`);
|
63
|
+
waitUntilPromises.delete(promiseLike);
|
64
|
+
});
|
65
|
+
}
|
32
66
|
if (options?.plugins != null) {
|
33
67
|
for (const plugin of options.plugins) {
|
34
|
-
if (plugin
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
68
|
+
if (plugin != null) {
|
69
|
+
if (plugin.onRequest) {
|
70
|
+
onRequestHooks.push(plugin.onRequest);
|
71
|
+
}
|
72
|
+
if (plugin.onResponse) {
|
73
|
+
onResponseHooks.push(plugin.onResponse);
|
74
|
+
}
|
75
|
+
const disposeFn = plugin[disposablestack_1.DisposableSymbols.asyncDispose] || plugin[disposablestack_1.DisposableSymbols.dispose];
|
76
|
+
if (disposeFn != null) {
|
77
|
+
disposableStack.defer(disposeFn);
|
78
|
+
}
|
39
79
|
}
|
40
80
|
}
|
41
81
|
}
|
@@ -46,12 +86,13 @@ function createServerAdapter(serverAdapterBaseObject, options) {
|
|
46
86
|
if (onRequestHooks.length === 0) {
|
47
87
|
return handleEarlyResponse();
|
48
88
|
}
|
49
|
-
let url =
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
89
|
+
let url = request['parsedUrl'] ||
|
90
|
+
new Proxy(EMPTY_OBJECT, {
|
91
|
+
get(_target, prop, _receiver) {
|
92
|
+
url = new fetchAPI.URL(request.url, 'http://localhost');
|
93
|
+
return Reflect.get(url, prop, url);
|
94
|
+
},
|
95
|
+
});
|
55
96
|
const onRequestHooksIteration$ = (0, utils_js_1.iterateAsyncVoid)(onRequestHooks, (onRequestHook, stopEarly) => onRequestHook({
|
56
97
|
request,
|
57
98
|
setRequest(newRequest) {
|
@@ -109,7 +150,7 @@ function createServerAdapter(serverAdapterBaseObject, options) {
|
|
109
150
|
// TODO: Remove this on the next major version
|
110
151
|
function handleNodeRequest(nodeRequest, ...ctx) {
|
111
152
|
const serverContext = ctx.length > 1 ? (0, utils_js_1.completeAssign)(...ctx) : ctx[0] || {};
|
112
|
-
const request = (0, utils_js_1.normalizeNodeRequest)(nodeRequest, fetchAPI);
|
153
|
+
const request = (0, utils_js_1.normalizeNodeRequest)(nodeRequest, fetchAPI, registerSignal);
|
113
154
|
return handleRequest(request, serverContext);
|
114
155
|
}
|
115
156
|
function handleNodeRequestAndResponse(nodeRequest, nodeResponseOrContainer, ...ctx) {
|
@@ -118,13 +159,10 @@ function createServerAdapter(serverAdapterBaseObject, options) {
|
|
118
159
|
return handleNodeRequest(nodeRequest, ...ctx);
|
119
160
|
}
|
120
161
|
function requestListener(nodeRequest, nodeResponse, ...ctx) {
|
121
|
-
const waitUntilPromises = [];
|
122
162
|
const defaultServerContext = {
|
123
163
|
req: nodeRequest,
|
124
164
|
res: nodeResponse,
|
125
|
-
waitUntil
|
126
|
-
waitUntilPromises.push(cb.catch(err => console.error(err)));
|
127
|
-
},
|
165
|
+
waitUntil,
|
128
166
|
};
|
129
167
|
let response$;
|
130
168
|
try {
|
@@ -149,19 +187,17 @@ function createServerAdapter(serverAdapterBaseObject, options) {
|
|
149
187
|
}
|
150
188
|
}
|
151
189
|
function handleUWS(res, req, ...ctx) {
|
152
|
-
const waitUntilPromises = [];
|
153
190
|
const defaultServerContext = {
|
154
191
|
res,
|
155
192
|
req,
|
156
|
-
waitUntil
|
157
|
-
waitUntilPromises.push(cb.catch(err => console.error(err)));
|
158
|
-
},
|
193
|
+
waitUntil,
|
159
194
|
};
|
160
195
|
const filteredCtxParts = ctx.filter(partCtx => partCtx != null);
|
161
196
|
const serverContext = filteredCtxParts.length > 0
|
162
197
|
? (0, utils_js_1.completeAssign)(defaultServerContext, ...ctx)
|
163
198
|
: defaultServerContext;
|
164
199
|
const signal = new utils_js_1.ServerAdapterRequestAbortSignal();
|
200
|
+
registerSignal(signal);
|
165
201
|
const originalResEnd = res.end.bind(res);
|
166
202
|
let resEnded = false;
|
167
203
|
res.end = function (data) {
|
@@ -222,17 +258,12 @@ function createServerAdapter(serverAdapterBaseObject, options) {
|
|
222
258
|
}
|
223
259
|
function handleRequestWithWaitUntil(request, ...ctx) {
|
224
260
|
const filteredCtxParts = ctx.filter(partCtx => partCtx != null);
|
225
|
-
let waitUntilPromises;
|
226
261
|
const serverContext = filteredCtxParts.length > 1
|
227
262
|
? (0, utils_js_1.completeAssign)({}, ...filteredCtxParts)
|
228
263
|
: (0, utils_js_1.isolateObject)(filteredCtxParts[0], filteredCtxParts[0] == null || filteredCtxParts[0].waitUntil == null
|
229
|
-
?
|
264
|
+
? waitUntil
|
230
265
|
: undefined);
|
231
|
-
|
232
|
-
if (waitUntilPromises?.length) {
|
233
|
-
return handleWaitUntils(waitUntilPromises).then(() => response$);
|
234
|
-
}
|
235
|
-
return response$;
|
266
|
+
return handleRequest(request, serverContext);
|
236
267
|
}
|
237
268
|
const fetchFn = (input, ...maybeCtx) => {
|
238
269
|
if (typeof input === 'string' || 'href' in input) {
|
@@ -285,6 +316,19 @@ function createServerAdapter(serverAdapterBaseObject, options) {
|
|
285
316
|
handleEvent,
|
286
317
|
handleUWS,
|
287
318
|
handle: genericRequestHandler,
|
319
|
+
disposableStack,
|
320
|
+
[disposablestack_1.DisposableSymbols.asyncDispose]() {
|
321
|
+
if (!disposableStack.disposed) {
|
322
|
+
return disposableStack.disposeAsync();
|
323
|
+
}
|
324
|
+
return (0, uwebsockets_js_1.fakePromise)(undefined);
|
325
|
+
},
|
326
|
+
dispose() {
|
327
|
+
if (!disposableStack.disposed) {
|
328
|
+
return disposableStack.disposeAsync();
|
329
|
+
}
|
330
|
+
return (0, uwebsockets_js_1.fakePromise)(undefined);
|
331
|
+
},
|
288
332
|
};
|
289
333
|
const serverAdapter = new Proxy(genericRequestHandler, {
|
290
334
|
// It should have all the attributes of the handler function and the server instance
|
@@ -47,7 +47,6 @@ function useContentEncoding() {
|
|
47
47
|
}
|
48
48
|
},
|
49
49
|
onResponse({ request, response, setResponse, fetchAPI, serverContext }) {
|
50
|
-
const waitUntil = serverContext.waitUntil?.bind(serverContext) || (() => { });
|
51
50
|
// Hack for avoiding to create whatwg-node to create a readable stream until it's needed
|
52
51
|
if (response['bodyInit'] || response.body) {
|
53
52
|
const encodings = encodingMap.get(request);
|
@@ -61,8 +60,10 @@ function useContentEncoding() {
|
|
61
60
|
const bufOfRes = response._buffer;
|
62
61
|
if (bufOfRes) {
|
63
62
|
const writer = compressionStream.writable.getWriter();
|
64
|
-
|
65
|
-
waitUntil(
|
63
|
+
const write$ = writer.write(bufOfRes);
|
64
|
+
serverContext.waitUntil?.(write$);
|
65
|
+
const close$ = writer.close();
|
66
|
+
serverContext.waitUntil?.(close$);
|
66
67
|
const uint8Arrays$ = (0, utils_js_1.isReadable)(compressionStream.readable['readable'])
|
67
68
|
? collectReadableValues(compressionStream.readable['readable'])
|
68
69
|
: (0, utils_js_1.isAsyncIterable)(compressionStream.readable)
|
@@ -80,7 +81,8 @@ function useContentEncoding() {
|
|
80
81
|
});
|
81
82
|
utils_js_1.decompressedResponseMap.set(compressedResponse, response);
|
82
83
|
setResponse(compressedResponse);
|
83
|
-
|
84
|
+
const close$ = compressionStream.writable.close();
|
85
|
+
serverContext.waitUntil?.(close$);
|
84
86
|
});
|
85
87
|
}
|
86
88
|
}
|
package/cjs/utils.js
CHANGED
@@ -19,6 +19,7 @@ exports.createDeferredPromise = createDeferredPromise;
|
|
19
19
|
exports.handleAbortSignalAndPromiseResponse = handleAbortSignalAndPromiseResponse;
|
20
20
|
exports.getSupportedEncodings = getSupportedEncodings;
|
21
21
|
exports.handleResponseDecompression = handleResponseDecompression;
|
22
|
+
exports.ensureDisposableStackRegisteredForTerminateEvents = ensureDisposableStackRegisteredForTerminateEvents;
|
22
23
|
function isAsyncIterable(body) {
|
23
24
|
return (body != null && typeof body === 'object' && typeof body[Symbol.asyncIterator] === 'function');
|
24
25
|
}
|
@@ -101,7 +102,7 @@ class ServerAdapterRequestAbortSignal extends EventTarget {
|
|
101
102
|
exports.ServerAdapterRequestAbortSignal = ServerAdapterRequestAbortSignal;
|
102
103
|
let bunNodeCompatModeWarned = false;
|
103
104
|
exports.nodeRequestResponseMap = new WeakMap();
|
104
|
-
function normalizeNodeRequest(nodeRequest, fetchAPI) {
|
105
|
+
function normalizeNodeRequest(nodeRequest, fetchAPI, registerSignal) {
|
105
106
|
const rawRequest = nodeRequest.raw || nodeRequest.req || nodeRequest;
|
106
107
|
let fullUrl = buildFullUrl(rawRequest);
|
107
108
|
if (nodeRequest.query) {
|
@@ -127,7 +128,9 @@ function normalizeNodeRequest(nodeRequest, fetchAPI) {
|
|
127
128
|
let sendAbortSignal;
|
128
129
|
// If ponyfilled
|
129
130
|
if (fetchAPI.Request !== globalThis.Request) {
|
130
|
-
|
131
|
+
const newSignal = new ServerAdapterRequestAbortSignal();
|
132
|
+
registerSignal?.(newSignal);
|
133
|
+
signal = newSignal;
|
131
134
|
sendAbortSignal = () => signal.sendAbort();
|
132
135
|
}
|
133
136
|
else {
|
@@ -440,21 +443,17 @@ function handleErrorFromRequestHandler(error, ResponseCtor) {
|
|
440
443
|
status: error.status || 500,
|
441
444
|
});
|
442
445
|
}
|
443
|
-
function isolateObject(originalCtx,
|
446
|
+
function isolateObject(originalCtx, waitUntilFn) {
|
444
447
|
if (originalCtx == null) {
|
445
|
-
if (
|
448
|
+
if (waitUntilFn == null) {
|
446
449
|
return {};
|
447
450
|
}
|
448
451
|
return {
|
449
|
-
waitUntil
|
450
|
-
waitUntilPromises.push(promise.catch(err => console.error(err)));
|
451
|
-
},
|
452
|
+
waitUntil: waitUntilFn,
|
452
453
|
};
|
453
454
|
}
|
454
455
|
return completeAssign(Object.create(originalCtx), {
|
455
|
-
waitUntil
|
456
|
-
waitUntilPromises?.push(promise.catch(err => console.error(err)));
|
457
|
-
},
|
456
|
+
waitUntil: waitUntilFn,
|
458
457
|
}, originalCtx);
|
459
458
|
}
|
460
459
|
function createDeferredPromise() {
|
@@ -548,3 +547,31 @@ function handleResponseDecompression(response, fetchAPI) {
|
|
548
547
|
}
|
549
548
|
return decompressedResponse;
|
550
549
|
}
|
550
|
+
const terminateEvents = ['SIGINT', 'SIGTERM', 'exit'];
|
551
|
+
const disposableStacks = new Set();
|
552
|
+
let eventListenerRegistered = false;
|
553
|
+
function ensureEventListenerForDisposableStacks() {
|
554
|
+
if (eventListenerRegistered) {
|
555
|
+
return;
|
556
|
+
}
|
557
|
+
eventListenerRegistered = true;
|
558
|
+
for (const event of terminateEvents) {
|
559
|
+
globalThis.process.once(event, function terminateHandler() {
|
560
|
+
return Promise.allSettled([...disposableStacks].map(stack => !stack.disposed &&
|
561
|
+
stack.disposeAsync().catch(e => {
|
562
|
+
console.error('Error while disposing:', e);
|
563
|
+
})));
|
564
|
+
});
|
565
|
+
}
|
566
|
+
}
|
567
|
+
function ensureDisposableStackRegisteredForTerminateEvents(disposableStack) {
|
568
|
+
if (globalThis.process) {
|
569
|
+
ensureEventListenerForDisposableStacks();
|
570
|
+
if (!disposableStacks.has(disposableStack)) {
|
571
|
+
disposableStacks.add(disposableStack);
|
572
|
+
disposableStack.defer(() => {
|
573
|
+
disposableStacks.delete(disposableStack);
|
574
|
+
});
|
575
|
+
}
|
576
|
+
}
|
577
|
+
}
|
@@ -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, nodeRequestResponseMap, normalizeNodeRequest, sendNodeResponse, ServerAdapterRequestAbortSignal, } from './utils.js';
|
4
|
-
import { getRequestFromUWSRequest, isUWSResponse, sendResponseToUwsOpts, } from './uwebsockets.js';
|
5
|
-
async function handleWaitUntils(waitUntilPromises) {
|
6
|
-
await Promise.allSettled(waitUntilPromises);
|
7
|
-
}
|
4
|
+
import { completeAssign, ensureDisposableStackRegisteredForTerminateEvents, handleAbortSignalAndPromiseResponse, handleErrorFromRequestHandler, isFetchEvent, isNodeRequest, isolateObject, isPromise, isRequestInit, isServerResponse, iterateAsyncVoid, nodeRequestResponseMap, normalizeNodeRequest, sendNodeResponse, ServerAdapterRequestAbortSignal, } from './utils.js';
|
5
|
+
import { fakePromise, getRequestFromUWSRequest, isUWSResponse, sendResponseToUwsOpts, } from './uwebsockets.js';
|
8
6
|
// Required for envs like nextjs edge runtime
|
9
7
|
function isRequestAccessible(serverContext) {
|
10
8
|
try {
|
@@ -25,13 +23,55 @@ 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
|
+
disposableStack.defer(() => {
|
41
|
+
if (waitUntilPromises.size > 0) {
|
42
|
+
return Promise.allSettled(waitUntilPromises).then(() => {
|
43
|
+
waitUntilPromises.clear();
|
44
|
+
}, () => {
|
45
|
+
waitUntilPromises.clear();
|
46
|
+
});
|
47
|
+
}
|
48
|
+
});
|
49
|
+
function waitUntil(promiseLike) {
|
50
|
+
// If it is a Node.js environment, we should register the disposable stack to handle process termination events
|
51
|
+
if (globalThis.process) {
|
52
|
+
ensureDisposableStackRegisteredForTerminateEvents(disposableStack);
|
53
|
+
}
|
54
|
+
waitUntilPromises.add(promiseLike);
|
55
|
+
promiseLike.then(() => {
|
56
|
+
waitUntilPromises.delete(promiseLike);
|
57
|
+
}, err => {
|
58
|
+
console.error(`Unexpected error while waiting: ${err.message || err}`);
|
59
|
+
waitUntilPromises.delete(promiseLike);
|
60
|
+
});
|
61
|
+
}
|
28
62
|
if (options?.plugins != null) {
|
29
63
|
for (const plugin of options.plugins) {
|
30
|
-
if (plugin
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
64
|
+
if (plugin != null) {
|
65
|
+
if (plugin.onRequest) {
|
66
|
+
onRequestHooks.push(plugin.onRequest);
|
67
|
+
}
|
68
|
+
if (plugin.onResponse) {
|
69
|
+
onResponseHooks.push(plugin.onResponse);
|
70
|
+
}
|
71
|
+
const disposeFn = plugin[DisposableSymbols.asyncDispose] || plugin[DisposableSymbols.dispose];
|
72
|
+
if (disposeFn != null) {
|
73
|
+
disposableStack.defer(disposeFn);
|
74
|
+
}
|
35
75
|
}
|
36
76
|
}
|
37
77
|
}
|
@@ -42,12 +82,13 @@ function createServerAdapter(serverAdapterBaseObject, options) {
|
|
42
82
|
if (onRequestHooks.length === 0) {
|
43
83
|
return handleEarlyResponse();
|
44
84
|
}
|
45
|
-
let url =
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
85
|
+
let url = request['parsedUrl'] ||
|
86
|
+
new Proxy(EMPTY_OBJECT, {
|
87
|
+
get(_target, prop, _receiver) {
|
88
|
+
url = new fetchAPI.URL(request.url, 'http://localhost');
|
89
|
+
return Reflect.get(url, prop, url);
|
90
|
+
},
|
91
|
+
});
|
51
92
|
const onRequestHooksIteration$ = iterateAsyncVoid(onRequestHooks, (onRequestHook, stopEarly) => onRequestHook({
|
52
93
|
request,
|
53
94
|
setRequest(newRequest) {
|
@@ -105,7 +146,7 @@ function createServerAdapter(serverAdapterBaseObject, options) {
|
|
105
146
|
// TODO: Remove this on the next major version
|
106
147
|
function handleNodeRequest(nodeRequest, ...ctx) {
|
107
148
|
const serverContext = ctx.length > 1 ? completeAssign(...ctx) : ctx[0] || {};
|
108
|
-
const request = normalizeNodeRequest(nodeRequest, fetchAPI);
|
149
|
+
const request = normalizeNodeRequest(nodeRequest, fetchAPI, registerSignal);
|
109
150
|
return handleRequest(request, serverContext);
|
110
151
|
}
|
111
152
|
function handleNodeRequestAndResponse(nodeRequest, nodeResponseOrContainer, ...ctx) {
|
@@ -114,13 +155,10 @@ function createServerAdapter(serverAdapterBaseObject, options) {
|
|
114
155
|
return handleNodeRequest(nodeRequest, ...ctx);
|
115
156
|
}
|
116
157
|
function requestListener(nodeRequest, nodeResponse, ...ctx) {
|
117
|
-
const waitUntilPromises = [];
|
118
158
|
const defaultServerContext = {
|
119
159
|
req: nodeRequest,
|
120
160
|
res: nodeResponse,
|
121
|
-
waitUntil
|
122
|
-
waitUntilPromises.push(cb.catch(err => console.error(err)));
|
123
|
-
},
|
161
|
+
waitUntil,
|
124
162
|
};
|
125
163
|
let response$;
|
126
164
|
try {
|
@@ -145,19 +183,17 @@ function createServerAdapter(serverAdapterBaseObject, options) {
|
|
145
183
|
}
|
146
184
|
}
|
147
185
|
function handleUWS(res, req, ...ctx) {
|
148
|
-
const waitUntilPromises = [];
|
149
186
|
const defaultServerContext = {
|
150
187
|
res,
|
151
188
|
req,
|
152
|
-
waitUntil
|
153
|
-
waitUntilPromises.push(cb.catch(err => console.error(err)));
|
154
|
-
},
|
189
|
+
waitUntil,
|
155
190
|
};
|
156
191
|
const filteredCtxParts = ctx.filter(partCtx => partCtx != null);
|
157
192
|
const serverContext = filteredCtxParts.length > 0
|
158
193
|
? completeAssign(defaultServerContext, ...ctx)
|
159
194
|
: defaultServerContext;
|
160
195
|
const signal = new ServerAdapterRequestAbortSignal();
|
196
|
+
registerSignal(signal);
|
161
197
|
const originalResEnd = res.end.bind(res);
|
162
198
|
let resEnded = false;
|
163
199
|
res.end = function (data) {
|
@@ -218,17 +254,12 @@ function createServerAdapter(serverAdapterBaseObject, options) {
|
|
218
254
|
}
|
219
255
|
function handleRequestWithWaitUntil(request, ...ctx) {
|
220
256
|
const filteredCtxParts = ctx.filter(partCtx => partCtx != null);
|
221
|
-
let waitUntilPromises;
|
222
257
|
const serverContext = filteredCtxParts.length > 1
|
223
258
|
? completeAssign({}, ...filteredCtxParts)
|
224
259
|
: isolateObject(filteredCtxParts[0], filteredCtxParts[0] == null || filteredCtxParts[0].waitUntil == null
|
225
|
-
?
|
260
|
+
? waitUntil
|
226
261
|
: undefined);
|
227
|
-
|
228
|
-
if (waitUntilPromises?.length) {
|
229
|
-
return handleWaitUntils(waitUntilPromises).then(() => response$);
|
230
|
-
}
|
231
|
-
return response$;
|
262
|
+
return handleRequest(request, serverContext);
|
232
263
|
}
|
233
264
|
const fetchFn = (input, ...maybeCtx) => {
|
234
265
|
if (typeof input === 'string' || 'href' in input) {
|
@@ -281,6 +312,19 @@ function createServerAdapter(serverAdapterBaseObject, options) {
|
|
281
312
|
handleEvent,
|
282
313
|
handleUWS,
|
283
314
|
handle: genericRequestHandler,
|
315
|
+
disposableStack,
|
316
|
+
[DisposableSymbols.asyncDispose]() {
|
317
|
+
if (!disposableStack.disposed) {
|
318
|
+
return disposableStack.disposeAsync();
|
319
|
+
}
|
320
|
+
return fakePromise(undefined);
|
321
|
+
},
|
322
|
+
dispose() {
|
323
|
+
if (!disposableStack.disposed) {
|
324
|
+
return disposableStack.disposeAsync();
|
325
|
+
}
|
326
|
+
return fakePromise(undefined);
|
327
|
+
},
|
284
328
|
};
|
285
329
|
const serverAdapter = new Proxy(genericRequestHandler, {
|
286
330
|
// It should have all the attributes of the handler function and the server instance
|
@@ -44,7 +44,6 @@ export function useContentEncoding() {
|
|
44
44
|
}
|
45
45
|
},
|
46
46
|
onResponse({ request, response, setResponse, fetchAPI, serverContext }) {
|
47
|
-
const waitUntil = serverContext.waitUntil?.bind(serverContext) || (() => { });
|
48
47
|
// Hack for avoiding to create whatwg-node to create a readable stream until it's needed
|
49
48
|
if (response['bodyInit'] || response.body) {
|
50
49
|
const encodings = encodingMap.get(request);
|
@@ -58,8 +57,10 @@ export function useContentEncoding() {
|
|
58
57
|
const bufOfRes = response._buffer;
|
59
58
|
if (bufOfRes) {
|
60
59
|
const writer = compressionStream.writable.getWriter();
|
61
|
-
|
62
|
-
waitUntil(
|
60
|
+
const write$ = writer.write(bufOfRes);
|
61
|
+
serverContext.waitUntil?.(write$);
|
62
|
+
const close$ = writer.close();
|
63
|
+
serverContext.waitUntil?.(close$);
|
63
64
|
const uint8Arrays$ = isReadable(compressionStream.readable['readable'])
|
64
65
|
? collectReadableValues(compressionStream.readable['readable'])
|
65
66
|
: isAsyncIterable(compressionStream.readable)
|
@@ -77,7 +78,8 @@ export function useContentEncoding() {
|
|
77
78
|
});
|
78
79
|
decompressedResponseMap.set(compressedResponse, response);
|
79
80
|
setResponse(compressedResponse);
|
80
|
-
|
81
|
+
const close$ = compressionStream.writable.close();
|
82
|
+
serverContext.waitUntil?.(close$);
|
81
83
|
});
|
82
84
|
}
|
83
85
|
}
|
package/esm/utils.js
CHANGED
@@ -79,7 +79,7 @@ export class ServerAdapterRequestAbortSignal extends EventTarget {
|
|
79
79
|
}
|
80
80
|
let bunNodeCompatModeWarned = false;
|
81
81
|
export const nodeRequestResponseMap = new WeakMap();
|
82
|
-
export function normalizeNodeRequest(nodeRequest, fetchAPI) {
|
82
|
+
export function normalizeNodeRequest(nodeRequest, fetchAPI, registerSignal) {
|
83
83
|
const rawRequest = nodeRequest.raw || nodeRequest.req || nodeRequest;
|
84
84
|
let fullUrl = buildFullUrl(rawRequest);
|
85
85
|
if (nodeRequest.query) {
|
@@ -105,7 +105,9 @@ export function normalizeNodeRequest(nodeRequest, fetchAPI) {
|
|
105
105
|
let sendAbortSignal;
|
106
106
|
// If ponyfilled
|
107
107
|
if (fetchAPI.Request !== globalThis.Request) {
|
108
|
-
|
108
|
+
const newSignal = new ServerAdapterRequestAbortSignal();
|
109
|
+
registerSignal?.(newSignal);
|
110
|
+
signal = newSignal;
|
109
111
|
sendAbortSignal = () => signal.sendAbort();
|
110
112
|
}
|
111
113
|
else {
|
@@ -418,21 +420,17 @@ export function handleErrorFromRequestHandler(error, ResponseCtor) {
|
|
418
420
|
status: error.status || 500,
|
419
421
|
});
|
420
422
|
}
|
421
|
-
export function isolateObject(originalCtx,
|
423
|
+
export function isolateObject(originalCtx, waitUntilFn) {
|
422
424
|
if (originalCtx == null) {
|
423
|
-
if (
|
425
|
+
if (waitUntilFn == null) {
|
424
426
|
return {};
|
425
427
|
}
|
426
428
|
return {
|
427
|
-
waitUntil
|
428
|
-
waitUntilPromises.push(promise.catch(err => console.error(err)));
|
429
|
-
},
|
429
|
+
waitUntil: waitUntilFn,
|
430
430
|
};
|
431
431
|
}
|
432
432
|
return completeAssign(Object.create(originalCtx), {
|
433
|
-
waitUntil
|
434
|
-
waitUntilPromises?.push(promise.catch(err => console.error(err)));
|
435
|
-
},
|
433
|
+
waitUntil: waitUntilFn,
|
436
434
|
}, originalCtx);
|
437
435
|
}
|
438
436
|
export function createDeferredPromise() {
|
@@ -526,3 +524,31 @@ export function handleResponseDecompression(response, fetchAPI) {
|
|
526
524
|
}
|
527
525
|
return decompressedResponse;
|
528
526
|
}
|
527
|
+
const terminateEvents = ['SIGINT', 'SIGTERM', 'exit'];
|
528
|
+
const disposableStacks = new Set();
|
529
|
+
let eventListenerRegistered = false;
|
530
|
+
function ensureEventListenerForDisposableStacks() {
|
531
|
+
if (eventListenerRegistered) {
|
532
|
+
return;
|
533
|
+
}
|
534
|
+
eventListenerRegistered = true;
|
535
|
+
for (const event of terminateEvents) {
|
536
|
+
globalThis.process.once(event, function terminateHandler() {
|
537
|
+
return Promise.allSettled([...disposableStacks].map(stack => !stack.disposed &&
|
538
|
+
stack.disposeAsync().catch(e => {
|
539
|
+
console.error('Error while disposing:', e);
|
540
|
+
})));
|
541
|
+
});
|
542
|
+
}
|
543
|
+
}
|
544
|
+
export function ensureDisposableStackRegisteredForTerminateEvents(disposableStack) {
|
545
|
+
if (globalThis.process) {
|
546
|
+
ensureEventListenerForDisposableStacks();
|
547
|
+
if (!disposableStacks.has(disposableStack)) {
|
548
|
+
disposableStacks.add(disposableStack);
|
549
|
+
disposableStack.defer(() => {
|
550
|
+
disposableStacks.delete(disposableStack);
|
551
|
+
});
|
552
|
+
}
|
553
|
+
}
|
554
|
+
}
|
package/package.json
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
{
|
2
2
|
"name": "@whatwg-node/server",
|
3
|
-
"version": "0.9.
|
3
|
+
"version": "0.9.57-alpha-20241125145154-b2c748560e2a94065f9a25e55132e073f78bea40",
|
4
4
|
"description": "Fetch API compliant HTTP Server adapter",
|
5
5
|
"sideEffects": false,
|
6
6
|
"dependencies": {
|
7
|
+
"@whatwg-node/disposablestack": "^0.0.5",
|
7
8
|
"@whatwg-node/fetch": "^0.10.0",
|
8
9
|
"tslib": "^2.6.3"
|
9
10
|
},
|
@@ -1,8 +1,10 @@
|
|
1
1
|
import { FetchAPI, ServerAdapterRequestHandler, type ServerAdapterInitialContext } from '../types.cjs';
|
2
|
-
export
|
2
|
+
export type ServerAdapterPlugin<TServerContext = {}> = {
|
3
3
|
onRequest?: OnRequestHook<TServerContext & ServerAdapterInitialContext>;
|
4
4
|
onResponse?: OnResponseHook<TServerContext & ServerAdapterInitialContext>;
|
5
|
-
|
5
|
+
[Symbol.dispose]?: () => void;
|
6
|
+
[Symbol.asyncDispose]?: () => PromiseLike<void> | void;
|
7
|
+
} | undefined;
|
6
8
|
export type OnRequestHook<TServerContext> = (payload: OnRequestEventPayload<TServerContext>) => Promise<void> | void;
|
7
9
|
export interface OnRequestEventPayload<TServerContext> {
|
8
10
|
request: Request;
|
@@ -1,8 +1,10 @@
|
|
1
1
|
import { FetchAPI, ServerAdapterRequestHandler, type ServerAdapterInitialContext } from '../types.js';
|
2
|
-
export
|
2
|
+
export type ServerAdapterPlugin<TServerContext = {}> = {
|
3
3
|
onRequest?: OnRequestHook<TServerContext & ServerAdapterInitialContext>;
|
4
4
|
onResponse?: OnResponseHook<TServerContext & ServerAdapterInitialContext>;
|
5
|
-
|
5
|
+
[Symbol.dispose]?: () => void;
|
6
|
+
[Symbol.asyncDispose]?: () => PromiseLike<void> | void;
|
7
|
+
} | undefined;
|
6
8
|
export type OnRequestHook<TServerContext> = (payload: OnRequestEventPayload<TServerContext>) => Promise<void> | void;
|
7
9
|
export interface OnRequestEventPayload<TServerContext> {
|
8
10
|
request: Request;
|
package/typings/types.d.cts
CHANGED
@@ -13,7 +13,7 @@ export interface ServerAdapterBaseObject<TServerContext, THandleRequest extends
|
|
13
13
|
*/
|
14
14
|
handle: THandleRequest;
|
15
15
|
}
|
16
|
-
export interface ServerAdapterObject<TServerContext> extends EventListenerObject {
|
16
|
+
export interface ServerAdapterObject<TServerContext> extends EventListenerObject, AsyncDisposable {
|
17
17
|
/**
|
18
18
|
* A basic request listener that takes a `Request` with the server context and returns a `Response`.
|
19
19
|
*/
|
@@ -56,6 +56,8 @@ export interface ServerAdapterObject<TServerContext> extends EventListenerObject
|
|
56
56
|
handle(container: {
|
57
57
|
request: Request;
|
58
58
|
} & Partial<TServerContext & ServerAdapterInitialContext>, ...ctx: Partial<TServerContext & ServerAdapterInitialContext>[]): Promise<Response> | Response;
|
59
|
+
disposableStack: AsyncDisposableStack;
|
60
|
+
dispose(): Promise<void> | void;
|
59
61
|
}
|
60
62
|
export interface RequestLike {
|
61
63
|
url: string;
|
package/typings/types.d.ts
CHANGED
@@ -13,7 +13,7 @@ export interface ServerAdapterBaseObject<TServerContext, THandleRequest extends
|
|
13
13
|
*/
|
14
14
|
handle: THandleRequest;
|
15
15
|
}
|
16
|
-
export interface ServerAdapterObject<TServerContext> extends EventListenerObject {
|
16
|
+
export interface ServerAdapterObject<TServerContext> extends EventListenerObject, AsyncDisposable {
|
17
17
|
/**
|
18
18
|
* A basic request listener that takes a `Request` with the server context and returns a `Response`.
|
19
19
|
*/
|
@@ -56,6 +56,8 @@ export interface ServerAdapterObject<TServerContext> extends EventListenerObject
|
|
56
56
|
handle(container: {
|
57
57
|
request: Request;
|
58
58
|
} & Partial<TServerContext & ServerAdapterInitialContext>, ...ctx: Partial<TServerContext & ServerAdapterInitialContext>[]): Promise<Response> | Response;
|
59
|
+
disposableStack: AsyncDisposableStack;
|
60
|
+
dispose(): Promise<void> | void;
|
59
61
|
}
|
60
62
|
export interface RequestLike {
|
61
63
|
url: string;
|
package/typings/utils.d.cts
CHANGED
@@ -31,7 +31,7 @@ export declare class ServerAdapterRequestAbortSignal extends EventTarget impleme
|
|
31
31
|
any(signals: Iterable<AbortSignal>): AbortSignal;
|
32
32
|
}
|
33
33
|
export declare const nodeRequestResponseMap: WeakMap<NodeRequest, NodeResponse>;
|
34
|
-
export declare function normalizeNodeRequest(nodeRequest: NodeRequest, fetchAPI: FetchAPI): Request;
|
34
|
+
export declare function normalizeNodeRequest(nodeRequest: NodeRequest, fetchAPI: FetchAPI, registerSignal?: (signal: ServerAdapterRequestAbortSignal) => void): Request;
|
35
35
|
export declare function isReadable(stream: any): stream is Readable;
|
36
36
|
export declare function isNodeRequest(request: any): request is NodeRequest;
|
37
37
|
export declare function isServerResponse(stream: any): stream is NodeResponse;
|
@@ -43,7 +43,7 @@ export declare function completeAssign(...args: any[]): any;
|
|
43
43
|
export declare function isPromise<T>(val: T | Promise<T>): val is Promise<T>;
|
44
44
|
export declare function iterateAsyncVoid<TInput>(iterable: Iterable<TInput>, callback: (input: TInput, stopEarly: () => void) => Promise<void> | void): Promise<void> | void;
|
45
45
|
export declare function handleErrorFromRequestHandler(error: any, ResponseCtor: typeof Response): Response;
|
46
|
-
export declare function isolateObject<TIsolatedObject extends object>(originalCtx: TIsolatedObject,
|
46
|
+
export declare function isolateObject<TIsolatedObject extends object>(originalCtx: TIsolatedObject, waitUntilFn?: (promiseLike: PromiseLike<unknown>) => void): TIsolatedObject;
|
47
47
|
export interface DeferredPromise<T = void> {
|
48
48
|
promise: Promise<T>;
|
49
49
|
resolve: (value: T) => void;
|
@@ -54,3 +54,4 @@ export declare function handleAbortSignalAndPromiseResponse(response$: Promise<R
|
|
54
54
|
export declare const decompressedResponseMap: WeakMap<Response, Response>;
|
55
55
|
export declare function getSupportedEncodings(fetchAPI: FetchAPI): CompressionFormat[];
|
56
56
|
export declare function handleResponseDecompression(response: Response, fetchAPI: FetchAPI): Response;
|
57
|
+
export declare function ensureDisposableStackRegisteredForTerminateEvents(disposableStack: AsyncDisposableStack): void;
|
package/typings/utils.d.ts
CHANGED
@@ -31,7 +31,7 @@ export declare class ServerAdapterRequestAbortSignal extends EventTarget impleme
|
|
31
31
|
any(signals: Iterable<AbortSignal>): AbortSignal;
|
32
32
|
}
|
33
33
|
export declare const nodeRequestResponseMap: WeakMap<NodeRequest, NodeResponse>;
|
34
|
-
export declare function normalizeNodeRequest(nodeRequest: NodeRequest, fetchAPI: FetchAPI): Request;
|
34
|
+
export declare function normalizeNodeRequest(nodeRequest: NodeRequest, fetchAPI: FetchAPI, registerSignal?: (signal: ServerAdapterRequestAbortSignal) => void): Request;
|
35
35
|
export declare function isReadable(stream: any): stream is Readable;
|
36
36
|
export declare function isNodeRequest(request: any): request is NodeRequest;
|
37
37
|
export declare function isServerResponse(stream: any): stream is NodeResponse;
|
@@ -43,7 +43,7 @@ export declare function completeAssign(...args: any[]): any;
|
|
43
43
|
export declare function isPromise<T>(val: T | Promise<T>): val is Promise<T>;
|
44
44
|
export declare function iterateAsyncVoid<TInput>(iterable: Iterable<TInput>, callback: (input: TInput, stopEarly: () => void) => Promise<void> | void): Promise<void> | void;
|
45
45
|
export declare function handleErrorFromRequestHandler(error: any, ResponseCtor: typeof Response): Response;
|
46
|
-
export declare function isolateObject<TIsolatedObject extends object>(originalCtx: TIsolatedObject,
|
46
|
+
export declare function isolateObject<TIsolatedObject extends object>(originalCtx: TIsolatedObject, waitUntilFn?: (promiseLike: PromiseLike<unknown>) => void): TIsolatedObject;
|
47
47
|
export interface DeferredPromise<T = void> {
|
48
48
|
promise: Promise<T>;
|
49
49
|
resolve: (value: T) => void;
|
@@ -54,3 +54,4 @@ export declare function handleAbortSignalAndPromiseResponse(response$: Promise<R
|
|
54
54
|
export declare const decompressedResponseMap: WeakMap<Response, Response>;
|
55
55
|
export declare function getSupportedEncodings(fetchAPI: FetchAPI): CompressionFormat[];
|
56
56
|
export declare function handleResponseDecompression(response: Response, fetchAPI: FetchAPI): Response;
|
57
|
+
export declare function ensureDisposableStackRegisteredForTerminateEvents(disposableStack: AsyncDisposableStack): void;
|