fulmine.js 5.5.0 → 5.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/package.json +1 -1
- package/src/application.js +6 -21
- package/src/request.js +63 -0
- package/src/response.js +297 -21
- package/src/router.js +26 -3
- package/src/utils.js +5 -0
package/README.md
CHANGED
|
@@ -26,12 +26,6 @@ npx fulmine profile # what listen() decided about each route
|
|
|
26
26
|
|
|
27
27
|
See [Migrating](#migrating) for what it handles and what it deliberately does not.
|
|
28
28
|
|
|
29
|
-
There is a **[live demo](https://fulmine-demo.fly.dev)**, which is an ordinary Express application:
|
|
30
|
-
real routes, `helmet`, `cors`, `compression`, `express-session` and `morgan` unmodified, and a
|
|
31
|
-
WebSocket chat served by `app.ws()`. It links to [its own source](https://fulmine-demo.fly.dev/source),
|
|
32
|
-
which is [in this repository](./demo). It shows no throughput figure on purpose: it runs on a small
|
|
33
|
-
shared machine, so the number would describe the machine rather than the framework.
|
|
34
|
-
|
|
35
29
|
[](https://www.npmjs.com/package/fulmine.js)
|
|
36
30
|
[](https://nodejs.org)
|
|
37
31
|
[](https://coveralls.io/github/nigrosimone/fulmine.js?branch=main)
|
|
@@ -147,11 +141,16 @@ as external in `angular.json`:
|
|
|
147
141
|
```
|
|
148
142
|
|
|
149
143
|
What it is worth, measured on an Angular 22 application with each server reporting its own CPU per
|
|
150
|
-
request, nine alternating rounds: **static assets 3.29x**, and **a page served from
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
144
|
+
request, nine alternating rounds: **static assets 3.29x**, and **a page served from a cache 1.50x**.
|
|
145
|
+
The render itself is the same JavaScript on both sides and measures the same, so on a cache miss the
|
|
146
|
+
framework is not what your page is waiting for. Which is the useful way round: an SSR application
|
|
147
|
+
spends most of its traffic outside the render, and that is where the difference is.
|
|
148
|
+
|
|
149
|
+
Caching those pages is [`ng-ssr-caching`](https://www.npmjs.com/package/ng-ssr-caching), a middleware
|
|
150
|
+
that runs on Express and here alike, and the same measurement says a page costs 17.2ms to render and
|
|
151
|
+
1.9ms to serve from it. It is worth knowing why it keeps the ETag beside the bytes: a cache that
|
|
152
|
+
stores only the body makes the server hash the whole document again on every hit, and measures level
|
|
153
|
+
with no cache at all on the serving side.
|
|
155
154
|
|
|
156
155
|
### When Express is somebody else's dependency
|
|
157
156
|
|
|
@@ -225,6 +224,7 @@ A single-stage `node:26-trixie-slim` image works too if you `apt-get install -y
|
|
|
225
224
|
- `app.listen()` returns the app, not an `http.Server`. There is no node server underneath, so `server.close()`, `server.address()` and anything that attaches itself to a real `http.Server` need a look. `app.close()`, `app.address()` and `app.listening` are there and do what you would expect.
|
|
226
225
|
- `x-powered-by` is disabled by default. Express sends `X-Powered-By: Express` unless you turn it off; Fulmine does not send it unless you turn it on with `app.set("x-powered-by", true)`. The header only tells anyone asking which framework is running.
|
|
227
226
|
- request body is only read for POST, PUT, PATCH and QUERY requests by default. You can add additional methods by setting `body methods` to array with uppercased methods.
|
|
227
|
+
- **Informational responses go nowhere.** `res.writeEarlyHints()`, `res.writeContinue()` and `res.writeProcessing()` are all there, take what node's take and throw what node's throw once the head has gone out, but nothing reaches the wire: µWebSockets.js has no API for a `1xx`. They exist so that code written for Express keeps running rather than dying on "is not a function", which is the only thing a drop-in can honestly promise here. `res.addTrailers()` is the same story, and `res.setTimeout()` and `req.setTimeout()` register the listener without changing anything, since µWS runs its own idle timeout through `uwsOptions.idleTimeout`.
|
|
228
228
|
- For HTTPS, instead of doing this:
|
|
229
229
|
|
|
230
230
|
```js
|
package/package.json
CHANGED
package/src/application.js
CHANGED
|
@@ -500,31 +500,16 @@ class Application extends Router {
|
|
|
500
500
|
* @param {any} res the uWS response
|
|
501
501
|
* @param {any} req the uWS request
|
|
502
502
|
*/
|
|
503
|
-
|
|
503
|
+
_serveGeneric(res, req) {
|
|
504
504
|
const request = this.handleRequest(res, req);
|
|
505
505
|
const response = request.res;
|
|
506
|
-
// armed up front here: this handler
|
|
507
|
-
// on every path through it
|
|
506
|
+
// armed up front here: this handler can outlive the callback on every path through it
|
|
508
507
|
this._armAbort(res, response);
|
|
509
508
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
response._corkNeeded = true;
|
|
515
|
-
const matchedRoute = await routed;
|
|
516
|
-
if (!matchedRoute && !response.headersSent && !response.aborted) {
|
|
517
|
-
this._endUnmatched(request, response);
|
|
518
|
-
}
|
|
519
|
-
} catch (err) {
|
|
520
|
-
// an internal throw answers 500 as express's final handler would, instead of
|
|
521
|
-
// dying as an unhandled rejection
|
|
522
|
-
if (response.aborted || response.finished) {
|
|
523
|
-
console.error(err);
|
|
524
|
-
} else {
|
|
525
|
-
this._handleError(err, null, request, response);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
509
|
+
this._routeRequestDirect(request, response);
|
|
510
|
+
// the synchronous stretch has run under the cork uWS holds for this callback, and
|
|
511
|
+
// whatever comes after it is outside
|
|
512
|
+
response._corkNeeded = true;
|
|
528
513
|
}
|
|
529
514
|
|
|
530
515
|
/**
|
package/src/request.js
CHANGED
|
@@ -700,6 +700,69 @@ module.exports = class Request extends LazyReadable {
|
|
|
700
700
|
return this.res?.finished || this.res?.aborted;
|
|
701
701
|
}
|
|
702
702
|
|
|
703
|
+
/** @type {AbortController|undefined} */
|
|
704
|
+
#abortController;
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* node's `req.signal`, an AbortSignal that fires when the request is over, so work started for
|
|
708
|
+
* a visitor who has gone away can be stopped. `@angular/ssr` reads it when it builds a web
|
|
709
|
+
* Request out of this one, which is how an SSR render learns to give up.
|
|
710
|
+
*
|
|
711
|
+
* Made on the first ask rather than for every request: most requests never look at it, and an
|
|
712
|
+
* AbortController each would be an allocation nobody reads.
|
|
713
|
+
*
|
|
714
|
+
* @returns {AbortSignal}
|
|
715
|
+
*/
|
|
716
|
+
get signal() {
|
|
717
|
+
if (!this.#abortController) {
|
|
718
|
+
const controller = new AbortController();
|
|
719
|
+
this.#abortController = controller;
|
|
720
|
+
const stop = () => controller.abort();
|
|
721
|
+
if (this.res?.aborted || this.res?.finished) {
|
|
722
|
+
stop();
|
|
723
|
+
} else {
|
|
724
|
+
// the request, not the response: a client abort is reported by destroying this
|
|
725
|
+
// stream and emitting "aborted" on it, and the response is never told to close
|
|
726
|
+
this.once("aborted", stop);
|
|
727
|
+
this.once("close", stop);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return this.#abortController.signal;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* The trailing headers of a chunked request. µWebSockets.js does not surface them, so these
|
|
736
|
+
* stay empty, which is also what node hands back until the request has ended.
|
|
737
|
+
*
|
|
738
|
+
* @returns {Record<string, string>}
|
|
739
|
+
*/
|
|
740
|
+
get trailers() {
|
|
741
|
+
return {};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* @returns {Record<string, string[]>}
|
|
746
|
+
*/
|
|
747
|
+
get trailersDistinct() {
|
|
748
|
+
return {};
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* node's per-request socket timeout. µWS runs its own idle timeout, set through
|
|
753
|
+
* `uwsOptions.idleTimeout`, and this cannot change it. The listener is registered as node's is.
|
|
754
|
+
*
|
|
755
|
+
* @param {number} msecs
|
|
756
|
+
* @param {() => void} [callback]
|
|
757
|
+
* @returns {this}
|
|
758
|
+
*/
|
|
759
|
+
setTimeout(msecs, callback) {
|
|
760
|
+
if (typeof callback === "function") {
|
|
761
|
+
this.once("timeout", callback);
|
|
762
|
+
}
|
|
763
|
+
return this;
|
|
764
|
+
}
|
|
765
|
+
|
|
703
766
|
/**
|
|
704
767
|
* Readable's pull. uWS pushes the body rather than being pulled from, so all this does is
|
|
705
768
|
* lift the backpressure that a full queue put on it.
|
package/src/response.js
CHANGED
|
@@ -51,6 +51,18 @@ const { EventEmitter } = require("events");
|
|
|
51
51
|
const http = require("http");
|
|
52
52
|
const ms = require("ms");
|
|
53
53
|
|
|
54
|
+
// How much a chunked response may gather before it is handed to uWS. Measured on uWS alone with
|
|
55
|
+
// 33 KB written in 500 pieces: 16.2ms handed over one piece at a time, 0.79ms in 4 KB blocks,
|
|
56
|
+
// 0.43ms in 8 KB, 0.45ms in 16 KB. Past 8 KB the curve is flat, so this is the smallest size that
|
|
57
|
+
// buys the whole saving, and it bounds what one response can hold back.
|
|
58
|
+
const COALESCE_LIMIT = 16 * 1024;
|
|
59
|
+
|
|
60
|
+
// Below which a chunk is worth gathering. Merging costs a copy, and a chunk that is already
|
|
61
|
+
// substantial does not earn it back: measured at 33 KB in eight pieces, gathering them made the
|
|
62
|
+
// response 28% dearer, while the same bytes in sixty-six pieces got 3.4x cheaper. Anything from
|
|
63
|
+
// here up goes straight through.
|
|
64
|
+
const COALESCE_BELOW = 4 * 1024;
|
|
65
|
+
|
|
54
66
|
const outgoingMessage = new http.OutgoingMessage();
|
|
55
67
|
const symbols = Object.getOwnPropertySymbols(outgoingMessage);
|
|
56
68
|
// if a future node renames it, fall back to a private symbol rather than writing a property
|
|
@@ -208,6 +220,25 @@ module.exports = class Response extends LazyWritable {
|
|
|
208
220
|
/** @type {((err?: Error|null) => void)|null} */
|
|
209
221
|
#pendingCallback = null;
|
|
210
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Chunks written but not yet handed to uWS, and their total size.
|
|
225
|
+
*
|
|
226
|
+
* uWS charges for a write against everything already buffered behind it, so a chunked response
|
|
227
|
+
* written in many small pieces costs quadratically once it passes the socket's own buffer:
|
|
228
|
+
* measured on uWS alone, 500 writes of 66 bytes take 13ms against 0.06ms for 100 of them.
|
|
229
|
+
* Handing it the same bytes in blocks costs 0.4ms. Nothing here changes what goes on the wire,
|
|
230
|
+
* only how many calls it takes to put it there.
|
|
231
|
+
* Null until the first chunked write, since a res.send never queues anything.
|
|
232
|
+
* @type {Buffer[]|null}
|
|
233
|
+
*/
|
|
234
|
+
#queued = null;
|
|
235
|
+
|
|
236
|
+
/** How many bytes {@link #queued} holds, kept alongside so the flush does not add them up. */
|
|
237
|
+
#queuedBytes = 0;
|
|
238
|
+
|
|
239
|
+
/** Whether a flush is already booked for the end of this turn. */
|
|
240
|
+
#flushBooked = false;
|
|
241
|
+
|
|
211
242
|
/** @type {any} */
|
|
212
243
|
#outHeaders = null;
|
|
213
244
|
|
|
@@ -400,10 +431,58 @@ module.exports = class Response extends LazyWritable {
|
|
|
400
431
|
return this.#socket;
|
|
401
432
|
}
|
|
402
433
|
|
|
434
|
+
/**
|
|
435
|
+
* Hands everything queued to uWS as one write, which is where the saving is, and keeps the
|
|
436
|
+
* backpressure the single write used to do.
|
|
437
|
+
*
|
|
438
|
+
* @param {((err?: Error|null) => void)|null} callback the stream's, when there is one waiting
|
|
439
|
+
*/
|
|
440
|
+
#flushQueued(callback) {
|
|
441
|
+
if (this.#queued === null || this.#queuedBytes === 0) {
|
|
442
|
+
if (callback) callback(null);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const body = this.#queued.length === 1 ? this.#queued[0] : Buffer.concat(this.#queued, this.#queuedBytes);
|
|
446
|
+
this.#queued = null;
|
|
447
|
+
this.#queuedBytes = 0;
|
|
448
|
+
|
|
449
|
+
const ok = this._res.write(body);
|
|
450
|
+
if (ok) {
|
|
451
|
+
this.writingChunk = false;
|
|
452
|
+
if (callback) callback(null);
|
|
453
|
+
else this.emit("drain");
|
|
454
|
+
} else if (callback) {
|
|
455
|
+
this.#pendingCallback = callback;
|
|
456
|
+
this._res.onWritable(() => {
|
|
457
|
+
if (this.aborted || this.finished) return true;
|
|
458
|
+
const cb = this.#pendingCallback;
|
|
459
|
+
this.#pendingCallback = null;
|
|
460
|
+
this.writingChunk = false;
|
|
461
|
+
if (cb) cb(null);
|
|
462
|
+
return true;
|
|
463
|
+
});
|
|
464
|
+
} else {
|
|
465
|
+
// nothing is waiting on this one: uWS drains it and the next write finds out
|
|
466
|
+
this.writingChunk = false;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* The booked flush. Static, so a response that never writes in pieces allocates nothing for it:
|
|
472
|
+
* nextTick forwards the receiver as an argument.
|
|
473
|
+
*
|
|
474
|
+
* @param {any} res
|
|
475
|
+
*/
|
|
476
|
+
static #flushOnTick(res) {
|
|
477
|
+
res.#flushBooked = false;
|
|
478
|
+
if (res.aborted || res.finished || res.#queuedBytes === 0) return;
|
|
479
|
+
res._res.cork(() => res.#flushQueued(null));
|
|
480
|
+
}
|
|
481
|
+
|
|
403
482
|
/**
|
|
404
483
|
* Writable's sink. Sends the headers if they have not gone yet, then hands the chunk to uWS,
|
|
405
|
-
* either
|
|
406
|
-
* much there would be. Backpressure comes back as onWritable, which is what defers the
|
|
484
|
+
* either through the queue above for a chunked response or through tryEnd when a Content-Length
|
|
485
|
+
* said how much there would be. Backpressure comes back as onWritable, which is what defers the
|
|
407
486
|
* callback rather than dropping the chunk.
|
|
408
487
|
*
|
|
409
488
|
* @param {any} chunk
|
|
@@ -441,20 +520,23 @@ module.exports = class Response extends LazyWritable {
|
|
|
441
520
|
}
|
|
442
521
|
|
|
443
522
|
if (this.chunkedTransfer) {
|
|
444
|
-
|
|
445
|
-
|
|
523
|
+
// Held back rather than written, and handed over in one piece at the end of this
|
|
524
|
+
// turn or once it is big enough to be worth a call. A stream that writes once per
|
|
525
|
+
// turn, an SSE feed for instance, still leaves on its own turn: the queue only ever
|
|
526
|
+
// gathers what was written without yielding in between.
|
|
527
|
+
(this.#queued ??= []).push(/** @type {Buffer} */ (chunk));
|
|
528
|
+
this.#queuedBytes += /** @type {Buffer} */ (chunk).byteLength;
|
|
529
|
+
// a chunk that is already big enough to be worth its own call leaves with whatever
|
|
530
|
+
// was waiting in front of it, rather than paying for a copy it does not need
|
|
531
|
+
if (this.#queuedBytes >= COALESCE_LIMIT || /** @type {Buffer} */ (chunk).byteLength >= COALESCE_BELOW) {
|
|
532
|
+
this.#flushQueued(callback);
|
|
533
|
+
} else {
|
|
534
|
+
if (!this.#flushBooked) {
|
|
535
|
+
this.#flushBooked = true;
|
|
536
|
+
process.nextTick(Response.#flushOnTick, this);
|
|
537
|
+
}
|
|
446
538
|
this.writingChunk = false;
|
|
447
539
|
callback(null);
|
|
448
|
-
} else {
|
|
449
|
-
this.#pendingCallback = callback;
|
|
450
|
-
this._res.onWritable(() => {
|
|
451
|
-
if (this.aborted || this.finished) return true;
|
|
452
|
-
const cb = this.#pendingCallback;
|
|
453
|
-
this.#pendingCallback = null;
|
|
454
|
-
this.writingChunk = false;
|
|
455
|
-
if (cb) cb(null);
|
|
456
|
-
return true;
|
|
457
|
-
});
|
|
458
540
|
}
|
|
459
541
|
} else {
|
|
460
542
|
const lastOffset = this._res.getWriteOffset();
|
|
@@ -684,6 +766,8 @@ module.exports = class Response extends LazyWritable {
|
|
|
684
766
|
} else if (!data && contentLength) {
|
|
685
767
|
this._res.endWithoutBody(contentLength.toString());
|
|
686
768
|
} else if (headWasAlreadyOut && this.chunkedTransfer) {
|
|
769
|
+
// whatever is still queued goes first: end() must not overtake the body written before it
|
|
770
|
+
this.#flushQueued(null);
|
|
687
771
|
// The head has already gone out without a length, which is what flushHeaders() and the
|
|
688
772
|
// first res.write() both do, so this response is committed to chunked framing and a
|
|
689
773
|
// length can no longer describe it. node is committed the same way: after a flush,
|
|
@@ -1249,11 +1333,16 @@ module.exports = class Response extends LazyWritable {
|
|
|
1249
1333
|
}
|
|
1250
1334
|
|
|
1251
1335
|
/**
|
|
1252
|
-
*
|
|
1336
|
+
* Hands the status line and the headers over now, without waiting for a body, which is node's
|
|
1253
1337
|
* flushHeaders(). Callers use it to let the client start on the head while the body is still
|
|
1254
1338
|
* being produced, and one of them is `@angular/ssr`'s writeResponseToNodeResponse, which calls
|
|
1255
1339
|
* it before streaming a rendered page.
|
|
1256
1340
|
*
|
|
1341
|
+
* **The head does not reach the wire here.** uWS holds it until the first body chunk, so a
|
|
1342
|
+
* client sees nothing until then, where express answers at once. `beginWrite` is uWS's API for
|
|
1343
|
+
* this and is unusable: it emits a stray CRLF before the first chunk size, which node's parser
|
|
1344
|
+
* rejects as HPE_INVALID_CHUNK_SIZE. Checked against v20.69.0, the latest release.
|
|
1345
|
+
*
|
|
1257
1346
|
* A second call does nothing, as node's does. Nothing is written for a response already
|
|
1258
1347
|
* finished or aborted: uWS has let go of it by then.
|
|
1259
1348
|
*
|
|
@@ -1276,6 +1365,195 @@ module.exports = class Response extends LazyWritable {
|
|
|
1276
1365
|
});
|
|
1277
1366
|
}
|
|
1278
1367
|
|
|
1368
|
+
/**
|
|
1369
|
+
* node's `writeEarlyHints`, which sends a `103` carrying the resources the page will want, so a
|
|
1370
|
+
* browser can start fetching them while the server is still rendering.
|
|
1371
|
+
*
|
|
1372
|
+
* **Nothing is sent here.** µWebSockets.js has no API for an informational response, so the
|
|
1373
|
+
* hints cannot reach the wire, and this exists so that code written for Express keeps running
|
|
1374
|
+
* rather than dying on "res.writeEarlyHints is not a function". The callback is still called,
|
|
1375
|
+
* because node calls it once the hints are out and a caller may be waiting on it.
|
|
1376
|
+
*
|
|
1377
|
+
* `writeContinue` and `writeProcessing` below are the same story with `100` and `102`.
|
|
1378
|
+
*
|
|
1379
|
+
* @param {Record<string, string|string[]>} [hints]
|
|
1380
|
+
* @param {() => void} [callback]
|
|
1381
|
+
* @returns {void}
|
|
1382
|
+
*/
|
|
1383
|
+
|
|
1384
|
+
/**
|
|
1385
|
+
*
|
|
1386
|
+
*/
|
|
1387
|
+
writeEarlyHints(hints, callback) {
|
|
1388
|
+
this.#refuseInformationAfterHead();
|
|
1389
|
+
// node writes the hints first and calls back after, so a caller that sequences work on it
|
|
1390
|
+
// gets the same order here
|
|
1391
|
+
if (typeof callback === "function") {
|
|
1392
|
+
process.nextTick(callback);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/**
|
|
1397
|
+
* node's `writeContinue`, the `100` that answers an `Expect: 100-continue`. Nothing is sent:
|
|
1398
|
+
* see {@link Response#writeEarlyHints}.
|
|
1399
|
+
*
|
|
1400
|
+
* @returns {void}
|
|
1401
|
+
*/
|
|
1402
|
+
writeContinue() {
|
|
1403
|
+
this.#refuseInformationAfterHead();
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/**
|
|
1407
|
+
* node's `writeProcessing`, the `102`. Nothing is sent: see {@link Response#writeEarlyHints}.
|
|
1408
|
+
*
|
|
1409
|
+
* @returns {void}
|
|
1410
|
+
*/
|
|
1411
|
+
writeProcessing() {
|
|
1412
|
+
this.#refuseInformationAfterHead();
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/**
|
|
1416
|
+
* node throws from all three of the above once the head has gone out, since an informational
|
|
1417
|
+
* response can only come before it, and an application may well be relying on that throw.
|
|
1418
|
+
*
|
|
1419
|
+
* @returns {void}
|
|
1420
|
+
*/
|
|
1421
|
+
#refuseInformationAfterHead() {
|
|
1422
|
+
if (this.headersSent) {
|
|
1423
|
+
/** @type {NodeJS.ErrnoException} */
|
|
1424
|
+
const err = new Error("Cannot write headers after they are sent to the client");
|
|
1425
|
+
err.code = "ERR_HTTP_HEADERS_SENT";
|
|
1426
|
+
throw err;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
/**
|
|
1431
|
+
* node's `addTrailers`, the headers that follow a chunked body. µWebSockets.js cannot send
|
|
1432
|
+
* them, so nothing is written and the response is otherwise unaffected.
|
|
1433
|
+
*
|
|
1434
|
+
* @param {Record<string, string>|[string, string][]} [headers]
|
|
1435
|
+
* @returns {void}
|
|
1436
|
+
*/
|
|
1437
|
+
addTrailers(headers) {}
|
|
1438
|
+
|
|
1439
|
+
/**
|
|
1440
|
+
* node's per-response socket timeout. µWS runs its own idle timeout, set through
|
|
1441
|
+
* `uwsOptions.idleTimeout`, and this cannot change it. The callback is registered on "timeout"
|
|
1442
|
+
* as node's does, so nothing is lost by calling it, and nothing happens either.
|
|
1443
|
+
*
|
|
1444
|
+
* @param {number} msecs
|
|
1445
|
+
* @param {() => void} [callback]
|
|
1446
|
+
* @returns {this}
|
|
1447
|
+
*/
|
|
1448
|
+
setTimeout(msecs, callback) {
|
|
1449
|
+
if (typeof callback === "function") {
|
|
1450
|
+
this.once("timeout", callback);
|
|
1451
|
+
}
|
|
1452
|
+
return this;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
/**
|
|
1456
|
+
* node's `assignSocket` and `detachSocket`, which the http server uses when a response is
|
|
1457
|
+
* handed a raw socket. There is no such socket here.
|
|
1458
|
+
*
|
|
1459
|
+
* @param {any} [socket]
|
|
1460
|
+
* @returns {void}
|
|
1461
|
+
*/
|
|
1462
|
+
assignSocket(socket) {}
|
|
1463
|
+
|
|
1464
|
+
/**
|
|
1465
|
+
* @param {any} [socket]
|
|
1466
|
+
* @returns {void}
|
|
1467
|
+
*/
|
|
1468
|
+
detachSocket(socket) {}
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* node's `statusMessage`, the reason phrase. It is held as `statusText` here, and the two are
|
|
1472
|
+
* the same thing: this is the name node and Express use, so code that sets it keeps working.
|
|
1473
|
+
*
|
|
1474
|
+
* @returns {string|undefined}
|
|
1475
|
+
*/
|
|
1476
|
+
get statusMessage() {
|
|
1477
|
+
return this.statusText;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
set statusMessage(value) {
|
|
1481
|
+
this.statusText = value;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
/**
|
|
1485
|
+
* Whether a header has been set on this response, which is node's `hasHeader`. Names are
|
|
1486
|
+
* compared lowercased, as node compares them.
|
|
1487
|
+
*
|
|
1488
|
+
* @param {string} name
|
|
1489
|
+
* @returns {boolean}
|
|
1490
|
+
*/
|
|
1491
|
+
hasHeader(name) {
|
|
1492
|
+
return this.headers[name.toLowerCase()] !== undefined;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
/**
|
|
1496
|
+
* The names of the headers set so far, lowercased, which is node's `getHeaderNames`.
|
|
1497
|
+
*
|
|
1498
|
+
* @returns {string[]}
|
|
1499
|
+
*/
|
|
1500
|
+
getHeaderNames() {
|
|
1501
|
+
return Object.keys(this.headers);
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
/**
|
|
1505
|
+
* node's `getRawHeaderNames`, which returns the names in the case they were set in. Header
|
|
1506
|
+
* names are held lowercased here, so this returns what {@link Response#getHeaderNames} does.
|
|
1507
|
+
*
|
|
1508
|
+
* @returns {string[]}
|
|
1509
|
+
*/
|
|
1510
|
+
getRawHeaderNames() {
|
|
1511
|
+
return Object.keys(this.headers);
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
/**
|
|
1515
|
+
* Adds a value to a header without replacing what is there, which is node's `appendHeader`.
|
|
1516
|
+
* A header that already has one value becomes a list, as node makes it.
|
|
1517
|
+
*
|
|
1518
|
+
* @param {string} name
|
|
1519
|
+
* @param {string|readonly string[]} value
|
|
1520
|
+
* @returns {this}
|
|
1521
|
+
*/
|
|
1522
|
+
appendHeader(name, value) {
|
|
1523
|
+
const key = name.toLowerCase();
|
|
1524
|
+
const current = this.headers[key];
|
|
1525
|
+
if (current === undefined) {
|
|
1526
|
+
return this.setHeader(name, /** @type {any} */ (value));
|
|
1527
|
+
}
|
|
1528
|
+
const merged = [].concat(/** @type {any} */ (current), /** @type {any} */ (value));
|
|
1529
|
+
return this.setHeader(name, /** @type {any} */ (merged));
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
/**
|
|
1533
|
+
* Sets several headers at once from a Headers or a Map, which is node's `setHeaders`. A
|
|
1534
|
+
* `Headers` gives `set-cookie` back through getSetCookie, so those stay separate values rather
|
|
1535
|
+
* than one folded string.
|
|
1536
|
+
*
|
|
1537
|
+
* @param {Headers|Map<string, string|readonly string[]>} headers
|
|
1538
|
+
* @returns {this}
|
|
1539
|
+
*/
|
|
1540
|
+
setHeaders(headers) {
|
|
1541
|
+
if (typeof Headers === "function" && headers instanceof Headers) {
|
|
1542
|
+
for (const name of new Set([...headers.keys()])) {
|
|
1543
|
+
if (name === "set-cookie") {
|
|
1544
|
+
this.setHeader(name, /** @type {any} */ (headers.getSetCookie()));
|
|
1545
|
+
} else {
|
|
1546
|
+
this.setHeader(name, /** @type {any} */ (headers.get(name)));
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
return this;
|
|
1550
|
+
}
|
|
1551
|
+
for (const [name, value] of headers) {
|
|
1552
|
+
this.setHeader(name, /** @type {any} */ (value));
|
|
1553
|
+
}
|
|
1554
|
+
return this;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1279
1557
|
/**
|
|
1280
1558
|
* Node asks this before validating a header value, and answering true keeps it permissive.
|
|
1281
1559
|
* Only reached through code that goes down node's own header path.
|
|
@@ -1720,12 +1998,6 @@ module.exports = class Response extends LazyWritable {
|
|
|
1720
1998
|
return this.set("content-type", ct);
|
|
1721
1999
|
}
|
|
1722
2000
|
|
|
1723
|
-
/**
|
|
1724
|
-
* express carries both names for the same method, and middleware reaches for either.
|
|
1725
|
-
* @type {(type: string) => any}
|
|
1726
|
-
*/
|
|
1727
|
-
contentType = this.type;
|
|
1728
|
-
|
|
1729
2001
|
/**
|
|
1730
2002
|
* Adds a field to Vary, without repeating one already there.
|
|
1731
2003
|
* @param {string|string[]} field
|
|
@@ -1755,3 +2027,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1755
2027
|
return this.finished;
|
|
1756
2028
|
}
|
|
1757
2029
|
};
|
|
2030
|
+
|
|
2031
|
+
// res.contentType is res.type under express's other name. On the prototype rather than an instance
|
|
2032
|
+
// field, which wrote one own property per response in the constructor.
|
|
2033
|
+
/** @type {any} */ (module.exports.prototype).contentType = module.exports.prototype.type;
|
package/src/router.js
CHANGED
|
@@ -446,7 +446,7 @@ class Walk {
|
|
|
446
446
|
return this.step(undefined);
|
|
447
447
|
}
|
|
448
448
|
if (kind === CALLBACK_ROUTER) {
|
|
449
|
-
if (callback.
|
|
449
|
+
if (callback._isApplication) {
|
|
450
450
|
rememberApp(this, route, req);
|
|
451
451
|
useApp(req, callback);
|
|
452
452
|
}
|
|
@@ -616,10 +616,15 @@ function nativeFail(err) {
|
|
|
616
616
|
* @returns {number}
|
|
617
617
|
*/
|
|
618
618
|
function mountPrefixLength(route, req) {
|
|
619
|
-
|
|
619
|
+
// a use with no path is EMPTY_REGEX, which matches "" at 0 whatever the path is. Answered
|
|
620
|
+
// without the exec, since this runs per hop and most middleware is pathless
|
|
621
|
+
if (route.pattern === EMPTY_REGEX) {
|
|
622
|
+
return 0;
|
|
623
|
+
}
|
|
620
624
|
if (typeof route.pattern === "string") {
|
|
621
625
|
return route.pattern.length;
|
|
622
626
|
}
|
|
627
|
+
const path = req._opPath;
|
|
623
628
|
const matched = route.pattern.exec(path === "" ? "/" : path);
|
|
624
629
|
return matched ? Math.min(matched[0].length, path.length) : 0;
|
|
625
630
|
}
|
|
@@ -1057,7 +1062,7 @@ function guardsInside(router, mount, pathPrefix, chain, inherited) {
|
|
|
1057
1062
|
* @param {any} req
|
|
1058
1063
|
*/
|
|
1059
1064
|
function rememberApp(walk, route, req) {
|
|
1060
|
-
if (walk.router._isApplication && route.callbacks[0]?.
|
|
1065
|
+
if (walk.router._isApplication && route.callbacks[0]?._isApplication) {
|
|
1061
1066
|
(req._appStack ??= []).push(route, req.app);
|
|
1062
1067
|
}
|
|
1063
1068
|
}
|
|
@@ -2536,6 +2541,24 @@ module.exports = class Router extends EventEmitter {
|
|
|
2536
2541
|
});
|
|
2537
2542
|
}
|
|
2538
2543
|
|
|
2544
|
+
/**
|
|
2545
|
+
* The same walk without the promise pair, for a uWS handler that never awaited it. nativeDone
|
|
2546
|
+
* and nativeFail defer their epilogues to the microtask the await used to resume on, so the
|
|
2547
|
+
* visible order holds.
|
|
2548
|
+
*
|
|
2549
|
+
* @param {any} req
|
|
2550
|
+
* @param {any} res
|
|
2551
|
+
*/
|
|
2552
|
+
_routeRequestDirect(req, res) {
|
|
2553
|
+
const walk = new Walk(this, req, res, this._routes, false, undefined, nativeDone, nativeFail);
|
|
2554
|
+
try {
|
|
2555
|
+
walk.dispatch(0);
|
|
2556
|
+
} catch (err) {
|
|
2557
|
+
// what a throw inside a promise executor did: reject, once
|
|
2558
|
+
nativeFail.call(walk, err);
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2539
2562
|
/**
|
|
2540
2563
|
* Mounts middleware, or a whole router, at a path. The path is optional, and a mount matches
|
|
2541
2564
|
* everything under it, which is what separates it from all(). Mounting a Router sets its
|
package/src/utils.js
CHANGED
|
@@ -1102,6 +1102,11 @@ function createETagGenerator(options) {
|
|
|
1102
1102
|
if (body instanceof Stats) {
|
|
1103
1103
|
return statTag(body, options.weak);
|
|
1104
1104
|
}
|
|
1105
|
+
// crypto.hash reads a string as the utf8 bytes Buffer.from would have produced, so the tag
|
|
1106
|
+
// is the same one without copying the whole body first
|
|
1107
|
+
if (typeof body === "string" && (encoding === undefined || encoding === "utf8" || encoding === "utf-8")) {
|
|
1108
|
+
return entityTag(body, options.weak);
|
|
1109
|
+
}
|
|
1105
1110
|
const buf = !Buffer.isBuffer(body) ? Buffer.from(body, encoding) : body;
|
|
1106
1111
|
return entityTag(buf, options.weak);
|
|
1107
1112
|
};
|