fulmine.js 5.19.2 → 5.19.3

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/walk.js ADDED
@@ -0,0 +1,580 @@
1
+ /*
2
+ Copyright 2024 dimden.dev
3
+ Copyright 2026 Nigro Simone
4
+
5
+ This file is derived from Ultimate Express and has been modified.
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
18
+ */
19
+
20
+ const {
21
+ mountPrefixLength,
22
+ setMountedPath,
23
+ stepsOver,
24
+ rememberApp,
25
+ restoreApp,
26
+ useApp,
27
+ CALLBACK_ERROR,
28
+ CALLBACK_ROUTER
29
+ } = require("./router-utils.js");
30
+
31
+ /** @typedef {import("./request.js")} Request */
32
+ /** @typedef {import("./response.js")} Response */
33
+ /** @typedef {import("./router.js")} Router */
34
+
35
+ /**
36
+ * One walk of one router's routes, for one request.
37
+ *
38
+ * next() is made once here instead of once per hop. As a closure per hop it captured eleven
39
+ * bindings, one of them mutable, so a heap context every time a middleware handed over. The hop's
40
+ * own state is three fields on this instead.
41
+ *
42
+ * A nested router gets its own walk, so req.next belongs to whoever is running the request.
43
+ */
44
+ class Walk {
45
+ /**
46
+ * @param {Router} router
47
+ * @param {Request} req
48
+ * @param {Response} res
49
+ * @param {any[]} routes the route table being walked, see createRoute in router.js
50
+ * @param {boolean} skipCheck take the route at the index without matching it, which is how an
51
+ * already-decided chain is walked
52
+ * @param {any} skipUntil route to resume after when this chain runs out, or undefined
53
+ * @param {(value: any) => void} resolve
54
+ * @param {(err: any) => void} reject
55
+ */
56
+ constructor(router, req, res, routes, skipCheck, skipUntil, resolve, reject) {
57
+ this.router = router;
58
+ this.req = req;
59
+ this.res = res;
60
+ this.routes = routes;
61
+ this.skipCheck = skipCheck;
62
+ this.skipUntil = skipUntil;
63
+ this.resolve = resolve;
64
+ this.reject = reject;
65
+ // read only by the native pair below, which has no promise to settle once for it; the
66
+ // promise path leaves it false. Initialized here to keep every walk the same shape
67
+ this.settled = false;
68
+ this.routeIndex = 0;
69
+ this.route = null;
70
+ this.callbackIndex = 0;
71
+ // bound, not wrapped in an arrow: an arrow forwarding into step() is one more call on every
72
+ // hop, and it measured 495 microseconds per thousand requests of nothing else
73
+ this.next = this.step.bind(this);
74
+ // What res.sendFile reports a failure to. Express hands it req.next, the router next and
75
+ // not the route one, so a file that cannot be served leaves the route and its error reaches
76
+ // the router error handlers. req.next itself is left alone: making it mean this everywhere
77
+ // breaks express own res.format and app.routes.error tests here.
78
+ //
79
+ // Null here and bound on the first route with more than one callback, the only shape that
80
+ // reads it: a request that never meets one paid a bind for nothing
81
+ this.leaveRoute = null;
82
+ }
83
+
84
+ /**
85
+ * Leaves the rest of this route, with the error if there is one, and carries on with the route
86
+ * after it.
87
+ *
88
+ * @param {any} [err] whatever was thrown, which need not be an Error
89
+ */
90
+ stepOutOfRoute(err) {
91
+ if (err) {
92
+ const req = this.req;
93
+ req._error = err;
94
+ req._errorKey = this.route.routeKey;
95
+ req._errorGroup = this.route.group;
96
+ }
97
+ this.step("route");
98
+ }
99
+
100
+ /**
101
+ * Finds the next route that matches and runs it. next() comes back here for the route after, so
102
+ * a chain of N middlewares costs one promise instead of N nested ones.
103
+ *
104
+ * @param {number} startIndex where to resume the scan
105
+ */
106
+ dispatch(startIndex) {
107
+ const req = this.req;
108
+ const routes = this.routes;
109
+ const router = this.router;
110
+ // a middleware assigned req.url, which express honours: the rest of the walk matches the
111
+ // new path. One identity compare per hop, since the router writes both sides itself; the
112
+ // handling lives out of line so this function stays small enough to inline
113
+ if (req.url !== req._lastUrl && this.takeUrlRewrite(startIndex)) {
114
+ return;
115
+ }
116
+ // and the same for req.method, which method-override assigns: the compiled chain was
117
+ // picked by the verb the request arrived with, so it no longer stands for this one
118
+ if (req.method !== req._lastMethod && this.takeMethodRewrite(startIndex)) {
119
+ return;
120
+ }
121
+ let routeIndex = startIndex;
122
+ // a compiled chain runs what is in it without matching again, so this is where a layer that
123
+ // provably has nothing to do for this request is stepped over rather than entered
124
+ if (this.skipCheck) {
125
+ while (routeIndex < routes.length && routes[routeIndex].bodyParserOnly === true) {
126
+ if (!stepsOver(routes[routeIndex], req)) {
127
+ break;
128
+ }
129
+ routeIndex++;
130
+ }
131
+ }
132
+ if (!this.skipCheck) {
133
+ // express matches a layer's path before the method and decodes the parameters there, so
134
+ // a malformed escape answers 400 even when no route of this method exists. Only a path
135
+ // with a percent can produce one. Scanned once per rewrite and kept on the request
136
+ const mayFailDecode = (req._mayFailDecode ??= req._originalPath.indexOf("%") !== -1);
137
+ // frozen here, once per scan: _pathMatches reads the two flags as bare fields, and
138
+ // calling this per route measured 0.45us of a scan of four hundred
139
+ router._freezeRoutingFlags();
140
+ if (routes === router._routes) {
141
+ // the router's own table has an index over its literal routes, so the scan visits
142
+ // the handful that could match instead of every one, see _scanFrom
143
+ routeIndex = router._scanFrom(req, routeIndex, mayFailDecode);
144
+ } else {
145
+ // a compiled chain's own array, always short: the linear scan stays.
146
+ // Written out rather than through a predicate handed to findIndexStartingFrom,
147
+ // which was one closure per hop of every request not on a compiled chain
148
+ const method = req.method;
149
+ const length = routes.length;
150
+ for (; routeIndex < length; routeIndex++) {
151
+ const r = routes[routeIndex];
152
+ // A HEAD request enters a route whose path matched even when its verb cannot
153
+ // serve one: express exempts HEAD from the method check ("if (!hasMethod &&
154
+ // method !== 'HEAD')" in router/index.js), so the layer's parameters are
155
+ // captured and its param() callbacks run first. Only asked when the router has
156
+ // callbacks to run. runRoute steps over it.
157
+ if (!(
158
+ r.all ||
159
+ r.method === method ||
160
+ req._isOptions ||
161
+ (req._isHead && (r.gettable || r.paramCallbacks.size > 0))
162
+ )) {
163
+ // taken only to fail: _preprocessRequest decodes again and turns it into
164
+ // the error, so the handlers of a route this request cannot run never see it
165
+ if (mayFailDecode && router._pathMatches(r, req) && router._paramsFailToDecode(r, req)) {
166
+ break;
167
+ }
168
+ continue;
169
+ }
170
+ if (router._pathMatches(r, req)) {
171
+ // matched, and then stepped over: a body parser this request gets nothing
172
+ // out of costs a hop and answers with next() at the end of it
173
+ if (r.bodyParserOnly === true && stepsOver(r, req)) {
174
+ continue;
175
+ }
176
+ break;
177
+ }
178
+ }
179
+ }
180
+ }
181
+ const route = routes[routeIndex];
182
+ if (!route) {
183
+ if (!this.skipCheck) {
184
+ // on normal unoptimized routes, if theres no match then there is no route
185
+ return this.resolve(false);
186
+ }
187
+ // the chain ran out, so ordinary routing takes over from the top and skips what has
188
+ // already run
189
+ useApp(req, router);
190
+ // a chain that went into a mount never left it, since keepMount stops the pop, so the
191
+ // path is still relative to it. /alone/skip must not be offered to the app as /skip
192
+ if (req._stack !== null && req._stack.length > 0) {
193
+ req._stack.length = 0;
194
+ req._consumed = 0;
195
+ setMountedPath(req);
196
+ }
197
+ // an error out of a mount is attributed to the mount, so error handlers declared before
198
+ // it do not catch it, as in ordinary dispatch
199
+ if (
200
+ req._error &&
201
+ this.skipUntil &&
202
+ this.skipUntil.keepMount &&
203
+ this.skipUntil.routeKey > /** @type {number} */ (req._errorKey)
204
+ ) {
205
+ req._errorKey = this.skipUntil.routeKey;
206
+ req._errorGroup = this.skipUntil.group;
207
+ }
208
+ this.routes = router._routes;
209
+ this.skipCheck = false;
210
+ return this.dispatch(0);
211
+ }
212
+
213
+ this.routeIndex = routeIndex;
214
+ this.route = route;
215
+ this.callbackIndex = 0;
216
+
217
+ // _preprocessRequest returns a promise only when param callbacks will really run, so the
218
+ // common case stays synchronous even in an app that uses app.param. A microtask every 300
219
+ // routes resets the stack, which a long chain would otherwise blow
220
+ const continueRoute = router._preprocessRequest(req, this.res, route);
221
+ if (continueRoute instanceof Promise || req.routeCount % 300 === 0) {
222
+ // .catch and not a rejection argument: a throw inside runRoute itself must reject
223
+ // the walk instead of becoming an unhandled rejection
224
+ Promise.resolve(continueRoute)
225
+ .then((resumed) => this.runRoute(resumed))
226
+ // wrapped so the native pair keeps the walk as receiver; a promise's reject
227
+ // would not have cared
228
+ .catch((err) => this.reject(err));
229
+ return;
230
+ }
231
+ return this.runRoute(continueRoute);
232
+ }
233
+
234
+ /**
235
+ * Takes over a req.url a middleware assigned. On an ordinary walk the scan simply continues
236
+ * against the new path; a compiled chain was computed for the old one, so ordinary routing
237
+ * takes over, skipping only what has already run.
238
+ *
239
+ * @param {number} startIndex where dispatch was about to resume
240
+ * @returns {boolean} whether this rerouted the walk itself
241
+ */
242
+ takeUrlRewrite(startIndex) {
243
+ const req = this.req;
244
+ const router = this.router;
245
+ req._absorbUrlRewrite();
246
+ if (!this.skipCheck) {
247
+ return false;
248
+ }
249
+ this.skipUntil = startIndex > 0 ? this.routes[startIndex - 1] : undefined;
250
+ if (req._stack !== null && req._stack.length > 0) {
251
+ req._stack.length = 0;
252
+ req._consumed = 0;
253
+ setMountedPath(req);
254
+ }
255
+ this.routes = router._routes;
256
+ this.skipCheck = false;
257
+ this.dispatch(0);
258
+ return true;
259
+ }
260
+
261
+ /**
262
+ * Takes over a req.method a middleware assigned, which method-override does. The ordinary scan
263
+ * reads req.method per route and is right from the next hop. A compiled chain was chosen by the
264
+ * method uWS dispatched on, so ordinary routing takes over from the top.
265
+ *
266
+ * @param {number} startIndex where dispatch was about to resume
267
+ * @returns {boolean} whether this rerouted the walk itself
268
+ */
269
+ takeMethodRewrite(startIndex) {
270
+ const req = this.req;
271
+ const router = this.router;
272
+ req._absorbMethodRewrite();
273
+ if (!this.skipCheck) {
274
+ return false;
275
+ }
276
+ this.skipUntil = startIndex > 0 ? this.routes[startIndex - 1] : undefined;
277
+ if (req._stack !== null && req._stack.length > 0) {
278
+ req._stack.length = 0;
279
+ req._consumed = 0;
280
+ setMountedPath(req);
281
+ }
282
+ this.routes = router._routes;
283
+ this.skipCheck = false;
284
+ this.dispatch(0);
285
+ return true;
286
+ }
287
+
288
+ /**
289
+ * Enters the route the walk is on: a mount adjusts req.url, req.path and the mount stack on the
290
+ * way in, and then the route's callbacks run one after another through next().
291
+ *
292
+ * @param {any} continueRoute what _preprocessRequest decided: true to run, "route" to skip
293
+ */
294
+ runRoute(continueRoute) {
295
+ const req = this.req;
296
+ const route = this.route;
297
+ // A compiled chain walks into a mount rather than entering it, so the rule above needs
298
+ // saying here too: everything after this marker is inside the mount, and a mount is stepped
299
+ // over while an error is in flight. Ordinary routing takes over after the mount.
300
+ if (route.keepMount === true && req._error) {
301
+ return this.dispatch(this.routes.length);
302
+ }
303
+ if (route.use) {
304
+ if (route.mountApp) {
305
+ // optimized chain: normal dispatch swaps req.app when it enters a mounted
306
+ // Application, but the compiled mount route has no callback to do it
307
+ rememberApp(this, route, req);
308
+ useApp(req, route.mountApp);
309
+ }
310
+ const taken = mountPrefixLength(route, req);
311
+ // pushed negative when this mount consumes the whole remaining path: express invents
312
+ // the "/" the routes below see, and the pop has to know, see leaveHop and issue #17
313
+ (req._stack ??= []).push(
314
+ taken !== 0 && req._consumed + taken === req._originalPath.length ? -taken : taken
315
+ );
316
+ // a use with no path consumes nothing, so everything below would work out the values
317
+ // that are already there. Only skipped without a trailing slash, where the rules about
318
+ // one cannot bite. An application is mostly pathless middleware, and this is per hop
319
+ if (taken !== 0 || req.endsWithSlash) {
320
+ req._consumed += taken;
321
+ // a mount that took a trailing slash: req.baseUrl then has to join the pieces
322
+ // rather than slice the path, which is the slower half of its getter
323
+ if (taken !== 0 && req._originalPath.charCodeAt(req._consumed - 1) === 0x2f) {
324
+ req._mountSlash = true;
325
+ }
326
+ setMountedPath(req);
327
+ }
328
+ }
329
+ req.next = this.next;
330
+ // the same step when the route has one callback, and then it has to be the same object:
331
+ // express hands res.format's handlers the next its own layer received, and its test asserts
332
+ // that identity. With more than one callback express hands over the one that leaves the route
333
+ req._leaveRoute = route.callbacks.length > 1 ? (this.leaveRoute ??= this.stepOutOfRoute.bind(this)) : this.next;
334
+ if (continueRoute === "route") {
335
+ this.step("route");
336
+ } else if (continueRoute) {
337
+ this.step(undefined);
338
+ } else {
339
+ this.resolve(true);
340
+ }
341
+ }
342
+
343
+ /**
344
+ * A hop while the request carries an error, or over an error handler it cannot run: the
345
+ * handler is invoked when the error is its to catch, everything else is skipped.
346
+ *
347
+ * @param {number} kind what the callback is, one of the CALLBACK_ constants
348
+ * @param {Function} callback
349
+ */
350
+ errorHop(kind, callback) {
351
+ const req = this.req;
352
+ const route = this.route;
353
+ // A four argument handler written inside a route only sees what that route raised: express
354
+ // skips a route layer while an error is in flight, so an error from a middleware before it,
355
+ // or out of a mount, walks past to the router's own error handlers. Middleware error
356
+ // handlers keep the ordinary rule.
357
+ const reachable = route.use
358
+ ? req._errorKey !== undefined && route.routeKey >= req._errorKey
359
+ : route.routeKey === req._errorKey || (route.group !== undefined && route.group === req._errorGroup);
360
+ if (req._error && kind === CALLBACK_ERROR && reachable) {
361
+ const out = this.router._handleError(req._error, callback, req, this.res);
362
+ if (out instanceof Promise) {
363
+ // an error handler's rejected promise moves on to the next error handler, and
364
+ // a bare rejection gets the error express invents for it
365
+ out.catch((err) => {
366
+ req._error = err || new Error("Rejected promise");
367
+ req._errorKey = route.routeKey;
368
+ req._errorGroup = route.group;
369
+ return this.step(undefined);
370
+ });
371
+ }
372
+ return;
373
+ }
374
+ return this.step(undefined);
375
+ }
376
+
377
+ /**
378
+ * Leaves the route the walk is on: the mount pop, the router hand-back, and the hop to the
379
+ * route after. Out of step so the commonest hop, callbacks exhausted by a plain next(), goes
380
+ * here without re-running step's prologue and compares.
381
+ *
382
+ * @param {boolean} isRouter next("router") rather than next("route")
383
+ */
384
+ leaveHop(isRouter) {
385
+ const req = this.req;
386
+ const route = this.route;
387
+ if (route.use && !route.keepMount) {
388
+ const pushed = req._stack.pop();
389
+ const taken = pushed < 0 ? -pushed : pushed;
390
+ // a rewrite done inside this middleware is taken now: the pop below recomputes
391
+ // req.url from the original path and would silently revert it. The slashAdded
392
+ // mangle belongs to the mount that consumed a prefix, not to a pathless use
393
+ if (req.url !== req._lastUrl) {
394
+ req._absorbUrlRewrite(taken !== 0);
395
+ req._consumed -= taken;
396
+ setMountedPath(req);
397
+ } else {
398
+ if (pushed < 0 && req._originalPath.length > req._consumed) {
399
+ // a rewrite below this mount left a remainder where entry had none: express
400
+ // strips the first character of it when it rejoins, see issue #17
401
+ req._originalPath =
402
+ req._originalPath.slice(0, req._consumed) + req._originalPath.slice(req._consumed + 1);
403
+ req._mayFailDecode = null;
404
+ }
405
+ if (taken !== 0) {
406
+ // a pathless use consumed nothing and rewrote nothing, so the recompute would
407
+ // write back the very values it reads
408
+ req._consumed -= taken;
409
+ setMountedPath(req);
410
+ }
411
+ }
412
+ restoreApp(route, req);
413
+ }
414
+ if (isRouter) {
415
+ if (this.skipCheck) {
416
+ // on a compiled chain, leaving the router is what running out of chain already
417
+ // means: ordinary routing takes over after the mount. With no mount in the chain
418
+ // the router being left is the app's own, and nothing of it may run afterwards
419
+ if (this.skipUntil?.keepMount) {
420
+ return this.dispatch(this.routes.length);
421
+ }
422
+ return this.resolve(false);
423
+ }
424
+ // out of this router entirely, so whoever mounted it carries on after the
425
+ // mount. The app's own walk has nobody after it, and answers 404
426
+ return this.resolve(false);
427
+ }
428
+ req.routeCount++;
429
+ // dispatch is a plain call, so a synchronous throw would escape here instead of
430
+ // rejecting, as it used to when this recursed through the async _routeRequest
431
+ try {
432
+ return this.dispatch(this.routeIndex + 1);
433
+ } catch (err) {
434
+ return this.reject(err);
435
+ }
436
+ }
437
+
438
+ /**
439
+ * One hop, which is what next() does: with nothing, run the route's next callback; with "route",
440
+ * leave the route; with anything else, remember it as the error and carry on.
441
+ *
442
+ * @param {any} thingamabob what next() was called with: nothing, "route", or an error
443
+ */
444
+ step(thingamabob) {
445
+ const req = this.req;
446
+ const res = this.res;
447
+ const route = this.route;
448
+ const router = this.router;
449
+ if (thingamabob) {
450
+ if (thingamabob === "route" || thingamabob === "router") {
451
+ return this.leaveHop(thingamabob === "router");
452
+ } else {
453
+ req._error = thingamabob;
454
+ req._errorKey = route.routeKey;
455
+ req._errorGroup = route.group;
456
+ }
457
+ }
458
+ const kind = route.callbackKinds[this.callbackIndex];
459
+ const callback = route.callbacks[this.callbackIndex++];
460
+ if (!callback) {
461
+ return this.leaveHop(false);
462
+ }
463
+ // skipping routes we already went through via optimized path. Before the Router branch
464
+ // below and not after it: a mount whose chain was compiled has already run, and running it
465
+ // again would answer from inside the router a request that had just left it
466
+ if (!this.skipCheck && this.skipUntil && this.skipUntil.routeKey >= route.routeKey) {
467
+ return this.step(undefined);
468
+ }
469
+ // A mounted router or application is stepped over while an error is in flight. Its handle
470
+ // takes three arguments, so express's Layer#handleError hands the error straight on: what a
471
+ // mount catches is what it raised itself. Entering it left req.app pointing at the mounted
472
+ // application, whose settings answered, and a 500 carried an ETag under etag false.
473
+ if (kind === CALLBACK_ROUTER && !req._error) {
474
+ if (callback._isApplication) {
475
+ rememberApp(this, route, req);
476
+ useApp(req, callback);
477
+ }
478
+ const pushedParams = callback._settings.mergeParams;
479
+ if (pushedParams) {
480
+ (req._paramStack ??= []).push(req.params);
481
+ }
482
+ // express restores req.params when a router hands back, so what runs after the mount
483
+ // sees the params it had before it
484
+ const parentParams = req.params;
485
+ // each router answers OPTIONS with the verbs it knows itself, so the one being entered
486
+ // starts its own list: express keeps that list per router, and a router that hands back
487
+ // without answering leaves the outer one's untouched
488
+ const parentMethods = req._matchedMethods;
489
+ if (parentMethods !== null) {
490
+ req._matchedMethods = new Set();
491
+ }
492
+ callback
493
+ ._routeRequest(req, res, 0)
494
+ .then((routed) => {
495
+ // the child's params are scoped to it, and must not leak into the routes after
496
+ if (pushedParams) {
497
+ req._paramStack.pop();
498
+ }
499
+ req.params = parentParams;
500
+ if (req._error) {
501
+ req._errorKey = route.routeKey;
502
+ req._errorGroup = route.group;
503
+ }
504
+ if (routed) {
505
+ if (parentMethods !== null) {
506
+ req._matchedMethods = parentMethods;
507
+ }
508
+ return this.resolve(true);
509
+ }
510
+ const childMethods = req._matchedMethods;
511
+ if (parentMethods !== null) {
512
+ req._matchedMethods = parentMethods;
513
+ }
514
+ if (req._isOptions && childMethods !== null && childMethods.size && !req._error) {
515
+ // OPTIONS routing is different, it stops in the router if matched.
516
+ // Express answers as the router hands back, so a throw while answering,
517
+ // a head already written being the way, walks on to later error handlers
518
+ try {
519
+ router._sendOptionsReply(req, res, childMethods);
520
+ return this.resolve(true);
521
+ } catch (err) {
522
+ return this.step(err);
523
+ }
524
+ }
525
+ // An error carried out of the mount is not answered by the automatic reply, and
526
+ // stopping here handed it to the default page: express walks on to the error
527
+ // handlers written after the mount, for OPTIONS as for any other method.
528
+ this.step(undefined);
529
+ })
530
+ // a rejection out of the nested walk, or a throw above, must reject this one
531
+ // instead of dying as an unhandled rejection; wrapped for the native pair's
532
+ // receiver
533
+ .catch((err) => this.reject(err));
534
+ } else {
535
+ // errors and error handlers live out of line: this is the cold path, and its size
536
+ // was pushing step past the inlining threshold
537
+ if (req._error || kind === CALLBACK_ERROR) {
538
+ return this.errorHop(kind, callback);
539
+ }
540
+
541
+ try {
542
+ // handling OPTIONS method
543
+ if (req._isOptions && !route.all && route.method !== "OPTIONS") {
544
+ // an OPTIONS request always carries the set, see the Request constructor
545
+ const matched = /** @type {Set<string>} */ (req._matchedMethods);
546
+ matched.add(route.method);
547
+ if (route.gettable) {
548
+ matched.add("HEAD");
549
+ }
550
+ return this.step(undefined);
551
+ }
552
+ // entered only so its param callbacks could run, see the scan in dispatch: the verb
553
+ // cannot serve a HEAD, so nothing here answers it
554
+ if (req._isHead && !route.all && !route.gettable && route.method !== "HEAD") {
555
+ return this.step(undefined);
556
+ }
557
+
558
+ const out = callback(req, res, this.next);
559
+ if (out instanceof Promise) {
560
+ // Express 5 forwards a rejected handler promise to the error middleware on its
561
+ // own, so there is nothing for "catch async errors" or express-async-errors to
562
+ // opt into. A bare rejection carries no error, and express invents this one
563
+ out.catch((err) => {
564
+ req._error = err || new Error("Rejected promise");
565
+ req._errorKey = route.routeKey;
566
+ req._errorGroup = route.group;
567
+ return this.step(undefined);
568
+ });
569
+ }
570
+ } catch (err) {
571
+ req._error = err;
572
+ req._errorKey = route.routeKey;
573
+ req._errorGroup = route.group;
574
+ return this.step(undefined);
575
+ }
576
+ }
577
+ }
578
+ }
579
+
580
+ module.exports = Walk;
package/src/websocket.js CHANGED
@@ -18,10 +18,13 @@ 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
+
21
24
  // the parameter names in a path, in the order µWS numbers them
