fulmine.js 5.21.1 → 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.
@@ -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,29 +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
- // its own execArgv, not the parent thread's: a worker inherits them, and a --require or
63
- // --import written for the parent (Angular's route extraction registers a loader that reads
64
- // workerData) throws inside a thread that only reads files
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
65
66
  this.worker = new Worker(path.join(__dirname, "worker.js"), { execArgv: [] });
66
67
 
67
68
  this.worker.on("message", (message) => {
68
- // node speaks on this channel too: under --watch a worker reports the files it loaded
69
- // as {"watch:import": [...]}, which carries no key of ours
69
+ // under --watch node reports {"watch:import": [...]} here too, with no key of ours
70
70
  if (workerTasks[message.key] === undefined) return;
71
71
  this.busy = false;
72
72
  if (message.err) {
73
73
  workerTasks[message.key].reject(new Error(message.err));
74
74
  } else {
75
- // worker transfers file contents as an ArrayBuffer; wrap it in a Buffer (zero-copy) so
76
- // consumers get the same type as fs.readFile. A bare ArrayBuffer is rejected by wrapped
77
- // 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
78
77
  workerTasks[message.key].resolve(
79
78
  message.data instanceof ArrayBuffer ? Buffer.from(message.data) : message.data
80
79
  );
@@ -87,31 +86,27 @@ class FSWorker {
87
86
  }
88
87
  }
89
88
 
90
- // the worker path's own bound: a file bigger than this streams instead, so the cache never
91
- // holds an entry the read path would not have produced whole
89
+ // the worker path's bound, a bigger file streams
92
90
  const FILE_CACHE_MAX_ENTRY = 768 * 1024;
93
- // oldest-first once the budget is spent. A static directory that beats this is being served by
94
- // something other than an application server anyway
91
+ // oldest-first once the budget is spent
95
92
  const FILE_CACHE_BUDGET = 64 * 1024 * 1024;
96
93
 
97
94
  class Application extends Router {
98
95
  /**
99
- * An application reads an unset routing flag from the app it is mounted on, which a plain
100
- * 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.
101
97
  *
102
98
  * @type {boolean}
103
99
  */
104
100
  _inheritsSettings = true;
105
101
 
106
102
  /**
107
- * An application, which a plain Router is not. See Router#_isApplication.
103
+ * See Router#_isApplication.
108
104
  * @type {boolean}
109
105
  */
110
106
  _isApplication = true;
111
107
 
112
108
  /**
113
- * Whether express.testing already compiled the routes of this app. Written there and nowhere
114
- * 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.
115
110
  * @type {boolean|undefined}
116
111
  */
117
112
  _testingCompiled;
@@ -125,12 +120,71 @@ class Application extends Router {
125
120
  /** What uWS.App or uWS.SSLApp is given, kept until the app is made. */
126
121
  _uwsOptions;
127
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
+
128
184
  /**
129
- * @param {object} [settings] the options express() takes. uwsOptions goes to uWS and decides
130
- * between an HTTP, an HTTPS and an HTTP/3 server; threads sizes the file-reading pool, and 0
131
- * turns it off; cluster forks one process per core over the same port; uwsApp adopts an
132
- * existing uWS app instead of making one. Everything else is an application setting and
133
- * lands next to the defaults.
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
134
188
  */
135
189
  constructor(settings = new NullObject()) {
136
190
  super(settings);
@@ -140,42 +194,30 @@ class Application extends Router {
140
194
  if (typeof settings.threads !== "number") {
141
195
  settings.threads = cpuCount > 1 ? 1 : 0;
142
196
  }
143
- // how many processes listen() should fork, counted here so a setting nobody can read is a
144
- // throw where the application is written and not where it is started. Saying it here also
145
- // 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
146
199
  this._clusterWorkers = workerCount(settings.cluster);
147
200
  if (this._clusterWorkers > 0 && cluster.isPrimary) {
148
201
  becomeSupervisor();
149
202
  }
150
203
  if (settings.http3) {
151
- // uWS.H3App exists in the pinned build but its QUIC stack does not: the constructor
152
- // segfaults on Linux and hangs forever on Windows before serving a single request,
153
- // verified 2026-08-05 with uWS alone. A clear throw beats a native crash; this
154
- // branch goes back to H3App once uNetworking ships working QUIC in the prebuilts.
204
+ // uWS.H3App segfaults on Linux and hangs on Windows in the pinned build, verified
205
+ // 2026-08-05 with uWS alone
155
206
  throw new Error(
156
207
  "http3 is not usable with the pinned uWebSockets.js build: its H3App crashes " +
157
208
  "during construction. Track uNetworking/uWebSockets.js for working QUIC support."
158
209
  );
159
210
  }
160
211
  this.ssl = settings.uwsOptions.key_file_name && settings.uwsOptions.cert_file_name;
161
- // the uWS app is made on first use, see the uwsApp getter: what listen(), ws() and the
162
- // optimizer need, and what an app served through node's http never asks for
212
+ // the uWS app is made on first use, see the uwsApp getter
163
213
  this._uwsApp = settings.uwsApp;
164
214
  this._uwsOptions = settings.uwsOptions;
165
- this.cache = new NullObject();
166
- this.engines = { __proto__: null };
167
- // a null prototype, as express gives app.locals, so a local named like an Object method
168
- // is just a local
169
- this.locals = Object.create(null);
170
215
  this.locals.settings = this.settings;
171
- // each app gets its own request/response prototype layer, so extending app.request cannot
172
- // leak into another app; a mounted sub-app re-parents its layer onto the parent's below.
173
- // The constructors are written out: the implicit derived one spreads its arguments, which
174
- // 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
175
219
  this._request = class extends Request {
176
220
  /**
177
- * The base constructor's arguments, written out rather than spread. See Request.
178
- *
179
221
  * @param {import("uWebSockets.js").HttpRequest} req uWS request
180
222
  * @param {import("uWebSockets.js").HttpResponse} res uWS response
181
223
  * @param {Application} app the application this request arrived at
@@ -188,8 +230,6 @@ class Application extends Router {
188
230
  };
189
231
  this._response = class extends Response {
190
232
  /**
191
- * The base constructor's arguments, written out rather than spread. See Response.
192
- *
193
233
  * @param {import("uWebSockets.js").HttpResponse} res uWS response
194
234
  * @param {Request} req the Request, already built
195
235
  * @param {Application} app the application this request arrived at
@@ -201,22 +241,17 @@ class Application extends Router {
201
241
  this.request = this._request.prototype;
202
242
  this.response = this._response.prototype;
203
243
  this.on("mount", (parent) => {
204
- // the parent's extensions show through, and an override here stays here. Only an
205
- // 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
206
245
  if (parent.request) {
207
246
  Object.setPrototypeOf(this.request, parent.request);
208
247
  }
209
248
  if (parent.response) {
210
249
  Object.setPrototypeOf(this.response, parent.response);
211
250
  }
212
- // and the engines with them, which is the same chaining express does: a sub-app renders
213
- // with whatever the parent registered unless it registered its own. Without this a
214
- // render inside a mounted app looked for a module named after the extension.
215
251
  if (parent.engines) {
216
252
  Object.setPrototypeOf(this.engines, parent.engines);
217
253
  }
218
- // a "trust proxy" this app never set is inherited from the parent, as express does:
219
- // 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
220
255
  if (
221
256
  this._settings[trustProxyDefaultSymbol] === true &&
222
257
  typeof parent._settings["trust proxy fn"] === "function"
@@ -225,8 +260,6 @@ class Application extends Router {
225
260
  delete this._settings["trust proxy fn"];
226
261
  }
227
262
  });
228
- this.listenCalled = false;
229
- this.workers = [];
230
263
  for (let i = 0; i < settings.threads; i++) {
231
264
  if (workers[i]) {
232
265
  this.workers[i] = workers[i];
@@ -234,30 +267,8 @@ class Application extends Router {
234
267
  this.workers[i] = new FSWorker();
235
268
  }
236
269
  }
237
- this.port = undefined;
238
- this.listening = false;
239
- // the host handed to listen(), which is all address() has to go on
240
- this._listenHost = undefined;
241
- // the uWS listen socket, and the responses being served right now: close() stops the
242
- // first and waits for the second, the way node's server.close() does
243
- this._listenSocket = undefined;
244
- // the fork supervisor, in the primary of a clustered app and nowhere else
245
- /** @type {{stop: () => void}|undefined} */
246
- this._clusterHandle = undefined;
247
- // readSmallFile's cache and its in-flight reads, see the method
248
- this._fileCache = new Map();
249
- this._fileCacheBytes = 0;
250
- this._fileReadsInFlight = new Map();
251
- // the responses being served right now, an intrusive list: linking is three pointer
252
- // stores where a Set paid identity hashing and table upkeep per request. A holder object
253
- // rather than a bare field, because the callable app copies own scalars by value and two
254
- // copies of a head would disagree; an object rides by reference, the way the Set did
255
- this._pending = /** @type {{ head: Response|null }} */ ({ head: null });
256
- // on the per-app prototype layer, not per response, same as the Set was
257
270
  /** @type {{_pendingIn?: {head: Response|null}}} */ (this.response)._pendingIn = this._pending;
258
- this._draining = false;
259
- // read here, at construction, the way express does; an empty NODE_ENV means development,
260
- // which the ?? in the shared default would miss
271
+ // at construction as express reads it; an empty NODE_ENV is development
261
272
  if (typeof this._settings.env === "undefined") {
262
273
  this._settings.env = process.env.NODE_ENV || "development";
263
274
  }
@@ -270,7 +281,6 @@ class Application extends Router {
270
281
  }
271
282
  }
272
283
  }
273
- // non-enumerable, so the marker never shows up walking the settings
274
284
  Object.defineProperty(this._settings, trustProxyDefaultSymbol, {
275
285
  configurable: true,
276
286
  value: true
@@ -280,9 +290,8 @@ class Application extends Router {
280
290
  }
281
291
 
282
292
  /**
283
- * Parks a promise's settle functions under a key the worker can send back, since a worker
284
- * message carries data and not closures. The counter wraps rather than growing without bound,
285
- * 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.
286
295
  *
287
296
  * @param {(value: Buffer) => void} resolve
288
297
  * @param {(err: Error) => void} reject
@@ -298,9 +307,8 @@ class Application extends Router {
298
307
  }
299
308
 
300
309
  /**
301
- * Reads a file on one of the file threads, picked at random, rather than on the event loop.
302
- * Only worth it below the size where the copy back costs more than the read, which is why
303
- * 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.
304
312
  *
305
313
  * @param {string} path absolute path to read
306
314
  * @returns {Promise<Buffer>}
@@ -315,11 +323,9 @@ class Application extends Router {
315
323
  }
316
324
 
317
325
  /**
318
- * A small file through the worker pool, with two things on top: concurrent asks for the same
319
- * path share one read, and the bytes of an unchanged file come from a bounded cache, validated
320
- * against the stat the caller already paid for, so a touched file is re-read. A hit completes
321
- * on a macrotask, which is when a worker's answer would have arrived.
322
- * `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.
323
329
  *
324
330
  * @param {string} fullpath
325
331
  * @param {import("fs").Stats} stat
@@ -357,17 +363,14 @@ class Application extends Router {
357
363
  return data;
358
364
  });
359
365
  this._fileReadsInFlight.set(fullpath, pending);
360
- // never cached past settlement: a rejection clears the slot the same way
361
366
  const clear = () => this._fileReadsInFlight.delete(fullpath);
362
367
  pending.then(clear, clear);
363
368
  return pending;
364
369
  }
365
370
 
366
371
  /**
367
- * Reads or writes an application setting. One argument is the getter, and the check is on
368
- * `arguments.length`, so `set(key, undefined)` still writes. Some keys have a side effect:
369
- * `trust proxy`, `query parser` and `etag` compile the value into a function kept beside it,
370
- * 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.
371
374
  *
372
375
  * @param {string} key setting name
373
376
  * @param {*} [value] value to store; omit to read instead
@@ -379,19 +382,16 @@ class Application extends Router {
379
382
  }
380
383
  if (key === "trust proxy") {
381
384
  if (!value) {
382
- // compiled, not deleted: an explicit false must shadow a parent's setting when
383
- // 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
384
386
  this._settings["trust proxy fn"] = compileTrust(false);
385
387
  } else {
386
388
  this._settings["trust proxy fn"] = compileTrust(value);
387
389
  }
388
- // set explicitly, so a mount no longer inherits the parent's
389
390
  Object.defineProperty(this._settings, trustProxyDefaultSymbol, {
390
391
  configurable: true,
391
392
  value: false
392
393
  });
393
394
  } else if (key === "stat cache") {
394
- // compiled here so the read path is a number and not a duration to parse per request
395
395
  this._settings["stat cache ms"] = durationSetting(value, "stat cache");
396
396
  } else if (key === "query parser") {
397
397
  if (value === "extended") {
@@ -403,24 +403,18 @@ class Application extends Router {
403
403
  } else if (value === false) {
404
404
  this._settings["query parser fn"] = undefined;
405
405
  } else {
406
- // express's wording, which applications match on
407
406
  throw new TypeError("unknown value for query parser function: " + value);
408
407
  }
409
408
  } else if (key === "etag methods") {
410
- // fulmine's own: the methods whose send() computes a generated ETag. Unset means all
411
- // of them, which is express's behaviour and what its suite asserts per method; naming
412
- // ["GET", "HEAD"] skips the digest everywhere a validator can never match, which
413
- // 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
414
411
  if (value != null && (!Array.isArray(value) || value.some((m) => typeof m !== "string"))) {
415
412
  throw new TypeError('"etag methods" wants an array of method names, or null for all of them');
416
413
  }
417
414
  value = value == null ? undefined : value.map((/** @type {string} */ m) => m.toUpperCase());
418
415
  } else if (key === "etag") {
419
- // The skips are not taken back here. They used to be, because send consults freshness,
420
- // but that branch reads if-none-match, if-modified-since and cache-control by name
421
- // whatever this setting says, and req.fresh reads nothing else off the request.
422
- // Registering a route after listen still takes them back: that is a different
423
- // 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
424
418
  if (typeof value === "function") {
425
419
  this._settings["etag fn"] = value;
426
420
  } else {
@@ -436,14 +430,13 @@ class Application extends Router {
436
430
  delete this._settings["etag fn"];
437
431
  break;
438
432
  default:
439
- // express's wording, which applications match on
440
433
  throw new TypeError("unknown value for etag function: " + value);
441
434
  }
442
435
  }
443
436
  }
444
437
 
445
438
  this._settings[key] = value;
446
- // any app's hot-settings copy may resolve through this one, see Router#_hot
439
+ // see Router#_hot
447
440
  settingsEpoch.n++;
448
441
  return this;
449
442
  }
@@ -469,8 +462,7 @@ class Application extends Router {
469
462
  }
470
463
 
471
464
  /**
472
- * Whether a setting is truthy. Reads through to the app this one is mounted on, as get() does
473
- * 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.
474
466
  * @param {string} key setting name
475
467
  * @returns {boolean}
476
468
  */
@@ -488,25 +480,19 @@ class Application extends Router {
488
480
  }
489
481
 
490
482
  /**
491
- * Router's handleRequest plus the bookkeeping a graceful close() needs: every live response
492
- * is held in a set until it finishes, so close() knows when the last one is done. Native
493
- * 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.
494
484
  *
495
485
  * @param {import("uWebSockets.js").HttpResponse} res uWS response
496
486
  * @param {import("uWebSockets.js").HttpRequest} req uWS request, readable only during this call
497
- * @param {import("./router-utils.js").NativePreset} [preset] a literal registration's constants,
498
- * see nativePreset in the router
499
- * @param {import("./router-utils.js").SkipHolder} [skipHolder] where a granted header skip lives,
500
- * forwarded whole: dropping it here silently turned every skip off, since the native closures
501
- * call this override
502
- * @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
503
491
  */
504
492
  handleRequest(res, req, preset, skipHolder) {
505
493
  const request = super.handleRequest(res, req, preset, skipHolder);
506
- // removal rides the close listener the Response constructor already has, since a second
507
- // once() per request measured a tenth of a microsecond on the hot path.
508
- // An aborted response only flips its flags without emitting 'close', which is why
509
- // 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
510
496
  const response = request.res;
511
497
  const pending = this._pending;
512
498
  response._pendingLinked = true;
@@ -520,10 +506,8 @@ class Application extends Router {
520
506
  }
521
507
 
522
508
  /**
523
- * The µWS app underneath, for anything µWS offers that this does not: socket.io attaches to
524
- * it. Made the first time it is asked for, so an application that only ever answers through
525
- * node's http, a test through supertest or Angular's build extracting routes in a worker
526
- * thread, never loads the binary. See src/uws.js for why that matters.
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.
527
511
  *
528
512
  * @returns {any}
529
513
  */
@@ -535,19 +519,14 @@ class Application extends Router {
535
519
  return this._uwsApp;
536
520
  }
537
521
 
538
- /**
539
- * Registers the catch-all uWS handler, which is what serves every request that no optimized
540
- * route took natively. It walks this app's own chain and, when nothing in it answered, decides
541
- * between an error, the automatic OPTIONS reply and a 404.
542
- */
522
+ /** The catch-all uWS handler, for every request no native route took. */
543
523
  _createRequestHandler() {
544
524
  this.uwsApp.any("/*", (res, req) => this._serveGeneric(res, req));
545
525
  }
546
526
 
547
527
  /**
548
- * Serves one request by walking this app's chain, with no registration-time shortcut. It is
549
- * what the catch-all runs, and also what a native registration falls back to when it sees a
550
- * 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.
551
530
  *
552
531
  * @param {import("uWebSockets.js").HttpResponse} res the uWS response
553
532
  * @param {import("uWebSockets.js").HttpRequest} req the uWS request
@@ -561,11 +540,9 @@ class Application extends Router {
561
540
  try {
562
541
  this._routeRequestDirect(request, response);
563
542
  } finally {
564
- // the synchronous stretch has run under the cork uWS holds for this callback, and
565
- // whatever comes after it is outside
543
+ // the synchronous stretch ran under uWS's own cork
566
544
  response._corkNeeded = true;
567
- // an abort can only arrive after this callback returns, as the native handler's
568
- // finally says: a response that finished inside it never needs uWS told at all
545
+ // an abort can only arrive after this callback returns
569
546
  if (!response.finished) {
570
547
  this._armAbort(res, response);
571
548
  }
@@ -573,12 +550,8 @@ class Application extends Router {
573
550
  }
574
551
 
575
552
  /**
576
- * Binds the server and starts accepting requests.
577
- *
578
- * Returns the app and not an `http.Server`, since there is no node server underneath. The app
579
- * carries `address()`, `close()`, `listening` and the 'listening' and 'close' events; anything
580
- * needing a real server, socket.io being the usual case, wants `app.uwsApp`. A path instead of
581
- * 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.
582
555
  *
583
556
  * @param {number|string} [port] port, or a unix socket path; 0 picks a free port
584
557
  * @param {string} [host] interface to bind; every interface when omitted
@@ -587,13 +560,9 @@ class Application extends Router {
587
560
  * @returns {this} the app, which doubles as the server handle
588
561
  */
589
562
  listen(port, host, backlog, callback) {
590
- // With { cluster } the primary has nothing to bind. Each worker binds this same port with
591
- // uWS's shared flag, SO_REUSEPORT, so the kernel hands each connection to one of them and
592
- // the primary only forks and replaces a worker that dies. Everything below this runs in the
593
- // workers, listen callback included, so once per worker rather than once.
594
- //
595
- // The test is the process and not this app: a second app on a TLS port, without a cluster
596
- // 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
597
566
  if (cluster.isPrimary && isSupervising()) {
598
567
  if (this._clusterWorkers > 0 && !this._clusterHandle) {
599
568
  this._clusterHandle = forkWorkers(this._clusterWorkers);
@@ -601,11 +570,9 @@ class Application extends Router {
601
570
  return this;
602
571
  }
603
572
  this._compileOptimizedRoutes();
604
- // before the catch-all: µWS sends an upgrade to the websocket route even when a
605
- // catch-all covers the same path, so the two coexist and the order is only tidiness
606
573
  registerWebSocketRoutes(this);
607
574
  this._createRequestHandler();
608
- // 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)
609
576
  if (typeof port === "function") {
610
577
  callback = port;
611
578
  port = 0;
@@ -615,45 +582,36 @@ class Application extends Router {
615
582
  } else if (typeof backlog === "function") {
616
583
  callback = backlog;
617
584
  }
618
- // bare listen() and listen(undefined, cb) bind an OS-assigned port, as node does; left
619
- // undefined the port fell through to the unix-socket branch below
585
+ // a bare listen() binds an OS-assigned port, as node does
620
586
  if (port == null) {
621
587
  port = 0;
622
588
  }
623
- // uWS runs this handler from inside its own listen(), so everything it hands back to the
624
- // caller is deferred a tick. Express binds synchronously too but reports through events,
625
- // 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'
626
591
  const onListen = (/** @type {import("uWebSockets.js").us_listen_socket|false} */ socket) => {
627
592
  if (!socket) {
628
593
  /** @type {NodeJS.ErrnoException} */
629
594
  const err = new Error("listen EADDRINUSE: address already in use :::" + port);
630
595
  err.code = "EADDRINUSE";
631
- // Express 5 registers the listen callback on 'error' as well as on 'listening',
632
- // 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
633
597
  if (callback) {
634
598
  return process.nextTick(() => callback.call(this, err));
635
599
  }
636
- // no callback means no 'error' listener either, and an EventEmitter carrying an
637
- // 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
638
601
  return process.nextTick(() => {
639
602
  throw err;
640
603
  });
641
604
  }
642
- // the port is known synchronously, as it is in Express, so address() works as soon as
643
- // listen() returns. The callback is not: running it here would run it before listen()
644
- // had returned, and `const server = app.listen(p, () => server.address())` - the form
645
- // the Express docs use - would die on the temporal dead zone.
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
646
608
  this.port = loadUWS().us_socket_local_port(socket);
647
609
  this.listening = true;
648
610
  this._listenHost = host;
649
- // kept so close() can stop accepting without dropping what is in flight
650
611
  this._listenSocket = socket;
651
612
  process.nextTick(() => {
652
- // `this` is the app, which is what listen() returns here. Express binds it to the
653
- // http.Server, which is what listen() returns there, so
654
- // `function () { this.address() }` reads the same on both.
655
- // The callback goes first: in Express it is registered as a 'listening' listener
656
- // 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
657
615
  if (callback) callback.call(this);
658
616
  this.emit("listening");
659
617
  });
@@ -685,10 +643,7 @@ class Application extends Router {
685
643
  }
686
644
 
687
645
  /**
688
- * Publishes a message to every socket subscribed to a topic, from outside any of them.
689
- *
690
- * The socket's own `publish` reaches the same topics; this one is for the sender that is
691
- * 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.
692
647
  *
693
648
  * @param {string} topic
694
649
  * @param {string|ArrayBuffer|Buffer} message
@@ -711,9 +666,7 @@ class Application extends Router {
711
666
  }
712
667
 
713
668
  /**
714
- * The router the application routes through, which express 5 hands out so that a caller can
715
- * walk `app.router.stack`. Here the application is the router, so it hands back itself and the
716
- * walk finds the same layers.
669
+ * express 5's `app.router`, for a caller walking `app.router.stack`: the application itself.
717
670
  *
718
671
  * @returns {this}
719
672
  */
@@ -729,10 +682,8 @@ class Application extends Router {
729
682
  if (!this.listening || !this.port) {
730
683
  return null;
731
684
  }
732
- // uWS hands back the port and nothing else, so the address reported is the one we asked
733
- // it to bind. No host means every interface, which node reports as "::". A hostname is
734
- // reported as written, since what it resolved to is not readable back from here: node
735
- // 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"
736
687
  const host = this._listenHost;
737
688
  if (!host) {
738
689
  return { address: "::", family: "IPv6", port: this.port };
@@ -741,8 +692,7 @@ class Application extends Router {
741
692
  }
742
693
 
743
694
  /**
744
- * The full mount path of this app, walking up through every parent it is mounted on.
745
- * A top level app returns the empty string rather than "/".
695
+ * The full mount path through every parent, "" at the top level.
746
696
  * @returns {string}
747
697
  */
748
698
  path() {
@@ -757,9 +707,7 @@ class Application extends Router {
757
707
  }
758
708
 
759
709
  /**
760
- * Registers a template engine for a file extension.
761
- *
762
- * 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.
763
711
  *
764
712
  * @param {string} ext file extension the engine handles
765
713
  * @param {(path: string, options: object, callback: (err: Error|null, rendered?: string) => void) => void} fn
@@ -777,29 +725,22 @@ class Application extends Router {
777
725
  }
778
726
 
779
727
  /**
780
- * Renders a view and hands the result to the callback, without sending anything.
781
- * `res.render()` is the one that responds.
782
- *
783
- * `app.locals` and `options._locals` are merged into the options, in that order, so a
784
- * per-request local wins. Caching follows the "view cache" setting unless `options.cache` says
785
- * 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.
786
731
  *
787
732
  * @param {string} name view name, resolved against the "views" setting
788
- * @param {Record<string, any>|((err: Error|null, html?: string) => void)} [options] locals for
789
- * the view, or the callback in its place
790
- * @param {(err: Error|null, html?: string) => void} [callback] receives the rendered view. It
791
- * 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
792
736
  */
793
737
  render(name, options, callback) {
794
738
  if (typeof options === "function") {
795
739
  callback = /** @type {(err: Error|null, html?: string) => void} */ (options);
796
740
  options = new NullObject();
797
741
  }
798
- // render exists to hand the result somewhere, so there is always a callback by this point:
799
- // either the third argument or the second one, shuffled above
800
742
  const done = /** @type {(err: Error|null, html?: string) => void} */ (callback);
801
- // express's order, least specific first: app.locals, then res.locals riding in as _locals,
802
- // 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
803
744
  const opts = options || new NullObject();
804
745
  options = new NullObject();
805
746
  for (const key in this.locals) {
@@ -828,11 +769,8 @@ class Application extends Router {
828
769
  view = new View(name, {
829
770
  defaultEngine: this.get("view engine"),
830
771
  root: this.get("views"),
831
- // the object itself, not a copy of it: a mounted app reaches its parent's engines
832
- // through the prototype chain, and a spread only carries what the app owns, so a
833
- // sub-app rendering with the parent's engine went off to require() a module named
834
- // after the extension. Express hands its own object over too, and means to: a view
835
- // 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
836
774
  engines: this.engines
837
775
  });
838
776
  if (!view.path) {
@@ -864,23 +802,17 @@ class Application extends Router {
864
802
  }
865
803
 
866
804
  /**
867
- * Stops accepting connections, lets in-flight requests finish, then emits 'close'.
868
- *
869
- * Node's server.close() only closes the listen socket and waits for what is being served; uWS's
870
- * close() terminates every connection, so calling it first aborted whatever a graceful shutdown
871
- * was waiting for. It still runs, but only once the last pending response is done, to drop the
872
- * idle keep-alive connections nothing else would close.
873
- *
874
- * The callback is the first 'close' listener. Closing a server that was not listening still
875
- * 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.
876
809
  *
877
810
  * @param {(err?: Error) => void} [callback] called once closed
878
811
  * @returns {this} the app, for chaining
879
812
  */
880
813
  close(callback) {
881
- // the primary of a clustered app never bound anything, so closing it means stopping the
882
- // workers. They are killed rather than drained: each one holds its own listening socket
883
- // 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
884
816
  if (this._clusterHandle) {
885
817
  this._clusterHandle.stop();
886
818
  this._clusterHandle = undefined;
@@ -892,8 +824,7 @@ class Application extends Router {
892
824
  }
893
825
  const wasListening = this.listening;
894
826
  this.listening = false;
895
- // in Express the close callback is nothing more than the first 'close' listener, and a
896
- // server that was not running still gets called back, with an error
827
+ // the callback is the first 'close' listener, as in Express
897
828
  if (callback) {
898
829
  this.once("close", () => {
899
830
  if (wasListening) {
@@ -906,8 +837,7 @@ class Application extends Router {
906
837
  });
907
838
  }
908
839
  if (!this.listenCalled || !wasListening) {
909
- // a close while a drain is underway does not emit again: the pending drain's single
910
- // 'close' serves both calls, which is what node does too
840
+ // a close during a drain does not emit again, as in node
911
841
  if (!this._draining) {
912
842
  process.nextTick(() => this.emit("close"));
913
843
  }
@@ -927,12 +857,11 @@ class Application extends Router {
927
857
  process.nextTick(finish);
928
858
  return this;
929
859
  }
930
- // a finished response emits 'close' and unlinks itself; an aborted one only flips its
931
- // 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
932
862
  const sweep = setInterval(() => {
933
863
  let response = this._pending.head;
934
864
  while (response !== null) {
935
- // taken before the unlink, which nulls the pointers
936
865
  const next = response._pendingNext;
937
866
  if (response.finished || response.aborted) {
938
867
  response._unlinkPending();
@@ -948,22 +877,15 @@ class Application extends Router {
948
877
  }
949
878
  }
950
879
 
951
- // An app is a function, as it is in Express, and not the Application instance whose properties it
952
- // carries. Middleware that takes a whole app and calls it, vhost being the one everybody meets, was
953
- // given something it could not call.
954
- //
955
- // Tried once before and reverted the same day, because a callable app broke supertest: `request(app)`
956
- // reads `typeof app === "function"` and wraps what it finds in http.createServer, and there was
957
- // 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.
958
882
  /** @param {object} [options] the settings express() takes, see the Application constructor */
959
883
  module.exports = function (options) {
960
884
  return new Application(options)._asCallable();
961
885
  };
962
886
 
963
- // the class itself, so index.js can expose its prototype as express.application does. Adding a
964
- // 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
965
888
  module.exports.Application = Application;
966
889
 
967
- // and what makes an application answer as an http.Server, since that is what a library handed the
968
- // 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
969
891
  addServerMembers(Application.prototype);