fulmine.js 5.19.3 → 5.19.4

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.
@@ -30,6 +30,48 @@ const { METHODS } = require("http");
30
30
  * would be a second copy of createRoute that nothing keeps in step.
31
31
  * @typedef {any} RouteEntry
32
32
  */
33
+ /**
34
+ * What nativePreset builds for a literal registration: the constants every request to that route
35
+ * shares, read off the preset instead of the URL.
36
+ * @typedef {object} NativePreset
37
+ * @property {string} path
38
+ * @property {string} method
39
+ * @property {boolean} endsWithSlash
40
+ * @property {string} opPath
41
+ * @property {boolean} isOptions
42
+ * @property {boolean} isHead
43
+ * @property {boolean} skipHeaders written at registration, and taken back by a middleware added
44
+ * after listen, see _skipPresets
45
+ * @property {boolean} skipQuery the same, for the query
46
+ */
47
+ /**
48
+ * Where a granted header skip lives: the preset itself for a literal registration, a holder of its
49
+ * own for a parameterised one, see makeHandler in optimizer.js.
50
+ * @typedef {object} SkipHolder
51
+ * @property {boolean} skipHeaders
52
+ * @property {boolean} skipQuery
53
+ * @property {string|null} [method]
54
+ * @property {boolean} [isOptions]
55
+ * @property {boolean} [isHead]
56
+ */
57
+ /**
58
+ * An earlier registration a mount is guarded by, see guardsInside.
59
+ * @typedef {{path: string, use: boolean, method: string, all: boolean}} MountGuard
60
+ */
61
+ /**
62
+ * A layer as express shapes it, which is what app.stack and router.stack hand out.
63
+ * @typedef {object} Layer
64
+ * @property {Function} handle
65
+ * @property {string} name
66
+ * @property {undefined} params
67
+ * @property {undefined} path
68
+ * @property {never[]} keys
69
+ * @property {RouteEntry|undefined} route
70
+ */
71
+ /**
72
+ * A websocket registration, kept until listen() hands it to µWS.
73
+ * @typedef {{path: string, behavior: Record<string, unknown>, owner: Router}} WsRoute
74
+ */
33
75
 
34
76
  // whether a registered path could be asked for in another case, which is what decides whether the
35
77
  // native router can be trusted to prefer it, see _optimizeRoute
