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