fulmine.js 5.21.2 → 5.21.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.
package/src/optimizer.js CHANGED
@@ -20,6 +20,8 @@ limitations under the License.
20
20
  /** @typedef {import("./router.js")} Router */
21
21
  /** @typedef {import("./router-utils.js").RouteEntry} RouteEntry */
22
22
  /** @typedef {import("./application.js").Application} Application */
23
+ /** @typedef {import("uWebSockets.js").HttpRequest} UwsRequest */
24
+ /** @typedef {import("uWebSockets.js").HttpResponse} UwsResponse */
23
25
 
24
26
  const {
25
27
  patternToRegex,
@@ -44,6 +46,7 @@ const {
44
46
  hasErrorMiddleware,
45
47
  couldAnswer,
46
48
  shadowsLeaf,
49
+ headEntersGuard,
47
50
  guardsInside,
48
51
  supportedUwsMethods,
49
52
  regExParam
@@ -79,6 +82,8 @@ function optimizeRoute(router, route, routes) {
79
82
  const strictHere = (route.owner ?? router)._strictRouting();
80
83
  /** @type {string[]|null} earlier literals a case variant could smuggle a request past */
81
84
  let caseGuards = null;
85
+ // whether the HEAD twin has to stay generic, see headEnters
86
+ let headGeneric = false;
82
87
 
83
88
  for (let i = 0; i < routes.length; i++) {
84
89
  const r = routes[i];
@@ -95,6 +100,12 @@ function optimizeRoute(router, route, routes) {
95
100
  if (route.use && typeof route.path === "string" && couldAnswer(r, route.path)) {
96
101
  return false;
97
102
  }
103
+ // Express exempts HEAD from the method check: a HEAD enters a matching route of
104
+ // any verb and its param() callbacks run, as the generic walk does. The chain
105
+ // cannot say whether such a route matches, so the HEAD twin stays generic
106
+ if (route.method === "GET" && headEnters(r, route, caseSensitive, strictHere)) {
107
+ headGeneric = true;
108
+ }
98
109
  continue;
99
110
  }
100
111
  }
@@ -115,7 +126,7 @@ function optimizeRoute(router, route, routes) {
115
126
  route.path.slice(0, matched[0].length) === matched[0] &&
116
127
  (route.path.length === matched[0].length || route.path[matched[0].length] === "/");
117
128
  if (runsAlways) {
118
- if (r.callbacks.some((c) => c instanceof Router)) {
129
+ if (r.callbacks.some((/** @type {unknown} */ c) => c instanceof Router)) {
119
130
  return false;
120
131
  }
121
132
  optimizedPath.push(r);
@@ -137,7 +148,7 @@ function optimizeRoute(router, route, routes) {
137
148
  (!caseSensitive && r.pattern.toLowerCase() === routePathFolded) ||
138
149
  r.pattern === "/*"))
139
150
  ) {
140
- if (r.callbacks.some((c) => c instanceof Router)) {
151
+ if (r.callbacks.some((/** @type {unknown} */ c) => c instanceof Router)) {
141
152
  return false;
142
153
  }
143
154
  optimizedPath.push(r);
@@ -164,7 +175,7 @@ function optimizeRoute(router, route, routes) {
164
175
  }
165
176
  // the same path lands on the same µWS registration, so the earlier route runs from the chain
166
177
  if (rPathFolded === routePathFolded) {
167
- if (r.callbacks.some((c) => c instanceof Router)) {
178
+ if (r.callbacks.some((/** @type {unknown} */ c) => c instanceof Router)) {
168
179
  return false;
169
180
  }
170
181
  optimizedPath.push(r);
@@ -186,10 +197,45 @@ function optimizeRoute(router, route, routes) {
186
197
  }
187
198
  optimizedPath.push(route);
188
199
  route._caseGuards = caseGuards;
200
+ route._headGeneric = headGeneric;
189
201
 
190
202
  return optimizedPath;
191
203
  }
192
204
 
205
+ /**
206
+ * Whether a HEAD of some path this GET route answers would enter the earlier route of another
207
+ * verb for its param() callbacks: the router has callbacks and the paths can meet. A wrong yes
208
+ * costs the HEAD twin its native registration and nothing else.
209
+ *
210
+ * @param {RouteEntry} r the earlier route, of another verb
211
+ * @param {RouteEntry} route the GET route
212
+ * @param {boolean} caseSensitive
213
+ * @param {boolean} strictHere
214
+ * @returns {boolean}
215
+ */
216
+ function headEnters(r, route, caseSensitive, strictHere) {
217
+ if (r.paramCallbacks.size === 0 || typeof route.path !== "string") {
218
+ return false;
219
+ }
220
+ if (!route.path.includes(":")) {
221
+ if (typeof r.pattern === "string") {
222
+ return (
223
+ r.pattern === "/*" ||
224
+ (caseSensitive ? r.pattern === route.path : r.patternLower === route.path.toLowerCase())
225
+ );
226
+ }
227
+ return r.pattern.test(route.path) || (!strictHere && r.pattern.test(route.path + "/"));
228
+ }
229
+ if (typeof r.path !== "string" || !canBeOptimizedWithParams(r.path)) {
230
+ return true;
231
+ }
232
+ return pathsCanOverlap(
233
+ caseSensitive ? r.path : r.path.toLowerCase(),
234
+ caseSensitive ? route.path : route.path.toLowerCase(),
235
+ r.use
236
+ );
237
+ }
238
+
193
239
  /**
194
240
  * Hands every route reachable by path alone to the native uWS router, walking into mounted
195
241
  * routers with their prefix. Runs once, at listen.
@@ -284,6 +330,16 @@ function compileOptimizedRoutes(root) {
284
330
  route._whyGeneric = "something before it in the same router overlaps its paths";
285
331
  continue;
286
332
  }
333
+ // a route of another verb before the mount that a HEAD would enter, see headEnters
334
+ if (
335
+ !route._headGeneric &&
336
+ route.method === "GET" &&
337
+ outerGuards.length > 0 &&
338
+ typeof route.path === "string" &&
339
+ outerGuards.some((g) => headEntersGuard(g, pathPrefix + route.path))
340
+ ) {
341
+ route._headGeneric = true;
342
+ }
287
343
  // an earlier parameter route in the same router would take this literal path
288
344
  if (leafPath.length > 1) {
289
345
  const shadow = leafPath[leafPath.length - 2];
@@ -360,6 +416,7 @@ function registerUwsRoute(router, route, optimizedPath) {
360
416
  * @param {import("./router-utils.js").NativePreset|undefined} preset
361
417
  * @param {{skipHeaders: boolean, skipQuery: boolean}} skips
362
418
  * @param {string|null} wireMethod
419
+ * @returns {(res: UwsResponse, req: UwsRequest) => unknown} what µWS calls
363
420
  */
364
421
  const makeHandler = (chain, preset, skips, wireMethod) => {
365
422
  // where a granted skip lives, so a middleware added after listen can take it back: the
@@ -395,10 +452,11 @@ function registerUwsRoute(router, route, optimizedPath) {
395
452
  return router._refuseRequest(response);
396
453
  }
397
454
  if (optimizedParams) {
398
- // slicing them out of the path instead measured a wash
455
+ // slicing them out of the path instead measured a wash. The index is one the
456
+ // registered path declares, so the value is never undefined
399
457
  request.optimizedParams = new NullObject();
400
458
  for (let i = 0; i < optimizedParams.length; i++) {
401
- request.optimizedParams[optimizedParams[i]] = req.getParameter(i);
459
+ request.optimizedParams[optimizedParams[i]] = /** @type {string} */ (req.getParameter(i));
402
460
  }
403
461
  }
404
462
  const walk = new Walk(router, request, response, chain, true, skipUntil, nativeDone, nativeFail);
@@ -513,7 +571,7 @@ function registerUwsRoute(router, route, optimizedPath) {
513
571
  ? makeHandler(getChain, makePreset(route.path + "/", route.method, getSkips), getSkips, wireMethod)
514
572
  : fn;
515
573
  router.uwsApp[method](replacedPath + "/", slashFn);
516
- if (method === "get") {
574
+ if (method === "get" && !route._headGeneric) {
517
575
  router.uwsApp.head(
518
576
  replacedPath + "/",
519
577
  makeHandler(
@@ -525,7 +583,7 @@ function registerUwsRoute(router, route, optimizedPath) {
525
583
  );
526
584
  }
527
585
  }
528
- if (method === "get") {
586
+ if (method === "get" && !route._headGeneric) {
529
587
  // its own handler, the shared one would carry GET
530
588
  router.uwsApp.head(
531
589
  replacedPath,
package/src/options.d.ts CHANGED
@@ -43,7 +43,7 @@ export interface SendFileOptions {
43
43
  /** Extra headers for the response. */
44
44
  headers?: Record<string, string>;
45
45
  /** Internal: express.static's setHeaders, which res.sendFile itself does not take. */
46
- _setHeaders?: (res: any, path: string, stat: any) => void;
46
+ _setHeaders?: ((res: any, path: string, stat: any) => void) | undefined;
47
47
  /** First byte of the window to send. */
48
48
  start?: number;
49
49
  /** Last byte of the window to send. */
package/src/request.js CHANGED
@@ -43,6 +43,7 @@ const {
43
43
 
44
44
  // whose headers #collectHeader is filling: uWS's forEach is synchronous, so one callback serves
45
45
  // every request
46
+ /** @type {import("./request.js")|null} */
46
47
  let currentRequest = null;
47
48
 
48
49
  module.exports = class Request extends LazyReadable {
@@ -83,6 +84,12 @@ module.exports = class Request extends LazyReadable {
83
84
  /** @type {string[]|null} */
84
85
  #cachedSubdomains = null;
85
86
 
87
+ /** The socket stand-in `req.connection` once built, see the getter. @type {import("./socket.js")|null} */
88
+ #cachedConnection = null;
89
+
90
+ /** Behind `req.signal`, made on the first read. @type {AbortController|undefined} */
91
+ #abortController;
92
+
86
93
  /**
87
94
  * Copies one header out of uWS and notices what the constructor decides by. One function for
88
95
  * every request through currentRequest, an arrow per request cost a closure.
@@ -91,7 +98,8 @@ module.exports = class Request extends LazyReadable {
91
98
  * @param {string} value
92
99
  */
93
100
  static #collectHeader = (headerKey, value) => {
94
- const r = currentRequest;
101
+ // set by the two forEach callers, never null while uWS is calling back
102
+ const r = /** @type {Request} */ (currentRequest);
95
103
  r.#rawHeadersEntries.push(headerKey, value);
96
104
  // the response, built right after, must not answer keep-alive to a client that said close
97
105
  if (headerKey.length === 10 && headerKey === "connection" && saysClose(value)) {
@@ -321,7 +329,10 @@ module.exports = class Request extends LazyReadable {
321
329
  /** The verbs a path answers, for an OPTIONS; null on every other method. @type {Set<string>|null} */
322
330
  _matchedMethods = null;
323
331
 
324
- /** What each app.param() callback was called with, per router, made on first use. @type {Map<any, Map<any, any>>|null} */
332
+ /**
333
+ * What each app.param() callback was called with, per router, made on first use.
334
+ * @type {Map<Map<string, Function[]>, Map<string, import("./router.js").ParamCall>>|null}
335
+ */
325
336
  _paramCalled = null;
326
337
 
327
338
  /**
@@ -573,9 +584,6 @@ module.exports = class Request extends LazyReadable {
573
584
  return this.res?.finished || this.res?.aborted;
574
585
  }
575
586
 
576
- /** @type {AbortController|undefined} */
577
- #abortController;
578
-
579
587
  /**
580
588
  * node's `req.signal`, fired when the request is over: `@angular/ssr` reads it to give up a
581
589
  * render. Made on the first ask.
@@ -630,7 +638,10 @@ module.exports = class Request extends LazyReadable {
630
638
  return this;
631
639
  }
632
640
 
633
- /** Readable's pull: uWS pushes, so this only lifts the backpressure a full queue put on it. */
641
+ /**
642
+ * Readable's pull: uWS pushes, so this only lifts the backpressure a full queue put on it.
643
+ * @override
644
+ */
634
645
  _read() {
635
646
  // first, so a bodyless stream still ends for a consumer arriving after the response
636
647
  if (this.#emptyBody) {
@@ -1011,9 +1022,6 @@ module.exports = class Request extends LazyReadable {
1011
1022
  return ip;
1012
1023
  }
1013
1024
 
1014
- /** @type {import("./socket.js")|null} */
1015
- #cachedConnection = null;
1016
-
1017
1025
  /**
1018
1026
  * The socket stand-in, the same object as `res.socket`, kept here so it still answers once
1019
1027
  * the response is over and `res.socket` is null.
@@ -1214,8 +1222,8 @@ module.exports = class Request extends LazyReadable {
1214
1222
  *
1215
1223
  * @param {number} size length of the resource being served
1216
1224
  * @param {{combine?: boolean}} [options] combine adjacent and overlapping ranges
1217
- * @returns {Array|number|undefined} the ranges, -1 when unsatisfiable, -2 when malformed,
1218
- * or undefined when there is no Range header
1225
+ * @returns {import("range-parser").Ranges|-1|-2|undefined} the ranges, -1 when unsatisfiable,
1226
+ * -2 when malformed, or undefined when there is no Range header
1219
1227
  */
1220
1228
  range(size, options) {
1221
1229
  const range = this.headers["range"];
@@ -1302,10 +1310,8 @@ module.exports = class Request extends LazyReadable {
1302
1310
  return this.#rawHeadersEntries.slice();
1303
1311
  }
1304
1312
 
1305
- // the three below report work this request was made to do, for src/work.js
1306
-
1307
1313
  /**
1308
- * Whether the folded `req.headers` object was built.
1314
+ * Whether the folded `req.headers` object was built, which src/work.js reports.
1309
1315
  * @returns {boolean}
1310
1316
  */
1311
1317
  get _headersBuilt() {
@@ -1313,7 +1319,7 @@ module.exports = class Request extends LazyReadable {
1313
1319
  }
1314
1320
 
1315
1321
  /**
1316
- * Whether the query string was parsed at least once.
1322
+ * Whether the query string was parsed at least once, which src/work.js reports.
1317
1323
  * @returns {boolean}
1318
1324
  */
1319
1325
  get _queryParsed() {
@@ -1321,7 +1327,7 @@ module.exports = class Request extends LazyReadable {
1321
1327
  }
1322
1328
 
1323
1329
  /**
1324
- * Whether the socket stand-in `req.connection` was allocated.
1330
+ * Whether the socket stand-in `req.connection` was allocated, which src/work.js reports.
1325
1331
  * @returns {boolean}
1326
1332
  */
1327
1333
  get _socketBuilt() {
@@ -38,9 +38,12 @@ const kShapeMode =
38
38
  const VALIDATED_HEADER_NAMES = new Map();
39
39
  // The names and values that recur on every response, kept as Buffers for the uWS crossing: a Buffer
40
40
  // is memcpy'd as it is, a string pays a UTF-8 scan and copy per call. A header that is not here
41
- // crosses as the string it was. Names must stay lowercase, as writeHeaders receives them.
42
- const HEADER_NAME_BUF = { __proto__: null };
43
- const HEADER_VALUE_BUF = { __proto__: null };
41
+ // crosses as the string it was. Names must stay lowercase, as writeHeaders receives them. The
42
+ // casts are for the __proto__ key, which the type checker reads as a property
43
+ /** @type {Record<string, Buffer>} */
44
+ const HEADER_NAME_BUF = /** @type {any} */ ({ __proto__: null });
45
+ /** @type {Record<string, Buffer>} */
46
+ const HEADER_VALUE_BUF = /** @type {any} */ ({ __proto__: null });
44
47
  for (const s of ["connection", "keep-alive", "content-type", "vary", "x-powered-by", "content-encoding"]) {
45
48
  HEADER_NAME_BUF[s] = Buffer.from(s);
46
49
  }
@@ -81,11 +84,45 @@ function statusLine(code, text) {
81
84
  return `${code} ${text ?? statuses.message[code] ?? "unknown"}`.trim();
82
85
  }
83
86
 
87
+ /**
88
+ * The TypeError node throws for a chunk that is not a string, a Buffer or a Uint8Array, worded as
89
+ * node words it.
90
+ *
91
+ * @param {unknown} chunk what end() was handed, known here not to be a string or a Uint8Array
92
+ * @returns {NodeJS.ErrnoException}
93
+ */
94
+ function invalidChunkError(chunk) {
95
+ let received;
96
+ if (chunk === null) {
97
+ received = "null";
98
+ } else if (typeof chunk === "object" || typeof chunk === "function") {
99
+ const name = /** @type {any} */ (chunk).constructor?.name;
100
+ received = name ? `an instance of ${name}` : "an instance of Object";
101
+ } else if (typeof chunk === "bigint") {
102
+ received = `type bigint (${chunk}n)`;
103
+ } else if (typeof chunk === "symbol") {
104
+ received = `type symbol (${String(chunk)})`;
105
+ } else {
106
+ received = `type ${typeof chunk} (${chunk})`;
107
+ }
108
+ /** @type {NodeJS.ErrnoException} */
109
+ const err = new TypeError(
110
+ `The "chunk" argument must be of type string or an instance of Buffer or Uint8Array. Received ${received}`
111
+ );
112
+ err.code = "ERR_INVALID_ARG_TYPE";
113
+ // as node: the bracketed name goes into the stack's first line, then err.name reads TypeError
114
+ err.name = "TypeError [ERR_INVALID_ARG_TYPE]";
115
+ void err.stack;
116
+ delete (/** @type {Record<string, unknown>} */ (/** @type {unknown} */ (err)).name);
117
+ return err;
118
+ }
119
+
84
120
  module.exports = {
85
121
  kOutHeaders,
86
122
  kShapeMode,
87
123
  VALIDATED_HEADER_NAMES,
88
124
  HEADER_NAME_BUF,
89
125
  HEADER_VALUE_BUF,
90
- statusLine
126
+ statusLine,
127
+ invalidChunkError
91
128
  };