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