fulmine.js 5.0.0-rc.1

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/router.js ADDED
@@ -0,0 +1,1240 @@
1
+ /*
2
+ Copyright 2024 dimden.dev
3
+ Copyright 2026 Nigro Simone
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+ */
17
+
18
+ const {
19
+ patternToRegex,
20
+ getPatternMeta,
21
+ decodeParam,
22
+ needsConversionToRegex,
23
+ findIndexStartingFrom,
24
+ canBeOptimized,
25
+ canBeOptimizedWithParams,
26
+ pathsCanOverlap,
27
+ NullObject,
28
+ EMPTY_REGEX
29
+ } = require("./utils.js");
30
+ const Response = require("./response.js");
31
+ const Request = require("./request.js");
32
+ const { EventEmitter } = require("tseep");
33
+ const compileDeclarative = require("./declarative.js");
34
+ const statuses = require("statuses");
35
+ const { METHODS } = require("http");
36
+ const { isNodeRequest, serveNodeRequest } = require("./node-shim.js");
37
+
38
+ const resCodes = {},
39
+ resDecMethods = ["set", "setHeader", "header", "send", "end", "append", "status"];
40
+ for (const method of resDecMethods) {
41
+ resCodes[method] = Response.prototype[method].toString();
42
+ }
43
+
44
+ let routeKey = 0;
45
+
46
+ /**
47
+ * Hands the request and the response to the app about to handle them, so that a mounted sub-app's
48
+ * settings decide what its responses do. Express re-parents both objects for the same reason.
49
+ *
50
+ * @param {any} req
51
+ * @param {any} app
52
+ */
53
+ function useApp(req, app) {
54
+ req.app = app;
55
+ if (req.res) {
56
+ req.res.app = app;
57
+ }
58
+ }
59
+
60
+ // Every verb node knows about, which is the list the methods package hands Express, and "all" on
61
+ // top of it. Taken from node rather than written out: the written out one was missing acl, bind,
62
+ // link, rebind, source, unbind, unlink and unlock, and had four of the others twice.
63
+ //
64
+ // GET is left out on purpose. get() is declared in the class, because it doubles as the settings
65
+ // reader, and the loop at the end of this file would replace it.
66
+ const methods = ["all", ...METHODS.filter((method) => method !== "GET").map((method) => method.toLowerCase())];
67
+ const supportedUwsMethods = new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "CONNECT", "TRACE"]);
68
+
69
+ const regExParam = /:(\w+)/g;
70
+
71
+ // Internals here are _underscore and not #private: a callable router is a function with the
72
+ // router's properties copied onto it, and a # field cannot be copied, so #routes would throw
73
+ // "Cannot read private member" on the first call.
74
+
75
+ // one intermediate prototype per class, built the first time a callable of that class is made
76
+ const callablePrototypes = new WeakMap();
77
+
78
+ /**
79
+ * The prototype for a callable router or app: the class prototype, with apply and call put back.
80
+ *
81
+ * Setting a function's prototype to a class prototype drops Function.prototype from the chain, and
82
+ * node calls a request listener with handler.apply. An intermediate object, so express.application
83
+ * stays in the chain. constructor and bind are not restored: the code asks constructor.name, and
84
+ * BIND is an HTTP verb, so app.bind registers a route as it does in Express.
85
+ *
86
+ * @param {object} classPrototype
87
+ * @returns {object}
88
+ */
89
+ function callablePrototypeFor(classPrototype) {
90
+ let prototype = callablePrototypes.get(classPrototype);
91
+ if (prototype) {
92
+ return prototype;
93
+ }
94
+ prototype = Object.create(classPrototype);
95
+ for (const name of ["apply", "call", "toString"]) {
96
+ Object.defineProperty(prototype, name, {
97
+ value: /** @type {any} */ (Function.prototype)[name],
98
+ writable: true,
99
+ configurable: true,
100
+ enumerable: false
101
+ });
102
+ }
103
+ callablePrototypes.set(classPrototype, prototype);
104
+ return prototype;
105
+ }
106
+
107
+ /**
108
+ * The default error page, which is the one Express produces: the stack in a pre, and nothing else.
109
+ * What reaches it has already been redacted when the environment calls for it.
110
+ *
111
+ * @param {any} err
112
+ * @returns {string}
113
+ */
114
+ function generateErrorPageHtml(err) {
115
+ return (
116
+ `<!DOCTYPE html>\n` +
117
+ `<html lang="en">\n` +
118
+ `<head>\n` +
119
+ `<meta charset="utf-8">\n` +
120
+ `<title>Error</title>\n` +
121
+ `</head>\n` +
122
+ `<body>\n` +
123
+ `<pre>${err?.stack ?? err}</pre>\n` +
124
+ `</body>\n` +
125
+ `</html>\n`
126
+ );
127
+ }
128
+
129
+ module.exports = class Router extends EventEmitter {
130
+ parent;
131
+
132
+ listenCalled;
133
+
134
+ uwsApp;
135
+
136
+ /**
137
+ * @param {object} [settings] router options. caseSensitive and strict are accepted under the
138
+ * names Express's Router takes, and stored under the setting names the rest of the code reads
139
+ */
140
+ constructor(settings = {}) {
141
+ super();
142
+
143
+ this._paramCallbacks = new Map();
144
+ this._mountpathCache = new Map();
145
+ this._routes = [];
146
+ // an array when mounted on several paths at once, as Express allows
147
+ /** @type {string|string[]} */
148
+ this.mountpath = "/";
149
+ this.settings = settings;
150
+ this._request = Request;
151
+ this._response = Response;
152
+ this.request = this._request.prototype;
153
+ this.response = this._response.prototype;
154
+
155
+ if (typeof settings.caseSensitive !== "undefined") {
156
+ this.settings["case sensitive routing"] = settings.caseSensitive;
157
+ delete this.settings.caseSensitive;
158
+ }
159
+ if (typeof settings.strict !== "undefined") {
160
+ this.settings["strict routing"] = settings.strict;
161
+ delete this.settings.strict;
162
+ }
163
+
164
+ if (typeof this.settings["case sensitive routing"] === "undefined") {
165
+ this.settings["case sensitive routing"] = true;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * This router as middleware, which is what express.Router() hands back: a function carrying the
171
+ * router's own properties with the router's prototype behind it. The properties are the same
172
+ * objects, not copies, so the function and the instance are one router seen twice.
173
+ *
174
+ * @returns {any} the callable
175
+ */
176
+ _asCallable() {
177
+ // handle() comes from the prototype set below, which nothing can see from here
178
+ const fn = /** @type {any} */ (
179
+ function (req, res, next) {
180
+ return fn.handle(req, res, next);
181
+ }
182
+ );
183
+ Object.assign(fn, this);
184
+ Object.setPrototypeOf(fn, callablePrototypeFor(Object.getPrototypeOf(this)));
185
+ return fn;
186
+ }
187
+
188
+ /**
189
+ * Routes a request through this router, as Express's app.handle and router.handle do. next() is
190
+ * called when nothing answered, so an unmatched request goes back to whoever is running this.
191
+ *
192
+ * @param {any} req
193
+ * @param {any} res
194
+ * @param {(err?: any) => void} [next]
195
+ * @returns {Promise<void>}
196
+ */
197
+ async handle(req, res, next) {
198
+ // a request from node's own server, which is what http.createServer(app) delivers
199
+ if (isNodeRequest(req)) {
200
+ return serveNodeRequest(this, req, /** @type {any} */ (res), next);
201
+ }
202
+ // an app taking over a request becomes that request's app, as it does when mounted, so
203
+ // req.app.get("view engine") inside a sub-app reads the sub-app's settings and not the
204
+ // settings of whatever handed the request over. A plain router is not an app and leaves it
205
+ // alone, which is what Express's router.handle does too.
206
+ if (this.constructor.name === "Application") {
207
+ useApp(req, this);
208
+ }
209
+ const routed = await this._routeRequest(req, res, 0);
210
+ if (!routed && next) {
211
+ next();
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Two methods sharing a name, as in Express.
217
+ *
218
+ * With a string and no handlers it reads a setting, falling back to the parent router when
219
+ * this one does not have it. With handlers it registers a GET route. A GET route also
220
+ * answers HEAD.
221
+ *
222
+ * @param {string} path setting name, or route path
223
+ * @param {...(Function|Array<Function>)} callbacks handlers; none means read a setting
224
+ * @returns {*} the setting value, or the created route
225
+ */
226
+ get(path, ...callbacks) {
227
+ if (typeof path === "string" && callbacks.length === 0) {
228
+ const key = path;
229
+ const res = this.settings[key];
230
+ if (typeof res === "undefined" && this.parent) {
231
+ return this.parent.get(key);
232
+ } else {
233
+ return res;
234
+ }
235
+ }
236
+ return this.createRoute("GET", path, this, ...callbacks);
237
+ }
238
+
239
+ /**
240
+ * The pattern matching everything the mounts on this request have consumed so far, which is
241
+ * what a nested router strips off the path before matching against it. Cached per stack, since
242
+ * the same mount chain is walked by every request that reaches it.
243
+ *
244
+ * @param {any} req
245
+ * @returns {RegExp}
246
+ */
247
+ getFullMountpath(req) {
248
+ // path-less app.use() pushes "", so a stack of only those joins to "" no matter how deep it is.
249
+ // patternToRegex("", true) is EMPTY_REGEX, so this returns exactly what the join path would,
250
+ // without walking the whole stack on every hop
251
+ if (!req._stack.length || req._stackMounted === 0) {
252
+ return EMPTY_REGEX;
253
+ }
254
+ const fullStack = req._stack.join("");
255
+ let fullMountpath = this._mountpathCache.get(fullStack);
256
+ if (!fullMountpath) {
257
+ fullMountpath = patternToRegex(fullStack, true);
258
+ this._mountpathCache.set(fullStack, fullMountpath);
259
+ }
260
+ return fullMountpath;
261
+ }
262
+
263
+ /**
264
+ * Whether a route's path matches this request. A plain string compares directly, which is what
265
+ * makes a route eligible for the native router; anything carrying a parameter or a wildcard was
266
+ * turned into a regular expression when it was registered.
267
+ *
268
+ * @param {any} route
269
+ * @param {any} req
270
+ * @returns {boolean}
271
+ */
272
+ _pathMatches(route, req) {
273
+ let path = req._opPath;
274
+ let pattern = route.pattern;
275
+
276
+ if (req.endsWithSlash && path.endsWith("/") && !this.get("strict routing")) {
277
+ path = path.slice(0, -1);
278
+ }
279
+ // the line above turns the root path into the empty string, which no pattern is written
280
+ // against. A regex route was tested against it and app.get("*path") answered every request
281
+ // but "/"
282
+ if (path === "") {
283
+ path = "/";
284
+ }
285
+
286
+ if (typeof pattern === "string") {
287
+ if (pattern === "/*") {
288
+ return true;
289
+ }
290
+ if (!this.get("case sensitive routing")) {
291
+ path = path.toLowerCase();
292
+ pattern = pattern.toLowerCase();
293
+ }
294
+ return pattern === path;
295
+ }
296
+ if (pattern === EMPTY_REGEX) {
297
+ return true;
298
+ }
299
+ return pattern.test(path);
300
+ }
301
+
302
+ /**
303
+ * Registers a route, which every method helper and use() funnel into. Several paths at once
304
+ * become several routes sharing the callbacks, as Express allows. Paths are normalised here and
305
+ * not at match time: no trailing slash unless strict routing, "*" becomes "/{*splat}", and
306
+ * anything not comparable as a string is compiled to a regular expression and marked complex.
307
+ *
308
+ * @param {string} method HTTP method, or USE for a mount
309
+ * @param {any} path one path or several
310
+ * @param {any} [parent] what to return, so chaining lands on the app rather than the router
311
+ * @param {...any} callbacks
312
+ * @returns {any} parent
313
+ */
314
+ createRoute(method, path, parent = this, ...callbacks) {
315
+ method = method.toUpperCase();
316
+ callbacks = callbacks.flat();
317
+ const paths = Array.isArray(path) ? path : [path];
318
+ const routes = [];
319
+ for (let path of paths) {
320
+ if (!this.get("strict routing") && typeof path === "string" && path.endsWith("/") && path !== "/") {
321
+ path = path.slice(0, -1);
322
+ }
323
+ if (path === "*") {
324
+ path = "/{*splat}";
325
+ }
326
+ const route = {
327
+ method: method === "USE" ? "ALL" : method,
328
+ path,
329
+ pattern:
330
+ method === "USE" || needsConversionToRegex(path) ? patternToRegex(path, method === "USE") : path,
331
+ callbacks,
332
+ routeKey: routeKey++,
333
+ // the router this was registered on. Ordinary dispatch is done by that router, so
334
+ // it could ask itself, but an optimized chain is walked by the app whatever it
335
+ // contains, and param() callbacks belong to the router that declared them
336
+ owner: this,
337
+ // and its callbacks by reference, since dispatch asks for them on every hop of
338
+ // every request and param() only ever writes into this map, never replaces it.
339
+ // Reading them through owner measured 8 microseconds per thousand requests
340
+ paramCallbacks: this._paramCallbacks,
341
+ use: method === "USE",
342
+ all: method === "ALL" || method === "USE",
343
+ gettable: method === "GET" || method === "HEAD"
344
+ };
345
+ if (
346
+ typeof route.path === "string" &&
347
+ (route.path.includes(":") || route.path.includes("*") || route.path.includes("{")) &&
348
+ route.pattern instanceof RegExp
349
+ ) {
350
+ route.complex = true;
351
+ }
352
+ routes.push(route);
353
+ }
354
+ this._routes.push(...routes);
355
+
356
+ return parent;
357
+ }
358
+
359
+ /**
360
+ * The chain a request would walk to reach this route, or false when it cannot be known ahead of
361
+ * time. The native router jumps straight to the route, so everything registered before it that
362
+ * could also match has to be in the chain, in order.
363
+ *
364
+ * @param {any} route
365
+ * @param {any[]} routes every route of this router, in registration order
366
+ * @returns {any[]|false} the chain, ending in the route itself
367
+ */
368
+ _optimizeRoute(route, routes) {
369
+ const optimizedPath = [];
370
+
371
+ for (let i = 0; i < routes.length; i++) {
372
+ const r = routes[i];
373
+ if (r.routeKey > route.routeKey) {
374
+ break;
375
+ }
376
+ if (r === route) {
377
+ continue;
378
+ }
379
+ // if the methods are not the same, and its not an all method, skip it
380
+ if (!r.all && r.method !== route.method) {
381
+ // check if the methods are compatible (GET and HEAD)
382
+ if (!(r.method === "HEAD" && route.method === "GET")) {
383
+ continue;
384
+ }
385
+ }
386
+
387
+ // check if the paths match
388
+ if (
389
+ (r.pattern instanceof RegExp && r.pattern.test(route.path)) ||
390
+ (typeof r.pattern === "string" && (r.pattern === route.path || r.pattern === "/*"))
391
+ ) {
392
+ if (r.callbacks.some((c) => c instanceof Router)) {
393
+ return false; // cant optimize nested routers with matches
394
+ }
395
+ optimizedPath.push(r);
396
+ }
397
+ }
398
+ optimizedPath.push(route);
399
+
400
+ return optimizedPath;
401
+ }
402
+
403
+ /**
404
+ * Hands every route reachable by path alone to the native uWS router, walking into mounted
405
+ * routers and carrying their prefix down. Runs once, when the app starts listening, since it
406
+ * needs every route to have been registered first.
407
+ */
408
+ _compileOptimizedRoutes() {
409
+ if (!this.uwsApp || !this.get("case sensitive routing")) {
410
+ return;
411
+ }
412
+
413
+ // pathPrefix/chainPrefix accumulate across nested sole-callback mounts
414
+ const walk = (router, pathPrefix, chainPrefix) => {
415
+ for (const route of router._routes) {
416
+ if (route.use) {
417
+ // only sole-callback mounts
418
+ if (
419
+ !route.complex &&
420
+ canBeOptimized(route.path) &&
421
+ route.path !== "/*" &&
422
+ route.callbacks.length === 1 &&
423
+ route.callbacks[0] instanceof Router
424
+ ) {
425
+ let pathToMount = router._optimizeRoute(route, router._routes);
426
+ if (!pathToMount) {
427
+ continue;
428
+ }
429
+ pathToMount = pathToMount.slice(0, -1);
430
+ walk(route.callbacks[0], pathPrefix + route.path, [
431
+ ...chainPrefix,
432
+ ...pathToMount,
433
+ {
434
+ ...route,
435
+ callbacks: [],
436
+ keepMount: true,
437
+ // mounted sub-apps become req.app during their dispatch, like express
438
+ mountApp:
439
+ route.callbacks[0].constructor.name === "Application"
440
+ ? route.callbacks[0]
441
+ : undefined
442
+ }
443
+ ]);
444
+ }
445
+ // µWS picks by specificity and Express by registration order, so the chain
446
+ // computed for whichever route µWS lands on runs everything that could have
447
+ // matched before it
448
+ } else if (
449
+ (canBeOptimized(route.path) ||
450
+ // parameters that are whole segments are matched by µWS the same way
451
+ (canBeOptimizedWithParams(route.path) &&
452
+ // inside a mounted router, only when nothing after it could match
453
+ (!pathPrefix || !router._isFollowedByAnOverlap(route, router._routes)))) &&
454
+ supportedUwsMethods.has(route.method)
455
+ ) {
456
+ const leafPath = router._optimizeRoute(route, router._routes);
457
+ if (!leafPath) {
458
+ continue;
459
+ }
460
+ // param route earlier in the same router would steal this static path
461
+ if (leafPath.length > 1) {
462
+ const shadow = leafPath[leafPath.length - 2];
463
+ if (
464
+ shadow &&
465
+ !shadow.use &&
466
+ shadow.method === route.method &&
467
+ shadow.path !== route.path &&
468
+ shadow.pattern instanceof RegExp
469
+ ) {
470
+ continue;
471
+ }
472
+ }
473
+ if (pathPrefix) {
474
+ this._registerUwsRoute(
475
+ {
476
+ ...route,
477
+ path: pathPrefix + route.path,
478
+ pattern: pathPrefix + route.path,
479
+ optimizedRouter: true
480
+ },
481
+ [...chainPrefix, ...leafPath]
482
+ );
483
+ } else {
484
+ this._registerUwsRoute(route, leafPath);
485
+ }
486
+ }
487
+ }
488
+ };
489
+
490
+ walk(this, "", []);
491
+ }
492
+
493
+ /**
494
+ * Wraps a uWS request and response in ours and links them, which is the first thing every
495
+ * request does whichever path serves it.
496
+ *
497
+ * @param {any} res uWS response
498
+ * @param {any} req uWS request, readable only during this call
499
+ * @returns {{request: any, response: any}}
500
+ */
501
+ handleRequest(res, req) {
502
+ const request = new this._request(req, res, this);
503
+ const response = new this._response(res, request, this);
504
+ request.res = response;
505
+ response.req = request;
506
+ res.onAborted(() => {
507
+ /** @type {NodeJS.ErrnoException} */
508
+ const err = new Error("Connection closed");
509
+ err.code = "ECONNRESET";
510
+ response.aborted = true;
511
+ response.finished = true;
512
+ response.socket?.emit("error", err);
513
+ });
514
+
515
+ return { request, response };
516
+ }
517
+
518
+ /**
519
+ * Whether a route registered later in the same router could match a path this one matches.
520
+ *
521
+ * A route inside a mounted router may only go to µWS when the answer is no: a native chain that
522
+ * runs out resumes after the mount, not inside the router, so a later sibling would be lost. A
523
+ * mount or a pattern of an unknown shape counts as an overlap; two paths µWS could match itself
524
+ * are compared segment by segment.
525
+ *
526
+ * @param {any} route
527
+ * @param {any[]} routes every route of the router this one belongs to
528
+ * @returns {boolean}
529
+ */
530
+ _isFollowedByAnOverlap(route, routes) {
531
+ for (let i = routes.length - 1; i >= 0; i--) {
532
+ const later = routes[i];
533
+ if (later.routeKey <= route.routeKey) {
534
+ return false;
535
+ }
536
+ // a different verb cannot answer the same request, unless it answers every verb
537
+ if (!later.all && !later.use && later.method !== route.method) {
538
+ continue;
539
+ }
540
+ if (later.use) {
541
+ return true;
542
+ }
543
+ if (typeof later.path === "string" && canBeOptimizedWithParams(later.path)) {
544
+ if (pathsCanOverlap(route.path, later.path)) {
545
+ return true;
546
+ }
547
+ continue;
548
+ }
549
+ return true;
550
+ }
551
+ return false;
552
+ }
553
+
554
+ /**
555
+ * Hands one route to µWS, along with the chain of everything that has to run in front of it,
556
+ * and records that chain on the route so the handler can walk it.
557
+ *
558
+ * @param {any} route
559
+ * @param {any[]} optimizedPath the routes to run, in order, ending with this one
560
+ */
561
+ _registerUwsRoute(route, optimizedPath) {
562
+ let method = route.method.toLowerCase();
563
+ if (method === "all") {
564
+ method = "any";
565
+ } else if (method === "delete") {
566
+ method = "del";
567
+ }
568
+ if (route.path.includes(":")) {
569
+ route.optimizedParams = route.path.match(regExParam).map((p) => p.slice(1));
570
+ }
571
+ let fn = async (res, req) => {
572
+ const { request, response } = this.handleRequest(res, req);
573
+ if (route.optimizedParams) {
574
+ request.optimizedParams = new NullObject();
575
+ for (let i = 0; i < route.optimizedParams.length; i++) {
576
+ request.optimizedParams[route.optimizedParams[i]] = req.getParameter(i);
577
+ }
578
+ }
579
+ // falling back resumes after the mount, not after the router's leaf: the leaf can have a
580
+ // lower routeKey than the parent's middlewares, and an error handler declared before the
581
+ // mount must not catch what the router threw
582
+ const mount = optimizedPath.find((r) => r.keepMount);
583
+ const skipUntil = mount ?? (optimizedPath.length ? optimizedPath[optimizedPath.length - 1] : route);
584
+ const matchedRoute = await this._routeRequest(request, response, 0, optimizedPath, true, skipUntil);
585
+ if (!matchedRoute && !response.headersSent && !response.aborted) {
586
+ this._endUnmatched(request, response);
587
+ }
588
+ };
589
+ route.optimizedPath = optimizedPath;
590
+
591
+ let replacedPath = route.path;
592
+ const realFn = fn;
593
+
594
+ // check if route is declarative
595
+ if (
596
+ optimizedPath.length === 1 && // must not have middlewares
597
+ route.callbacks.length === 1 && // must not have multiple callbacks
598
+ typeof route.callbacks[0] === "function" && // must be a function
599
+ route.paramCallbacks.size === 0 && // a param callback has to run, and this answers without running anything
600
+ !resDecMethods.some((method) => resCodes[method] !== this.response[method].toString()) && // must not have injected methods
601
+ this.get("declarative responses") // must have declarative responses enabled
602
+ ) {
603
+ const decRes = compileDeclarative(route.callbacks[0], this);
604
+ if (decRes) {
605
+ fn = decRes;
606
+ }
607
+ } else {
608
+ replacedPath = route.path.replace(regExParam, ":x");
609
+ }
610
+
611
+ this.uwsApp[method](replacedPath, fn);
612
+ // the route's own router decides, not the app running the registration: a router created
613
+ // with { strict: true } and mounted on an app without it does not answer /things/, and
614
+ // registering that path here is the only way it could
615
+ if (!(route.owner ?? this).get("strict routing") && route.path[route.path.length - 1] !== "/") {
616
+ this.uwsApp[method](replacedPath + "/", fn);
617
+ if (method === "get") {
618
+ this.uwsApp.head(replacedPath + "/", realFn);
619
+ }
620
+ }
621
+ if (method === "get") {
622
+ this.uwsApp.head(replacedPath, realFn);
623
+ }
624
+ }
625
+
626
+ /**
627
+ * Gives an error to the handler that asked for it, or answers with it when there is none.
628
+ * Passing something to next() from an error handler clears the error and resumes routing,
629
+ * which is how Express lets a handler decide the error was not fatal.
630
+ *
631
+ * @param {any} err
632
+ * @param {Function|null} handler the four-argument handler to call, or null for the default
633
+ * @param {any} request
634
+ * @param {any} response
635
+ */
636
+ _handleError(err, handler, request, response) {
637
+ if (handler) {
638
+ return handler(err, request, response, (pass) => {
639
+ delete request._error;
640
+ delete request._errorKey;
641
+ return request.next(pass);
642
+ });
643
+ }
644
+ console.error(err);
645
+ if (response.statusCode === 200) {
646
+ response.statusCode = 500;
647
+ }
648
+ this._sendErrorPage(request, response, err, true);
649
+ }
650
+
651
+ /**
652
+ * The HTML for an error, which in production says only what the status means rather than what
653
+ * went wrong, so a stack trace does not reach the client.
654
+ *
655
+ * @param {any} err
656
+ * @param {number} statusCode
657
+ * @param {boolean} [checkEnv] whether production should redact it
658
+ * @returns {string}
659
+ */
660
+ _generateErrorPage(err, statusCode, checkEnv = false) {
661
+ if (checkEnv && this.get("env") === "production") {
662
+ err =
663
+ statusCode >= 400 ? (statuses.message[statusCode] ?? "Internal Server Error") : "Internal Server Error";
664
+ }
665
+ return generateErrorPageHtml(err);
666
+ }
667
+
668
+ /**
669
+ * @param {import("./utils.js").PathRegExp} pattern
670
+ * @param {string} path
671
+ */
672
+ _extractParams(pattern, path) {
673
+ let match = pattern.exec(path);
674
+ if (!match && path.length > 1 && path.endsWith("/")) {
675
+ // a pattern compiled without a trailing slash still matches a path written with one,
676
+ // which is what non-strict routing means. Retried rather than stripped up front, so
677
+ // that a wildcard captures the path as it arrived and "/a/b/" keeps its last, empty
678
+ // segment the way Express reports it
679
+ match = pattern.exec(path.slice(0, -1));
680
+ }
681
+ // Object.create(null) rather than the { __proto__: null } literal, which is the same object
682
+ // for 9ns more. Null-prototyped either way, as Express 5 makes params.
683
+ const obj = Object.create(null);
684
+ if (!match?.groups) {
685
+ return obj;
686
+ }
687
+
688
+ const groups = match.groups;
689
+ const meta = getPatternMeta(pattern);
690
+ if (meta === undefined) {
691
+ // a RegExp the application supplied itself, which was never compiled here
692
+ for (const name in groups) {
693
+ const value = groups[name];
694
+ if (value === undefined) {
695
+ continue;
696
+ }
697
+ obj[name] = decodeParam(value);
698
+ }
699
+ return obj;
700
+ }
701
+
702
+ // asking for each name in turn rather than walking the groups object, which is a
703
+ // null-prototype dictionary and slow to enumerate, and reading the wildcard answer that was
704
+ // worked out when the pattern was compiled instead of searching an array for it
705
+ const { paramNames, isWildcard } = meta;
706
+ for (let i = 0, len = paramNames.length; i < len; i++) {
707
+ const name = paramNames[i];
708
+ const value = groups[name];
709
+ // an optional group that did not match is absent in v5, not present as undefined
710
+ if (value === undefined) {
711
+ continue;
712
+ }
713
+ // a wildcard is an array of segments in v5, and each segment is decoded on its own so
714
+ // that an encoded slash inside one stays inside it
715
+ obj[name] = isWildcard[i] ? value.split("/").map(decodeParam) : decodeParam(value);
716
+ }
717
+ return obj;
718
+ }
719
+
720
+ /**
721
+ * Fills in what a route needs before its handlers run: req.route, req.params from the pattern
722
+ * and from any mergeParams parents, and the app.param callbacks for the parameters this route
723
+ * matched that this request has not already seen.
724
+ *
725
+ * @param {any} req
726
+ * @param {any} res
727
+ * @param {any} route
728
+ * @returns {any} a promise only when a param callback is involved
729
+ */
730
+ _preprocessRequest(req, res, route) {
731
+ req.route = route;
732
+ // both, not the route flag alone: the flag says the route was registered natively, the
733
+ // values say this request came in that way
734
+ if (route.optimizedParams && req.optimizedParams) {
735
+ req.params = Object.create(null);
736
+ try {
737
+ // µWS hands back the raw text, as the regex does, so both paths decode here
738
+ for (const name in req.optimizedParams) {
739
+ req.params[name] = decodeParam(req.optimizedParams[name]);
740
+ }
741
+ } catch (err) {
742
+ req._error = err;
743
+ req._errorKey = route.routeKey;
744
+ return "route";
745
+ }
746
+ } else if (route.complex) {
747
+ let path = req._originalPath;
748
+ if (req._stack.length > 0) {
749
+ const fullMountpath = this.getFullMountpath(req);
750
+ if (fullMountpath !== EMPTY_REGEX) {
751
+ path = path.replace(fullMountpath, "");
752
+ }
753
+ }
754
+ try {
755
+ req.params = this._extractParams(route.pattern, path);
756
+ } catch (err) {
757
+ // a parameter that will not decode. Express throws out of the match and lets the
758
+ // error reach the error handler, which answers 400, so the route is skipped rather
759
+ // than run with a value nobody can read.
760
+ req._error = err;
761
+ req._errorKey = route.routeKey;
762
+ return "route";
763
+ }
764
+ if (req._paramStack.length > 0) {
765
+ for (const params of req._paramStack) {
766
+ req.params = Object.assign(Object.create(null), params, req.params);
767
+ }
768
+ }
769
+ } else {
770
+ req.params = {};
771
+ if (req._paramStack.length > 0) {
772
+ for (const params of req._paramStack) {
773
+ req.params = Object.assign(Object.create(null), params, req.params);
774
+ }
775
+ }
776
+ }
777
+
778
+ // the route's own router's callbacks: an optimized chain is walked by the app even when it
779
+ // ends in a mounted router's route
780
+ const paramCallbacks = route.paramCallbacks;
781
+ if (paramCallbacks.size > 0) {
782
+ // known issue: an async executor swallows what it throws, so a param callback that
783
+ // throws synchronously is lost. Fixing it moves when the callbacks run
784
+ // eslint-disable-next-line no-async-promise-executor
785
+ return new Promise(async (resolve) => {
786
+ for (const param in req.params) {
787
+ const pcs = paramCallbacks.get(param);
788
+ // built here rather than for every request, since only an application using
789
+ // app.param() ever reaches this line
790
+ if (pcs && !req._gotParams?.has(param)) {
791
+ (req._gotParams ??= new Set()).add(param);
792
+ for (let i = 0, len = pcs.length; i < len; i++) {
793
+ const fn = pcs[i];
794
+ await /** @type {Promise<void>} */ (
795
+ new Promise((resolveRoute) => {
796
+ const next = (thingamabob) => {
797
+ if (thingamabob) {
798
+ if (thingamabob === "route") {
799
+ return resolve("route");
800
+ } else {
801
+ req._error = thingamabob;
802
+ req._errorKey = route.routeKey;
803
+ }
804
+ }
805
+ return resolveRoute();
806
+ };
807
+ req.next = next;
808
+ fn(req, res, next, req.params[param], param);
809
+ })
810
+ );
811
+ }
812
+ }
813
+ }
814
+
815
+ resolve(true);
816
+ });
817
+ }
818
+ return true;
819
+ }
820
+
821
+ /**
822
+ * Registers a callback that runs whenever a route parameter of this name is matched, before
823
+ * the route's own handlers, once per request per parameter.
824
+ *
825
+ * @example
826
+ * app.param("id", (req, res, next, value) => { req.user = lookup(value); next(); });
827
+ *
828
+ * @param {string|string[]} name parameter name, or several
829
+ * @param {(req: object, res: object, next: Function, value: string, name: string) => void} fn
830
+ * @returns {this} the router, for chaining
831
+ * @throws {TypeError} if name is neither a string nor an array
832
+ */
833
+ param(name, fn) {
834
+ // the message has to read exactly like this: it is the one the router package throws,
835
+ // and it is what reaches anyone catching it
836
+ if (typeof name !== "string" && !Array.isArray(name)) {
837
+ throw new TypeError("argument name must be a string");
838
+ }
839
+ const names = Array.isArray(name) ? name : [name];
840
+ for (const key of names) {
841
+ if (!this._paramCallbacks.has(key)) {
842
+ this._paramCallbacks.set(key, []);
843
+ }
844
+ this._paramCallbacks.get(key).push(fn);
845
+ }
846
+ return this;
847
+ }
848
+
849
+ /**
850
+ * Resolves with the route that answered, or false when nothing matched.
851
+ * @returns {Promise<any>}
852
+ */
853
+ _routeRequest(req, res, startIndex = 0, routes = this._routes, skipCheck = false, skipUntil) {
854
+ return new Promise((resolve, reject) => {
855
+ this._dispatchRoute(req, res, startIndex, routes, skipCheck, skipUntil, resolve, reject);
856
+ });
857
+ }
858
+
859
+ /**
860
+ * Finds the next route that matches and runs it, carrying the same resolve and reject the whole
861
+ * way. next() calls this again for the route after, so a chain of N middlewares costs one
862
+ * promise instead of N nested ones.
863
+ *
864
+ * @param {any} req
865
+ * @param {any} res
866
+ * @param {number} startIndex where to resume the scan
867
+ * @param {any[]} routes
868
+ * @param {boolean} skipCheck take the route at startIndex without matching it, which is how an
869
+ * already-decided chain is walked
870
+ * @param {any} skipUntil route to resume after when this chain runs out, or undefined
871
+ * @param {(value: any) => void} resolve
872
+ * @param {(err: any) => void} reject
873
+ */
874
+ _dispatchRoute(req, res, startIndex, routes, skipCheck, skipUntil, resolve, reject) {
875
+ const routeIndex = skipCheck
876
+ ? startIndex
877
+ : findIndexStartingFrom(
878
+ routes,
879
+ (r) =>
880
+ (r.all || r.method === req.method || req._isOptions || (r.gettable && req._isHead)) &&
881
+ this._pathMatches(r, req),
882
+ startIndex
883
+ );
884
+ const route = routes[routeIndex];
885
+ if (!route) {
886
+ if (!skipCheck) {
887
+ // on normal unoptimized routes, if theres no match then there is no route
888
+ return resolve(false);
889
+ }
890
+ // the chain ran out, so ordinary routing takes over from the top and skips what has
891
+ // already run
892
+ useApp(req, this);
893
+ // a chain that went into a mount never left it, since keepMount stops the pop, so the
894
+ // path is still relative to it. /alone/skip must not be offered to the app as /skip
895
+ if (req._stack.length > 0) {
896
+ req._stack.length = 0;
897
+ req._stackMounted = 0;
898
+ req.path = req._originalPath;
899
+ req.url = req._originalPath + req.urlQuery;
900
+ req._opPath =
901
+ req.endsWithSlash && req._originalPath !== "/" && !this.get("strict routing")
902
+ ? req._originalPath.slice(0, -1)
903
+ : req._originalPath;
904
+ }
905
+ // an error out of a mount is attributed to the mount, so error handlers declared before
906
+ // it do not catch it, as in ordinary dispatch
907
+ if (req._error && skipUntil && skipUntil.keepMount && skipUntil.routeKey > req._errorKey) {
908
+ req._errorKey = skipUntil.routeKey;
909
+ }
910
+ return this._dispatchRoute(req, res, 0, this._routes, false, skipUntil, resolve, reject);
911
+ }
912
+
913
+ // _preprocessRequest returns a promise only when there are param callbacks, so the common
914
+ // case stays synchronous. A microtask every 300 routes resets the stack, which a long chain
915
+ // would otherwise blow
916
+ const continueRoute = this._preprocessRequest(req, res, route);
917
+ if (route.paramCallbacks.size !== 0 || req.routeCount % 300 === 0) {
918
+ Promise.resolve(continueRoute).then(
919
+ (resumed) =>
920
+ this._runRoute(req, res, routeIndex, route, routes, skipCheck, skipUntil, resolve, reject, resumed),
921
+ reject
922
+ );
923
+ return;
924
+ }
925
+ return this._runRoute(
926
+ req,
927
+ res,
928
+ routeIndex,
929
+ route,
930
+ routes,
931
+ skipCheck,
932
+ skipUntil,
933
+ resolve,
934
+ reject,
935
+ continueRoute
936
+ );
937
+ }
938
+
939
+ /**
940
+ * Runs one route's callbacks, one after another through next(). A mount adjusts req.url,
941
+ * req.path and the mount stack on the way in, and puts them back if next("route") leaves it.
942
+ *
943
+ * @param {any} req
944
+ * @param {any} res
945
+ * @param {number} routeIndex
946
+ * @param {any} route
947
+ * @param {any[]} routes
948
+ * @param {boolean} skipCheck
949
+ * @param {any} skipUntil
950
+ * @param {(value: any) => void} resolve
951
+ * @param {(err: any) => void} reject
952
+ * @param {any} continueRoute
953
+ */
954
+ _runRoute(req, res, routeIndex, route, routes, skipCheck, skipUntil, resolve, reject, continueRoute) {
955
+ let callbackindex = 0;
956
+ if (route.use) {
957
+ if (route.mountApp) {
958
+ // optimized chain: normal dispatch swaps req.app when it enters a mounted Application,
959
+ // but the compiled mount route has no callback to do it, so swap it here
960
+ useApp(req, route.mountApp);
961
+ }
962
+ req._stack.push(route.path);
963
+ // a use with no path consumes nothing, so everything below would work out the values
964
+ // that are already there. Only skipped without a trailing slash, where the rules about
965
+ // one cannot bite. An application is mostly pathless middleware, and this is per hop
966
+ if (route.path !== "" || req.endsWithSlash) {
967
+ if (route.path !== "") {
968
+ req._stackMounted++;
969
+ }
970
+ const fullMountpath = this.getFullMountpath(req);
971
+ req._opPath =
972
+ fullMountpath !== EMPTY_REGEX ? req._originalPath.replace(fullMountpath, "") : req._originalPath;
973
+ if (req.endsWithSlash && req._opPath[req._opPath.length - 1] !== "/") {
974
+ req._opPath = this.get("strict routing") ? req._opPath + "/" : req._opPath.slice(0, -1);
975
+ }
976
+ req.url = req._opPath + req.urlQuery;
977
+ req.path = req._opPath;
978
+ if (req._opPath === "") {
979
+ req.url = "/";
980
+ req.path = "/";
981
+ }
982
+ }
983
+ }
984
+ // plain (non-async) function: an async next() would allocate an unconsumed promise
985
+ // on every middleware/handler step of every request
986
+ const next = (thingamabob) => {
987
+ if (thingamabob) {
988
+ if (thingamabob === "route") {
989
+ if (route.use && !route.keepMount) {
990
+ if (req._stack.pop() !== "") {
991
+ req._stackMounted--;
992
+ }
993
+
994
+ const strictRouting = this.get("strict routing");
995
+ const poppedMountpath = req._stack.length > 0 ? this.getFullMountpath(req) : EMPTY_REGEX;
996
+ req._opPath =
997
+ poppedMountpath !== EMPTY_REGEX
998
+ ? req._originalPath.replace(poppedMountpath, "")
999
+ : req._originalPath;
1000
+ if (strictRouting) {
1001
+ if (req.endsWithSlash && req._opPath[req._opPath.length - 1] !== "/") {
1002
+ req._opPath += "/";
1003
+ }
1004
+ }
1005
+ req.url = req._opPath + req.urlQuery;
1006
+ req.path = req._opPath;
1007
+ if (req._opPath === "") {
1008
+ req.url = "/";
1009
+ req.path = "/";
1010
+ }
1011
+ if (
1012
+ !strictRouting &&
1013
+ req.endsWithSlash &&
1014
+ req._originalPath !== "/" &&
1015
+ req._opPath[req._opPath.length - 1] === "/"
1016
+ ) {
1017
+ req._opPath = req._opPath.slice(0, -1);
1018
+ }
1019
+ if (req.app.parent && route.callbacks[0]?.constructor.name === "Application") {
1020
+ useApp(req, req.app.parent);
1021
+ }
1022
+ }
1023
+ req.routeCount++;
1024
+ // _dispatchRoute is a plain function, so a synchronous throw would escape here instead
1025
+ // of rejecting, like it used to when this recursed through the async _routeRequest
1026
+ try {
1027
+ return this._dispatchRoute(
1028
+ req,
1029
+ res,
1030
+ routeIndex + 1,
1031
+ routes,
1032
+ skipCheck,
1033
+ skipUntil,
1034
+ resolve,
1035
+ reject
1036
+ );
1037
+ } catch (err) {
1038
+ return reject(err);
1039
+ }
1040
+ } else {
1041
+ req._error = thingamabob;
1042
+ req._errorKey = route.routeKey;
1043
+ }
1044
+ }
1045
+ const callback = route.callbacks[callbackindex++];
1046
+ if (!callback) {
1047
+ return next("route");
1048
+ }
1049
+ // skipping routes we already went through via optimized path. Before the Router branch
1050
+ // below and not after it: a mount whose chain was compiled has already run, and running
1051
+ // it again would answer from inside the router a request that had just left it
1052
+ if (!skipCheck && skipUntil && skipUntil.routeKey >= route.routeKey) {
1053
+ return next();
1054
+ }
1055
+ if (callback instanceof Router) {
1056
+ if (callback.constructor.name === "Application") {
1057
+ useApp(req, callback);
1058
+ }
1059
+ if (callback.settings.mergeParams) {
1060
+ req._paramStack.push(req.params);
1061
+ }
1062
+ if (
1063
+ callback.settings["strict routing"] &&
1064
+ req.endsWithSlash &&
1065
+ req._opPath[req._opPath.length - 1] !== "/"
1066
+ ) {
1067
+ req._opPath += "/";
1068
+ }
1069
+ callback._routeRequest(req, res, 0).then((routed) => {
1070
+ if (req._error) {
1071
+ req._errorKey = route.routeKey;
1072
+ }
1073
+ if (routed) return resolve(true);
1074
+ if (req._isOptions && req._matchedMethods.size) {
1075
+ // OPTIONS routing is different, it stops in the router if matched
1076
+ return resolve(false);
1077
+ }
1078
+ next();
1079
+ });
1080
+ } else {
1081
+ // handle errors and error handlers
1082
+ if (req._error || callback.length === 4) {
1083
+ if (req._error && callback.length === 4 && route.routeKey >= req._errorKey) {
1084
+ return this._handleError(req._error, callback, req, res);
1085
+ } else {
1086
+ return next();
1087
+ }
1088
+ }
1089
+
1090
+ try {
1091
+ // handling OPTIONS method
1092
+ if (req._isOptions && !route.all && route.method !== "OPTIONS") {
1093
+ req._matchedMethods.add(route.method);
1094
+ if (route.gettable) {
1095
+ req._matchedMethods.add("HEAD");
1096
+ }
1097
+ return next();
1098
+ }
1099
+
1100
+ const out = callback(req, res, next);
1101
+ if (out instanceof Promise) {
1102
+ // Express 5 forwards a rejected handler promise to the error middleware on
1103
+ // its own, so there is nothing left for the "catch async errors" setting or
1104
+ // for express-async-errors to opt into
1105
+ out.catch((err) => {
1106
+ req._error = err;
1107
+ req._errorKey = route.routeKey;
1108
+ return next();
1109
+ });
1110
+ }
1111
+ } catch (err) {
1112
+ req._error = err;
1113
+ req._errorKey = route.routeKey;
1114
+ return next();
1115
+ }
1116
+ }
1117
+ };
1118
+ req.next = next;
1119
+ if (continueRoute === "route") {
1120
+ next("route");
1121
+ } else if (continueRoute) {
1122
+ next();
1123
+ } else {
1124
+ resolve(true);
1125
+ }
1126
+ }
1127
+
1128
+ /**
1129
+ * Mounts middleware, or a whole router, at a path. The path is optional, and a mount matches
1130
+ * everything under it, which is what separates it from all(). Mounting a Router sets its
1131
+ * mountpath and parent and emits 'mount' on it.
1132
+ *
1133
+ * @param {string|string[]|Function|Router|Array<Function|Router>} [path] mount path, or the
1134
+ * first handler
1135
+ * @param {...(Function|Router|Array<Function|Router>)} callbacks handlers, nested arrays allowed
1136
+ * @returns {this} the router, for chaining
1137
+ */
1138
+ use(path, ...callbacks) {
1139
+ if (
1140
+ typeof path === "function" ||
1141
+ path instanceof Router ||
1142
+ (Array.isArray(path) && path.every((p) => typeof p === "function" || p instanceof Router))
1143
+ ) {
1144
+ callbacks.unshift(path);
1145
+ path = "";
1146
+ }
1147
+ if (path === "/") {
1148
+ path = "";
1149
+ }
1150
+ callbacks = callbacks.flat();
1151
+
1152
+ for (const callback of callbacks) {
1153
+ if (callback instanceof Router) {
1154
+ callback.mountpath = /** @type {string|string[]} */ (path);
1155
+ callback.parent = this;
1156
+ callback.emit("mount", this);
1157
+ }
1158
+ }
1159
+ this.createRoute("USE", path, this, ...callbacks);
1160
+ return this;
1161
+ }
1162
+
1163
+ /**
1164
+ * A builder for one path, so the path is written once and the verbs chain off it.
1165
+ *
1166
+ * @example
1167
+ * app.route("/book").get(list).post(create);
1168
+ *
1169
+ * @param {string} path the path every verb on the returned object registers against
1170
+ * @returns {object} an object with one method per HTTP verb, each returning it again
1171
+ */
1172
+ route(path) {
1173
+ const fns = new NullObject();
1174
+ for (const method of methods) {
1175
+ fns[method] = (...callbacks) => {
1176
+ return this.createRoute(method, path, /** @type {any} */ (fns), ...callbacks);
1177
+ };
1178
+ }
1179
+ fns.get = (...callbacks) => {
1180
+ return this.createRoute("GET", path, /** @type {any} */ (fns), ...callbacks);
1181
+ };
1182
+ return fns;
1183
+ }
1184
+
1185
+ /**
1186
+ * Answers with an error page, locked down: no sniffing, no ETag, and a content security policy
1187
+ * that allows nothing, since the page carries a message that came from somewhere else.
1188
+ *
1189
+ * @param {any} request
1190
+ * @param {any} response
1191
+ * @param {any} err
1192
+ * @param {boolean} [checkEnv] whether production should redact it
1193
+ */
1194
+ _sendErrorPage(request, response, err, checkEnv = false) {
1195
+ err = this._generateErrorPage(err, response.statusCode, checkEnv);
1196
+ request.noEtag = true;
1197
+ response.setHeader("Content-Type", "text/html; charset=utf-8");
1198
+ response.setHeader("X-Content-Type-Options", "nosniff");
1199
+ response.setHeader("Content-Security-Policy", "default-src 'none'");
1200
+ response.send(err);
1201
+ }
1202
+
1203
+ /**
1204
+ * How a request that nothing answered ends: with the error it carries, with the automatic
1205
+ * OPTIONS reply, or with a 404. The native chain, the app's catch-all handler and the node shim
1206
+ * all end here, so that they end a request the same way.
1207
+ *
1208
+ * @param {any} request
1209
+ * @param {any} response
1210
+ */
1211
+ _endUnmatched(request, response) {
1212
+ if (request._error) {
1213
+ return this._handleError(request._error, null, request, response);
1214
+ }
1215
+ if (request._isOptions && request._matchedMethods.size > 0) {
1216
+ // Express 5 sorts the methods and joins them with ", ", so the header reads the same
1217
+ // regardless of the order the routes happened to be registered in
1218
+ const allowedMethods = Array.from(request._matchedMethods).sort().join(", ");
1219
+ response.setHeader("Allow", allowedMethods);
1220
+ // the router package answers this one itself, with a plain-text body, the nosniff
1221
+ // header and end() rather than send(), so no ETag comes with it
1222
+ response.setHeader("Content-Type", "text/plain");
1223
+ response.setHeader("X-Content-Type-Options", "nosniff");
1224
+ response.end(allowedMethods);
1225
+ return;
1226
+ }
1227
+ response.status(404);
1228
+ // the whole path, not what a mount left behind in req.path
1229
+ this._sendErrorPage(request, response, `Cannot ${request.method} ${request._originalPath}`, false);
1230
+ }
1231
+ };
1232
+
1233
+ // The verb methods go on the prototype, not on each instance. As own arrows they closed over the
1234
+ // instance they were built on, so express.Router().post(...) answered with the object the callable
1235
+ // was copied from. One closure per name for the process instead of one per router, too.
1236
+ for (const method of methods) {
1237
+ module.exports.prototype[method] = function (path, ...callbacks) {
1238
+ return this.createRoute(method, path, this, ...callbacks);
1239
+ };
1240
+ }