fulmine.js 5.0.0-rc.1 → 5.1.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 +26 -8
- package/package.json +2 -1
- package/src/application.js +203 -37
- package/src/cli.js +3 -3
- package/src/declarative.js +20 -12
- package/src/index.js +5 -0
- package/src/middlewares.js +479 -122
- package/src/node-shim.js +20 -10
- package/src/request.js +174 -60
- package/src/response.js +232 -106
- package/src/route.js +180 -0
- package/src/router.js +1073 -404
- package/src/utils.js +92 -6
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ npx fulmine differences # just the list of what to check by hand
|
|
|
23
23
|
|
|
24
24
|
See [Migrating](#migrating) for what it handles and what it deliberately does not.
|
|
25
25
|
|
|
26
|
+
[](https://www.npmjs.com/package/fulmine.js)
|
|
26
27
|
[](https://nodejs.org)
|
|
27
28
|
[](./LICENSE)
|
|
28
29
|
|
|
@@ -30,13 +31,13 @@ See [Migrating](#migrating) for what it handles and what it deliberately does no
|
|
|
30
31
|
|
|
31
32
|
There are several fast HTTP servers for Node built on [µWebSockets.js](https://github.com/uNetworking/uWebSockets.js). What is scarce is one you can actually drop into an existing Express application without rewriting it.
|
|
32
33
|
|
|
33
|
-
Compatibility here is not a claim, it is a test suite. Every test runs against real Express first and then against Fulmine, and the outputs have to match byte for byte. That is what makes `helmet`, `cors`, `passport`, `morgan`, `multer`, `express-session` and the rest of the ecosystem work rather than "mostly work".
|
|
34
|
+
Compatibility here is not a claim, it is a test suite. Every test runs against real Express first and then against Fulmine, and the outputs have to match byte for byte. That is what makes `helmet`, `cors`, `passport`, `morgan`, `multer`, `express-session` and the rest of the ecosystem work rather than "mostly work". Express 5's own test suite runs against Fulmine too, and passes whole: 1130 passing, 0 failing at the pinned Express version.
|
|
34
35
|
|
|
35
36
|
## Performance
|
|
36
37
|
|
|
37
38
|
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
39
|
|
|
39
|
-
**Where it is clearly faster.** Routing and dispatch, request shapes with params and query strings, connection handling.
|
|
40
|
+
**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.9x: hello-world 1.9x to 2.7x, an API endpoint with params and a query 3x to 3.8x, five route shapes served by one process 2.7x to 3.9x, nested routers 2.2x to 2.8x, a urlencoded body 3.2x to 3.8x, a thousand concurrent connections 2.7x to 3x. Route tables are where the native router shows: a thousand routes 9.8x to 12.7x, with a parameter in every one of them 9.9x to 14.3x, a parameterised route in a mounted router 7.4x to 8.8x. Those routes go to µWS's own router instead of being scanned, so the gap grows with the table instead of shrinking. Even the chain of 100 middlewares, for a long time the one routing row that stayed even because its cost is calling application code a hundred times, sits at 1.35x to 1.55x after the per-request allocation work of August 2026.
|
|
40
41
|
|
|
41
42
|
**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
43
|
|
|
@@ -47,8 +48,7 @@ Two things worth knowing before comparing numbers with anyone:
|
|
|
47
48
|
|
|
48
49
|
There is no table here on purpose. CI runs the whole benchmark on every push and every pull request
|
|
49
50
|
and posts the result where it belongs: as a comment on the commit or the pull request, and as a
|
|
50
|
-
`benchmark-summary` artifact on the run
|
|
51
|
-
on one day, and would start rotting immediately. See [`benchmark/README.md`](./benchmark/README.md)
|
|
51
|
+
`benchmark-summary` artifact on the run, see [`benchmark/README.md`](./benchmark/README.md)
|
|
52
52
|
to run it yourself.
|
|
53
53
|
|
|
54
54
|
## Attribution
|
|
@@ -84,7 +84,6 @@ command whose name ends in `.js` on Windows, where it exits without a word.
|
|
|
84
84
|
## Differences from Express
|
|
85
85
|
|
|
86
86
|
- `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.
|
|
87
|
-
- `case sensitive routing` is enabled by default.
|
|
88
87
|
- `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.
|
|
89
88
|
- 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.
|
|
90
89
|
- For HTTPS, instead of doing this:
|
|
@@ -134,8 +133,7 @@ app.listen(3000, () => {
|
|
|
134
133
|
|
|
135
134
|
1. Fulmine tries to optimize routing as much as possible, but it's only possible if:
|
|
136
135
|
|
|
137
|
-
- `
|
|
138
|
-
- the path is a plain string, or its parameters are whole segments: `/users/:id` and `/a/:b/c/:d` qualify, `/flights/:from-:to` does not, and neither does a `*splat` or a `{}` group.
|
|
136
|
+
- the path is a plain string, or its parameters are whole segments: `/users/:id` and `/a/:b/c/:d` qualify, `/flights/:from-:to` does not, and neither does a `*splat` or a `{}` group. Routing is case-insensitive by default, as in Express; a request in the registered case is still served natively, any other case takes the ordinary path, and a route whose overlap with an earlier one leans on a cased literal goes the ordinary way for every request.
|
|
139
137
|
- inside a mounted router, nothing registered after the route in that router could match the same path. `/orders/:id`, `/orders/:id/items` and `/invoices/:id` are all optimized together, since no request reaches two of them. `/users/:id` followed by `/users/me` is not: Express answers `/users/me` with the first of the two and the native router would answer it with the second, so both go the ordinary way.
|
|
140
138
|
|
|
141
139
|
Optimized routes can be up to 10 times faster than normal routes, as they're using native uWS router and have pre-calculated path.
|
|
@@ -237,7 +235,7 @@ In general, basically all features and options are supported. Use the [Express 5
|
|
|
237
235
|
- 🚧 express.request (this is not a constructor but a prototype for replacing methods)
|
|
238
236
|
- 🚧 express.response (this is not a constructor but a prototype for replacing methods)
|
|
239
237
|
- 🚧 express.application (likewise: a method added here is on every app)
|
|
240
|
-
-
|
|
238
|
+
- ✅ express.Route. Both `app.route("/path").get(...).post(...)` and the class itself, for building a route by hand and dispatching to it.
|
|
241
239
|
|
|
242
240
|
### Application
|
|
243
241
|
|
|
@@ -459,6 +457,26 @@ twice, once with `express` and once with this, and fails on any difference. That
|
|
|
459
457
|
test means writing something that prints what you want compared, and why a test that prints from
|
|
460
458
|
both the server and the client at once is a bug: the two orderings are a race.
|
|
461
459
|
|
|
460
|
+
### Writing a comparison test
|
|
461
|
+
|
|
462
|
+
A test file is an ordinary script. The first line is its description, the second may carry a marker,
|
|
463
|
+
and the rest sets up an app, makes requests and prints. `tests/helpers.js` has what to print with:
|
|
464
|
+
|
|
465
|
+
| | |
|
|
466
|
+
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
467
|
+
| `fetchTest(url, init)` | `fetch`, plus a line with the status and the headers worth comparing. Returns the response untouched, so the test goes on to read the body as it would have. Lines come out in call order, never in arrival order. |
|
|
468
|
+
| `sequential([() => …])` | Runs requests one at a time. `Promise.all` starts them together and the two servers then answer in whatever order they scheduled, which is a difference the runner would report as a failure. |
|
|
469
|
+
| `// INSPECT` | On the second line. The runner then mounts `inspectRequest` in front of every app the file makes, and each request prints its `method`, `url`, `originalUrl`, `baseUrl`, `path`, `protocol`, `secure`, `hostname`, `host`, `xhr`, `subdomains` and `query`. |
|
|
470
|
+
| `// OFF: reason` | Skips the file. |
|
|
471
|
+
|
|
472
|
+
`// INSPECT` is not free everywhere, which is why it is asked for rather than always on. It is a
|
|
473
|
+
middleware, so a route behind it stops being compiled into a declarative response and is served by
|
|
474
|
+
the ordinary path instead: a file whose routes do compile would quietly stop covering the compiled
|
|
475
|
+
one. And Express builds its router at the first `use()`, freezing `strict routing` and
|
|
476
|
+
`case sensitive routing` as they are at that moment, so a file that sets either one afterwards must
|
|
477
|
+
not ask for it. Everywhere else it is worth having: it is what caught a pathless mount dropping the
|
|
478
|
+
middleware in front of it.
|
|
479
|
+
|
|
462
480
|
`npm run test:express` is the other kind of test: it clones Express at the version in
|
|
463
481
|
`devDependencies`, points its entry at this source and runs its suite against it. It is a bug mine
|
|
464
482
|
rather than a gate, and its exit status says nothing. Read the header of `tools/express-suite.js`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fulmine.js",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.1.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
|
@@ -29,6 +29,8 @@ const {
|
|
|
29
29
|
NullObject
|
|
30
30
|
} = require("./utils.js");
|
|
31
31
|
const querystring = require("fast-querystring");
|
|
32
|
+
const Request = require("./request.js");
|
|
33
|
+
const Response = require("./response.js");
|
|
32
34
|
const ViewClass = require("./view.js");
|
|
33
35
|
const path = require("path");
|
|
34
36
|
const os = require("os");
|
|
@@ -37,6 +39,10 @@ const cluster = require("cluster");
|
|
|
37
39
|
|
|
38
40
|
const cpuCount = os.cpus().length;
|
|
39
41
|
|
|
42
|
+
// marks a "trust proxy" that was never set by the application, under the key express uses, so a
|
|
43
|
+
// mounted sub-app knows it may inherit the parent's
|
|
44
|
+
const trustProxyDefaultSymbol = "@@symbol:trust_proxy_default";
|
|
45
|
+
|
|
40
46
|
const workers = [];
|
|
41
47
|
let taskKey = 0;
|
|
42
48
|
const workerTasks = new NullObject();
|
|
@@ -101,9 +107,69 @@ class Application extends Router {
|
|
|
101
107
|
this.ssl = settings.uwsOptions.key_file_name && settings.uwsOptions.cert_file_name;
|
|
102
108
|
this.cache = new NullObject();
|
|
103
109
|
this.engines = { __proto__: null };
|
|
104
|
-
|
|
105
|
-
|
|
110
|
+
// a null prototype, as express gives app.locals, so a local named like an Object method
|
|
111
|
+
// is just a local
|
|
112
|
+
this.locals = Object.create(null);
|
|
113
|
+
this.locals.settings = this.settings;
|
|
114
|
+
// each app gets its own request/response prototype layer, so extending app.request cannot
|
|
115
|
+
// leak into another app; a mounted sub-app re-parents its layer onto the parent's below.
|
|
116
|
+
// The constructors are written out: the implicit derived one spreads its arguments, which
|
|
117
|
+
// was an allocation on every request
|
|
118
|
+
this._request = class extends Request {
|
|
119
|
+
/**
|
|
120
|
+
* @param {any} req
|
|
121
|
+
* @param {any} res
|
|
122
|
+
* @param {any} app
|
|
123
|
+
*/
|
|
124
|
+
constructor(req, res, app) {
|
|
125
|
+
super(req, res, app);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
this._response = class extends Response {
|
|
129
|
+
/**
|
|
130
|
+
* @param {any} res
|
|
131
|
+
* @param {any} req
|
|
132
|
+
* @param {any} app
|
|
133
|
+
*/
|
|
134
|
+
constructor(res, req, app) {
|
|
135
|
+
super(res, req, app);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Node counts an explicit writeHead as the head gone out; remembered here so the
|
|
140
|
+
* automatic OPTIONS reply can refuse to add headers after it, as express's does.
|
|
141
|
+
*
|
|
142
|
+
* @param {number} statusCode
|
|
143
|
+
* @param {string|Record<string, any>} [statusMessage]
|
|
144
|
+
* @param {Record<string, any>} [headers]
|
|
145
|
+
* @returns {this}
|
|
146
|
+
*/
|
|
147
|
+
writeHead(statusCode, statusMessage, headers) {
|
|
148
|
+
this._headWritten = true;
|
|
149
|
+
return super.writeHead(statusCode, statusMessage, headers);
|
|
150
|
+
}
|
|
106
151
|
};
|
|
152
|
+
this.request = this._request.prototype;
|
|
153
|
+
this.response = this._response.prototype;
|
|
154
|
+
this.on("mount", (parent) => {
|
|
155
|
+
// the parent's extensions show through, and an override here stays here. Only an
|
|
156
|
+
// application has a layer to hang onto: a plain router mount leaves things alone
|
|
157
|
+
if (parent.request) {
|
|
158
|
+
Object.setPrototypeOf(this.request, parent.request);
|
|
159
|
+
}
|
|
160
|
+
if (parent.response) {
|
|
161
|
+
Object.setPrototypeOf(this.response, parent.response);
|
|
162
|
+
}
|
|
163
|
+
// a "trust proxy" this app never set is inherited from the parent, as express does:
|
|
164
|
+
// the defaults are deleted so get() falls through to the parent's value
|
|
165
|
+
if (
|
|
166
|
+
this.settings[trustProxyDefaultSymbol] === true &&
|
|
167
|
+
typeof parent.settings["trust proxy fn"] === "function"
|
|
168
|
+
) {
|
|
169
|
+
delete this.settings["trust proxy"];
|
|
170
|
+
delete this.settings["trust proxy fn"];
|
|
171
|
+
}
|
|
172
|
+
});
|
|
107
173
|
this.listenCalled = false;
|
|
108
174
|
this.workers = [];
|
|
109
175
|
for (let i = 0; i < settings.threads; i++) {
|
|
@@ -117,6 +183,19 @@ class Application extends Router {
|
|
|
117
183
|
this.listening = false;
|
|
118
184
|
// the host handed to listen(), which is all address() has to go on
|
|
119
185
|
this._listenHost = undefined;
|
|
186
|
+
// the uWS listen socket, and the responses being served right now: close() stops the
|
|
187
|
+
// first and waits for the second, the way node's server.close() does
|
|
188
|
+
this._listenSocket = undefined;
|
|
189
|
+
this._pendingResponses = new Set();
|
|
190
|
+
// on the per-app prototype layer, not per response: the set is the same for every
|
|
191
|
+
// response this app serves, and the per-request write was pure repetition
|
|
192
|
+
/** @type {any} */ (this.response)._pendingIn = this._pendingResponses;
|
|
193
|
+
this._draining = false;
|
|
194
|
+
// read here, at construction, the way express does; an empty NODE_ENV means development,
|
|
195
|
+
// which the ?? in the shared default would miss
|
|
196
|
+
if (typeof this.settings.env === "undefined") {
|
|
197
|
+
this.settings.env = process.env.NODE_ENV || "development";
|
|
198
|
+
}
|
|
120
199
|
for (const key in defaultSettings) {
|
|
121
200
|
if (typeof this.settings[key] === "undefined") {
|
|
122
201
|
if (typeof defaultSettings[key] === "function") {
|
|
@@ -126,6 +205,11 @@ class Application extends Router {
|
|
|
126
205
|
}
|
|
127
206
|
}
|
|
128
207
|
}
|
|
208
|
+
// non-enumerable, so the marker never shows up walking the settings
|
|
209
|
+
Object.defineProperty(this.settings, trustProxyDefaultSymbol, {
|
|
210
|
+
configurable: true,
|
|
211
|
+
value: true
|
|
212
|
+
});
|
|
129
213
|
this.set("view", ViewClass);
|
|
130
214
|
this.set("views", path.resolve("views"));
|
|
131
215
|
}
|
|
@@ -169,7 +253,7 @@ class Application extends Router {
|
|
|
169
253
|
* Reads or writes an application setting. One argument is the getter, and the check is on
|
|
170
254
|
* `arguments.length`, so `set(key, undefined)` still writes. Some keys have a side effect:
|
|
171
255
|
* `trust proxy`, `query parser` and `etag` compile the value into a function kept beside it,
|
|
172
|
-
* `views` becomes an absolute path
|
|
256
|
+
* and `views` becomes an absolute path.
|
|
173
257
|
*
|
|
174
258
|
* @param {string} key setting name
|
|
175
259
|
* @param {*} [value] value to store; omit to read instead
|
|
@@ -181,28 +265,33 @@ class Application extends Router {
|
|
|
181
265
|
}
|
|
182
266
|
if (key === "trust proxy") {
|
|
183
267
|
if (!value) {
|
|
184
|
-
|
|
268
|
+
// compiled, not deleted: an explicit false must shadow a parent's setting when
|
|
269
|
+
// this app is mounted, and a deleted key would read straight through to it
|
|
270
|
+
this.settings["trust proxy fn"] = compileTrust(false);
|
|
185
271
|
} else {
|
|
186
272
|
this.settings["trust proxy fn"] = compileTrust(value);
|
|
187
273
|
}
|
|
274
|
+
// set explicitly, so a mount no longer inherits the parent's
|
|
275
|
+
Object.defineProperty(this.settings, trustProxyDefaultSymbol, {
|
|
276
|
+
configurable: true,
|
|
277
|
+
value: false
|
|
278
|
+
});
|
|
188
279
|
} else if (key === "query parser") {
|
|
189
280
|
if (value === "extended") {
|
|
190
281
|
this.settings["query parser fn"] = fastQueryParse;
|
|
191
|
-
} else if (value === "simple") {
|
|
282
|
+
} else if (value === "simple" || value === true) {
|
|
192
283
|
this.settings["query parser fn"] = querystring.parse;
|
|
193
284
|
} else if (typeof value === "function") {
|
|
194
285
|
this.settings["query parser fn"] = value;
|
|
195
|
-
} else {
|
|
286
|
+
} else if (value === false) {
|
|
196
287
|
this.settings["query parser fn"] = undefined;
|
|
197
|
-
}
|
|
198
|
-
} else if (key === "env") {
|
|
199
|
-
if (value === "production") {
|
|
200
|
-
this.settings["view cache"] = true;
|
|
201
288
|
} else {
|
|
202
|
-
|
|
289
|
+
// express's wording, which applications match on
|
|
290
|
+
throw new TypeError("unknown value for query parser function: " + value);
|
|
203
291
|
}
|
|
204
292
|
} else if (key === "views") {
|
|
205
|
-
|
|
293
|
+
// a list of directories is searched in order by View.lookup, each resolved here once
|
|
294
|
+
this.settings[key] = Array.isArray(value) ? value.map((dir) => path.resolve(dir)) : path.resolve(value);
|
|
206
295
|
return this;
|
|
207
296
|
} else if (key === "etag") {
|
|
208
297
|
if (typeof value === "function") {
|
|
@@ -220,7 +309,8 @@ class Application extends Router {
|
|
|
220
309
|
delete this.settings["etag fn"];
|
|
221
310
|
break;
|
|
222
311
|
default:
|
|
223
|
-
|
|
312
|
+
// express's wording, which applications match on
|
|
313
|
+
throw new TypeError("unknown value for etag function: " + value);
|
|
224
314
|
}
|
|
225
315
|
}
|
|
226
316
|
}
|
|
@@ -268,6 +358,25 @@ class Application extends Router {
|
|
|
268
358
|
return !this.settings[key];
|
|
269
359
|
}
|
|
270
360
|
|
|
361
|
+
/**
|
|
362
|
+
* Router's handleRequest plus the bookkeeping a graceful close() needs: every live response
|
|
363
|
+
* is held in a set until it finishes, so close() knows when the last one is done. Native
|
|
364
|
+
* routes and the catch-all both come through here, since both call it on the app.
|
|
365
|
+
*
|
|
366
|
+
* @param {any} res uWS response
|
|
367
|
+
* @param {any} req uWS request, readable only during this call
|
|
368
|
+
* @returns {any} the request, with the response reachable as request.res
|
|
369
|
+
*/
|
|
370
|
+
handleRequest(res, req) {
|
|
371
|
+
const request = super.handleRequest(res, req);
|
|
372
|
+
// removal rides the close listener the Response constructor already has, since a second
|
|
373
|
+
// once() per request measured a tenth of a microsecond on the hot path.
|
|
374
|
+
// An aborted response only flips its flags without emitting 'close', which is why
|
|
375
|
+
// close()'s drain also sweeps the set by those flags instead of trusting this alone
|
|
376
|
+
this._pendingResponses.add(request.res);
|
|
377
|
+
return request;
|
|
378
|
+
}
|
|
379
|
+
|
|
271
380
|
/**
|
|
272
381
|
* Registers the catch-all uWS handler, which is what serves every request that no optimized
|
|
273
382
|
* route took natively. It walks this app's own chain and, when nothing in it answered, decides
|
|
@@ -275,11 +384,22 @@ class Application extends Router {
|
|
|
275
384
|
*/
|
|
276
385
|
_createRequestHandler() {
|
|
277
386
|
this.uwsApp.any("/*", async (res, req) => {
|
|
278
|
-
const
|
|
387
|
+
const request = this.handleRequest(res, req);
|
|
388
|
+
const response = request.res;
|
|
279
389
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
390
|
+
try {
|
|
391
|
+
const matchedRoute = await this._routeRequest(request, response);
|
|
392
|
+
if (!matchedRoute && !response.headersSent && !response.aborted) {
|
|
393
|
+
this._endUnmatched(request, response);
|
|
394
|
+
}
|
|
395
|
+
} catch (err) {
|
|
396
|
+
// an internal throw answers 500 as express's final handler would, instead of
|
|
397
|
+
// dying as an unhandled rejection
|
|
398
|
+
if (response.aborted || response.finished) {
|
|
399
|
+
console.error(err);
|
|
400
|
+
} else {
|
|
401
|
+
this._handleError(err, null, request, response);
|
|
402
|
+
}
|
|
283
403
|
}
|
|
284
404
|
});
|
|
285
405
|
}
|
|
@@ -295,21 +415,27 @@ class Application extends Router {
|
|
|
295
415
|
*
|
|
296
416
|
* @param {number|string} [port] port, or a unix socket path; 0 picks a free port
|
|
297
417
|
* @param {string} [host] interface to bind; every interface when omitted
|
|
418
|
+
* @param {number} [backlog] accepted for node's signature; uWS sizes its own queue
|
|
298
419
|
* @param {(err?: Error) => void} [callback] called once bound, or with the bind error
|
|
299
420
|
* @returns {this} the app, which doubles as the server handle
|
|
300
421
|
*/
|
|
301
|
-
listen(port, host, callback) {
|
|
422
|
+
listen(port, host, backlog, callback) {
|
|
302
423
|
this._compileOptimizedRoutes();
|
|
303
424
|
this._createRequestHandler();
|
|
304
|
-
//
|
|
305
|
-
if (
|
|
425
|
+
// node's shapes: (cb), (port, cb), (port, host, cb) and (port, host, backlog, cb)
|
|
426
|
+
if (typeof port === "function") {
|
|
306
427
|
callback = port;
|
|
307
428
|
port = 0;
|
|
308
|
-
}
|
|
309
|
-
// support listen(port, callback)
|
|
310
|
-
if (typeof host === "function") {
|
|
429
|
+
} else if (typeof host === "function") {
|
|
311
430
|
callback = host;
|
|
312
431
|
host = undefined;
|
|
432
|
+
} else if (typeof backlog === "function") {
|
|
433
|
+
callback = backlog;
|
|
434
|
+
}
|
|
435
|
+
// bare listen() and listen(undefined, cb) bind an OS-assigned port, as node does; left
|
|
436
|
+
// undefined the port fell through to the unix-socket branch below
|
|
437
|
+
if (port == null) {
|
|
438
|
+
port = 0;
|
|
313
439
|
}
|
|
314
440
|
// uWS runs this handler from inside its own listen(), so everything it hands back to the
|
|
315
441
|
// caller is deferred a tick. Express binds synchronously too but reports through events,
|
|
@@ -337,6 +463,8 @@ class Application extends Router {
|
|
|
337
463
|
this.port = uWS.us_socket_local_port(socket);
|
|
338
464
|
this.listening = true;
|
|
339
465
|
this._listenHost = host;
|
|
466
|
+
// kept so close() can stop accepting without dropping what is in flight
|
|
467
|
+
this._listenSocket = socket;
|
|
340
468
|
process.nextTick(() => {
|
|
341
469
|
// `this` is the app, which is what listen() returns here. Express binds it to the
|
|
342
470
|
// http.Server, which is what listen() returns there, so
|
|
@@ -451,20 +579,21 @@ class Application extends Router {
|
|
|
451
579
|
// render exists to hand the result somewhere, so there is always a callback by this point:
|
|
452
580
|
// either the third argument or the second one, shuffled above
|
|
453
581
|
const done = /** @type {(err: Error|null, html?: string) => void} */ (callback);
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
}
|
|
582
|
+
// express's order, least specific first: app.locals, then res.locals riding in as _locals,
|
|
583
|
+
// and what was passed to this call wins over both
|
|
584
|
+
const opts = options || new NullObject();
|
|
585
|
+
options = new NullObject();
|
|
459
586
|
for (const key in this.locals) {
|
|
460
587
|
options[key] = this.locals[key];
|
|
461
588
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
options[key] = options._locals[key];
|
|
589
|
+
if (opts._locals) {
|
|
590
|
+
for (const key in opts._locals) {
|
|
591
|
+
options[key] = opts._locals[key];
|
|
466
592
|
}
|
|
467
593
|
}
|
|
594
|
+
for (const key in opts) {
|
|
595
|
+
options[key] = opts[key];
|
|
596
|
+
}
|
|
468
597
|
|
|
469
598
|
if (options.cache == null) {
|
|
470
599
|
options.cache = this.enabled("view cache");
|
|
@@ -511,7 +640,13 @@ class Application extends Router {
|
|
|
511
640
|
}
|
|
512
641
|
|
|
513
642
|
/**
|
|
514
|
-
* Stops
|
|
643
|
+
* Stops accepting connections, lets in-flight requests finish, then emits 'close'.
|
|
644
|
+
*
|
|
645
|
+
* Node's server.close(), which Express hands back from listen(), only closes the listen
|
|
646
|
+
* socket and waits for what is being served; uWS's close() forcefully terminates every
|
|
647
|
+
* connection, so calling it first aborted whatever a graceful shutdown was waiting for.
|
|
648
|
+
* It still runs, but only once the last pending response is done, to drop the idle
|
|
649
|
+
* keep-alive connections nothing else would close.
|
|
515
650
|
*
|
|
516
651
|
* The callback is the first 'close' listener, so it runs before any added afterwards. Closing
|
|
517
652
|
* a server that was not listening still calls back, with an ERR_SERVER_NOT_RUNNING error, the
|
|
@@ -522,9 +657,6 @@ class Application extends Router {
|
|
|
522
657
|
*/
|
|
523
658
|
close(callback) {
|
|
524
659
|
const wasListening = this.listening;
|
|
525
|
-
if (this.listenCalled && wasListening) {
|
|
526
|
-
this.uwsApp.close();
|
|
527
|
-
}
|
|
528
660
|
this.listening = false;
|
|
529
661
|
// in Express the close callback is nothing more than the first 'close' listener, and a
|
|
530
662
|
// server that was not running still gets called back, with an error
|
|
@@ -539,7 +671,41 @@ class Application extends Router {
|
|
|
539
671
|
callback(err);
|
|
540
672
|
});
|
|
541
673
|
}
|
|
542
|
-
|
|
674
|
+
if (!this.listenCalled || !wasListening) {
|
|
675
|
+
// a close while a drain is underway does not emit again: the pending drain's single
|
|
676
|
+
// 'close' serves both calls, which is what node does too
|
|
677
|
+
if (!this._draining) {
|
|
678
|
+
process.nextTick(() => this.emit("close"));
|
|
679
|
+
}
|
|
680
|
+
return this;
|
|
681
|
+
}
|
|
682
|
+
if (this._listenSocket) {
|
|
683
|
+
uWS.us_listen_socket_close(this._listenSocket);
|
|
684
|
+
this._listenSocket = undefined;
|
|
685
|
+
}
|
|
686
|
+
this._draining = true;
|
|
687
|
+
const finish = () => {
|
|
688
|
+
this._draining = false;
|
|
689
|
+
this.uwsApp.close();
|
|
690
|
+
this.emit("close");
|
|
691
|
+
};
|
|
692
|
+
if (this._pendingResponses.size === 0) {
|
|
693
|
+
process.nextTick(finish);
|
|
694
|
+
return this;
|
|
695
|
+
}
|
|
696
|
+
// a finished response emits 'close' and removes itself; an aborted one only flips its
|
|
697
|
+
// flags, so the drain sweeps by them. The timer also keeps the loop alive until done.
|
|
698
|
+
const sweep = setInterval(() => {
|
|
699
|
+
for (const response of this._pendingResponses) {
|
|
700
|
+
if (response.finished || response.aborted) {
|
|
701
|
+
this._pendingResponses.delete(response);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (this._pendingResponses.size === 0) {
|
|
705
|
+
clearInterval(sweep);
|
|
706
|
+
finish();
|
|
707
|
+
}
|
|
708
|
+
}, 10);
|
|
543
709
|
return this;
|
|
544
710
|
}
|
|
545
711
|
}
|
package/src/cli.js
CHANGED
|
@@ -51,9 +51,9 @@ const DIFFERENCES = [
|
|
|
51
51
|
'A body sent with GET or DELETE is not read unless you add the method: app.set("body methods", [...]).'
|
|
52
52
|
],
|
|
53
53
|
[
|
|
54
|
-
"case sensitive routing
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
"case sensitive routing matches Express: insensitive by default",
|
|
55
|
+
"/Users and /users are the same route, as in Express 5. A request in the registered case is still\n" +
|
|
56
|
+
'answered by the native router; set app.set("case sensitive routing", true) to make case matter.'
|
|
57
57
|
],
|
|
58
58
|
[
|
|
59
59
|
"x-powered-by is off by default",
|
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
|
package/src/index.js
CHANGED
|
@@ -21,6 +21,7 @@ const uWS = require("uWebSockets.js");
|
|
|
21
21
|
const uWSAny = /** @type {any} */ (uWS);
|
|
22
22
|
const Application = require("./application.js");
|
|
23
23
|
const Router = require("./router.js");
|
|
24
|
+
const Route = require("./route.js");
|
|
24
25
|
const middlewares = require("./middlewares.js");
|
|
25
26
|
const Request = require("./request.js");
|
|
26
27
|
const Response = require("./response.js");
|
|
@@ -41,6 +42,7 @@ try {
|
|
|
41
42
|
/**
|
|
42
43
|
* @type {typeof Application & {
|
|
43
44
|
* Router: Function,
|
|
45
|
+
* Route: typeof Route,
|
|
44
46
|
* request: object,
|
|
45
47
|
* response: object,
|
|
46
48
|
* application: object,
|
|
@@ -59,6 +61,9 @@ module.exports.Router = function (options) {
|
|
|
59
61
|
return new Router(options)._asCallable();
|
|
60
62
|
};
|
|
61
63
|
|
|
64
|
+
// express exports it, and code that builds a route by hand rather than through a router uses it
|
|
65
|
+
module.exports.Route = Route;
|
|
66
|
+
|
|
62
67
|
module.exports.request = Request.prototype;
|
|
63
68
|
module.exports.response = Response.prototype;
|
|
64
69
|
// the third of the trio: adding a method here adds it to every app, the same as express.application
|