fulmine.js 5.0.0-rc.1 → 5.0.0
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 -1
- package/package.json +2 -1
- package/src/application.js +101 -25
- package/src/declarative.js +20 -12
- package/src/middlewares.js +413 -107
- package/src/node-shim.js +18 -9
- package/src/request.js +79 -23
- package/src/response.js +102 -40
- package/src/router.js +640 -350
- package/src/utils.js +51 -3
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ Compatibility here is not a claim, it is a test suite. Every test runs against r
|
|
|
36
36
|
|
|
37
37
|
Fulmine is faster than Express where the framework itself is doing the work, and the same speed where it is not. Both halves of that sentence matter, so here is the honest version.
|
|
38
38
|
|
|
39
|
-
**Where it is clearly faster.** Routing and dispatch, request shapes with params and query strings, connection handling.
|
|
39
|
+
**Where it is clearly faster.** Routing and dispatch, request shapes with params and query strings, connection handling. Plain routing lands between 1.9x and 3.2x: hello-world 1.9x to 2.2x, an API endpoint with params and a query 2.6x to 3.2x, nested routers 2x to 2.5x, a urlencoded body 2.6x to 3.2x, 5000 concurrent connections 2.4x to 2.7x. Route tables are where the native router shows: a thousand routes 5.4x to 8.1x, with a parameter in every one of them 5.8x to 8.7x, a parameterised route in a mounted router 3.8x to 5.9x. Those routes go to µWS's own router instead of being scanned, so the gap grows with the table instead of shrinking. The one routing scenario still even is a chain of 100 middlewares, 0.95x to 1x, where the cost is calling application code a hundred times rather than routing.
|
|
40
40
|
|
|
41
41
|
**Where it is a wash.** Any request whose cost is dominated by work both servers hand to the same library. A 512 KiB JSON body is `JSON.parse`, a gzipped response is zlib, a hashed upload is OpenSSL, a 5 MiB stream is memory bandwidth. On those the ratio is capped by arithmetic somewhere around 1.0x to 1.2x, and no amount of work on either server moves it. The benchmark labels those rows rather than quietly publishing them as if the two were equivalent.
|
|
42
42
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fulmine.js",
|
|
3
|
-
"version": "5.0.0
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"description": "Drop-in Express 5 replacement on uWebSockets.js. Your existing middleware keeps working.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
"fast-querystring": "^1.1.2",
|
|
74
74
|
"fast-zlib": "^2.0.1",
|
|
75
75
|
"fresh": "^2.0.0",
|
|
76
|
+
"iconv-lite": "^0.7.3",
|
|
76
77
|
"mime-types": "^3.0.2",
|
|
77
78
|
"ms": "^2.1.3",
|
|
78
79
|
"proxy-addr": "^2.0.7",
|
package/src/application.js
CHANGED
|
@@ -117,6 +117,11 @@ class Application extends Router {
|
|
|
117
117
|
this.listening = false;
|
|
118
118
|
// the host handed to listen(), which is all address() has to go on
|
|
119
119
|
this._listenHost = undefined;
|
|
120
|
+
// the uWS listen socket, and the responses being served right now: close() stops the
|
|
121
|
+
// first and waits for the second, the way node's server.close() does
|
|
122
|
+
this._listenSocket = undefined;
|
|
123
|
+
this._pendingResponses = new Set();
|
|
124
|
+
this._draining = false;
|
|
120
125
|
for (const key in defaultSettings) {
|
|
121
126
|
if (typeof this.settings[key] === "undefined") {
|
|
122
127
|
if (typeof defaultSettings[key] === "function") {
|
|
@@ -169,7 +174,7 @@ class Application extends Router {
|
|
|
169
174
|
* Reads or writes an application setting. One argument is the getter, and the check is on
|
|
170
175
|
* `arguments.length`, so `set(key, undefined)` still writes. Some keys have a side effect:
|
|
171
176
|
* `trust proxy`, `query parser` and `etag` compile the value into a function kept beside it,
|
|
172
|
-
* `views` becomes an absolute path
|
|
177
|
+
* and `views` becomes an absolute path.
|
|
173
178
|
*
|
|
174
179
|
* @param {string} key setting name
|
|
175
180
|
* @param {*} [value] value to store; omit to read instead
|
|
@@ -195,14 +200,9 @@ class Application extends Router {
|
|
|
195
200
|
} else {
|
|
196
201
|
this.settings["query parser fn"] = undefined;
|
|
197
202
|
}
|
|
198
|
-
} else if (key === "env") {
|
|
199
|
-
if (value === "production") {
|
|
200
|
-
this.settings["view cache"] = true;
|
|
201
|
-
} else {
|
|
202
|
-
this.settings["view cache"] = undefined;
|
|
203
|
-
}
|
|
204
203
|
} else if (key === "views") {
|
|
205
|
-
|
|
204
|
+
// a list of directories is searched in order by View.lookup, each resolved here once
|
|
205
|
+
this.settings[key] = Array.isArray(value) ? value.map((dir) => path.resolve(dir)) : path.resolve(value);
|
|
206
206
|
return this;
|
|
207
207
|
} else if (key === "etag") {
|
|
208
208
|
if (typeof value === "function") {
|
|
@@ -268,6 +268,27 @@ class Application extends Router {
|
|
|
268
268
|
return !this.settings[key];
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Router's handleRequest plus the bookkeeping a graceful close() needs: every live response
|
|
273
|
+
* is held in a set until it finishes, so close() knows when the last one is done. Native
|
|
274
|
+
* routes and the catch-all both come through here, since both call it on the app.
|
|
275
|
+
*
|
|
276
|
+
* @param {any} res uWS response
|
|
277
|
+
* @param {any} req uWS request, readable only during this call
|
|
278
|
+
* @returns {{request: any, response: any}}
|
|
279
|
+
*/
|
|
280
|
+
handleRequest(res, req) {
|
|
281
|
+
const handled = super.handleRequest(res, req);
|
|
282
|
+
const response = handled.response;
|
|
283
|
+
this._pendingResponses.add(response);
|
|
284
|
+
// removal rides the close listener the Response constructor already has, since a second
|
|
285
|
+
// once() per request measured a tenth of a microsecond on the hot path.
|
|
286
|
+
// An aborted response only flips its flags without emitting 'close', which is why
|
|
287
|
+
// close()'s drain also sweeps the set by those flags instead of trusting this alone
|
|
288
|
+
response._pendingIn = this._pendingResponses;
|
|
289
|
+
return handled;
|
|
290
|
+
}
|
|
291
|
+
|
|
271
292
|
/**
|
|
272
293
|
* Registers the catch-all uWS handler, which is what serves every request that no optimized
|
|
273
294
|
* route took natively. It walks this app's own chain and, when nothing in it answered, decides
|
|
@@ -277,9 +298,19 @@ class Application extends Router {
|
|
|
277
298
|
this.uwsApp.any("/*", async (res, req) => {
|
|
278
299
|
const { request, response } = this.handleRequest(res, req);
|
|
279
300
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
301
|
+
try {
|
|
302
|
+
const matchedRoute = await this._routeRequest(request, response);
|
|
303
|
+
if (!matchedRoute && !response.headersSent && !response.aborted) {
|
|
304
|
+
this._endUnmatched(request, response);
|
|
305
|
+
}
|
|
306
|
+
} catch (err) {
|
|
307
|
+
// an internal throw answers 500 as express's final handler would, instead of
|
|
308
|
+
// dying as an unhandled rejection
|
|
309
|
+
if (response.aborted || response.finished) {
|
|
310
|
+
console.error(err);
|
|
311
|
+
} else {
|
|
312
|
+
this._handleError(err, null, request, response);
|
|
313
|
+
}
|
|
283
314
|
}
|
|
284
315
|
});
|
|
285
316
|
}
|
|
@@ -311,6 +342,11 @@ class Application extends Router {
|
|
|
311
342
|
callback = host;
|
|
312
343
|
host = undefined;
|
|
313
344
|
}
|
|
345
|
+
// bare listen() and listen(undefined, cb) bind an OS-assigned port, as node does; left
|
|
346
|
+
// undefined the port fell through to the unix-socket branch below
|
|
347
|
+
if (port == null) {
|
|
348
|
+
port = 0;
|
|
349
|
+
}
|
|
314
350
|
// uWS runs this handler from inside its own listen(), so everything it hands back to the
|
|
315
351
|
// caller is deferred a tick. Express binds synchronously too but reports through events,
|
|
316
352
|
// and node emits both 'listening' and 'error' from a process.nextTick.
|
|
@@ -337,6 +373,8 @@ class Application extends Router {
|
|
|
337
373
|
this.port = uWS.us_socket_local_port(socket);
|
|
338
374
|
this.listening = true;
|
|
339
375
|
this._listenHost = host;
|
|
376
|
+
// kept so close() can stop accepting without dropping what is in flight
|
|
377
|
+
this._listenSocket = socket;
|
|
340
378
|
process.nextTick(() => {
|
|
341
379
|
// `this` is the app, which is what listen() returns here. Express binds it to the
|
|
342
380
|
// http.Server, which is what listen() returns there, so
|
|
@@ -451,20 +489,21 @@ class Application extends Router {
|
|
|
451
489
|
// render exists to hand the result somewhere, so there is always a callback by this point:
|
|
452
490
|
// either the third argument or the second one, shuffled above
|
|
453
491
|
const done = /** @type {(err: Error|null, html?: string) => void} */ (callback);
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
}
|
|
492
|
+
// express's order, least specific first: app.locals, then res.locals riding in as _locals,
|
|
493
|
+
// and what was passed to this call wins over both
|
|
494
|
+
const opts = options || new NullObject();
|
|
495
|
+
options = new NullObject();
|
|
459
496
|
for (const key in this.locals) {
|
|
460
497
|
options[key] = this.locals[key];
|
|
461
498
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
options[key] = options._locals[key];
|
|
499
|
+
if (opts._locals) {
|
|
500
|
+
for (const key in opts._locals) {
|
|
501
|
+
options[key] = opts._locals[key];
|
|
466
502
|
}
|
|
467
503
|
}
|
|
504
|
+
for (const key in opts) {
|
|
505
|
+
options[key] = opts[key];
|
|
506
|
+
}
|
|
468
507
|
|
|
469
508
|
if (options.cache == null) {
|
|
470
509
|
options.cache = this.enabled("view cache");
|
|
@@ -511,7 +550,13 @@ class Application extends Router {
|
|
|
511
550
|
}
|
|
512
551
|
|
|
513
552
|
/**
|
|
514
|
-
* Stops
|
|
553
|
+
* Stops accepting connections, lets in-flight requests finish, then emits 'close'.
|
|
554
|
+
*
|
|
555
|
+
* Node's server.close(), which Express hands back from listen(), only closes the listen
|
|
556
|
+
* socket and waits for what is being served; uWS's close() forcefully terminates every
|
|
557
|
+
* connection, so calling it first aborted whatever a graceful shutdown was waiting for.
|
|
558
|
+
* It still runs, but only once the last pending response is done, to drop the idle
|
|
559
|
+
* keep-alive connections nothing else would close.
|
|
515
560
|
*
|
|
516
561
|
* The callback is the first 'close' listener, so it runs before any added afterwards. Closing
|
|
517
562
|
* a server that was not listening still calls back, with an ERR_SERVER_NOT_RUNNING error, the
|
|
@@ -522,9 +567,6 @@ class Application extends Router {
|
|
|
522
567
|
*/
|
|
523
568
|
close(callback) {
|
|
524
569
|
const wasListening = this.listening;
|
|
525
|
-
if (this.listenCalled && wasListening) {
|
|
526
|
-
this.uwsApp.close();
|
|
527
|
-
}
|
|
528
570
|
this.listening = false;
|
|
529
571
|
// in Express the close callback is nothing more than the first 'close' listener, and a
|
|
530
572
|
// server that was not running still gets called back, with an error
|
|
@@ -539,7 +581,41 @@ class Application extends Router {
|
|
|
539
581
|
callback(err);
|
|
540
582
|
});
|
|
541
583
|
}
|
|
542
|
-
|
|
584
|
+
if (!this.listenCalled || !wasListening) {
|
|
585
|
+
// a close while a drain is underway does not emit again: the pending drain's single
|
|
586
|
+
// 'close' serves both calls, which is what node does too
|
|
587
|
+
if (!this._draining) {
|
|
588
|
+
process.nextTick(() => this.emit("close"));
|
|
589
|
+
}
|
|
590
|
+
return this;
|
|
591
|
+
}
|
|
592
|
+
if (this._listenSocket) {
|
|
593
|
+
uWS.us_listen_socket_close(this._listenSocket);
|
|
594
|
+
this._listenSocket = undefined;
|
|
595
|
+
}
|
|
596
|
+
this._draining = true;
|
|
597
|
+
const finish = () => {
|
|
598
|
+
this._draining = false;
|
|
599
|
+
this.uwsApp.close();
|
|
600
|
+
this.emit("close");
|
|
601
|
+
};
|
|
602
|
+
if (this._pendingResponses.size === 0) {
|
|
603
|
+
process.nextTick(finish);
|
|
604
|
+
return this;
|
|
605
|
+
}
|
|
606
|
+
// a finished response emits 'close' and removes itself; an aborted one only flips its
|
|
607
|
+
// flags, so the drain sweeps by them. The timer also keeps the loop alive until done.
|
|
608
|
+
const sweep = setInterval(() => {
|
|
609
|
+
for (const response of this._pendingResponses) {
|
|
610
|
+
if (response.finished || response.aborted) {
|
|
611
|
+
this._pendingResponses.delete(response);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (this._pendingResponses.size === 0) {
|
|
615
|
+
clearInterval(sweep);
|
|
616
|
+
finish();
|
|
617
|
+
}
|
|
618
|
+
}, 10);
|
|
543
619
|
return this;
|
|
544
620
|
}
|
|
545
621
|
}
|
package/src/declarative.js
CHANGED
|
@@ -220,9 +220,8 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
const [req, res] = args;
|
|
223
|
-
let queryName,
|
|
224
|
-
|
|
225
|
-
queries = [],
|
|
223
|
+
let queryName, paramsName;
|
|
224
|
+
const queries = [],
|
|
226
225
|
params = [];
|
|
227
226
|
|
|
228
227
|
if (fn.params[0].type === "ObjectPattern") {
|
|
@@ -384,12 +383,14 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
384
383
|
if (call.arguments[0].type !== "Literal" || call.arguments[1].type !== "Literal") {
|
|
385
384
|
return false;
|
|
386
385
|
}
|
|
387
|
-
|
|
386
|
+
// String() at capture: a numeric literal would reach uWS's writeHeader as itself,
|
|
387
|
+
// and uWS refuses anything that is not a string
|
|
388
|
+
let [header, value] = [call.arguments[0].value, String(call.arguments[1].value)];
|
|
388
389
|
const name = String(header).toLowerCase();
|
|
389
390
|
// res.set charsets a content-type and res.setHeader does not, since the second is
|
|
390
391
|
// node's and node does not know what a media type is
|
|
391
392
|
if (call.obj.propertyName !== "setHeader" && name === "content-type") {
|
|
392
|
-
value = withDefaultCharset(
|
|
393
|
+
value = withDefaultCharset(value);
|
|
393
394
|
}
|
|
394
395
|
const index = headers.findIndex((entry) => String(entry[0]).toLowerCase() === name);
|
|
395
396
|
if (index === -1) {
|
|
@@ -409,7 +410,7 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
409
410
|
if (call.arguments[0].type !== "Literal" || call.arguments[1].type !== "Literal") {
|
|
410
411
|
return false;
|
|
411
412
|
}
|
|
412
|
-
headers.push([call.arguments[0].value, call.arguments[1].value]);
|
|
413
|
+
headers.push([call.arguments[0].value, String(call.arguments[1].value)]);
|
|
413
414
|
} else if (call.obj.propertyName === "sendStatus") {
|
|
414
415
|
if (call.arguments[0].type !== "Literal") {
|
|
415
416
|
return false;
|
|
@@ -552,6 +553,11 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
552
553
|
* @returns {boolean}
|
|
553
554
|
*/
|
|
554
555
|
function check(node) {
|
|
556
|
+
// only "+" concatenates; any other operator computes a value the
|
|
557
|
+
// parts cannot represent, so the handler falls back
|
|
558
|
+
if (node.operator !== "+") {
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
555
561
|
if (node.right.type === "Literal") {
|
|
556
562
|
stuff.push({ type: "text", value: node.right.value });
|
|
557
563
|
} else if (node.right.type === "MemberExpression") {
|
|
@@ -602,12 +608,14 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
602
608
|
|
|
603
609
|
let decRes = new uWSAny.DeclarativeResponse();
|
|
604
610
|
|
|
605
|
-
if (statusCode
|
|
606
|
-
const statusMessage = statuses.message[statusCode] ?? "";
|
|
607
|
-
decRes = decRes.writeStatus(`${statusCode} ${statusMessage}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
+
if (statusCode !== 200) {
|
|
612
|
+
const statusMessage = statuses.message[statusCode] ?? "unknown";
|
|
613
|
+
decRes = decRes.writeStatus(`${statusCode} ${statusMessage}`);
|
|
614
|
+
}
|
|
615
|
+
// only sendStatus types its body: it goes through res.type("txt") on the ordinary path,
|
|
616
|
+
// while status(n).end() sends no Content-Type at all, in Express and here alike
|
|
617
|
+
if (sendStatusUsed && !headers.some((header) => header[0].toLowerCase() === "content-type")) {
|
|
618
|
+
decRes = decRes.writeHeader("content-type", "text/plain; charset=utf-8");
|
|
611
619
|
}
|
|
612
620
|
|
|
613
621
|
// the same two the ordinary path seeds every response with. Without them a route answered
|