fulmine.js 5.19.8 → 5.19.9

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.8",
3
+ "version": "5.19.9",
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": {
@@ -47,7 +47,7 @@ const cpuCount = os.cpus().length;
47
47
  // mounted sub-app knows it may inherit the parent's
48
48
  const trustProxyDefaultSymbol = "@@symbol:trust_proxy_default";
49
49
 
50
- const workers = [];
50
+ const workers = /** @type {FSWorker[]} */ ([]);
51
51
  let taskKey = 0;
52
52
  const workerTasks = new NullObject();
53
53
 
@@ -133,7 +133,7 @@ class Application extends Router {
133
133
  becomeSupervisor();
134
134
  }
135
135
  if (settings.uwsApp) {
136
- this.uwsApp = settings.uwsApp;
136
+ this.uwsApp = /** @type {import("uWebSockets.js").TemplatedApp} */ (settings.uwsApp);
137
137
  } else if (settings.http3) {
138
138
  // uWS.H3App exists in the pinned build but its QUIC stack does not: the constructor
139
139
  // segfaults on Linux and hangs forever on Windows before serving a single request,
@@ -401,7 +401,7 @@ class Application extends Router {
401
401
  if (value != null && (!Array.isArray(value) || value.some((m) => typeof m !== "string"))) {
402
402
  throw new TypeError('"etag methods" wants an array of method names, or null for all of them');
403
403
  }
404
- value = value == null ? undefined : value.map((m) => m.toUpperCase());
404
+ value = value == null ? undefined : value.map((/** @type {string} */ m) => m.toUpperCase());
405
405
  } else if (key === "etag") {
406
406
  // The skips are not taken back here. They used to be, because send consults freshness,
407
407
  // but that branch reads if-none-match, if-modified-since and cache-control by name
@@ -594,7 +594,7 @@ class Application extends Router {
594
594
  // uWS runs this handler from inside its own listen(), so everything it hands back to the
595
595
  // caller is deferred a tick. Express binds synchronously too but reports through events,
596
596
  // and node emits both 'listening' and 'error' from a process.nextTick.
597
- const onListen = (socket) => {
597
+ const onListen = (/** @type {import("uWebSockets.js").us_listen_socket|false} */ socket) => {
598
598
  if (!socket) {
599
599
  /** @type {NodeJS.ErrnoException} */
600
600
  const err = new Error("listen EADDRINUSE: address already in use :::" + port);
@@ -926,6 +926,7 @@ class Application extends Router {
926
926
  // Tried once before and reverted the same day, because a callable app broke supertest: `request(app)`
927
927
  // reads `typeof app === "function"` and wraps what it finds in http.createServer, and there was
928
928
  // nothing underneath that could serve node's IncomingMessage. src/node-shim.js closes that hole.
929
+ /** @param {object} [options] the settings express() takes, see the Application constructor */
929
930
  module.exports = function (options) {
930
931
  return new Application(options)._asCallable();
931
932
  };
package/src/cli.js CHANGED
@@ -214,6 +214,7 @@ function findSpecifiersTypeScript(source, fileName, ts, seen) {
214
214
  }
215
215
  };
216
216
 
217
+ /** @param {import("typescript").Node} node */
217
218
  const visit = (node) => {
218
219
  // import express from "express", import type { Request } from "express", export * from it.
219
220
  // A type-only import is rewritten too: the types come from the new package as well.
@@ -465,7 +466,7 @@ function findEntry(given) {
465
466
  * owns listen.
466
467
  *
467
468
  * @param {string} entry
468
- * @returns {object[]} the prototypes to stub, this command's copy first
469
+ * @returns {Application[]} the prototypes to stub, this command's copy first
469
470
  */
470
471
  function listenOwners(entry) {
471
472
  const builds = new Set([require("./index.js")]);
@@ -477,6 +478,7 @@ function listenOwners(entry) {
477
478
  }
478
479
  }
479
480
 
481
+ /** @type {Application[]} */
480
482
  const owners = [];
481
483
  for (const build of builds) {
482
484
  if (typeof build !== "function") {
@@ -531,6 +533,7 @@ function loadApps(argv, command) {
531
533
  return null;
532
534
  }
533
535
 
536
+ /** @type {Application[]} */
534
537
  const listened = [];
535
538
  const real = owners.map((proto) => proto.listen);
536
539
  for (const proto of owners) {
@@ -161,6 +161,7 @@ function reusableCompressor(create, finishFlag, oneShot) {
161
161
  return oneShot;
162
162
  };
163
163
 
164
+ /** @param {Buffer} body */
164
165
  const compress = (body) => {
165
166
  if (broken || busy) {
166
167
  return oneShot(body);
@@ -26,6 +26,7 @@ const uWSAny = /** @type {any} */ (uWS);
26
26
  const statuses = require("statuses");
27
27
 
28
28
  /** @typedef {import("./application.js").Application} Application */
29
+ /** @typedef {import("./router.js")} Router */
29
30
 
30
31
  const parser = acorn.Parser;
31
32
 
@@ -45,7 +46,11 @@ const allowedResMethods = [
45
46
 
46
47
  const allowedIdentifiers = ["query", "params", ...allowedResMethods];
47
48
 
48
- /** What res.type(x) sets the content type to. A lookup on a literal. */
49
+ /**
50
+ * What res.type(x) sets the content type to. A lookup on a literal.
51
+ *
52
+ * @param {string} type
53
+ */
49
54
  const typeValueOf = (type) => (type.indexOf("/") === -1 ? contentTypeFor(type) : type);
50
55
 
51
56
  // what one instruction of a declarative response can carry, since uWS writes its length as a u16
@@ -307,7 +312,7 @@ function readStatusAndHeaders(callExprs, headers) {
307
312
  * @param {any[]} callExprs the res calls, in run order, as readStatusAndHeaders takes them
308
313
  * @param {[string, string][]} headers the headers read so far, written to
309
314
  * @param {any[]} body the body parts, written to; loose because a literal's value is kept as it is
310
- * @param {Application} app the application, for the json settings
315
+ * @param {Application|Router} app the application or router the route hangs on, for the json settings
311
316
  * @param {string[]} queries names bound by a destructured req.query
312
317
  * @param {string[]} params names bound by a destructured req.params
313
318
  * @returns {{sendUsed: boolean, bodyFromSend: boolean}|null}
@@ -438,6 +443,7 @@ function readBody(callExprs, headers, body, app, queries, params) {
438
443
  }
439
444
  body.push({ type: arg.object.property.name, value: arg.property.name });
440
445
  } else if (arg.type === "BinaryExpression") {
446
+ /** @type {any[]} the parts, in the same loose shape as body */
441
447
  const stuff = [];
442
448
  /**
443
449
  * Reads a chain of string concatenations right to left. Each side must be a literal or a
@@ -768,6 +774,10 @@ function identifiersAllowed(fn, args, names) {
768
774
  // - doesnt create variables
769
775
  // - only uses req.query and req.params
770
776
  // basically, its only simple, static responses
777
+ /**
778
+ * @param {Function} cb the handler
779
+ * @param {Application|Router} app the application or router the route hangs on, for the json settings
780
+ */
771
781
  module.exports = function compileDeclarative(cb, app) {
772
782
  try {
773
783
  const handler = readHandler(cb);
@@ -791,8 +801,8 @@ module.exports = function compileDeclarative(cb, app) {
791
801
  return false;
792
802
  }
793
803
 
804
+ /** @type {[string, string][]} */
794
805
  const headers = [];
795
- const body = [];
796
806
 
797
807
  const status = readStatusAndHeaders(callExprs, headers);
798
808
  if (status === null) {
@@ -800,6 +810,8 @@ module.exports = function compileDeclarative(cb, app) {
800
810
  }
801
811
  const { statusCode, sendStatusUsed } = status;
802
812
 
813
+ /** @type {any[]} loose because a literal's value is kept as it is, see readBody */
814
+ const body = [];
803
815
  const read = readBody(callExprs, headers, body, app, queries, params);
804
816
  if (read === null) {
805
817
  return false;
@@ -27,14 +27,21 @@ class HotSettings {
27
27
  constructor() {
28
28
  this.epoch = 0;
29
29
  this.xPoweredBy = false;
30
+ /** @type {((body: string|Buffer|import("fs").Stats, encoding?: BufferEncoding) => string)|undefined} */
30
31
  this.etagFn = undefined;
31
32
  // null means every method, which is express's behaviour and the default
33
+ /** @type {Set<string>|null} */
32
34
  this.etagMethods = null;
35
+ /** @type {((query: string|null) => Record<string, any>)|undefined} */
33
36
  this.queryParserFn = undefined;
37
+ /** @type {import("./utils.js").TrustFn|undefined} */
34
38
  this.trustProxyFn = undefined;
35
39
  this.trustProxyProtocol = false;
40
+ /** @type {boolean|undefined} */
36
41
  this.jsonEscape = undefined;
42
+ /** @type {any} the "json replacer" setting, as stringify takes it */
37
43
  this.jsonReplacer = undefined;
44
+ /** @type {string|number|undefined} */
38
45
  this.jsonSpaces = undefined;
39
46
  }
40
47
  }
package/src/index.js CHANGED
@@ -68,6 +68,7 @@ try {
68
68
  module.exports = /** @type {any} */ (Application);
69
69
 
70
70
  // a router is a function too: it has to be callable to be used as middleware
71
+ /** @param {object} [options] the options express.Router() takes */
71
72
  module.exports.Router = function (options) {
72
73
  return new Router(options)._asCallable();
73
74
  };
@@ -139,6 +139,7 @@ function stripBom(text) {
139
139
  return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
140
140
  }
141
141
 
142
+ /** @type {typeof import("iconv-lite")|undefined} */
142
143
  let iconv;
143
144
 
144
145
  /**
@@ -966,6 +967,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
966
967
  (contentType) => !!typeis.is(contentType, /** @type {string[]} */ (options.type))
967
968
  );
968
969
 
970
+ /** @type {string[]|null|undefined} the "body methods" setting, read on the first request */
969
971
  let additionalMethods;
970
972
 
971
973
  const parserMiddleware = (req, res, next) => {
@@ -1038,6 +1040,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
1038
1040
  // halves sit on either side of the encoding, which is the order body-parser reads
1039
1041
  // them in: the charset this parser accepts at all, then the Content-Encoding, then
1040
1042
  // whether iconv knows the charset.
1043
+ /** @type {string|undefined} */
1041
1044
  let encoding;
1042
1045
  if (charsetPolicy) {
1043
1046
  encoding = charsetOf(type) ?? defaultCharset;
@@ -1129,7 +1132,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
1129
1132
  // not us, and would wait for an end that is never coming
1130
1133
  req.complete = true;
1131
1134
  req.readable = false;
1132
- req._res.collectBody(limit, (body) => {
1135
+ req._res.collectBody(limit, (/** @type {ArrayBuffer|null} */ body) => {
1133
1136
  if (body === null) {
1134
1137
  // over maxSize: uWS refused it natively
1135
1138
  return next(
@@ -1165,6 +1168,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
1165
1168
  // known and we are not inflating, the final size is known up front, so chunks go
1166
1169
  // straight into one buffer and the body is copied once. The cap means a client that
1167
1170
  // declares a body and never sends it costs no more than one that sends it
1171
+ /** @type {Buffer[]} */
1168
1172
  const abs = [];
1169
1173
  const declaredLength = inflate ? -1 : Number(length);
1170
1174
  let target =
@@ -1310,7 +1314,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
1310
1314
  // if we are fast enough (not async), we can do it
1311
1315
  // otherwise we need to use a stream since it already started streaming it
1312
1316
  if (!req.receivedData) {
1313
- req._res.onData((ab, isLast) => {
1317
+ req._res.onData((/** @type {ArrayBuffer} */ ab, /** @type {boolean} */ isLast) => {
1314
1318
  onData(ab);
1315
1319
  if (isLast) {
1316
1320
  // this subscription replaced the Readable's own, so the stream will
package/src/node-shim.js CHANGED
@@ -39,21 +39,30 @@ function addressToBytes(address) {
39
39
  if (!address) {
40
40
  return new ArrayBuffer(0);
41
41
  }
42
- // node reports an IPv4 client on a dual stack socket as ::ffff:127.0.0.1, and the address that
43
- // belongs in req.ip is the v4 one
44
- const mapped = address.startsWith("::ffff:") ? address.slice(7) : address;
45
- if (mapped.includes(".")) {
46
- const parts = mapped.split(".");
42
+ // node reports an IPv4 client on a dual stack socket as ::ffff:127.0.0.1 and one on an IPv4
43
+ // socket as 127.0.0.1, which is the difference req.ip reads back out of the width: uWS hands
44
+ // the mapped peer over as the sixteen bytes, the plain one as four. Keeping node's own form
45
+ // rather than flattening both to four is what makes the shim answer what node answers,
46
+ // whichever socket accepted the connection
47
+ const mapped = address.startsWith("::ffff:") && address.includes(".");
48
+ const dotted = mapped ? address.slice(7) : address;
49
+ if (dotted.includes(".")) {
50
+ const parts = dotted.split(".");
47
51
  if (parts.length !== 4) {
48
52
  return new ArrayBuffer(0);
49
53
  }
50
- const bytes = new Uint8Array(4);
54
+ const bytes = new Uint8Array(mapped ? 16 : 4);
55
+ const offset = mapped ? 12 : 0;
56
+ if (mapped) {
57
+ bytes[10] = 0xff;
58
+ bytes[11] = 0xff;
59
+ }
51
60
  for (let i = 0; i < 4; i++) {
52
61
  const value = Number(parts[i]);
53
62
  if (!Number.isInteger(value) || value < 0 || value > 255) {
54
63
  return new ArrayBuffer(0);
55
64
  }
56
- bytes[i] = value;
65
+ bytes[offset + i] = value;
57
66
  }
58
67
  return bytes.buffer;
59
68
  }
@@ -80,13 +89,19 @@ function addressToBytes(address) {
80
89
  return view.buffer;
81
90
  }
82
91
 
83
- /** A chunk as the ArrayBuffer uWS deals in, copied rather than viewed so nothing aliases node's. */
92
+ /**
93
+ * A chunk as the ArrayBuffer uWS deals in, copied rather than viewed so nothing aliases node's.
94
+ *
95
+ * @param {ArrayBuffer|Buffer|string} chunk
96
+ * @returns {ArrayBuffer}
97
+ */
84
98
  function toArrayBuffer(chunk) {
85
99
  if (chunk instanceof ArrayBuffer) {
86
100
  return chunk;
87
101
  }
88
102
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
89
- return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
103
+ // node types the store as possibly shared, but a Buffer it made or was given here never is
104
+ return /** @type {ArrayBuffer} */ (buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength));
90
105
  }
91
106
 
92
107
  /**
@@ -175,6 +190,7 @@ class NodeHttpResponse {
175
190
  // how much of the body has gone out, which is what uWS reports through getWriteOffset and
176
191
  // hands back to an onWritable callback
177
192
  this._offset = 0;
193
+ /** @type {((offset: number) => boolean)|null} */
178
194
  this._onWritable = null;
179
195
  this._aborted = false;
180
196
  // the current body handler: uWS keeps one and a second onData replaces it, so this does too
@@ -195,6 +211,8 @@ class NodeHttpResponse {
195
211
  * uWS batches everything written inside this into one syscall. node has no equivalent that
196
212
  * means the same thing, and its own cork would hold the write until the callback returned
197
213
  * without changing what is sent, so this only runs it.
214
+ *
215
+ * @param {() => void} cb
198
216
  */
199
217
  cork(cb) {
200
218
  cb();
@@ -300,13 +318,19 @@ class NodeHttpResponse {
300
318
  * Called when there is room to write again. uWS asks the handler to answer whether it managed
301
319
  * to write everything, and calls it again if not; node's drain says nothing, so the handler is
302
320
  * kept until the next drain and its answer ignored.
321
+ *
322
+ * @param {(offset: number) => boolean} handler
303
323
  */
304
324
  onWritable(handler) {
305
325
  this._onWritable = handler;
306
326
  return this;
307
327
  }
308
328
 
309
- /** Called when the connection goes before the response is finished. */
329
+ /**
330
+ * Called when the connection goes before the response is finished.
331
+ *
332
+ * @param {() => void} handler
333
+ */
310
334
  onAborted(handler) {
311
335
  this._nodeRes.on("close", () => {
312
336
  if (!this._nodeRes.writableFinished) {
package/src/optimizer.js CHANGED
@@ -51,6 +51,7 @@ const {
51
51
 
52
52
  // router.js requires this file while it is still being evaluated, so its class cannot be required
53
53
  // from here: it hands it over on the line under its own export instead.
54
+ /** @type {typeof import("./router.js")} */
54
55
  let Router;
55
56
 
56
57
  /**
@@ -233,6 +234,12 @@ function compileOptimizedRoutes(root) {
233
234
 
234
235
  // pathPrefix/chainPrefix accumulate across nested sole-callback mounts, and outerGuards
235
236
  // carries what was written before them and answers only part of what is under them
237
+ /**
238
+ * @param {Router} router
239
+ * @param {string} pathPrefix
240
+ * @param {RouteEntry[]} chainPrefix
241
+ * @param {import("./router-utils.js").MountGuard[]} outerGuards
242
+ */
236
243
  const walk = (router, pathPrefix, chainPrefix, outerGuards) => {
237
244
  for (const route of router._routes) {
238
245
  if (route.use) {
@@ -344,7 +351,7 @@ function compileOptimizedRoutes(root) {
344
351
  if (route._caseGuards) {
345
352
  // compared against the whole path µWS matched, so they carry the mount
346
353
  // prefix, folded along with the rest of it
347
- registered._caseGuards = route._caseGuards.map((p) => pathPrefix + p);
354
+ registered._caseGuards = route._caseGuards.map((/** @type {string} */ p) => pathPrefix + p);
348
355
  }
349
356
  root._registerUwsRoute(registered, chain);
350
357
  // the chain holds the original object, so the request-time guard has to find
@@ -388,18 +395,27 @@ function registerUwsRoute(router, route, optimizedPath) {
388
395
  method = "del";
389
396
  }
390
397
  if (route.path.includes(":")) {
391
- route.optimizedParams = route.path.match(regExParam).map((p) => p.slice(1));
398
+ route.optimizedParams = route.path.match(regExParam).map((/** @type {string} */ p) => p.slice(1));
392
399
  }
393
400
  // null for almost every route: only a parameter route with an earlier literal that a case
394
401
  // variant could slip past carries one, see _optimizeRoute. Built once here, and matched
395
402
  // insensitively, since that is the folding the guard exists for
396
403
  const caseGuards = route._caseGuards
397
- ? route._caseGuards.map((p) => (needsConversionToRegex(p) ? patternToRegex(p, false, false) : p.toLowerCase()))
404
+ ? route._caseGuards.map((/** @type {string} */ p) =>
405
+ needsConversionToRegex(p) ? patternToRegex(p, false, false) : p.toLowerCase()
406
+ )
398
407
  : null;
408
+ /**
409
+ * @param {RouteEntry[]} chain
410
+ * @param {import("./router-utils.js").NativePreset|undefined} preset
411
+ * @param {{skipHeaders: boolean, skipQuery: boolean}} skips
412
+ * @param {string|null} wireMethod
413
+ */
399
414
  const makeHandler = (chain, preset, skips, wireMethod) => {
400
415
  // the mutable object a granted skip lives on, so a middleware arriving after listen can
401
416
  // take it back: a literal registration's preset doubles as it, a parameterised one gets a
402
417
  // holder of its own. It carries the method too, so the constructor settles it in one compare
418
+ /** @type {import("./router-utils.js").SkipHolder|undefined} */
403
419
  let skipHolder = preset;
404
420
  if (skipHolder === undefined && (skips.skipHeaders || skips.skipQuery || wireMethod !== null)) {
405
421
  skipHolder = {
@@ -504,6 +520,11 @@ function registerUwsRoute(router, route, optimizedPath) {
504
520
  }
505
521
  }
506
522
  // remembered so a middleware or setting arriving after listen can take the skips back
523
+ /**
524
+ * @param {string} path
525
+ * @param {string} method
526
+ * @param {{skipHeaders: boolean, skipQuery: boolean}} skips
527
+ */
507
528
  const makePreset = (path, method, skips) => {
508
529
  const preset = nativePreset(path, method);
509
530
  if (skips.skipHeaders || skips.skipQuery) {
@@ -104,8 +104,9 @@ function isMappedIPv4(bytes) {
104
104
 
105
105
  /**
106
106
  * Whether node would report an IPv4 peer of this app in mapped form, "::ffff:a.b.c.d". Node maps it
107
- * whenever the listener is dual stack, which is every listen() without an IPv4 address. uWS already
108
- * gives mapped peers as sixteen bytes, four bytes come only from a v4 listener or the node shim.
107
+ * whenever the listener is dual stack, which is every listen() without an IPv4 address. Only the
108
+ * address invented after the response has ended asks: every address actually read says which form
109
+ * it is in by its width, uWS handing a mapped peer over as sixteen bytes and a plain one as four.
109
110
  *
110
111
  * @param {import("./application.js").Application} app the application the request arrived at
111
112
  * @returns {boolean}
package/src/request.js CHANGED
@@ -204,13 +204,6 @@ module.exports = class Request extends LazyReadable {
204
204
  */
205
205
  rawIp;
206
206
 
207
- /**
208
- * Whether rawIp came from a PROXY protocol preamble rather than from the socket. Only the
209
- * IPv4 mapping reads it, see parsedIp. Declared for the same reason as rawIp.
210
- * @type {boolean}
211
- */
212
- _ipFromProxy = false;
213
-
214
207
  /**
215
208
  * Whether the request declared a body, content-length or transfer-encoding, spotted during
216
209
  * the header copy. Declared for the same reason as rawIp.
@@ -457,7 +450,9 @@ module.exports = class Request extends LazyReadable {
457
450
  this._lastMethod = this.method;
458
451
  // the folded _opPath and the percent scan of _originalPath, built on the hop that first
459
452
  // wants them and dropped by every rewrite, see _pathMatches and Walk#dispatch
453
+ /** @type {string|null} */
460
454
  this._opPathLower = null;
455
+ /** @type {boolean|null} */
461
456
  this._mayFailDecode = null;
462
457
  this.params = {};
463
458
 
@@ -478,6 +473,7 @@ module.exports = class Request extends LazyReadable {
478
473
  this._paramStack = null;
479
474
  // route and application in pairs, one pair per mounted application entered from another
480
475
  // application, so handing back puts the one that was current back, see rememberApp
476
+ /** @type {any[]|undefined} route and app alternating, loose because the pairs share one array */
481
477
  this._appStack = undefined;
482
478
  this.receivedData = false;
483
479
  // node's IncomingMessage flag: false until the whole body has arrived. on-finished
@@ -1048,7 +1044,6 @@ module.exports = class Request extends LazyReadable {
1048
1044
  const proxied = uwsRes.getProxiedRemoteAddress();
1049
1045
  // empty unless a preamble arrived, which is the only thing that tells the two apart
1050
1046
  if (proxied.byteLength !== 0) {
1051
- this._ipFromProxy = true;
1052
1047
  return proxied;
1053
1048
  }
1054
1049
  }
@@ -1084,14 +1079,12 @@ module.exports = class Request extends LazyReadable {
1084
1079
  /** @type {string|undefined} */
1085
1080
  let ip;
1086
1081
  if (rawIp.byteLength === 4) {
1087
- // ipv4
1082
+ // ipv4, and plain: four bytes mean the peer arrived over IPv4 on an IPv4 socket, which
1083
+ // is what node writes plain as well. A dual stack listener hands an IPv4 peer over as
1084
+ // the mapped sixteen below, the node shim passes node's own form through, and an
1085
+ // address a proxy declared is the four numbers the proxy sent, so none of them wants
1086
+ // a prefix invented here
1088
1087
  ip = new Uint8Array(rawIp).join(".");
1089
- // the mapped form belongs to a dual stack listener, which is what makes an IPv4 peer
1090
- // arrive as ::ffff:a.b.c.d. An address a proxy declared never came through that socket,
1091
- // so it is left as the four numbers the proxy sent
1092
- if (!this._ipFromProxy && mapsIPv4Peer(this.app)) {
1093
- ip = "::ffff:" + ip;
1094
- }
1095
1088
  } else if (rawIp.byteLength === 16) {
1096
1089
  const bytes = new Uint8Array(rawIp);
1097
1090
  if (isMappedIPv4(bytes)) {
@@ -68,7 +68,7 @@ for (const s of [
68
68
  // One status line per code, built on first use: the default path, with no custom reason phrase,
69
69
  // paid a template string and a trim per request for a line that never changes. Bounded to real
70
70
  // HTTP codes so a wild writeHead value cannot grow the array or flip it into dictionary mode.
71
- const STATUS_LINES = [];
71
+ const STATUS_LINES = /** @type {string[]} */ ([]);
72
72
  /**
73
73
  * @param {number} code
74
74
  * @param {string|undefined} text an explicit reason phrase, which bypasses the cache
package/src/response.js CHANGED
@@ -208,6 +208,10 @@ module.exports = class Response extends LazyWritable {
208
208
  this.req = req;
209
209
  this._res = res;
210
210
  this.headersSent = false;
211
+ // whether a body goes out, decided from the method that arrived, as node decides it when
212
+ // it builds its ServerResponse: a middleware that rewrites req.method afterwards, which
213
+ // method-override does, changes what the router matches and not what the wire carries
214
+ this._hasBody = req._isHead !== true;
211
215
  this.app = app;
212
216
  this.locals = new NullObject();
213
217
  this.finished = false;
@@ -238,6 +242,7 @@ module.exports = class Response extends LazyWritable {
238
242
  this.headers["x-powered-by"] = "Fulmine";
239
243
  }
240
244
 
245
+ /** @type {unknown} a slot for whatever a middleware puts on res.body, never read here */
241
246
  this.body = undefined;
242
247
  // what was handed to uWS, kept so a caller asking for content-length after the fact can be
243
248
  // answered, see get(). Undefined until the response ends, and for one that sends no body
@@ -811,7 +816,7 @@ module.exports = class Response extends LazyWritable {
811
816
  } else {
812
817
  // a Buffer goes to uWS as the view it is: copying it into a fresh ArrayBuffer was
813
818
  // an allocation per body, and uWS reads the view's own offset and length
814
- if (this.req.method === "HEAD") {
819
+ if (!this._hasBody) {
815
820
  const length = Buffer.byteLength(data ?? "");
816
821
  this.headers["content-length"] = String(length);
817
822
  this._res.endWithoutBody(length, closeConnection);
@@ -961,6 +966,15 @@ module.exports = class Response extends LazyWritable {
961
966
  delete this.headers["transfer-encoding"];
962
967
  body = "";
963
968
  }
969
+ // express's send reads req.method here and hands node's end() nothing for a HEAD, the
970
+ // length already set, and node frames what it is handed: a GET a middleware made a HEAD
971
+ // answers its length and no body. end() alone decides by the wire, see _hasBody
972
+ if (this.req.method === "HEAD") {
973
+ if (this.statusCode !== 204 && this.statusCode !== 304) {
974
+ this.headers["content-length"] = String(Buffer.byteLength(body));
975
+ }
976
+ return this.end();
977
+ }
964
978
  return this.end(body);
965
979
  }
966
980
 
package/src/route.js CHANGED
@@ -48,6 +48,10 @@ class Route {
48
48
  * @returns {boolean}
49
49
  */
50
50
  handlesMethod(method) {
51
+ // all() answers every verb, registered or not, which is what express reads _all for
52
+ if (this.methods._all) {
53
+ return true;
54
+ }
51
55
  const lowered = method.toLowerCase();
52
56
  return Boolean(this.methods[lowered] || (lowered === "head" && this.methods.get));
53
57
  }
@@ -87,6 +91,7 @@ class Route {
87
91
  }
88
92
  req.route = this;
89
93
 
94
+ /** @param {unknown} [err] */
90
95
  const next = (err) => {
91
96
  // next("route") leaves this route, and next("router") leaves whoever is running it
92
97
  if (err === "route") {
@@ -129,7 +134,7 @@ class Route {
129
134
  // does it. A bare rejection carries none and gets the one express invents. Thenable
130
135
  // too, which express deprecates but still waits for
131
136
  if (out && typeof out.then === "function") {
132
- out.then(null, (thrown) => next(thrown || new Error("Rejected promise")));
137
+ out.then(null, (/** @type {unknown} */ thrown) => next(thrown || new Error("Rejected promise")));
133
138
  }
134
139
  } catch (thrown) {
135
140
  next(thrown);
@@ -162,14 +167,15 @@ for (const method of ["all", ...METHODS.map((verb) => verb.toLowerCase())]) {
162
167
  if (method !== "all" && typeof Route.prototype[method] === "function") {
163
168
  continue;
164
169
  }
165
- Route.prototype[method] = function (...handlers) {
170
+ Route.prototype[method] = function (/** @type {unknown[]} */ ...handlers) {
166
171
  const flattened = handlers.flat(Infinity);
167
172
  checkRouteHandlers(method, flattened);
168
- for (const handle of flattened) {
173
+ // the check above threw on anything else
174
+ for (const handle of /** @type {Function[]} */ (flattened)) {
169
175
  this.stack.push({ method: method === "all" ? undefined : method, handle });
170
- if (method !== "all") {
171
- this.methods[method] = true;
172
- }
176
+ // express marks the route _all rather than naming every verb, and whoever reads
177
+ // req.route.methods reads that key, so it is written the same way here
178
+ this.methods[method === "all" ? "_all" : method] = true;
173
179
  }
174
180
  return this;
175
181
  };
@@ -175,8 +175,14 @@ function layerFor(route, callback) {
175
175
  * @returns {Layer} the layer object, which is express's shape and not one of ours
176
176
  */
177
177
  function routeLayer(route) {
178
+ /**
179
+ * @param {Request} req
180
+ * @param {Response} res
181
+ * @param {(err?: unknown) => void} next
182
+ */
178
183
  const handle = function handle(req, res, next) {
179
184
  let index = 0;
185
+ /** @param {unknown} [err] */
180
186
  const step = (err) => {
181
187
  const callback = route.callbacks[index++];
182
188
  if (callback === undefined) {
@@ -206,6 +212,7 @@ function routeLayer(route) {
206
212
  * The 404 epilogue stays on a microtask, where the await used to resume: a middleware that writes
207
213
  * after calling next() must still win the headersSent check, as it does in express.
208
214
  * @this {Walk}
215
+ * @param {RouteEntry|false} matched what the walk ended on, as the resolve receives it
209
216
  */
210
217
  function nativeDone(matched) {
211
218
  if (this.settled) {
@@ -238,6 +245,7 @@ function nativeDone(matched) {
238
245
  * an unhandled rejection. Deferred like the resolve, since every rejection used to reach the
239
246
  * handler's catch through an await.
240
247
  * @this {Walk}
248
+ * @param {unknown} err
241
249
  */
242
250
  function nativeFail(err) {
243
251
  if (this.settled) {
@@ -313,7 +321,7 @@ const PATH_PROPERTY = {
313
321
  const ABSORB_URL = Request.prototype._absorbUrlRewrite;
314
322
  const ABSORB_METHOD = Request.prototype._absorbMethodRewrite;
315
323
 
316
- const NO_PARAM_NAMES = [];
324
+ const NO_PARAM_NAMES = /** @type {string[]} */ ([]);
317
325
 
318
326
  /**
319
327
  * The parameter names a route captures with its own pattern.
@@ -636,7 +644,8 @@ function hasErrorMiddleware(router) {
636
644
  }
637
645
 
638
646
  /**
639
- *
647
+ * @param {unknown[]} handlers what a registration was given, flattened
648
+ * @param {string} [emptyMessage]
640
649
  */
641
650
  function checkHandlers(handlers, emptyMessage = "argument handler is required") {
642
651
  if (handlers.length === 0) {
package/src/router.js CHANGED
@@ -169,6 +169,7 @@ module.exports = class Router extends EventEmitter {
169
169
 
170
170
  this._paramCallbacks = new Map();
171
171
  this._mountpathCache = new Map();
172
+ /** @type {RouteEntry[]} */
172
173
  this._routes = [];
173
174
  // websocket routes, kept apart from the HTTP ones: µWS serves them itself and listen()
174
175
  // hands them over whole, mount paths and all
@@ -908,6 +909,7 @@ module.exports = class Router extends EventEmitter {
908
909
  */
909
910
  _handleError(err, handler, request, response) {
910
911
  if (handler) {
912
+ /** @param {unknown} [pass] */
911
913
  const next = (pass) => {
912
914
  delete request._error;
913
915
  delete request._errorKey;
@@ -1154,15 +1156,20 @@ module.exports = class Router extends EventEmitter {
1154
1156
  return new Promise((resolve) => {
1155
1157
  let index = 0;
1156
1158
  let name = "";
1159
+ /** @type {unknown} */
1157
1160
  let value;
1158
1161
  let entry;
1162
+ /** @type {Function[]} */
1159
1163
  let fns = [];
1160
1164
  let fnIndex = 0;
1161
1165
 
1162
1166
  // one parameter after the other, err being what the last one's callbacks ended with
1167
+ /** @param {unknown} [err] */
1163
1168
  const nextParam = (err) => {
1164
1169
  if (err) {
1165
- if (err !== "route") {
1170
+ // an error already in flight stays the one in flight: express reaches a mount's
1171
+ // param callbacks with it pending and goes on with next(layerError || err)
1172
+ if (err !== "route" && !req._error) {
1166
1173
  req._error = err;
1167
1174
  req._errorKey = route.routeKey;
1168
1175
  req._errorGroup = route.group;
@@ -1188,6 +1195,7 @@ module.exports = class Router extends EventEmitter {
1188
1195
  };
1189
1196
 
1190
1197
  // and one callback of the current parameter after the other
1198
+ /** @param {unknown} [err] */
1191
1199
  const nextCallback = (err) => {
1192
1200
  const fn = fns[fnIndex++];
1193
1201
  // read before the callback runs and again after it: one that rewrites
@@ -1248,6 +1256,13 @@ module.exports = class Router extends EventEmitter {
1248
1256
 
1249
1257
  /**
1250
1258
  * Resolves with the route that answered, or false when nothing matched.
1259
+ *
1260
+ * @param {Request} req
1261
+ * @param {Response} res
1262
+ * @param {number} [startIndex]
1263
+ * @param {RouteEntry[]} [routes]
1264
+ * @param {boolean} [skipCheck] take the route at the index without matching it, see Walk
1265
+ * @param {RouteEntry} [skipUntil] route to resume after when this chain runs out, see Walk
1251
1266
  * @returns {Promise<RouteEntry|false>}
1252
1267
  */
1253
1268
  _routeRequest(req, res, startIndex = 0, routes = this._routes, skipCheck = false, skipUntil) {
@@ -1366,11 +1381,16 @@ module.exports = class Router extends EventEmitter {
1366
1381
  // one map for the whole chain, because express builds one Route for it: a request answered
1367
1382
  // by the get() of an app.route() reads post() in its req.route.methods too
1368
1383
  const groupMethods = new NullObject();
1384
+ /** @type {(Omit<Layer, "route"> & {method: string|undefined})[]} express's Route#stack, see createRoute */
1369
1385
  const groupStack = [];
1370
1386
  // express hands back a Route, which carries these three beside the verb methods
1371
1387
  fns.path = path;
1372
1388
  fns.methods = groupMethods;
1373
1389
  fns.stack = groupStack;
1390
+ /**
1391
+ * @param {string} method
1392
+ * @param {unknown[]} callbacks
1393
+ */
1374
1394
  const inGroup = (method, callbacks) => {
1375
1395
  this._pendingGroup = group;
1376
1396
  this._pendingGroupMethods = groupMethods;
@@ -1384,9 +1404,9 @@ module.exports = class Router extends EventEmitter {
1384
1404
  }
1385
1405
  };
1386
1406
  for (const method of methods) {
1387
- fns[method] = (...callbacks) => inGroup(method, callbacks);
1407
+ fns[method] = (/** @type {unknown[]} */ ...callbacks) => inGroup(method, callbacks);
1388
1408
  }
1389
- fns.get = (...callbacks) => inGroup("GET", callbacks);
1409
+ fns.get = (/** @type {unknown[]} */ ...callbacks) => inGroup("GET", callbacks);
1390
1410
  return fns;
1391
1411
  }
1392
1412
 
@@ -1482,7 +1502,10 @@ module.exports = class Router extends EventEmitter {
1482
1502
  useRouterClass(module.exports);
1483
1503
 
1484
1504
  for (const method of methods) {
1485
- module.exports.prototype[method] = function (path, ...callbacks) {
1505
+ module.exports.prototype[method] = function (
1506
+ /** @type {string|RegExp|(string|RegExp)[]} */ path,
1507
+ /** @type {unknown[]} */ ...callbacks
1508
+ ) {
1486
1509
  return this.createRoute(method, path, this, ...callbacks);
1487
1510
  };
1488
1511
  }
package/src/testing.js CHANGED
@@ -134,7 +134,7 @@ function select(app, patterns, caller) {
134
134
  throw new TypeError(`${caller} needs a path, or a list of them, to check`);
135
135
  }
136
136
  const report = routeReport(app);
137
- const selected = [];
137
+ const selected = /** @type {ReturnType<typeof routeReport>} */ ([]);
138
138
  for (const pattern of wanted) {
139
139
  const matched = report.filter((entry) => names(entry, pattern));
140
140
  if (matched.length === 0) {
package/src/utils.js CHANGED
@@ -208,6 +208,9 @@ function getPatternMeta(pattern) {
208
208
  * A bare `*`, an unnamed parameter, an inline regex like :id(\\d+) and the `+`, `?`, `()` operators
209
209
  * throw: a route that quietly stops matching is worse than one that fails at startup. The names it
210
210
  * captures go in a WeakMap beside the regex, see PatternMeta.
211
+ *
212
+ * @param {string|RegExp} pattern
213
+ * @returns {RegExp}
211
214
  */
212
215
  function patternToRegex(pattern, isPrefix = false, caseSensitive = true, strict = false) {
213
216
  if (pattern instanceof RegExp) {
@@ -222,11 +225,12 @@ function patternToRegex(pattern, isPrefix = false, caseSensitive = true, strict
222
225
  let regexPattern = "";
223
226
  let i = 0;
224
227
  const len = pattern.length;
225
- const wildcardNames = [];
228
+ const wildcardNames = /** @type {string[]} */ ([]);
226
229
  // express takes /:a/:a, and two capture groups cannot share a name, so a repeat is compiled
227
230
  // under a spelling of its own and mapped back when the parameters are read out. Reading them
228
231
  // in order then leaves the last occurrence in place, which is the value express reports
229
- const groupOutputName = new Map();
232
+ const groupOutputName = /** @type {Map<string, string>} */ (new Map());
233
+ /** @param {string} name */
230
234
  const uniqueGroupName = (name) => {
231
235
  if (!groupOutputName.has(name)) {
232
236
  groupOutputName.set(name, name);
@@ -255,7 +259,11 @@ function patternToRegex(pattern, isPrefix = false, caseSensitive = true, strict
255
259
  let lastCaptureWasWildcard = false;
256
260
  let wildcardInSegment = false;
257
261
  let paramInSegment = false;
258
- /** Records literal text as it is emitted, which is what the rules above are written against. */
262
+ /**
263
+ * Records literal text as it is emitted, which is what the rules above are written against.
264
+ *
265
+ * @param {string} text
266
+ */
259
267
  const literal = (text) => {
260
268
  backtrack += text;
261
269
  if (lastCaptureWasWildcard) {
@@ -750,7 +758,8 @@ function acceptParams(str) {
750
758
  const length = str.length;
751
759
  const colonIndex = str.indexOf(";");
752
760
  let index = colonIndex === -1 ? length : colonIndex;
753
- const ret = { value: str.slice(0, index).trim(), quality: 1, params: {} };
761
+ const params = /** @type {Record<string, string>} */ ({});
762
+ const ret = { value: str.slice(0, index).trim(), quality: 1, params };
754
763
 
755
764
  while (index < length) {
756
765
  const splitIndex = str.indexOf("=", index);
@@ -1098,13 +1107,20 @@ function durationSetting(value, name) {
1098
1107
  return parsed;
1099
1108
  }
1100
1109
 
1110
+ /**
1111
+ * The predicate "trust proxy" compiles to: whether the address at hop i is trusted. The address is
1112
+ * undefined over a unix socket, see Request#parsedIp.
1113
+ *
1114
+ * @typedef {(addr: string|undefined, i: number) => boolean} TrustFn
1115
+ */
1116
+
1101
1117
  /**
1102
1118
  * Turns whatever "trust proxy" was set to into the function proxy-addr wants: a predicate saying
1103
1119
  * whether the address at hop i is trusted. true trusts everything, a number trusts that many hops,
1104
1120
  * and a string or a list is read as addresses and subnet names.
1105
1121
  *
1106
- * @param {boolean|number|string|string[]|Function} val
1107
- * @returns {Function}
1122
+ * @param {boolean|number|string|string[]|TrustFn} val
1123
+ * @returns {TrustFn}
1108
1124
  */
1109
1125
  function compileTrust(val) {
1110
1126
  if (typeof val === "function") return val;
@@ -1118,8 +1134,9 @@ function compileTrust(val) {
1118
1134
 
1119
1135
  if (typeof val === "number") {
1120
1136
  // Support trusting hop count
1121
- return function (a, i) {
1122
- return i < val;
1137
+ const hops = val;
1138
+ return function (/** @type {string|undefined} */ a, /** @type {number} */ i) {
1139
+ return i < hops;
1123
1140
  };
1124
1141
  }
1125
1142
 
@@ -1130,7 +1147,9 @@ function compileTrust(val) {
1130
1147
  });
1131
1148
  }
1132
1149
 
1133
- return proxyaddr.compile(val || []);
1150
+ // proxy-addr answers false to an address it cannot parse, undefined included, though its
1151
+ // typing does not admit one
1152
+ return /** @type {TrustFn} */ (proxyaddr.compile(val || []));
1134
1153
  }
1135
1154
 
1136
1155
  const shownWarnings = new Set();
package/src/walk.js CHANGED
@@ -79,6 +79,7 @@ class Walk {
79
79
  //
80
80
  // Null here and bound on the first route with more than one callback, the only shape that
81
81
  // reads it: a request that never meets one paid a bind for nothing
82
+ /** @type {((err?: unknown) => void)|null} */
82
83
  this.leaveRoute = null;
83
84
  }
84
85
 
@@ -103,6 +104,7 @@ class Walk {
103
104
  * a chain of N middlewares costs one promise instead of N nested ones.
104
105
  *
105
106
  * @param {number} startIndex where to resume the scan
107
+ * @returns {void}
106
108
  */
107
109
  dispatch(startIndex) {
108
110
  const req = this.req;
@@ -226,7 +228,7 @@ class Walk {
226
228
  .then((resumed) => this.runRoute(resumed))
227
229
  // wrapped so the native pair keeps the walk as receiver; a promise's reject
228
230
  // would not have cared
229
- .catch((err) => this.reject(err));
231
+ .catch((/** @type {unknown} */ err) => this.reject(err));
230
232
  return;
231
233
  }
232
234
  return this.runRoute(continueRoute);
@@ -291,6 +293,7 @@ class Walk {
291
293
  * way in, and then the route's callbacks run one after another through next().
292
294
  *
293
295
  * @param {true|"route"} continueRoute what _preprocessRequest decided: true to run, "route" to skip
296
+ * @returns {void}
294
297
  */
295
298
  runRoute(continueRoute) {
296
299
  const req = this.req;
@@ -347,6 +350,7 @@ class Walk {
347
350
  *
348
351
  * @param {number} kind what the callback is, one of the CALLBACK_ constants
349
352
  * @param {Function} callback
353
+ * @returns {void}
350
354
  */
351
355
  errorHop(kind, callback) {
352
356
  const req = this.req;
@@ -441,6 +445,7 @@ class Walk {
441
445
  * leave the route; with anything else, remember it as the error and carry on.
442
446
  *
443
447
  * @param {unknown} thingamabob what next() was called with: nothing, "route", or an error
448
+ * @returns {void}
444
449
  */
445
450
  step(thingamabob) {
446
451
  const req = this.req;
@@ -492,7 +497,7 @@ class Walk {
492
497
  }
493
498
  callback
494
499
  ._routeRequest(req, res, 0)
495
- .then((routed) => {
500
+ .then((/** @type {RouteEntry|false} */ routed) => {
496
501
  // the child's params are scoped to it, and must not leak into the routes after
497
502
  if (pushedParams) {
498
503
  req._paramStack.pop();
@@ -531,7 +536,7 @@ class Walk {
531
536
  // a rejection out of the nested walk, or a throw above, must reject this one
532
537
  // instead of dying as an unhandled rejection; wrapped for the native pair's
533
538
  // receiver
534
- .catch((err) => this.reject(err));
539
+ .catch((/** @type {unknown} */ err) => this.reject(err));
535
540
  } else {
536
541
  // errors and error handlers live out of line: this is the cold path, and its size
537
542
  // was pushing step past the inlining threshold
package/src/websocket.js CHANGED
@@ -208,7 +208,7 @@ function makeUpgradeHandler(app, path, behavior) {
208
208
  * @param {Application} app
209
209
  */
210
210
  function registerWebSocketRoutes(app) {
211
- const routes = [];
211
+ const routes = /** @type {WsRoute[]} */ ([]);
212
212
  collectRoutes(app, "", routes, new Set());
213
213
  for (const route of routes) {
214
214
  const uwsBehavior = { ...route.behavior };