fulmine.js 5.21.0 → 5.21.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,7 +17,7 @@ See the License for the specific language governing permissions and
17
17
  limitations under the License.
18
18
  */
19
19
 
20
- const uWS = require("uWebSockets.js");
20
+ const { loadUWS } = require("./uws.js");
21
21
  const Router = require("./router.js");
22
22
  const {
23
23
  removeDuplicateSlashes,
@@ -43,8 +43,7 @@ const { workerCount, forkWorkers, isSupervising, becomeSupervisor } = require(".
43
43
 
44
44
  const cpuCount = os.cpus().length;
45
45
 
46
- // marks a "trust proxy" that was never set by the application, under the key express uses, so a
47
- // mounted sub-app knows it may inherit the parent's
46
+ // marks a "trust proxy" the application never set, under express's key, so a sub-app may inherit
48
47
  const trustProxyDefaultSymbol = "@@symbol:trust_proxy_default";
49
48
 
50
49
  const workers = /** @type {FSWorker[]} */ ([]);
@@ -52,26 +51,29 @@ let taskKey = 0;
52
51
  const workerTasks = new NullObject();
53
52
 
54
53
  class FSWorker {
54
+ /** Whether a read is in flight on it. @type {boolean} */
55
+ busy = false;
56
+
57
+ /** @type {Worker} */
58
+ worker;
59
+
55
60
  /**
56
- * A worker thread that does nothing but read files, so a read does not sit on the event loop.
57
- * It is unref'd, so an idle one does not keep the process alive, and it is shared between every
58
- * app in the process rather than started per app.
61
+ * A worker thread that only reads files, unref'd and shared by every app in the process.
59
62
  */
60
63
  constructor() {
61
- this.busy = false;
62
- this.worker = new Worker(path.join(__dirname, "worker.js"));
64
+ // its own execArgv: a --import written for the parent (Angular's route extraction loader
65
+ // reads workerData) throws in a thread that only reads files
66
+ this.worker = new Worker(path.join(__dirname, "worker.js"), { execArgv: [] });
63
67
 
64
68
  this.worker.on("message", (message) => {
65
- // node speaks on this channel too: under --watch a worker reports the files it loaded
66
- // as {"watch:import": [...]}, which carries no key of ours
69
+ // under --watch node reports {"watch:import": [...]} here too, with no key of ours
67
70
  if (workerTasks[message.key] === undefined) return;
68
71
  this.busy = false;
69
72
  if (message.err) {
70
73
  workerTasks[message.key].reject(new Error(message.err));
71
74
  } else {
72
- // worker transfers file contents as an ArrayBuffer; wrap it in a Buffer (zero-copy) so
73
- // consumers get the same type as fs.readFile. A bare ArrayBuffer is rejected by wrapped
74
- // res.end() implementations (e.g. express-session calls Buffer.byteLength on the chunk).
75
+ // the transferred ArrayBuffer as a Buffer, zero-copy: express-session calls
76
+ // Buffer.byteLength on what res.end() gets
75
77
  workerTasks[message.key].resolve(
76
78
  message.data instanceof ArrayBuffer ? Buffer.from(message.data) : message.data
77
79
  );
@@ -84,41 +86,105 @@ class FSWorker {
84
86
  }
85
87
  }
86
88
 
87
- // the worker path's own bound: a file bigger than this streams instead, so the cache never
88
- // holds an entry the read path would not have produced whole
89
+ // the worker path's bound, a bigger file streams
89
90
  const FILE_CACHE_MAX_ENTRY = 768 * 1024;
90
- // oldest-first once the budget is spent. A static directory that beats this is being served by
91
- // something other than an application server anyway
91
+ // oldest-first once the budget is spent
92
92
  const FILE_CACHE_BUDGET = 64 * 1024 * 1024;
93
93
 
94
94
  class Application extends Router {
95
95
  /**
96
- * An application reads an unset routing flag from the app it is mounted on, which a plain
97
- * Router does not: express chains a mounted app's settings onto its parent's.
96
+ * A mounted app's settings chain onto its parent's, as in express.
98
97
  *
99
98
  * @type {boolean}
100
99
  */
101
100
  _inheritsSettings = true;
102
101
 
103
102
  /**
104
- * An application, which a plain Router is not. See Router#_isApplication.
103
+ * See Router#_isApplication.
105
104
  * @type {boolean}
106
105
  */
107
106
  _isApplication = true;
108
107
 
109
108
  /**
110
- * Whether express.testing already compiled the routes of this app. Written there and nowhere
111
- * else: a second compilation would register everything with uWS twice. See src/testing.js.
109
+ * Whether express.testing already compiled the routes, a second time would register them twice.
112
110
  * @type {boolean|undefined}
113
111
  */
114
112
  _testingCompiled;
115
113
 
116
114
  /**
117
- * @param {object} [settings] the options express() takes. uwsOptions goes to uWS and decides
118
- * between an HTTP, an HTTPS and an HTTP/3 server; threads sizes the file-reading pool, and 0
119
- * turns it off; cluster forks one process per core over the same port; uwsApp adopts an
120
- * existing uWS app instead of making one. Everything else is an application setting and
121
- * lands next to the defaults.
115
+ * The uWS app once made, or the one settings.uwsApp handed in. See the uwsApp getter.
116
+ * @type {any}
117
+ */
118
+ _uwsApp;
119
+
120
+ /** What uWS.App or uWS.SSLApp is given, kept until the app is made. */
121
+ _uwsOptions;
122
+
123
+ /** The forks the cluster setting asks for, 0 when none. @type {number} */
124
+ _clusterWorkers;
125
+
126
+ /** Whether uwsOptions carries a key and a certificate, which picks uWS.SSLApp. @type {string|undefined} */
127
+ ssl;
128
+
129
+ /** express's app.cache, the view cache. @type {Record<string, any>} */
130
+ cache = new NullObject();
131
+
132
+ /** The view engines by extension, chained onto the parent's on mount. @type {Record<string, any>} */
133
+ engines = { __proto__: null };
134
+
135
+ /** A null prototype, as express gives app.locals. @type {Record<string, any>} */
136
+ locals = Object.create(null);
137
+
138
+ /** The request prototype layer of this app, what app.request extends. @type {Request} */
139
+ request;
140
+
141
+ /** @type {Response} */
142
+ response;
143
+
144
+ /** @type {boolean} */
145
+ listenCalled = false;
146
+
147
+ /** @type {FSWorker[]} */
148
+ workers = [];
149
+
150
+ /** @type {number|undefined} */
151
+ port;
152
+
153
+ /** @type {boolean} */
154
+ listening = false;
155
+
156
+ /** What address() has to go on. @type {string|undefined} */
157
+ _listenHost;
158
+
159
+ /** close() stops the listen socket, then waits for the pending responses, as node does. @type {import("uWebSockets.js").us_listen_socket|undefined} */
160
+ _listenSocket;
161
+
162
+ /** The fork supervisor, in the primary of a clustered app only. @type {{stop: () => void}|undefined} */
163
+ _clusterHandle;
164
+
165
+ /** readSmallFile's cache, by absolute path. @type {Map<string, {mtimeMs: number, size: number, data: Buffer}>} */
166
+ _fileCache = new Map();
167
+
168
+ /** @type {number} */
169
+ _fileCacheBytes = 0;
170
+
171
+ /** readSmallFile's reads in flight, so concurrent asks share one. @type {Map<string, Promise<Buffer>>} */
172
+ _fileReadsInFlight = new Map();
173
+
174
+ /**
175
+ * The responses being served, an intrusive list (a Set paid hashing per request). A holder
176
+ * object: the callable app copies own scalars by value.
177
+ * @type {{head: Response|null}}
178
+ */
179
+ _pending = { head: null };
180
+
181
+ /** Whether close() is waiting for the pending responses. @type {boolean} */
182
+ _draining = false;
183
+
184
+ /**
185
+ * @param {Record<string, any>} [settings] the options express() takes: uwsOptions (HTTP or HTTPS), threads
186
+ * (the file-reading pool, 0 off), cluster, uwsApp (an existing uWS app); the rest are
187
+ * application settings
122
188
  */
123
189
  constructor(settings = new NullObject()) {
124
190
  super(settings);
@@ -128,44 +194,30 @@ class Application extends Router {
128
194
  if (typeof settings.threads !== "number") {
129
195
  settings.threads = cpuCount > 1 ? 1 : 0;
130
196
  }
131
- // how many processes listen() should fork, counted here so a setting nobody can read is a
132
- // throw where the application is written and not where it is started. Saying it here also
133
- // settles it for the whole process before any app has listened, see becomeSupervisor
197
+ // counted here so a bad setting throws where the app is written, and the process becomes
198
+ // the supervisor before any app listens
134
199
  this._clusterWorkers = workerCount(settings.cluster);
135
200
  if (this._clusterWorkers > 0 && cluster.isPrimary) {
136
201
  becomeSupervisor();
137
202
  }
138
- if (settings.uwsApp) {
139
- this.uwsApp = /** @type {import("uWebSockets.js").TemplatedApp} */ (settings.uwsApp);
140
- } else if (settings.http3) {
141
- // uWS.H3App exists in the pinned build but its QUIC stack does not: the constructor
142
- // segfaults on Linux and hangs forever on Windows before serving a single request,
143
- // verified 2026-08-05 with uWS alone. A clear throw beats a native crash; this
144
- // branch goes back to H3App once uNetworking ships working QUIC in the prebuilts.
203
+ if (settings.http3) {
204
+ // uWS.H3App segfaults on Linux and hangs on Windows in the pinned build, verified
205
+ // 2026-08-05 with uWS alone
145
206
  throw new Error(
146
207
  "http3 is not usable with the pinned uWebSockets.js build: its H3App crashes " +
147
208
  "during construction. Track uNetworking/uWebSockets.js for working QUIC support."
148
209
  );
149
- } else if (settings.uwsOptions.key_file_name && settings.uwsOptions.cert_file_name) {
150
- this.uwsApp = uWS.SSLApp(settings.uwsOptions);
151
- } else {
152
- this.uwsApp = uWS.App(settings.uwsOptions);
153
210
  }
154
211
  this.ssl = settings.uwsOptions.key_file_name && settings.uwsOptions.cert_file_name;
155
- this.cache = new NullObject();
156
- this.engines = { __proto__: null };
157
- // a null prototype, as express gives app.locals, so a local named like an Object method
158
- // is just a local
159
- this.locals = Object.create(null);
212
+ // the uWS app is made on first use, see the uwsApp getter
213
+ this._uwsApp = settings.uwsApp;
214
+ this._uwsOptions = settings.uwsOptions;
160
215
  this.locals.settings = this.settings;
161
- // each app gets its own request/response prototype layer, so extending app.request cannot
162
- // leak into another app; a mounted sub-app re-parents its layer onto the parent's below.
163
- // The constructors are written out: the implicit derived one spreads its arguments, which
164
- // was an allocation on every request
216
+ // a request/response prototype layer per app, so extending app.request cannot leak into
217
+ // another app. The constructors are written out: the implicit one spreads its arguments,
218
+ // an allocation per request
165
219
  this._request = class extends Request {
166
220
  /**
167
- * The base constructor's arguments, written out rather than spread. See Request.
168
- *
169
221
  * @param {import("uWebSockets.js").HttpRequest} req uWS request
170
222
  * @param {import("uWebSockets.js").HttpResponse} res uWS response
171
223
  * @param {Application} app the application this request arrived at
@@ -178,8 +230,6 @@ class Application extends Router {
178
230
  };
179
231
  this._response = class extends Response {
180
232
  /**
181
- * The base constructor's arguments, written out rather than spread. See Response.
182
- *
183
233
  * @param {import("uWebSockets.js").HttpResponse} res uWS response
184
234
  * @param {Request} req the Request, already built
185
235
  * @param {Application} app the application this request arrived at
@@ -191,22 +241,17 @@ class Application extends Router {
191
241
  this.request = this._request.prototype;
192
242
  this.response = this._response.prototype;
193
243
  this.on("mount", (parent) => {
194
- // the parent's extensions show through, and an override here stays here. Only an
195
- // application has a layer to hang onto: a plain router mount leaves things alone
244
+ // the parent's extensions and engines show through, as express chains them
196
245
  if (parent.request) {
197
246
  Object.setPrototypeOf(this.request, parent.request);
198
247
  }
199
248
  if (parent.response) {
200
249
  Object.setPrototypeOf(this.response, parent.response);
201
250
  }
202
- // and the engines with them, which is the same chaining express does: a sub-app renders
203
- // with whatever the parent registered unless it registered its own. Without this a
204
- // render inside a mounted app looked for a module named after the extension.
205
251
  if (parent.engines) {
206
252
  Object.setPrototypeOf(this.engines, parent.engines);
207
253
  }
208
- // a "trust proxy" this app never set is inherited from the parent, as express does:
209
- // the defaults are deleted so get() falls through to the parent's value
254
+ // a "trust proxy" never set here is inherited: the defaults are deleted so get() falls through
210
255
  if (
211
256
  this._settings[trustProxyDefaultSymbol] === true &&
212
257
  typeof parent._settings["trust proxy fn"] === "function"
@@ -215,8 +260,6 @@ class Application extends Router {
215
260
  delete this._settings["trust proxy fn"];
216
261
  }
217
262
  });
218
- this.listenCalled = false;
219
- this.workers = [];
220
263
  for (let i = 0; i < settings.threads; i++) {
221
264
  if (workers[i]) {
222
265
  this.workers[i] = workers[i];
@@ -224,30 +267,8 @@ class Application extends Router {
224
267
  this.workers[i] = new FSWorker();
225
268
  }
226
269
  }
227
- this.port = undefined;
228
- this.listening = false;
229
- // the host handed to listen(), which is all address() has to go on
230
- this._listenHost = undefined;
231
- // the uWS listen socket, and the responses being served right now: close() stops the
232
- // first and waits for the second, the way node's server.close() does
233
- this._listenSocket = undefined;
234
- // the fork supervisor, in the primary of a clustered app and nowhere else
235
- /** @type {{stop: () => void}|undefined} */
236
- this._clusterHandle = undefined;
237
- // readSmallFile's cache and its in-flight reads, see the method
238
- this._fileCache = new Map();
239
- this._fileCacheBytes = 0;
240
- this._fileReadsInFlight = new Map();
241
- // the responses being served right now, an intrusive list: linking is three pointer
242
- // stores where a Set paid identity hashing and table upkeep per request. A holder object
243
- // rather than a bare field, because the callable app copies own scalars by value and two
244
- // copies of a head would disagree; an object rides by reference, the way the Set did
245
- this._pending = /** @type {{ head: Response|null }} */ ({ head: null });
246
- // on the per-app prototype layer, not per response, same as the Set was
247
270
  /** @type {{_pendingIn?: {head: Response|null}}} */ (this.response)._pendingIn = this._pending;
248
- this._draining = false;
249
- // read here, at construction, the way express does; an empty NODE_ENV means development,
250
- // which the ?? in the shared default would miss
271
+ // at construction as express reads it; an empty NODE_ENV is development
251
272
  if (typeof this._settings.env === "undefined") {
252
273
  this._settings.env = process.env.NODE_ENV || "development";
253
274
  }
@@ -260,7 +281,6 @@ class Application extends Router {
260
281
  }
261
282
  }
262
283
  }
263
- // non-enumerable, so the marker never shows up walking the settings
264
284
  Object.defineProperty(this._settings, trustProxyDefaultSymbol, {
265
285
  configurable: true,
266
286
  value: true
@@ -270,9 +290,8 @@ class Application extends Router {
270
290
  }
271
291
 
272
292
  /**
273
- * Parks a promise's settle functions under a key the worker can send back, since a worker
274
- * message carries data and not closures. The counter wraps rather than growing without bound,
275
- * a million tasks being far more than can be outstanding at once.
293
+ * Parks a promise's settle functions under a key the worker sends back. The counter wraps at a
294
+ * million.
276
295
  *
277
296
  * @param {(value: Buffer) => void} resolve
278
297
  * @param {(err: Error) => void} reject
@@ -288,9 +307,8 @@ class Application extends Router {
288
307
  }
289
308
 
290
309
  /**
291
- * Reads a file on one of the file threads, picked at random, rather than on the event loop.
292
- * Only worth it below the size where the copy back costs more than the read, which is why
293
- * res.sendFile uses it for small files and streams the rest.
310
+ * Reads a file on a file thread picked at random. Only worth it for a small file, res.sendFile
311
+ * streams the rest.
294
312
  *
295
313
  * @param {string} path absolute path to read
296
314
  * @returns {Promise<Buffer>}
@@ -305,11 +323,9 @@ class Application extends Router {
305
323
  }
306
324
 
307
325
  /**
308
- * 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, validated
310
- * against the stat the caller already paid for, so a touched file is re-read. A hit completes
311
- * on a macrotask, which is when a worker's answer would have arrived.
312
- * `app.set("file cache", false)` turns the cache off, the shared read stays.
326
+ * A small file through the worker pool: concurrent asks share one read, and an unchanged file
327
+ * (by the stat the caller paid for) comes from a bounded cache, on a macrotask as a worker's
328
+ * answer would. `app.set("file cache", false)` keeps only the shared read.
313
329
  *
314
330
  * @param {string} fullpath
315
331
  * @param {import("fs").Stats} stat
@@ -347,17 +363,14 @@ class Application extends Router {
347
363
  return data;
348
364
  });
349
365
  this._fileReadsInFlight.set(fullpath, pending);
350
- // never cached past settlement: a rejection clears the slot the same way
351
366
  const clear = () => this._fileReadsInFlight.delete(fullpath);
352
367
  pending.then(clear, clear);
353
368
  return pending;
354
369
  }
355
370
 
356
371
  /**
357
- * Reads or writes an application setting. One argument is the getter, and the check is on
358
- * `arguments.length`, so `set(key, undefined)` still writes. Some keys have a side effect:
359
- * `trust proxy`, `query parser` and `etag` compile the value into a function kept beside it,
360
- * and `views` becomes an absolute path.
372
+ * Reads or writes a setting; `set(key, undefined)` still writes. `trust proxy`, `query parser`
373
+ * and `etag` compile the value into a function kept beside it.
361
374
  *
362
375
  * @param {string} key setting name
363
376
  * @param {*} [value] value to store; omit to read instead
@@ -369,19 +382,16 @@ class Application extends Router {
369
382
  }
370
383
  if (key === "trust proxy") {
371
384
  if (!value) {
372
- // compiled, not deleted: an explicit false must shadow a parent's setting when
373
- // this app is mounted, and a deleted key would read straight through to it
385
+ // compiled, not deleted: an explicit false must shadow a parent's setting
374
386
  this._settings["trust proxy fn"] = compileTrust(false);
375
387
  } else {
376
388
  this._settings["trust proxy fn"] = compileTrust(value);
377
389
  }
378
- // set explicitly, so a mount no longer inherits the parent's
379
390
  Object.defineProperty(this._settings, trustProxyDefaultSymbol, {
380
391
  configurable: true,
381
392
  value: false
382
393
  });
383
394
  } else if (key === "stat cache") {
384
- // compiled here so the read path is a number and not a duration to parse per request
385
395
  this._settings["stat cache ms"] = durationSetting(value, "stat cache");
386
396
  } else if (key === "query parser") {
387
397
  if (value === "extended") {
@@ -393,24 +403,18 @@ class Application extends Router {
393
403
  } else if (value === false) {
394
404
  this._settings["query parser fn"] = undefined;
395
405
  } else {
396
- // express's wording, which applications match on
397
406
  throw new TypeError("unknown value for query parser function: " + value);
398
407
  }
399
408
  } else if (key === "etag methods") {
400
- // fulmine's own: the methods whose send() computes a generated ETag. Unset means all
401
- // of them, which is express's behaviour and what its suite asserts per method; naming
402
- // ["GET", "HEAD"] skips the digest everywhere a validator can never match, which
403
- // measured +21% on a 4KB POST answer. See issue #10.
409
+ // fulmine's own: the methods whose send() computes an ETag, all of them unset as
410
+ // express does; ["GET", "HEAD"] measured +21% on a 4KB POST answer, see issue #10
404
411
  if (value != null && (!Array.isArray(value) || value.some((m) => typeof m !== "string"))) {
405
412
  throw new TypeError('"etag methods" wants an array of method names, or null for all of them');
406
413
  }
407
414
  value = value == null ? undefined : value.map((/** @type {string} */ m) => m.toUpperCase());
408
415
  } else if (key === "etag") {
409
- // The skips are not taken back here. They used to be, because send consults freshness,
410
- // but that branch reads if-none-match, if-modified-since and cache-control by name
411
- // whatever this setting says, and req.fresh reads nothing else off the request.
412
- // Registering a route after listen still takes them back: that is a different
413
- // question, about code the analysis never saw
416
+ // the header skips stay: the skip branch reads the conditional pair by name whatever
417
+ // this says
414
418
  if (typeof value === "function") {
415
419
  this._settings["etag fn"] = value;
416
420
  } else {
@@ -426,14 +430,13 @@ class Application extends Router {
426
430
  delete this._settings["etag fn"];
427
431
  break;
428
432
  default:
429
- // express's wording, which applications match on
430
433
  throw new TypeError("unknown value for etag function: " + value);
431
434
  }
432
435
  }
433
436
  }
434
437
 
435
438
  this._settings[key] = value;
436
- // any app's hot-settings copy may resolve through this one, see Router#_hot
439
+ // see Router#_hot
437
440
  settingsEpoch.n++;
438
441
  return this;
439
442
  }
@@ -459,8 +462,7 @@ class Application extends Router {
459
462
  }
460
463
 
461
464
  /**
462
- * Whether a setting is truthy. Reads through to the app this one is mounted on, as get() does
463
- * and as express does: mounting chains a sub-app's settings onto its parent's.
465
+ * Whether a setting is truthy, through the parent as get() reads it.
464
466
  * @param {string} key setting name
465
467
  * @returns {boolean}
466
468
  */
@@ -478,25 +480,19 @@ class Application extends Router {
478
480
  }
479
481
 
480
482
  /**
481
- * Router's handleRequest plus the bookkeeping a graceful close() needs: every live response
482
- * is held in a set until it finishes, so close() knows when the last one is done. Native
483
- * routes and the catch-all both come through here, since both call it on the app.
483
+ * Router's handleRequest plus the pending list a graceful close() drains.
484
484
  *
485
485
  * @param {import("uWebSockets.js").HttpResponse} res uWS response
486
486
  * @param {import("uWebSockets.js").HttpRequest} req uWS request, readable only during this call
487
- * @param {import("./router-utils.js").NativePreset} [preset] a literal registration's constants,
488
- * see nativePreset in the router
489
- * @param {import("./router-utils.js").SkipHolder} [skipHolder] where a granted header skip lives,
490
- * forwarded whole: dropping it here silently turned every skip off, since the native closures
491
- * call this override
492
- * @returns {Request} the request, with the response reachable as request.res
487
+ * @param {import("./router-utils.js").NativePreset} [preset] see nativePreset
488
+ * @param {import("./router-utils.js").SkipHolder} [skipHolder] forwarded whole, dropping it
489
+ * silently turned every skip off
490
+ * @returns {Request} the request, with the response as request.res
493
491
  */
494
492
  handleRequest(res, req, preset, skipHolder) {
495
493
  const request = super.handleRequest(res, req, preset, skipHolder);
496
- // removal rides the close listener the Response constructor already has, since a second
497
- // once() per request measured a tenth of a microsecond on the hot path.
498
- // An aborted response only flips its flags without emitting 'close', which is why
499
- // close()'s drain also sweeps the list by those flags instead of trusting this alone
494
+ // unlinked by the close listener the Response already has; an aborted response only
495
+ // flips its flags, so close()'s drain sweeps by them too
500
496
  const response = request.res;
501
497
  const pending = this._pending;
502
498
  response._pendingLinked = true;
@@ -510,18 +506,27 @@ class Application extends Router {
510
506
  }
511
507
 
512
508
  /**
513
- * Registers the catch-all uWS handler, which is what serves every request that no optimized
514
- * route took natively. It walks this app's own chain and, when nothing in it answered, decides
515
- * between an error, the automatic OPTIONS reply and a 404.
509
+ * The µWS app, for what µWS offers that this does not (socket.io attaches to it). Made on
510
+ * first ask, so an app served through node's http never loads the binary, see src/uws.js.
511
+ *
512
+ * @returns {any}
516
513
  */
514
+ get uwsApp() {
515
+ if (this._uwsApp === undefined) {
516
+ const uWS = loadUWS();
517
+ this._uwsApp = this.ssl ? uWS.SSLApp(this._uwsOptions) : uWS.App(this._uwsOptions);
518
+ }
519
+ return this._uwsApp;
520
+ }
521
+
522
+ /** The catch-all uWS handler, for every request no native route took. */
517
523
  _createRequestHandler() {
518
524
  this.uwsApp.any("/*", (res, req) => this._serveGeneric(res, req));
519
525
  }
520
526
 
521
527
  /**
522
- * Serves one request by walking this app's chain, with no registration-time shortcut. It is
523
- * what the catch-all runs, and also what a native registration falls back to when it sees a
524
- * request it must not answer itself, see the case guard in Router#_registerUwsRoute.
528
+ * Serves one request by walking the chain: the catch-all, and what a native registration
529
+ * falls back to on a request it must not answer itself, see the case guard in _registerUwsRoute.
525
530
  *
526
531
  * @param {import("uWebSockets.js").HttpResponse} res the uWS response
527
532
  * @param {import("uWebSockets.js").HttpRequest} req the uWS request
@@ -535,11 +540,9 @@ class Application extends Router {
535
540
  try {
536
541
  this._routeRequestDirect(request, response);
537
542
  } finally {
538
- // the synchronous stretch has run under the cork uWS holds for this callback, and
539
- // whatever comes after it is outside
543
+ // the synchronous stretch ran under uWS's own cork
540
544
  response._corkNeeded = true;
541
- // an abort can only arrive after this callback returns, as the native handler's
542
- // finally says: a response that finished inside it never needs uWS told at all
545
+ // an abort can only arrive after this callback returns
543
546
  if (!response.finished) {
544
547
  this._armAbort(res, response);
545
548
  }
@@ -547,12 +550,8 @@ class Application extends Router {
547
550
  }
548
551
 
549
552
  /**
550
- * Binds the server and starts accepting requests.
551
- *
552
- * Returns the app and not an `http.Server`, since there is no node server underneath. The app
553
- * 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`. A path instead of
555
- * a port is a unix socket.
553
+ * Binds and starts accepting. Returns the app, which answers as an `http.Server`; socket.io
554
+ * wants `app.uwsApp`. A path instead of a port is a unix socket.
556
555
  *
557
556
  * @param {number|string} [port] port, or a unix socket path; 0 picks a free port
558
557
  * @param {string} [host] interface to bind; every interface when omitted
@@ -561,13 +560,9 @@ class Application extends Router {
561
560
  * @returns {this} the app, which doubles as the server handle
562
561
  */
563
562
  listen(port, host, backlog, callback) {
564
- // With { cluster } the primary has nothing to bind. Each worker binds this same port with
565
- // uWS's shared flag, SO_REUSEPORT, so the kernel hands each connection to one of them and
566
- // the primary only forks and replaces a worker that dies. Everything below this runs in the
567
- // workers, listen callback included, so once per worker rather than once.
568
- //
569
- // The test is the process and not this app: a second app on a TLS port, without a cluster
570
- // setting of its own, would take that port here exclusively and every worker would fail
563
+ // With { cluster } the primary only forks: each worker binds this port with SO_REUSEPORT
564
+ // and everything below runs once per worker. The test is the process, not this app: a
565
+ // second app without a cluster setting would take its port here and every worker would fail
571
566
  if (cluster.isPrimary && isSupervising()) {
572
567
  if (this._clusterWorkers > 0 && !this._clusterHandle) {
573
568
  this._clusterHandle = forkWorkers(this._clusterWorkers);
@@ -575,11 +570,9 @@ class Application extends Router {
575
570
  return this;
576
571
  }
577
572
  this._compileOptimizedRoutes();
578
- // before the catch-all: µWS sends an upgrade to the websocket route even when a
579
- // catch-all covers the same path, so the two coexist and the order is only tidiness
580
573
  registerWebSocketRoutes(this);
581
574
  this._createRequestHandler();
582
- // node's shapes: (cb), (port, cb), (port, host, cb) and (port, host, backlog, cb)
575
+ // node's shapes: (cb), (port, cb), (port, host, cb), (port, host, backlog, cb)
583
576
  if (typeof port === "function") {
584
577
  callback = port;
585
578
  port = 0;
@@ -589,45 +582,36 @@ class Application extends Router {
589
582
  } else if (typeof backlog === "function") {
590
583
  callback = backlog;
591
584
  }
592
- // bare listen() and listen(undefined, cb) bind an OS-assigned port, as node does; left
593
- // undefined the port fell through to the unix-socket branch below
585
+ // a bare listen() binds an OS-assigned port, as node does
594
586
  if (port == null) {
595
587
  port = 0;
596
588
  }
597
- // uWS runs this handler from inside its own listen(), so everything it hands back to the
598
- // caller is deferred a tick. Express binds synchronously too but reports through events,
599
- // and node emits both 'listening' and 'error' from a process.nextTick.
589
+ // uWS runs this inside its own listen(), so everything reported to the caller is deferred
590
+ // a tick, as node emits 'listening' and 'error'
600
591
  const onListen = (/** @type {import("uWebSockets.js").us_listen_socket|false} */ socket) => {
601
592
  if (!socket) {
602
593
  /** @type {NodeJS.ErrnoException} */
603
594
  const err = new Error("listen EADDRINUSE: address already in use :::" + port);
604
595
  err.code = "EADDRINUSE";
605
- // Express 5 registers the listen callback on 'error' as well as on 'listening',
606
- // so a failed bind arrives at the callback rather than being thrown past it
596
+ // Express 5 hands a failed bind to the listen callback
607
597
  if (callback) {
608
598
  return process.nextTick(() => callback.call(this, err));
609
599
  }
610
- // no callback means no 'error' listener either, and an EventEmitter carrying an
611
- // unhandled error rethrows it from the tick that emitted it, not from listen()
600
+ // without one it is thrown from the tick, as an unhandled 'error' would be
612
601
  return process.nextTick(() => {
613
602
  throw err;
614
603
  });
615
604
  }
616
- // the port is known synchronously, as it is in Express, so address() works as soon as
617
- // listen() returns. The callback is not: running it here would run it before listen()
618
- // had returned, and `const server = app.listen(p, () => server.address())` - the form
619
- // the Express docs use - would die on the temporal dead zone.
620
- this.port = uWS.us_socket_local_port(socket);
605
+ // the port synchronously, so address() works as soon as listen() returns; the
606
+ // callback on a tick, `const server = app.listen(p, () => server.address())` would
607
+ // hit the temporal dead zone
608
+ this.port = loadUWS().us_socket_local_port(socket);
621
609
  this.listening = true;
622
610
  this._listenHost = host;
623
- // kept so close() can stop accepting without dropping what is in flight
624
611
  this._listenSocket = socket;
625
612
  process.nextTick(() => {
626
- // `this` is the app, which is what listen() returns here. Express binds it to the
627
- // http.Server, which is what listen() returns there, so
628
- // `function () { this.address() }` reads the same on both.
629
- // The callback goes first: in Express it is registered as a 'listening' listener
630
- // before the caller can add any of their own.
613
+ // `this` is what listen() returns, as in Express; the callback first, as the
614
+ // first 'listening' listener
631
615
  if (callback) callback.call(this);
632
616
  this.emit("listening");
633
617
  });
@@ -659,10 +643,7 @@ class Application extends Router {
659
643
  }
660
644
 
661
645
  /**
662
- * Publishes a message to every socket subscribed to a topic, from outside any of them.
663
- *
664
- * The socket's own `publish` reaches the same topics; this one is for the sender that is
665
- * not a socket, a timer or a route handler broadcasting to a room.
646
+ * Publishes a message to every socket subscribed to a topic, from outside any socket.
666
647
  *
667
648
  * @param {string} topic
668
649
  * @param {string|ArrayBuffer|Buffer} message
@@ -685,9 +666,7 @@ class Application extends Router {
685
666
  }
686
667
 
687
668
  /**
688
- * The router the application routes through, which express 5 hands out so that a caller can
689
- * walk `app.router.stack`. Here the application is the router, so it hands back itself and the
690
- * walk finds the same layers.
669
+ * express 5's `app.router`, for a caller walking `app.router.stack`: the application itself.
691
670
  *
692
671
  * @returns {this}
693
672
  */
@@ -703,10 +682,8 @@ class Application extends Router {
703
682
  if (!this.listening || !this.port) {
704
683
  return null;
705
684
  }
706
- // uWS hands back the port and nothing else, so the address reported is the one we asked
707
- // it to bind. No host means every interface, which node reports as "::". A hostname is
708
- // reported as written, since what it resolved to is not readable back from here: node
709
- // would say "::1" where this says "localhost".
685
+ // uWS hands back only the port: no host is "::" as node reports it, a hostname is
686
+ // reported as written where node would say "::1"
710
687
  const host = this._listenHost;
711
688
  if (!host) {
712
689
  return { address: "::", family: "IPv6", port: this.port };
@@ -715,8 +692,7 @@ class Application extends Router {
715
692
  }
716
693
 
717
694
  /**
718
- * The full mount path of this app, walking up through every parent it is mounted on.
719
- * A top level app returns the empty string rather than "/".
695
+ * The full mount path through every parent, "" at the top level.
720
696
  * @returns {string}
721
697
  */
722
698
  path() {
@@ -731,9 +707,7 @@ class Application extends Router {
731
707
  }
732
708
 
733
709
  /**
734
- * Registers a template engine for a file extension.
735
- *
736
- * The leading dot is optional: "pug" and ".pug" register the same thing.
710
+ * Registers a template engine for an extension, with or without the dot.
737
711
  *
738
712
  * @param {string} ext file extension the engine handles
739
713
  * @param {(path: string, options: object, callback: (err: Error|null, rendered?: string) => void) => void} fn
@@ -751,29 +725,22 @@ class Application extends Router {
751
725
  }
752
726
 
753
727
  /**
754
- * Renders a view and hands the result to the callback, without sending anything.
755
- * `res.render()` is the one that responds.
756
- *
757
- * `app.locals` and `options._locals` are merged into the options, in that order, so a
758
- * per-request local wins. Caching follows the "view cache" setting unless `options.cache` says
759
- * otherwise. A function in the options position is taken as the callback.
728
+ * Renders a view into the callback, without sending: `res.render()` is the one that responds.
729
+ * `app.locals` then `options._locals` are merged in, so a per-request local wins. Caching
730
+ * follows the "view cache" setting unless `options.cache` says otherwise.
760
731
  *
761
732
  * @param {string} name view name, resolved against the "views" setting
762
- * @param {Record<string, any>|((err: Error|null, html?: string) => void)} [options] locals for
763
- * the view, or the callback in its place
764
- * @param {(err: Error|null, html?: string) => void} [callback] receives the rendered view. It
765
- * is what render is for, so leaving it out throws, as it does in Express
733
+ * @param {Record<string, any>|((err: Error|null, html?: string) => void)} [options] locals, or
734
+ * the callback in its place
735
+ * @param {(err: Error|null, html?: string) => void} [callback] required, as in Express
766
736
  */
767
737
  render(name, options, callback) {
768
738
  if (typeof options === "function") {
769
739
  callback = /** @type {(err: Error|null, html?: string) => void} */ (options);
770
740
  options = new NullObject();
771
741
  }
772
- // render exists to hand the result somewhere, so there is always a callback by this point:
773
- // either the third argument or the second one, shuffled above
774
742
  const done = /** @type {(err: Error|null, html?: string) => void} */ (callback);
775
- // express's order, least specific first: app.locals, then res.locals riding in as _locals,
776
- // and what was passed to this call wins over both
743
+ // express's order: app.locals, then res.locals as _locals, then what was passed
777
744
  const opts = options || new NullObject();
778
745
  options = new NullObject();
779
746
  for (const key in this.locals) {
@@ -802,11 +769,8 @@ class Application extends Router {
802
769
  view = new View(name, {
803
770
  defaultEngine: this.get("view engine"),
804
771
  root: this.get("views"),
805
- // the object itself, not a copy of it: a mounted app reaches its parent's engines
806
- // through the prototype chain, and a spread only carries what the app owns, so a
807
- // sub-app rendering with the parent's engine went off to require() a module named
808
- // after the extension. Express hands its own object over too, and means to: a view
809
- // that loads an engine by require caches it back here
772
+ // the object itself, as express hands it: a sub-app reaches its parent's engines
773
+ // through the prototype chain, and a view caches a required engine back here
810
774
  engines: this.engines
811
775
  });
812
776
  if (!view.path) {
@@ -838,23 +802,17 @@ class Application extends Router {
838
802
  }
839
803
 
840
804
  /**
841
- * Stops accepting connections, lets in-flight requests finish, then emits 'close'.
842
- *
843
- * Node's server.close() only closes the listen socket and waits for what is being served; uWS's
844
- * close() terminates every connection, so calling it first aborted whatever a graceful shutdown
845
- * was waiting for. It still runs, but only once the last pending response is done, to drop the
846
- * idle keep-alive connections nothing else would close.
847
- *
848
- * The callback is the first 'close' listener. Closing a server that was not listening still
849
- * calls back with ERR_SERVER_NOT_RUNNING, the way node does.
805
+ * Stops accepting connections, lets in-flight requests finish, then emits 'close'. uWS's
806
+ * close() kills every connection, so it runs only after the last pending response, to drop the
807
+ * idle keep-alive ones. Closing a server that was not listening calls back with
808
+ * ERR_SERVER_NOT_RUNNING, as node does.
850
809
  *
851
810
  * @param {(err?: Error) => void} [callback] called once closed
852
811
  * @returns {this} the app, for chaining
853
812
  */
854
813
  close(callback) {
855
- // the primary of a clustered app never bound anything, so closing it means stopping the
856
- // workers. They are killed rather than drained: each one holds its own listening socket
857
- // and drains itself when the signal reaches it
814
+ // the primary of a clustered app never bound anything: it stops the workers, each of
815
+ // which drains itself
858
816
  if (this._clusterHandle) {
859
817
  this._clusterHandle.stop();
860
818
  this._clusterHandle = undefined;
@@ -866,8 +824,7 @@ class Application extends Router {
866
824
  }
867
825
  const wasListening = this.listening;
868
826
  this.listening = false;
869
- // in Express the close callback is nothing more than the first 'close' listener, and a
870
- // server that was not running still gets called back, with an error
827
+ // the callback is the first 'close' listener, as in Express
871
828
  if (callback) {
872
829
  this.once("close", () => {
873
830
  if (wasListening) {
@@ -880,15 +837,14 @@ class Application extends Router {
880
837
  });
881
838
  }
882
839
  if (!this.listenCalled || !wasListening) {
883
- // a close while a drain is underway does not emit again: the pending drain's single
884
- // 'close' serves both calls, which is what node does too
840
+ // a close during a drain does not emit again, as in node
885
841
  if (!this._draining) {
886
842
  process.nextTick(() => this.emit("close"));
887
843
  }
888
844
  return this;
889
845
  }
890
846
  if (this._listenSocket) {
891
- uWS.us_listen_socket_close(this._listenSocket);
847
+ loadUWS().us_listen_socket_close(this._listenSocket);
892
848
  this._listenSocket = undefined;
893
849
  }
894
850
  this._draining = true;
@@ -901,12 +857,11 @@ class Application extends Router {
901
857
  process.nextTick(finish);
902
858
  return this;
903
859
  }
904
- // a finished response emits 'close' and unlinks itself; an aborted one only flips its
905
- // flags, so the drain sweeps by them. The timer also keeps the loop alive until done.
860
+ // an aborted response only flips its flags, so the drain sweeps by them; the timer keeps
861
+ // the loop alive
906
862
  const sweep = setInterval(() => {
907
863
  let response = this._pending.head;
908
864
  while (response !== null) {
909
- // taken before the unlink, which nulls the pointers
910
865
  const next = response._pendingNext;
911
866
  if (response.finished || response.aborted) {
912
867
  response._unlinkPending();
@@ -922,22 +877,15 @@ class Application extends Router {
922
877
  }
923
878
  }
924
879
 
925
- // An app is a function, as it is in Express, and not the Application instance whose properties it
926
- // carries. Middleware that takes a whole app and calls it, vhost being the one everybody meets, was
927
- // given something it could not call.
928
- //
929
- // Tried once before and reverted the same day, because a callable app broke supertest: `request(app)`
930
- // reads `typeof app === "function"` and wraps what it finds in http.createServer, and there was
931
- // nothing underneath that could serve node's IncomingMessage. src/node-shim.js closes that hole.
880
+ // An app is a function, as in Express: vhost and the like call it. supertest then wraps it in
881
+ // http.createServer, which is what src/node-shim.js serves.
932
882
  /** @param {object} [options] the settings express() takes, see the Application constructor */
933
883
  module.exports = function (options) {
934
884
  return new Application(options)._asCallable();
935
885
  };
936
886
 
937
- // the class itself, so index.js can expose its prototype as express.application does. Adding a
938
- // method to that prototype adds it to every app, which is what the property is for.
887
+ // the class, so index.js exposes its prototype as express.application
939
888
  module.exports.Application = Application;
940
889
 
941
- // and what makes an application answer as an http.Server, since that is what a library handed the
942
- // result of listen() looks for. See server-shape.js for what is answered and what is not.
890
+ // what makes an application answer as an http.Server, see server-shape.js
943
891
  addServerMembers(Application.prototype);