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/websocket.js CHANGED
@@ -18,10 +18,19 @@ limitations under the License.
18
18
 
19
19
  const { canBeOptimizedWithParams, decodeParam, NullObject } = require("./utils.js");
20
20
 
21
+ /** @typedef {import("./router.js")} Router */
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 */
29
+
21
30
  // the parameter names in a path, in the order µWS numbers them
22
31
  const PARAM = /:(\w+)/g;
23
32
 
24
- // Handlers µWS calls with the socket. Everything else in a behavior object is a µWS setting
33
+ // Handlers uWS calls with the socket. Everything else in a behavior object is a uWS setting
25
34
  // (maxPayloadLength, idleTimeout, compression, ...) and rides through untouched.
26
35
  const SOCKET_HANDLERS = ["open", "message", "dropped", "drain", "close", "ping", "pong", "subscription"];
27
36
 
@@ -46,15 +55,14 @@ function joinPaths(prefix, path) {
46
55
  /**
47
56
  * Every websocket route reachable from this router, with the mount paths already applied.
48
57
  *
49
- * Walked separately from the HTTP routes: those fall back to ordinary routing when µWS cannot
50
- * match them, and a websocket has no fallback to fall back to, so an unmountable one has to be
51
- * refused out loud instead.
58
+ * Walked separately from the HTTP routes: those fall back to ordinary routing when uWS cannot
59
+ * match them, a websocket has no fallback, so an unmountable one is refused out loud.
52
60
  *
53
- * @param {any} router
61
+ * @param {Router} router
54
62
  * @param {string|null} prefix the mount path accumulated so far, or null once a mount was a
55
63
  * shape µWS cannot match, which makes everything below it unreachable
56
- * @param {any[]} out
57
- * @param {Set<any>} seen routers already walked, since a router may be mounted twice
64
+ * @param {WsRoute[]} out
65
+ * @param {Set<Router>} seen routers already walked, since a router may be mounted twice
58
66
  */
59
67
  function collectRoutes(router, prefix, out, seen) {
60
68
  if (seen.has(router)) {
@@ -97,21 +105,23 @@ function collectRoutes(router, prefix, out, seen) {
97
105
  }
98
106
 
99
107
  /**
100
- * The µWS upgrade handler for one route: it builds this project's request and response, offers
101
- * them to the application's own `upgrade` hook, and completes the handshake unless that hook
102
- * answered the request itself.
108
+ * The uWS upgrade handler for one route: builds this project's request and response, offers them
109
+ * to the application's own `upgrade` hook, and completes the handshake unless that hook answered.
103
110
  *
104
- * @param {any} app the application whose request and response classes serve this route
111
+ * @param {Router} app the router whose request and response classes serve this route
105
112
  * @param {string} path the composed path, whose parameters are read back by index
106
- * @param {any} behavior what the caller registered
107
- * @returns {(res: any, req: any, context: any) => void}
113
+ * @param {Record<string, unknown>} behavior what the caller registered
114
+ * @returns {(res: UwsResponse, req: UwsRequest, context: UwsContext) => void}
108
115
  */
109
116
  function makeUpgradeHandler(app, path, behavior) {
110
117
  const paramNames = [...path.matchAll(PARAM)].map((match) => match[1]);
111
- const userUpgrade = behavior.upgrade;
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
+ );
112
122
 
113
123
  return (res, req, context) => {
114
- // read off the µWS request before anything can await: it is neutered on return, and the
124
+ // read off the uWS request before anything can await: it is neutered on return, and the
115
125
  // handshake needs these three even when the upgrade is decided asynchronously
116
126
  const key = req.getHeader("sec-websocket-key");
117
127
  const protocol = req.getHeader("sec-websocket-protocol");
@@ -121,7 +131,7 @@ function makeUpgradeHandler(app, path, behavior) {
121
131
  if (paramNames.length) {
122
132
  const params = new NullObject();
123
133
  for (let i = 0; i < paramNames.length; i++) {
124
- params[paramNames[i]] = decodeParam(req.getParameter(i));
134
+ params[paramNames[i]] = decodeParam(/** @type {string} */ (req.getParameter(i)));
125
135
  }
126
136
  request.params = params;
127
137
  }
@@ -169,17 +179,16 @@ function makeUpgradeHandler(app, path, behavior) {
169
179
  return;
170
180
  }
171
181
 
172
- // an async hook (a session lookup, a token check) outlives this callback, so µWS has to
173
- // be told who to call if the client leaves first. Registered now, still inside the
174
- // handler, which is the only place µWS accepts it
182
+ // an async hook outlives this callback, so uWS has to be told who to call if the client
183
+ // leaves first. Registered now, inside the handler, the only place uWS accepts it
175
184
  res.onAborted(() => {
176
185
  aborted = true;
177
186
  // and on the response too, so a hook that is still awaiting can see the client left
178
187
  // rather than working on towards a handshake nobody is waiting for
179
188
  response.aborted = true;
180
189
  });
181
- // and whatever the hook writes now lands outside the cork µWS holds for this callback,
182
- // so the response opens its own, exactly as a route handler answering late does
190
+ // whatever the hook writes now lands outside the cork uWS holds for this callback, so the
191
+ // response opens its own, exactly as a route handler answering late does
183
192
  response._corkNeeded = true;
184
193
  decision.then(accept, (err) => {
185
194
  if (!aborted && !response.finished) {
@@ -193,11 +202,10 @@ function makeUpgradeHandler(app, path, behavior) {
193
202
  }
194
203
 
195
204
  /**
196
- * Hands every websocket route this application can reach to µWS. Called from listen(), before
197
- * the catch-all goes on: µWS routes an upgrade to the websocket route even when a catch-all
198
- * covers the same path, so the two live side by side.
205
+ * Hands every websocket route to uWS. Called from listen(), before the catch-all: uWS routes an
206
+ * upgrade to the websocket route even when a catch-all covers the same path.
199
207
  *
200
- * @param {any} app
208
+ * @param {Application} app
201
209
  */
202
210
  function registerWebSocketRoutes(app) {
203
211
  const routes = [];
@@ -217,7 +225,7 @@ function registerWebSocketRoutes(app) {
217
225
  * used: a handler under a misspelled name would otherwise never run and never say why.
218
226
  *
219
227
  * @param {string} path
220
- * @param {any} behavior
228
+ * @param {unknown} behavior whatever was passed as one, which is what is being checked
221
229
  */
222
230
  function checkBehavior(path, behavior) {
223
231
  if (typeof path !== "string") {
package/src/work.js CHANGED
@@ -16,27 +16,21 @@ limitations under the License.
16
16
 
17
17
  // What one request actually made this framework do, read from state it already keeps.
18
18
  //
19
- // Most of what makes this faster than Express is work that does not happen: the Readable and the
20
- // Writable are not built, the headers are not folded into an object, the query is not parsed, the
21
- // socket stand-in is not allocated. None of that is visible from the outside, and all of it is one
22
- // careless middleware away from coming back: a `req.headers.host` where `req.get("host")` would do
23
- // puts the folded object back on every request, and the answer stays correct, so nothing fails.
19
+ // Most of the speed here is work that does not happen: no Readable, no Writable, no folded headers
20
+ // object, no parsed query, no socket stand-in. One careless middleware brings it back, and the
21
+ // answer stays correct, so nothing fails. Every field below is already kept for other reasons, so
22
+ // asking costs a load and nothing is counted or wrapped for the sake of being asked.
24
23
  //
25
- // Every field below is a property this framework already had to keep for its own reasons, so
26
- // asking costs a load and nothing is counted, stamped or wrapped for the sake of being asked. That
27
- // is the whole design rule here: a probe that charges the requests nobody is probing would be
28
- // paid for by everyone, forever, to be read once.
24
+ // Not here: whether the constructor copied the headers out of uWS. That is about the chain and
25
+ // `routeReport().skipHeaders` reports it already.
29
26
  //
30
- // What is deliberately not here is whether the constructor copied the headers out of µWS. That is
31
- // a decision about the chain rather than about the request, `routeReport().skipHeaders` reports it
32
- // already, and the one case where the two differ, a granted route whose request declares a body,
33
- // would cost a flag written on every request to be read on almost none.
34
- //
35
- // The two readers are `express.testing.expectLazy`, which fails a build that lost one of these,
36
- // and `express.serverTiming()`, which writes them into the header for a browser to show.
27
+ // Read by `express.testing.expectLazy` and by `express.serverTiming()`.
37
28
 
38
29
  "use strict";
39
30
 
31
+ /** @typedef {import("./request.js")} Request */
32
+ /** @typedef {import("./response.js")} Response */
33
+
40
34
  /**
41
35
  * @typedef {object} Work
42
36
  * @property {boolean} native whether µWS matched this route itself
@@ -50,29 +44,30 @@ limitations under the License.
50
44
  */
51
45
 
52
46
  /**
53
- * What this request did, as it stands right now: the answer changes while the chain runs, so a
54
- * reader that wants the whole picture asks at the end of it.
47
+ * What this request did so far. The answer changes while the chain runs, so ask at the end of it.
55
48
  *
56
- * @param {any} req
57
- * @param {any} res the response, since half of this is about the response
49
+ * @param {Request} req
50
+ * @param {Response} res the response, since half of this is about the response
58
51
  * @returns {Work}
59
52
  */
60
53
  function work(req, res) {
61
54
  const native = req.route?._native;
55
+ // cast for the three the classes do not declare: `body` is deliberately not a field of
56
+ // Request, and the two stream states are node's own, written when a lazy stream is built
57
+ const loose = /** @type {{body?: unknown, _readableState?: unknown}} */ (req);
62
58
  return {
63
59
  native: Boolean(native),
64
60
  declarative: Boolean(native?.declarative),
65
61
  headers: req._headersBuilt,
66
62
  query: req._queryParsed,
67
- body: req.body !== undefined,
68
- requestStream: req._readableState !== undefined,
69
- responseStream: res._writableState !== undefined,
63
+ body: loose.body !== undefined,
64
+ requestStream: loose._readableState !== undefined,
65
+ responseStream: /** @type {{_writableState?: unknown}} */ (res)._writableState !== undefined,
70
66
  socket: req._socketBuilt || res._socketBuilt
71
67
  };
72
68
  }
73
69
 
74
- // The order the two readers list them in: what the request was made to do, cheapest first, so a
75
- // header and a failure message read the same way.
70
+ // The order both readers list them in, cheapest first, so a header and a failure message agree.
76
71
  const NAMES = [
77
72
  ["headers", "headers"],
78
73
  ["query", "query"],
@@ -83,8 +78,7 @@ const NAMES = [
83
78
  ];
84
79
 
85
80
  /**
86
- * The names of everything that did happen, for a message or a header. Empty for the request that
87
- * did none of it, which is the one this framework is built to serve.
81
+ * The names of everything that did happen, for a message or a header. Empty is the good case.
88
82
  *
89
83
  * @param {Work} done
90
84
  * @returns {string[]}
@@ -92,7 +86,7 @@ const NAMES = [
92
86
  function names(done) {
93
87
  const listed = [];
94
88
  for (const [key, name] of NAMES) {
95
- if (/** @type {any} */ (done)[key]) {
89
+ if (done[key]) {
96
90
  listed.push(name);
97
91
  }
98
92
  }