fulmine.js 5.19.3 → 5.19.5
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 +9 -4
- package/package.json +1 -1
- package/src/application.js +25 -36
- package/src/cli.js +22 -16
- package/src/cluster.js +2 -2
- package/src/compression.js +102 -61
- package/src/declarative.js +61 -32
- package/src/hot-settings.js +5 -5
- package/src/lazy-readable.js +8 -6
- package/src/lazy-writable.js +3 -3
- package/src/middlewares.js +168 -96
- package/src/nest.js +3 -2
- package/src/node-shim.js +10 -5
- package/src/optimizer.js +9 -7
- package/src/options.d.ts +9 -4
- package/src/request-utils.js +3 -2
- package/src/request.js +49 -38
- package/src/response-utils.js +4 -1
- package/src/response.js +244 -119
- package/src/route.js +3 -3
- package/src/router-utils.js +62 -14
- package/src/router.js +69 -35
- package/src/server-shape.js +14 -10
- package/src/server-timing.js +16 -4
- package/src/socket.js +3 -3
- package/src/testing.js +4 -3
- package/src/usage.js +10 -5
- package/src/utils.js +132 -23
- package/src/verify.js +4 -3
- package/src/view.js +1 -1
- package/src/walk.js +8 -7
- package/src/websocket.js +17 -8
- package/src/work.js +3 -3
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 {
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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}}
|
package/src/utils.js
CHANGED
|
@@ -30,6 +30,19 @@ const { Stats } = require("fs");
|
|
|
30
30
|
|
|
31
31
|
/** @typedef {import("./request.js")} Request */
|
|
32
32
|
/** @typedef {import("./response.js")} Response */
|
|
33
|
+
/**
|
|
34
|
+
* An error with the http-errors fields on it, which are what `res.status(err.status || 500)`, the
|
|
35
|
+
* error page and the body parsers read. None is required: a plain throw carries none of them.
|
|
36
|
+
* @typedef {Error & {
|
|
37
|
+
* status?: number,
|
|
38
|
+
* statusCode?: number,
|
|
39
|
+
* expose?: boolean,
|
|
40
|
+
* code?: string,
|
|
41
|
+
* type?: string,
|
|
42
|
+
* types?: string[],
|
|
43
|
+
* headers?: Record<string, any>
|
|
44
|
+
* }} HttpError
|
|
45
|
+
*/
|
|
33
46
|
|
|
34
47
|
const EMPTY_REGEX = new RegExp(``);
|
|
35
48
|
|
|
@@ -765,8 +778,9 @@ const MEMO_LIMIT = 512;
|
|
|
765
778
|
* The wrapped function must never answer undefined, since that is what the cache reads as a miss.
|
|
766
779
|
* The mime lookups here answer false for something they do not know, which caches correctly.
|
|
767
780
|
*
|
|
768
|
-
* @
|
|
769
|
-
* @
|
|
781
|
+
* @template T
|
|
782
|
+
* @param {(key: string) => T} fn
|
|
783
|
+
* @returns {(key: string) => T}
|
|
770
784
|
*/
|
|
771
785
|
function memoizeByString(fn) {
|
|
772
786
|
const cache = new Map();
|
|
@@ -794,6 +808,16 @@ const lookupType = memoizeByString((type) => mime.lookup(type) || "application/o
|
|
|
794
808
|
*/
|
|
795
809
|
const contentTypeFor = memoizeByString((type) => mime.contentType(type) || "application/octet-stream");
|
|
796
810
|
|
|
811
|
+
/**
|
|
812
|
+
* The content-type res.set stores for a value, which is what express stores: an extension resolved
|
|
813
|
+
* to its media type with the charset the database gives it, and false when it resolves to nothing.
|
|
814
|
+
* A false there is falsy for every default below it, so send() and json() write their own.
|
|
815
|
+
*
|
|
816
|
+
* @param {string} value
|
|
817
|
+
* @returns {string|false}
|
|
818
|
+
*/
|
|
819
|
+
const contentTypeSet = memoizeByString((value) => mime.contentType(value));
|
|
820
|
+
|
|
797
821
|
/**
|
|
798
822
|
* A media type from either spelling: "html" is looked up in the mime database, while anything
|
|
799
823
|
* containing a slash is already one and is parsed for its parameters.
|
|
@@ -811,7 +835,7 @@ function normalizeType(type) {
|
|
|
811
835
|
* unicode escapes, so a string in the body cannot close a script tag in an HTML page that embeds
|
|
812
836
|
* the response.
|
|
813
837
|
*
|
|
814
|
-
* @param {
|
|
838
|
+
* @param {unknown} value whatever the handler passed to res.json
|
|
815
839
|
* @param {any} [replacer] the "json replacer" setting
|
|
816
840
|
* @param {string|number} [spaces] the "json spaces" setting
|
|
817
841
|
* @param {boolean} [escape] the "json escape" setting
|
|
@@ -1042,13 +1066,17 @@ function cachedStat(file, ttl) {
|
|
|
1042
1066
|
/**
|
|
1043
1067
|
* A duration setting as milliseconds: false is off, a string is read by ms, a number is itself.
|
|
1044
1068
|
*
|
|
1045
|
-
* @param {
|
|
1069
|
+
* @param {string|number|boolean|undefined} value the setting as the application wrote it
|
|
1046
1070
|
* @param {string} name for the error, which names the setting the application wrote
|
|
1047
1071
|
* @returns {number}
|
|
1048
1072
|
*/
|
|
1049
1073
|
function durationSetting(value, name) {
|
|
1050
1074
|
const parsed =
|
|
1051
|
-
value === false || value === undefined
|
|
1075
|
+
value === false || value === undefined
|
|
1076
|
+
? 0
|
|
1077
|
+
: typeof value === "string"
|
|
1078
|
+
? ms(/** @type {import("ms").StringValue} */ (value))
|
|
1079
|
+
: value;
|
|
1052
1080
|
if (typeof parsed !== "number" || !(parsed >= 0)) {
|
|
1053
1081
|
throw new TypeError(`${name} must be a duration`);
|
|
1054
1082
|
}
|
|
@@ -1129,8 +1157,9 @@ function deprecated(oldMethod, newMethod, full = false) {
|
|
|
1129
1157
|
* request, each time picking up after the route it just ran, and Array.findIndex has no way to
|
|
1130
1158
|
* start anywhere but the beginning.
|
|
1131
1159
|
*
|
|
1132
|
-
* @
|
|
1133
|
-
* @param {
|
|
1160
|
+
* @template T
|
|
1161
|
+
* @param {T[]} arr
|
|
1162
|
+
* @param {(item: T, index: number, arr: T[]) => boolean} fn
|
|
1134
1163
|
* @param {number} [index] where to start
|
|
1135
1164
|
* @returns {number} the index, or -1
|
|
1136
1165
|
*/
|
|
@@ -1165,7 +1194,7 @@ function decode(path) {
|
|
|
1165
1194
|
*
|
|
1166
1195
|
* @param {string} value
|
|
1167
1196
|
* @returns {string}
|
|
1168
|
-
* @throws {
|
|
1197
|
+
* @throws {HttpError} carrying status 400 when the value cannot be decoded
|
|
1169
1198
|
*/
|
|
1170
1199
|
function decodeParam(value) {
|
|
1171
1200
|
// the common case, and worth the check: a parameter is usually a number or a word, and
|
|
@@ -1178,7 +1207,8 @@ function decodeParam(value) {
|
|
|
1178
1207
|
} catch {
|
|
1179
1208
|
// a URIError, not an Error: express throws what decodeURIComponent threw, so an error
|
|
1180
1209
|
// handler written as `err instanceof URIError` has to keep working here
|
|
1181
|
-
|
|
1210
|
+
/** @type {HttpError} */
|
|
1211
|
+
const err = new URIError(`Failed to decode param '${value}'`);
|
|
1182
1212
|
err.status = 400;
|
|
1183
1213
|
err.statusCode = 400;
|
|
1184
1214
|
err.expose = true;
|
|
@@ -1335,7 +1365,7 @@ function statTag(stat, weak) {
|
|
|
1335
1365
|
* ETag comes from its size and mtime while a body's comes from its contents.
|
|
1336
1366
|
*
|
|
1337
1367
|
* @param {{weak: boolean}} options
|
|
1338
|
-
* @returns {(body:
|
|
1368
|
+
* @returns {(body: string|Buffer|import("fs").Stats, encoding?: BufferEncoding) => string}
|
|
1339
1369
|
*/
|
|
1340
1370
|
function createETagGenerator(options) {
|
|
1341
1371
|
return function generateETag(body, encoding) {
|
|
@@ -1362,14 +1392,15 @@ function createETagGenerator(options) {
|
|
|
1362
1392
|
* @returns {boolean}
|
|
1363
1393
|
*/
|
|
1364
1394
|
function isRangeFresh(req, res) {
|
|
1365
|
-
|
|
1395
|
+
// folded to one string, as every header but set-cookie is
|
|
1396
|
+
const ifRange = /** @type {string|undefined} */ (req.headers["if-range"]);
|
|
1366
1397
|
if (!ifRange) {
|
|
1367
1398
|
return true;
|
|
1368
1399
|
}
|
|
1369
1400
|
|
|
1370
1401
|
// if-range as etag
|
|
1371
1402
|
if (ifRange.indexOf('"') !== -1) {
|
|
1372
|
-
const etag = res.get("etag");
|
|
1403
|
+
const etag = /** @type {string|undefined} */ (res.get("etag"));
|
|
1373
1404
|
return Boolean(etag && ifRange.indexOf(etag) !== -1);
|
|
1374
1405
|
}
|
|
1375
1406
|
|
|
@@ -1496,16 +1527,73 @@ function headerError(message, code) {
|
|
|
1496
1527
|
void err.stack;
|
|
1497
1528
|
// back to the prototype's "TypeError", which is what node leaves behind. Cast because Error
|
|
1498
1529
|
// declares name as always present, and this deletes the own property to uncover it again
|
|
1499
|
-
delete (/** @type {
|
|
1530
|
+
delete (/** @type {{name?: string}} */ (err).name);
|
|
1500
1531
|
err.code = code;
|
|
1501
1532
|
return err;
|
|
1502
1533
|
}
|
|
1503
1534
|
|
|
1535
|
+
/**
|
|
1536
|
+
* node's ERR_HTTP_HEADERS_SENT, for a head that can no longer change: "set" from setHeader,
|
|
1537
|
+
* "remove" from removeHeader, "write" from writeHead, which is how node words each one.
|
|
1538
|
+
*
|
|
1539
|
+
* @param {string} verb
|
|
1540
|
+
* @returns {NodeJS.ErrnoException}
|
|
1541
|
+
*/
|
|
1542
|
+
function headersSentError(verb) {
|
|
1543
|
+
/** @type {NodeJS.ErrnoException} */
|
|
1544
|
+
const err = new Error(`Cannot ${verb} headers after they are sent to the client`);
|
|
1545
|
+
// the first line of the stack reads as node's, see headerError
|
|
1546
|
+
err.name = "Error [ERR_HTTP_HEADERS_SENT]";
|
|
1547
|
+
void err.stack;
|
|
1548
|
+
delete (/** @type {{name?: string}} */ (err).name);
|
|
1549
|
+
err.code = "ERR_HTTP_HEADERS_SENT";
|
|
1550
|
+
return err;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
/**
|
|
1554
|
+
* Applies the headers a writeHead call carries, in either of node's shapes, (status, headers) or
|
|
1555
|
+
* (status, reason, headers), and answers the reason phrase when there was one. Shared with the
|
|
1556
|
+
* middleware that hooks writeHead: what it decides at the head has to see these first, the way
|
|
1557
|
+
* on-headers applies them before its listeners run.
|
|
1558
|
+
*
|
|
1559
|
+
* @param {{setHeader(name: string, value: import("http").OutgoingHttpHeader|undefined): unknown}} res
|
|
1560
|
+
* @param {string|import("http").OutgoingHttpHeaders|import("http").OutgoingHttpHeader[]} [statusMessage]
|
|
1561
|
+
* @param {import("http").OutgoingHttpHeaders|import("http").OutgoingHttpHeader[]} [headers]
|
|
1562
|
+
* @returns {string|undefined} the reason phrase
|
|
1563
|
+
*/
|
|
1564
|
+
function applyWriteHead(res, statusMessage, headers) {
|
|
1565
|
+
let reason;
|
|
1566
|
+
if (typeof statusMessage === "string") {
|
|
1567
|
+
reason = statusMessage;
|
|
1568
|
+
} else if (!headers) {
|
|
1569
|
+
// the two-argument shape, where what looked like a reason phrase is the headers
|
|
1570
|
+
headers = statusMessage;
|
|
1571
|
+
}
|
|
1572
|
+
if (Array.isArray(headers)) {
|
|
1573
|
+
// node takes a flat list here, name then value, and not a list of pairs. An odd length is
|
|
1574
|
+
// the caller's mistake and node names the argument in what it throws
|
|
1575
|
+
if (headers.length % 2 !== 0) {
|
|
1576
|
+
/** @type {NodeJS.ErrnoException} */
|
|
1577
|
+
const err = new TypeError(`The argument 'headers' is invalid. Received ${JSON.stringify(headers)}`);
|
|
1578
|
+
err.code = "ERR_INVALID_ARG_VALUE";
|
|
1579
|
+
throw err;
|
|
1580
|
+
}
|
|
1581
|
+
for (let i = 0; i < headers.length; i += 2) {
|
|
1582
|
+
res.setHeader(/** @type {string} */ (headers[i]), headers[i + 1]);
|
|
1583
|
+
}
|
|
1584
|
+
} else if (headers) {
|
|
1585
|
+
for (const header in headers) {
|
|
1586
|
+
res.setHeader(header, headers[header]);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
return reason;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1504
1592
|
/**
|
|
1505
1593
|
* Refuses a header name that is not an HTTP token, the way node's setHeader does and with its
|
|
1506
1594
|
* error, so an application catching ERR_INVALID_HTTP_TOKEN behind Express catches it here.
|
|
1507
1595
|
*
|
|
1508
|
-
* @param {
|
|
1596
|
+
* @param {unknown} name whatever a caller passed as a header name, which is what is being checked
|
|
1509
1597
|
* @returns {void}
|
|
1510
1598
|
* @throws {TypeError} if the name is not a token, which includes not being a string
|
|
1511
1599
|
*/
|
|
@@ -1579,21 +1667,39 @@ const STAT_ERROR_STATUS = { ENAMETOOLONG: 404, ENOTDIR: 404, ENOENT: 404 };
|
|
|
1579
1667
|
* of that handler as 500. The message is the status's own name, as http-errors writes it.
|
|
1580
1668
|
*
|
|
1581
1669
|
* @param {number} status
|
|
1582
|
-
* @
|
|
1670
|
+
* @param {string} [message] the status text unless given, as http-errors has it
|
|
1671
|
+
* @returns {HttpError}
|
|
1583
1672
|
*/
|
|
1584
|
-
function httpError(status) {
|
|
1585
|
-
|
|
1586
|
-
const err =
|
|
1673
|
+
function httpError(status, message = statuses.message[status] ?? "Error") {
|
|
1674
|
+
/** @type {HttpError} */
|
|
1675
|
+
const err = new Error(message);
|
|
1587
1676
|
// http-errors names these BadRequestError, ForbiddenError and so on, and the name is what the
|
|
1588
1677
|
// error page shows: an application looking at a 400 sees the same word Express shows it. Set
|
|
1589
1678
|
// before anything reads the stack, which V8 formats on first read
|
|
1590
|
-
err.name =
|
|
1679
|
+
err.name = httpErrorName(status);
|
|
1591
1680
|
err.expose = status < 500;
|
|
1592
1681
|
err.statusCode = status;
|
|
1593
1682
|
err.status = status;
|
|
1594
1683
|
return err;
|
|
1595
1684
|
}
|
|
1596
1685
|
|
|
1686
|
+
/**
|
|
1687
|
+
* The name http-errors gives an error for a status, NotFoundError for 404: each word of the status
|
|
1688
|
+
* text capitalised and run together, plus Error unless it already ends in it, which is how
|
|
1689
|
+
* "Internal Server Error" stays InternalServerError.
|
|
1690
|
+
*
|
|
1691
|
+
* @param {number} status
|
|
1692
|
+
* @returns {string}
|
|
1693
|
+
*/
|
|
1694
|
+
function httpErrorName(status) {
|
|
1695
|
+
const name = (statuses.message[status] ?? "Error")
|
|
1696
|
+
.split(" ")
|
|
1697
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
1698
|
+
.join("")
|
|
1699
|
+
.replace(/[^ _0-9a-z]/gi, "");
|
|
1700
|
+
return name.endsWith("Error") ? name : name + "Error";
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1597
1703
|
/**
|
|
1598
1704
|
* Marks an fs error the way send does before it is handed on, so an error handler reading
|
|
1599
1705
|
* err.status or err.statusCode finds what it would find behind Express. The three properties are
|
|
@@ -1602,8 +1708,8 @@ function httpError(status) {
|
|
|
1602
1708
|
*
|
|
1603
1709
|
* The error itself is returned rather than a new one, so its errno, code, syscall and path survive.
|
|
1604
1710
|
*
|
|
1605
|
-
* @param {
|
|
1606
|
-
* @returns {
|
|
1711
|
+
* @param {HttpError} err the fs error, which carries its errno and path
|
|
1712
|
+
* @returns {HttpError} the same error
|
|
1607
1713
|
*/
|
|
1608
1714
|
function asStatError(err) {
|
|
1609
1715
|
err.expose = false;
|
|
@@ -1616,8 +1722,7 @@ function asStatError(err) {
|
|
|
1616
1722
|
// A constructor whose instances have no prototype, so a key from a request body or a query string
|
|
1617
1723
|
// cannot reach Object.prototype. Typed as returning a plain record: without that, assigning one
|
|
1618
1724
|
// reads as assigning `any`, which resets narrowing instead of removing undefined from it.
|
|
1619
|
-
/** @type {new () => Record<string, any>} */
|
|
1620
|
-
const NullObject = /** @type {any} */ (function () {});
|
|
1725
|
+
const NullObject = /** @type {new () => Record<string, any>} */ (/** @type {unknown} */ (function () {}));
|
|
1621
1726
|
NullObject.prototype = Object.create(null);
|
|
1622
1727
|
|
|
1623
1728
|
module.exports = {
|
|
@@ -1646,6 +1751,7 @@ module.exports = {
|
|
|
1646
1751
|
entityTag,
|
|
1647
1752
|
statTag,
|
|
1648
1753
|
contentTypeFor,
|
|
1754
|
+
contentTypeSet,
|
|
1649
1755
|
negotiateEncoding,
|
|
1650
1756
|
ENCODING_BR,
|
|
1651
1757
|
ENCODING_GZIP,
|
|
@@ -1669,6 +1775,9 @@ module.exports = {
|
|
|
1669
1775
|
withUtf8Charset,
|
|
1670
1776
|
asStatError,
|
|
1671
1777
|
httpError,
|
|
1778
|
+
httpErrorName,
|
|
1779
|
+
headersSentError,
|
|
1780
|
+
applyWriteHead,
|
|
1672
1781
|
EMPTY_REGEX,
|
|
1673
1782
|
settingsEpoch
|
|
1674
1783
|
};
|
package/src/verify.js
CHANGED
|
@@ -109,7 +109,8 @@ function checkNode(running = process.versions.node, required = require("../packa
|
|
|
109
109
|
* @returns {string|undefined}
|
|
110
110
|
*/
|
|
111
111
|
function currentGlibc() {
|
|
112
|
-
return /** @type {
|
|
112
|
+
return /** @type {{header: {glibcVersionRuntime?: string}}} */ (process.report.getReport()).header
|
|
113
|
+
.glibcVersionRuntime;
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
/**
|
|
@@ -206,7 +207,7 @@ function checkBinary(platform = process.platform, arch = process.arch, abi = pro
|
|
|
206
207
|
*/
|
|
207
208
|
function abiToNode(abi) {
|
|
208
209
|
const known = { 108: "18", 115: "20", 127: "22", 131: "23", 137: "24", 147: "26" };
|
|
209
|
-
return
|
|
210
|
+
return known[abi] ?? `ABI ${abi}`;
|
|
210
211
|
}
|
|
211
212
|
|
|
212
213
|
/**
|
|
@@ -269,7 +270,7 @@ function checkDependencies(dir) {
|
|
|
269
270
|
const installed = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
270
271
|
for (const name of Object.keys(NEEDS_A_LOOK)) {
|
|
271
272
|
if (installed[name]) {
|
|
272
|
-
results.push(result("note", `${name} needs a different API here`,
|
|
273
|
+
results.push(result("note", `${name} needs a different API here`, NEEDS_A_LOOK[name]));
|
|
273
274
|
}
|
|
274
275
|
}
|
|
275
276
|
return results;
|
package/src/view.js
CHANGED
package/src/walk.js
CHANGED
|
@@ -31,6 +31,7 @@ const {
|
|
|
31
31
|
/** @typedef {import("./request.js")} Request */
|
|
32
32
|
/** @typedef {import("./response.js")} Response */
|
|
33
33
|
/** @typedef {import("./router.js")} Router */
|
|
34
|
+
/** @typedef {import("./router-utils.js").RouteEntry} RouteEntry */
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* One walk of one router's routes, for one request.
|
|
@@ -46,12 +47,12 @@ class Walk {
|
|
|
46
47
|
* @param {Router} router
|
|
47
48
|
* @param {Request} req
|
|
48
49
|
* @param {Response} res
|
|
49
|
-
* @param {
|
|
50
|
+
* @param {RouteEntry[]} routes the route table being walked, see createRoute in router.js
|
|
50
51
|
* @param {boolean} skipCheck take the route at the index without matching it, which is how an
|
|
51
52
|
* already-decided chain is walked
|
|
52
|
-
* @param {
|
|
53
|
-
* @param {(value:
|
|
54
|
-
* @param {(err:
|
|
53
|
+
* @param {RouteEntry|undefined} skipUntil route to resume after when this chain runs out, or undefined
|
|
54
|
+
* @param {(value: RouteEntry|false) => void} resolve
|
|
55
|
+
* @param {(err: unknown) => void} reject
|
|
55
56
|
*/
|
|
56
57
|
constructor(router, req, res, routes, skipCheck, skipUntil, resolve, reject) {
|
|
57
58
|
this.router = router;
|
|
@@ -85,7 +86,7 @@ class Walk {
|
|
|
85
86
|
* Leaves the rest of this route, with the error if there is one, and carries on with the route
|
|
86
87
|
* after it.
|
|
87
88
|
*
|
|
88
|
-
* @param {
|
|
89
|
+
* @param {unknown} [err] whatever was thrown, which need not be an Error
|
|
89
90
|
*/
|
|
90
91
|
stepOutOfRoute(err) {
|
|
91
92
|
if (err) {
|
|
@@ -289,7 +290,7 @@ class Walk {
|
|
|
289
290
|
* Enters the route the walk is on: a mount adjusts req.url, req.path and the mount stack on the
|
|
290
291
|
* way in, and then the route's callbacks run one after another through next().
|
|
291
292
|
*
|
|
292
|
-
* @param {
|
|
293
|
+
* @param {true|"route"} continueRoute what _preprocessRequest decided: true to run, "route" to skip
|
|
293
294
|
*/
|
|
294
295
|
runRoute(continueRoute) {
|
|
295
296
|
const req = this.req;
|
|
@@ -439,7 +440,7 @@ class Walk {
|
|
|
439
440
|
* One hop, which is what next() does: with nothing, run the route's next callback; with "route",
|
|
440
441
|
* leave the route; with anything else, remember it as the error and carry on.
|
|
441
442
|
*
|
|
442
|
-
* @param {
|
|
443
|
+
* @param {unknown} thingamabob what next() was called with: nothing, "route", or an error
|
|
443
444
|
*/
|
|
444
445
|
step(thingamabob) {
|
|
445
446
|
const req = this.req;
|
package/src/websocket.js
CHANGED
|
@@ -20,6 +20,12 @@ const { canBeOptimizedWithParams, decodeParam, NullObject } = require("./utils.j
|
|
|
20
20
|
|
|
21
21
|
/** @typedef {import("./router.js")} Router */
|
|
22
22
|
/** @typedef {import("./application.js").Application} Application */
|
|
23
|
+
/** @typedef {import("./request.js")} Request */
|
|
24
|
+
/** @typedef {import("./response.js")} Response */
|
|
25
|
+
/** @typedef {import("./router-utils.js").WsRoute} WsRoute */
|
|
26
|
+
/** @typedef {import("uWebSockets.js").HttpRequest} UwsRequest */
|
|
27
|
+
/** @typedef {import("uWebSockets.js").HttpResponse} UwsResponse */
|
|
28
|
+
/** @typedef {import("uWebSockets.js").us_socket_context_t} UwsContext */
|
|
23
29
|
|
|
24
30
|
// the parameter names in a path, in the order µWS numbers them
|
|
25
31
|
const PARAM = /:(\w+)/g;
|
|
@@ -55,8 +61,8 @@ function joinPaths(prefix, path) {
|
|
|
55
61
|
* @param {Router} router
|
|
56
62
|
* @param {string|null} prefix the mount path accumulated so far, or null once a mount was a
|
|
57
63
|
* shape µWS cannot match, which makes everything below it unreachable
|
|
58
|
-
* @param {
|
|
59
|
-
* @param {Set<
|
|
64
|
+
* @param {WsRoute[]} out
|
|
65
|
+
* @param {Set<Router>} seen routers already walked, since a router may be mounted twice
|
|
60
66
|
*/
|
|
61
67
|
function collectRoutes(router, prefix, out, seen) {
|
|
62
68
|
if (seen.has(router)) {
|
|
@@ -102,14 +108,17 @@ function collectRoutes(router, prefix, out, seen) {
|
|
|
102
108
|
* The uWS upgrade handler for one route: builds this project's request and response, offers them
|
|
103
109
|
* to the application's own `upgrade` hook, and completes the handshake unless that hook answered.
|
|
104
110
|
*
|
|
105
|
-
* @param {
|
|
111
|
+
* @param {Router} app the router whose request and response classes serve this route
|
|
106
112
|
* @param {string} path the composed path, whose parameters are read back by index
|
|
107
|
-
* @param {
|
|
108
|
-
* @returns {(res:
|
|
113
|
+
* @param {Record<string, unknown>} behavior what the caller registered
|
|
114
|
+
* @returns {(res: UwsResponse, req: UwsRequest, context: UwsContext) => void}
|
|
109
115
|
*/
|
|
110
116
|
function makeUpgradeHandler(app, path, behavior) {
|
|
111
117
|
const paramNames = [...path.matchAll(PARAM)].map((match) => match[1]);
|
|
112
|
-
|
|
118
|
+
// a function, checked by checkBehavior where it was registered
|
|
119
|
+
const userUpgrade = /** @type {((req: Request, res: Response) => void|Promise<void>)|undefined} */ (
|
|
120
|
+
behavior.upgrade
|
|
121
|
+
);
|
|
113
122
|
|
|
114
123
|
return (res, req, context) => {
|
|
115
124
|
// read off the uWS request before anything can await: it is neutered on return, and the
|
|
@@ -122,7 +131,7 @@ function makeUpgradeHandler(app, path, behavior) {
|
|
|
122
131
|
if (paramNames.length) {
|
|
123
132
|
const params = new NullObject();
|
|
124
133
|
for (let i = 0; i < paramNames.length; i++) {
|
|
125
|
-
params[paramNames[i]] = decodeParam(req.getParameter(i));
|
|
134
|
+
params[paramNames[i]] = decodeParam(/** @type {string} */ (req.getParameter(i)));
|
|
126
135
|
}
|
|
127
136
|
request.params = params;
|
|
128
137
|
}
|
|
@@ -216,7 +225,7 @@ function registerWebSocketRoutes(app) {
|
|
|
216
225
|
* used: a handler under a misspelled name would otherwise never run and never say why.
|
|
217
226
|
*
|
|
218
227
|
* @param {string} path
|
|
219
|
-
* @param {
|
|
228
|
+
* @param {unknown} behavior whatever was passed as one, which is what is being checked
|
|
220
229
|
*/
|
|
221
230
|
function checkBehavior(path, behavior) {
|
|
222
231
|
if (typeof path !== "string") {
|
package/src/work.js
CHANGED
|
@@ -54,7 +54,7 @@ function work(req, res) {
|
|
|
54
54
|
const native = req.route?._native;
|
|
55
55
|
// cast for the three the classes do not declare: `body` is deliberately not a field of
|
|
56
56
|
// Request, and the two stream states are node's own, written when a lazy stream is built
|
|
57
|
-
const loose = /** @type {
|
|
57
|
+
const loose = /** @type {{body?: unknown, _readableState?: unknown}} */ (req);
|
|
58
58
|
return {
|
|
59
59
|
native: Boolean(native),
|
|
60
60
|
declarative: Boolean(native?.declarative),
|
|
@@ -62,7 +62,7 @@ function work(req, res) {
|
|
|
62
62
|
query: req._queryParsed,
|
|
63
63
|
body: loose.body !== undefined,
|
|
64
64
|
requestStream: loose._readableState !== undefined,
|
|
65
|
-
responseStream: /** @type {
|
|
65
|
+
responseStream: /** @type {{_writableState?: unknown}} */ (res)._writableState !== undefined,
|
|
66
66
|
socket: req._socketBuilt || res._socketBuilt
|
|
67
67
|
};
|
|
68
68
|
}
|
|
@@ -86,7 +86,7 @@ const NAMES = [
|
|
|
86
86
|
function names(done) {
|
|
87
87
|
const listed = [];
|
|
88
88
|
for (const [key, name] of NAMES) {
|
|
89
|
-
if (
|
|
89
|
+
if (done[key]) {
|
|
90
90
|
listed.push(name);
|
|
91
91
|
}
|
|
92
92
|
}
|