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/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 µWS itself only while it stays eligible, and eligibility is not a property
20
- // of the route alone: a `const` in the wrong place, a middleware that reads a header, a new route
21
- // written above an old one, and it quietly falls back to the ordinary router. The answer is still
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 to read. This is the half a test can
26
- // hold on to, so a pull request that loses the fast path fails in CI with the reason written out
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 {any} router
40
+ * @param {Router} router
38
41
  * @param {string} prefix
39
- * @param {any[]} [into]
40
- * @returns {{route: any, full: string}[]}
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 {any} app
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
- * This is the primitive the two assertions below are written on, and it is exported because an
72
- * application with rules of its own is better served asserting them itself: how many routes may
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 {any} app an application, listening or not
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 it was registered, not a URL: "/api/items/:id" and not "/api/items/7". It
98
- * may carry the method, "GET /health", and it may end in "*" to name everything under a prefix.
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, refusing a pattern that names none: a test that asserts about a
122
- * route it misspelled has to fail rather than pass on an empty list.
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 {any} app
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 µWS itself.
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 {any} app
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 µWS already matches is still not compiled into a response.
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 for one of those sends the reader to rewrite something that was already simple enough.
180
+ * handler sends the reader to rewrite something that was already simple enough.
181
181
  *
182
- * @param {any} app
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, which is the
204
- * step past native: µWS answers it without entering javascript at all.
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 {any} app
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 {any} req
232
- * @param {any} res
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, which is what expectLazy is about. The route verdict is
240
- // not in here: expectNative and expectDeclarative are what assert on that.
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 is a property of the application and holds for every request; this is the
247
- * other half, which holds for one. A route can stay native and still slow down request by request,
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, which is most of the point: a route that parses a body is
252
- * asserted as one that parses a body and nothing else.
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 {any} req
255
- * @param {any} res
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
- /** @type {any} */ (unwanted)[field] = false;
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
- // reads never reach a header. Anything else, computed access included, keeps the copy.
28
- // req.res and req.app are deliberately absent: the walk judges only the member directly on
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
- * shape this walk does not understand and any alias of req, res or next could do anything.
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 {any} */ (tree.body[0]);
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
- const params = /** @type {any[]} */ (root.params);
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 in the whole body is judged, nested functions
139
- // included: an inner binding that shadows one of them only makes this stricter, never
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 or with an error the
196
- // request lands in the framework's own final answer, which the constructor's
197
- // accept pre-read covers. Anything but a direct call aliases the continuation,
198
- // and an argument that could be the string "route" would leave the chain for
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
- * @param {any} parent
240
- * @param {(node: any, parent: any) => void} visit
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 callback that calls next() bare passes anywhere but in the terminal route, where it would
269
- * fall out of the chain: there it only passes when the caller established that no later route
270
- * could catch the fall-through. The framework's own 404 answers with the path alone, so the
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 {any[]} chain the routes the native handler runs, in order, this route last
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}}