@whatwg-node/server 0.10.0-alpha-20241125144944-df106a17746126e3c8089afa16af852a4483c8b3 → 0.10.0-alpha-20250205002109-e61c2d65aed8aa582581368511e66b20e33632b8

Sign up to get free protection for your applications and to get access to all the features.
package/cjs/utils.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.decompressedResponseMap = exports.nodeRequestResponseMap = exports.ServerAdapterRequestAbortSignal = void 0;
3
+ exports.decompressedResponseMap = exports.nodeRequestResponseMap = void 0;
4
4
  exports.isAsyncIterable = isAsyncIterable;
5
5
  exports.normalizeNodeRequest = normalizeNodeRequest;
6
6
  exports.isReadable = isReadable;
@@ -69,40 +69,9 @@ function isRequestBody(body) {
69
69
  }
70
70
  return false;
71
71
  }
72
- class ServerAdapterRequestAbortSignal extends EventTarget {
73
- aborted = false;
74
- _onabort = null;
75
- reason;
76
- throwIfAborted() {
77
- if (this.aborted) {
78
- throw this.reason;
79
- }
80
- }
81
- sendAbort() {
82
- this.reason = new DOMException('This operation was aborted', 'AbortError');
83
- this.aborted = true;
84
- this.dispatchEvent(new Event('abort'));
85
- }
86
- get onabort() {
87
- return this._onabort;
88
- }
89
- set onabort(value) {
90
- this._onabort = value;
91
- if (value) {
92
- this.addEventListener('abort', value);
93
- }
94
- else {
95
- this.removeEventListener('abort', value);
96
- }
97
- }
98
- any(signals) {
99
- return AbortSignal.any([...signals]);
100
- }
101
- }
102
- exports.ServerAdapterRequestAbortSignal = ServerAdapterRequestAbortSignal;
103
72
  let bunNodeCompatModeWarned = false;
104
73
  exports.nodeRequestResponseMap = new WeakMap();
105
- function normalizeNodeRequest(nodeRequest, fetchAPI, registerSignal) {
74
+ function normalizeNodeRequest(nodeRequest, fetchAPI) {
106
75
  const rawRequest = nodeRequest.raw || nodeRequest.req || nodeRequest;
107
76
  let fullUrl = buildFullUrl(rawRequest);
108
77
  if (nodeRequest.query) {
@@ -112,7 +81,6 @@ function normalizeNodeRequest(nodeRequest, fetchAPI, registerSignal) {
112
81
  }
113
82
  fullUrl = url.toString();
114
83
  }
115
- let signal;
116
84
  const nodeResponse = exports.nodeRequestResponseMap.get(nodeRequest);
117
85
  exports.nodeRequestResponseMap.delete(nodeRequest);
118
86
  let normalizedHeaders = nodeRequest.headers;
@@ -124,24 +92,12 @@ function normalizeNodeRequest(nodeRequest, fetchAPI, registerSignal) {
124
92
  }
125
93
  }
126
94
  }