22
25
  const PARAM = /:(\w+)/g;
23
26
 
24
- // Handlers µWS calls with the socket. Everything else in a behavior object is a µWS setting
27
+ // Handlers uWS calls with the socket. Everything else in a behavior object is a uWS setting
25
28
  // (maxPayloadLength, idleTimeout, compression, ...) and rides through untouched.
26
29
  const SOCKET_HANDLERS = ["open", "message", "dropped", "drain", "close", "ping", "pong", "subscription"];
27
30
 
@@ -46,11 +49,10 @@ function joinPaths(prefix, path) {
46
49
  /**
47
50
  * Every websocket route reachable from this router, with the mount paths already applied.
48
51
  *
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.
52
+ * Walked separately from the HTTP routes: those fall back to ordinary routing when uWS cannot
53
+ * match them, a websocket has no fallback, so an unmountable one is refused out loud.
52
54
  *
53
- * @param {any} router
55
+ * @param {Router} router
54
56
  * @param {string|null} prefix the mount path accumulated so far, or null once a mount was a
55
57
  * shape µWS cannot match, which makes everything below it unreachable
56
58
  * @param {any[]} out
@@ -97,11 +99,10 @@ function collectRoutes(router, prefix, out, seen) {
97
99
  }
98
100
 
99
101
  /**
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.
102
+ * The uWS upgrade handler for one route: builds this project's request and response, offers them
103
+ * to the application's own `upgrade` hook, and completes the handshake unless that hook answered.
103
104
  *
104
- * @param {any} app the application whose request and response classes serve this route
105
+ * @param {Application} app the application whose request and response classes serve this route
105
106
  * @param {string} path the composed path, whose parameters are read back by index
106
107
  * @param {any} behavior what the caller registered
107
108
  * @returns {(res: any, req: any, context: any) => void}
@@ -111,7 +112,7 @@ function makeUpgradeHandler(app, path, behavior) {
111
112
  const userUpgrade = behavior.upgrade;
112
113
 
113
114
  return (res, req, context) => {
114
- // read off the µWS request before anything can await: it is neutered on return, and the
115
+ // read off the uWS request before anything can await: it is neutered on return, and the
115
116
  // handshake needs these three even when the upgrade is decided asynchronously
116
117
  const key = req.getHeader("sec-websocket-key");
117
118
  const protocol = req.getHeader("sec-websocket-protocol");
@@ -169,17 +170,16 @@ function makeUpgradeHandler(app, path, behavior) {
169
170
  return;
170
171
  }
171
172
 
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
173
+ // an async hook outlives this callback, so uWS has to be told who to call if the client
174
+ // leaves first. Registered now, inside the handler, the only place uWS accepts it
175
175
  res.onAborted(() => {
176
176
  aborted = true;
177
177
  // and on the response too, so a hook that is still awaiting can see the client left
178
178
  // rather than working on towards a handshake nobody is waiting for
179
179
  response.aborted = true;
180
180
  });
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
181
+ // whatever the hook writes now lands outside the cork uWS holds for this callback, so the
182
+ // response opens its own, exactly as a route handler answering late does
183
183
  response._corkNeeded = true;
184
184
  decision.then(accept, (err) => {
185
185
  if (!aborted && !response.finished) {
@@ -193,11 +193,10 @@ function makeUpgradeHandler(app, path, behavior) {
193
193
  }
194
194
 
195
195
  /**
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.
196
+ * Hands every websocket route to uWS. Called from listen(), before the catch-all: uWS routes an
197
+ * upgrade to the websocket route even when a catch-all covers the same path.
199
198
  *
200
- * @param {any} app
199
+ * @param {Application} app
201
200
  */
202
201
  function registerWebSocketRoutes(app) {
203
202
  const routes = [];
@@ -217,7 +216,7 @@ function registerWebSocketRoutes(app) {
217
216
  * used: a handler under a misspelled name would otherwise never run and never say why.
218
217
  *
219
218
  * @param {string} path
220
- * @param {any} behavior
219
+ * @param {any} behavior uWS's WebSocketBehavior, whose shipped typings do not describe it
221
220
  */
222
221
  function checkBehavior(path, behavior) {
223
222
  if (typeof path !== "string") {