fulmine.js 5.1.7 → 5.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/package.json +5 -3
- package/src/application.js +9 -7
- package/src/declarative.js +7 -0
- package/src/request.js +10 -3
- package/src/router.js +16 -12
- package/src/usage.js +88 -43
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ See [Migrating](#migrating) for what it handles and what it deliberately does no
|
|
|
25
25
|
|
|
26
26
|
[](https://www.npmjs.com/package/fulmine.js)
|
|
27
27
|
[](https://nodejs.org)
|
|
28
|
+
[](https://coveralls.io/github/nigrosimone/fulmine.js?branch=main)
|
|
28
29
|
[](./LICENSE)
|
|
29
30
|
|
|
30
31
|
## Why this exists
|
|
@@ -55,7 +56,7 @@ to run it yourself.
|
|
|
55
56
|
|
|
56
57
|
Numbers produced by a project about itself deserve suspicion, so Fulmine also stands in public arenas, run by their own rigs under their own rules:
|
|
57
58
|
|
|
58
|
-
- **[HttpArena](https://www.http-arena.com/#sort=rps:-1&q=Js)** (the link lands filtered on the JavaScript entries): first among the JavaScript entries
|
|
59
|
+
- **[HttpArena](https://www.http-arena.com/#sort=rps:-1&q=Js)** (the link lands filtered on the JavaScript entries): first among the JavaScript entries, across fifteen subscribed profiles. The saved runs measure 24.3 million WebSocket echoes per second pipelined and 3.77 million one-at-a-time (past Bun's own dedicated WebSocket entry), 7.3 million pipelined HTTP requests per second, 1.12 million on the json profile with 1.04 million of that surviving TLS, 457 thousand on compressed json, and 359 thousand on the Postgres CRUD profile, within ten percent of the leading Rust and C# entries there.
|
|
59
60
|
- **[web-frameworks](https://github.com/the-benchmarker/web-frameworks)**: entry merged, numbers arrive with their next published round.
|
|
60
61
|
|
|
61
62
|
More to come as their maintainers take the entries in.
|
|
@@ -226,9 +227,10 @@ which runs the same file against Express and against Fulmine and compares the ou
|
|
|
226
227
|
|
|
227
228
|
## HTTP/3
|
|
228
229
|
|
|
229
|
-
HTTP/3 is
|
|
230
|
+
There is an `http3: true` option, inherited from Ultimate Express, that asks µWebSockets.js for its experimental HTTP/3 app. **It is guarded off with the currently pinned µWS build**: asking for it throws a clear error, because the underlying `H3App` segfaults during construction on Linux, verified with µWS alone before a single request is served. On Windows the listener does come up, but nothing answers over QUIC that we could verify, and shipping an option that works on no deployable platform helps nobody. A skipped canary test probes `H3App` on every CI run and will turn red the day µWS ships working QUIC in its prebuilt binaries, which is when the guard goes and this section changes.
|
|
230
231
|
|
|
231
232
|
```js
|
|
233
|
+
// what it would look like, once µWS's H3 support actually works
|
|
232
234
|
const app = express({
|
|
233
235
|
http3: true,
|
|
234
236
|
uwsOptions: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fulmine.js",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.9",
|
|
4
4
|
"description": "Drop-in Express 5 replacement on uWebSockets.js. Your existing middleware keeps working.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"test:types": "tsd --files tests/types/*.test-d.ts",
|
|
14
14
|
"test:express": "node tools/express-suite.js",
|
|
15
15
|
"benchmark:compare": "node benchmark/run.js",
|
|
16
|
-
"cover": "npm run cover:
|
|
16
|
+
"cover": "npm run cover:full && npm run cover:report",
|
|
17
17
|
"cover:unit": "nyc --silent npm run test",
|
|
18
18
|
"cover:report": "nyc report --reporter=html",
|
|
19
19
|
"lint": "eslint .",
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
"typecheck": "tsc -p tsconfig.typecheck.json",
|
|
26
26
|
"benchmark:ab": "node benchmark/ab.js",
|
|
27
27
|
"benchmark:profile": "node benchmark/profile.js",
|
|
28
|
-
"release:local": "node tools/release-local.js"
|
|
28
|
+
"release:local": "node tools/release-local.js",
|
|
29
|
+
"cover:full": "nyc --silent npm run test && nyc --silent --no-clean npm run test:unit && nyc --silent --no-clean npm run test:express && nyc report",
|
|
30
|
+
"cover:check": "nyc check-coverage --statements 93 --branches 88 --functions 92 --lines 93"
|
|
29
31
|
},
|
|
30
32
|
"engines": {
|
|
31
33
|
"node": ">=22"
|
package/src/application.js
CHANGED
|
@@ -15,10 +15,7 @@ See the License for the specific language governing permissions and
|
|
|
15
15
|
limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
// H3App, DeclarativeResponse and _cfg all exist at runtime but are missing from the
|
|
19
|
-
// declaration file the package ships, so the module is read through a loose alias
|
|
20
18
|
const uWS = require("uWebSockets.js");
|
|
21
|
-
const uWSAny = /** @type {any} */ (uWS);
|
|
22
19
|
const Router = require("./router.js");
|
|
23
20
|
const {
|
|
24
21
|
removeDuplicateSlashes,
|
|
@@ -102,10 +99,14 @@ class Application extends Router {
|
|
|
102
99
|
if (settings.uwsApp) {
|
|
103
100
|
this.uwsApp = settings.uwsApp;
|
|
104
101
|
} else if (settings.http3) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
// uWS.H3App exists in the pinned build but its QUIC stack does not: the constructor
|
|
103
|
+
// segfaults on Linux and hangs forever on Windows before serving a single request,
|
|
104
|
+
// verified 2026-08-05 with uWS alone. A clear throw beats a native crash; this
|
|
105
|
+
// branch goes back to H3App once uNetworking ships working QUIC in the prebuilts.
|
|
106
|
+
throw new Error(
|
|
107
|
+
"http3 is not usable with the pinned uWebSockets.js build: its H3App crashes " +
|
|
108
|
+
"during construction. Track uNetworking/uWebSockets.js for working QUIC support."
|
|
109
|
+
);
|
|
109
110
|
} else if (settings.uwsOptions.key_file_name && settings.uwsOptions.cert_file_name) {
|
|
110
111
|
this.uwsApp = uWS.SSLApp(settings.uwsOptions);
|
|
111
112
|
} else {
|
|
@@ -365,6 +366,7 @@ class Application extends Router {
|
|
|
365
366
|
if (value !== false && this._skipPresets?.size) {
|
|
366
367
|
for (const preset of this._skipPresets) {
|
|
367
368
|
preset.skipHeaders = false;
|
|
369
|
+
preset.skipQuery = false;
|
|
368
370
|
}
|
|
369
371
|
this._skipPresets.clear();
|
|
370
372
|
}
|
package/src/declarative.js
CHANGED
|
@@ -606,6 +606,13 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
606
606
|
}
|
|
607
607
|
}
|
|
608
608
|
|
|
609
|
+
// a handler that never sends is not a response: Express leaves the request waiting, so
|
|
610
|
+
// compiling the empty shape would answer a bare 200 where the ordinary path answers
|
|
611
|
+
// nothing at all. It has to fall back instead.
|
|
612
|
+
if (!sendUsed && !sendStatusUsed) {
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
|
|
609
616
|
let decRes = new uWSAny.DeclarativeResponse();
|
|
610
617
|
|
|
611
618
|
if (statusCode !== 200) {
|
package/src/request.js
CHANGED
|
@@ -263,9 +263,16 @@ module.exports = class Request extends Readable {
|
|
|
263
263
|
this.app = app;
|
|
264
264
|
// both forms are kept, because both are asked for: the query with its "?" goes into
|
|
265
265
|
// req.url, and req.query parses the raw one. Keeping only the first meant slicing the "?"
|
|
266
|
-
// back off for every request that reads req.query.
|
|
267
|
-
|
|
268
|
-
|
|
266
|
+
// back off for every request that reads req.query. When the chain provably reads
|
|
267
|
+
// neither, the native call is not made at all: the framework's own answers, the 404
|
|
268
|
+
// included, are written from the path alone
|
|
269
|
+
if (skipHolder !== undefined && skipHolder.skipQuery) {
|
|
270
|
+
this._rawQuery = "";
|
|
271
|
+
this.urlQuery = "";
|
|
272
|
+
} else {
|
|
273
|
+
this._rawQuery = req.getQuery() ?? "";
|
|
274
|
+
this.urlQuery = this._rawQuery === "" ? "" : "?" + this._rawQuery;
|
|
275
|
+
}
|
|
269
276
|
if (preset) {
|
|
270
277
|
// the registration's constants: two native crossings and their strings not asked for
|
|
271
278
|
this.path = preset.path;
|
package/src/router.js
CHANGED
|
@@ -36,7 +36,7 @@ const compileDeclarative = require("./declarative.js");
|
|
|
36
36
|
const statuses = require("statuses");
|
|
37
37
|
const { METHODS } = require("http");
|
|
38
38
|
const { isNodeRequest, serveNodeRequest } = require("./node-shim.js");
|
|
39
|
-
const {
|
|
39
|
+
const { chainUsage } = require("./usage.js");
|
|
40
40
|
|
|
41
41
|
// every method the declarative compiler can emit: a patched one must disable compilation, or the
|
|
42
42
|
// patch would be honoured everywhere but on compiled routes
|
|
@@ -693,9 +693,10 @@ function nativePreset(path, method, strict) {
|
|
|
693
693
|
opPath: endsWithSlash && path !== "/" && !strict ? path.slice(0, -1) : path,
|
|
694
694
|
isOptions: method === "OPTIONS",
|
|
695
695
|
isHead: method === "HEAD",
|
|
696
|
-
// set at registration when the whole chain provably never reads a header
|
|
697
|
-
// because a middleware added after listen
|
|
698
|
-
skipHeaders: false
|
|
696
|
+
// set at registration when the whole chain provably never reads a header, or never
|
|
697
|
+
// reads the query; mutable, because a middleware added after listen takes them back
|
|
698
|
+
skipHeaders: false,
|
|
699
|
+
skipQuery: false
|
|
699
700
|
};
|
|
700
701
|
}
|
|
701
702
|
|
|
@@ -1148,6 +1149,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1148
1149
|
if (this._skipPresets?.size) {
|
|
1149
1150
|
for (const preset of this._skipPresets) {
|
|
1150
1151
|
preset.skipHeaders = false;
|
|
1152
|
+
preset.skipQuery = false;
|
|
1151
1153
|
}
|
|
1152
1154
|
this._skipPresets.clear();
|
|
1153
1155
|
}
|
|
@@ -1469,8 +1471,8 @@ module.exports = class Router extends EventEmitter {
|
|
|
1469
1471
|
// listen can take it back: a literal registration's preset doubles as it, and a
|
|
1470
1472
|
// parameterised one, which has no preset, gets a holder of its own
|
|
1471
1473
|
let skipHolder = preset;
|
|
1472
|
-
if (skipHolder === undefined && skips) {
|
|
1473
|
-
skipHolder = { skipHeaders:
|
|
1474
|
+
if (skipHolder === undefined && (skips.skipHeaders || skips.skipQuery)) {
|
|
1475
|
+
skipHolder = { skipHeaders: skips.skipHeaders, skipQuery: skips.skipQuery };
|
|
1474
1476
|
(this._skipPresets ??= new Set()).add(skipHolder);
|
|
1475
1477
|
}
|
|
1476
1478
|
// all three are registration-time constants: computing them in the handler was a
|
|
@@ -1532,8 +1534,9 @@ module.exports = class Router extends EventEmitter {
|
|
|
1532
1534
|
// headers), no error middleware may exist anywhere (a throw hands the request to code
|
|
1533
1535
|
// the analysis never saw), and every callback in the chain has to pass the source
|
|
1534
1536
|
// analysis in usage.js, whose default answer is no.
|
|
1535
|
-
|
|
1536
|
-
let
|
|
1537
|
+
const NO_SKIPS = { skipHeaders: false, skipQuery: false };
|
|
1538
|
+
let getSkips = NO_SKIPS;
|
|
1539
|
+
let headSkips = NO_SKIPS;
|
|
1537
1540
|
if (route.method === "GET" && this.get("etag") === false) {
|
|
1538
1541
|
let hasErr = this._hasErrMwCache;
|
|
1539
1542
|
if (hasErr === undefined) {
|
|
@@ -1544,15 +1547,16 @@ module.exports = class Router extends EventEmitter {
|
|
|
1544
1547
|
// route may be able to catch the same path
|
|
1545
1548
|
const owner = route.owner ?? this;
|
|
1546
1549
|
const noLaterMatch = !owner._isFollowedByAnOverlap.call(owner, route, owner._routes);
|
|
1547
|
-
getSkips =
|
|
1548
|
-
headSkips = headChain === getChain ? getSkips :
|
|
1550
|
+
getSkips = chainUsage(getChain, noLaterMatch);
|
|
1551
|
+
headSkips = headChain === getChain ? getSkips : chainUsage(headChain, noLaterMatch);
|
|
1549
1552
|
}
|
|
1550
1553
|
}
|
|
1551
1554
|
// remembered so a middleware or setting arriving after listen can take the skips back
|
|
1552
1555
|
const makePreset = (path, method, skips) => {
|
|
1553
1556
|
const preset = nativePreset(path, method, strictHere);
|
|
1554
|
-
if (skips) {
|
|
1555
|
-
preset.skipHeaders =
|
|
1557
|
+
if (skips.skipHeaders || skips.skipQuery) {
|
|
1558
|
+
preset.skipHeaders = skips.skipHeaders;
|
|
1559
|
+
preset.skipQuery = skips.skipQuery;
|
|
1556
1560
|
(this._skipPresets ??= new Set()).add(preset);
|
|
1557
1561
|
}
|
|
1558
1562
|
return preset;
|
package/src/usage.js
CHANGED
|
@@ -13,6 +13,13 @@ const kGetSafe = Symbol("fulmine.getSafe");
|
|
|
13
13
|
// the parameter, so anything that can reach another object could reach headers through it.
|
|
14
14
|
const REQ_OK = new Set(["query", "params", "body", "method", "path", "url", "baseUrl", "originalUrl", "route"]);
|
|
15
15
|
|
|
16
|
+
// Reading any of these needs the query string fetched: req.url and req.originalUrl carry it
|
|
17
|
+
const REQ_QUERY = new Set(["query", "url", "originalUrl"]);
|
|
18
|
+
|
|
19
|
+
// Writing any of these re-enters routing: dispatch treats a changed req.url as a rewrite and
|
|
20
|
+
// walks routes nobody analyzed, so an assignment is as disqualifying as an unknown call
|
|
21
|
+
const REQ_NO_WRITE = new Set(["url", "originalUrl", "path", "baseUrl", "method"]);
|
|
22
|
+
|
|
16
23
|
// What a handler may do with `res`: writing the response. Anything that negotiates against
|
|
17
24
|
// request headers (format, redirect, sendFile, jsonp) is deliberately absent.
|
|
18
25
|
const RES_OK = new Set([
|
|
@@ -38,25 +45,26 @@ const RES_OK = new Set([
|
|
|
38
45
|
"cork"
|
|
39
46
|
]);
|
|
40
47
|
|
|
41
|
-
// what the analysis can say about one callback
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
48
|
+
// what the analysis can say about one callback, as independent facts
|
|
49
|
+
const UNKNOWN = 1; // a shape the walk cannot vouch for: could do anything
|
|
50
|
+
const NEXT_PLAIN = 2; // calls next() bare: advances the chain, may fall off its end
|
|
51
|
+
const NEXT_ERROR = 4; // calls next(err): lands in the framework's own error answer
|
|
52
|
+
const QUERY = 8; // reads req.query, req.url or req.originalUrl
|
|
46
53
|
|
|
47
54
|
const verdicts = new WeakMap();
|
|
48
55
|
|
|
49
56
|
/**
|
|
50
|
-
* What one callback provably does. The default is
|
|
51
|
-
* and any alias of req, res or next
|
|
52
|
-
* source analysis sound to act on.
|
|
57
|
+
* What one callback provably does, as a mask of the facts above. The default is UNKNOWN: any
|
|
58
|
+
* shape this walk does not understand and any alias of req, res or next could do anything.
|
|
59
|
+
* That inversion is what makes source analysis sound to act on.
|
|
53
60
|
*
|
|
54
61
|
* @param {Function} fn
|
|
55
|
-
* @returns {number}
|
|
62
|
+
* @returns {number}
|
|
56
63
|
*/
|
|
57
|
-
function
|
|
64
|
+
function callbackUsage(fn) {
|
|
58
65
|
if (fn[kGetSafe]) {
|
|
59
|
-
|
|
66
|
+
// the body parsers: they advance the chain and read nothing a skip would miss
|
|
67
|
+
return NEXT_PLAIN;
|
|
60
68
|
}
|
|
61
69
|
let verdict = verdicts.get(fn);
|
|
62
70
|
if (verdict === undefined) {
|
|
@@ -68,7 +76,13 @@ function callbackSkipsHeaders(fn) {
|
|
|
68
76
|
|
|
69
77
|
/** @param {Function} fn @returns {number} */
|
|
70
78
|
function analyze(fn) {
|
|
71
|
-
|
|
79
|
+
// toString is application-controlled and may throw or answer anything: unreadable is unknown
|
|
80
|
+
let code;
|
|
81
|
+
try {
|
|
82
|
+
code = String(fn.toString());
|
|
83
|
+
} catch {
|
|
84
|
+
return UNKNOWN;
|
|
85
|
+
}
|
|
72
86
|
if (code.startsWith("function") || code.startsWith("async function")) {
|
|
73
87
|
code = code.replace(/function *\(/, "function __cb(");
|
|
74
88
|
}
|
|
@@ -77,11 +91,11 @@ function analyze(fn) {
|
|
|
77
91
|
tree = acorn.parse(code, { ecmaVersion: "latest" });
|
|
78
92
|
} catch {
|
|
79
93
|
// class methods and native functions do not parse alone, and unread code is unknown code
|
|
80
|
-
return
|
|
94
|
+
return UNKNOWN;
|
|
81
95
|
}
|
|
82
96
|
let root = /** @type {any} */ (tree.body[0]);
|
|
83
97
|
if (!root) {
|
|
84
|
-
return
|
|
98
|
+
return UNKNOWN;
|
|
85
99
|
}
|
|
86
100
|
if (root.type === "ExpressionStatement") {
|
|
87
101
|
root = root.expression;
|
|
@@ -91,14 +105,14 @@ function analyze(fn) {
|
|
|
91
105
|
root.type !== "ArrowFunctionExpression" &&
|
|
92
106
|
root.type !== "FunctionExpression"
|
|
93
107
|
) {
|
|
94
|
-
return
|
|
108
|
+
return UNKNOWN;
|
|
95
109
|
}
|
|
96
110
|
|
|
97
111
|
const params = /** @type {any[]} */ (root.params);
|
|
98
112
|
// rest or destructured parameters alias the objects somewhere the walk cannot follow
|
|
99
113
|
for (const p of params) {
|
|
100
114
|
if (p.type !== "Identifier") {
|
|
101
|
-
return
|
|
115
|
+
return UNKNOWN;
|
|
102
116
|
}
|
|
103
117
|
}
|
|
104
118
|
const reqName = params[0] ? params[0].name : null;
|
|
@@ -108,15 +122,38 @@ function analyze(fn) {
|
|
|
108
122
|
// Every appearance of the three names in the whole body is judged, nested functions
|
|
109
123
|
// included: an inner binding that shadows one of them only makes this stricter, never
|
|
110
124
|
// looser, so scope tracking is not needed for soundness.
|
|
111
|
-
let
|
|
112
|
-
let usesNext = false;
|
|
125
|
+
let mask = 0;
|
|
113
126
|
walk(root.body, null, (node, parent) => {
|
|
114
|
-
if (
|
|
127
|
+
if (mask & UNKNOWN) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (node.type === "MemberExpression" && !node.computed && node.object.type === "Identifier") {
|
|
131
|
+
const owner = node.object.name;
|
|
132
|
+
if (owner !== reqName) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const member = node.property.name;
|
|
136
|
+
if (REQ_QUERY.has(member)) {
|
|
137
|
+
mask |= QUERY;
|
|
138
|
+
}
|
|
139
|
+
// an assignment, an update or a delete on the routing members re-enters dispatch
|
|
140
|
+
if (
|
|
141
|
+
REQ_NO_WRITE.has(member) &&
|
|
142
|
+
parent &&
|
|
143
|
+
((parent.type === "AssignmentExpression" && parent.left === node) ||
|
|
144
|
+
(parent.type === "UpdateExpression" && parent.argument === node) ||
|
|
145
|
+
(parent.type === "UnaryExpression" && parent.operator === "delete" && parent.argument === node))
|
|
146
|
+
) {
|
|
147
|
+
mask |= UNKNOWN;
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (node.type !== "Identifier") {
|
|
115
152
|
return;
|
|
116
153
|
}
|
|
117
154
|
const name = node.name;
|
|
118
155
|
if (name === "eval" || name === "arguments") {
|
|
119
|
-
|
|
156
|
+
mask |= UNKNOWN;
|
|
120
157
|
return;
|
|
121
158
|
}
|
|
122
159
|
if (name !== reqName && name !== resName && name !== nextName) {
|
|
@@ -140,17 +177,17 @@ function analyze(fn) {
|
|
|
140
177
|
}
|
|
141
178
|
if (name === nextName) {
|
|
142
179
|
// calling next is how a chain advances, and past its end or with an error the
|
|
143
|
-
// request lands in the framework's own final
|
|
180
|
+
// request lands in the framework's own final answer, which the constructor's
|
|
144
181
|
// accept pre-read covers. Anything but a direct call aliases the continuation,
|
|
145
182
|
// and an argument that could be the string "route" would leave the chain for
|
|
146
183
|
// routes nobody analyzed, so only shapes that cannot be a string pass.
|
|
147
184
|
if (!parent || parent.type !== "CallExpression" || parent.callee !== node) {
|
|
148
|
-
|
|
185
|
+
mask |= UNKNOWN;
|
|
149
186
|
return;
|
|
150
187
|
}
|
|
151
|
-
usesNext = true;
|
|
152
188
|
const args = parent.arguments;
|
|
153
189
|
if (args.length === 0) {
|
|
190
|
+
mask |= NEXT_PLAIN;
|
|
154
191
|
return;
|
|
155
192
|
}
|
|
156
193
|
const arg = args[0];
|
|
@@ -160,20 +197,22 @@ function analyze(fn) {
|
|
|
160
197
|
arg.type !== "ObjectExpression" &&
|
|
161
198
|
!(arg.type === "Literal" && typeof arg.value !== "string"))
|
|
162
199
|
) {
|
|
163
|
-
|
|
200
|
+
mask |= UNKNOWN;
|
|
201
|
+
return;
|
|
164
202
|
}
|
|
203
|
+
mask |= NEXT_ERROR;
|
|
165
204
|
return;
|
|
166
205
|
}
|
|
167
206
|
if (!parent || parent.type !== "MemberExpression" || parent.object !== node || parent.computed) {
|
|
168
|
-
|
|
207
|
+
mask |= UNKNOWN;
|
|
169
208
|
return;
|
|
170
209
|
}
|
|
171
210
|
const member = parent.property.name;
|
|
172
211
|
if (name === reqName ? !REQ_OK.has(member) : !RES_OK.has(member)) {
|
|
173
|
-
|
|
212
|
+
mask |= UNKNOWN;
|
|
174
213
|
}
|
|
175
214
|
});
|
|
176
|
-
return
|
|
215
|
+
return mask;
|
|
177
216
|
}
|
|
178
217
|
|
|
179
218
|
/**
|
|
@@ -207,41 +246,47 @@ function walk(node, parent, visit) {
|
|
|
207
246
|
}
|
|
208
247
|
|
|
209
248
|
/**
|
|
210
|
-
*
|
|
211
|
-
*
|
|
249
|
+
* What a native route's whole chain provably never does, so the request constructor may leave
|
|
250
|
+
* that work undone: skipHeaders spares the header copy, skipQuery the query fetch.
|
|
212
251
|
*
|
|
213
|
-
* A callback that calls next passes anywhere but in the terminal route, where
|
|
214
|
-
* out of the chain: there it only passes when the caller established that no later route
|
|
215
|
-
* could catch the fall-through.
|
|
252
|
+
* A callback that calls next() bare passes anywhere but in the terminal route, where it would
|
|
253
|
+
* fall out of the chain: there it only passes when the caller established that no later route
|
|
254
|
+
* could catch the fall-through. The framework's own 404 answers with the path alone, so the
|
|
255
|
+
* fall-through itself needs neither headers nor query.
|
|
216
256
|
*
|
|
217
257
|
* @param {any[]} chain the routes the native handler runs, in order, this route last
|
|
218
258
|
* @param {boolean} allowTerminalNext whether a fall-through past the chain lands only in the
|
|
219
|
-
* framework's own final
|
|
220
|
-
* @returns {boolean}
|
|
259
|
+
* framework's own final answer
|
|
260
|
+
* @returns {{skipHeaders: boolean, skipQuery: boolean}}
|
|
221
261
|
*/
|
|
222
|
-
function
|
|
262
|
+
function chainUsage(chain, allowTerminalNext) {
|
|
263
|
+
const none = { skipHeaders: false, skipQuery: false };
|
|
264
|
+
let query = false;
|
|
223
265
|
for (let i = 0; i < chain.length; i++) {
|
|
224
266
|
const entry = chain[i];
|
|
225
267
|
const callbacks = entry.callbacks;
|
|
226
268
|
if (!Array.isArray(callbacks)) {
|
|
227
|
-
return
|
|
269
|
+
return none;
|
|
228
270
|
}
|
|
229
271
|
const terminal = i === chain.length - 1;
|
|
230
272
|
for (const cb of callbacks) {
|
|
231
273
|
if (typeof cb !== "function") {
|
|
232
|
-
return
|
|
274
|
+
return none;
|
|
275
|
+
}
|
|
276
|
+
const mask = callbackUsage(cb);
|
|
277
|
+
if (mask & UNKNOWN || (mask & NEXT_PLAIN && terminal && !allowTerminalNext)) {
|
|
278
|
+
return none;
|
|
233
279
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
return false;
|
|
280
|
+
if (mask & QUERY) {
|
|
281
|
+
query = true;
|
|
237
282
|
}
|
|
238
283
|
}
|
|
239
284
|
// a param callback runs code this walk never saw
|
|
240
285
|
if (entry.paramCallbacks && entry.paramCallbacks.size > 0) {
|
|
241
|
-
return
|
|
286
|
+
return none;
|
|
242
287
|
}
|
|
243
288
|
}
|
|
244
|
-
return true;
|
|
289
|
+
return { skipHeaders: true, skipQuery: !query };
|
|
245
290
|
}
|
|
246
291
|
|
|
247
|
-
module.exports = {
|
|
292
|
+
module.exports = { chainUsage, callbackUsage, kGetSafe, UNKNOWN, NEXT_PLAIN, NEXT_ERROR, QUERY };
|