fulmine.js 5.19.2 → 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.
- package/package.json +2 -1
- package/src/adopt.js +20 -26
- package/src/application.js +63 -73
- package/src/cli.js +48 -48
- package/src/cluster.js +18 -27
- package/src/compression.js +141 -112
- package/src/declarative.js +611 -540
- package/src/hot-settings.js +80 -0
- package/src/index.js +11 -17
- package/src/lazy-readable.js +131 -0
- package/src/lazy-writable.js +97 -0
- package/src/middlewares.js +153 -130
- package/src/nest.js +22 -36
- package/src/node-shim.js +19 -16
- package/src/optimizer.js +600 -0
- package/src/options.d.ts +9 -4
- package/src/parse-query.js +3 -3
- package/src/request-utils.js +307 -0
- package/src/request.js +147 -548
- package/src/response-utils.js +88 -0
- package/src/response.js +228 -535
- package/src/route.js +7 -8
- package/src/router-utils.js +998 -0
- package/src/router.js +166 -2170
- package/src/server-shape.js +40 -51
- package/src/server-timing.js +32 -33
- package/src/socket.js +208 -0
- package/src/testing.js +43 -45
- package/src/usage.js +25 -25
- package/src/utils.js +165 -78
- package/src/verify.js +22 -31
- package/src/view.js +6 -8
- package/src/walk.js +581 -0
- package/src/websocket.js +34 -26
- package/src/work.js +22 -28
package/src/testing.js
CHANGED
|
@@ -16,28 +16,31 @@ limitations under the License.
|
|
|
16
16
|
|
|
17
17
|
// express.testing: what listen() decided about each route, as something a test can assert on.
|
|
18
18
|
//
|
|
19
|
-
// A route is answered by
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// correct, which is why nothing complains. What changes is the throughput, and by the time anyone
|
|
23
|
-
// notices, the commit that did it is three weeks back.
|
|
19
|
+
// A route is answered by uWS itself only while it stays eligible: a `const` in the wrong place, a
|
|
20
|
+
// middleware that reads a header, a new route above an old one, and it falls back to the ordinary
|
|
21
|
+
// router. The answer stays correct, so nothing complains, only the throughput changes.
|
|
24
22
|
//
|
|
25
|
-
// `npx fulmine profile` prints the same verdicts for a human
|
|
26
|
-
//
|
|
27
|
-
// instead of being found in production.
|
|
23
|
+
// `npx fulmine profile` prints the same verdicts for a human. This is the half a test can hold on
|
|
24
|
+
// to, so a pull request that loses the fast path fails in CI.
|
|
28
25
|
|
|
29
26
|
"use strict";
|
|
30
27
|
|
|
31
28
|
const { work, names: workNames } = require("./work.js");
|
|
32
29
|
|
|
30
|
+
/** @typedef {import("./request.js")} Request */
|
|
31
|
+
/** @typedef {import("./response.js")} Response */
|
|
32
|
+
/** @typedef {import("./router.js")} Router */
|
|
33
|
+
/** @typedef {import("./application.js").Application} Application */
|
|
34
|
+
/** @typedef {import("./router-utils.js").RouteEntry} RouteEntry */
|
|
35
|
+
|
|
33
36
|
/**
|
|
34
37
|
* Every route of an application and of the routers mounted under it, each with the path it answers
|
|
35
38
|
* from the outside.
|
|
36
39
|
*
|
|
37
|
-
* @param {
|
|
40
|
+
* @param {Router} router
|
|
38
41
|
* @param {string} prefix
|
|
39
|
-
* @param {
|
|
40
|
-
* @returns {{route:
|
|
42
|
+
* @param {{route: RouteEntry, full: string}[]} [into]
|
|
43
|
+
* @returns {{route: RouteEntry, full: string}[]}
|
|
41
44
|
*/
|
|
42
45
|
function collectRoutes(router, prefix, into = []) {
|
|
43
46
|
for (const route of router._routes ?? []) {
|
|
@@ -55,7 +58,7 @@ function collectRoutes(router, prefix, into = []) {
|
|
|
55
58
|
* Compiles the routes, which is what listen() does before it binds, without binding anything. Once
|
|
56
59
|
* per application: a second compilation would register everything with µWS twice.
|
|
57
60
|
*
|
|
58
|
-
* @param {
|
|
61
|
+
* @param {Application} app
|
|
59
62
|
*/
|
|
60
63
|
function compileOnce(app) {
|
|
61
64
|
if (app.listenCalled || app._testingCompiled) {
|
|
@@ -68,11 +71,10 @@ function compileOnce(app) {
|
|
|
68
71
|
/**
|
|
69
72
|
* What compiling the routes decided, one entry per route, in the order they were registered.
|
|
70
73
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* fall back, which ones may read headers, that the one route carrying the traffic is declarative.
|
|
74
|
+
* The primitive the two assertions below are written on. Exported so an application with rules of
|
|
75
|
+
* its own can assert them directly.
|
|
74
76
|
*
|
|
75
|
-
* @param {
|
|
77
|
+
* @param {Application} app an application, listening or not
|
|
76
78
|
* @returns {{method: string, path: string, native: boolean, declarative: boolean, skipHeaders: boolean,
|
|
77
79
|
* skipQuery: boolean, reason: string|undefined}[]}
|
|
78
80
|
*/
|
|
@@ -94,8 +96,8 @@ function routeReport(app) {
|
|
|
94
96
|
/**
|
|
95
97
|
* Whether one of the patterns given names this route.
|
|
96
98
|
*
|
|
97
|
-
* A pattern is a path as
|
|
98
|
-
*
|
|
99
|
+
* A pattern is a path as registered, not a URL: "/api/items/:id", not "/api/items/7". It may carry
|
|
100
|
+
* the method, "GET /health", and may end in "*" for everything under a prefix.
|
|
99
101
|
*
|
|
100
102
|
* @param {{method: string, path: string}} entry
|
|
101
103
|
* @param {string} pattern
|
|
@@ -118,10 +120,10 @@ function names(entry, pattern) {
|
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
/**
|
|
121
|
-
* The routes the patterns name
|
|
122
|
-
*
|
|
123
|
+
* The routes the patterns name. A pattern that names none throws, so a misspelled route fails
|
|
124
|
+
* instead of passing on an empty list.
|
|
123
125
|
*
|
|
124
|
-
* @param {
|
|
126
|
+
* @param {Application} app
|
|
125
127
|
* @param {string|string[]} patterns
|
|
126
128
|
* @param {string} caller the name in the message
|
|
127
129
|
* @returns {ReturnType<typeof routeReport>}
|
|
@@ -152,12 +154,10 @@ function select(app, patterns, caller) {
|
|
|
152
154
|
}
|
|
153
155
|
|
|
154
156
|
/**
|
|
155
|
-
* Throws unless every route named is answered by
|
|
156
|
-
*
|
|
157
|
-
* The message is the point: it names each route that fell back and why, in the same words
|
|
158
|
-
* `npx fulmine profile` uses, so the failure says what to change.
|
|
157
|
+
* Throws unless every route named is answered by uWS itself. The message names each route that
|
|
158
|
+
* fell back and why, in the same words `npx fulmine profile` uses.
|
|
159
159
|
*
|
|
160
|
-
* @param {
|
|
160
|
+
* @param {Application} app
|
|
161
161
|
* @param {string|string[]} patterns paths as they were registered, "GET /path" to pin the method,
|
|
162
162
|
* a trailing "*" for everything under a prefix
|
|
163
163
|
*/
|
|
@@ -174,12 +174,12 @@ function expectNative(app, patterns) {
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
/**
|
|
177
|
-
* Why a route
|
|
177
|
+
* Why a route uWS already matches is still not compiled into a response.
|
|
178
178
|
*
|
|
179
179
|
* The handler is the last answer, not the first: three refusals come before it, and blaming the
|
|
180
|
-
* handler
|
|
180
|
+
* handler sends the reader to rewrite something that was already simple enough.
|
|
181
181
|
*
|
|
182
|
-
* @param {
|
|
182
|
+
* @param {Application} app
|
|
183
183
|
* @param {{path: string}} entry
|
|
184
184
|
* @returns {string}
|
|
185
185
|
*/
|
|
@@ -200,10 +200,10 @@ function whyNotCompiled(app, entry) {
|
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
/**
|
|
203
|
-
* Throws unless every route named is answered from a response written at startup
|
|
204
|
-
*
|
|
203
|
+
* Throws unless every route named is answered from a response written at startup. One step past
|
|
204
|
+
* native: uWS answers it without entering javascript.
|
|
205
205
|
*
|
|
206
|
-
* @param {
|
|
206
|
+
* @param {Application} app
|
|
207
207
|
* @param {string|string[]} patterns as in expectNative
|
|
208
208
|
*/
|
|
209
209
|
function expectDeclarative(app, patterns) {
|
|
@@ -228,31 +228,29 @@ function expectDeclarative(app, patterns) {
|
|
|
228
228
|
* What this one request made the framework do, asked from inside a handler or from a `finish`
|
|
229
229
|
* listener. See src/work.js for what each field means and why asking is free.
|
|
230
230
|
*
|
|
231
|
-
* @param {
|
|
232
|
-
* @param {
|
|
231
|
+
* @param {Request} req
|
|
232
|
+
* @param {Response} res
|
|
233
233
|
* @returns {import("./work.js").Work}
|
|
234
234
|
*/
|
|
235
235
|
function workReport(req, res) {
|
|
236
236
|
return work(req, res);
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
-
// The work a fast request does none of
|
|
240
|
-
//
|
|
239
|
+
// The work a fast request does none of. The route verdict is not here, expectNative and
|
|
240
|
+
// expectDeclarative assert on that.
|
|
241
241
|
const LAZY = ["headers", "query", "body", "requestStream", "responseStream", "socket"];
|
|
242
242
|
|
|
243
243
|
/**
|
|
244
244
|
* Throws if this request built anything it did not have to.
|
|
245
245
|
*
|
|
246
|
-
* The route verdict
|
|
247
|
-
*
|
|
248
|
-
* because a middleware read `req.headers.host` or piped instead of sending: the answer stays
|
|
249
|
-
* correct, the route report stays green, and the throughput does not.
|
|
246
|
+
* The route verdict holds for every request, this holds for one. A native route still slows down
|
|
247
|
+
* request by request if a middleware reads `req.headers.host` or pipes instead of sending.
|
|
250
248
|
*
|
|
251
|
-
* `allow` names what is fine here
|
|
252
|
-
*
|
|
249
|
+
* `allow` names what is fine here: a route that parses a body is asserted as one that parses a
|
|
250
|
+
* body and nothing else.
|
|
253
251
|
*
|
|
254
|
-
* @param {
|
|
255
|
-
* @param {
|
|
252
|
+
* @param {Request} req
|
|
253
|
+
* @param {Response} res
|
|
256
254
|
* @param {object} [options]
|
|
257
255
|
* @param {string[]} [options.allow] fields of the report this route is expected to do anyway
|
|
258
256
|
*/
|
|
@@ -266,7 +264,7 @@ function expectLazy(req, res, options) {
|
|
|
266
264
|
const done = work(req, res);
|
|
267
265
|
const unwanted = { ...done };
|
|
268
266
|
for (const field of allowed) {
|
|
269
|
-
|
|
267
|
+
unwanted[field] = false;
|
|
270
268
|
}
|
|
271
269
|
const listed = workNames(unwanted);
|
|
272
270
|
if (listed.length === 0) {
|
package/src/usage.js
CHANGED
|
@@ -18,15 +18,16 @@ 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).
|
|
24
26
|
const kGetSafe = Symbol("fulmine.getSafe");
|
|
25
27
|
|
|
26
|
-
// What a handler may do with `req` and still let the header copy be skipped: members whose
|
|
27
|
-
//
|
|
28
|
-
// req.
|
|
29
|
-
// the parameter, so anything that can reach another object could reach headers through it.
|
|
28
|
+
// What a handler may do with `req` and still let the header copy be skipped: members whose reads
|
|
29
|
+
// never reach a header. Anything else, computed access included, keeps the copy. req.res and
|
|
30
|
+
// req.app are absent because they could reach headers through another object.
|
|
30
31
|
const REQ_OK = new Set(["query", "params", "body", "method", "path", "url", "baseUrl", "originalUrl", "route"]);
|
|
31
32
|
|
|
32
33
|
// Reading any of these needs the query string fetched: req.url and req.originalUrl carry it
|
|
@@ -70,9 +71,8 @@ const QUERY = 8; // reads req.query, req.url or req.originalUrl
|
|
|
70
71
|
const verdicts = new WeakMap();
|
|
71
72
|
|
|
72
73
|
/**
|
|
73
|
-
* What one callback provably does, as a mask of the facts above. The default is UNKNOWN: any
|
|
74
|
-
*
|
|
75
|
-
* That inversion is what makes source analysis sound to act on.
|
|
74
|
+
* What one callback provably does, as a mask of the facts above. The default is UNKNOWN: any shape
|
|
75
|
+
* this walk does not understand, and any alias of req, res or next, could do anything.
|
|
76
76
|
*
|
|
77
77
|
* @param {Function} fn
|
|
78
78
|
* @returns {number}
|
|
@@ -109,7 +109,7 @@ function analyze(fn) {
|
|
|
109
109
|
// class methods and native functions do not parse alone, and unread code is unknown code
|
|
110
110
|
return UNKNOWN;
|
|
111
111
|
}
|
|
112
|
-
let root = /** @type {
|
|
112
|
+
let root = /** @type {import("acorn").AnyNode} */ (tree.body[0]);
|
|
113
113
|
if (!root) {
|
|
114
114
|
return UNKNOWN;
|
|
115
115
|
}
|
|
@@ -124,7 +124,8 @@ function analyze(fn) {
|
|
|
124
124
|
return UNKNOWN;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
// checked by the loop below, which answers UNKNOWN for anything else
|
|
128
|
+
const params = /** @type {import("acorn").Identifier[]} */ (root.params);
|
|
128
129
|
// rest or destructured parameters alias the objects somewhere the walk cannot follow
|
|
129
130
|
for (const p of params) {
|
|
130
131
|
if (p.type !== "Identifier") {
|
|
@@ -135,9 +136,8 @@ function analyze(fn) {
|
|
|
135
136
|
const resName = params[1] ? params[1].name : null;
|
|
136
137
|
const nextName = params[2] ? params[2].name : null;
|
|
137
138
|
|
|
138
|
-
// Every appearance of the three names
|
|
139
|
-
//
|
|
140
|
-
// looser, so scope tracking is not needed for soundness.
|
|
139
|
+
// Every appearance of the three names is judged, nested functions included. An inner binding
|
|
140
|
+
// that shadows one only makes this stricter, so no scope tracking is needed.
|
|
141
141
|
let mask = 0;
|
|
142
142
|
walk(root.body, null, (node, parent) => {
|
|
143
143
|
if (mask & UNKNOWN) {
|
|
@@ -192,11 +192,10 @@ function analyze(fn) {
|
|
|
192
192
|
return;
|
|
193
193
|
}
|
|
194
194
|
if (name === nextName) {
|
|
195
|
-
// calling next is how a chain advances, and past its end
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
// routes nobody analyzed, so only shapes that cannot be a string pass.
|
|
195
|
+
// calling next is how a chain advances, and past its end the request lands in the
|
|
196
|
+
// framework's own final answer, which the constructor's accept pre-read covers.
|
|
197
|
+
// Anything but a direct call aliases the continuation, and an argument that could be
|
|
198
|
+
// the string "route" would leave the chain, so only non-string shapes pass.
|
|
200
199
|
if (!parent || parent.type !== "CallExpression" || parent.callee !== node) {
|
|
201
200
|
mask |= UNKNOWN;
|
|
202
201
|
return;
|
|
@@ -235,9 +234,11 @@ function analyze(fn) {
|
|
|
235
234
|
* Walks every node, handing each its parent. Arrays and nested objects are entered, nothing
|
|
236
235
|
* is interpreted: the judging happens in the visitor.
|
|
237
236
|
*
|
|
238
|
-
* @param {any} node
|
|
239
|
-
*
|
|
240
|
-
* @param {
|
|
237
|
+
* @param {any} node an acorn node, or an array or a scalar under one: walked by key, so no shape
|
|
238
|
+
* is assumed
|
|
239
|
+
* @param {any} parent its parent node, or null at the root
|
|
240
|
+
* @param {(node: any, parent: any) => void} visit handed every node, loose because the visitor
|
|
241
|
+
* reads edges of its own off each
|
|
241
242
|
*/
|
|
242
243
|
function walk(node, parent, visit) {
|
|
243
244
|
if (!node || typeof node.type !== "string") {
|
|
@@ -265,12 +266,11 @@ function walk(node, parent, visit) {
|
|
|
265
266
|
* What a native route's whole chain provably never does, so the request constructor may leave
|
|
266
267
|
* that work undone: skipHeaders spares the header copy, skipQuery the query fetch.
|
|
267
268
|
*
|
|
268
|
-
* A
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
* fall-through itself needs neither headers nor query.
|
|
269
|
+
* A bare next() passes anywhere but in the terminal route, where it falls out of the chain: there
|
|
270
|
+
* it passes only when no later route could catch the fall-through. The framework's own 404 answers
|
|
271
|
+
* from the path alone.
|
|
272
272
|
*
|
|
273
|
-
* @param {
|
|
273
|
+
* @param {RouteEntry[]} chain the routes the native handler runs, in order, this route last
|
|
274
274
|
* @param {boolean} allowTerminalNext whether a fall-through past the chain lands only in the
|
|
275
275
|
* framework's own final answer
|
|
276
276
|
* @returns {{skipHeaders: boolean, skipQuery: boolean}}
|