95
+ const controller = new AbortController();
127
96
  if (nodeResponse?.once) {
128
- let sendAbortSignal;
129
- // If ponyfilled
130
- if (fetchAPI.Request !== globalThis.Request) {
131
- const newSignal = new ServerAdapterRequestAbortSignal();
132
- registerSignal?.(newSignal);
133
- signal = newSignal;
134
- sendAbortSignal = () => signal.sendAbort();
135
- }
136
- else {
137
- const controller = new AbortController();
138
- signal = controller.signal;
139
- sendAbortSignal = () => controller.abort();
140
- }
141
97
  const closeEventListener = () => {
142
- if (signal && !signal.aborted) {
143
- rawRequest.aborted = true;
144
- sendAbortSignal();
98
+ if (!controller.signal.aborted) {
99
+ Object.defineProperty(rawRequest, 'aborted', { value: true });
100
+ controller.abort(nodeResponse.errored ?? undefined);
145
101
  }
146
102
  };
147
103
  nodeResponse.once('error', closeEventListener);
@@ -154,7 +110,7 @@ function normalizeNodeRequest(nodeRequest, fetchAPI, registerSignal) {
154
110
  return new fetchAPI.Request(fullUrl, {
155
111
  method: nodeRequest.method,
156
112
  headers: normalizedHeaders,
157
- signal,
113
+ signal: controller.signal,
158
114
  });
159
115
  }
160
116
  /**
@@ -167,16 +123,16 @@ function normalizeNodeRequest(nodeRequest, fetchAPI, registerSignal) {
167
123
  if (maybeParsedBody != null && Object.keys(maybeParsedBody).length > 0) {
168
124
  if (isRequestBody(maybeParsedBody)) {
169
125
  return new fetchAPI.Request(fullUrl, {
170
- method: nodeRequest.method,
126
+ method: nodeRequest.method || 'GET',
171
127
  headers: normalizedHeaders,
172
128
  body: maybeParsedBody,
173
- signal,
129
+ signal: controller.signal,
174
130
  });
175
131
  }
176
132
  const request = new fetchAPI.Request(fullUrl, {
177
- method: nodeRequest.method,
133
+ method: nodeRequest.method || 'GET',
178
134
  headers: normalizedHeaders,
179
- signal,
135
+ signal: controller.signal,
180
136
  });
181
137
  if (!request.headers.get('content-type')?.includes('json')) {
182
138
  request.headers.set('content-type', 'application/json; charset=utf-8');
@@ -221,16 +177,17 @@ It will affect your performance. Please check our Bun integration recipe, and av
221
177
  rawRequest.destroy(e);
222
178
  },
223
179
  }),
224
- signal,
180
+ signal: controller.signal,
225
181
  });
226
182
  }
227
183
  // perf: instead of spreading the object, we can just pass it as is and it performs better
228
184
  return new fetchAPI.Request(fullUrl, {
229
185
  method: nodeRequest.method,
230
186
  headers: normalizedHeaders,
187
+ signal: controller.signal,
188
+ // @ts-expect-error - AsyncIterable is supported as body
231
189
  body: rawRequest,
232
190
  duplex: 'half',
233
- signal,
234
191
  });
235
192
  }
236
193
  function isReadable(stream) {
@@ -276,13 +233,25 @@ async function sendAsyncIterable(serverResponse, asyncIterable) {
276
233
  if (closed) {
277
234
  break;
278
235
  }
279
- if (!serverResponse
280
- // @ts-expect-error http and http2 writes are actually compatible
281
- .write(chunk)) {
282
- if (closed) {
283
- break;
236
+ const shouldBreak = await new Promise(resolve => {
237
+ if (!serverResponse
238
+ // @ts-expect-error http and http2 writes are actually compatible
239
+ .write(chunk, err => {
240
+ if (err) {
241
+ resolve(true);
242
+ }
243
+ })) {
244
+ if (closed) {
245
+ resolve(true);
246
+ return;
247
+ }
248
+ serverResponse.once('drain', () => {
249
+ resolve(false);
250
+ });
284
251
  }
285
- await new Promise(resolve => serverResponse.once('drain', resolve));
252
+ });
253
+ if (shouldBreak) {
254
+ break;
286
255
  }
287
256
  }
288
257
  endResponse(serverResponse);
@@ -343,15 +312,15 @@ function sendNodeResponse(fetchResponse, serverResponse, nodeRequest) {
343
312
  return;
344
313
  }
345
314
  if (isReadableStream(fetchBody)) {
346
- return sendReadableStream(serverResponse, fetchBody);
315
+ return sendReadableStream(nodeRequest, serverResponse, fetchBody);
347
316
  }
348
317
  if (isAsyncIterable(fetchBody)) {
349
318
  return sendAsyncIterable(serverResponse, fetchBody);
350
319
  }
351
320
  }
352
- async function sendReadableStream(serverResponse, readableStream) {
321
+ async function sendReadableStream(nodeRequest, serverResponse, readableStream) {
353
322
  const reader = readableStream.getReader();
354
- serverResponse.req.once('error', err => {
323
+ nodeRequest?.once?.('error', err => {
355
324
  reader.cancel(err);
356
325
  });
357
326
  while (true) {
@@ -474,11 +443,14 @@ function createDeferredPromise() {
474
443
  };
475
444
  }
476
445
  function handleAbortSignalAndPromiseResponse(response$, abortSignal) {
446
+ if (abortSignal?.aborted) {
447
+ throw abortSignal.reason;
448
+ }
477
449
  if (isPromise(response$) && abortSignal) {
478
450
  const deferred$ = createDeferredPromise();
479
451
  abortSignal.addEventListener('abort', function abortSignalFetchErrorHandler() {
480
452
  deferred$.reject(abortSignal.reason);
481
- });
453
+ }, { once: true });
482
454
  response$
483
455
  .then(function fetchSuccessHandler(res) {
484
456
  deferred$.resolve(res);
@@ -547,7 +519,7 @@ function handleResponseDecompression(response, fetchAPI) {
547
519
  }
548
520
  return decompressedResponse;
549
521
  }
550
- const terminateEvents = ['SIGINT', 'SIGTERM', 'exit'];
522
+ const terminateEvents = ['SIGINT', 'exit', 'SIGTERM'];
551
523
  const disposableStacks = new Set();
552
524
  let eventListenerRegistered = false;
553
525
  function ensureEventListenerForDisposableStacks() {
@@ -557,10 +529,7 @@ function ensureEventListenerForDisposableStacks() {
557
529
  eventListenerRegistered = true;
558
530
  for (const event of terminateEvents) {
559
531
  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
- })));
532
+ return Promise.allSettled([...disposableStacks].map(stack => !stack.disposed && stack.disposeAsync()));
564
533
  });
565
534
  }
566
535
  }
@@ -9,7 +9,7 @@ const utils_js_1 = require("./utils.js");
9
9
  function isUWSResponse(res) {
10
10
  return !!res.onData;
11
11
  }
12
- function getRequestFromUWSRequest({ req, res, fetchAPI, signal }) {
12
+ function getRequestFromUWSRequest({ req, res, fetchAPI, controller, }) {
13
13
  const method = req.getMethod();
14
14
  let duplex;
15
15
  const chunks = [];
@@ -43,9 +43,9 @@ function getRequestFromUWSRequest({ req, res, fetchAPI, signal }) {
43
43
  let getReadableStream;
44
44
  if (method !== 'get' && method !== 'head') {
45
45
  duplex = 'half';
46
- signal.addEventListener('abort', () => {
46
+ controller.signal.addEventListener('abort', () => {
47
47
  stop();
48
- });
48
+ }, { once: true });
49
49
  let readableStream;
50
50
  getReadableStream = () => {
51
51
  if (!readableStream) {
@@ -97,7 +97,7 @@ function getRequestFromUWSRequest({ req, res, fetchAPI, signal }) {
97
97
  get body() {
98
98
  return getBody();
99
99
  },
100
- signal,
100
+ signal: controller.signal,
101
101
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
102
102
  // @ts-ignore - not in the TS types yet
103
103
  duplex,
@@ -171,14 +171,14 @@ function createWritableFromUWS(uwsResponse, fetchAPI) {
171
171
  },
172
172
  });
173
173
  }
174
- function sendResponseToUwsOpts(uwsResponse, fetchResponse, signal, fetchAPI) {
174
+ function sendResponseToUwsOpts(uwsResponse, fetchResponse, controller, fetchAPI) {
175
175
  if (!fetchResponse) {
176
176
  uwsResponse.writeStatus('404 Not Found');
177
177
  uwsResponse.end();
178
178
  return;
179
179
  }
180
180
  const bufferOfRes = fetchResponse._buffer;
181
- if (signal.aborted) {
181
+ if (controller.signal.aborted) {
182
182
  return;
183
183
  }
184
184
  uwsResponse.cork(() => {
@@ -208,17 +208,17 @@ function sendResponseToUwsOpts(uwsResponse, fetchResponse, signal, fetchAPI) {
208
208
  if (bufferOfRes || !fetchResponse.body) {
209
209
  return;
210
210
  }
211
- signal.addEventListener('abort', () => {
211
+ controller.signal.addEventListener('abort', () => {
212
212
  if (!fetchResponse.body?.locked) {
213
- fetchResponse.body?.cancel(signal.reason);
213
+ fetchResponse.body?.cancel(controller.signal.reason);
214
214
  }
215
- });
215
+ }, { once: true });
216
216
  return fetchResponse.body
217
217
  .pipeTo(createWritableFromUWS(uwsResponse, fetchAPI), {
218
- signal,
218
+ signal: controller.signal,
219
219
  })
220
220
  .catch(err => {
221
- if (signal.aborted) {
221
+ if (controller.signal.aborted) {
222
222
  return;
223
223
  }
224
224
  throw err;
@@ -1,7 +1,6 @@
1
- /* eslint-disable @typescript-eslint/ban-types */
2
1
  import { AsyncDisposableStack, DisposableSymbols } from '@whatwg-node/disposablestack';
3
2
  import * as DefaultFetchAPI from '@whatwg-node/fetch';
4
- import { completeAssign, ensureDisposableStackRegisteredForTerminateEvents, handleAbortSignalAndPromiseResponse, handleErrorFromRequestHandler, isFetchEvent, isNodeRequest, isolateObject, isPromise, isRequestInit, isServerResponse, iterateAsyncVoid, nodeRequestResponseMap, normalizeNodeRequest, sendNodeResponse, ServerAdapterRequestAbortSignal, } from './utils.js';
3
+ import { completeAssign, ensureDisposableStackRegisteredForTerminateEvents, handleAbortSignalAndPromiseResponse, handleErrorFromRequestHandler, isFetchEvent, isNodeRequest, isolateObject, isPromise, isRequestInit, isServerResponse, iterateAsyncVoid, nodeRequestResponseMap, normalizeNodeRequest, sendNodeResponse, } from './utils.js';
5
4
  import { fakePromise, getRequestFromUWSRequest, isUWSResponse, sendResponseToUwsOpts, } from './uwebsockets.js';
6
5
  // Required for envs like nextjs edge runtime
7
6
  function isRequestAccessible(serverContext) {
@@ -24,54 +23,56 @@ function createServerAdapter(serverAdapterBaseObject, options) {
24
23
  const onRequestHooks = [];
25
24
  const onResponseHooks = [];
26
25
  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();
26
+ let _disposableStack;
27
+ function ensureDisposableStack() {
28
+ if (!_disposableStack) {
29
+ _disposableStack = new AsyncDisposableStack();
30
+ if (options?.disposeOnProcessTerminate) {
31
+ ensureDisposableStackRegisteredForTerminateEvents(_disposableStack);
32
+ }
33
+ _disposableStack.defer(() => {
34
+ if (waitUntilPromises.size > 0) {
35
+ return Promise.allSettled(waitUntilPromises).then(() => {
36
+ waitUntilPromises.clear();
37
+ }, () => {
38
+ waitUntilPromises.clear();
39
+ });
40
+ }
46
41
  });
47
42
  }
48
- });
43
+ return _disposableStack;
44
+ }
49
45
  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);
46
+ // Ensure that the disposable stack is created
47
+ if (isPromise(promiseLike)) {
48
+ ensureDisposableStack();
49
+ waitUntilPromises.add(promiseLike);
50
+ promiseLike.then(() => {
51
+ waitUntilPromises.delete(promiseLike);
52
+ }, err => {
53
+ console.error(`Unexpected error while waiting: ${err.message || err}`);
54
+ waitUntilPromises.delete(promiseLike);
55
+ });
53
56
  }
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
57
  }
