fulmine.js 5.5.1 → 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 +1 -6
- package/package.json +1 -1
- package/src/application.js +6 -21
- package/src/request.js +0 -4
- package/src/response.js +26 -35
- 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)
|
|
@@ -230,6 +224,7 @@ A single-stage `node:26-trixie-slim` image works too if you `apt-get install -y
|
|
|
230
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.
|
|
231
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.
|
|
232
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`.
|
|
233
228
|
- For HTTPS, instead of doing this:
|
|
234
229
|
|
|
235
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
|
@@ -756,10 +756,6 @@ module.exports = class Request extends LazyReadable {
|
|
|
756
756
|
* @param {() => void} [callback]
|
|
757
757
|
* @returns {this}
|
|
758
758
|
*/
|
|
759
|
-
|
|
760
|
-
/**
|
|
761
|
-
*
|
|
762
|
-
*/
|
|
763
759
|
setTimeout(msecs, callback) {
|
|
764
760
|
if (typeof callback === "function") {
|
|
765
761
|
this.once("timeout", callback);
|
package/src/response.js
CHANGED
|
@@ -228,9 +228,10 @@ module.exports = class Response extends LazyWritable {
|
|
|
228
228
|
* measured on uWS alone, 500 writes of 66 bytes take 13ms against 0.06ms for 100 of them.
|
|
229
229
|
* Handing it the same bytes in blocks costs 0.4ms. Nothing here changes what goes on the wire,
|
|
230
230
|
* only how many calls it takes to put it there.
|
|
231
|
-
*
|
|
231
|
+
* Null until the first chunked write, since a res.send never queues anything.
|
|
232
|
+
* @type {Buffer[]|null}
|
|
232
233
|
*/
|
|
233
|
-
#queued =
|
|
234
|
+
#queued = null;
|
|
234
235
|
|
|
235
236
|
/** How many bytes {@link #queued} holds, kept alongside so the flush does not add them up. */
|
|
236
237
|
#queuedBytes = 0;
|
|
@@ -437,12 +438,12 @@ module.exports = class Response extends LazyWritable {
|
|
|
437
438
|
* @param {((err?: Error|null) => void)|null} callback the stream's, when there is one waiting
|
|
438
439
|
*/
|
|
439
440
|
#flushQueued(callback) {
|
|
440
|
-
if (this.#queuedBytes === 0) {
|
|
441
|
+
if (this.#queued === null || this.#queuedBytes === 0) {
|
|
441
442
|
if (callback) callback(null);
|
|
442
443
|
return;
|
|
443
444
|
}
|
|
444
445
|
const body = this.#queued.length === 1 ? this.#queued[0] : Buffer.concat(this.#queued, this.#queuedBytes);
|
|
445
|
-
this.#queued =
|
|
446
|
+
this.#queued = null;
|
|
446
447
|
this.#queuedBytes = 0;
|
|
447
448
|
|
|
448
449
|
const ok = this._res.write(body);
|
|
@@ -467,13 +468,16 @@ module.exports = class Response extends LazyWritable {
|
|
|
467
468
|
}
|
|
468
469
|
|
|
469
470
|
/**
|
|
470
|
-
* The booked flush.
|
|
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
|
|
471
475
|
*/
|
|
472
|
-
#flushOnTick
|
|
473
|
-
|
|
474
|
-
if (
|
|
475
|
-
|
|
476
|
-
}
|
|
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
|
+
}
|
|
477
481
|
|
|
478
482
|
/**
|
|
479
483
|
* Writable's sink. Sends the headers if they have not gone yet, then hands the chunk to uWS,
|
|
@@ -520,7 +524,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
520
524
|
// turn or once it is big enough to be worth a call. A stream that writes once per
|
|
521
525
|
// turn, an SSE feed for instance, still leaves on its own turn: the queue only ever
|
|
522
526
|
// gathers what was written without yielding in between.
|
|
523
|
-
this.#queued.push(/** @type {Buffer} */ (chunk));
|
|
527
|
+
(this.#queued ??= []).push(/** @type {Buffer} */ (chunk));
|
|
524
528
|
this.#queuedBytes += /** @type {Buffer} */ (chunk).byteLength;
|
|
525
529
|
// a chunk that is already big enough to be worth its own call leaves with whatever
|
|
526
530
|
// was waiting in front of it, rather than paying for a copy it does not need
|
|
@@ -529,7 +533,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
529
533
|
} else {
|
|
530
534
|
if (!this.#flushBooked) {
|
|
531
535
|
this.#flushBooked = true;
|
|
532
|
-
process.nextTick(
|
|
536
|
+
process.nextTick(Response.#flushOnTick, this);
|
|
533
537
|
}
|
|
534
538
|
this.writingChunk = false;
|
|
535
539
|
callback(null);
|
|
@@ -1329,11 +1333,16 @@ module.exports = class Response extends LazyWritable {
|
|
|
1329
1333
|
}
|
|
1330
1334
|
|
|
1331
1335
|
/**
|
|
1332
|
-
*
|
|
1336
|
+
* Hands the status line and the headers over now, without waiting for a body, which is node's
|
|
1333
1337
|
* flushHeaders(). Callers use it to let the client start on the head while the body is still
|
|
1334
1338
|
* being produced, and one of them is `@angular/ssr`'s writeResponseToNodeResponse, which calls
|
|
1335
1339
|
* it before streaming a rendered page.
|
|
1336
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
|
+
*
|
|
1337
1346
|
* A second call does nothing, as node's does. Nothing is written for a response already
|
|
1338
1347
|
* finished or aborted: uWS has let go of it by then.
|
|
1339
1348
|
*
|
|
@@ -1425,10 +1434,6 @@ module.exports = class Response extends LazyWritable {
|
|
|
1425
1434
|
* @param {Record<string, string>|[string, string][]} [headers]
|
|
1426
1435
|
* @returns {void}
|
|
1427
1436
|
*/
|
|
1428
|
-
|
|
1429
|
-
/**
|
|
1430
|
-
*
|
|
1431
|
-
*/
|
|
1432
1437
|
addTrailers(headers) {}
|
|
1433
1438
|
|
|
1434
1439
|
/**
|
|
@@ -1440,10 +1445,6 @@ module.exports = class Response extends LazyWritable {
|
|
|
1440
1445
|
* @param {() => void} [callback]
|
|
1441
1446
|
* @returns {this}
|
|
1442
1447
|
*/
|
|
1443
|
-
|
|
1444
|
-
/**
|
|
1445
|
-
*
|
|
1446
|
-
*/
|
|
1447
1448
|
setTimeout(msecs, callback) {
|
|
1448
1449
|
if (typeof callback === "function") {
|
|
1449
1450
|
this.once("timeout", callback);
|
|
@@ -1458,20 +1459,12 @@ module.exports = class Response extends LazyWritable {
|
|
|
1458
1459
|
* @param {any} [socket]
|
|
1459
1460
|
* @returns {void}
|
|
1460
1461
|
*/
|
|
1461
|
-
|
|
1462
|
-
/**
|
|
1463
|
-
*
|
|
1464
|
-
*/
|
|
1465
1462
|
assignSocket(socket) {}
|
|
1466
1463
|
|
|
1467
1464
|
/**
|
|
1468
1465
|
* @param {any} [socket]
|
|
1469
1466
|
* @returns {void}
|
|
1470
1467
|
*/
|
|
1471
|
-
|
|
1472
|
-
/**
|
|
1473
|
-
*
|
|
1474
|
-
*/
|
|
1475
1468
|
detachSocket(socket) {}
|
|
1476
1469
|
|
|
1477
1470
|
/**
|
|
@@ -2005,12 +1998,6 @@ module.exports = class Response extends LazyWritable {
|
|
|
2005
1998
|
return this.set("content-type", ct);
|
|
2006
1999
|
}
|
|
2007
2000
|
|
|
2008
|
-
/**
|
|
2009
|
-
* express carries both names for the same method, and middleware reaches for either.
|
|
2010
|
-
* @type {(type: string) => any}
|
|
2011
|
-
*/
|
|
2012
|
-
contentType = this.type;
|
|
2013
|
-
|
|
2014
2001
|
/**
|
|
2015
2002
|
* Adds a field to Vary, without repeating one already there.
|
|
2016
2003
|
* @param {string|string[]} field
|
|
@@ -2040,3 +2027,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
2040
2027
|
return this.finished;
|
|
2041
2028
|
}
|
|
2042
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
|
};
|