@whatwg-node/server 0.10.0-alpha-20240726141316-c6ce93b3598457ebe73b3b725986723af8f5e609 → 0.10.0-alpha-20241123133536-975c9068dde45574fcfa26567e4bab96f45d1f85

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,50 +2,75 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.isUWSResponse = isUWSResponse;
4
4
  exports.getRequestFromUWSRequest = getRequestFromUWSRequest;
5
+ exports.createWritableFromUWS = createWritableFromUWS;
5
6
  exports.sendResponseToUwsOpts = sendResponseToUwsOpts;
7
+ exports.fakePromise = fakePromise;
8
+ const utils_js_1 = require("./utils.js");
6
9
  function isUWSResponse(res) {
7
10
  return !!res.onData;
8
11
  }
9
12
  function getRequestFromUWSRequest({ req, res, fetchAPI, signal }) {
10
- let body;
11
13
  const method = req.getMethod();
12
- if (method !== 'get' && method !== 'head') {
13
- let controller;
14
- body = new fetchAPI.ReadableStream({
15
- start(c) {
16
- controller = c;
17
- },
18
- });
19
- const readable = body.readable;
20
- if (readable) {
21
- signal.addEventListener('abort', () => {
22
- readable.push(null);
23
- });
24
- res.onData(function (ab, isLast) {
25
- const chunk = Buffer.from(ab, 0, ab.byteLength);
26
- readable.push(Buffer.from(chunk));
27
- if (isLast) {
28
- readable.push(null);
29
- }
30
- });
14
+ let duplex;
15
+ const chunks = [];
16
+ const pushFns = [
17
+ (chunk) => {
18
+ chunks.push(chunk);
19
+ },
20
+ ];
21
+ const push = (chunk) => {
22
+ for (const pushFn of pushFns) {
23
+ pushFn(chunk);
31
24
  }
32
- else {
33
- let closed = false;
34
- signal.addEventListener('abort', () => {
35
- if (!closed) {
36
- closed = true;
37
- controller.close();
38
- }
39
- });
40
- res.onData(function (ab, isLast) {
41
- const chunk = Buffer.from(ab, 0, ab.byteLength);
42
- controller.enqueue(Buffer.from(chunk));
43
- if (isLast) {
44
- closed = true;
45
- controller.close();
46
- }
47
- });
25
+ };
26
+ let stopped = false;
27
+ const stopFns = [
28
+ () => {
29
+ stopped = true;
30
+ },
31
+ ];
32
+ const stop = () => {
33
+ for (const stopFn of stopFns) {
34
+ stopFn();
48
35
  }
36
+ };
37
+ res.onData(function (ab, isLast) {
38
+ push(Buffer.from(Buffer.from(ab, 0, ab.byteLength)));
39
+ if (isLast) {
40
+ stop();
41
+ }
42
+ });
43
+ let getReadableStream;
44
+ if (method !== 'get' && method !== 'head') {
45
+ duplex = 'half';
46
+ signal.addEventListener('abort', () => {
47
+ stop();
48
+ });
49
+ let readableStream;
50
+ getReadableStream = () => {
51
+ if (!readableStream) {
52
+ readableStream = new fetchAPI.ReadableStream({
53
+ start(controller) {
54
+ for (const chunk of chunks) {
55
+ controller.enqueue(chunk);
56
+ }
57
+ if (stopped) {
58
+ controller.close();
59
+ return;
60
+ }
61
+ pushFns.push((chunk) => {
62
+ controller.enqueue(chunk);
63
+ });
64
+ stopFns.push(() => {
65
+ if (controller.desiredSize) {
66
+ controller.close();
67
+ }
68
+ });
69
+ },
70
+ });
71
+ }
72
+ return readableStream;
73
+ };
49
74
  }
50
75
  const headers = new fetchAPI.Headers();
51
76
  req.forEach((key, value) => {
@@ -56,30 +81,97 @@ function getRequestFromUWSRequest({ req, res, fetchAPI, signal }) {
56
81
  if (query) {
57
82
  url += `?${query}`;
58
83
  }
59
- return new fetchAPI.Request(url, {
84
+ let buffer;
85
+ function getBody() {
86
+ if (!getReadableStream) {
87
+ return null;
88
+ }
89
+ if (stopped) {
90
+ return getBufferFromChunks();
91
+ }
92
+ return getReadableStream();
93
+ }
94
+ const request = new fetchAPI.Request(url, {
60
95
  method,
61
96
  headers,
62
- body: body,
97
+ get body() {
98
+ return getBody();
99
+ },
63
100
  signal,
64
101
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
65
102
  // @ts-ignore - not in the TS types yet
66
- duplex: 'half',
103
+ duplex,
67
104
  });
68
- }
69
- async function forwardResponseBodyToUWSResponse(uwsResponse, fetchResponse, signal) {
70
- for await (const chunk of fetchResponse.body) {
71
- if (signal.aborted) {
72
- return;
105
+ function getBufferFromChunks() {
106
+ if (!buffer) {
107
+ buffer = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks);
73
108
  }
74
- uwsResponse.cork(() => {
75
- uwsResponse.write(chunk);
109
+ return buffer;
110
+ }
111
+ function collectBuffer() {
112
+ if (stopped) {
113
+ return fakePromise(getBufferFromChunks());
114
+ }
115
+ return new Promise((resolve, reject) => {
116
+ try {
117
+ stopFns.push(() => {
118
+ resolve(getBufferFromChunks());
119
+ });
120
+ }
121
+ catch (e) {
122
+ reject(e);
123
+ }
76
124
  });
77
125
  }
78
- uwsResponse.cork(() => {
79
- uwsResponse.end();
126
+ Object.defineProperties(request, {
127
+ body: {
128
+ get() {
129
+ return getBody();
130
+ },
131
+ configurable: true,
132
+ enumerable: true,
133
+ },
134
+ json: {
135
+ value() {
136
+ return collectBuffer()
137
+ .then(b => b.toString('utf8'))
138
+ .then(t => JSON.parse(t));
139
+ },
140
+ configurable: true,
141
+ enumerable: true,
142
+ },
143
+ text: {
144
+ value() {
145
+ return collectBuffer().then(b => b.toString('utf8'));
146
+ },
147
+ configurable: true,
148
+ enumerable: true,
149
+ },
150
+ arrayBuffer: {
151
+ value() {
152
+ return collectBuffer();
153
+ },
154
+ configurable: true,
155
+ enumerable: true,
156
+ },
80
157
  });
158
+ return request;
81
159
  }
82
- function sendResponseToUwsOpts(uwsResponse, fetchResponse, signal) {
160
+ function createWritableFromUWS(uwsResponse, fetchAPI) {
161
+ return new fetchAPI.WritableStream({
162
+ write(chunk) {
163
+ uwsResponse.cork(() => {
164
+ uwsResponse.write(chunk);
165
+ });
166
+ },
167
+ close() {
168
+ uwsResponse.cork(() => {
169
+ uwsResponse.end();
170
+ });
171
+ },
172
+ });
173
+ }
174
+ function sendResponseToUwsOpts(uwsResponse, fetchResponse, signal, fetchAPI) {
83
175
  if (!fetchResponse) {
84
176
  uwsResponse.writeStatus('404 Not Found');
85
177
  uwsResponse.end();
@@ -109,13 +201,59 @@ function sendResponseToUwsOpts(uwsResponse, fetchResponse, signal) {
109
201
  if (bufferOfRes) {
110
202
  uwsResponse.end(bufferOfRes);
111
203
  }
204
+ else if (!fetchResponse.body) {
205
+ uwsResponse.end();
206
+ }
112
207
  });
113
- if (bufferOfRes) {
208
+ if (bufferOfRes || !fetchResponse.body) {
114
209
  return;
115
210
  }
116
- if (!fetchResponse.body) {
117
- uwsResponse.end();
118
- return;
211
+ signal.addEventListener('abort', () => {
212
+ if (!fetchResponse.body?.locked) {
213
+ fetchResponse.body?.cancel(signal.reason);
214
+ }
215
+ });
216
+ return fetchResponse.body
217
+ .pipeTo(createWritableFromUWS(uwsResponse, fetchAPI), {
218
+ signal,
219
+ })
220
+ .catch(err => {
221
+ if (signal.aborted) {
222
+ return;
223
+ }
224
+ throw err;
225
+ });
226
+ }
227
+ function fakePromise(value) {
228
+ if ((0, utils_js_1.isPromise)(value)) {
229
+ return value;
119
230
  }
120
- return forwardResponseBodyToUWSResponse(uwsResponse, fetchResponse, signal);
231
+ // Write a fake promise to avoid the promise constructor
232
+ // being called with `new Promise` in the browser.
233
+ return {
234
+ then(resolve) {
235
+ if (resolve) {
236
+ const callbackResult = resolve(value);
237
+ if ((0, utils_js_1.isPromise)(callbackResult)) {
238
+ return callbackResult;
239
+ }
240
+ return fakePromise(callbackResult);
241
+ }
242
+ return this;
243
+ },
244
+ catch() {
245
+ return this;
246
+ },
247
+ finally(cb) {
248
+ if (cb) {
249
+ const callbackResult = cb();
250
+ if ((0, utils_js_1.isPromise)(callbackResult)) {
251
+ return callbackResult.then(() => value);
252
+ }
253
+ return fakePromise(value);
254
+ }
255
+ return this;
256
+ },
257
+ [Symbol.toStringTag]: 'Promise',
258
+ };
121
259
  }
@@ -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) {
@@ -103,20 +131,21 @@ function createServerAdapter(serverAdapterBaseObject, options) {
103
131
  }
104
132
  : givenHandleRequest;
105
133
  // TODO: Remove this on the next major version
106
- function handleNodeRequestAndResponse(nodeRequest, nodeResponseOrContainer, ...ctx) {
107
- const nodeResponse = nodeResponseOrContainer.raw || nodeResponseOrContainer;
134
+ function handleNodeRequest(nodeRequest, ...ctx) {
108
135
  const serverContext = ctx.length > 1 ? completeAssign(...ctx) : ctx[0] || {};
109
- const request = normalizeNodeRequest(nodeRequest, nodeResponse, fetchAPI.Request);
136
+ const request = normalizeNodeRequest(nodeRequest, fetchAPI, registerSignal);
110
137
  return handleRequest(request, serverContext);
111
138
  }
139
+ function handleNodeRequestAndResponse(nodeRequest, nodeResponseOrContainer, ...ctx) {
140
+ const nodeResponse = nodeResponseOrContainer.raw || nodeResponseOrContainer;
141
+ nodeRequestResponseMap.set(nodeRequest, nodeResponse);
142
+ return handleNodeRequest(nodeRequest, ...ctx);
143
+ }
112
144
  function requestListener(nodeRequest, nodeResponse, ...ctx) {
113
- const waitUntilPromises = [];
114
145
  const defaultServerContext = {
115
146
  req: nodeRequest,
116
147
  res: nodeResponse,
117
- waitUntil(cb) {
118
- waitUntilPromises.push(cb.catch(err => console.error(err)));
119
- },
148
+ waitUntil,
120
149
  };
121
150
  let response$;
122
151
  try {
@@ -141,19 +170,17 @@ function createServerAdapter(serverAdapterBaseObject, options) {
141
170
  }
142
171
  }
143
172
  function handleUWS(res, req, ...ctx) {
144
- const waitUntilPromises = [];
145
173
  const defaultServerContext = {
146
174
  res,
147
175
  req,
148
- waitUntil(cb) {
149
- waitUntilPromises.push(cb.catch(err => console.error(err)));
150
- },
176
+ waitUntil,
151
177
  };
152
178
  const filteredCtxParts = ctx.filter(partCtx => partCtx != null);
153
179
  const serverContext = filteredCtxParts.length > 0
154
180
  ? completeAssign(defaultServerContext, ...ctx)
155
181
  : defaultServerContext;
156
182
  const signal = new ServerAdapterRequestAbortSignal();
183
+ registerSignal(signal);
157
184
  const originalResEnd = res.end.bind(res);
158
185
  let resEnded = false;
159
186
  res.end = function (data) {
@@ -185,7 +212,7 @@ function createServerAdapter(serverAdapterBaseObject, options) {
185
212
  .catch((e) => handleErrorFromRequestHandler(e, fetchAPI.Response))
186
213
  .then(response => {
187
214
  if (!signal.aborted && !resEnded) {
188
- return sendResponseToUwsOpts(res, response, signal);
215
+ return sendResponseToUwsOpts(res, response, signal, fetchAPI);
189
216
  }
190
217
  })
191
218
  .catch(err => {
@@ -194,7 +221,7 @@ function createServerAdapter(serverAdapterBaseObject, options) {
194
221
  }
195
222
  try {
196
223
  if (!signal.aborted && !resEnded) {
197
- return sendResponseToUwsOpts(res, response$, signal);
224
+ return sendResponseToUwsOpts(res, response$, signal, fetchAPI);
198
225
  }
199
226
  }
200
227
  catch (err) {
@@ -214,17 +241,12 @@ function createServerAdapter(serverAdapterBaseObject, options) {
214
241
  }
215
242
  function handleRequestWithWaitUntil(request, ...ctx) {
216
243
  const filteredCtxParts = ctx.filter(partCtx => partCtx != null);
217
- let waitUntilPromises;
218
244
  const serverContext = filteredCtxParts.length > 1
219
245
  ? completeAssign({}, ...filteredCtxParts)
220
246
  : isolateObject(filteredCtxParts[0], filteredCtxParts[0] == null || filteredCtxParts[0].waitUntil == null
221
- ? (waitUntilPromises = [])
247
+ ? waitUntil
222
248
  : undefined);
223
- const response$ = handleRequest(request, serverContext);
224
- if (waitUntilPromises?.length) {
225
- return handleWaitUntils(waitUntilPromises).then(() => response$);
226
- }
227
- return response$;
249
+ return handleRequest(request, serverContext);
228
250
  }
229
251
  const fetchFn = (input, ...maybeCtx) => {
230
252
  if (typeof input === 'string' || 'href' in input) {
@@ -271,11 +293,19 @@ function createServerAdapter(serverAdapterBaseObject, options) {
271
293
  const adapterObj = {
272
294
  handleRequest: handleRequestWithWaitUntil,
273
295
  fetch: fetchFn,
296
+ handleNodeRequest,
274
297
  handleNodeRequestAndResponse,
275
298
  requestListener,
276
299
  handleEvent,
277
300
  handleUWS,
278
301
  handle: genericRequestHandler,
302
+ disposableStack,
303
+ [DisposableSymbols.asyncDispose]() {
304
+ return disposableStack.disposeAsync();
305
+ },
306
+ dispose() {
307
+ return disposableStack.disposeAsync();
308
+ },
279
309
  };
280
310
  const serverAdapter = new Proxy(genericRequestHandler, {
281
311
  // It should have all the attributes of the handler function and the server instance
@@ -1,4 +1,4 @@
1
- import { decompressedResponseMap, getSupportedEncodings } from '../utils.js';
1
+ import { decompressedResponseMap, getSupportedEncodings, isAsyncIterable, isReadable, } from '../utils.js';
2
2
  export function useContentEncoding() {
3
3
  const encodingMap = new WeakMap();
4
4
  return {
@@ -18,7 +18,7 @@ export function useContentEncoding() {
18
18
  for (const contentEncoding of contentEncodings) {
19
19
  newBody = newBody.pipeThrough(new fetchAPI.DecompressionStream(contentEncoding));
20
20
  }
21
- const newRequest = new fetchAPI.Request(request.url, {
21
+ request = new fetchAPI.Request(request.url, {
22
22
  body: newBody,
23
23
  cache: request.cache,
24
24
  credentials: request.credentials,
@@ -35,7 +35,7 @@ export function useContentEncoding() {
35
35
  // @ts-ignore - not in the TS types yet
36
36
  duplex: 'half',
37
37
  });
38
- setRequest(newRequest);
38
+ setRequest(request);
39
39
  }
40
40
  }
41
41
  const acceptEncoding = request.headers.get('accept-encoding');
@@ -43,7 +43,8 @@ export function useContentEncoding() {
43
43
  encodingMap.set(request, acceptEncoding.split(','));
44
44
  }
45
45
  },
46
- onResponse({ request, response, setResponse, fetchAPI }) {
46
+ onResponse({ request, response, setResponse, fetchAPI, serverContext }) {
47
+ const waitUntil = serverContext.waitUntil?.bind(serverContext) || (() => { });
47
48
  // Hack for avoiding to create whatwg-node to create a readable stream until it's needed
48
49
  if (response['bodyInit'] || response.body) {
49
50
  const encodings = encodingMap.get(request);
@@ -57,21 +58,15 @@ export function useContentEncoding() {
57
58
  const bufOfRes = response._buffer;
58
59
  if (bufOfRes) {
59
60
  const writer = compressionStream.writable.getWriter();
60
- writer.write(bufOfRes);
61
- writer.close();
62
- const reader = compressionStream.readable.getReader();
63
- return Promise.resolve().then(async () => {
64
- const chunks = [];
65
- while (true) {
66
- const { done, value } = await reader.read();
67
- if (done) {
68
- reader.releaseLock();
69
- break;
70
- }
71
- else if (value) {
72
- chunks.push(...value);
73
- }
74
- }
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]);
75
70
  const uint8Array = new Uint8Array(chunks);
76
71
  const newHeaders = new fetchAPI.Headers(response.headers);
77
72
  newHeaders.set('content-encoding', supportedEncoding);
@@ -82,6 +77,7 @@ export function useContentEncoding() {
82
77
  });
83
78
  decompressedResponseMap.set(compressedResponse, response);
84
79
  setResponse(compressedResponse);
80
+ waitUntil(compressionStream.writable.close());
85
81
  });
86
82
  }
87
83
  }
@@ -102,3 +98,33 @@ export function useContentEncoding() {
102
98
  },
103
99
  };
104
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
  }