fulmine.js 5.19.3 → 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 +1 -1
- package/src/application.js +25 -36
- package/src/cli.js +22 -16
- package/src/cluster.js +2 -2
- package/src/compression.js +102 -61
- package/src/declarative.js +47 -24
- package/src/hot-settings.js +5 -5
- package/src/lazy-readable.js +8 -6
- package/src/lazy-writable.js +3 -3
- package/src/middlewares.js +94 -55
- package/src/nest.js +3 -2
- package/src/node-shim.js +10 -5
- package/src/optimizer.js +9 -7
- package/src/options.d.ts +9 -4
- package/src/request-utils.js +3 -2
- package/src/request.js +49 -38
- package/src/response.js +145 -109
- package/src/route.js +3 -3
- package/src/router-utils.js +62 -14
- package/src/router.js +46 -28
- package/src/server-shape.js +14 -10
- package/src/server-timing.js +16 -4
- package/src/socket.js +3 -3
- package/src/testing.js +4 -3
- package/src/usage.js +10 -5
- package/src/utils.js +120 -23
- package/src/verify.js +4 -3
- package/src/view.js +1 -1
- package/src/walk.js +8 -7
- package/src/websocket.js +17 -8
- package/src/work.js +3 -3
package/src/router-utils.js
CHANGED
|
@@ -30,6 +30,48 @@ const { METHODS } = require("http");
|
|
|
30
30
|
* would be a second copy of createRoute that nothing keeps in step.
|
|
31
31
|
* @typedef {any} RouteEntry
|
|
32
32
|
*/
|
|
33
|
+
/**
|
|
34
|
+
* What nativePreset builds for a literal registration: the constants every request to that route
|
|
35
|
+
* shares, read off the preset instead of the URL.
|
|
36
|
+
* @typedef {object} NativePreset
|
|
37
|
+
* @property {string} path
|
|
38
|
+
* @property {string} method
|
|
39
|
+
* @property {boolean} endsWithSlash
|
|
40
|
+
* @property {string} opPath
|
|
41
|
+
* @property {boolean} isOptions
|
|
42
|
+
* @property {boolean} isHead
|
|
43
|
+
* @property {boolean} skipHeaders written at registration, and taken back by a middleware added
|
|
44
|
+
* after listen, see _skipPresets
|
|
45
|
+
* @property {boolean} skipQuery the same, for the query
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* Where a granted header skip lives: the preset itself for a literal registration, a holder of its
|
|
49
|
+
* own for a parameterised one, see makeHandler in optimizer.js.
|
|
50
|
+
* @typedef {object} SkipHolder
|
|
51
|
+
* @property {boolean} skipHeaders
|
|
52
|
+
* @property {boolean} skipQuery
|
|
53
|
+
* @property {string|null} [method]
|
|
54
|
+
* @property {boolean} [isOptions]
|
|
55
|
+
* @property {boolean} [isHead]
|
|
56
|
+
*/
|
|
57
|
+
/**
|
|
58
|
+
* An earlier registration a mount is guarded by, see guardsInside.
|
|
59
|
+
* @typedef {{path: string, use: boolean, method: string, all: boolean}} MountGuard
|
|
60
|
+
*/
|
|
61
|
+
/**
|
|
62
|
+
* A layer as express shapes it, which is what app.stack and router.stack hand out.
|
|
63
|
+
* @typedef {object} Layer
|
|
64
|
+
* @property {Function} handle
|
|
65
|
+
* @property {string} name
|
|
66
|
+
* @property {undefined} params
|
|
67
|
+
* @property {undefined} path
|
|
68
|
+
* @property {never[]} keys
|
|
69
|
+
* @property {RouteEntry|undefined} route
|
|
70
|
+
*/
|
|
71
|
+
/**
|
|
72
|
+
* A websocket registration, kept until listen() hands it to µWS.
|
|
73
|
+
* @typedef {{path: string, behavior: Record<string, unknown>, owner: Router}} WsRoute
|
|
74
|
+
*/
|
|
33
75
|
|
|
34
76
|
// whether a registered path could be asked for in another case, which is what decides whether the
|
|
35
77
|
// native router can be trusted to prefer it, see _optimizeRoute
|
|
@@ -100,8 +142,9 @@ for (const method of resDecMethods) {
|
|
|
100
142
|
* them off the handle.
|
|
101
143
|
*
|
|
102
144
|
* @param {RouteEntry} route
|
|
103
|
-
* @param {
|
|
104
|
-
*
|
|
145
|
+
* @param {Function & {_routes?: RouteEntry[], _isApplication?: boolean}} callback a handler, or a
|
|
146
|
+
* mounted router, which is callable and carries _routes
|
|
147
|
+
* @returns {Layer} the layer object, which is express's shape and not one of ours
|
|
105
148
|
*/
|
|
106
149
|
function layerFor(route, callback) {
|
|
107
150
|
const layer = {
|
|
@@ -129,7 +172,7 @@ function layerFor(route, callback) {
|
|
|
129
172
|
* that name.
|
|
130
173
|
*
|
|
131
174
|
* @param {RouteEntry} route
|
|
132
|
-
* @returns {
|
|
175
|
+
* @returns {Layer} the layer object, which is express's shape and not one of ours
|
|
133
176
|
*/
|
|
134
177
|
function routeLayer(route) {
|
|
135
178
|
const handle = function handle(req, res, next) {
|
|
@@ -172,7 +215,9 @@ function nativeDone(matched) {
|
|
|
172
215
|
if (!matched) {
|
|
173
216
|
queueMicrotask(() => {
|
|
174
217
|
const response = this.res;
|
|
175
|
-
|
|
218
|
+
// a 404 after the head is left as it is, as express's final handler leaves it; an error
|
|
219
|
+
// after it goes on to _handleError, which closes the connection as that handler does
|
|
220
|
+
if (response.aborted || (response.headersSent && !this.req._error)) {
|
|
176
221
|
return;
|
|
177
222
|
}
|
|
178
223
|
try {
|
|
@@ -326,7 +371,7 @@ const EMPTY_INDICES = /** @type {number[]} */ ([]);
|
|
|
326
371
|
* patterns are pure literals, everything else, "/*" included, stays in alwaysVisit and is still
|
|
327
372
|
* matched per request by _pathMatches.
|
|
328
373
|
*
|
|
329
|
-
* @param {
|
|
374
|
+
* @param {RouteEntry[]} routes the router's own table
|
|
330
375
|
* @param {boolean} caseFlag the frozen case-sensitivity flag
|
|
331
376
|
* @returns {{map: Map<string, number[]>, alwaysVisit: number[]}}
|
|
332
377
|
*/
|
|
@@ -513,7 +558,7 @@ function logError(router, err) {
|
|
|
513
558
|
/**
|
|
514
559
|
* The uWS onAborted handler, bound to the response: a closure here captured two locals and cost
|
|
515
560
|
* a context plus a function per request, for a path that only ever runs on a client abort.
|
|
516
|
-
* @this {
|
|
561
|
+
* @this {Response} the response, with the request linked as this.req
|
|
517
562
|
*/
|
|
518
563
|
function onNativeAborted() {
|
|
519
564
|
const response = this;
|
|
@@ -620,7 +665,7 @@ const CALLBACK_ROUTER = 2;
|
|
|
620
665
|
*
|
|
621
666
|
* @param {Request} req
|
|
622
667
|
* @param {RouteEntry} route
|
|
623
|
-
* @param {
|
|
668
|
+
* @param {unknown} err whatever decoding threw
|
|
624
669
|
*/
|
|
625
670
|
function raiseDecodeFailure(req, route, err) {
|
|
626
671
|
if (req._error) {
|
|
@@ -686,7 +731,7 @@ function stepsOver(route, req) {
|
|
|
686
731
|
// it would answer a GET differently from express. See the same seeding in middlewares.js
|
|
687
732
|
if (!("body" in req)) {
|
|
688
733
|
// cast because `body` is deliberately not a field of Request, see the comment there
|
|
689
|
-
/** @type {
|
|
734
|
+
/** @type {{body?: unknown}} */ (req).body = undefined;
|
|
690
735
|
}
|
|
691
736
|
return true;
|
|
692
737
|
}
|
|
@@ -740,10 +785,10 @@ function shadowsLeaf(guard, leafPath, leaf) {
|
|
|
740
785
|
* @param {Router} router the router the mount belongs to
|
|
741
786
|
* @param {RouteEntry} mount
|
|
742
787
|
* @param {string} pathPrefix what the mounts above this one consumed
|
|
743
|
-
* @param {
|
|
744
|
-
* @param {
|
|
788
|
+
* @param {RouteEntry[]} chain the layers that always run before the mount, which need no guard
|
|
789
|
+
* @param {MountGuard[]} inherited the guards from further out, since a mount two levels down is under
|
|
745
790
|
* everything written before either of them
|
|
746
|
-
* @returns {
|
|
791
|
+
* @returns {MountGuard[]|null} null when a path cannot be read segment by segment, which leaves the mount
|
|
747
792
|
* to ordinary dispatch rather than guessing about it
|
|
748
793
|
*/
|
|
749
794
|
function guardsInside(router, mount, pathPrefix, chain, inherited) {
|
|
@@ -809,10 +854,13 @@ function restoreApp(route, req) {
|
|
|
809
854
|
/**
|
|
810
855
|
* useApp
|
|
811
856
|
* @param {Request} req
|
|
812
|
-
* @param {
|
|
857
|
+
* @param {Router & {request?: object, response?: object}} app the application taking the request
|
|
858
|
+
* over. Typed as a router because the callers hold one, and only an application carries the two
|
|
859
|
+
* prototype layers read below
|
|
813
860
|
*/
|
|
814
861
|
function useApp(req, app) {
|
|
815
|
-
|
|
862
|
+
// only an application takes a request over, see rememberApp
|
|
863
|
+
req.app = /** @type {import("./application.js").Application} */ (app);
|
|
816
864
|
if (req.res) {
|
|
817
865
|
req.res.app = app;
|
|
818
866
|
}
|
|
@@ -864,7 +912,7 @@ function callablePrototypeFor(classPrototype) {
|
|
|
864
912
|
prototype = Object.create(classPrototype);
|
|
865
913
|
for (const name of ["apply", "call", "toString"]) {
|
|
866
914
|
Object.defineProperty(prototype, name, {
|
|
867
|
-
value:
|
|
915
|
+
value: Function.prototype[name],
|
|
868
916
|
writable: true,
|
|
869
917
|
configurable: true,
|
|
870
918
|
enumerable: false
|
package/src/router.js
CHANGED
|
@@ -26,6 +26,7 @@ const {
|
|
|
26
26
|
pathsCanOverlap,
|
|
27
27
|
regexpGroupKeys,
|
|
28
28
|
NullObject,
|
|
29
|
+
headersSentError,
|
|
29
30
|
EMPTY_REGEX,
|
|
30
31
|
settingsEpoch
|
|
31
32
|
} = require("./utils.js");
|
|
@@ -67,6 +68,12 @@ const {
|
|
|
67
68
|
} = require("./router-utils.js");
|
|
68
69
|
|
|
69
70
|
/** @typedef {import("./router-utils.js").RouteEntry} RouteEntry */
|
|
71
|
+
/** @typedef {import("./router-utils.js").SkipHolder} SkipHolder */
|
|
72
|
+
/** @typedef {import("./router-utils.js").NativePreset} NativePreset */
|
|
73
|
+
/** @typedef {import("./router-utils.js").Layer} Layer */
|
|
74
|
+
/** @typedef {import("./router-utils.js").WsRoute} WsRoute */
|
|
75
|
+
/** @typedef {import("uWebSockets.js").HttpRequest} UwsRequest */
|
|
76
|
+
/** @typedef {import("uWebSockets.js").HttpResponse} UwsResponse */
|
|
70
77
|
|
|
71
78
|
// hands out one number per app.route(), so the routes it creates know they belong together
|
|
72
79
|
let routeGroups = 0;
|
|
@@ -76,7 +83,7 @@ let routeKey = 0;
|
|
|
76
83
|
module.exports = class Router extends EventEmitter {
|
|
77
84
|
/**
|
|
78
85
|
* The router or application this one is mounted on, undefined until it is.
|
|
79
|
-
* @type {
|
|
86
|
+
* @type {Router|undefined}
|
|
80
87
|
*/
|
|
81
88
|
parent;
|
|
82
89
|
|
|
@@ -165,11 +172,11 @@ module.exports = class Router extends EventEmitter {
|
|
|
165
172
|
this._routes = [];
|
|
166
173
|
// websocket routes, kept apart from the HTTP ones: µWS serves them itself and listen()
|
|
167
174
|
// hands them over whole, mount paths and all
|
|
168
|
-
/** @type {
|
|
175
|
+
/** @type {WsRoute[]|null} */
|
|
169
176
|
this._wsRoutes = null;
|
|
170
177
|
// the native presets allowed to skip the header copy, so a late middleware or an etag
|
|
171
178
|
// arriving after listen can take the permission back; null until one is granted
|
|
172
|
-
/** @type {Set<
|
|
179
|
+
/** @type {Set<SkipHolder>|null} */
|
|
173
180
|
this._skipPresets = null;
|
|
174
181
|
/** @type {boolean|undefined} */
|
|
175
182
|
this._hasErrMwCache = undefined;
|
|
@@ -226,13 +233,13 @@ module.exports = class Router extends EventEmitter {
|
|
|
226
233
|
*
|
|
227
234
|
* @param {any} req a Request, or the plain object express's own router tests drive it with
|
|
228
235
|
* @param {any} res a Response, or whatever the caller is serving with
|
|
229
|
-
* @param {(err?:
|
|
236
|
+
* @param {(err?: unknown) => void} [next]
|
|
230
237
|
* @returns {Promise<void>}
|
|
231
238
|
*/
|
|
232
239
|
async handle(req, res, next) {
|
|
233
240
|
// a request from node's own server, which is what http.createServer(app) delivers
|
|
234
241
|
if (isNodeRequest(req)) {
|
|
235
|
-
return serveNodeRequest(this, req,
|
|
242
|
+
return serveNodeRequest(this, req, res, next);
|
|
236
243
|
}
|
|
237
244
|
// an app taking over a request becomes that request's app, as it does when mounted, so
|
|
238
245
|
// req.app.get("view engine") inside a sub-app reads the sub-app's settings. A plain router
|
|
@@ -562,7 +569,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
562
569
|
* splicing one out moves nothing. The layer objects are kept, so identities compare across two
|
|
563
570
|
* reads the way they do in Express.
|
|
564
571
|
*
|
|
565
|
-
* @returns {
|
|
572
|
+
* @returns {Layer[]}
|
|
566
573
|
*/
|
|
567
574
|
get stack() {
|
|
568
575
|
const layers = [];
|
|
@@ -585,9 +592,11 @@ module.exports = class Router extends EventEmitter {
|
|
|
585
592
|
* anything not comparable as a string is compiled to a regular expression and marked complex.
|
|
586
593
|
*
|
|
587
594
|
* @param {string} method HTTP method, or USE for a mount
|
|
588
|
-
* @param {
|
|
589
|
-
* @param {any} [parent] what to return, so chaining lands on the app rather than the router
|
|
590
|
-
*
|
|
595
|
+
* @param {string|RegExp|(string|RegExp)[]} path one path or several
|
|
596
|
+
* @param {any} [parent] what to return, so chaining lands on the app rather than the router.
|
|
597
|
+
* Loose because it is also the builder app.route() hands back
|
|
598
|
+
* @param {...any} callbacks handlers, or arrays of them at any depth, flattened below. Loose
|
|
599
|
+
* because a parameter keeps its declared type through that reassignment
|
|
591
600
|
* @returns {any} parent
|
|
592
601
|
*/
|
|
593
602
|
createRoute(method, path, parent = this, ...callbacks) {
|
|
@@ -706,7 +715,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
706
715
|
stack,
|
|
707
716
|
// the route as a request sees it, which is the route itself unless the path was
|
|
708
717
|
// normalised. Written into the literal so every route keeps one shape
|
|
709
|
-
exposed: /** @type {
|
|
718
|
+
exposed: /** @type {RouteEntry|undefined} */ (undefined),
|
|
710
719
|
routeKey: routeKey++,
|
|
711
720
|
// which app.route() this came from, when it came from one, so the routes it built
|
|
712
721
|
// count as one route where an error is concerned. undefined for every other route
|
|
@@ -768,8 +777,8 @@ module.exports = class Router extends EventEmitter {
|
|
|
768
777
|
* as a method because a mounted router is asked for its own through it.
|
|
769
778
|
*
|
|
770
779
|
* @param {RouteEntry} route
|
|
771
|
-
* @param {
|
|
772
|
-
* @returns {
|
|
780
|
+
* @param {RouteEntry[]} routes every route of this router, in registration order
|
|
781
|
+
* @returns {RouteEntry[]|false} the chain, ending in the route itself
|
|
773
782
|
*/
|
|
774
783
|
_optimizeRoute(route, routes) {
|
|
775
784
|
return optimizeRoute(this, route, routes);
|
|
@@ -788,12 +797,12 @@ module.exports = class Router extends EventEmitter {
|
|
|
788
797
|
* request does whichever path serves it. The response rides back as request.res: returning
|
|
789
798
|
* a `{ request, response }` pair was one throwaway object per request.
|
|
790
799
|
*
|
|
791
|
-
* @param {
|
|
792
|
-
* @param {
|
|
793
|
-
* @param {
|
|
794
|
-
* @param {
|
|
800
|
+
* @param {UwsResponse} res uWS response
|
|
801
|
+
* @param {UwsRequest} req uWS request, readable only during this call
|
|
802
|
+
* @param {NativePreset} [preset] a literal registration's constants, see nativePreset
|
|
803
|
+
* @param {SkipHolder} [skipHolder] the object a granted header skip lives on: the preset itself
|
|
795
804
|
* for a literal registration, a holder of its own for a parameterised one
|
|
796
|
-
* @returns {
|
|
805
|
+
* @returns {Request} the request, with the response reachable as request.res
|
|
797
806
|
*/
|
|
798
807
|
handleRequest(res, req, preset, skipHolder) {
|
|
799
808
|
const request = new this._request(req, res, this, preset, skipHolder);
|
|
@@ -830,7 +839,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
830
839
|
* for a response that outlives its handler callback: the native handler arms it in its
|
|
831
840
|
* finally when the answer is still pending, which on a synchronous route it never is.
|
|
832
841
|
*
|
|
833
|
-
* @param {
|
|
842
|
+
* @param {UwsResponse} res uWS response
|
|
834
843
|
* @param {Response} response
|
|
835
844
|
*/
|
|
836
845
|
_armAbort(res, response) {
|
|
@@ -846,7 +855,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
846
855
|
* are compared segment by segment.
|
|
847
856
|
*
|
|
848
857
|
* @param {RouteEntry} route
|
|
849
|
-
* @param {
|
|
858
|
+
* @param {RouteEntry[]} routes every route of the router this one belongs to
|
|
850
859
|
* @returns {boolean}
|
|
851
860
|
*/
|
|
852
861
|
_isFollowedByAnOverlap(route, routes) {
|
|
@@ -881,7 +890,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
881
890
|
* the optimizer tests replace it to see which routes went native.
|
|
882
891
|
*
|
|
883
892
|
* @param {RouteEntry} route
|
|
884
|
-
* @param {
|
|
893
|
+
* @param {RouteEntry[]} optimizedPath the chain the route was optimized with
|
|
885
894
|
*/
|
|
886
895
|
_registerUwsRoute(route, optimizedPath) {
|
|
887
896
|
registerUwsRoute(this, route, optimizedPath);
|
|
@@ -913,6 +922,14 @@ module.exports = class Router extends EventEmitter {
|
|
|
913
922
|
}
|
|
914
923
|
}
|
|
915
924
|
logError(this, err);
|
|
925
|
+
// no error page can follow a head that is out, so express's final handler closes the
|
|
926
|
+
// connection instead, and leaves a response that was already ended as it is
|
|
927
|
+
if (response.headersSent) {
|
|
928
|
+
if (!response.finished) {
|
|
929
|
+
response.destroy();
|
|
930
|
+
}
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
916
933
|
if (response.statusCode === 200) {
|
|
917
934
|
// the status the error carries, as express's own final handler reads it: a body that
|
|
918
935
|
// was too large or a request cut short is the client's 4xx, not a 500 from here
|
|
@@ -926,7 +943,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
926
943
|
* The HTML for an error, which in production says only what the status means rather than what
|
|
927
944
|
* went wrong, so a stack trace does not reach the client.
|
|
928
945
|
*
|
|
929
|
-
* @param {
|
|
946
|
+
* @param {unknown} err whatever was thrown, which need not be an Error
|
|
930
947
|
* @param {number} statusCode
|
|
931
948
|
* @param {boolean} [checkEnv] whether production should redact it
|
|
932
949
|
* @returns {string}
|
|
@@ -1223,7 +1240,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1223
1240
|
|
|
1224
1241
|
/**
|
|
1225
1242
|
* Resolves with the route that answered, or false when nothing matched.
|
|
1226
|
-
* @returns {Promise<
|
|
1243
|
+
* @returns {Promise<RouteEntry|false>}
|
|
1227
1244
|
*/
|
|
1228
1245
|
_routeRequest(req, res, startIndex = 0, routes = this._routes, skipCheck = false, skipUntil) {
|
|
1229
1246
|
return new Promise((resolve, reject) => {
|
|
@@ -1289,7 +1306,8 @@ module.exports = class Router extends EventEmitter {
|
|
|
1289
1306
|
settingsEpoch.n++;
|
|
1290
1307
|
}
|
|
1291
1308
|
}
|
|
1292
|
-
|
|
1309
|
+
// a handler in the path position was moved to the callbacks above
|
|
1310
|
+
this.createRoute("USE", /** @type {string|RegExp|(string|RegExp)[]} */ (path), this, ...callbacks);
|
|
1293
1311
|
return this;
|
|
1294
1312
|
}
|
|
1295
1313
|
|
|
@@ -1314,7 +1332,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1314
1332
|
* });
|
|
1315
1333
|
*
|
|
1316
1334
|
* @param {string} path a literal path, or one whose parameters are whole segments
|
|
1317
|
-
* @param {
|
|
1335
|
+
* @param {Record<string, unknown>} behavior uWS's WebSocketBehavior, plus the optional `upgrade` above
|
|
1318
1336
|
* @returns {this}
|
|
1319
1337
|
*/
|
|
1320
1338
|
ws(path, behavior) {
|
|
@@ -1350,7 +1368,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1350
1368
|
this._pendingGroupMethods = groupMethods;
|
|
1351
1369
|
this._pendingGroupStack = groupStack;
|
|
1352
1370
|
try {
|
|
1353
|
-
return this.createRoute(method, path,
|
|
1371
|
+
return this.createRoute(method, path, fns, ...callbacks);
|
|
1354
1372
|
} finally {
|
|
1355
1373
|
this._pendingGroup = undefined;
|
|
1356
1374
|
this._pendingGroupMethods = undefined;
|
|
@@ -1370,7 +1388,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1370
1388
|
*
|
|
1371
1389
|
* @param {Request} request
|
|
1372
1390
|
* @param {Response} response
|
|
1373
|
-
* @param {
|
|
1391
|
+
* @param {unknown} err whatever was thrown, which need not be an Error
|
|
1374
1392
|
* @param {boolean} [checkEnv] whether production should redact it
|
|
1375
1393
|
*/
|
|
1376
1394
|
_sendErrorPage(request, response, err, checkEnv = false) {
|
|
@@ -1395,8 +1413,8 @@ module.exports = class Router extends EventEmitter {
|
|
|
1395
1413
|
* @param {Set<string>} methods the verbs the answering router knows, which are its own
|
|
1396
1414
|
*/
|
|
1397
1415
|
_sendOptionsReply(request, response, methods) {
|
|
1398
|
-
if (response.
|
|
1399
|
-
throw
|
|
1416
|
+
if (response.headersSent) {
|
|
1417
|
+
throw headersSentError("set");
|
|
1400
1418
|
}
|
|
1401
1419
|
// Express 5 sorts the methods and joins them with ", ", so the header reads the same
|
|
1402
1420
|
// regardless of the order the routes happened to be registered in
|
package/src/server-shape.js
CHANGED
|
@@ -34,6 +34,8 @@ limitations under the License.
|
|
|
34
34
|
const http = require("http");
|
|
35
35
|
const net = require("net");
|
|
36
36
|
|
|
37
|
+
/** @typedef {import("./application.js").Application} Application */
|
|
38
|
+
|
|
37
39
|
// what marks an application, read by the instanceof hook below. A symbol, so no plain field name
|
|
38
40
|
// can be mistaken for it
|
|
39
41
|
const kIsApplication = Symbol.for("fulmine.application");
|
|
@@ -45,20 +47,20 @@ const kIsApplication = Symbol.for("fulmine.application");
|
|
|
45
47
|
* @param {Function} klass http.Server or net.Server
|
|
46
48
|
*/
|
|
47
49
|
function acceptApplications(klass) {
|
|
48
|
-
const previous =
|
|
50
|
+
const previous = klass[Symbol.hasInstance];
|
|
49
51
|
// already taught, which happens when two copies of this package share one process
|
|
50
|
-
if (
|
|
52
|
+
if (klass[kIsApplication] === true) {
|
|
51
53
|
return;
|
|
52
54
|
}
|
|
53
55
|
Object.defineProperty(klass, Symbol.hasInstance, {
|
|
54
|
-
/** @param {
|
|
56
|
+
/** @param {unknown} value @returns {boolean} */
|
|
55
57
|
value: function (value) {
|
|
56
58
|
if (previous.call(this, value)) {
|
|
57
59
|
return true;
|
|
58
60
|
}
|
|
59
61
|
// an application is a function and a property read works on one. The guard is for the
|
|
60
62
|
// primitives and nulls that reach any instanceof
|
|
61
|
-
return value != null &&
|
|
63
|
+
return value != null && value[kIsApplication] === true;
|
|
62
64
|
},
|
|
63
65
|
configurable: true,
|
|
64
66
|
writable: true
|
|
@@ -73,7 +75,8 @@ acceptApplications(net.Server);
|
|
|
73
75
|
* The net.Server members Express's API does not give, on the application prototype. Each one
|
|
74
76
|
* answers for uWS, not for a node socket.
|
|
75
77
|
*
|
|
76
|
-
* @param {any} prototype Application.prototype
|
|
78
|
+
* @param {any} prototype Application.prototype, loose because the members are written here and not
|
|
79
|
+
* declared on the class
|
|
77
80
|
*/
|
|
78
81
|
function addServerMembers(prototype) {
|
|
79
82
|
Object.defineProperty(prototype, kIsApplication, { value: true, configurable: true });
|
|
@@ -97,13 +100,14 @@ function addServerMembers(prototype) {
|
|
|
97
100
|
* A handle this does not own: uWS's loop keeps the process alive and a caller cannot unref it.
|
|
98
101
|
* Both are no-ops returning the server, so a chain written against node's API keeps working.
|
|
99
102
|
*
|
|
100
|
-
* @
|
|
103
|
+
* @this {Application}
|
|
104
|
+
* @returns {Application}
|
|
101
105
|
*/
|
|
102
106
|
prototype.ref = function ref() {
|
|
103
107
|
return this;
|
|
104
108
|
};
|
|
105
109
|
|
|
106
|
-
/** @returns {
|
|
110
|
+
/** @this {Application} @returns {Application} */
|
|
107
111
|
prototype.unref = function unref() {
|
|
108
112
|
return this;
|
|
109
113
|
};
|
|
@@ -112,10 +116,10 @@ function addServerMembers(prototype) {
|
|
|
112
116
|
* Registers the callback like node's does and remembers the value, which is all a caller can
|
|
113
117
|
* observe. The timeout belongs to uWS and is set through uwsOptions.idleTimeout.
|
|
114
118
|
*
|
|
115
|
-
* @this {
|
|
119
|
+
* @this {Application & {timeout?: number}}
|
|
116
120
|
* @param {number} [msecs]
|
|
117
121
|
* @param {() => void} [callback]
|
|
118
|
-
* @returns {
|
|
122
|
+
* @returns {Application}
|
|
119
123
|
*/
|
|
120
124
|
prototype.setTimeout = function setTimeout(msecs, callback) {
|
|
121
125
|
this.timeout = msecs;
|
|
@@ -127,7 +131,7 @@ function addServerMembers(prototype) {
|
|
|
127
131
|
|
|
128
132
|
// The numbers node's http.Server carries. Inert here, but declared rather than left undefined:
|
|
129
133
|
// a library reads `server.keepAliveTimeout` to work out what it is talking to.
|
|
130
|
-
for (const [name, value] of /** @type {[string,
|
|
134
|
+
for (const [name, value] of /** @type {[string, number|null][]} */ ([
|
|
131
135
|
["timeout", 0],
|
|
132
136
|
["keepAliveTimeout", 5000],
|
|
133
137
|
["headersTimeout", 60000],
|
package/src/server-timing.js
CHANGED
|
@@ -34,6 +34,9 @@ limitations under the License.
|
|
|
34
34
|
"use strict";
|
|
35
35
|
|
|
36
36
|
const { work, names } = require("./work.js");
|
|
37
|
+
const { applyWriteHead } = require("./utils.js");
|
|
38
|
+
|
|
39
|
+
/** @typedef {import("./response.js")} Response */
|
|
37
40
|
|
|
38
41
|
/**
|
|
39
42
|
* A duration in milliseconds, as Server-Timing writes them: two decimals.
|
|
@@ -63,7 +66,8 @@ function describe(text) {
|
|
|
63
66
|
* true. Nothing is written for a request that built none of it, which is the usual one.
|
|
64
67
|
* @param {boolean} [options.total] whether to report the time up to the head. Default true.
|
|
65
68
|
* @param {string} [options.name] what the total is called. Default "total".
|
|
66
|
-
* @returns {(req: any, res: any, next: (err?:
|
|
69
|
+
* @returns {(req: any, res: any, next: (err?: unknown) => void) => void} the middleware. The pair is
|
|
70
|
+
* loose because the two methods below are added to the response here
|
|
67
71
|
*/
|
|
68
72
|
function serverTiming(options) {
|
|
69
73
|
const opts = options || {};
|
|
@@ -84,7 +88,7 @@ function serverTiming(options) {
|
|
|
84
88
|
* @param {string} name a token: letters, digits, dash and underscore
|
|
85
89
|
* @param {number} [duration] milliseconds
|
|
86
90
|
* @param {string} [description]
|
|
87
|
-
* @returns {
|
|
91
|
+
* @returns {Response} the response, so calls chain
|
|
88
92
|
*/
|
|
89
93
|
res.timing = function timing(name, duration, description) {
|
|
90
94
|
let mark = String(name).replace(/[^\w-]/g, "");
|
|
@@ -118,11 +122,11 @@ function serverTiming(options) {
|
|
|
118
122
|
}
|
|
119
123
|
if (value && typeof value.then === "function") {
|
|
120
124
|
return value.then(
|
|
121
|
-
/** @param {
|
|
125
|
+
/** @param {unknown} resolved */ (resolved) => {
|
|
122
126
|
done();
|
|
123
127
|
return resolved;
|
|
124
128
|
},
|
|
125
|
-
/** @param {
|
|
129
|
+
/** @param {unknown} err */ (err) => {
|
|
126
130
|
done();
|
|
127
131
|
throw err;
|
|
128
132
|
}
|
|
@@ -172,6 +176,14 @@ function serverTiming(options) {
|
|
|
172
176
|
}
|
|
173
177
|
};
|
|
174
178
|
|
|
179
|
+
const _writeHead = res.writeHead;
|
|
180
|
+
// writeHead settles the head, so a handler that calls it is stamped there, with the headers
|
|
181
|
+
// it carries applied first, as on-headers orders it
|
|
182
|
+
res.writeHead = function writeHead(statusCode, statusMessage, headers) {
|
|
183
|
+
const reason = applyWriteHead(this, statusMessage, headers);
|
|
184
|
+
stamp();
|
|
185
|
+
return _writeHead.call(this, statusCode, reason);
|
|
186
|
+
};
|
|
175
187
|
res.write = function write(chunk, encoding, callback) {
|
|
176
188
|
stamp();
|
|
177
189
|
return _write.call(this, chunk, encoding, callback);
|
package/src/socket.js
CHANGED
|
@@ -29,8 +29,8 @@ class Socket extends EventEmitter {
|
|
|
29
29
|
* The Socket's error listener, shared across sockets: an error closes the stand-in, which is
|
|
30
30
|
* the close connection trackers wait for. EventEmitter calls it with this = the emitter.
|
|
31
31
|
*
|
|
32
|
-
* @this {
|
|
33
|
-
* @param {
|
|
32
|
+
* @this {Socket}
|
|
33
|
+
* @param {unknown} err whatever the response reported, which need not be an Error
|
|
34
34
|
*/
|
|
35
35
|
static _onError(err) {
|
|
36
36
|
this.emit("close");
|
|
@@ -96,7 +96,7 @@ class Socket extends EventEmitter {
|
|
|
96
96
|
/**
|
|
97
97
|
* Finishes the response through the socket, which is how the middleware that only knows
|
|
98
98
|
* about sockets ends one.
|
|
99
|
-
* @param {
|
|
99
|
+
* @param {string|Buffer|Uint8Array} [body] whatever node's socket.end() would take
|
|
100
100
|
*/
|
|
101
101
|
end(body) {
|
|
102
102
|
this.response.end(body);
|
package/src/testing.js
CHANGED
|
@@ -31,6 +31,7 @@ const { work, names: workNames } = require("./work.js");
|
|
|
31
31
|
/** @typedef {import("./response.js")} Response */
|
|
32
32
|
/** @typedef {import("./router.js")} Router */
|
|
33
33
|
/** @typedef {import("./application.js").Application} Application */
|
|
34
|
+
/** @typedef {import("./router-utils.js").RouteEntry} RouteEntry */
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* Every route of an application and of the routers mounted under it, each with the path it answers
|
|
@@ -38,8 +39,8 @@ const { work, names: workNames } = require("./work.js");
|
|
|
38
39
|
*
|
|
39
40
|
* @param {Router} router
|
|
40
41
|
* @param {string} prefix
|
|
41
|
-
* @param {
|
|
42
|
-
* @returns {{route:
|
|
42
|
+
* @param {{route: RouteEntry, full: string}[]} [into]
|
|
43
|
+
* @returns {{route: RouteEntry, full: string}[]}
|
|
43
44
|
*/
|
|
44
45
|
function collectRoutes(router, prefix, into = []) {
|
|
45
46
|
for (const route of router._routes ?? []) {
|
|
@@ -263,7 +264,7 @@ function expectLazy(req, res, options) {
|
|
|
263
264
|
const done = work(req, res);
|
|
264
265
|
const unwanted = { ...done };
|
|
265
266
|
for (const field of allowed) {
|
|
266
|
-
|
|
267
|
+
unwanted[field] = false;
|
|
267
268
|
}
|
|
268
269
|
const listed = workNames(unwanted);
|
|
269
270
|
if (listed.length === 0) {
|
package/src/usage.js
CHANGED
|
@@ -18,6 +18,8 @@ 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).
|
|
@@ -107,7 +109,7 @@ function analyze(fn) {
|
|
|
107
109
|
// class methods and native functions do not parse alone, and unread code is unknown code
|
|
108
110
|
return UNKNOWN;
|
|
109
111
|
}
|
|
110
|
-
let root = /** @type {
|
|
112
|
+
let root = /** @type {import("acorn").AnyNode} */ (tree.body[0]);
|
|
111
113
|
if (!root) {
|
|
112
114
|
return UNKNOWN;
|
|
113
115
|
}
|
|
@@ -122,7 +124,8 @@ function analyze(fn) {
|
|
|
122
124
|
return UNKNOWN;
|
|
123
125
|
}
|
|
124
126
|
|
|
125
|
-
|
|
127
|
+
// checked by the loop below, which answers UNKNOWN for anything else
|
|
128
|
+
const params = /** @type {import("acorn").Identifier[]} */ (root.params);
|
|
126
129
|
// rest or destructured parameters alias the objects somewhere the walk cannot follow
|
|
127
130
|
for (const p of params) {
|
|
128
131
|
if (p.type !== "Identifier") {
|
|
@@ -231,9 +234,11 @@ function analyze(fn) {
|
|
|
231
234
|
* Walks every node, handing each its parent. Arrays and nested objects are entered, nothing
|
|
232
235
|
* is interpreted: the judging happens in the visitor.
|
|
233
236
|
*
|
|
234
|
-
* @param {any} node an acorn
|
|
237
|
+
* @param {any} node an acorn node, or an array or a scalar under one: walked by key, so no shape
|
|
238
|
+
* is assumed
|
|
235
239
|
* @param {any} parent its parent node, or null at the root
|
|
236
|
-
* @param {(node: any, parent: any) => void} visit
|
|
240
|
+
* @param {(node: any, parent: any) => void} visit handed every node, loose because the visitor
|
|
241
|
+
* reads edges of its own off each
|
|
237
242
|
*/
|
|
238
243
|
function walk(node, parent, visit) {
|
|
239
244
|
if (!node || typeof node.type !== "string") {
|
|
@@ -265,7 +270,7 @@ function walk(node, parent, visit) {
|
|
|
265
270
|
* it passes only when no later route could catch the fall-through. The framework's own 404 answers
|
|
266
271
|
* from the path alone.
|
|
267
272
|
*
|
|
268
|
-
* @param {
|
|
273
|
+
* @param {RouteEntry[]} chain the routes the native handler runs, in order, this route last
|
|
269
274
|
* @param {boolean} allowTerminalNext whether a fall-through past the chain lands only in the
|
|
270
275
|
* framework's own final answer
|
|
271
276
|
* @returns {{skipHeaders: boolean, skipQuery: boolean}}
|