fulmine.js 5.1.0 → 5.1.2
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/README.md +10 -1
- package/package.json +1 -1
- package/src/application.js +11 -5
- package/src/request.js +67 -42
- package/src/response.js +64 -39
- package/src/router.js +65 -14
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ Compatibility here is not a claim, it is a test suite. Every test runs against r
|
|
|
37
37
|
|
|
38
38
|
Fulmine is faster than Express where the framework itself is doing the work, and the same speed where it is not. Both halves of that sentence matter, so here is the honest version.
|
|
39
39
|
|
|
40
|
-
**Where it is clearly faster.** Routing and dispatch, request shapes with params and query strings, connection handling. Plain routing lands between 1.9x and
|
|
40
|
+
**Where it is clearly faster.** Routing and dispatch, request shapes with params and query strings, connection handling. Plain routing lands between 1.9x and 4.3x: hello-world 1.9x to 2.2x, an API endpoint with params and a query 3.2x to 4.3x, five route shapes served by one process 2.5x to 3.3x, nested routers 2.1x to 3.1x, a urlencoded body 3.4x to 4.1x, a thousand concurrent connections 2.7x to 3.2x. Route tables are where the native router shows: a thousand routes 9.7x to 12.9x, with a parameter in every one of them 10.4x to 14x, a parameterised route in a mounted router 7.1x to 8.3x. Those routes go to µWS's own router instead of being scanned, so the gap grows with the table instead of shrinking. Even the chain of 100 middlewares, for a long time the one routing row that stayed even because its cost is calling application code a hundred times, sits at 1.5x to 1.7x after the per-request work of August 2026.
|
|
41
41
|
|
|
42
42
|
**Where it is a wash.** Any request whose cost is dominated by work both servers hand to the same library. A 512 KiB JSON body is `JSON.parse`, a gzipped response is zlib, a hashed upload is OpenSSL, a 5 MiB stream is memory bandwidth. On those the ratio is capped by arithmetic somewhere around 1.0x to 1.2x, and no amount of work on either server moves it. The benchmark labels those rows rather than quietly publishing them as if the two were equivalent.
|
|
43
43
|
|
|
@@ -51,6 +51,15 @@ and posts the result where it belongs: as a comment on the commit or the pull re
|
|
|
51
51
|
`benchmark-summary` artifact on the run, see [`benchmark/README.md`](./benchmark/README.md)
|
|
52
52
|
to run it yourself.
|
|
53
53
|
|
|
54
|
+
## Public benchmarks
|
|
55
|
+
|
|
56
|
+
Numbers produced by a project about itself deserve suspicion, so Fulmine also stands in public arenas, run by their own rigs under their own rules:
|
|
57
|
+
|
|
58
|
+
- **[HttpArena](https://www.http-arena.com/#sort=rps:-1&q=Js)** (the link lands filtered on the JavaScript entries): the saved run measures 5.89 million pipelined requests per second, 1.12 million on baseline and 1.04 million on the json profile, ahead of every JavaScript entry on the board.
|
|
59
|
+
- **[web-frameworks](https://github.com/the-benchmarker/web-frameworks)**: entry merged, numbers arrive with their next published round.
|
|
60
|
+
|
|
61
|
+
More to come as their maintainers take the entries in.
|
|
62
|
+
|
|
54
63
|
## Attribution
|
|
55
64
|
|
|
56
65
|
Fulmine is a derivative work of [Ultimate Express](https://github.com/dimdenGD/ultimate-express) by [@dimdenGD](https://github.com/dimdenGD), used under the Apache License 2.0. The full commit history is preserved, so the original authorship is visible in the repository itself.
|
package/package.json
CHANGED
package/src/application.js
CHANGED
|
@@ -120,9 +120,10 @@ class Application extends Router {
|
|
|
120
120
|
* @param {any} req
|
|
121
121
|
* @param {any} res
|
|
122
122
|
* @param {any} app
|
|
123
|
+
* @param {any} [preset]
|
|
123
124
|
*/
|
|
124
|
-
constructor(req, res, app) {
|
|
125
|
-
super(req, res, app);
|
|
125
|
+
constructor(req, res, app, preset) {
|
|
126
|
+
super(req, res, app, preset);
|
|
126
127
|
}
|
|
127
128
|
};
|
|
128
129
|
this._response = class extends Response {
|
|
@@ -365,10 +366,11 @@ class Application extends Router {
|
|
|
365
366
|
*
|
|
366
367
|
* @param {any} res uWS response
|
|
367
368
|
* @param {any} req uWS request, readable only during this call
|
|
369
|
+
* @param {any} [preset] a literal registration's constants, see nativePreset in the router
|
|
368
370
|
* @returns {any} the request, with the response reachable as request.res
|
|
369
371
|
*/
|
|
370
|
-
handleRequest(res, req) {
|
|
371
|
-
const request = super.handleRequest(res, req);
|
|
372
|
+
handleRequest(res, req, preset) {
|
|
373
|
+
const request = super.handleRequest(res, req, preset);
|
|
372
374
|
// removal rides the close listener the Response constructor already has, since a second
|
|
373
375
|
// once() per request measured a tenth of a microsecond on the hot path.
|
|
374
376
|
// An aborted response only flips its flags without emitting 'close', which is why
|
|
@@ -388,7 +390,11 @@ class Application extends Router {
|
|
|
388
390
|
const response = request.res;
|
|
389
391
|
|
|
390
392
|
try {
|
|
391
|
-
const
|
|
393
|
+
const routed = this._routeRequest(request, response);
|
|
394
|
+
// dispatch has run its synchronous stretch inside _routeRequest by now, still
|
|
395
|
+
// under the cork uWS holds for this callback; the await below leaves it
|
|
396
|
+
response._corkNeeded = true;
|
|
397
|
+
const matchedRoute = await routed;
|
|
392
398
|
if (!matchedRoute && !response.headersSent && !response.aborted) {
|
|
393
399
|
this._endUnmatched(request, response);
|
|
394
400
|
}
|
package/src/request.js
CHANGED
|
@@ -140,7 +140,8 @@ module.exports = class Request extends Readable {
|
|
|
140
140
|
|
|
141
141
|
// Flat, name then value: an array of pairs meant one array allocated per header on every
|
|
142
142
|
// request, and a request carries eight or ten of them. Everything that reads this walks it two
|
|
143
|
-
// at a time.
|
|
143
|
+
// at a time. The names are lowercase by contract: uWS lowers them on the wire and the node
|
|
144
|
+
// shim lowers them in its forEach, so readers compare without lowering again.
|
|
144
145
|
#rawHeadersEntries = [];
|
|
145
146
|
|
|
146
147
|
/** @type {string|undefined|null} */
|
|
@@ -148,6 +149,9 @@ module.exports = class Request extends Readable {
|
|
|
148
149
|
|
|
149
150
|
#paused = false;
|
|
150
151
|
|
|
152
|
+
// a bodyless request whose empty end has not been delivered yet, see the constructor
|
|
153
|
+
#emptyBody = false;
|
|
154
|
+
|
|
151
155
|
body;
|
|
152
156
|
|
|
153
157
|
res;
|
|
@@ -168,7 +172,9 @@ module.exports = class Request extends Readable {
|
|
|
168
172
|
) {
|
|
169
173
|
r._connectionClose = true;
|
|
170
174
|
} else if (
|
|
171
|
-
|
|
175
|
+
// content-length: 0 declares that there is nothing, which is the same as declaring
|
|
176
|
+
// nothing: the stream ends empty either way, without the onData subscription
|
|
177
|
+
(headerKey.length === 14 && headerKey === "content-length" && value !== "0") ||
|
|
172
178
|
(headerKey.length === 17 && headerKey === "transfer-encoding")
|
|
173
179
|
) {
|
|
174
180
|
// noticed here so the body decision in the constructor does not build the headers object
|
|
@@ -190,8 +196,11 @@ module.exports = class Request extends Readable {
|
|
|
190
196
|
* @param {any} req the uWS request, readable only during this call
|
|
191
197
|
* @param {any} res the uWS response
|
|
192
198
|
* @param {any} app the application or router this request arrived at
|
|
199
|
+
* @param {any} [preset] a literal native registration's constants: µWS matched the URL byte
|
|
200
|
+
* for byte against that exact pattern and dispatched by method, so path, method and what
|
|
201
|
+
* derives from them are known without asking
|
|
193
202
|
*/
|
|
194
|
-
constructor(req, res, app) {
|
|
203
|
+
constructor(req, res, app, preset) {
|
|
195
204
|
// the same object every time: Readable reads these options and never writes to them
|
|
196
205
|
super(READABLE_OPTIONS);
|
|
197
206
|
this._res = res;
|
|
@@ -211,25 +220,39 @@ module.exports = class Request extends Readable {
|
|
|
211
220
|
// back off for every request that reads req.query.
|
|
212
221
|
this._rawQuery = req.getQuery() ?? "";
|
|
213
222
|
this.urlQuery = this._rawQuery === "" ? "" : "?" + this._rawQuery;
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
223
|
+
if (preset) {
|
|
224
|
+
// the registration's constants: two native crossings and their strings not asked for
|
|
225
|
+
this.path = preset.path;
|
|
226
|
+
this.originalUrl = preset.path + this.urlQuery;
|
|
227
|
+
this.url = this.originalUrl;
|
|
228
|
+
this._lastUrl = this.originalUrl;
|
|
229
|
+
this.endsWithSlash = preset.endsWithSlash;
|
|
230
|
+
this._opPath = preset.opPath;
|
|
231
|
+
this._originalPath = preset.path;
|
|
232
|
+
this.method = preset.method;
|
|
233
|
+
this._isOptions = preset.isOptions;
|
|
234
|
+
this._isHead = preset.isHead;
|
|
235
|
+
} else {
|
|
236
|
+
// getUrl() is the path already, so the query is joined on and then not split off
|
|
237
|
+
// again. Building originalUrl and picking the path back out of it with indexOf and
|
|
238
|
+
// substring was a search and a second string for something uWS had just handed over.
|
|
239
|
+
this.path = req.getUrl();
|
|
240
|
+
this.originalUrl = this.path + this.urlQuery;
|
|
241
|
+
this.url = this.originalUrl;
|
|
242
|
+
// what the router last wrote to req.url. A middleware assigning something else is a
|
|
243
|
+
// rewrite, which express honours, and dispatch compares against this to notice it
|
|
244
|
+
this._lastUrl = this.originalUrl;
|
|
245
|
+
// charCodeAt rather than indexing: s[i] builds a one character string to throw away
|
|
246
|
+
this.endsWithSlash = this.path.charCodeAt(this.path.length - 1) === 0x2f;
|
|
247
|
+
this._opPath = this.path;
|
|
248
|
+
this._originalPath = this.path;
|
|
249
|
+
if (this.endsWithSlash && this.path !== "/" && !this.app.get("strict routing")) {
|
|
250
|
+
this._opPath = this._opPath.slice(0, -1);
|
|
251
|
+
}
|
|
252
|
+
this.method = req.getCaseSensitiveMethod().toUpperCase();
|
|
253
|
+
this._isOptions = this.method === "OPTIONS";
|
|
254
|
+
this._isHead = this.method === "HEAD";
|
|
255
|
+
}
|
|
233
256
|
this.params = {};
|
|
234
257
|
|
|
235
258
|
// Two Sets per request, for two things almost no request needs.
|
|
@@ -258,25 +281,19 @@ module.exports = class Request extends Readable {
|
|
|
258
281
|
this.rawIp = this._res.getRemoteAddress();
|
|
259
282
|
}
|
|
260
283
|
|
|
261
|
-
|
|
262
|
-
//
|
|
263
|
-
// this
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
this.method === "PATCH" ||
|
|
268
|
-
this.method === "QUERY" ||
|
|
269
|
-
(additionalMethods && additionalMethods.includes(this.method)) ||
|
|
270
|
-
// any request that declares a body carries one, whatever the verb: a GET with
|
|
271
|
-
// content-length left unread would end this stream empty and poison the keep-alive
|
|
272
|
-
// connection with its unconsumed bytes. uWS itself discards GET bodies, so this is
|
|
273
|
-
// the node shim's path
|
|
274
|
-
/** @type {any} */ (this)._declaresBody
|
|
275
|
-
) {
|
|
284
|
+
// A body exists on the wire only when the request declares one, content-length or
|
|
285
|
+
// transfer-encoding, whatever the verb, and that evidence was spotted during the header
|
|
286
|
+
// copy. The verb list and the "body methods" settings read this branch used to pay per
|
|
287
|
+
// request said nothing the headers had not already said; the setting still gates the
|
|
288
|
+
// body parsers, which is where it matters
|
|
289
|
+
if (/** @type {any} */ (this)._declaresBody) {
|
|
276
290
|
this._subscribeBody();
|
|
277
291
|
} else {
|
|
278
292
|
this.receivedData = true;
|
|
279
|
-
|
|
293
|
+
// not pushed here: ending a Readable costs a scheduled tick and its bookkeeping,
|
|
294
|
+
// and on a bodyless request nobody may ever look. The null goes out from _read(),
|
|
295
|
+
// which is where every consumer arrives
|
|
296
|
+
this.#emptyBody = true;
|
|
280
297
|
}
|
|
281
298
|
}
|
|
282
299
|
|
|
@@ -319,6 +336,13 @@ module.exports = class Request extends Readable {
|
|
|
319
336
|
* lift the backpressure that a full queue put on it.
|
|
320
337
|
*/
|
|
321
338
|
_read() {
|
|
339
|
+
// first, so a bodyless stream still ends for a consumer that arrives after the
|
|
340
|
+
// response finished, which express allows
|
|
341
|
+
if (this.#emptyBody) {
|
|
342
|
+
this.#emptyBody = false;
|
|
343
|
+
this.push(null);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
322
346
|
if (this.#paused && !this.#responseEnded) {
|
|
323
347
|
this.#paused = false;
|
|
324
348
|
this._res.resume();
|
|
@@ -665,10 +689,10 @@ module.exports = class Request extends Readable {
|
|
|
665
689
|
const entries = this.#rawHeadersEntries;
|
|
666
690
|
for (let i = 0, len = entries.length; i < len; i += 2) {
|
|
667
691
|
const key = entries[i];
|
|
668
|
-
// 'if-none-match'.length === 13, 'if-modified-since'.length === 17
|
|
692
|
+
// 'if-none-match'.length === 13, 'if-modified-since'.length === 17; the
|
|
693
|
+
// entries carry lowercase names by contract, so this compares them as they are
|
|
669
694
|
if (key.length === 13 || key.length === 17) {
|
|
670
|
-
|
|
671
|
-
if (lower === "if-none-match" || lower === "if-modified-since") {
|
|
695
|
+
if (key === "if-none-match" || key === "if-modified-since") {
|
|
672
696
|
hasConditional = true;
|
|
673
697
|
break;
|
|
674
698
|
}
|
|
@@ -852,7 +876,8 @@ module.exports = class Request extends Readable {
|
|
|
852
876
|
const entries = this.#rawHeadersEntries;
|
|
853
877
|
for (let index = 0, len = entries.length; index < len; index += 2) {
|
|
854
878
|
const value = entries[index + 1];
|
|
855
|
-
|
|
879
|
+
// lowercase by the entries' contract, see the field declaration
|
|
880
|
+
const key = entries[index];
|
|
856
881
|
if (Object.hasOwn(headers, key)) {
|
|
857
882
|
if (discardedDuplicates.has(key)) {
|
|
858
883
|
continue;
|
package/src/response.js
CHANGED
|
@@ -175,6 +175,9 @@ module.exports = class Response extends Writable {
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
this.body = undefined;
|
|
178
|
+
// false while the uWS route handler is still in its synchronous window, where uWS holds
|
|
179
|
+
// the socket corked itself; the two uWS entry points flip it once that window closes
|
|
180
|
+
this._corkNeeded = false;
|
|
178
181
|
// shared methods, not arrows: two closures and a once() wrapper here were four
|
|
179
182
|
// allocations per request. EventEmitter calls listeners with this = the emitter.
|
|
180
183
|
this.on("error", this._onAbortError);
|
|
@@ -272,14 +275,17 @@ module.exports = class Response extends Writable {
|
|
|
272
275
|
if (!this.headersSent) {
|
|
273
276
|
this.writeHead(this.statusCode);
|
|
274
277
|
// "unknown" and not the bare number: node writes that reason phrase for a code it
|
|
275
|
-
// has no message for, so the raw status lines match
|
|
276
|
-
|
|
278
|
+
// has no message for, so the raw status lines match. The default 200 with no
|
|
279
|
+
// phrase is uWS's own head, byte for byte, so it is not written at all
|
|
280
|
+
if (this.statusCode !== 200 || this.statusText !== undefined) {
|
|
281
|
+
this._res.writeStatus(statusLine(this.statusCode, this.statusText));
|
|
282
|
+
}
|
|
277
283
|
this.writeHeaders(typeof chunk === "string");
|
|
278
284
|
}
|
|
279
285
|
|
|
280
286
|
if (!Buffer.isBuffer(chunk) && !(chunk instanceof ArrayBuffer)) {
|
|
287
|
+
// the Buffer view is enough, uWS reads its offset and length itself
|
|
281
288
|
chunk = Buffer.from(chunk);
|
|
282
|
-
chunk = chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength);
|
|
283
289
|
}
|
|
284
290
|
|
|
285
291
|
if (this.chunkedTransfer) {
|
|
@@ -475,47 +481,66 @@ module.exports = class Response extends Writable {
|
|
|
475
481
|
return this;
|
|
476
482
|
}
|
|
477
483
|
this.writeHead(this.statusCode);
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
484
|
+
// uWS holds the socket corked for the synchronous window of its route handler, and
|
|
485
|
+
// cork inside cork is a passthrough: the wrapper and its closure are only paid once the
|
|
486
|
+
// answer has outlived that window, which is what _corkNeeded records
|
|
487
|
+
if (this._corkNeeded) {
|
|
488
|
+
this._res.cork(() => this._finish(data, cb));
|
|
489
|
+
} else {
|
|
490
|
+
this._finish(data, cb);
|
|
491
|
+
}
|
|
492
|
+
return this;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* The corked tail of end(): status, headers, body and the finish events. Split out so a
|
|
497
|
+
* synchronous answer calls it straight, already inside uWS's own cork.
|
|
498
|
+
*
|
|
499
|
+
* @param {any} data
|
|
500
|
+
* @param {any} cb
|
|
501
|
+
*/
|
|
502
|
+
_finish(data, cb) {
|
|
503
|
+
if (!this.headersSent) {
|
|
504
|
+
// freshness is not decided here. node's end() knows nothing about conditional
|
|
505
|
+
// requests, and Express answers 304 from send() and from sendFile(), each of
|
|
506
|
+
// which strips the entity headers first. Deciding it here meant res.end("body")
|
|
507
|
+
// answered 304 and dropped the body that the caller had just written.
|
|
508
|
+
// "unknown" for a code without a message, as node's status line has it.
|
|
509
|
+
// The default 200 with no phrase is not written at all: uWS emits the identical
|
|
510
|
+
// "HTTP/1.1 200 OK" head on its own, and the crossing costs more than it says
|
|
511
|
+
if (this.statusCode !== 200 || this.statusText !== undefined) {
|
|
485
512
|
this._res.writeStatus(statusLine(this.statusCode, this.statusText));
|
|
486
|
-
this.writeHeaders(true);
|
|
487
513
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
514
|
+
this.writeHeaders(true);
|
|
515
|
+
}
|
|
516
|
+
const contentLength = this.headers["content-length"];
|
|
517
|
+
if (STATUSES_WITHOUT_BODY.has(this.statusCode) || this.statusCode < 200) {
|
|
518
|
+
// no body and no length describing one, whatever the caller passed. node decides
|
|
519
|
+
// this the same way, from the status alone, so res.status(304).end("x") sends the
|
|
520
|
+
// status and nothing else on either.
|
|
521
|
+
this._res.endWithoutBody();
|
|
522
|
+
} else if (!data && contentLength) {
|
|
523
|
+
this._res.endWithoutBody(contentLength.toString());
|
|
524
|
+
} else {
|
|
525
|
+
// a Buffer goes to uWS as the view it is: copying it into a fresh ArrayBuffer was
|
|
526
|
+
// an allocation per body, and uWS reads the view's own offset and length
|
|
527
|
+
if (this.req.method === "HEAD") {
|
|
528
|
+
const length = Buffer.byteLength(data ?? "");
|
|
529
|
+
this._res.endWithoutBody(length.toString());
|
|
496
530
|
} else {
|
|
497
|
-
|
|
498
|
-
data = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
499
|
-
}
|
|
500
|
-
if (this.req.method === "HEAD") {
|
|
501
|
-
const length = Buffer.byteLength(data ?? "");
|
|
502
|
-
this._res.endWithoutBody(length.toString());
|
|
503
|
-
} else {
|
|
504
|
-
this._res.end(data);
|
|
505
|
-
}
|
|
531
|
+
this._res.end(data);
|
|
506
532
|
}
|
|
533
|
+
}
|
|
507
534
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
});
|
|
518
|
-
return this;
|
|
535
|
+
this.finished = true;
|
|
536
|
+
this.#socket?.emit("close");
|
|
537
|
+
this.emit("finish");
|
|
538
|
+
this.emit("close");
|
|
539
|
+
cb &&
|
|
540
|
+
queueMicrotask(() => {
|
|
541
|
+
this.#ended = true;
|
|
542
|
+
cb();
|
|
543
|
+
});
|
|
519
544
|
}
|
|
520
545
|
|
|
521
546
|
/**
|
package/src/router.js
CHANGED
|
@@ -671,6 +671,30 @@ function onNativeAborted() {
|
|
|
671
671
|
response.socket?.emit("error", err);
|
|
672
672
|
}
|
|
673
673
|
|
|
674
|
+
/**
|
|
675
|
+
*
|
|
676
|
+
*/
|
|
677
|
+
/**
|
|
678
|
+
* The per-request constants of a fully literal native registration. µWS matched the URL byte for
|
|
679
|
+
* byte against this exact pattern and dispatches by method, so the request constructor can take
|
|
680
|
+
* these as given instead of asking uWS and recomputing them on every request.
|
|
681
|
+
*
|
|
682
|
+
* @param {string} path the registered pattern, which is what getUrl() would have answered
|
|
683
|
+
* @param {string} method uppercase, fixed by which uWS verb the registration used
|
|
684
|
+
* @param {boolean} strict the owner's strict routing, frozen here like the twin registration is
|
|
685
|
+
*/
|
|
686
|
+
function nativePreset(path, method, strict) {
|
|
687
|
+
const endsWithSlash = path.charCodeAt(path.length - 1) === 0x2f;
|
|
688
|
+
return {
|
|
689
|
+
path,
|
|
690
|
+
method,
|
|
691
|
+
endsWithSlash,
|
|
692
|
+
opPath: endsWithSlash && path !== "/" && !strict ? path.slice(0, -1) : path,
|
|
693
|
+
isOptions: method === "OPTIONS",
|
|
694
|
+
isHead: method === "HEAD"
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
674
698
|
/**
|
|
675
699
|
*
|
|
676
700
|
*/
|
|
@@ -1313,10 +1337,11 @@ module.exports = class Router extends EventEmitter {
|
|
|
1313
1337
|
*
|
|
1314
1338
|
* @param {any} res uWS response
|
|
1315
1339
|
* @param {any} req uWS request, readable only during this call
|
|
1340
|
+
* @param {any} [preset] a literal registration's constants, see nativePreset
|
|
1316
1341
|
* @returns {any} the request, with the response reachable as request.res
|
|
1317
1342
|
*/
|
|
1318
|
-
handleRequest(res, req) {
|
|
1319
|
-
const request = new this._request(req, res, this);
|
|
1343
|
+
handleRequest(res, req, preset) {
|
|
1344
|
+
const request = new this._request(req, res, this, preset);
|
|
1320
1345
|
const response = new this._response(res, request, this);
|
|
1321
1346
|
request.res = response;
|
|
1322
1347
|
response.req = request;
|
|
@@ -1381,7 +1406,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1381
1406
|
if (route.path.includes(":")) {
|
|
1382
1407
|
route.optimizedParams = route.path.match(regExParam).map((p) => p.slice(1));
|
|
1383
1408
|
}
|
|
1384
|
-
const makeHandler = (chain) => {
|
|
1409
|
+
const makeHandler = (chain, preset) => {
|
|
1385
1410
|
// all three are registration-time constants: computing them in the handler was a
|
|
1386
1411
|
// closure and a scan of the chain on every native request.
|
|
1387
1412
|
// Falling back resumes after the mount, not after the router's leaf: the leaf can have
|
|
@@ -1394,7 +1419,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1394
1419
|
// and this one never did. nativeDone and nativeFail defer their epilogues to a
|
|
1395
1420
|
// microtask, which is where the await used to resume, so the visible order holds
|
|
1396
1421
|
return (res, req) => {
|
|
1397
|
-
const request = this.handleRequest(res, req);
|
|
1422
|
+
const request = this.handleRequest(res, req, preset);
|
|
1398
1423
|
const response = request.res;
|
|
1399
1424
|
if (optimizedParams) {
|
|
1400
1425
|
request.optimizedParams = new NullObject();
|
|
@@ -1408,6 +1433,10 @@ module.exports = class Router extends EventEmitter {
|
|
|
1408
1433
|
} catch (err) {
|
|
1409
1434
|
// what a throw inside a promise executor did: reject, once
|
|
1410
1435
|
nativeFail.call(walk, err);
|
|
1436
|
+
} finally {
|
|
1437
|
+
// whatever runs after this line is outside the cork uWS held for this
|
|
1438
|
+
// callback, so later writes have to open their own
|
|
1439
|
+
response._corkNeeded = true;
|
|
1411
1440
|
}
|
|
1412
1441
|
};
|
|
1413
1442
|
};
|
|
@@ -1415,12 +1444,22 @@ module.exports = class Router extends EventEmitter {
|
|
|
1415
1444
|
// chain runs without re-matching the method, so the get registration must not see it
|
|
1416
1445
|
const getChain =
|
|
1417
1446
|
route.method === "GET" ? optimizedPath.filter((r) => r.all || r.method !== "HEAD") : optimizedPath;
|
|
1418
|
-
let fn = makeHandler(getChain);
|
|
1419
1447
|
route.optimizedPath = optimizedPath;
|
|
1420
1448
|
|
|
1449
|
+
// A fully literal registration knows path and method here, so each registration site
|
|
1450
|
+
// hands the request constructor its own constants. An "any" registration serves every
|
|
1451
|
+
// verb and a parameterised one matches paths it cannot spell, so both stay dynamic
|
|
1452
|
+
const canPreset = !route.optimizedParams && method !== "any";
|
|
1453
|
+
// the route's own router decides, not the app running the registration: a router created
|
|
1454
|
+
// with { strict: true } and mounted on an app without it does not answer /things/, and
|
|
1455
|
+
// registering that path here is the only way it could
|
|
1456
|
+
const strictHere = Boolean((route.owner ?? this).get("strict routing"));
|
|
1457
|
+
|
|
1458
|
+
let fn = makeHandler(getChain, canPreset ? nativePreset(route.path, route.method, strictHere) : undefined);
|
|
1459
|
+
const jsFn = fn;
|
|
1460
|
+
|
|
1421
1461
|
let replacedPath = route.path;
|
|
1422
|
-
const
|
|
1423
|
-
const headFn = getChain.length === optimizedPath.length ? realFn : makeHandler(optimizedPath);
|
|
1462
|
+
const headChain = getChain.length === optimizedPath.length ? getChain : optimizedPath;
|
|
1424
1463
|
|
|
1425
1464
|
// the response prototype the route will really run under: its own app's, which sees a
|
|
1426
1465
|
// method patched there or inherited from a parent app, falling back to the registering app
|
|
@@ -1443,17 +1482,29 @@ module.exports = class Router extends EventEmitter {
|
|
|
1443
1482
|
}
|
|
1444
1483
|
|
|
1445
1484
|
this.uwsApp[method](replacedPath, fn);
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1485
|
+
if (!strictHere && route.path[route.path.length - 1] !== "/") {
|
|
1486
|
+
// a declarative response answers the twin as itself; a preset handler cannot be
|
|
1487
|
+
// shared, since the twin's path is its own constant
|
|
1488
|
+
const slashFn =
|
|
1489
|
+
fn !== jsFn
|
|
1490
|
+
? fn
|
|
1491
|
+
: canPreset
|
|
1492
|
+
? makeHandler(getChain, nativePreset(route.path + "/", route.method, strictHere))
|
|
1493
|
+
: fn;
|
|
1494
|
+
this.uwsApp[method](replacedPath + "/", slashFn);
|
|
1451
1495
|
if (method === "get") {
|
|
1452
|
-
this.uwsApp.head(
|
|
1496
|
+
this.uwsApp.head(
|
|
1497
|
+
replacedPath + "/",
|
|
1498
|
+
makeHandler(headChain, canPreset ? nativePreset(route.path + "/", "HEAD", strictHere) : undefined)
|
|
1499
|
+
);
|
|
1453
1500
|
}
|
|
1454
1501
|
}
|
|
1455
1502
|
if (method === "get") {
|
|
1456
|
-
|
|
1503
|
+
// its own handler always: the shared one would carry the GET registration's method
|
|
1504
|
+
this.uwsApp.head(
|
|
1505
|
+
replacedPath,
|
|
1506
|
+
makeHandler(headChain, canPreset ? nativePreset(route.path, "HEAD", strictHere) : undefined)
|
|
1507
|
+
);
|
|
1457
1508
|
}
|
|
1458
1509
|
}
|
|
1459
1510
|
|