62
58
  if (options?.plugins != null) {
63
59
  for (const plugin of options.plugins) {
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
- }
60
+ if (plugin.onRequest) {
61
+ onRequestHooks.push(plugin.onRequest);
62
+ }
63
+ if (plugin.onResponse) {
64
+ onResponseHooks.push(plugin.onResponse);
65
+ }
66
+ const disposeFn = plugin[DisposableSymbols.dispose];
67
+ if (disposeFn) {
68
+ ensureDisposableStack().defer(disposeFn);
69
+ }
70
+ const asyncDisposeFn = plugin[DisposableSymbols.asyncDispose];
71
+ if (asyncDisposeFn) {
72
+ ensureDisposableStack().defer(asyncDisposeFn);
73
+ }
74
+ if (plugin.onDispose) {
75
+ ensureDisposableStack().defer(plugin.onDispose);
75
76
  }
76
77
  }
77
78
  }
@@ -146,7 +147,11 @@ function createServerAdapter(serverAdapterBaseObject, options) {
146
147
  // TODO: Remove this on the next major version
147
148
  function handleNodeRequest(nodeRequest, ...ctx) {
148
149
  const serverContext = ctx.length > 1 ? completeAssign(...ctx) : ctx[0] || {};
149
- const request = normalizeNodeRequest(nodeRequest, fetchAPI, registerSignal);
150
+ // Ensure `waitUntil` is available in the server context
151
+ if (!serverContext.waitUntil) {
152
+ serverContext.waitUntil = waitUntil;
153
+ }
154
+ const request = normalizeNodeRequest(nodeRequest, fetchAPI);
150
155
  return handleRequest(request, serverContext);
151
156
  }
152
157
  function handleNodeRequestAndResponse(nodeRequest, nodeResponseOrContainer, ...ctx) {
@@ -192,8 +197,7 @@ function createServerAdapter(serverAdapterBaseObject, options) {
192
197
  const serverContext = filteredCtxParts.length > 0
193
198
  ? completeAssign(defaultServerContext, ...ctx)
194
199
  : defaultServerContext;
195
- const signal = new ServerAdapterRequestAbortSignal();
196
- registerSignal(signal);
200
+ const controller = new AbortController();
197
201
  const originalResEnd = res.end.bind(res);
198
202
  let resEnded = false;
199
203
  res.end = function (data) {
@@ -202,16 +206,16 @@ function createServerAdapter(serverAdapterBaseObject, options) {
202
206
  };
203
207
  const originalOnAborted = res.onAborted.bind(res);
204
208
  originalOnAborted(function () {
205
- signal.sendAbort();
209
+ controller.abort();
206
210
  });
207
211
  res.onAborted = function (cb) {
208
- signal.addEventListener('abort', cb);
212
+ controller.signal.addEventListener('abort', cb, { once: true });
209
213
  };
210
214
  const request = getRequestFromUWSRequest({
211
215
  req,
212
216
  res,
213
217
  fetchAPI,
214
- signal,
218
+ controller,
215
219
  });
216
220
  let response$;
217
221
  try {
@@ -224,8 +228,8 @@ function createServerAdapter(serverAdapterBaseObject, options) {
224
228
  return response$
225
229
  .catch((e) => handleErrorFromRequestHandler(e, fetchAPI.Response))
226
230
  .then(response => {
227
- if (!signal.aborted && !resEnded) {
228
- return sendResponseToUwsOpts(res, response, signal, fetchAPI);
231
+ if (!controller.signal.aborted && !resEnded) {
232
+ return sendResponseToUwsOpts(res, response, controller, fetchAPI);
229
233
  }
230
234
  })
231
235
  .catch(err => {
@@ -233,8 +237,8 @@ function createServerAdapter(serverAdapterBaseObject, options) {
233
237
  });
234
238
  }
235
239
  try {
236
- if (!signal.aborted && !resEnded) {
237
- return sendResponseToUwsOpts(res, response$, signal, fetchAPI);
240
+ if (!controller.signal.aborted && !resEnded) {
241
+ return sendResponseToUwsOpts(res, response$, controller, fetchAPI);
238
242
  }
239
243
  }
240
244
  catch (err) {
@@ -273,7 +277,7 @@ function createServerAdapter(serverAdapterBaseObject, options) {
273
277
  return handleRequestWithWaitUntil(request, ...maybeCtx);
274
278
  }
275
279
  const res$ = handleRequestWithWaitUntil(input, ...maybeCtx);
276
- return handleAbortSignalAndPromiseResponse(res$, input._signal);
280
+ return handleAbortSignalAndPromiseResponse(res$, input.signal);
277
281
  };
278
282
  const genericRequestHandler = (input, ...maybeCtx) => {
279
283
  // If it is a Node request
@@ -312,16 +316,18 @@ function createServerAdapter(serverAdapterBaseObject, options) {
312
316
  handleEvent,
313
317
  handleUWS,
314
318
  handle: genericRequestHandler,
315
- disposableStack,
319
+ get disposableStack() {
320
+ return ensureDisposableStack();
321
+ },
316
322
  [DisposableSymbols.asyncDispose]() {
317
- if (!disposableStack.disposed) {
318
- return disposableStack.disposeAsync();
323
+ if (_disposableStack && !_disposableStack.disposed) {
324
+ return _disposableStack.disposeAsync();
319
325
  }
320
326
  return fakePromise(undefined);
321
327
  },
322
328
  dispose() {
323
- if (!disposableStack.disposed) {
324
- return disposableStack.disposeAsync();
329
+ if (_disposableStack && !_disposableStack.disposed) {
330
+ return _disposableStack.disposeAsync();
325
331
  }
326
332
  return fakePromise(undefined);
327
333
  },
package/esm/index.js CHANGED
@@ -7,3 +7,4 @@ export * from './plugins/useErrorHandling.js';
7
7
  export * from './plugins/useContentEncoding.js';
8
8
  export * from './uwebsockets.js';
9
9
  export { Response } from '@whatwg-node/fetch';
10
+ export { DisposableSymbols } from '@whatwg-node/disposablestack';