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.
@@ -0,0 +1,598 @@
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
+ /** @typedef {import("./router.js")} Router */
21
+ /** @typedef {import("./router-utils.js").RouteEntry} RouteEntry */
22
+
23
+ const {
24
+ patternToRegex,
25
+ needsConversionToRegex,
26
+ canBeOptimized,
27
+ canBeOptimizedWithParams,
28
+ pathsCanOverlap,
29
+ uwsPrefersEarlier,
30
+ NullObject
31
+ } = require("./utils.js");
32
+ const Walk = require("./walk.js");
33
+ const compileDeclarative = require("./declarative.js");
34
+ const { chainUsage } = require("./usage.js");
35
+ const {
36
+ HAS_LETTER,
37
+ anyGuardHits,
38
+ resCodes,
39
+ resDecMethods,
40
+ nativeDone,
41
+ nativeFail,
42
+ nativePreset,
43
+ hasErrorMiddleware,
44
+ couldAnswer,
45
+ shadowsLeaf,
46
+ guardsInside,
47
+ supportedUwsMethods,
48
+ regExParam
49
+ } = require("./router-utils.js");
50
+
51
+ // router.js requires this file while it is still being evaluated, so its class cannot be required
52
+ // from here: it hands it over on the line under its own export instead.
53
+ let Router;
54
+
55
+ /**
56
+ * @param {any} cls the Router class, passed in to keep this module out of its require cycle
57
+ */
58
+ function useRouterClass(cls) {
59
+ Router = cls;
60
+ }
61
+
62
+ /**
63
+ * The chain a request would walk to reach this route, or false when it cannot be known ahead of
64
+ * time. The native router jumps straight to the route, so everything registered before it that
65
+ * could also match has to be in the chain, in order.
66
+ *
67
+ * @param {Router} router
68
+ * @param {RouteEntry} route
69
+ * @param {any[]} routes every route of this router, in registration order
70
+ * @returns {any[]|false} the chain, ending in the route itself
71
+ */
72
+ function optimizeRoute(router, route, routes) {
73
+ const optimizedPath = [];
74
+ // a route with a parameter matches paths its own text does not, so what an earlier route
75
+ // could answer is compared shape against shape and not against that text
76
+ const withParams = typeof route.path === "string" && route.path.includes(":");
77
+ // under insensitive routing two paths that differ only in case answer the same requests, so
78
+ // the text comparisons below run on the folded form. uWS still matches bytes: a request in the
79
+ // registered case takes the chain, any other case takes the fallback
80
+ const caseSensitive = router._caseSensitive();
81
+ const routePathFolded = caseSensitive || typeof route.path !== "string" ? route.path : route.path.toLowerCase();
82
+ // whether this route answers only the path as written, or the one with a trailing slash too
83
+ const strictHere = (route.owner ?? router)._strictRouting();
84
+ /** @type {string[]|null} earlier literals a case variant could smuggle a request past */
85
+ let caseGuards = null;
86
+
87
+ for (let i = 0; i < routes.length; i++) {
88
+ const r = routes[i];
89
+ if (r.routeKey > route.routeKey) {
90
+ break;
91
+ }
92
+ if (r === route) {
93
+ continue;
94
+ }
95
+ // if the methods are not the same, and its not an all method, skip it
96
+ if (!r.all && r.method !== route.method) {
97
+ // check if the methods are compatible (GET and HEAD)
98
+ if (!(r.method === "HEAD" && route.method === "GET")) {
99
+ // A mount is registered ALL, because what lives under it can answer any method,
100
+ // and this chain is computed once for all of them. So an earlier route of another
101
+ // method belongs in the chain of the leaves that share its method and in no other,
102
+ // which one chain cannot say: uWS would jump to a leaf as if the earlier route did
103
+ // not exist. Leave the mount to ordinary dispatch, where express's order decides.
104
+ if (route.use && typeof route.path === "string" && couldAnswer(r, route.path)) {
105
+ return false;
106
+ }
107
+ continue;
108
+ }
109
+ }
110
+
111
+ // The same rule as above, reached by another road. A mount's chain is inherited by every
112
+ // path under it, and a route that is not a mount answers the mount point rather than the
113
+ // subtree: in the chain it ran for the whole of it, so router.all("/:p1") answered the
114
+ // /posts/a-b of the router mounted at /posts. guardsInside is written for this.
115
+ if (route.use && !r.use && typeof route.path === "string" && couldAnswer(r, route.path)) {
116
+ return false;
117
+ }
118
+
119
+ // a RegExp mount runs only where its match starts the path and breaks on a separator,
120
+ // which is decidable here against a literal path and not against one with a parameter
121
+ if (r.regexMount) {
122
+ const matched = typeof route.path === "string" ? r.pattern.exec(route.path) : null;
123
+ const runsAlways =
124
+ matched !== null &&
125
+ !matched[0].includes(":") &&
126
+ route.path.slice(0, matched[0].length) === matched[0] &&
127
+ (route.path.length === matched[0].length || route.path[matched[0].length] === "/");
128
+ if (runsAlways) {
129
+ if (r.callbacks.some((c) => c instanceof Router)) {
130
+ return false;
131
+ }
132
+ optimizedPath.push(r);
133
+ continue;
134
+ }
135
+ // it may still answer some of the paths this route matches, and the chain has no
136
+ // way to say "only sometimes"
137
+ if (matched !== null || withParams) {
138
+ return false;
139
+ }
140
+ continue;
141
+ }
142
+
143
+ // check if the paths match. A route with parameters is excluded from the text test:
144
+ // its literal ":name" text would let an earlier regex in on requests it never matches.
145
+ const regexCanMatch = r.pattern instanceof RegExp && (!withParams || r.use);
146
+ if (
147
+ (regexCanMatch && r.pattern.test(route.path)) ||
148
+ (typeof r.pattern === "string" &&
149
+ (r.pattern === route.path ||
150
+ (!caseSensitive && r.pattern.toLowerCase() === routePathFolded) ||
151
+ r.pattern === "/*"))
152
+ ) {
153
+ if (r.callbacks.some((c) => c instanceof Router)) {
154
+ return false; // cant optimize nested routers with matches
155
+ }
156
+ optimizedPath.push(r);
157
+ continue;
158
+ }
159
+ // Without strict routing this registration answers "/x/" as well as "/x". An earlier
160
+ // pattern matching only the second answers part of what the registration takes, which the
161
+ // chain cannot say: it runs what is in it without matching again. So
162
+ // app.all("/:p0/{:o1}/{:o2}") answered a GET /list/Mixed belonging to the route after it.
163
+ if (regexCanMatch && !strictHere && r.pattern.test(route.path + "/")) {
164
+ return false;
165
+ }
166
+ if (!withParams) {
167
+ continue;
168
+ }
169
+ // an earlier route that answers only some of the paths this one matches cannot go in
170
+ // the chain, which runs what is in it without matching again
171
+ if (typeof r.path !== "string" || !canBeOptimizedWithParams(r.path)) {
172
+ return false;
173
+ }
174
+ const rPathFolded = caseSensitive ? r.path : r.path.toLowerCase();
175
+ if (!pathsCanOverlap(rPathFolded, routePathFolded, r.use)) {
176
+ continue;
177
+ }
178
+ if (r.use) {
179
+ return false;
180
+ }
181
+ // the same path lands on the same µWS registration, so the earlier route runs first
182
+ // from inside the chain, under its own parameter names; a case variant of it lands on
183
+ // its own registration, whose chain was computed the same way, or on the fallback
184
+ if (rPathFolded === routePathFolded) {
185
+ if (r.callbacks.some((c) => c instanceof Router)) {
186
+ return false;
187
+ }
188
+ optimizedPath.push(r);
189
+ continue;
190
+ }
191
+ // otherwise the two overlap only where µWS itself hands the request to the earlier,
192
+ // more specific registration, so this chain never sees those paths
193
+ if (
194
+ !r.optimizedPath ||
195
+ !uwsPrefersEarlier(r.path, route.path) ||
196
+ (!caseSensitive && route.path !== routePathFolded)
197
+ ) {
198
+ return false;
199
+ }
200
+ // that argument is about bytes. Under insensitive routing "/POSTS" byte-matches no
201
+ // registration of "/posts", so uWS hands it here, where this chain would answer as if the
202
+ // earlier route did not exist. The literal is remembered so the registration can send those
203
+ // requests to the generic router. A path with no letter has no other case to arrive in
204
+ if (!caseSensitive && HAS_LETTER.test(r.path)) {
205
+ (caseGuards ??= []).push(r.path);
206
+ }
207
+ }
208
+ optimizedPath.push(route);
209
+ route._caseGuards = caseGuards;
210
+
211
+ return optimizedPath;
212
+ }
213
+
214
+ /**
215
+ * Hands every route reachable by path alone to the native uWS router, walking into mounted
216
+ * routers and carrying their prefix down. Runs once, when the app starts listening, since it
217
+ * needs every route to have been registered first.
218
+ *
219
+ * @param {any} root the application whose routes are being compiled
220
+ */
221
+ function compileOptimizedRoutes(root) {
222
+ if (!root.uwsApp) {
223
+ return;
224
+ }
225
+ // Everything below is what makes this framework fast, and every decision it takes claims that
226
+ // uWS answering by itself gives the same answer the chain would. Turned off, the claim is not
227
+ // made. `npm run fuzz -- --self` serves one application both ways and compares the answers.
228
+ if (root.get("native routes") === false) {
229
+ return;
230
+ }
231
+
232
+ // pathPrefix/chainPrefix accumulate across nested sole-callback mounts, and outerGuards
233
+ // carries what was written before them and answers only part of what is under them
234
+ const walk = (router, pathPrefix, chainPrefix, outerGuards) => {
235
+ for (const route of router._routes) {
236
+ if (route.use) {
237
+ // only sole-callback mounts. Case rules do not gate the walk: each level's
238
+ // _optimizeRoute guards its own routes under its own setting, and a request in
239
+ // another case takes the fallback, which honours the child's setting
240
+ if (
241
+ !route.complex &&
242
+ canBeOptimized(route.path) &&
243
+ route.path !== "/*" &&
244
+ route.callbacks.length === 1 &&
245
+ route.callbacks[0] instanceof Router
246
+ ) {
247
+ let pathToMount = router._optimizeRoute(route, router._routes);
248
+ if (!pathToMount) {
249
+ route._whyGeneric = "something before it in the same router overlaps its paths";
250
+ continue;
251
+ }
252
+ pathToMount = pathToMount.slice(0, -1);
253
+ const guards = guardsInside(router, route, pathPrefix, pathToMount, outerGuards);
254
+ if (guards === null) {
255
+ route._whyGeneric = "a path written before it cannot be read segment by segment";
256
+ continue;
257
+ }
258
+ route._walkedInto = true;
259
+ walk(
260
+ route.callbacks[0],
261
+ pathPrefix + route.path,
262
+ [
263
+ ...chainPrefix,
264
+ ...pathToMount,
265
+ {
266
+ ...route,
267
+ callbacks: [],
268
+ callbackKinds: [],
269
+ keepMount: true,
270
+ // mounted sub-apps become req.app during their dispatch, like express
271
+ mountApp:
272
+ route.callbacks[0].constructor.name === "Application"
273
+ ? route.callbacks[0]
274
+ : undefined
275
+ }
276
+ ],
277
+ guards
278
+ );
279
+ } else {
280
+ // said once here rather than at each condition above: a mount is walked into
281
+ // only when µWS can match its path on its own and it carries exactly one
282
+ // router, and those are the two things worth telling anyone about
283
+ route._whyGeneric = !(route.callbacks.length === 1 && route.callbacks[0] instanceof Router)
284
+ ? "it is middleware rather than a single mounted router"
285
+ : "µWS cannot match this mount path on its own";
286
+ }
287
+ // µWS picks by specificity and Express by registration order, so the chain
288
+ // computed for whichever route µWS lands on runs everything that could have
289
+ // matched before it
290
+ } else if (
291
+ // parameters that are whole segments are matched by µWS the same way
292
+ (canBeOptimized(route.path) || canBeOptimizedWithParams(route.path)) &&
293
+ // Inside a mounted router, only when nothing after it could answer the same path.
294
+ // Asked of literal routes too, not only parameter ones: uWS picks by specificity
295
+ // where Express picks by registration order, and a chain carries only what runs in
296
+ // front of its route, so `router.get("/a", (req, res, next) => next())` before
297
+ // `router.get("/:x", ...)` left the mount and answered 404. Found by the fuzzer,
298
+ // replay with --seed 221940161 --rounds 1.
299
+ (!pathPrefix || !router._isFollowedByAnOverlap(route, router._routes)) &&
300
+ supportedUwsMethods.has(route.method)
301
+ ) {
302
+ // something outside this router, written before the mount it is in, that could
303
+ // answer this exact path. µWS would jump here and never give it its turn
304
+ if (outerGuards.length > 0 && typeof route.path === "string") {
305
+ const absolute = pathPrefix + route.path;
306
+ const guard = outerGuards.find((g) => shadowsLeaf(g, absolute, route));
307
+ if (guard) {
308
+ route._whyGeneric = `${guard.path} is written before the mount it is in and answers the same paths`;
309
+ continue;
310
+ }
311
+ }
312
+ const leafPath = router._optimizeRoute(route, router._routes);
313
+ if (!leafPath) {
314
+ route._whyGeneric = "something before it in the same router overlaps its paths";
315
+ continue;
316
+ }
317
+ // param route earlier in the same router would steal this static path
318
+ if (leafPath.length > 1) {
319
+ const shadow = leafPath[leafPath.length - 2];
320
+ if (
321
+ shadow &&
322
+ !shadow.use &&
323
+ shadow.method === route.method &&
324
+ shadow.path !== route.path &&
325
+ shadow.pattern instanceof RegExp
326
+ ) {
327
+ route._whyGeneric = `the parameter route ${shadow.path} is written before it`;
328
+ continue;
329
+ }
330
+ }
331
+ // the prefix goes in whether or not the mount had a path: a pathless mount
332
+ // adds nothing to the path and everything to the chain, the middlewares in
333
+ // front of it and the mount entry that says where to resume
334
+ const chain = chainPrefix.length > 0 ? [...chainPrefix, ...leafPath] : leafPath;
335
+ if (pathPrefix) {
336
+ const registered = {
337
+ ...route,
338
+ path: pathPrefix + route.path,
339
+ pattern: pathPrefix + route.path,
340
+ optimizedRouter: true
341
+ };
342
+ if (route._caseGuards) {
343
+ // compared against the whole path µWS matched, so they carry the mount
344
+ // prefix, folded along with the rest of it
345
+ registered._caseGuards = route._caseGuards.map((p) => pathPrefix + p);
346
+ }
347
+ root._registerUwsRoute(registered, chain);
348
+ // the chain holds the original object, so the request-time guard has to find
349
+ // the computed fields there, or a mounted param route extracts its params
350
+ // twice. The names match: the prefix is static and adds no parameter
351
+ route.optimizedParams = registered.optimizedParams;
352
+ route.optimizedPath = registered.optimizedPath;
353
+ // and what was decided about it, for the same reason: the copy is thrown
354
+ // away and the profile reads the route the application actually holds
355
+ route._native = registered._native;
356
+ } else {
357
+ root._registerUwsRoute(route, chain);
358
+ }
359
+ } else if (!supportedUwsMethods.has(route.method)) {
360
+ route._whyGeneric = `µWS does not serve ${route.method}`;
361
+ } else if (canBeOptimized(route.path) || canBeOptimizedWithParams(route.path)) {
362
+ // eligible but for the overlap test, which only applies inside a mount
363
+ route._whyGeneric = "a route after it in the same mounted router could answer the same paths";
364
+ } else {
365
+ route._whyGeneric = "µWS cannot match this path on its own";
366
+ }
367
+ }
368
+ };
369
+
370
+ walk(root, "", [], []);
371
+ }
372
+
373
+ /**
374
+ * Hands one route to µWS, along with the chain of everything that has to run in front of it,
375
+ * and records that chain on the route so the handler can walk it.
376
+ *
377
+ * @param {Router} router
378
+ * @param {RouteEntry} route
379
+ * @param {any[]} optimizedPath the routes to run, in order, ending with this one
380
+ */
381
+ function registerUwsRoute(router, route, optimizedPath) {
382
+ let method = route.method.toLowerCase();
383
+ if (method === "all") {
384
+ method = "any";
385
+ } else if (method === "delete") {
386
+ method = "del";
387
+ }
388
+ if (route.path.includes(":")) {
389
+ route.optimizedParams = route.path.match(regExParam).map((p) => p.slice(1));
390
+ }
391
+ // null for almost every route: only a parameter route with an earlier literal that a case
392
+ // variant could slip past carries one, see _optimizeRoute. Built once here, and matched
393
+ // insensitively, since that is the folding the guard exists for
394
+ const caseGuards = route._caseGuards
395
+ ? route._caseGuards.map((p) => (needsConversionToRegex(p) ? patternToRegex(p, false, false) : p.toLowerCase()))
396
+ : null;
397
+ const makeHandler = (chain, preset, skips, wireMethod) => {
398
+ // the mutable object a granted skip lives on, so a middleware arriving after listen can
399
+ // take it back: a literal registration's preset doubles as it, a parameterised one gets a
400
+ // holder of its own. It carries the method too, so the constructor settles it in one compare
401
+ let skipHolder = preset;
402
+ if (skipHolder === undefined && (skips.skipHeaders || skips.skipQuery || wireMethod !== null)) {
403
+ skipHolder = {
404
+ skipHeaders: skips.skipHeaders,
405
+ skipQuery: skips.skipQuery,
406
+ method: wireMethod,
407
+ isOptions: wireMethod === "OPTIONS",
408
+ isHead: wireMethod === "HEAD"
409
+ };
410
+ if (skips.skipHeaders || skips.skipQuery) {
411
+ (router._skipPresets ??= new Set()).add(skipHolder);
412
+ }
413
+ }
414
+ // all three are registration-time constants: computing them in the handler was a
415
+ // closure and a scan of the chain on every native request.
416
+ // Falling back resumes after the mount, not after the router's leaf: the leaf can have
417
+ // a lower routeKey than the parent's middlewares, and an error handler declared before
418
+ // the mount must not catch what the router threw
419
+ const mount = chain.find((r) => r.keepMount);
420
+ const skipUntil = mount ?? chain[chain.length - 1];
421
+ const optimizedParams = route.optimizedParams;
422
+ // not async, and no _routeRequest: its promise pair exists for callers that await,
423
+ // and this one never did. nativeDone and nativeFail defer their epilogues to a
424
+ // microtask, which is where the await used to resume, so the visible order holds
425
+ return (res, req) => {
426
+ // a request that is an earlier literal in another case: express answers it with
427
+ // that route, and the chain here does not contain it, so the generic router takes
428
+ // this one
429
+ if (caseGuards !== null && anyGuardHits(caseGuards, req.getUrl())) {
430
+ // an application is what registers native routes, and only it serves
431
+ return /** @type {any} */ (router)._serveGeneric(res, req);
432
+ }
433
+ const request = router.handleRequest(res, req, preset, skipHolder);
434
+ const response = request.res;
435
+ if (request._mustRefuse === true) {
436
+ return router._refuseRequest(response);
437
+ }
438
+ if (optimizedParams) {
439
+ // slicing these out of the already-fetched path instead measured a wash:
440
+ // the segment scan costs what the crossing costs
441
+ request.optimizedParams = new NullObject();
442
+ for (let i = 0; i < optimizedParams.length; i++) {
443
+ request.optimizedParams[optimizedParams[i]] = req.getParameter(i);
444
+ }
445
+ }
446
+ const walk = new Walk(router, request, response, chain, true, skipUntil, nativeDone, nativeFail);
447
+ try {
448
+ walk.dispatch(0);
449
+ } catch (err) {
450
+ // what a throw inside a promise executor did: reject, once
451
+ nativeFail.call(walk, err);
452
+ } finally {
453
+ // whatever runs after this line is outside the cork uWS held for this
454
+ // callback, so later writes have to open their own
455
+ response._corkNeeded = true;
456
+ // an abort can only arrive after this callback returns, so a response that
457
+ // already finished inside it never needs uWS told at all
458
+ if (!response.finished) {
459
+ router._armAbort(res, response);
460
+ }
461
+ }
462
+ };
463
+ };
464
+ // a HEAD route may sit in a GET route's chain so the head registration runs it, but a
465
+ // chain runs without re-matching the method, so the get registration must not see it
466
+ const getChain = route.method === "GET" ? optimizedPath.filter((r) => r.all || r.method !== "HEAD") : optimizedPath;
467
+ route.optimizedPath = optimizedPath;
468
+ const headChain = getChain.length === optimizedPath.length ? getChain : optimizedPath;
469
+
470
+ // A fully literal registration knows path and method here, so each registration site
471
+ // hands the request constructor its own constants. An "any" registration serves every
472
+ // verb and a parameterised one matches paths it cannot spell, so both stay dynamic
473
+ const canPreset = !route.optimizedParams && method !== "any";
474
+ // the route's own router decides, not the app running the registration: a router created
475
+ // with { strict: true } and mounted on an app without it does not answer /things/, and
476
+ // registering that path here is the only way it could
477
+ const strictHere = (route.owner ?? router)._strictRouting();
478
+
479
+ // Whether requests served by this registration may skip the header copy: GET and its HEAD twins
480
+ // only, no error middleware anywhere (a throw hands the request to code the analysis never
481
+ // saw), and every callback in the chain has to pass usage.js, whose default answer is no.
482
+ //
483
+ // The etag setting is not a condition. It used to be, because send consults freshness, but the
484
+ // skip branch reads if-none-match and if-modified-since by name whatever the setting, see the
485
+ // comment in request.js, and req.fresh reads nothing else. Requiring etag off as well cost the
486
+ // copy to every application that left it on.
487
+ const NO_SKIPS = { skipHeaders: false, skipQuery: false };
488
+ let getSkips = NO_SKIPS;
489
+ let headSkips = NO_SKIPS;
490
+ if (route.method === "GET") {
491
+ let hasErr = router._hasErrMwCache;
492
+ if (hasErr === undefined) {
493
+ hasErr = router._hasErrMwCache = hasErrorMiddleware(router);
494
+ }
495
+ if (!hasErr) {
496
+ // a terminal next() may only fall into the framework's own 404, so no later
497
+ // route may be able to catch the same path
498
+ const owner = route.owner ?? router;
499
+ const noLaterMatch = !owner._isFollowedByAnOverlap.call(owner, route, owner._routes);
500
+ getSkips = chainUsage(getChain, noLaterMatch);
501
+ headSkips = headChain === getChain ? getSkips : chainUsage(headChain, noLaterMatch);
502
+ }
503
+ }
504
+ // remembered so a middleware or setting arriving after listen can take the skips back
505
+ const makePreset = (path, method, skips) => {
506
+ const preset = nativePreset(path, method);
507
+ if (skips.skipHeaders || skips.skipQuery) {
508
+ preset.skipHeaders = skips.skipHeaders;
509
+ preset.skipQuery = skips.skipQuery;
510
+ (router._skipPresets ??= new Set()).add(preset);
511
+ }
512
+ return preset;
513
+ };
514
+
515
+ // the wire token this registration answers; "any" serves every verb and stays dynamic
516
+ const wireMethod = method === "any" ? null : route.method;
517
+ let fn = makeHandler(
518
+ getChain,
519
+ canPreset ? makePreset(route.path, route.method, getSkips) : undefined,
520
+ getSkips,
521
+ wireMethod
522
+ );
523
+ const jsFn = fn;
524
+
525
+ let replacedPath = route.path;
526
+
527
+ // the response prototype the route will really run under: its own app's, which sees a
528
+ // method patched there or inherited from a parent app, falling back to the registering app
529
+ const responseProto = /** @type {any} */ (route.owner)?.response ?? /** @type {any} */ (router).response;
530
+ // check if route is declarative
531
+ if (
532
+ optimizedPath.length === 1 && // must not have middlewares
533
+ route.callbacks.length === 1 && // must not have multiple callbacks
534
+ typeof route.callbacks[0] === "function" && // must be a function
535
+ route.paramCallbacks.size === 0 && // a param callback has to run, and this answers without running anything
536
+ // a captured value is decoded when the route runs, and one that cannot be decoded is a
537
+ // 400 in express and on the ordinary path here. Nothing runs to raise it on a
538
+ // declarative response, so GET /a-b%5Ec@d%e came back 200 from app.get("/:p12")
539
+ route.optimizedParams === undefined &&
540
+ // a declarative response is answered by µWS itself, so no javascript runs and the case
541
+ // guard could not: a route that needs one has to stay an ordinary handler
542
+ caseGuards === null &&
543
+ !resDecMethods.some((method) => resCodes[method] !== responseProto[method].toString()) && // must not have injected methods
544
+ router.get("declarative responses") // must have declarative responses enabled
545
+ ) {
546
+ const decRes = compileDeclarative(route.callbacks[0], router);
547
+ if (decRes) {
548
+ fn = decRes;
549
+ }
550
+ } else {
551
+ replacedPath = route.path.replace(regExParam, ":x");
552
+ }
553
+
554
+ // what listen() settled about this route, kept so `npx fulmine profile` can print it rather
555
+ // than making anyone read the source or instrument it. Written once, during compilation,
556
+ // so no request pays for it
557
+ route._native = {
558
+ path: replacedPath,
559
+ declarative: fn !== jsFn,
560
+ skipHeaders: getSkips.skipHeaders === true,
561
+ skipQuery: getSkips.skipQuery === true,
562
+ ahead: optimizedPath.length - 1,
563
+ guards: caseGuards ? caseGuards.length : 0
564
+ };
565
+
566
+ router.uwsApp[method](replacedPath, fn);
567
+ if (!strictHere && route.path[route.path.length - 1] !== "/") {
568
+ // a declarative response answers the twin as itself; a preset handler cannot be
569
+ // shared, since the twin's path is its own constant
570
+ const slashFn =
571
+ fn !== jsFn
572
+ ? fn
573
+ : canPreset
574
+ ? makeHandler(getChain, makePreset(route.path + "/", route.method, getSkips), getSkips, wireMethod)
575
+ : fn;
576
+ router.uwsApp[method](replacedPath + "/", slashFn);
577
+ if (method === "get") {
578
+ router.uwsApp.head(
579
+ replacedPath + "/",
580
+ makeHandler(
581
+ headChain,
582
+ canPreset ? makePreset(route.path + "/", "HEAD", headSkips) : undefined,
583
+ headSkips,
584
+ "HEAD"
585
+ )
586
+ );
587
+ }
588
+ }
589
+ if (method === "get") {
590
+ // its own handler always: the shared one would carry the GET registration's method
591
+ router.uwsApp.head(
592
+ replacedPath,
593
+ makeHandler(headChain, canPreset ? makePreset(route.path, "HEAD", headSkips) : undefined, headSkips, "HEAD")
594
+ );
595
+ }
596
+ }
597
+
598
+ module.exports = { useRouterClass, optimizeRoute, compileOptimizedRoutes, registerUwsRoute };
@@ -37,9 +37,9 @@ const plusRegex = /\+/g;
37
37
  * `capture` collects the decoded pairs flat, key then value, so a caller can replay the stores
38
38
  * without scanning again; a repeated key marks it invalid instead. See `get query`.
39
39
  *
40
- * `separatorLimit` refuses a body with that many "&" separators the way body-parser's
41
- * parameterCount does, but inside this scan instead of a scan of its own: the overflow flag on
42
- * the function is set, the partial result is to be discarded, and the caller answers 413.
40
+ * `separatorLimit` refuses a body with that many "&" separators like body-parser's parameterCount,
41
+ * inside this scan: the overflow flag is set, the partial result is discarded, the caller answers
42
+ * 413.
43
43
  *
44
44
  * @param {string} input
45
45
  * @param {string[] & {invalid?: boolean}} [capture]