@@ -100,8 +142,9 @@ for (const method of resDecMethods) {
100
142
  * them off the handle.
101
143
  *
102
144
  * @param {RouteEntry} route
103
- * @param {any} callback a handler, or a mounted router, which is callable and carries _routes
104
- * @returns {any} the layer object, which is express's shape and not one of ours
145
+ * @param {Function & {_routes?: RouteEntry[], _isApplication?: boolean}} callback a handler, or a
146
+ * mounted router, which is callable and carries _routes
147
+ * @returns {Layer} the layer object, which is express's shape and not one of ours
105
148
  */
106
149
  function layerFor(route, callback) {
107
150
  const layer = {
@@ -129,7 +172,7 @@ function layerFor(route, callback) {
129
172
  * that name.
130
173
  *
131
174
  * @param {RouteEntry} route
132
- * @returns {any} the layer object, which is express's shape and not one of ours
175
+ * @returns {Layer} the layer object, which is express's shape and not one of ours
133
176
  */
134
177
  function routeLayer(route) {
135
178
  const handle = function handle(req, res, next) {
@@ -172,7 +215,9 @@ function nativeDone(matched) {
172
215
  if (!matched) {
173
216
  queueMicrotask(() => {
174
217
  const response = this.res;
175
- if (response.headersSent || response.aborted) {
218
+ // a 404 after the head is left as it is, as express's final handler leaves it; an error
219
+ // after it goes on to _handleError, which closes the connection as that handler does
220
+ if (response.aborted || (response.headersSent && !this.req._error)) {
176
221
  return;
177
222
  }
178
223
  try {
@@ -326,7 +371,7 @@ const EMPTY_INDICES = /** @type {number[]} */ ([]);
326
371
  * patterns are pure literals, everything else, "/*" included, stays in alwaysVisit and is still
327
372
  * matched per request by _pathMatches.
328
373
  *
329
- * @param {any[]} routes the router's own table
374
+ * @param {RouteEntry[]} routes the router's own table
330
375
  * @param {boolean} caseFlag the frozen case-sensitivity flag
331
376
  * @returns {{map: Map<string, number[]>, alwaysVisit: number[]}}
332
377
  */
@@ -513,7 +558,7 @@ function logError(router, err) {
513
558
  /**
514
559
  * The uWS onAborted handler, bound to the response: a closure here captured two locals and cost
515
560
  * a context plus a function per request, for a path that only ever runs on a client abort.
516
- * @this {any} the response, with the request linked as this.req
561
+ * @this {Response} the response, with the request linked as this.req
517
562
  */
518
563
  function onNativeAborted() {
519
564
  const response = this;
@@ -620,7 +665,7 @@ const CALLBACK_ROUTER = 2;
620
665
  *
621
666
  * @param {Request} req
622
667
  * @param {RouteEntry} route
623
- * @param {any} err whatever decoding threw
668
+ * @param {unknown} err whatever decoding threw
624
669
  */
625
670
  function raiseDecodeFailure(req, route, err) {
626
671
  if (req._error) {
@@ -686,7 +731,7 @@ function stepsOver(route, req) {
686
731
  // it would answer a GET differently from express. See the same seeding in middlewares.js
687
732
  if (!("body" in req)) {
688
733
  // cast because `body` is deliberately not a field of Request, see the comment there
689
- /** @type {any} */ (req).body = undefined;
734
+ /** @type {{body?: unknown}} */ (req).body = undefined;
690
735
  }
691
736
  return true;
692
737
  }
@@ -740,10 +785,10 @@ function shadowsLeaf(guard, leafPath, leaf) {
740
785
  * @param {Router} router the router the mount belongs to
741
786
  * @param {RouteEntry} mount
742
787
  * @param {string} pathPrefix what the mounts above this one consumed
743
- * @param {any[]} chain the layers that always run before the mount, which need no guard
744
- * @param {any[]} inherited the guards from further out, since a mount two levels down is under
788
+ * @param {RouteEntry[]} chain the layers that always run before the mount, which need no guard
789
+ * @param {MountGuard[]} inherited the guards from further out, since a mount two levels down is under
745
790
  * everything written before either of them
746
- * @returns {any[]|null} null when a path cannot be read segment by segment, which leaves the mount
791
+ * @returns {MountGuard[]|null} null when a path cannot be read segment by segment, which leaves the mount
747
792
  * to ordinary dispatch rather than guessing about it
748
793
  */
749
794
  function guardsInside(router, mount, pathPrefix, chain, inherited) {
@@ -809,10 +854,13 @@ function restoreApp(route, req) {
809
854
  /**
810
855
  * useApp
811
856
  * @param {Request} req
812
- * @param {any} app the application taking the request over
857
+ * @param {Router & {request?: object, response?: object}} app the application taking the request
858
+ * over. Typed as a router because the callers hold one, and only an application carries the two
859
+ * prototype layers read below
813
860
  */
814
861
  function useApp(req, app) {
815
- req.app = app;
862
+ // only an application takes a request over, see rememberApp
863
+ req.app = /** @type {import("./application.js").Application} */ (app);
816
864
  if (req.res) {
817
865
  req.res.app = app;
818
866
  }
@@ -864,7 +912,7 @@ function callablePrototypeFor(classPrototype) {
864
912
  prototype = Object.create(classPrototype);
865
913
  for (const name of ["apply", "call", "toString"]) {
866
914
  Object.defineProperty(prototype, name, {
867
- value: /** @type {any} */ (Function.prototype)[name],
915
+ value: Function.prototype[name],
868
916
  writable: true,
869
917
  configurable: true,
870
918
  enumerable: false
package/src/router.js CHANGED
@@ -26,6 +26,7 @@ const {
26
26
  pathsCanOverlap,
27
27
  regexpGroupKeys,
28
28
  NullObject,
29
+ headersSentError,
29
30
  EMPTY_REGEX,
30
31
  settingsEpoch
31
32
  } = require("./utils.js");
@@ -67,6 +68,12 @@ const {
67
68
  } = require("./router-utils.js");
68
69
 
69
70
  /** @typedef {import("./router-utils.js").RouteEntry} RouteEntry */
71
+ /** @typedef {import("./router-utils.js").SkipHolder} SkipHolder */
72
+ /** @typedef {import("./router-utils.js").NativePreset} NativePreset */
73
+ /** @typedef {import("./router-utils.js").Layer} Layer */
74
+ /** @typedef {import("./router-utils.js").WsRoute} WsRoute */
75
+ /** @typedef {import("uWebSockets.js").HttpRequest} UwsRequest */
76
+ /** @typedef {import("uWebSockets.js").HttpResponse} UwsResponse */
70
77
 
71
78
  // hands out one number per app.route(), so the routes it creates know they belong together
72
79
  let routeGroups = 0;
@@ -76,7 +83,7 @@ let routeKey = 0;
76
83
  module.exports = class Router extends EventEmitter {
77
84
  /**
78
85
  * The router or application this one is mounted on, undefined until it is.
79
- * @type {any}
86
+ * @type {Router|undefined}
80
87
  */
81
88
  parent;
82
89
 
@@ -165,11 +172,11 @@ module.exports = class Router extends EventEmitter {
165
172
  this._routes = [];
166
173
  // websocket routes, kept apart from the HTTP ones: µWS serves them itself and listen()
167
174
  // hands them over whole, mount paths and all
168
- /** @type {any[]|null} */
175
+ /** @type {WsRoute[]|null} */
169
176
  this._wsRoutes = null;
170
177
  // the native presets allowed to skip the header copy, so a late middleware or an etag
171
178
  // arriving after listen can take the permission back; null until one is granted
172
- /** @type {Set<any>|null} */
179
+ /** @type {Set<SkipHolder>|null} */
173
180
  this._skipPresets = null;
174
181
  /** @type {boolean|undefined} */
175
182
  this._hasErrMwCache = undefined;
@@ -226,13 +233,13 @@ module.exports = class Router extends EventEmitter {
226
233
  *
227
234
  * @param {any} req a Request, or the plain object express's own router tests drive it with
228
235
  * @param {any} res a Response, or whatever the caller is serving with
229
- * @param {(err?: any) => void} [next]
236
+ * @param {(err?: unknown) => void} [next]
230
237
  * @returns {Promise<void>}
231
238
  */
232
239
  async handle(req, res, next) {
233
240
  // a request from node's own server, which is what http.createServer(app) delivers
234
241
  if (isNodeRequest(req)) {
235
- return serveNodeRequest(this, req, /** @type {any} */ (res), next);
242
+ return serveNodeRequest(this, req, res, next);
236
243
  }
237
244
  // an app taking over a request becomes that request's app, as it does when mounted, so
238
245
  // req.app.get("view engine") inside a sub-app reads the sub-app's settings. A plain router
@@ -562,7 +569,7 @@ module.exports = class Router extends EventEmitter {
562
569
  * splicing one out moves nothing. The layer objects are kept, so identities compare across two
563
570
  * reads the way they do in Express.
564
571
  *
565
- * @returns {any[]}
572
+ * @returns {Layer[]}
566
573
  */
567
574
  get stack() {
568
575
  const layers = [];
@@ -585,9 +592,11 @@ module.exports = class Router extends EventEmitter {
585
592
  * anything not comparable as a string is compiled to a regular expression and marked complex.
586
593
  *
587
594
  * @param {string} method HTTP method, or USE for a mount
588
- * @param {any} path one path or several
589
- * @param {any} [parent] what to return, so chaining lands on the app rather than the router
590
- * @param {...any} callbacks
595
+ * @param {string|RegExp|(string|RegExp)[]} path one path or several
596
+ * @param {any} [parent] what to return, so chaining lands on the app rather than the router.
597
+ * Loose because it is also the builder app.route() hands back
598
+ * @param {...any} callbacks handlers, or arrays of them at any depth, flattened below. Loose
599
+ * because a parameter keeps its declared type through that reassignment
591
600
  * @returns {any} parent
592
601
  */
593
602
  createRoute(method, path, parent = this, ...callbacks) {
@@ -706,7 +715,7 @@ module.exports = class Router extends EventEmitter {
706
715
  stack,
707
716
  // the route as a request sees it, which is the route itself unless the path was
708
717
  // normalised. Written into the literal so every route keeps one shape
709
- exposed: /** @type {any} */ (undefined),
718
+ exposed: /** @type {RouteEntry|undefined} */ (undefined),
710
719
  routeKey: routeKey++,
711
720
  // which app.route() this came from, when it came from one, so the routes it built
712
721
  // count as one route where an error is concerned. undefined for every other route
@@ -768,8 +777,8 @@ module.exports = class Router extends EventEmitter {
768
777
  * as a method because a mounted router is asked for its own through it.
769
778
  *
770
779
  * @param {RouteEntry} route
771
- * @param {any[]} routes every route of this router, in registration order
772
- * @returns {any[]|false} the chain, ending in the route itself
780
+ * @param {RouteEntry[]} routes every route of this router, in registration order
781
+ * @returns {RouteEntry[]|false} the chain, ending in the route itself
773
782
  */
774
783
  _optimizeRoute(route, routes) {
775
784
  return optimizeRoute(this, route, routes);
@@ -788,12 +797,12 @@ module.exports = class Router extends EventEmitter {
788
797
  * request does whichever path serves it. The response rides back as request.res: returning
789
798
  * a `{ request, response }` pair was one throwaway object per request.
790
799
  *
791
- * @param {any} res uWS response, which the shipped typings do not describe
792
- * @param {any} req uWS request, readable only during this call
793
- * @param {any} [preset] a literal registration's constants, see nativePreset
794
- * @param {any} [skipHolder] the object a granted header skip lives on: the preset itself
800
+ * @param {UwsResponse} res uWS response
801
+ * @param {UwsRequest} req uWS request, readable only during this call
802
+ * @param {NativePreset} [preset] a literal registration's constants, see nativePreset
803
+ * @param {SkipHolder} [skipHolder] the object a granted header skip lives on: the preset itself
795
804
  * for a literal registration, a holder of its own for a parameterised one
796
- * @returns {any} the request, with the response reachable as request.res
805
+ * @returns {Request} the request, with the response reachable as request.res
797
806
  */
798
807
  handleRequest(res, req, preset, skipHolder) {
799
808
  const request = new this._request(req, res, this, preset, skipHolder);
@@ -830,7 +839,7 @@ module.exports = class Router extends EventEmitter {
830
839
  * for a response that outlives its handler callback: the native handler arms it in its
831
840
  * finally when the answer is still pending, which on a synchronous route it never is.
832
841
  *
833
- * @param {any} res uWS response, which the shipped typings do not describe
842
+ * @param {UwsResponse} res uWS response
834
843
  * @param {Response} response
835
844
  */
836
845
  _armAbort(res, response) {
@@ -846,7 +855,7 @@ module.exports = class Router extends EventEmitter {
846
855
  * are compared segment by segment.
847
856
  *
848
857
  * @param {RouteEntry} route
849
- * @param {any[]} routes every route of the router this one belongs to
858
+ * @param {RouteEntry[]} routes every route of the router this one belongs to
850
859
  * @returns {boolean}
851
860
  */
852
861
  _isFollowedByAnOverlap(route, routes) {
@@ -881,7 +890,7 @@ module.exports = class Router extends EventEmitter {
881
890
  * the optimizer tests replace it to see which routes went native.
882
891
  *
883
892
  * @param {RouteEntry} route
884
- * @param {any[]} optimizedPath the chain the route was optimized with
893
+ * @param {RouteEntry[]} optimizedPath the chain the route was optimized with
885
894
  */
886
895
  _registerUwsRoute(route, optimizedPath) {
887
896
  registerUwsRoute(this, route, optimizedPath);
@@ -913,6 +922,14 @@ module.exports = class Router extends EventEmitter {
913
922
  }
914
923
  }
915
924
  logError(this, err);
925
+ // no error page can follow a head that is out, so express's final handler closes the
926
+ // connection instead, and leaves a response that was already ended as it is
927
+ if (response.headersSent) {
928
+ if (!response.finished) {
929
+ response.destroy();
930
+ }
931
+ return;
932
+ }
916
933
  if (response.statusCode === 200) {
917
934
  // the status the error carries, as express's own final handler reads it: a body that
918
935
  // was too large or a request cut short is the client's 4xx, not a 500 from here
@@ -926,7 +943,7 @@ module.exports = class Router extends EventEmitter {
926
943
  * The HTML for an error, which in production says only what the status means rather than what
927
944
  * went wrong, so a stack trace does not reach the client.
928
945
  *
929
- * @param {any} err whatever was thrown, which need not be an Error
946
+ * @param {unknown} err whatever was thrown, which need not be an Error
930
947
  * @param {number} statusCode
931
948
  * @param {boolean} [checkEnv] whether production should redact it
932
949
  * @returns {string}
@@ -1223,7 +1240,7 @@ module.exports = class Router extends EventEmitter {
1223
1240
 
1224
1241
  /**
1225
1242
  * Resolves with the route that answered, or false when nothing matched.
1226
- * @returns {Promise<any>}
1243
+ * @returns {Promise<RouteEntry|false>}
1227
1244
  */
1228
1245
  _routeRequest(req, res, startIndex = 0, routes = this._routes, skipCheck = false, skipUntil) {
1229
1246
  return new Promise((resolve, reject) => {
@@ -1289,7 +1306,8 @@ module.exports = class Router extends EventEmitter {
1289
1306
  settingsEpoch.n++;
1290
1307
  }
1291
1308
  }
1292
- this.createRoute("USE", path, this, ...callbacks);
1309
+ // a handler in the path position was moved to the callbacks above
1310
+ this.createRoute("USE", /** @type {string|RegExp|(string|RegExp)[]} */ (path), this, ...callbacks);
1293
1311
  return this;
1294
1312
  }
1295
1313
 
@@ -1314,7 +1332,7 @@ module.exports = class Router extends EventEmitter {
1314
1332
  * });
1315
1333
  *
1316
1334
  * @param {string} path a literal path, or one whose parameters are whole segments
1317
- * @param {object} behavior uWS's WebSocketBehavior, plus the optional `upgrade` above
1335
+ * @param {Record<string, unknown>} behavior uWS's WebSocketBehavior, plus the optional `upgrade` above
1318
1336
  * @returns {this}
1319
1337
  */
1320
1338
  ws(path, behavior) {
@@ -1350,7 +1368,7 @@ module.exports = class Router extends EventEmitter {
1350
1368
  this._pendingGroupMethods = groupMethods;
1351
1369
  this._pendingGroupStack = groupStack;
1352
1370
  try {
1353
- return this.createRoute(method, path, /** @type {any} */ (fns), ...callbacks);
1371
+ return this.createRoute(method, path, fns, ...callbacks);
1354
1372
  } finally {
1355
1373
  this._pendingGroup = undefined;
1356
1374
  this._pendingGroupMethods = undefined;
@@ -1370,7 +1388,7 @@ module.exports = class Router extends EventEmitter {
1370
1388
  *
1371
1389
  * @param {Request} request
1372
1390
  * @param {Response} response
1373
- * @param {any} err whatever was thrown, which need not be an Error
1391
+ * @param {unknown} err whatever was thrown, which need not be an Error
1374
1392
  * @param {boolean} [checkEnv] whether production should redact it
1375
1393
  */
1376
1394
  _sendErrorPage(request, response, err, checkEnv = false) {
@@ -1395,8 +1413,8 @@ module.exports = class Router extends EventEmitter {
1395
1413
  * @param {Set<string>} methods the verbs the answering router knows, which are its own
1396
1414
  */
1397
1415
  _sendOptionsReply(request, response, methods) {
1398
- if (response._headWritten) {
1399
- throw new Error("Cannot set headers after they are sent to the client");
1416
+ if (response.headersSent) {
1417
+ throw headersSentError("set");
1400
1418
  }
1401
1419
  // Express 5 sorts the methods and joins them with ", ", so the header reads the same
1402
1420
  // regardless of the order the routes happened to be registered in
@@ -34,6 +34,8 @@ limitations under the License.
34
34
  const http = require("http");
35
35
  const net = require("net");
36
36
 
37
+ /** @typedef {import("./application.js").Application} Application */
38
+
37
39
  // what marks an application, read by the instanceof hook below. A symbol, so no plain field name
38
40
  // can be mistaken for it
39
41
  const kIsApplication = Symbol.for("fulmine.application");
@@ -45,20 +47,20 @@ const kIsApplication = Symbol.for("fulmine.application");
45
47
  * @param {Function} klass http.Server or net.Server
46
48
  */
47
49
  function acceptApplications(klass) {
48
- const previous = /** @type {any} */ (klass)[Symbol.hasInstance];
50
+ const previous = klass[Symbol.hasInstance];
49
51
  // already taught, which happens when two copies of this package share one process
50
- if (/** @type {any} */ (klass)[kIsApplication] === true) {
52
+ if (klass[kIsApplication] === true) {
51
53
  return;
52
54
  }
53
55
  Object.defineProperty(klass, Symbol.hasInstance, {
54
- /** @param {any} value @returns {boolean} */
56
+ /** @param {unknown} value @returns {boolean} */
55
57
  value: function (value) {
56
58
  if (previous.call(this, value)) {
57
59
  return true;
58
60
  }
59
61
  // an application is a function and a property read works on one. The guard is for the
60
62
  // primitives and nulls that reach any instanceof
61
- return value != null && /** @type {any} */ (value)[kIsApplication] === true;
63
+ return value != null && value[kIsApplication] === true;
62
64
  },
63
65
  configurable: true,
64
66
  writable: true
@@ -73,7 +75,8 @@ acceptApplications(net.Server);
73
75
  * The net.Server members Express's API does not give, on the application prototype. Each one
74
76
  * answers for uWS, not for a node socket.
75
77
  *
76
- * @param {any} prototype Application.prototype
78
+ * @param {any} prototype Application.prototype, loose because the members are written here and not
79
+ * declared on the class
77
80
  */
78
81
  function addServerMembers(prototype) {
79
82
  Object.defineProperty(prototype, kIsApplication, { value: true, configurable: true });
@@ -97,13 +100,14 @@ function addServerMembers(prototype) {
97
100
  * A handle this does not own: uWS's loop keeps the process alive and a caller cannot unref it.
98
101
  * Both are no-ops returning the server, so a chain written against node's API keeps working.
99
102
  *
100
- * @returns {any}
103
+ * @this {Application}
104
+ * @returns {Application}
101
105
  */
102
106
  prototype.ref = function ref() {
103
107
  return this;
104
108
  };
105
109
 
106
- /** @returns {any} */
110
+ /** @this {Application} @returns {Application} */
107
111
  prototype.unref = function unref() {
108
112
  return this;
109
113
  };
@@ -112,10 +116,10 @@ function addServerMembers(prototype) {
112
116
  * Registers the callback like node's does and remembers the value, which is all a caller can
113
117
  * observe. The timeout belongs to uWS and is set through uwsOptions.idleTimeout.
114
118
  *
115
- * @this {any}
119
+ * @this {Application & {timeout?: number}}
116
120
  * @param {number} [msecs]
117
121
  * @param {() => void} [callback]
118
- * @returns {any}
122
+ * @returns {Application}
119
123
  */
120
124
  prototype.setTimeout = function setTimeout(msecs, callback) {
121
125
  this.timeout = msecs;
@@ -127,7 +131,7 @@ function addServerMembers(prototype) {
127
131
 
128
132
  // The numbers node's http.Server carries. Inert here, but declared rather than left undefined:
129
133
  // a library reads `server.keepAliveTimeout` to work out what it is talking to.
130
- for (const [name, value] of /** @type {[string, any][]} */ ([
134
+ for (const [name, value] of /** @type {[string, number|null][]} */ ([
131
135
  ["timeout", 0],
132
136
  ["keepAliveTimeout", 5000],
133
137
  ["headersTimeout", 60000],
@@ -34,6 +34,9 @@ limitations under the License.
34
34
  "use strict";
35
35
 
36
36
  const { work, names } = require("./work.js");
37
+ const { applyWriteHead } = require("./utils.js");
38
+
39
+ /** @typedef {import("./response.js")} Response */
37
40
 
38
41
  /**
39
42
  * A duration in milliseconds, as Server-Timing writes them: two decimals.
@@ -63,7 +66,8 @@ function describe(text) {
63
66
  * true. Nothing is written for a request that built none of it, which is the usual one.
64
67
  * @param {boolean} [options.total] whether to report the time up to the head. Default true.
65
68
  * @param {string} [options.name] what the total is called. Default "total".
66
- * @returns {(req: any, res: any, next: (err?: any) => void) => void}
69
+ * @returns {(req: any, res: any, next: (err?: unknown) => void) => void} the middleware. The pair is
70
+ * loose because the two methods below are added to the response here
67
71
  */
68
72
  function serverTiming(options) {
69
73
  const opts = options || {};
@@ -84,7 +88,7 @@ function serverTiming(options) {
84
88
  * @param {string} name a token: letters, digits, dash and underscore
85
89
  * @param {number} [duration] milliseconds
86
90
  * @param {string} [description]
87
- * @returns {any} the response, so calls chain
91
+ * @returns {Response} the response, so calls chain
88
92
  */
89
93
  res.timing = function timing(name, duration, description) {
90
94
  let mark = String(name).replace(/[^\w-]/g, "");
@@ -118,11 +122,11 @@ function serverTiming(options) {
118
122
  }
119
123
  if (value && typeof value.then === "function") {
120
124
  return value.then(
121
- /** @param {any} resolved */ (resolved) => {
125
+ /** @param {unknown} resolved */ (resolved) => {
122
126
  done();
123
127
  return resolved;
124
128
  },
125
- /** @param {any} err */ (err) => {
129
+ /** @param {unknown} err */ (err) => {
126
130
  done();
127
131
  throw err;
128
132
  }
@@ -172,6 +176,14 @@ function serverTiming(options) {
172
176
  }
173
177
  };
174
178
 
179
+ const _writeHead = res.writeHead;
180
+ // writeHead settles the head, so a handler that calls it is stamped there, with the headers
181
+ // it carries applied first, as on-headers orders it
182
+ res.writeHead = function writeHead(statusCode, statusMessage, headers) {
183
+ const reason = applyWriteHead(this, statusMessage, headers);
184
+ stamp();
185
+ return _writeHead.call(this, statusCode, reason);
186
+ };
175
187
  res.write = function write(chunk, encoding, callback) {
176
188
  stamp();
177
189
  return _write.call(this, chunk, encoding, callback);
package/src/socket.js CHANGED
@@ -29,8 +29,8 @@ class Socket extends EventEmitter {
29
29
  * The Socket's error listener, shared across sockets: an error closes the stand-in, which is
30
30
  * the close connection trackers wait for. EventEmitter calls it with this = the emitter.
31
31
  *
32
- * @this {any}
33
- * @param {any} err whatever the response reported, which need not be an Error
32
+ * @this {Socket}
33
+ * @param {unknown} err whatever the response reported, which need not be an Error
34
34
  */
35
35
  static _onError(err) {
36
36
  this.emit("close");
@@ -96,7 +96,7 @@ class Socket extends EventEmitter {
96
96
  /**
97
97
  * Finishes the response through the socket, which is how the middleware that only knows
98
98
  * about sockets ends one.
99
- * @param {any} [body] whatever node's socket.end() would take
99
+ * @param {string|Buffer|Uint8Array} [body] whatever node's socket.end() would take
100
100
  */
101
101
  end(body) {
102
102
  this.response.end(body);
package/src/testing.js CHANGED
@@ -31,6 +31,7 @@ const { work, names: workNames } = require("./work.js");
31
31
  /** @typedef {import("./response.js")} Response */
32
32
  /** @typedef {import("./router.js")} Router */
33
33
  /** @typedef {import("./application.js").Application} Application */
34
+ /** @typedef {import("./router-utils.js").RouteEntry} RouteEntry */
34
35
 
35
36
  /**
36
37
  * Every route of an application and of the routers mounted under it, each with the path it answers
@@ -38,8 +39,8 @@ const { work, names: workNames } = require("./work.js");
38
39
  *
39
40
  * @param {Router} router
40
41
  * @param {string} prefix
41
- * @param {any[]} [into]
42
- * @returns {{route: any, full: string}[]}
42
+ * @param {{route: RouteEntry, full: string}[]} [into]
43
+ * @returns {{route: RouteEntry, full: string}[]}
43
44
  */
44
45
  function collectRoutes(router, prefix, into = []) {
45
46
  for (const route of router._routes ?? []) {
@@ -263,7 +264,7 @@ function expectLazy(req, res, options) {
263
264
  const done = work(req, res);
264
265
  const unwanted = { ...done };
265
266
  for (const field of allowed) {
266
- /** @type {any} */ (unwanted)[field] = false;
267
+ unwanted[field] = false;
267
268
  }
268
269
  const listed = workNames(unwanted);
269
270
  if (listed.length === 0) {
package/src/usage.js CHANGED
@@ -18,6 +18,8 @@ limitations under the License.
18
18
 
19
19
  const acorn = require("acorn");
20
20
 
21
+ /** @typedef {import("./router-utils.js").RouteEntry} RouteEntry */
22
+
21
23
  // Marks a middleware the analysis may trust on a GET request without reading its source: the
22
24
  // body parsers set it, whose prologue only reads body-framing headers and leaves a bodyless
23
25
  // GET alone (and a GET that declares a body falls back to the full header copy).
@@ -107,7 +109,7 @@ function analyze(fn) {
107
109
  // class methods and native functions do not parse alone, and unread code is unknown code
108
110
  return UNKNOWN;
109
111
  }
110
- let root = /** @type {any} */ (tree.body[0]);
112
+ let root = /** @type {import("acorn").AnyNode} */ (tree.body[0]);
111
113
  if (!root) {
112
114
  return UNKNOWN;
113
115
  }
@@ -122,7 +124,8 @@ function analyze(fn) {
122
124
  return UNKNOWN;
123
125
  }
124
126
 
125
- const params = /** @type {any[]} */ (root.params);
127
+ // checked by the loop below, which answers UNKNOWN for anything else
128
+ const params = /** @type {import("acorn").Identifier[]} */ (root.params);
126
129
  // rest or destructured parameters alias the objects somewhere the walk cannot follow
127
130
  for (const p of params) {
128
131
  if (p.type !== "Identifier") {
@@ -231,9 +234,11 @@ function analyze(fn) {
231
234
  * Walks every node, handing each its parent. Arrays and nested objects are entered, nothing
232
235
  * is interpreted: the judging happens in the visitor.
233
236
  *
234
- * @param {any} node an acorn AST node. acorn ships no useful node types, and every shape here is checked by hand
237
+ * @param {any} node an acorn node, or an array or a scalar under one: walked by key, so no shape
238
+ * is assumed
235
239
  * @param {any} parent its parent node, or null at the root
236
- * @param {(node: any, parent: any) => void} visit
240
+ * @param {(node: any, parent: any) => void} visit handed every node, loose because the visitor
241
+ * reads edges of its own off each
237
242
  */
238
243
  function walk(node, parent, visit) {
239
244
  if (!node || typeof node.type !== "string") {
@@ -265,7 +270,7 @@ function walk(node, parent, visit) {
265
270
  * it passes only when no later route could catch the fall-through. The framework's own 404 answers
266
271
  * from the path alone.
267
272
  *
268
- * @param {any[]} chain the routes the native handler runs, in order, this route last
273
+ * @param {RouteEntry[]} chain the routes the native handler runs, in order, this route last
269
274
  * @param {boolean} allowTerminalNext whether a fall-through past the chain lands only in the
270
275
  * framework's own final answer
271
276
  * @returns {{skipHeaders: boolean, skipQuery: boolean}}