fulmine.js 5.19.2 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fulmine.js",
3
- "version": "5.19.2",
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, which is all an application needs. The
21
- // two cases it cannot reach are both a line in a JSON file that nobody remembers the shape of:
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 does not require it in your code, it requires it in its
24
- // own, so there is no specifier to rewrite. Every package manager can answer `express`
25
- // with this package instead, for the whole tree, and each one spells it differently.
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 µWS's native binary. Two names in `externalDependencies` fix it, and
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. Both commands go on to write the
76
- * file they read, and a check on a path followed by a write to the same path is the shape of a race
77
- * whatever the odds of losing it. `code` is what a caller names the missing file by.
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 deliberately does not run the install: the reinstall throws away
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 has nothing to declare external: the bundle it makes never loads µWS. What
239
- * marks a server build is `ssr`, `server` or `outputMode` in its options, which is what `ng add
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 µWebSockets.js external in every server build, which is what stops
261
- * esbuild trying to inline a native binary it cannot read.
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, and which one comes out of
271
- // reading it rather than out of a stat: a directory reads as EISDIR and a missing path as
272
- // ENOENT, and in both cases the file to look for is angular.json inside it. Asked this way
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")) {
@@ -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
- * @param {any} req
158
- * @param {any} res
159
- * @param {any} app
160
- * @param {any} [preset]
161
- * @param {any} [skipHolder]
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
- * @param {any} res
170
- * @param {any} req
171
- * @param {any} app
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
- * validated against the stat the caller already paid for, so a touched file is re-read.
311
- * A hit completes on a macrotask, which is when a worker's answer would have arrived; code
312
- * that passed the suites against worker timing keeps passing against this.
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
- // and the skip branch looked like it had not copied the headers for it, but that
412
- // branch reads if-none-match, if-modified-since and cache-control by name whatever
413
- // this setting says, see request.js:527, and req.fresh reads nothing else off the
414
- // request. Registering a route or a middleware after listen still takes them back,
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`. The callback runs
555
- * on the next tick with the bind error, if there was one. A path instead of a port is a unix
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
- // µWS's shared flag, which is SO_REUSEPORT, so the kernel hands each connection to one of
567
- // them and the primary is not in the path at all: it forks, replaces a worker that dies,
568
- // and nothing else. Everything below this runs in the workers, listen callback included,
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, one without a
572
- // cluster setting of its own, would otherwise take that port here, exclusively, and every
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 over an application-wide one. Caching follows the "view cache"
762
- * setting unless `options.cache` says otherwise.
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(), which Express hands back from listen(), only closes the listen
848
- * socket and waits for what is being served; uWS's close() forcefully terminates every
849
- * connection, so calling it first aborted whatever a graceful shutdown was waiting for.
850
- * It still runs, but only once the last pending response is done, to drop the idle
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, so it runs before any added afterwards. Closing
854
- * a server that was not listening still calls back, with an ERR_SERVER_NOT_RUNNING error, the
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
- // This was tried once before and reverted the same day, because a callable app broke supertest:
936
- // `request(app)` reads `typeof app === "function"` and wraps whatever it finds in
937
- // http.createServer, and there was nothing underneath that could serve node's IncomingMessage, so
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: what there is instead is a short list of things that behave
22
- // differently, printed at the end, because no rewrite can find those for you.
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 and normally keeps to itself: which ones µWS
27
- // answers on its own, which ones fell back to the ordinary router and why, and which ones were
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
- // µWebSockets.js binary, the base image a Dockerfile names. See src/verify.js.
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 source
39
- // file: the package manager substitution, for a framework that requires express in its own code,
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
- * it is resolved from there. A project without one is told its .ts files were left alone rather
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, and it publishes no JavaScript parser any more:
154
- * require("typescript") gives back a version number and nothing else. Its scanner survives, on an
155
- * ESM-only subpath that require() reads on every node this package supports, so 7 gets the token
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
- * own directory. That is usually the same file and sometimes is not: a global install, an
458
- * `npx fulmine.js@version`, a workspace that hoisted a second copy, or the `express` name pointing
459
- * here through an override. Patching only this command's copy leaves the application's own listen()
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 is not the one that carries the methods: walk up to
463
- * whichever link owns listen.
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 ever read
584
- * what compiling the routes decided: nothing here serves a file, so nothing here needs a thread.
585
- * They are unref'd, so leaving them would not hang the process, but they are threads holding the
586
- * library the application loaded, and this command is often not the whole process. It also stops
587
- * them outliving the directory they were loaded from, which is how a test that profiles a copy and
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 here.
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 instead is counted rather than judged, and the advice is only
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
- // A node process runs the application on one core, and the other fifteen sit there. The usual
20
- // answer is the cluster module, where the primary holds the listening socket and hands each
21
- // accepted connection to a worker over an IPC channel. µWS does not need that: it can bind with
22
- // the port marked shared, which is SO_REUSEPORT, and then every worker has its own listening
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
- // The flag has been passed for a while, see Application#listen: a worker binds shared and a lone
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
- * not the share of it the orchestrator gave away, so a 2-core pod on a 64-core node would fork 64
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 is a throw rather than a quiet zero: `cluster: "atuo"` running on one
107
- * core in production, with nothing said about it, is the failure this whole thing is against.
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 has forked workers, and so serves nothing itself. An application carries
128
- // the setting, but the answer is about the process: an entry with a second app on a TLS port would
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 and not when it listens, because the order is the
145
- * application's to choose: an entry that listens on its TLS port first would otherwise have taken
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 that dies is replaced, and there is nothing to rebuild when it comes back: it binds the
158
- * shared port again and the kernel starts handing it connections. A signal that reaches the
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; this only makes sure
191
- // it does, and being unref'd it never keeps the process up by itself
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);