fulmine.js 5.19.1 → 5.19.3
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 +3 -1
- package/package.json +2 -1
- package/src/adopt.js +20 -26
- package/src/application.js +46 -45
- package/src/cli.js +28 -34
- package/src/cluster.js +16 -25
- package/src/compression.js +39 -51
- package/src/declarative.js +586 -538
- package/src/hot-settings.js +80 -0
- package/src/index.js +11 -17
- package/src/lazy-readable.js +129 -0
- package/src/lazy-writable.js +97 -0
- package/src/middlewares.js +61 -77
- package/src/nest.js +19 -34
- package/src/node-shim.js +11 -13
- package/src/optimizer.js +598 -0
- package/src/parse-query.js +3 -3
- package/src/request-utils.js +306 -0
- package/src/request.js +101 -513
- package/src/response-utils.js +88 -0
- package/src/response.js +100 -443
- package/src/route.js +4 -5
- package/src/router-utils.js +950 -0
- package/src/router.js +126 -2148
- package/src/server-shape.js +26 -41
- package/src/server-timing.js +16 -29
- package/src/socket.js +208 -0
- package/src/testing.js +39 -42
- package/src/usage.js +16 -21
- package/src/utils.js +49 -59
- package/src/verify.js +18 -28
- package/src/view.js +5 -7
- package/src/walk.js +580 -0
- package/src/websocket.js +19 -20
- package/src/work.js +21 -27
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ See [Migrating](#migrating) for what it handles and what it deliberately does no
|
|
|
32
32
|
|
|
33
33
|
[](https://www.npmjs.com/package/fulmine.js)
|
|
34
34
|
[](https://nodejs.org)
|
|
35
|
-
[](https://www.http-arena.com/#tuned=0)
|
|
35
|
+
[](https://www.http-arena.com/#tuned=0)
|
|
36
36
|
[](https://coveralls.io/github/nigrosimone/fulmine.js?branch=main)
|
|
37
37
|
[](https://github.com/nigrosimone/fulmine.js/actions/workflows/codeql.yml)
|
|
38
38
|
[](https://scorecard.dev/viewer/?uri=github.com/nigrosimone/fulmine.js)
|
|
@@ -624,6 +624,8 @@ app.listen(3000, () => console.log(`worker ${process.pid} listening`));
|
|
|
624
624
|
|
|
625
625
|
Anything held per process is now held per worker: an in-memory cache, a rate-limit counter, a session store or a `Map` of connected sockets is not shared, and needs Redis or something like it to be. `app.close()` in the primary stops the workers, and a `SIGTERM` or `SIGINT` that reaches only the primary, which is what a container sends, is passed on to them. Runnable: [`examples/cluster.js`](./examples/cluster.js).
|
|
626
626
|
|
|
627
|
+
10. `app.set("connection headers", false)` stops `Connection: keep-alive` and `Keep-Alive: timeout=10` going out on every response. Express sends both, so Fulmine sends both by default. An HTTP/1.1 connection stays open without being told, so to an HTTP/1.1 client the two headers say nothing it does not know already, and they cost 46 bytes and two header writes per response. A request that asked for `Connection: close` still gets `Connection: close`, and the connection is closed. Turn it off for an API behind a proxy or serving HTTP/1.1 clients; keep the default where a client or a proxy relies on the header to keep the connection open. Worth 2% to 3.5% here on a route that is not compiled, plus the bytes.
|
|
628
|
+
|
|
627
629
|
## WebSockets
|
|
628
630
|
|
|
629
631
|
`app.ws()` registers a WebSocket route, served by µWS itself. The upgrade never reaches node, so `server.on("upgrade")` and the libraries built on it have nothing to hear; this is the replacement.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fulmine.js",
|
|
3
|
-
"version": "5.19.
|
|
3
|
+
"version": "5.19.3",
|
|
4
4
|
"description": "Drop-in Express 5 replacement on uWebSockets.js. Your existing middleware keeps working.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -149,6 +149,7 @@
|
|
|
149
149
|
"eslint": "^10.8.0",
|
|
150
150
|
"eslint-config-prettier": "^10.1.8",
|
|
151
151
|
"eslint-plugin-jsdoc": "^64.1.0",
|
|
152
|
+
"eslint-plugin-sonarjs": "^4.2.0",
|
|
152
153
|
"etag": "^1.8.1",
|
|
153
154
|
"eventsource": "^5.0.0",
|
|
154
155
|
"exit-hook": "^5.1.0",
|
package/src/adopt.js
CHANGED
|
@@ -17,15 +17,14 @@ limitations under the License.
|
|
|
17
17
|
// The two commands that edit a config file rather than source: `npx fulmine.js override` and
|
|
18
18
|
// `npx fulmine.js angular`.
|
|
19
19
|
//
|
|
20
|
-
// `migrate` rewrites `require("express")` in your own files
|
|
21
|
-
//
|
|
20
|
+
// `migrate` rewrites `require("express")` in your own files. The two cases it cannot reach are
|
|
21
|
+
// both a line in a JSON file:
|
|
22
22
|
//
|
|
23
|
-
// override A framework built on Express
|
|
24
|
-
//
|
|
25
|
-
//
|
|
23
|
+
// override A framework built on Express requires it in its own code, so there is no specifier
|
|
24
|
+
// to rewrite. Every package manager can answer `express` with this package instead,
|
|
25
|
+
// and each one spells it differently.
|
|
26
26
|
// angular An Angular server bundle is built with esbuild, which inlines every dependency and
|
|
27
|
-
// cannot load
|
|
28
|
-
// nothing in the error message it fails with says so.
|
|
27
|
+
// cannot load uWS's native binary. Two names in `externalDependencies` fix it.
|
|
29
28
|
|
|
30
29
|
"use strict";
|
|
31
30
|
|
|
@@ -72,9 +71,9 @@ function indentOf(source) {
|
|
|
72
71
|
/**
|
|
73
72
|
* Reads a JSON file, or explains why it could not be read rather than throwing a parser error.
|
|
74
73
|
*
|
|
75
|
-
* The read is attempted rather than guarded by an existence check
|
|
76
|
-
*
|
|
77
|
-
*
|
|
74
|
+
* The read is attempted rather than guarded by an existence check: both commands write the file
|
|
75
|
+
* they read, and a check followed by a write to the same path is a race. `code` is what a caller
|
|
76
|
+
* names the missing file by.
|
|
78
77
|
*
|
|
79
78
|
* @param {string} file
|
|
80
79
|
* @returns {{data: any, source: string}|{error: string, code: string|undefined}}
|
|
@@ -121,7 +120,7 @@ function detectManager(dir, pkg) {
|
|
|
121
120
|
/**
|
|
122
121
|
* Reads a nested key, and answers undefined rather than throwing on a missing level.
|
|
123
122
|
*
|
|
124
|
-
* @param {any} object
|
|
123
|
+
* @param {any} object parsed JSON, so its shape is whatever the file held
|
|
125
124
|
* @param {string[]} keys
|
|
126
125
|
* @returns {any}
|
|
127
126
|
*/
|
|
@@ -137,9 +136,9 @@ function readPath(object, keys) {
|
|
|
137
136
|
/**
|
|
138
137
|
* Writes a nested key, making the levels above it as it goes.
|
|
139
138
|
*
|
|
140
|
-
* @param {any} object
|
|
139
|
+
* @param {any} object parsed JSON, so its shape is whatever the file held
|
|
141
140
|
* @param {string[]} keys
|
|
142
|
-
* @param {any} value
|
|
141
|
+
* @param {any} value whatever belongs at that key
|
|
143
142
|
* @returns {void}
|
|
144
143
|
*/
|
|
145
144
|
function writePath(object, keys, value) {
|
|
@@ -155,9 +154,7 @@ function writePath(object, keys, value) {
|
|
|
155
154
|
* npx fulmine.js override [dir] [--dry-run]
|
|
156
155
|
*
|
|
157
156
|
* Puts the substitution in package.json where this project's package manager reads it, and says
|
|
158
|
-
* what to run next. It
|
|
159
|
-
* node_modules, and that is not something a command should do to somebody's working tree without
|
|
160
|
-
* being watched.
|
|
157
|
+
* what to run next. It does not run the install itself: that throws away node_modules.
|
|
161
158
|
*
|
|
162
159
|
* @param {string[]} argv everything after the command name
|
|
163
160
|
* @returns {number} exit code
|
|
@@ -235,9 +232,8 @@ function override(argv) {
|
|
|
235
232
|
/**
|
|
236
233
|
* Every build target in an angular.json that produces a server bundle.
|
|
237
234
|
*
|
|
238
|
-
* A browser-only build
|
|
239
|
-
*
|
|
240
|
-
* @angular/ssr` writes.
|
|
235
|
+
* A browser-only build never loads uWS. What marks a server build is `ssr`, `server` or
|
|
236
|
+
* `outputMode` in its options, which is what `ng add @angular/ssr` writes.
|
|
241
237
|
*
|
|
242
238
|
* @param {any} config the parsed angular.json
|
|
243
239
|
* @returns {{name: string, options: any}[]}
|
|
@@ -257,8 +253,8 @@ function serverBuilds(config) {
|
|
|
257
253
|
/**
|
|
258
254
|
* npx fulmine.js angular [dir] [--dry-run]
|
|
259
255
|
*
|
|
260
|
-
* Declares this package and
|
|
261
|
-
*
|
|
256
|
+
* Declares this package and uWebSockets.js external in every server build, which stops esbuild
|
|
257
|
+
* trying to inline a native binary it cannot read.
|
|
262
258
|
*
|
|
263
259
|
* @param {string[]} argv everything after the command name
|
|
264
260
|
* @returns {number} exit code
|
|
@@ -267,11 +263,9 @@ function angular(argv) {
|
|
|
267
263
|
const dryRun = argv.includes("--dry-run");
|
|
268
264
|
const given = path.resolve(argv.find((arg) => !arg.startsWith("--")) ?? ".");
|
|
269
265
|
|
|
270
|
-
// The argument is either the file or the directory holding it,
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
// round because this function goes on to write the file it read, and a check on a path followed
|
|
274
|
-
// by a write to the same path is the shape of a race whatever the odds of losing it.
|
|
266
|
+
// The argument is either the file or the directory holding it, told apart by reading it and not
|
|
267
|
+
// by a stat: a directory reads EISDIR, a missing path ENOENT, and in both cases the file to
|
|
268
|
+
// look for is angular.json inside it. A stat followed by a write to the same path is a race.
|
|
275
269
|
let file = given;
|
|
276
270
|
let read = readJson(file);
|
|
277
271
|
if ("error" in read && (read.code === "EISDIR" || read.code === "ENOENT")) {
|
package/src/application.js
CHANGED
|
@@ -103,6 +103,13 @@ class Application extends Router {
|
|
|
103
103
|
*/
|
|
104
104
|
_isApplication = true;
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Whether express.testing already compiled the routes of this app. Written there and nowhere
|
|
108
|
+
* else: a second compilation would register everything with uWS twice. See src/testing.js.
|
|
109
|
+
* @type {boolean|undefined}
|
|
110
|
+
*/
|
|
111
|
+
_testingCompiled;
|
|
112
|
+
|
|
106
113
|
/**
|
|
107
114
|
* @param {object} [settings] the options express() takes. uwsOptions goes to uWS and decides
|
|
108
115
|
* between an HTTP, an HTTPS and an HTTP/3 server; threads sizes the file-reading pool, and 0
|
|
@@ -154,11 +161,13 @@ class Application extends Router {
|
|
|
154
161
|
// was an allocation on every request
|
|
155
162
|
this._request = class extends Request {
|
|
156
163
|
/**
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
* @param {any}
|
|
160
|
-
* @param {any}
|
|
161
|
-
* @param {any}
|
|
164
|
+
* The base constructor's arguments, written out rather than spread. See Request.
|
|
165
|
+
*
|
|
166
|
+
* @param {any} req uWS request
|
|
167
|
+
* @param {any} res uWS response
|
|
168
|
+
* @param {any} app the application this request arrived at
|
|
169
|
+
* @param {any} [preset] a literal registration's constants
|
|
170
|
+
* @param {any} [skipHolder] where a granted header skip lives
|
|
162
171
|
*/
|
|
163
172
|
constructor(req, res, app, preset, skipHolder) {
|
|
164
173
|
super(req, res, app, preset, skipHolder);
|
|
@@ -166,9 +175,11 @@ class Application extends Router {
|
|
|
166
175
|
};
|
|
167
176
|
this._response = class extends Response {
|
|
168
177
|
/**
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* @param {any}
|
|
178
|
+
* The base constructor's arguments, written out rather than spread. See Response.
|
|
179
|
+
*
|
|
180
|
+
* @param {any} res uWS response
|
|
181
|
+
* @param {any} req the Request, already built
|
|
182
|
+
* @param {any} app the application this request arrived at
|
|
172
183
|
*/
|
|
173
184
|
constructor(res, req, app) {
|
|
174
185
|
super(res, req, app);
|
|
@@ -306,11 +317,10 @@ class Application extends Router {
|
|
|
306
317
|
|
|
307
318
|
/**
|
|
308
319
|
* A small file through the worker pool, with two things on top: concurrent asks for the same
|
|
309
|
-
* path share one read, and the bytes of an unchanged file come from a bounded cache,
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
* `app.set("file cache", false)` turns the cache off; the shared read stays.
|
|
320
|
+
* path share one read, and the bytes of an unchanged file come from a bounded cache, validated
|
|
321
|
+
* against the stat the caller already paid for, so a touched file is re-read. A hit completes
|
|
322
|
+
* on a macrotask, which is when a worker's answer would have arrived.
|
|
323
|
+
* `app.set("file cache", false)` turns the cache off, the shared read stays.
|
|
314
324
|
*
|
|
315
325
|
* @param {string} fullpath
|
|
316
326
|
* @param {import("fs").Stats} stat
|
|
@@ -407,12 +417,11 @@ class Application extends Router {
|
|
|
407
417
|
}
|
|
408
418
|
value = value == null ? undefined : value.map((m) => m.toUpperCase());
|
|
409
419
|
} else if (key === "etag") {
|
|
410
|
-
// The skips are not taken back here. They used to be, because send consults freshness
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
// see router.js:1615: that is a different question, about code the analysis never saw.
|
|
420
|
+
// The skips are not taken back here. They used to be, because send consults freshness,
|
|
421
|
+
// but that branch reads if-none-match, if-modified-since and cache-control by name
|
|
422
|
+
// whatever this setting says, and req.fresh reads nothing else off the request.
|
|
423
|
+
// Registering a route after listen still takes them back: that is a different
|
|
424
|
+
// question, about code the analysis never saw
|
|
416
425
|
if (typeof value === "function") {
|
|
417
426
|
this._settings["etag fn"] = value;
|
|
418
427
|
} else {
|
|
@@ -551,9 +560,8 @@ class Application extends Router {
|
|
|
551
560
|
*
|
|
552
561
|
* Returns the app and not an `http.Server`, since there is no node server underneath. The app
|
|
553
562
|
* carries `address()`, `close()`, `listening` and the 'listening' and 'close' events; anything
|
|
554
|
-
* needing a real server, socket.io being the usual case, wants `app.uwsApp`.
|
|
555
|
-
*
|
|
556
|
-
* socket.
|
|
563
|
+
* needing a real server, socket.io being the usual case, wants `app.uwsApp`. A path instead of
|
|
564
|
+
* a port is a unix socket.
|
|
557
565
|
*
|
|
558
566
|
* @param {number|string} [port] port, or a unix socket path; 0 picks a free port
|
|
559
567
|
* @param {string} [host] interface to bind; every interface when omitted
|
|
@@ -563,14 +571,12 @@ class Application extends Router {
|
|
|
563
571
|
*/
|
|
564
572
|
listen(port, host, backlog, callback) {
|
|
565
573
|
// With { cluster } the primary has nothing to bind. Each worker binds this same port with
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
//
|
|
569
|
-
// so it runs once per worker rather than once.
|
|
574
|
+
// uWS's shared flag, SO_REUSEPORT, so the kernel hands each connection to one of them and
|
|
575
|
+
// the primary only forks and replaces a worker that dies. Everything below this runs in the
|
|
576
|
+
// workers, listen callback included, so once per worker rather than once.
|
|
570
577
|
//
|
|
571
|
-
// The test is the process and not this app: a second app on a TLS port,
|
|
572
|
-
//
|
|
573
|
-
// worker would fail on it.
|
|
578
|
+
// The test is the process and not this app: a second app on a TLS port, without a cluster
|
|
579
|
+
// setting of its own, would take that port here exclusively and every worker would fail
|
|
574
580
|
if (cluster.isPrimary && isSupervising()) {
|
|
575
581
|
if (this._clusterWorkers > 0 && !this._clusterHandle) {
|
|
576
582
|
this._clusterHandle = forkWorkers(this._clusterWorkers);
|
|
@@ -758,10 +764,8 @@ class Application extends Router {
|
|
|
758
764
|
* `res.render()` is the one that responds.
|
|
759
765
|
*
|
|
760
766
|
* `app.locals` and `options._locals` are merged into the options, in that order, so a
|
|
761
|
-
* per-request local wins
|
|
762
|
-
*
|
|
763
|
-
*
|
|
764
|
-
* A function in the options position is taken as the callback.
|
|
767
|
+
* per-request local wins. Caching follows the "view cache" setting unless `options.cache` says
|
|
768
|
+
* otherwise. A function in the options position is taken as the callback.
|
|
765
769
|
*
|
|
766
770
|
* @param {string} name view name, resolved against the "views" setting
|
|
767
771
|
* @param {Record<string, any>} [options] locals for the view
|
|
@@ -844,15 +848,13 @@ class Application extends Router {
|
|
|
844
848
|
/**
|
|
845
849
|
* Stops accepting connections, lets in-flight requests finish, then emits 'close'.
|
|
846
850
|
*
|
|
847
|
-
* Node's server.close()
|
|
848
|
-
*
|
|
849
|
-
*
|
|
850
|
-
*
|
|
851
|
-
* keep-alive connections nothing else would close.
|
|
851
|
+
* Node's server.close() only closes the listen socket and waits for what is being served; uWS's
|
|
852
|
+
* close() terminates every connection, so calling it first aborted whatever a graceful shutdown
|
|
853
|
+
* was waiting for. It still runs, but only once the last pending response is done, to drop the
|
|
854
|
+
* idle keep-alive connections nothing else would close.
|
|
852
855
|
*
|
|
853
|
-
* The callback is the first 'close' listener
|
|
854
|
-
*
|
|
855
|
-
* way node does.
|
|
856
|
+
* The callback is the first 'close' listener. Closing a server that was not listening still
|
|
857
|
+
* calls back with ERR_SERVER_NOT_RUNNING, the way node does.
|
|
856
858
|
*
|
|
857
859
|
* @param {(err?: Error) => void} [callback] called once closed
|
|
858
860
|
* @returns {this} the app, for chaining
|
|
@@ -932,10 +934,9 @@ class Application extends Router {
|
|
|
932
934
|
// carries. Middleware that takes a whole app and calls it, vhost being the one everybody meets, was
|
|
933
935
|
// given something it could not call.
|
|
934
936
|
//
|
|
935
|
-
//
|
|
936
|
-
//
|
|
937
|
-
//
|
|
938
|
-
// every call timed out. src/node-shim.js is what closes that hole, and it is why this is safe now.
|
|
937
|
+
// Tried once before and reverted the same day, because a callable app broke supertest: `request(app)`
|
|
938
|
+
// reads `typeof app === "function"` and wraps what it finds in http.createServer, and there was
|
|
939
|
+
// nothing underneath that could serve node's IncomingMessage. src/node-shim.js closes that hole.
|
|
939
940
|
module.exports = function (options) {
|
|
940
941
|
return new Application(options)._asCallable();
|
|
941
942
|
};
|
package/src/cli.js
CHANGED
|
@@ -18,26 +18,25 @@ limitations under the License.
|
|
|
18
18
|
// npx fulmine migrate [dir]
|
|
19
19
|
//
|
|
20
20
|
// Rewrites the module specifier and nothing else. An Express 5 app is a Fulmine app already, so
|
|
21
|
-
// there is no code to translate
|
|
22
|
-
//
|
|
21
|
+
// there is no code to translate. The short list of things that behave differently is printed at
|
|
22
|
+
// the end, because no rewrite can find those for you.
|
|
23
23
|
//
|
|
24
24
|
// npx fulmine profile [entry]
|
|
25
25
|
//
|
|
26
|
-
// Prints what listen() worked out about each route
|
|
27
|
-
//
|
|
28
|
-
// compiled all the way down to a response written at startup.
|
|
26
|
+
// Prints what listen() worked out about each route: which ones uWS answers on its own, which ones
|
|
27
|
+
// fell back to the ordinary router and why, and which ones were compiled into a response.
|
|
29
28
|
//
|
|
30
29
|
// npx fulmine verify [dir]
|
|
31
30
|
//
|
|
32
31
|
// Whether this machine and this project can run it at all: the node version, the C library, the
|
|
33
|
-
//
|
|
32
|
+
// uWebSockets.js binary, the base image a Dockerfile names. See src/verify.js.
|
|
34
33
|
//
|
|
35
34
|
// npx fulmine override [dir]
|
|
36
35
|
// npx fulmine angular [dir]
|
|
37
36
|
//
|
|
38
|
-
// The two things a project needs that are a line in a JSON file rather than a specifier in a
|
|
39
|
-
// file: the package manager substitution, for a framework that requires express in its own
|
|
40
|
-
// and angular.json's externalDependencies. See src/adopt.js.
|
|
37
|
+
// The two things a project needs that are a line in a JSON file rather than a specifier in a
|
|
38
|
+
// source file: the package manager substitution, for a framework that requires express in its own
|
|
39
|
+
// code, and angular.json's externalDependencies. See src/adopt.js.
|
|
41
40
|
|
|
42
41
|
const fs = require("fs");
|
|
43
42
|
const path = require("path");
|
|
@@ -146,14 +145,12 @@ function collectFiles(dir) {
|
|
|
146
145
|
* A reader for the .ts files of the project being migrated, or null when it has no TypeScript.
|
|
147
146
|
*
|
|
148
147
|
* acorn cannot read TypeScript, and shipping a parser that can would put megabytes into this
|
|
149
|
-
* package for a command most people run once. A TypeScript project already has the compiler, so
|
|
150
|
-
*
|
|
151
|
-
* than having them quietly skipped, which is what happened before they were looked at at all.
|
|
148
|
+
* package for a command most people run once. A TypeScript project already has the compiler, so it
|
|
149
|
+
* is resolved from there. A project without one is told its .ts files were left alone.
|
|
152
150
|
*
|
|
153
|
-
* typescript 7 is the compiler rewritten in Go
|
|
154
|
-
* require("typescript") gives back a version number and nothing else. Its scanner survives
|
|
155
|
-
* ESM-only subpath that require() reads
|
|
156
|
-
* walk and 6 keeps the tree.
|
|
151
|
+
* typescript 7 is the compiler rewritten in Go and publishes no JavaScript parser any more:
|
|
152
|
+
* require("typescript") gives back a version number and nothing else. Its scanner survives on an
|
|
153
|
+
* ESM-only subpath that require() reads, so 7 gets the token walk and 6 keeps the tree.
|
|
157
154
|
*
|
|
158
155
|
* @param {string} target directory being migrated
|
|
159
156
|
* @returns {((source: string, fileName: string, seen?: Set<string>) => {start: number, end: number}[])|null}
|
|
@@ -364,7 +361,7 @@ function findSpecifiers(source, seen) {
|
|
|
364
361
|
* Visits every node. acorn produces plain objects, so the shape is walked rather than dispatched
|
|
365
362
|
* on: a table of node types would have to be kept in step with the parser, and being out of step
|
|
366
363
|
* would mean silently skipping an import.
|
|
367
|
-
* @param {any} node
|
|
364
|
+
* @param {any} node an acorn AST node. acorn ships no useful node types, and every shape here is checked by hand
|
|
368
365
|
* @param {(node: any) => void} visit
|
|
369
366
|
*/
|
|
370
367
|
function walk(node, visit) {
|
|
@@ -453,14 +450,13 @@ function findEntry(given) {
|
|
|
453
450
|
/**
|
|
454
451
|
* Every build of this library the application could load, as the prototype that owns listen().
|
|
455
452
|
*
|
|
456
|
-
* The command runs from its own copy, and the application loads whichever one resolves from its
|
|
457
|
-
*
|
|
458
|
-
* `npx fulmine.js@version`, a
|
|
459
|
-
*
|
|
460
|
-
* to bind the port, and the command then reports that the file built nothing.
|
|
453
|
+
* The command runs from its own copy, and the application loads whichever one resolves from its own
|
|
454
|
+
* directory. That is usually the same file and sometimes not: a global install, an
|
|
455
|
+
* `npx fulmine.js@version`, a hoisted second copy, or `express` pointing here through an override.
|
|
456
|
+
* Patching only this command's copy leaves the application's own listen() to bind the port.
|
|
461
457
|
*
|
|
462
|
-
* An app is a callable, so its own prototype
|
|
463
|
-
*
|
|
458
|
+
* An app is a callable, so its own prototype does not carry the methods: walk up to whichever link
|
|
459
|
+
* owns listen.
|
|
464
460
|
*
|
|
465
461
|
* @param {string} entry
|
|
466
462
|
* @returns {any[]} the prototypes to stub, this command's copy first
|
|
@@ -580,14 +576,13 @@ ${error.stack ?? error}`);
|
|
|
580
576
|
/**
|
|
581
577
|
* Ends the file-reading threads that building an application started.
|
|
582
578
|
*
|
|
583
|
-
* An Application starts one per `threads` in its constructor, and these commands only
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
* then removes it saw "Cannot find module .../src/worker.js" arrive after it had finished.
|
|
579
|
+
* An Application starts one per `threads` in its constructor, and these commands only read what
|
|
580
|
+
* compiling the routes decided. They are unref'd, so leaving them would not hang the process, but
|
|
581
|
+
* they hold the library the application loaded, and this command is often not the whole process.
|
|
582
|
+
* It also stops them outliving the directory they were loaded from, which is how a test that
|
|
583
|
+
* profiles a copy and then removes it saw "Cannot find module .../src/worker.js".
|
|
589
584
|
*
|
|
590
|
-
* Best effort throughout: a build with no workers, or a worker already gone, is not an error
|
|
585
|
+
* Best effort throughout: a build with no workers, or a worker already gone, is not an error.
|
|
591
586
|
*
|
|
592
587
|
* @param {any[]} apps
|
|
593
588
|
* @returns {void}
|
|
@@ -765,8 +760,7 @@ function matchesWanted(full, method, wanted) {
|
|
|
765
760
|
*
|
|
766
761
|
* There is no score here on purpose. A percentage of routes is not a percentage of traffic: an
|
|
767
762
|
* application with a thousand cold routes and one hot one that fell back would score well and
|
|
768
|
-
* serve badly. What is printed
|
|
769
|
-
* printed for the reasons somebody can actually act on.
|
|
763
|
+
* serve badly. What is printed is counted rather than judged.
|
|
770
764
|
*
|
|
771
765
|
* @param {any[]} routes
|
|
772
766
|
* @param {any[]} native
|
|
@@ -815,7 +809,7 @@ function printSummary(routes, native, declarative) {
|
|
|
815
809
|
}
|
|
816
810
|
|
|
817
811
|
/**
|
|
818
|
-
* @param {any} app
|
|
812
|
+
* @param {any} app the application the entry file built
|
|
819
813
|
* @param {boolean} several whether to say which application this is
|
|
820
814
|
*/
|
|
821
815
|
function printProfile(app, several) {
|
package/src/cluster.js
CHANGED
|
@@ -16,16 +16,12 @@ limitations under the License.
|
|
|
16
16
|
|
|
17
17
|
// express({ cluster: "auto" }): one process per core, all on the same port.
|
|
18
18
|
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// the
|
|
23
|
-
// socket on the same port and the kernel picks which one gets each connection. No primary in the
|
|
24
|
-
// path, no handle to pass, nothing serialised between processes.
|
|
19
|
+
// The usual answer is the cluster module, where the primary holds the listening socket and passes
|
|
20
|
+
// each accepted connection to a worker over IPC. uWS does not need that: it binds with the port
|
|
21
|
+
// marked shared, which is SO_REUSEPORT, so every worker has its own listening socket on the same
|
|
22
|
+
// port and the kernel picks who gets each connection. No primary in the path, nothing serialised.
|
|
25
23
|
//
|
|
26
|
-
//
|
|
27
|
-
// process binds exclusive. What was missing is the fork, which every application had to write for
|
|
28
|
-
// itself, and an application that does not write it uses one core.
|
|
24
|
+
// Application#listen already passes the flag. What was missing is the fork.
|
|
29
25
|
|
|
30
26
|
"use strict";
|
|
31
27
|
|
|
@@ -54,8 +50,7 @@ function parallelism() {
|
|
|
54
50
|
* The CPU quota a cgroup puts on this process, in cores, or undefined where there is none.
|
|
55
51
|
*
|
|
56
52
|
* This is the number that matters in a container: os.availableParallelism() reports the machine,
|
|
57
|
-
*
|
|
58
|
-
* processes that fight over two cores. Both cgroup layouts are read, v2 first.
|
|
53
|
+
* so a 2-core pod on a 64-core node would fork 64 processes. Both cgroup layouts are read, v2 first.
|
|
59
54
|
*
|
|
60
55
|
* @param {(file: string) => string} [read] the file reader, for a test that has no cgroup
|
|
61
56
|
* @returns {number|undefined}
|
|
@@ -103,8 +98,8 @@ function availableCores(read = readFile, cores = parallelism) {
|
|
|
103
98
|
* How many workers a `cluster` setting asks for. Zero means the setting is off and the process
|
|
104
99
|
* serves by itself, which is the default.
|
|
105
100
|
*
|
|
106
|
-
* A value nobody can read
|
|
107
|
-
* core in production
|
|
101
|
+
* A value nobody can read throws instead of quietly meaning zero: `cluster: "atuo"` would run on
|
|
102
|
+
* one core in production and say nothing about it.
|
|
108
103
|
*
|
|
109
104
|
* @param {boolean|number|"auto"|undefined} setting
|
|
110
105
|
* @param {number} [cores] counted only when the setting asks for it: every application calls this,
|
|
@@ -124,9 +119,8 @@ function workerCount(setting, cores) {
|
|
|
124
119
|
throw new TypeError(`cluster must be "auto", a boolean or a positive number, not ${JSON.stringify(setting)}`);
|
|
125
120
|
}
|
|
126
121
|
|
|
127
|
-
// Whether this process
|
|
128
|
-
//
|
|
129
|
-
// otherwise bind that one here, exclusively, and every worker would fail on it.
|
|
122
|
+
// Whether this process forked workers and serves nothing itself. About the process, not the app:
|
|
123
|
+
// an entry with a second app on a TLS port would bind that one here and every worker would fail.
|
|
130
124
|
let supervising = false;
|
|
131
125
|
|
|
132
126
|
/**
|
|
@@ -141,9 +135,8 @@ function isSupervising() {
|
|
|
141
135
|
/**
|
|
142
136
|
* Says this process is the primary of a clustered application, before it has forked anything.
|
|
143
137
|
*
|
|
144
|
-
* Written when the application is constructed
|
|
145
|
-
*
|
|
146
|
-
* that port here, exclusively, a line before the fork.
|
|
138
|
+
* Written when the application is constructed, not when it listens: an entry that listens on its
|
|
139
|
+
* TLS port first would take that port here, exclusively, one line before the fork.
|
|
147
140
|
*
|
|
148
141
|
* @returns {void}
|
|
149
142
|
*/
|
|
@@ -154,10 +147,8 @@ function becomeSupervisor() {
|
|
|
154
147
|
/**
|
|
155
148
|
* Forks the workers and keeps that many of them alive.
|
|
156
149
|
*
|
|
157
|
-
* A worker
|
|
158
|
-
*
|
|
159
|
-
* primary alone, which is what a container sends, is passed on rather than leaving the workers
|
|
160
|
-
* running with nobody watching them.
|
|
150
|
+
* A dead worker is replaced and has nothing to rebuild, it binds the shared port again. A signal
|
|
151
|
+
* that reaches only the primary, which is what a container sends, is passed on to the workers.
|
|
161
152
|
*
|
|
162
153
|
* @param {number} count
|
|
163
154
|
* @returns {{stop: () => void}}
|
|
@@ -187,8 +178,8 @@ function forkWorkers(count) {
|
|
|
187
178
|
for (const signal of ["SIGTERM", "SIGINT"]) {
|
|
188
179
|
process.on(signal, () => {
|
|
189
180
|
stop();
|
|
190
|
-
// the primary exits on its own once the last IPC channel closes
|
|
191
|
-
// it does
|
|
181
|
+
// the primary exits on its own once the last IPC channel closes, this only makes sure
|
|
182
|
+
// it does. Unref'd, so it never keeps the process up by itself
|
|
192
183
|
const done = setInterval(() => {
|
|
193
184
|
if (Object.keys(cluster.workers ?? {}).length === 0) {
|
|
194
185
|
clearInterval(done);
|