@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.
@@ -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.onRequest) {
35
- onRequestHooks.push(plugin.onRequest);
36
- }
37
- if (plugin.onResponse) {
38
- onResponseHooks.push(plugin.onResponse);
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 = new Proxy(EMPTY_OBJECT, {
50
- get(_target, prop, _receiver) {
51
- url = new fetchAPI.URL(request.url, 'http://localhost');
52
- return Reflect.get(url, prop, url);
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(cb) {
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(cb) {
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
- ? (waitUntilPromises = [])
264
+ ? waitUntil
230
265
  : undefined);
231
- const response$ = handleRequest(request, serverContext);
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
- waitUntil(writer.write(bufOfRes));
65
- waitUntil(writer.close());
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
- waitUntil(compressionStream.writable.close());
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
- signal = new ServerAdapterRequestAbortSignal();
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, waitUntilPromises) {
446
+ function isolateObject(originalCtx, waitUntilFn) {
444
447
  if (originalCtx == null) {
445
- if (waitUntilPromises == null) {
448
+ if (waitUntilFn == null) {
446
449
  return {};
447
450
  }
448
451
  return {
449
- waitUntil(promise) {
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(promise) {
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.onRequest) {
31
- onRequestHooks.push(plugin.onRequest);
32
- }
33
- if (plugin.onResponse) {
34
- onResponseHooks.push(plugin.onResponse);
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 = new Proxy(EMPTY_OBJECT, {
46
- get(_target, prop, _receiver) {
47
- url = new fetchAPI.URL(request.url, 'http://localhost');
48
- return Reflect.get(url, prop, url);
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(cb) {
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(cb) {
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
- ? (waitUntilPromises = [])
260
+ ? waitUntil
226
261
  : undefined);
227
- const response$ = handleRequest(request, serverContext);
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
- waitUntil(writer.write(bufOfRes));
62
- waitUntil(writer.close());
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
- waitUntil(compressionStream.writable.close());
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
- signal = new ServerAdapterRequestAbortSignal();
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, waitUntilPromises) {
423
+ export function isolateObject(originalCtx, waitUntilFn) {
422
424
  if (originalCtx == null) {
423
- if (waitUntilPromises == null) {
425
+ if (waitUntilFn == null) {
424
426
  return {};
425
427
  }
426
428
  return {
427
- waitUntil(promise) {
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(promise) {
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.56",
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 interface ServerAdapterPlugin<TServerContext = {}> {
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 interface ServerAdapterPlugin<TServerContext = {}> {
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;
@@ -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;
@@ -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;
@@ -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, waitUntilPromises?: Promise<unknown>[]): 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;
@@ -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, waitUntilPromises?: Promise<unknown>[]): 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;