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/README.md +3 -3
- package/package.json +5 -1
- package/src/adopt.js +4 -1
- package/src/application.js +11 -2
- package/src/cli.js +1 -0
- package/src/compression.js +40 -3
- package/src/declarative.js +66 -25
- package/src/lazy-readable.js +15 -15
- package/src/lazy-writable.js +15 -15
- package/src/middlewares.js +24 -11
- package/src/nest.js +2 -0
- package/src/optimizer.js +65 -7
- package/src/options.d.ts +1 -1
- package/src/request.js +22 -16
- package/src/response-utils.js +41 -4
- package/src/response.js +102 -62
- package/src/route.js +5 -3
- package/src/router-utils.js +40 -5
- package/src/router.js +23 -9
- package/src/server-shape.js +3 -3
- package/src/server-timing.js +21 -0
- package/src/testing.js +2 -1
- package/src/usage.js +40 -3
- package/src/utils.js +4 -1
- package/src/verify.js +2 -0
- package/src/view.js +1 -0
- package/src/websocket.js +2 -1
- package/src/work.js +1 -0
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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 {
|
|
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() {
|
package/src/response-utils.js
CHANGED
|
@@ -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
|
-
|
|
43
|
-
|
|
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
|
};
|