routup 5.2.0 → 6.0.0-beta.0
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 +68 -47
- package/dist/bun.d.mts +3 -3
- package/dist/bun.mjs +4 -4
- package/dist/bun.mjs.map +1 -1
- package/dist/cloudflare.d.mts +3 -3
- package/dist/cloudflare.mjs +4 -4
- package/dist/cloudflare.mjs.map +1 -1
- package/dist/deno.d.mts +3 -3
- package/dist/deno.mjs +4 -4
- package/dist/deno.mjs.map +1 -1
- package/dist/generic.d.mts +3 -3
- package/dist/generic.mjs +4 -4
- package/dist/generic.mjs.map +1 -1
- package/dist/index-kxLRw2Wc.d.mts +1950 -0
- package/dist/node.d.mts +4 -4
- package/dist/node.mjs +6 -6
- package/dist/node.mjs.map +1 -1
- package/dist/service-worker.d.mts +3 -3
- package/dist/service-worker.mjs +4 -4
- package/dist/service-worker.mjs.map +1 -1
- package/dist/{src-DX0rndew.mjs → src-gmPicCWT.mjs} +1507 -468
- package/dist/src-gmPicCWT.mjs.map +1 -0
- package/package.json +6 -4
- package/dist/index-DdsCL8RI.d.mts +0 -1159
- package/dist/src-DX0rndew.mjs.map +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import QuickLRU from "quick-lru";
|
|
1
2
|
import { FastURL } from "srvx";
|
|
2
3
|
import { hasInstanceof, markInstanceof } from "@ebec/core";
|
|
3
4
|
import { HTTPError, isHTTPError } from "@ebec/http";
|
|
@@ -7,6 +8,42 @@ import { compile } from "proxy-addr";
|
|
|
7
8
|
import { get, getType } from "mime-explorer";
|
|
8
9
|
import Negotiator from "negotiator";
|
|
9
10
|
import { pathToRegexp } from "path-to-regexp";
|
|
11
|
+
//#region src/cache/lru.ts
|
|
12
|
+
const DEFAULT_MAX_SIZE = 1024;
|
|
13
|
+
/**
|
|
14
|
+
* Default `ICache` implementation — a bounded LRU backed by
|
|
15
|
+
* [`quick-lru`](https://github.com/sindresorhus/quick-lru). Picked for
|
|
16
|
+
* its small footprint (~1kB), ESM-only build (matches routup), and
|
|
17
|
+
* stable API.
|
|
18
|
+
*
|
|
19
|
+
* For TTL, size-based eviction, or dispose hooks, write your own
|
|
20
|
+
* `ICache` (e.g. wrapping `lru-cache`) and pass it via the router's
|
|
21
|
+
* `BaseRouterOptions.cache` slot.
|
|
22
|
+
*/
|
|
23
|
+
var LruCache = class LruCache {
|
|
24
|
+
options;
|
|
25
|
+
inner;
|
|
26
|
+
constructor(options = {}) {
|
|
27
|
+
this.options = options;
|
|
28
|
+
this.inner = new QuickLRU({ maxSize: options.maxSize ?? DEFAULT_MAX_SIZE });
|
|
29
|
+
}
|
|
30
|
+
get(key) {
|
|
31
|
+
return this.inner.get(key);
|
|
32
|
+
}
|
|
33
|
+
set(key, value) {
|
|
34
|
+
this.inner.set(key, value);
|
|
35
|
+
}
|
|
36
|
+
delete(key) {
|
|
37
|
+
this.inner.delete(key);
|
|
38
|
+
}
|
|
39
|
+
clear() {
|
|
40
|
+
this.inner.clear();
|
|
41
|
+
}
|
|
42
|
+
clone() {
|
|
43
|
+
return new LruCache({ ...this.options });
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
10
47
|
//#region src/constants.ts
|
|
11
48
|
const MethodName = {
|
|
12
49
|
GET: "GET",
|
|
@@ -51,6 +88,66 @@ const HeaderName = {
|
|
|
51
88
|
X_FORWARDED_PROTO: "x-forwarded-proto"
|
|
52
89
|
};
|
|
53
90
|
//#endregion
|
|
91
|
+
//#region src/event/module.ts
|
|
92
|
+
var AppEvent = class {
|
|
93
|
+
request;
|
|
94
|
+
params;
|
|
95
|
+
path;
|
|
96
|
+
method;
|
|
97
|
+
mountPath;
|
|
98
|
+
headers;
|
|
99
|
+
searchParams;
|
|
100
|
+
response;
|
|
101
|
+
store;
|
|
102
|
+
signal;
|
|
103
|
+
appOptions;
|
|
104
|
+
_context;
|
|
105
|
+
_nextCalled = false;
|
|
106
|
+
_nextResult;
|
|
107
|
+
_nextCalledDeferred;
|
|
108
|
+
constructor(context) {
|
|
109
|
+
this._context = context;
|
|
110
|
+
this.request = context.request;
|
|
111
|
+
this.params = context.params;
|
|
112
|
+
this.path = context.path;
|
|
113
|
+
this.method = context.method;
|
|
114
|
+
this.mountPath = context.mountPath;
|
|
115
|
+
this.headers = context.headers;
|
|
116
|
+
this.searchParams = context.searchParams;
|
|
117
|
+
this.response = context.response;
|
|
118
|
+
this.store = context.store;
|
|
119
|
+
this.signal = context.signal;
|
|
120
|
+
this.appOptions = context.appOptions;
|
|
121
|
+
}
|
|
122
|
+
get nextCalled() {
|
|
123
|
+
return this._nextCalled;
|
|
124
|
+
}
|
|
125
|
+
get nextResult() {
|
|
126
|
+
return this._nextResult;
|
|
127
|
+
}
|
|
128
|
+
whenNextCalled() {
|
|
129
|
+
if (!this._nextCalledDeferred) {
|
|
130
|
+
let resolve;
|
|
131
|
+
const promise = new Promise((r) => {
|
|
132
|
+
resolve = r;
|
|
133
|
+
});
|
|
134
|
+
this._nextCalledDeferred = {
|
|
135
|
+
promise,
|
|
136
|
+
resolve
|
|
137
|
+
};
|
|
138
|
+
if (this._nextCalled) resolve();
|
|
139
|
+
}
|
|
140
|
+
return this._nextCalledDeferred.promise;
|
|
141
|
+
}
|
|
142
|
+
async next(error) {
|
|
143
|
+
if (this._nextCalled) return this._nextResult;
|
|
144
|
+
this._nextCalled = true;
|
|
145
|
+
this._nextResult = this._context.next(this, error);
|
|
146
|
+
if (this._nextCalledDeferred) this._nextCalledDeferred.resolve();
|
|
147
|
+
return this._nextResult;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
//#endregion
|
|
54
151
|
//#region src/response/helpers/cache.ts
|
|
55
152
|
function setResponseCacheHeaders(event, options) {
|
|
56
153
|
options = options || {};
|
|
@@ -64,11 +161,11 @@ function setResponseCacheHeaders(event, options) {
|
|
|
64
161
|
}
|
|
65
162
|
//#endregion
|
|
66
163
|
//#region src/error/module.ts
|
|
67
|
-
const ErrorSymbol = Symbol.for("
|
|
68
|
-
var
|
|
164
|
+
const ErrorSymbol = Symbol.for("AppError");
|
|
165
|
+
var AppError = class extends HTTPError {
|
|
69
166
|
constructor(input = {}) {
|
|
70
167
|
super(input);
|
|
71
|
-
this.name = "
|
|
168
|
+
this.name = "AppError";
|
|
72
169
|
markInstanceof(this, ErrorSymbol);
|
|
73
170
|
}
|
|
74
171
|
};
|
|
@@ -91,7 +188,7 @@ function serializeEventStreamMessage(message) {
|
|
|
91
188
|
//#region src/response/helpers/event-stream/module.ts
|
|
92
189
|
function createEventStream(event, options) {
|
|
93
190
|
if (options?.maxMessageSize !== void 0) {
|
|
94
|
-
if (!Number.isInteger(options.maxMessageSize) || options.maxMessageSize < 0) throw new
|
|
191
|
+
if (!Number.isInteger(options.maxMessageSize) || options.maxMessageSize < 0) throw new AppError("maxMessageSize must be a non-negative integer.");
|
|
95
192
|
}
|
|
96
193
|
let controller;
|
|
97
194
|
let closed = false;
|
|
@@ -133,6 +230,30 @@ function createEventStream(event, options) {
|
|
|
133
230
|
return handle;
|
|
134
231
|
}
|
|
135
232
|
//#endregion
|
|
233
|
+
//#region src/utils/accepts-json.ts
|
|
234
|
+
/**
|
|
235
|
+
* Check if the request accepts JSON responses.
|
|
236
|
+
*
|
|
237
|
+
* Parses the `Accept` header per RFC 7231 (media ranges + quality
|
|
238
|
+
* params) rather than substring-matching, so:
|
|
239
|
+
* - `application/json-seq` does NOT count as accepting `application/json`
|
|
240
|
+
* - `application/json;q=0` is treated as an explicit rejection
|
|
241
|
+
*
|
|
242
|
+
* Returns true if no Accept header is present (API-first default).
|
|
243
|
+
*/
|
|
244
|
+
function acceptsJson(request) {
|
|
245
|
+
const accept = request.headers.get("accept");
|
|
246
|
+
if (!accept) return true;
|
|
247
|
+
return accept.toLowerCase().split(",").some((entry) => {
|
|
248
|
+
const parts = entry.split(";").map((part) => part.trim());
|
|
249
|
+
const mediaRange = parts[0];
|
|
250
|
+
const qParam = parts.slice(1).map((param) => param.split("=").map((s) => s.trim())).find(([key]) => key === "q");
|
|
251
|
+
const q = qParam ? Number.parseFloat(qParam[1] ?? "") : 1;
|
|
252
|
+
if (!Number.isFinite(q) || q <= 0) return false;
|
|
253
|
+
return mediaRange === "*/*" || mediaRange === "application/*" || mediaRange === "application/json" || mediaRange.endsWith("+json");
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
//#endregion
|
|
136
257
|
//#region src/utils/header.ts
|
|
137
258
|
function sanitizeHeaderValue(value) {
|
|
138
259
|
return value.replace(/[\r\n]/g, "");
|
|
@@ -180,6 +301,16 @@ function buildEtagFn(input) {
|
|
|
180
301
|
return createEtag(body, options);
|
|
181
302
|
};
|
|
182
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* Default `EtagFn` used by `toResponse()` when `appOptions.etag` is
|
|
306
|
+
* undefined. Module-scoped so we don't allocate per-request and so
|
|
307
|
+
* all consumers share the same closure.
|
|
308
|
+
*
|
|
309
|
+
* `appOptions.etag === null` (explicitly disabled by the user)
|
|
310
|
+
* remains distinct: consumers must check `=== undefined`, not
|
|
311
|
+
* `== null`, before falling back to this default.
|
|
312
|
+
*/
|
|
313
|
+
const DEFAULT_ETAG_FN = buildEtagFn();
|
|
183
314
|
//#endregion
|
|
184
315
|
//#region src/utils/trust-proxy/module.ts
|
|
185
316
|
function buildTrustProxyFn(input) {
|
|
@@ -189,6 +320,15 @@ function buildTrustProxyFn(input) {
|
|
|
189
320
|
if (typeof input === "string") input = input.split(",").map((value) => value.trim());
|
|
190
321
|
return compile(input || []);
|
|
191
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Default `TrustProxyFn` used by request helpers when neither the
|
|
325
|
+
* call's `options.trustProxy` nor `event.appOptions.trustProxy` is
|
|
326
|
+
* set. Trusts no addresses — the conservative default.
|
|
327
|
+
*
|
|
328
|
+
* Module-scoped so all helpers share the same reference and we don't
|
|
329
|
+
* allocate per-request.
|
|
330
|
+
*/
|
|
331
|
+
const DEFAULT_TRUST_PROXY = buildTrustProxyFn();
|
|
192
332
|
//#endregion
|
|
193
333
|
//#region src/utils/mime.ts
|
|
194
334
|
function getMimeType(type) {
|
|
@@ -226,18 +366,12 @@ function basename(input, extension) {
|
|
|
226
366
|
return extension && lastSegment.endsWith(extension) ? lastSegment.slice(0, -extension.length) : lastSegment;
|
|
227
367
|
}
|
|
228
368
|
//#endregion
|
|
229
|
-
//#region src/utils/
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (!queryParams) return input.endsWith("/");
|
|
233
|
-
return TRAILING_SLASH_RE.test(input);
|
|
234
|
-
}
|
|
235
|
-
function withoutTrailingSlash(input = "", queryParams = false) {
|
|
236
|
-
if (!queryParams) return (hasTrailingSlash(input) ? input.slice(0, -1) : input) || "/";
|
|
237
|
-
if (!hasTrailingSlash(input, true)) return input || "/";
|
|
238
|
-
const [s0, ...s] = input.split("?");
|
|
239
|
-
return (s0.slice(0, -1) || "/") + (s.length ? `?${s.join("?")}` : "");
|
|
369
|
+
//#region src/utils/promise.ts
|
|
370
|
+
function isPromise(p) {
|
|
371
|
+
return isObject(p) && (p instanceof Promise || typeof p.then === "function");
|
|
240
372
|
}
|
|
373
|
+
//#endregion
|
|
374
|
+
//#region src/utils/url.ts
|
|
241
375
|
function hasLeadingSlash(input = "") {
|
|
242
376
|
return input.startsWith("/");
|
|
243
377
|
}
|
|
@@ -248,6 +382,33 @@ function cleanDoubleSlashes(input = "") {
|
|
|
248
382
|
if (input.includes("://")) return input.split("://").map((str) => cleanDoubleSlashes(str)).join("://");
|
|
249
383
|
return input.replace(/\/+/g, "/");
|
|
250
384
|
}
|
|
385
|
+
/**
|
|
386
|
+
* Concatenate path parts into a single mount path.
|
|
387
|
+
*
|
|
388
|
+
* - Drops `undefined` and empty parts.
|
|
389
|
+
* - A lone `'/'` part still contributes (so `joinPaths('/')` returns
|
|
390
|
+
* `'/'`, distinguishing "match the root exactly" from "no path").
|
|
391
|
+
* - Returns `undefined` when every part is missing — callers
|
|
392
|
+
* interpret this as "no path" (always-match middleware).
|
|
393
|
+
* - Joins remaining parts with `/`, normalizes the leading slash,
|
|
394
|
+
* collapses any inner `//`, and trims a trailing slash on results
|
|
395
|
+
* longer than `/`.
|
|
396
|
+
*
|
|
397
|
+
* Used at registration time to fold a handler / router's intrinsic
|
|
398
|
+
* path into the mount path so the active `IRouter` is the
|
|
399
|
+
* only place that builds path matchers.
|
|
400
|
+
*/
|
|
401
|
+
function joinPaths(...parts) {
|
|
402
|
+
const kept = [];
|
|
403
|
+
for (const part of parts) {
|
|
404
|
+
if (typeof part !== "string" || part === "") continue;
|
|
405
|
+
kept.push(part);
|
|
406
|
+
}
|
|
407
|
+
if (kept.length === 0) return;
|
|
408
|
+
const normalized = cleanDoubleSlashes(withLeadingSlash(kept.join("/")));
|
|
409
|
+
if (normalized.length > 1 && normalized.endsWith("/")) return normalized.slice(0, -1);
|
|
410
|
+
return normalized;
|
|
411
|
+
}
|
|
251
412
|
//#endregion
|
|
252
413
|
//#region src/response/helpers/header.ts
|
|
253
414
|
function appendResponseHeader(event, name, value) {
|
|
@@ -345,8 +506,8 @@ function isNativeError(input) {
|
|
|
345
506
|
}
|
|
346
507
|
/**
|
|
347
508
|
* Create an internal error object by
|
|
348
|
-
* - an existing
|
|
349
|
-
* - an HTTPError (wrapped into a
|
|
509
|
+
* - an existing AppError (returned as-is)
|
|
510
|
+
* - an HTTPError (wrapped into a AppError preserving status)
|
|
350
511
|
* - an Error (wrapped preserving message and cause)
|
|
351
512
|
* - an options object (status, message, etc.)
|
|
352
513
|
* - a message string
|
|
@@ -355,33 +516,51 @@ function isNativeError(input) {
|
|
|
355
516
|
*/
|
|
356
517
|
function createError(input) {
|
|
357
518
|
if (isError(input)) return input;
|
|
358
|
-
if (typeof input === "string") return new
|
|
359
|
-
if (isHTTPError(input)) return new
|
|
519
|
+
if (typeof input === "string") return new AppError(input);
|
|
520
|
+
if (isHTTPError(input)) return new AppError({
|
|
360
521
|
message: input.message,
|
|
361
522
|
code: input.code,
|
|
362
523
|
status: input.status,
|
|
363
524
|
redirectURL: input.redirectURL,
|
|
364
525
|
cause: input
|
|
365
526
|
});
|
|
366
|
-
if (isNativeError(input)) return new
|
|
527
|
+
if (isNativeError(input)) return new AppError({
|
|
367
528
|
message: input.message,
|
|
368
529
|
cause: input
|
|
369
530
|
});
|
|
370
|
-
if (!isObject(input)) return new
|
|
531
|
+
if (!isObject(input)) return new AppError();
|
|
371
532
|
const options = { ...input };
|
|
372
533
|
if (options.cause === void 0) options.cause = input;
|
|
373
|
-
return new
|
|
534
|
+
return new AppError(options);
|
|
374
535
|
}
|
|
375
536
|
//#endregion
|
|
376
537
|
//#region src/response/to-response.ts
|
|
377
538
|
function stripWeakPrefix(etag) {
|
|
378
539
|
return etag.startsWith("W/") ? etag.slice(2) : etag;
|
|
379
540
|
}
|
|
541
|
+
/**
|
|
542
|
+
* Resolve the effective etag fn for this request. `null` means the
|
|
543
|
+
* user explicitly disabled ETag; `undefined` means the option was
|
|
544
|
+
* never set, and we apply the framework default. Anything else is the
|
|
545
|
+
* user's own fn.
|
|
546
|
+
*/
|
|
547
|
+
function effectiveEtagFn(event) {
|
|
548
|
+
const opt = event.appOptions.etag;
|
|
549
|
+
if (opt === null) return null;
|
|
550
|
+
if (typeof opt === "undefined") return DEFAULT_ETAG_FN;
|
|
551
|
+
return opt;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Compute an ETag and conditionally return a 304, or set the header and
|
|
555
|
+
* return undefined to let the caller emit the full response. Always
|
|
556
|
+
* async because the ETag generator may be async (and typically is, via
|
|
557
|
+
* `uncrypto`).
|
|
558
|
+
*/
|
|
380
559
|
async function applyEtag(body, event, headers) {
|
|
381
|
-
const etagFn = event
|
|
382
|
-
if (!etagFn) return
|
|
560
|
+
const etagFn = effectiveEtagFn(event);
|
|
561
|
+
if (!etagFn) return;
|
|
383
562
|
const etag = await etagFn(body);
|
|
384
|
-
if (!etag) return
|
|
563
|
+
if (!etag) return;
|
|
385
564
|
headers.set("etag", etag);
|
|
386
565
|
const ifNoneMatch = event.headers.get("if-none-match");
|
|
387
566
|
if (ifNoneMatch && (ifNoneMatch === "*" || ifNoneMatch.split(",").some((t) => stripWeakPrefix(t.trim()) === stripWeakPrefix(etag)))) return new Response(null, {
|
|
@@ -389,41 +568,60 @@ async function applyEtag(body, event, headers) {
|
|
|
389
568
|
headers
|
|
390
569
|
});
|
|
391
570
|
}
|
|
392
|
-
|
|
571
|
+
/**
|
|
572
|
+
* Convert a handler's return value into a Web `Response`.
|
|
573
|
+
*
|
|
574
|
+
* Returns synchronously for the common cases (string, JSON object,
|
|
575
|
+
* binary, stream, blob) when ETag generation is disabled. Returns a
|
|
576
|
+
* `Promise` when an ETag must be computed (the generator is async).
|
|
577
|
+
*
|
|
578
|
+
* Callers that want the async return uniformly can `await` the result
|
|
579
|
+
* — `await` on a non-Promise still works but pays a microtask hop.
|
|
580
|
+
* The App fast path branches on `instanceof Promise` to keep the
|
|
581
|
+
* sync return truly sync.
|
|
582
|
+
*/
|
|
583
|
+
function toResponse(value, event) {
|
|
393
584
|
if (value === void 0) return;
|
|
394
585
|
if (value === null) return new Response(null, {
|
|
395
586
|
status: event.response.status,
|
|
396
587
|
headers: event.response.headers
|
|
397
588
|
});
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
589
|
+
const t = typeof value;
|
|
590
|
+
if (t === "string") {
|
|
591
|
+
const { status, headers } = event.response;
|
|
401
592
|
if (!headers.has("content-type")) headers.set("content-type", "text/plain; charset=utf-8");
|
|
402
|
-
|
|
403
|
-
if (cached) return cached;
|
|
404
|
-
return new Response(value, {
|
|
593
|
+
if (event.appOptions.etag !== null) return applyEtag(value, event, headers).then((cached) => cached ?? new Response(value, {
|
|
405
594
|
status,
|
|
406
595
|
headers
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
if (value instanceof ArrayBuffer || value instanceof Uint8Array) {
|
|
410
|
-
if (!headers.has("content-type")) headers.set("content-type", "application/octet-stream");
|
|
596
|
+
}));
|
|
411
597
|
return new Response(value, {
|
|
412
598
|
status,
|
|
413
599
|
headers
|
|
414
600
|
});
|
|
415
601
|
}
|
|
416
|
-
if (
|
|
417
|
-
|
|
418
|
-
headers
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
602
|
+
if (t === "object" && !Array.isArray(value)) {
|
|
603
|
+
if (value instanceof Response) return value;
|
|
604
|
+
const { status, headers } = event.response;
|
|
605
|
+
if (value instanceof ArrayBuffer || value instanceof Uint8Array) {
|
|
606
|
+
if (!headers.has("content-type")) headers.set("content-type", "application/octet-stream");
|
|
607
|
+
return new Response(value, {
|
|
608
|
+
status,
|
|
609
|
+
headers
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
if (value instanceof ReadableStream) return new Response(value, {
|
|
423
613
|
status,
|
|
424
614
|
headers
|
|
425
615
|
});
|
|
616
|
+
if (value instanceof Blob) {
|
|
617
|
+
if (!headers.has("content-type")) headers.set("content-type", value.type || "application/octet-stream");
|
|
618
|
+
return new Response(value, {
|
|
619
|
+
status,
|
|
620
|
+
headers
|
|
621
|
+
});
|
|
622
|
+
}
|
|
426
623
|
}
|
|
624
|
+
const { status, headers } = event.response;
|
|
427
625
|
if (!headers.has("content-type")) headers.set("content-type", "application/json; charset=utf-8");
|
|
428
626
|
let json;
|
|
429
627
|
try {
|
|
@@ -435,8 +633,10 @@ async function toResponse(value, event) {
|
|
|
435
633
|
cause: e
|
|
436
634
|
});
|
|
437
635
|
}
|
|
438
|
-
|
|
439
|
-
|
|
636
|
+
if (event.appOptions.etag !== null) return applyEtag(json, event, headers).then((cached) => cached ?? new Response(json, {
|
|
637
|
+
status,
|
|
638
|
+
headers
|
|
639
|
+
}));
|
|
440
640
|
return new Response(json, {
|
|
441
641
|
status,
|
|
442
642
|
headers
|
|
@@ -576,7 +776,7 @@ function isAllowedRedirectUrl(location) {
|
|
|
576
776
|
}
|
|
577
777
|
}
|
|
578
778
|
function sendRedirect(event, location, statusCode = 302) {
|
|
579
|
-
if (!isAllowedRedirectUrl(location)) throw new
|
|
779
|
+
if (!isAllowedRedirectUrl(location)) throw new AppError({
|
|
580
780
|
status: 400,
|
|
581
781
|
message: "Invalid redirect URL scheme."
|
|
582
782
|
});
|
|
@@ -601,69 +801,6 @@ function sendStream(event, stream) {
|
|
|
601
801
|
});
|
|
602
802
|
}
|
|
603
803
|
//#endregion
|
|
604
|
-
//#region src/event/module.ts
|
|
605
|
-
var RoutupEvent = class {
|
|
606
|
-
request;
|
|
607
|
-
params;
|
|
608
|
-
path;
|
|
609
|
-
method;
|
|
610
|
-
mountPath;
|
|
611
|
-
headers;
|
|
612
|
-
searchParams;
|
|
613
|
-
response;
|
|
614
|
-
store;
|
|
615
|
-
signal;
|
|
616
|
-
_context;
|
|
617
|
-
_routerOptions;
|
|
618
|
-
_nextCalled = false;
|
|
619
|
-
_nextResult;
|
|
620
|
-
_nextCalledDeferred;
|
|
621
|
-
constructor(context) {
|
|
622
|
-
this._context = context;
|
|
623
|
-
this.request = context.request;
|
|
624
|
-
this.params = context.params;
|
|
625
|
-
this.path = context.path;
|
|
626
|
-
this.method = context.method;
|
|
627
|
-
this.mountPath = context.mountPath;
|
|
628
|
-
this.headers = context.headers;
|
|
629
|
-
this.searchParams = context.searchParams;
|
|
630
|
-
this.response = context.response;
|
|
631
|
-
this.store = context.store;
|
|
632
|
-
this.signal = context.signal;
|
|
633
|
-
}
|
|
634
|
-
get routerOptions() {
|
|
635
|
-
if (!this._routerOptions) this._routerOptions = this._context.routerOptions();
|
|
636
|
-
return this._routerOptions;
|
|
637
|
-
}
|
|
638
|
-
get nextCalled() {
|
|
639
|
-
return this._nextCalled;
|
|
640
|
-
}
|
|
641
|
-
get nextResult() {
|
|
642
|
-
return this._nextResult;
|
|
643
|
-
}
|
|
644
|
-
whenNextCalled() {
|
|
645
|
-
if (!this._nextCalledDeferred) {
|
|
646
|
-
let resolve;
|
|
647
|
-
const promise = new Promise((r) => {
|
|
648
|
-
resolve = r;
|
|
649
|
-
});
|
|
650
|
-
this._nextCalledDeferred = {
|
|
651
|
-
promise,
|
|
652
|
-
resolve
|
|
653
|
-
};
|
|
654
|
-
if (this._nextCalled) resolve();
|
|
655
|
-
}
|
|
656
|
-
return this._nextCalledDeferred.promise;
|
|
657
|
-
}
|
|
658
|
-
async next(error) {
|
|
659
|
-
if (this._nextCalled) return this._nextResult;
|
|
660
|
-
this._nextCalled = true;
|
|
661
|
-
this._nextResult = this._context.next(this, error);
|
|
662
|
-
if (this._nextCalledDeferred) this._nextCalledDeferred.resolve();
|
|
663
|
-
return this._nextResult;
|
|
664
|
-
}
|
|
665
|
-
};
|
|
666
|
-
//#endregion
|
|
667
804
|
//#region src/dispatcher/module.ts
|
|
668
805
|
var DispatcherEvent = class {
|
|
669
806
|
request;
|
|
@@ -676,7 +813,20 @@ var DispatcherEvent = class {
|
|
|
676
813
|
methodsAllowed;
|
|
677
814
|
mountPath;
|
|
678
815
|
error;
|
|
679
|
-
|
|
816
|
+
/**
|
|
817
|
+
* Options of the App currently dispatching this event. Set on
|
|
818
|
+
* entry to `App.dispatch` and restored on exit (so nested apps
|
|
819
|
+
* temporarily override). Initialized to `{}` so consumers
|
|
820
|
+
* reading before any dispatch get a valid (empty) shape.
|
|
821
|
+
*/
|
|
822
|
+
appOptions;
|
|
823
|
+
/**
|
|
824
|
+
* `true` while at least one `App.dispatch` is on the call stack
|
|
825
|
+
* for this event. `App.dispatch` reads this on entry to derive
|
|
826
|
+
* `isRoot` and writes it on entry/exit so nested calls see it
|
|
827
|
+
* already set.
|
|
828
|
+
*/
|
|
829
|
+
isDispatching;
|
|
680
830
|
_dispatched;
|
|
681
831
|
_response;
|
|
682
832
|
_store;
|
|
@@ -705,7 +855,8 @@ var DispatcherEvent = class {
|
|
|
705
855
|
this.path = this._url.pathname;
|
|
706
856
|
this.mountPath = "/";
|
|
707
857
|
this.params = {};
|
|
708
|
-
this.
|
|
858
|
+
this.appOptions = {};
|
|
859
|
+
this.isDispatching = false;
|
|
709
860
|
this.methodsAllowed = /* @__PURE__ */ new Set();
|
|
710
861
|
this._dispatched = false;
|
|
711
862
|
this._nextCalled = false;
|
|
@@ -771,7 +922,7 @@ var DispatcherEvent = class {
|
|
|
771
922
|
this._nextResult = void 0;
|
|
772
923
|
}
|
|
773
924
|
build(signal) {
|
|
774
|
-
return new
|
|
925
|
+
return new AppEvent({
|
|
775
926
|
request: this.request,
|
|
776
927
|
params: this.params,
|
|
777
928
|
path: this.path,
|
|
@@ -782,7 +933,7 @@ var DispatcherEvent = class {
|
|
|
782
933
|
response: this.response,
|
|
783
934
|
store: this.store,
|
|
784
935
|
signal: signal ?? this.signal,
|
|
785
|
-
|
|
936
|
+
appOptions: this.appOptions,
|
|
786
937
|
next: (event, error) => this.next(event, error)
|
|
787
938
|
});
|
|
788
939
|
}
|
|
@@ -790,23 +941,6 @@ var DispatcherEvent = class {
|
|
|
790
941
|
if (!this._store) this._store = Object.create(null);
|
|
791
942
|
return this._store;
|
|
792
943
|
}
|
|
793
|
-
resolveOptions() {
|
|
794
|
-
const resolved = {
|
|
795
|
-
trustProxy: () => false,
|
|
796
|
-
subdomainOffset: 2,
|
|
797
|
-
etag: buildEtagFn(),
|
|
798
|
-
proxyIpMax: 0
|
|
799
|
-
};
|
|
800
|
-
for (let i = 0; i < this.routerPath.length; i++) {
|
|
801
|
-
const node = this.routerPath[i];
|
|
802
|
-
const entries = Object.entries(node.options);
|
|
803
|
-
for (const entry of entries) {
|
|
804
|
-
const [key, value] = entry;
|
|
805
|
-
if (typeof value !== "undefined") resolved[key] = value;
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
return resolved;
|
|
809
|
-
}
|
|
810
944
|
};
|
|
811
945
|
//#endregion
|
|
812
946
|
//#region src/handler/constants.ts
|
|
@@ -818,8 +952,17 @@ const HandlerSymbol = Symbol.for("Handler");
|
|
|
818
952
|
//#endregion
|
|
819
953
|
//#region src/hook/constants.ts
|
|
820
954
|
const HookName = {
|
|
821
|
-
|
|
822
|
-
|
|
955
|
+
/**
|
|
956
|
+
* Fired at the start of `App.dispatch`, before the pipeline walk.
|
|
957
|
+
* Once per router per request.
|
|
958
|
+
*/
|
|
959
|
+
START: "start",
|
|
960
|
+
/**
|
|
961
|
+
* Fired at the end of `App.dispatch`, after the pipeline walk
|
|
962
|
+
* (and OPTIONS auto-Allow synthesis) completes. Once per router per
|
|
963
|
+
* request.
|
|
964
|
+
*/
|
|
965
|
+
END: "end",
|
|
823
966
|
ERROR: "error",
|
|
824
967
|
CHILD_MATCH: "childMatch",
|
|
825
968
|
CHILD_DISPATCH_BEFORE: "childDispatchBefore",
|
|
@@ -829,8 +972,25 @@ const HookName = {
|
|
|
829
972
|
//#region src/hook/module.ts
|
|
830
973
|
var Hooks = class Hooks {
|
|
831
974
|
items;
|
|
975
|
+
/**
|
|
976
|
+
* Derived bit: `true` iff at least one entry exists across all
|
|
977
|
+
* hook names. Maintained on every `addListener` / `removeListener`
|
|
978
|
+
* so the dispatch hot path can short-circuit on a single boolean
|
|
979
|
+
* read rather than per-name lookup. Apps that never register a
|
|
980
|
+
* hook (the common case) pay one boolean check per request
|
|
981
|
+
* instead of a property access per pipeline step.
|
|
982
|
+
*/
|
|
983
|
+
_hasAny;
|
|
832
984
|
constructor() {
|
|
833
985
|
this.items = {};
|
|
986
|
+
this._hasAny = false;
|
|
987
|
+
}
|
|
988
|
+
hasAny() {
|
|
989
|
+
return this._hasAny;
|
|
990
|
+
}
|
|
991
|
+
hasListeners(name) {
|
|
992
|
+
if (!this._hasAny) return false;
|
|
993
|
+
return this.items[name] !== void 0;
|
|
834
994
|
}
|
|
835
995
|
addListener(name, fn, priority = 0) {
|
|
836
996
|
this.items[name] = this.items[name] || [];
|
|
@@ -841,6 +1001,7 @@ var Hooks = class Hooks {
|
|
|
841
1001
|
let i = 0;
|
|
842
1002
|
while (i < this.items[name].length && this.items[name][i].priority >= priority) i++;
|
|
843
1003
|
this.items[name].splice(i, 0, entry);
|
|
1004
|
+
this._hasAny = true;
|
|
844
1005
|
return () => {
|
|
845
1006
|
this.removeListener(name, fn);
|
|
846
1007
|
};
|
|
@@ -849,6 +1010,7 @@ var Hooks = class Hooks {
|
|
|
849
1010
|
if (!this.items[name]) return;
|
|
850
1011
|
if (typeof fn === "undefined") {
|
|
851
1012
|
delete this.items[name];
|
|
1013
|
+
this.recomputeHasAny();
|
|
852
1014
|
return;
|
|
853
1015
|
}
|
|
854
1016
|
if (typeof fn === "function") {
|
|
@@ -856,6 +1018,20 @@ var Hooks = class Hooks {
|
|
|
856
1018
|
if (index !== -1) this.items[name].splice(index, 1);
|
|
857
1019
|
}
|
|
858
1020
|
if (this.items[name].length === 0) delete this.items[name];
|
|
1021
|
+
this.recomputeHasAny();
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Recompute `_hasAny` from the current `items` map. O(k) where k
|
|
1025
|
+
* is the number of distinct hook names ever registered (≤ ~6) —
|
|
1026
|
+
* effectively O(1). Called from `removeListener` so the fast-path
|
|
1027
|
+
* flag stays in sync with registration state.
|
|
1028
|
+
*/
|
|
1029
|
+
recomputeHasAny() {
|
|
1030
|
+
for (const name in this.items) if (this.items[name] && this.items[name].length > 0) {
|
|
1031
|
+
this._hasAny = true;
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
this._hasAny = false;
|
|
859
1035
|
}
|
|
860
1036
|
/**
|
|
861
1037
|
* Create a new `Hooks` instance seeded with the same listeners as this
|
|
@@ -907,120 +1083,35 @@ var Hooks = class Hooks {
|
|
|
907
1083
|
}
|
|
908
1084
|
};
|
|
909
1085
|
//#endregion
|
|
910
|
-
//#region src/
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1086
|
+
//#region src/handler/module.ts
|
|
1087
|
+
var Handler = class {
|
|
1088
|
+
config;
|
|
1089
|
+
hooks;
|
|
1090
|
+
method;
|
|
1091
|
+
constructor(handler) {
|
|
1092
|
+
this.config = handler;
|
|
1093
|
+
this.hooks = new Hooks();
|
|
1094
|
+
this.mountHooks();
|
|
1095
|
+
if (typeof handler.path === "string") this.config.path = withLeadingSlash(handler.path);
|
|
1096
|
+
this.method = this.config.method ? toMethodName(this.config.method) : void 0;
|
|
1097
|
+
markInstanceof(this, HandlerSymbol);
|
|
918
1098
|
}
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
path;
|
|
922
|
-
regexp;
|
|
923
|
-
regexpKeys = [];
|
|
924
|
-
regexpOptions;
|
|
925
|
-
constructor(path, options) {
|
|
926
|
-
this.path = path;
|
|
927
|
-
this.regexpOptions = options || {};
|
|
928
|
-
const regexp = pathToRegexp(path, options);
|
|
929
|
-
this.regexp = regexp.regexp;
|
|
930
|
-
this.regexpKeys = regexp.keys;
|
|
1099
|
+
get type() {
|
|
1100
|
+
return this.config.type;
|
|
931
1101
|
}
|
|
932
|
-
|
|
933
|
-
return this.
|
|
934
|
-
}
|
|
935
|
-
exec(path) {
|
|
936
|
-
if (this.path === "/" && this.regexpOptions.end === false) return {
|
|
937
|
-
path: "/",
|
|
938
|
-
params: Object.create(null)
|
|
939
|
-
};
|
|
940
|
-
const match = this.regexp.exec(path);
|
|
941
|
-
if (!match) return;
|
|
942
|
-
const params = Object.create(null);
|
|
943
|
-
for (let i = 1; i < match.length; i++) {
|
|
944
|
-
const key = this.regexpKeys[i - 1];
|
|
945
|
-
if (!key) continue;
|
|
946
|
-
const prop = key.name;
|
|
947
|
-
const val = decodeParam(match[i]);
|
|
948
|
-
if (typeof val !== "undefined") params[prop] = val;
|
|
949
|
-
}
|
|
950
|
-
return {
|
|
951
|
-
path: match[0],
|
|
952
|
-
params
|
|
953
|
-
};
|
|
954
|
-
}
|
|
955
|
-
};
|
|
956
|
-
//#endregion
|
|
957
|
-
//#region src/path/utils.ts
|
|
958
|
-
function isPath(input) {
|
|
959
|
-
return typeof input === "string";
|
|
960
|
-
}
|
|
961
|
-
//#endregion
|
|
962
|
-
//#region src/handler/utils.ts
|
|
963
|
-
/**
|
|
964
|
-
* Build a `PathMatcher` for a handler-side path.
|
|
965
|
-
*
|
|
966
|
-
* Returns `undefined` when no path is supplied. The `end` flag controls
|
|
967
|
-
* whether the matcher requires a full match (`true` for method handlers
|
|
968
|
-
* matching exact routes) or accepts a prefix (`false` for middleware).
|
|
969
|
-
*/
|
|
970
|
-
function buildHandlerPathMatcher(path, end) {
|
|
971
|
-
if (typeof path === "undefined") return;
|
|
972
|
-
return new PathMatcher(typeof path === "string" ? withLeadingSlash(path) : path, { end });
|
|
973
|
-
}
|
|
974
|
-
/**
|
|
975
|
-
* Match a request method against a handler's bound method.
|
|
976
|
-
*
|
|
977
|
-
* - When the handler has no method bound, matches every request method.
|
|
978
|
-
* - Otherwise matches when the request method is the same.
|
|
979
|
-
* - HEAD requests additionally match GET handlers.
|
|
980
|
-
*/
|
|
981
|
-
function matchHandlerMethod(handlerMethod, requestMethod) {
|
|
982
|
-
return !handlerMethod || requestMethod === handlerMethod || requestMethod === MethodName.HEAD && handlerMethod === MethodName.GET;
|
|
983
|
-
}
|
|
984
|
-
//#endregion
|
|
985
|
-
//#region src/handler/module.ts
|
|
986
|
-
var Handler = class {
|
|
987
|
-
config;
|
|
988
|
-
hooks;
|
|
989
|
-
pathMatcher;
|
|
990
|
-
method;
|
|
991
|
-
constructor(handler) {
|
|
992
|
-
this.config = handler;
|
|
993
|
-
this.hooks = new Hooks();
|
|
994
|
-
this.mountHooks();
|
|
995
|
-
if (typeof handler.path === "string") this.config.path = withLeadingSlash(handler.path);
|
|
996
|
-
this.pathMatcher = buildHandlerPathMatcher(this.config.path, !!this.config.method);
|
|
997
|
-
this.method = this.config.method ? toMethodName(this.config.method) : void 0;
|
|
998
|
-
markInstanceof(this, HandlerSymbol);
|
|
999
|
-
}
|
|
1000
|
-
get type() {
|
|
1001
|
-
return this.config.type;
|
|
1002
|
-
}
|
|
1003
|
-
get path() {
|
|
1004
|
-
return this.config.path;
|
|
1102
|
+
get path() {
|
|
1103
|
+
return this.config.path;
|
|
1005
1104
|
}
|
|
1006
1105
|
async dispatch(event) {
|
|
1007
|
-
if (this.
|
|
1008
|
-
|
|
1009
|
-
if (
|
|
1010
|
-
...event.params,
|
|
1011
|
-
...pathMatch.params
|
|
1012
|
-
};
|
|
1106
|
+
if (this.hooks.hasListeners(HookName.CHILD_DISPATCH_BEFORE)) {
|
|
1107
|
+
await this.hooks.trigger(HookName.CHILD_DISPATCH_BEFORE, event);
|
|
1108
|
+
if (event.dispatched) return;
|
|
1013
1109
|
}
|
|
1014
|
-
await this.hooks.trigger(HookName.CHILD_DISPATCH_BEFORE, event);
|
|
1015
|
-
if (event.dispatched) return;
|
|
1016
1110
|
let response;
|
|
1017
1111
|
try {
|
|
1018
|
-
|
|
1019
|
-
const previewEvent = event.build();
|
|
1020
|
-
const effectiveTimeout = this.resolveTimeout(previewEvent.routerOptions);
|
|
1112
|
+
const effectiveTimeout = this.resolveTimeout(event.appOptions);
|
|
1021
1113
|
let childController;
|
|
1022
1114
|
let cleanupParentListener;
|
|
1023
|
-
let handlerEvent = previewEvent;
|
|
1024
1115
|
if (effectiveTimeout) {
|
|
1025
1116
|
const parentSignal = event.signal;
|
|
1026
1117
|
childController = new AbortController();
|
|
@@ -1030,37 +1121,44 @@ var Handler = class {
|
|
|
1030
1121
|
parentSignal.addEventListener("abort", onAbort, { once: true });
|
|
1031
1122
|
cleanupParentListener = () => parentSignal.removeEventListener("abort", onAbort);
|
|
1032
1123
|
}
|
|
1033
|
-
handlerEvent = event.build(childController.signal);
|
|
1034
1124
|
}
|
|
1125
|
+
const handlerEvent = childController ? event.build(childController.signal) : event.build();
|
|
1126
|
+
let result;
|
|
1035
1127
|
try {
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1128
|
+
let skipFn = false;
|
|
1129
|
+
let invocation;
|
|
1130
|
+
if (this.config.type === HandlerType.ERROR) if (event.error) {
|
|
1131
|
+
const { fn } = this.config;
|
|
1132
|
+
invocation = fn(event.error, handlerEvent);
|
|
1133
|
+
} else skipFn = true;
|
|
1134
|
+
else {
|
|
1043
1135
|
const { fn } = this.config;
|
|
1044
|
-
|
|
1136
|
+
invocation = fn(handlerEvent);
|
|
1045
1137
|
}
|
|
1138
|
+
if (skipFn) {} else if (effectiveTimeout) result = await this.executeWithTimeout(() => this.resolveHandlerResult(invocation, handlerEvent), effectiveTimeout, childController);
|
|
1139
|
+
else if (isPromise(invocation)) {
|
|
1140
|
+
const awaited = await invocation;
|
|
1141
|
+
result = typeof awaited === "undefined" ? await this.resolveHandlerResult(void 0, handlerEvent) : awaited;
|
|
1142
|
+
} else if (typeof invocation === "undefined") result = await this.resolveHandlerResult(void 0, handlerEvent);
|
|
1143
|
+
else result = invocation;
|
|
1046
1144
|
} finally {
|
|
1047
1145
|
if (cleanupParentListener) cleanupParentListener();
|
|
1048
1146
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1147
|
+
const toResp = toResponse(result, handlerEvent);
|
|
1148
|
+
response = isPromise(toResp) ? await toResp : toResp;
|
|
1149
|
+
if (response) {
|
|
1150
|
+
event.dispatched = true;
|
|
1151
|
+
if (this.config.type === HandlerType.ERROR && event.error) event.error = void 0;
|
|
1152
|
+
}
|
|
1051
1153
|
} catch (e) {
|
|
1052
1154
|
event.error = isError(e) ? e : createError(e);
|
|
1053
|
-
await this.hooks.trigger(HookName.ERROR, event);
|
|
1155
|
+
if (this.hooks.hasListeners(HookName.ERROR)) await this.hooks.trigger(HookName.ERROR, event);
|
|
1054
1156
|
if (event.dispatched) event.error = void 0;
|
|
1055
1157
|
else throw event.error;
|
|
1056
1158
|
}
|
|
1057
|
-
await this.hooks.trigger(HookName.CHILD_DISPATCH_AFTER, event);
|
|
1159
|
+
if (this.hooks.hasListeners(HookName.CHILD_DISPATCH_AFTER)) await this.hooks.trigger(HookName.CHILD_DISPATCH_AFTER, event);
|
|
1058
1160
|
return response;
|
|
1059
1161
|
}
|
|
1060
|
-
matchPath(path) {
|
|
1061
|
-
if (!this.pathMatcher) return true;
|
|
1062
|
-
return this.pathMatcher.test(path);
|
|
1063
|
-
}
|
|
1064
1162
|
/**
|
|
1065
1163
|
* Resolve a handler's return value into the final value handed to `toResponse`.
|
|
1066
1164
|
*
|
|
@@ -1096,8 +1194,7 @@ var Handler = class {
|
|
|
1096
1194
|
});
|
|
1097
1195
|
});
|
|
1098
1196
|
}
|
|
1099
|
-
async executeWithTimeout(fn,
|
|
1100
|
-
const effectiveTimeout = this.resolveTimeout(routerOptions);
|
|
1197
|
+
async executeWithTimeout(fn, effectiveTimeout, controller) {
|
|
1101
1198
|
if (!effectiveTimeout) return fn();
|
|
1102
1199
|
let timerId;
|
|
1103
1200
|
try {
|
|
@@ -1114,13 +1211,13 @@ var Handler = class {
|
|
|
1114
1211
|
clearTimeout(timerId);
|
|
1115
1212
|
}
|
|
1116
1213
|
}
|
|
1117
|
-
resolveTimeout(
|
|
1118
|
-
const routerDefault =
|
|
1214
|
+
resolveTimeout(appOptions) {
|
|
1215
|
+
const routerDefault = appOptions.handlerTimeout;
|
|
1119
1216
|
const handlerOverride = this.config.timeout;
|
|
1120
1217
|
if (!routerDefault && !handlerOverride) return;
|
|
1121
1218
|
if (!routerDefault) return handlerOverride;
|
|
1122
1219
|
if (!handlerOverride) return routerDefault;
|
|
1123
|
-
if (
|
|
1220
|
+
if (appOptions.handlerTimeoutOverridable) return handlerOverride;
|
|
1124
1221
|
return Math.min(routerDefault, handlerOverride);
|
|
1125
1222
|
}
|
|
1126
1223
|
mountHooks() {
|
|
@@ -1226,10 +1323,10 @@ function callMiddleware(handler, req, res) {
|
|
|
1226
1323
|
});
|
|
1227
1324
|
}
|
|
1228
1325
|
function createNodeBridge(handler, isMiddleware) {
|
|
1229
|
-
if (typeof handler !== "function") throw new
|
|
1326
|
+
if (typeof handler !== "function") throw new AppError("fromNodeHandler/fromNodeMiddleware expects a function.");
|
|
1230
1327
|
return defineCoreHandler({ fn: (async (event) => {
|
|
1231
1328
|
const node = event.request.runtime?.node;
|
|
1232
|
-
if (!node?.req || !node?.res) throw new
|
|
1329
|
+
if (!node?.req || !node?.res) throw new AppError("fromNodeHandler/fromNodeMiddleware requires a Node.js runtime.");
|
|
1233
1330
|
const req = node.req;
|
|
1234
1331
|
const res = node.res;
|
|
1235
1332
|
if ((isMiddleware ? await callMiddleware(handler, req, res) : await callHandler(handler, req, res)) === kHandled) return null;
|
|
@@ -1277,7 +1374,7 @@ function isWebHandler(input) {
|
|
|
1277
1374
|
//#region src/handler/adapters/web/define.ts
|
|
1278
1375
|
function fromWebHandler(input) {
|
|
1279
1376
|
if (isWebHandlerProvider(input)) return fromWebHandler(input.fetch.bind(input));
|
|
1280
|
-
if (typeof input !== "function") throw new
|
|
1377
|
+
if (typeof input !== "function") throw new AppError("fromWebHandler expects a function or an object with a fetch method.");
|
|
1281
1378
|
return defineCoreHandler({ fn: (event) => input(event.request) });
|
|
1282
1379
|
}
|
|
1283
1380
|
//#endregion
|
|
@@ -1289,6 +1386,18 @@ function isHandler(input) {
|
|
|
1289
1386
|
return hasInstanceof(input, HandlerSymbol);
|
|
1290
1387
|
}
|
|
1291
1388
|
//#endregion
|
|
1389
|
+
//#region src/handler/utils.ts
|
|
1390
|
+
/**
|
|
1391
|
+
* Match a request method against a handler's bound method.
|
|
1392
|
+
*
|
|
1393
|
+
* - When the handler has no method bound, matches every request method.
|
|
1394
|
+
* - Otherwise matches when the request method is the same.
|
|
1395
|
+
* - HEAD requests additionally match GET handlers.
|
|
1396
|
+
*/
|
|
1397
|
+
function matchHandlerMethod(handlerMethod, requestMethod) {
|
|
1398
|
+
return !handlerMethod || requestMethod === handlerMethod || requestMethod === MethodName.HEAD && handlerMethod === MethodName.GET;
|
|
1399
|
+
}
|
|
1400
|
+
//#endregion
|
|
1292
1401
|
//#region src/request/helpers/cache.ts
|
|
1293
1402
|
function isRequestCacheable(event, modifiedTime) {
|
|
1294
1403
|
const modifiedSince = event.headers.get(HeaderName.IF_MODIFIED_SINCE);
|
|
@@ -1343,7 +1452,7 @@ function matchRequestContentType(event, contentType) {
|
|
|
1343
1452
|
function getRequestHostName(event, options = {}) {
|
|
1344
1453
|
let trustProxy;
|
|
1345
1454
|
if (typeof options.trustProxy !== "undefined") trustProxy = buildTrustProxyFn(options.trustProxy);
|
|
1346
|
-
else trustProxy = event.
|
|
1455
|
+
else trustProxy = event.appOptions.trustProxy ?? DEFAULT_TRUST_PROXY;
|
|
1347
1456
|
let hostname = event.headers.get(HeaderName.X_FORWARDED_HOST);
|
|
1348
1457
|
if (!hostname || !event.request.ip || !trustProxy(event.request.ip, 0)) hostname = event.headers.get(HeaderName.HOST);
|
|
1349
1458
|
else if (hostname && hostname.includes(",")) hostname = hostname.substring(0, hostname.indexOf(",")).trimEnd();
|
|
@@ -1366,7 +1475,7 @@ function getRequestHostName(event, options = {}) {
|
|
|
1366
1475
|
function getRequestIP(event, options = {}) {
|
|
1367
1476
|
let trustProxy;
|
|
1368
1477
|
if (typeof options.trustProxy !== "undefined") trustProxy = buildTrustProxyFn(options.trustProxy);
|
|
1369
|
-
else trustProxy = event.
|
|
1478
|
+
else trustProxy = event.appOptions.trustProxy ?? DEFAULT_TRUST_PROXY;
|
|
1370
1479
|
const socketAddr = event.request.ip;
|
|
1371
1480
|
if (!socketAddr) return;
|
|
1372
1481
|
const forwarded = event.headers.get(HeaderName.X_FORWARDED_FOR);
|
|
@@ -1386,7 +1495,7 @@ function getRequestIP(event, options = {}) {
|
|
|
1386
1495
|
function getRequestProtocol(event, options = {}) {
|
|
1387
1496
|
let trustProxy;
|
|
1388
1497
|
if (typeof options.trustProxy !== "undefined") trustProxy = buildTrustProxyFn(options.trustProxy);
|
|
1389
|
-
else trustProxy = event.
|
|
1498
|
+
else trustProxy = event.appOptions.trustProxy ?? DEFAULT_TRUST_PROXY;
|
|
1390
1499
|
let protocol;
|
|
1391
1500
|
try {
|
|
1392
1501
|
if (new URL(event.request.url).protocol === "https:") protocol = "https";
|
|
@@ -1403,6 +1512,58 @@ function getRequestProtocol(event, options = {}) {
|
|
|
1403
1512
|
return protocol;
|
|
1404
1513
|
}
|
|
1405
1514
|
//#endregion
|
|
1515
|
+
//#region src/path/matcher.ts
|
|
1516
|
+
function decodeParam(val) {
|
|
1517
|
+
/* istanbul ignore next */
|
|
1518
|
+
if (typeof val !== "string" || val.length === 0) return val;
|
|
1519
|
+
try {
|
|
1520
|
+
return decodeURIComponent(val);
|
|
1521
|
+
} catch {
|
|
1522
|
+
return val;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
var PathMatcher = class {
|
|
1526
|
+
path;
|
|
1527
|
+
regexp;
|
|
1528
|
+
regexpKeys = [];
|
|
1529
|
+
regexpOptions;
|
|
1530
|
+
constructor(path, options) {
|
|
1531
|
+
this.path = path;
|
|
1532
|
+
this.regexpOptions = options || {};
|
|
1533
|
+
const regexp = pathToRegexp(path, options);
|
|
1534
|
+
this.regexp = regexp.regexp;
|
|
1535
|
+
this.regexpKeys = regexp.keys;
|
|
1536
|
+
}
|
|
1537
|
+
test(path) {
|
|
1538
|
+
return this.regexp.test(path);
|
|
1539
|
+
}
|
|
1540
|
+
exec(path) {
|
|
1541
|
+
if (this.path === "/" && this.regexpOptions.end === false) return {
|
|
1542
|
+
path: "/",
|
|
1543
|
+
params: Object.create(null)
|
|
1544
|
+
};
|
|
1545
|
+
const match = this.regexp.exec(path);
|
|
1546
|
+
if (!match) return;
|
|
1547
|
+
const params = Object.create(null);
|
|
1548
|
+
for (let i = 1; i < match.length; i++) {
|
|
1549
|
+
const key = this.regexpKeys[i - 1];
|
|
1550
|
+
if (!key) continue;
|
|
1551
|
+
const prop = key.name;
|
|
1552
|
+
const val = decodeParam(match[i]);
|
|
1553
|
+
if (typeof val !== "undefined") params[prop] = val;
|
|
1554
|
+
}
|
|
1555
|
+
return {
|
|
1556
|
+
path: match[0],
|
|
1557
|
+
params
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
//#endregion
|
|
1562
|
+
//#region src/path/utils.ts
|
|
1563
|
+
function isPath(input) {
|
|
1564
|
+
return typeof input === "string";
|
|
1565
|
+
}
|
|
1566
|
+
//#endregion
|
|
1406
1567
|
//#region src/plugin/error/constants.ts
|
|
1407
1568
|
const PluginErrorCode = {
|
|
1408
1569
|
PLUGIN: "PLUGIN",
|
|
@@ -1419,7 +1580,7 @@ function isPluginError(input) {
|
|
|
1419
1580
|
}
|
|
1420
1581
|
//#endregion
|
|
1421
1582
|
//#region src/plugin/error/module.ts
|
|
1422
|
-
var PluginError = class extends
|
|
1583
|
+
var PluginError = class extends AppError {
|
|
1423
1584
|
constructor(input = {}) {
|
|
1424
1585
|
const options = typeof input === "string" ? { message: input } : { ...input };
|
|
1425
1586
|
if (!("code" in options) || !options.code) options.code = PluginErrorCode.PLUGIN;
|
|
@@ -1477,22 +1638,795 @@ function isPlugin(input) {
|
|
|
1477
1638
|
return typeof input.install === "function" && input.install.length === 1;
|
|
1478
1639
|
}
|
|
1479
1640
|
//#endregion
|
|
1480
|
-
//#region src/router/
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1641
|
+
//#region src/router/utils.ts
|
|
1642
|
+
/**
|
|
1643
|
+
* Build a path-to-regexp-backed `PathMatcher` for the route's mount
|
|
1644
|
+
* path, applying the exact-vs-prefix convention every router should
|
|
1645
|
+
* agree on:
|
|
1646
|
+
*
|
|
1647
|
+
* - `route.method !== undefined` → exact match (method-bound route)
|
|
1648
|
+
* - `route.method === undefined` → prefix match (middleware / nested
|
|
1649
|
+
* app)
|
|
1650
|
+
*
|
|
1651
|
+
* Returns `undefined` when the route has no mount path — middleware
|
|
1652
|
+
* registered without a path matches every request.
|
|
1653
|
+
*
|
|
1654
|
+
* Routers are free to ignore this helper and build their own match
|
|
1655
|
+
* mechanism (radix tree, single aggregated regex, etc.) — it's
|
|
1656
|
+
* provided as a convenience for routers that want path-to-regexp
|
|
1657
|
+
* semantics with minimal boilerplate.
|
|
1658
|
+
*/
|
|
1659
|
+
function buildRoutePathMatcher(route) {
|
|
1660
|
+
if (typeof route.path === "undefined") return;
|
|
1661
|
+
const end = typeof route.method !== "undefined";
|
|
1662
|
+
if (!end && route.path === "/") return;
|
|
1663
|
+
return new PathMatcher(route.path, { end });
|
|
1664
|
+
}
|
|
1665
|
+
//#endregion
|
|
1666
|
+
//#region src/router/linear/module.ts
|
|
1667
|
+
/**
|
|
1668
|
+
* Default router — walks registered routes linearly per request and
|
|
1669
|
+
* runs each route's mount-level matcher (built via `buildRoutePathMatcher`,
|
|
1670
|
+
* path-to-regexp-backed). Routes without a mount path (mount-less
|
|
1671
|
+
* middleware / nested apps registered via `.use(handler)`) match every
|
|
1672
|
+
* request directly — there is no per-route `matchPath()` fallback.
|
|
1673
|
+
*
|
|
1674
|
+
* Behaviour-preserving wrapper around the previous in-line stack walk
|
|
1675
|
+
* in `executePipelineStepLookup`. The matcher allocations live here
|
|
1676
|
+
* (not on the registered route), so routers using a different matching
|
|
1677
|
+
* strategy (radix tree, aggregated regex, …) can ignore this file
|
|
1678
|
+
* entirely.
|
|
1679
|
+
*
|
|
1680
|
+
* Optional per-router lookup cache: pass an `ICache` via
|
|
1681
|
+
* `BaseRouterOptions.cache` to skip the linear walk on repeated
|
|
1682
|
+
* requests for the same path. Default is no caching.
|
|
1683
|
+
*/
|
|
1684
|
+
var LinearRouter = class LinearRouter {
|
|
1685
|
+
_routes;
|
|
1686
|
+
_matchers;
|
|
1687
|
+
cache;
|
|
1688
|
+
constructor(options = {}) {
|
|
1689
|
+
this._routes = [];
|
|
1690
|
+
this._matchers = [];
|
|
1691
|
+
this.cache = options.cache;
|
|
1692
|
+
}
|
|
1693
|
+
add(route) {
|
|
1694
|
+
this._routes.push(route);
|
|
1695
|
+
this._matchers.push(buildRoutePathMatcher(route));
|
|
1696
|
+
this.cache?.clear();
|
|
1697
|
+
}
|
|
1698
|
+
lookup(path, _method) {
|
|
1699
|
+
const cached = this.cache?.get(path);
|
|
1700
|
+
if (typeof cached !== "undefined") return cached;
|
|
1701
|
+
const matches = [];
|
|
1702
|
+
for (let i = 0; i < this._routes.length; i++) {
|
|
1703
|
+
const route = this._routes[i];
|
|
1704
|
+
const matcher = this._matchers[i];
|
|
1705
|
+
if (matcher) {
|
|
1706
|
+
const output = matcher.exec(path);
|
|
1707
|
+
if (typeof output === "undefined") continue;
|
|
1708
|
+
matches.push({
|
|
1709
|
+
route,
|
|
1710
|
+
index: i,
|
|
1711
|
+
params: output.params,
|
|
1712
|
+
path: output.path
|
|
1713
|
+
});
|
|
1714
|
+
continue;
|
|
1715
|
+
}
|
|
1716
|
+
matches.push({
|
|
1717
|
+
route,
|
|
1718
|
+
index: i,
|
|
1719
|
+
params: Object.create(null)
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
this.cache?.set(path, matches);
|
|
1723
|
+
return matches;
|
|
1724
|
+
}
|
|
1725
|
+
clone() {
|
|
1726
|
+
return new LinearRouter({ cache: this.cache?.clone() });
|
|
1727
|
+
}
|
|
1728
|
+
};
|
|
1729
|
+
//#endregion
|
|
1730
|
+
//#region src/router/trie/parser.ts
|
|
1731
|
+
/**
|
|
1732
|
+
* Trie-native path parser.
|
|
1733
|
+
*
|
|
1734
|
+
* Replaces the call-out to `path-to-regexp` for the syntax surface
|
|
1735
|
+
* the trie advertises:
|
|
1736
|
+
*
|
|
1737
|
+
* - Static segments: `users`, `v1`
|
|
1738
|
+
* - Named params: `:id`, `:slug`
|
|
1739
|
+
* - Optional params: `:id?` (T2 — expanded to two variants)
|
|
1740
|
+
* - Optional groups: `{...}` (T2 — expanded to two variants)
|
|
1741
|
+
* - Bare splat: `*` (matches the rest of the path)
|
|
1742
|
+
* - Named splat: `*rest`
|
|
1743
|
+
*
|
|
1744
|
+
* Returns a list of `Segment[]` *variants* — one path string can
|
|
1745
|
+
* expand into multiple route variants when it contains optional
|
|
1746
|
+
* markers. The trie inserts every variant with the same registration
|
|
1747
|
+
* `index` so they dedupe naturally on the candidate list.
|
|
1748
|
+
*
|
|
1749
|
+
* Returns `null` when the path uses syntax outside this surface
|
|
1750
|
+
* (compound segments `/files/:n.ext`, escape sequences `\:`, regex
|
|
1751
|
+
* constraints, splat-not-last, …). The caller falls back to the
|
|
1752
|
+
* universal bucket so correctness is preserved via path-to-regexp.
|
|
1753
|
+
*
|
|
1754
|
+
* Variant cap: nested optional groups expand combinatorially. The
|
|
1755
|
+
* parser caps the variant count at `MAX_VARIANTS` and falls back to
|
|
1756
|
+
* the universal bucket above that — registration-time explosion
|
|
1757
|
+
* isn't worth the lookup-time win on degenerate paths.
|
|
1758
|
+
*/
|
|
1759
|
+
const MAX_VARIANTS = 16;
|
|
1760
|
+
const PARAM_NAME = /^[a-zA-Z_]\w*$/;
|
|
1761
|
+
/**
|
|
1762
|
+
* Tokenize a single path segment (the substring between two `/`).
|
|
1763
|
+
* Returns `null` if the segment uses syntax we don't handle.
|
|
1764
|
+
*
|
|
1765
|
+
* Each segment must yield exactly one token — compound segments
|
|
1766
|
+
* (`/files/:n.ext`, `/users-:id`) produce multiple tokens and so
|
|
1767
|
+
* trip this check, falling back to the universal bucket.
|
|
1768
|
+
*/
|
|
1769
|
+
function tokenizeSegment(segment) {
|
|
1770
|
+
if (segment === "") return null;
|
|
1771
|
+
if (segment === "*") return {
|
|
1772
|
+
kind: "splat",
|
|
1773
|
+
name: "*"
|
|
1774
|
+
};
|
|
1775
|
+
if (segment.charAt(0) === "*") {
|
|
1776
|
+
const rest = segment.slice(1);
|
|
1777
|
+
if (PARAM_NAME.test(rest)) return {
|
|
1778
|
+
kind: "splat",
|
|
1779
|
+
name: rest
|
|
1780
|
+
};
|
|
1781
|
+
return null;
|
|
1782
|
+
}
|
|
1783
|
+
if (segment.charAt(0) === ":") {
|
|
1784
|
+
const optional = segment.charAt(segment.length - 1) === "?";
|
|
1785
|
+
const nameRaw = optional ? segment.slice(1, -1) : segment.slice(1);
|
|
1786
|
+
if (PARAM_NAME.test(nameRaw)) return {
|
|
1787
|
+
kind: "param",
|
|
1788
|
+
name: nameRaw,
|
|
1789
|
+
optional
|
|
1790
|
+
};
|
|
1791
|
+
return null;
|
|
1792
|
+
}
|
|
1793
|
+
if (/^[a-zA-Z0-9_\-.~%]+$/.test(segment)) return {
|
|
1794
|
+
kind: "literal",
|
|
1795
|
+
value: segment
|
|
1796
|
+
};
|
|
1797
|
+
return null;
|
|
1798
|
+
}
|
|
1799
|
+
/**
|
|
1800
|
+
* Tokenize the full path into a token stream, recognizing slash-
|
|
1801
|
+
* spanning optional groups (`/users{/edit/:id}`).
|
|
1802
|
+
*
|
|
1803
|
+
* The group-open marker is emitted in place of the leading `/`
|
|
1804
|
+
* inside `{...}`; group-close before the trailing `}`. The walker
|
|
1805
|
+
* later expands by either keeping the run between markers or
|
|
1806
|
+
* dropping it.
|
|
1807
|
+
*/
|
|
1808
|
+
function tokenizePath(path) {
|
|
1809
|
+
const trimmed = path.charAt(0) === "/" ? path.slice(1) : path;
|
|
1810
|
+
if (trimmed === "") return [];
|
|
1811
|
+
const tokens = [];
|
|
1812
|
+
let i = 0;
|
|
1813
|
+
const n = trimmed.length;
|
|
1814
|
+
while (i < n) {
|
|
1815
|
+
if (trimmed.charAt(i) === "{") {
|
|
1816
|
+
let close = -1;
|
|
1817
|
+
for (let j = i + 1; j < n; j++) {
|
|
1818
|
+
const c = trimmed.charAt(j);
|
|
1819
|
+
if (c === "{") return null;
|
|
1820
|
+
if (c === "}") {
|
|
1821
|
+
close = j;
|
|
1822
|
+
break;
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
if (close === -1) return null;
|
|
1826
|
+
const inner = trimmed.slice(i + 1, close);
|
|
1827
|
+
if (inner.charAt(0) !== "/") return null;
|
|
1828
|
+
const after = close + 1 < n ? trimmed.charAt(close + 1) : "";
|
|
1829
|
+
if (after !== "" && after !== "/" && after !== "{") return null;
|
|
1830
|
+
tokens.push({ kind: "groupOpen" });
|
|
1831
|
+
const innerTokens = tokenizePath(inner);
|
|
1832
|
+
if (innerTokens === null) return null;
|
|
1833
|
+
for (const t of innerTokens) tokens.push(t);
|
|
1834
|
+
tokens.push({ kind: "groupClose" });
|
|
1835
|
+
i = close + 1;
|
|
1836
|
+
continue;
|
|
1837
|
+
}
|
|
1838
|
+
let segEnd = i;
|
|
1839
|
+
while (segEnd < n) {
|
|
1840
|
+
const c = trimmed.charAt(segEnd);
|
|
1841
|
+
if (c === "/" || c === "{") break;
|
|
1842
|
+
segEnd++;
|
|
1843
|
+
}
|
|
1844
|
+
const segment = trimmed.slice(i, segEnd);
|
|
1845
|
+
if (segment !== "") {
|
|
1846
|
+
const token = tokenizeSegment(segment);
|
|
1847
|
+
if (token === null) return null;
|
|
1848
|
+
tokens.push(token);
|
|
1849
|
+
}
|
|
1850
|
+
i = segEnd;
|
|
1851
|
+
if (i < n && trimmed.charAt(i) === "/") i++;
|
|
1852
|
+
}
|
|
1853
|
+
return tokens;
|
|
1854
|
+
}
|
|
1855
|
+
/**
|
|
1856
|
+
* Expand the token stream into one or more concrete `Token[]`
|
|
1857
|
+
* variants by:
|
|
1858
|
+
* 1. Splitting `groupOpen` … `groupClose` runs into a "with run"
|
|
1859
|
+
* and a "without run" choice (one optional group → ×2 variants).
|
|
1860
|
+
* 2. Splitting `param.optional = true` into a "with" and "without"
|
|
1861
|
+
* choice (one `:id?` → ×2 variants).
|
|
1862
|
+
*
|
|
1863
|
+
* Caps at `MAX_VARIANTS` — beyond that, returns `null` so the path
|
|
1864
|
+
* falls back to the universal bucket.
|
|
1865
|
+
*
|
|
1866
|
+
* Returns `Token[][]` (not `Segment[][]`) so the recursive
|
|
1867
|
+
* group-expansion can splice inner variants back into the outer
|
|
1868
|
+
* token stream without a lossy round-trip through `Segment`.
|
|
1869
|
+
* `parsePath` does the final `Token → Segment` projection.
|
|
1870
|
+
*/
|
|
1871
|
+
function expand(tokens) {
|
|
1872
|
+
let variants = [[]];
|
|
1873
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1874
|
+
const token = tokens[i];
|
|
1875
|
+
if (token.kind === "groupOpen") {
|
|
1876
|
+
let depth = 1;
|
|
1877
|
+
let close = -1;
|
|
1878
|
+
for (let j = i + 1; j < tokens.length; j++) {
|
|
1879
|
+
const t = tokens[j];
|
|
1880
|
+
if (t.kind === "groupOpen") depth++;
|
|
1881
|
+
else if (t.kind === "groupClose") {
|
|
1882
|
+
depth--;
|
|
1883
|
+
if (depth === 0) {
|
|
1884
|
+
close = j;
|
|
1885
|
+
break;
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
if (close === -1) return null;
|
|
1890
|
+
const expandedInner = expand(tokens.slice(i + 1, close));
|
|
1891
|
+
if (expandedInner === null) return null;
|
|
1892
|
+
const next = [];
|
|
1893
|
+
for (const v of variants) {
|
|
1894
|
+
if (next.length >= MAX_VARIANTS) return null;
|
|
1895
|
+
next.push(v.slice());
|
|
1896
|
+
for (const innerVariant of expandedInner) {
|
|
1897
|
+
if (next.length >= MAX_VARIANTS) return null;
|
|
1898
|
+
next.push(v.concat(innerVariant));
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
variants = next;
|
|
1902
|
+
i = close;
|
|
1903
|
+
continue;
|
|
1904
|
+
}
|
|
1905
|
+
if (token.kind === "param" && token.optional) {
|
|
1906
|
+
const stripped = {
|
|
1907
|
+
kind: "param",
|
|
1908
|
+
name: token.name,
|
|
1909
|
+
optional: false
|
|
1910
|
+
};
|
|
1911
|
+
const next = [];
|
|
1912
|
+
for (const v of variants) {
|
|
1913
|
+
if (next.length >= MAX_VARIANTS) return null;
|
|
1914
|
+
next.push(v.slice());
|
|
1915
|
+
if (next.length >= MAX_VARIANTS) return null;
|
|
1916
|
+
next.push(v.concat([stripped]));
|
|
1917
|
+
}
|
|
1918
|
+
variants = next;
|
|
1919
|
+
continue;
|
|
1920
|
+
}
|
|
1921
|
+
for (const v of variants) v.push(token);
|
|
1922
|
+
}
|
|
1923
|
+
return variants;
|
|
1924
|
+
}
|
|
1925
|
+
function tokenToSegment(t) {
|
|
1926
|
+
if (t.kind === "literal") return {
|
|
1927
|
+
kind: "static",
|
|
1928
|
+
value: t.value
|
|
1929
|
+
};
|
|
1930
|
+
if (t.kind === "param") return {
|
|
1931
|
+
kind: "param",
|
|
1932
|
+
name: t.name
|
|
1933
|
+
};
|
|
1934
|
+
if (t.kind === "splat") return {
|
|
1935
|
+
kind: "splat",
|
|
1936
|
+
name: t.name
|
|
1937
|
+
};
|
|
1938
|
+
return null;
|
|
1939
|
+
}
|
|
1940
|
+
/**
|
|
1941
|
+
* Stable, structural identity for a variant — used to drop duplicate
|
|
1942
|
+
* expansions like `/users{/:id?}` (which produces the bare-`/users`
|
|
1943
|
+
* variant twice: once from the "without group" branch and once from
|
|
1944
|
+
* the "with group, without optional param" branch).
|
|
1945
|
+
*/
|
|
1946
|
+
function variantKey(segs) {
|
|
1947
|
+
let out = "";
|
|
1948
|
+
for (const s of segs) if (s.kind === "static") out += `/s:${s.value}`;
|
|
1949
|
+
else if (s.kind === "param") out += `/p:${s.name}`;
|
|
1950
|
+
else out += `/*:${s.name}`;
|
|
1951
|
+
return out;
|
|
1952
|
+
}
|
|
1953
|
+
function parsePath(path) {
|
|
1954
|
+
const tokens = tokenizePath(path);
|
|
1955
|
+
if (tokens === null) return null;
|
|
1956
|
+
const variants = expand(tokens);
|
|
1957
|
+
if (variants === null) return null;
|
|
1958
|
+
const result = [];
|
|
1959
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1960
|
+
for (const v of variants) {
|
|
1961
|
+
const segs = [];
|
|
1962
|
+
for (const t of v) {
|
|
1963
|
+
const s = tokenToSegment(t);
|
|
1964
|
+
if (s === null) return null;
|
|
1965
|
+
segs.push(s);
|
|
1966
|
+
}
|
|
1967
|
+
for (let i = 0; i < segs.length - 1; i++) if (segs[i].kind === "splat") return null;
|
|
1968
|
+
const key = variantKey(segs);
|
|
1969
|
+
if (seen.has(key)) continue;
|
|
1970
|
+
seen.add(key);
|
|
1971
|
+
result.push(segs);
|
|
1972
|
+
}
|
|
1973
|
+
return result;
|
|
1974
|
+
}
|
|
1975
|
+
//#endregion
|
|
1976
|
+
//#region src/router/trie/node.ts
|
|
1977
|
+
function createMethodBuckets() {
|
|
1978
|
+
return Object.create(null);
|
|
1979
|
+
}
|
|
1980
|
+
function createTrieNode() {
|
|
1981
|
+
return {
|
|
1982
|
+
staticChildren: /* @__PURE__ */ new Map(),
|
|
1983
|
+
splatRoutes: createMethodBuckets(),
|
|
1984
|
+
exactRoutes: createMethodBuckets(),
|
|
1985
|
+
prefixRoutes: []
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
//#endregion
|
|
1989
|
+
//#region src/router/trie/module.ts
|
|
1990
|
+
function decodeOrRaw(s) {
|
|
1991
|
+
try {
|
|
1992
|
+
return decodeURIComponent(s);
|
|
1993
|
+
} catch {
|
|
1994
|
+
return s;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
/**
|
|
1998
|
+
* Build a `params` object from a request's pre-split segments using
|
|
1999
|
+
* the variant's pre-computed `ParamCapture[]`. No regex execution —
|
|
2000
|
+
* each capture is one indexed read from `segments` (and a join for
|
|
2001
|
+
* splats). Replaces the `matcher.exec` confirm pass for trie-walked
|
|
2002
|
+
* routes (T3).
|
|
2003
|
+
*/
|
|
2004
|
+
function extractTrieParams(segments, indexMap) {
|
|
2005
|
+
const out = Object.create(null);
|
|
2006
|
+
for (const cap of indexMap) if (cap.kind === "segment") out[cap.name] = decodeOrRaw(segments[cap.depth]);
|
|
2007
|
+
else {
|
|
2008
|
+
const slice = segments.slice(cap.depth).join("/");
|
|
2009
|
+
out[cap.name] = decodeOrRaw(slice);
|
|
2010
|
+
}
|
|
2011
|
+
return out;
|
|
2012
|
+
}
|
|
2013
|
+
/**
|
|
2014
|
+
* Compute `match.path` (the matched-prefix string) from the request's
|
|
2015
|
+
* segments and the variant's recorded depth.
|
|
2016
|
+
*
|
|
2017
|
+
* - Non-splat variants: prefix = `segments[0..matchDepth].join('/')` —
|
|
2018
|
+
* exactly what the variant consumed (request length = variant
|
|
2019
|
+
* length for exact, request length ≥ variant length for prefix).
|
|
2020
|
+
* - Splat variants: the splat absorbed every remaining segment, so
|
|
2021
|
+
* the matched prefix is the entire request path. This mirrors what
|
|
2022
|
+
* `path-to-regexp`'s `output.path` would have returned pre-Phase-2
|
|
2023
|
+
* for `/files/*rest` matching `/files/a/b` (`/files/a/b`, not
|
|
2024
|
+
* `/files`).
|
|
2025
|
+
*/
|
|
2026
|
+
function trieMatchedPath(segments, matchDepth, splatTerminated) {
|
|
2027
|
+
const upTo = splatTerminated ? segments.length : matchDepth;
|
|
2028
|
+
if (upTo === 0) return "/";
|
|
2029
|
+
return `/${segments.slice(0, upTo).join("/")}`;
|
|
2030
|
+
}
|
|
2031
|
+
/**
|
|
2032
|
+
* Pre-compute the `ParamCapture[]` for a variant's segments. Walk
|
|
2033
|
+
* the segments in order; emit one entry per `param` segment and a
|
|
2034
|
+
* terminal one for `splat` (always last). Static segments are
|
|
2035
|
+
* structurally consumed by the trie walk; they don't appear here.
|
|
2036
|
+
*/
|
|
2037
|
+
function buildParamsIndexMap(segments) {
|
|
2038
|
+
const out = [];
|
|
2039
|
+
for (const [i, seg] of segments.entries()) if (seg.kind === "param") out.push({
|
|
2040
|
+
kind: "segment",
|
|
2041
|
+
depth: i,
|
|
2042
|
+
name: seg.name
|
|
2043
|
+
});
|
|
2044
|
+
else if (seg.kind === "splat") {
|
|
2045
|
+
out.push({
|
|
2046
|
+
kind: "splat",
|
|
2047
|
+
depth: i,
|
|
2048
|
+
name: seg.name
|
|
2049
|
+
});
|
|
2050
|
+
break;
|
|
2051
|
+
}
|
|
2052
|
+
return out;
|
|
2053
|
+
}
|
|
2054
|
+
/**
|
|
2055
|
+
* Decide which method buckets a given request method should pull
|
|
2056
|
+
* from. Always includes `''` (method-agnostic). For HEAD also
|
|
2057
|
+
* includes GET (per `matchHandlerMethod`). For OPTIONS or no-method
|
|
2058
|
+
* lookups, returns `null` to signal "emit every bucket" — needed so
|
|
2059
|
+
* `event.methodsAllowed` is populated for OPTIONS auto-Allow and so
|
|
2060
|
+
* `IRouter.lookup(path)` (no method) keeps returning a complete
|
|
2061
|
+
* candidate set.
|
|
2062
|
+
*/
|
|
2063
|
+
function methodBucketKeys(method) {
|
|
2064
|
+
if (typeof method === "undefined" || method === MethodName.OPTIONS) return null;
|
|
2065
|
+
if (method === MethodName.HEAD) return [
|
|
2066
|
+
"",
|
|
2067
|
+
MethodName.HEAD,
|
|
2068
|
+
MethodName.GET
|
|
2069
|
+
];
|
|
2070
|
+
return ["", method];
|
|
2071
|
+
}
|
|
2072
|
+
function emitBucket(buckets, method, out) {
|
|
2073
|
+
const keys = methodBucketKeys(method);
|
|
2074
|
+
if (keys === null) {
|
|
2075
|
+
for (const k in buckets) {
|
|
2076
|
+
const list = buckets[k];
|
|
2077
|
+
for (const r of list) out.push(r);
|
|
2078
|
+
}
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
for (const k of keys) {
|
|
2082
|
+
const list = buckets[k];
|
|
2083
|
+
if (!list) continue;
|
|
2084
|
+
for (const r of list) out.push(r);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
function hasAnyBucket(buckets) {
|
|
2088
|
+
for (const _k in buckets) return true;
|
|
2089
|
+
return false;
|
|
2090
|
+
}
|
|
2091
|
+
function pushIntoBucket(buckets, methodKey, route) {
|
|
2092
|
+
const bucket = buckets[methodKey];
|
|
2093
|
+
if (bucket) bucket.push(route);
|
|
2094
|
+
else buckets[methodKey] = [route];
|
|
2095
|
+
}
|
|
2096
|
+
/**
|
|
2097
|
+
* Radix-trie router — registers routes into a per-segment tree at
|
|
2098
|
+
* `add()` time and walks the tree at `lookup()` to collect
|
|
2099
|
+
* candidates by structure rather than by linear scan.
|
|
2100
|
+
*
|
|
2101
|
+
* Inspired by Hono's `TrieRouter` and rou3. The trie handles
|
|
2102
|
+
* routup's path vocabulary directly via its own parser
|
|
2103
|
+
* (`./parser.ts`):
|
|
2104
|
+
*
|
|
2105
|
+
* - Static segments (`/users`)
|
|
2106
|
+
* - Named params (`:id`)
|
|
2107
|
+
* - Optional params (`:id?`) — expanded to two route variants at
|
|
2108
|
+
* registration (T2)
|
|
2109
|
+
* - Optional groups (`/users{/edit}`) — same expansion strategy
|
|
2110
|
+
* - Bare and named splats (`/files/*`, `/files/*rest`)
|
|
2111
|
+
*
|
|
2112
|
+
* Per-leaf storage is bucketed by HTTP method (T4) so lookup
|
|
2113
|
+
* narrows to the request method's bucket(s) instead of emitting
|
|
2114
|
+
* every entry at the leaf and letting the dispatcher's filter
|
|
2115
|
+
* discard mismatches.
|
|
2116
|
+
*
|
|
2117
|
+
* Param extraction is `paramsIndexMap`-driven (T3): a pre-built
|
|
2118
|
+
* `Array<{ depth, name }>` per variant lets `extractTrieParams`
|
|
2119
|
+
* read params straight from the request's pre-split segments — no
|
|
2120
|
+
* regex execution per match.
|
|
2121
|
+
*
|
|
2122
|
+
* Paths the trie parser doesn't handle (compound segments like
|
|
2123
|
+
* `/files/:n.ext`, escape sequences `\:`, regex constraints) and
|
|
2124
|
+
* empty/root paths fall through to the `universal` bucket. That
|
|
2125
|
+
* bucket still uses `path-to-regexp` via `buildRoutePathMatcher`,
|
|
2126
|
+
* so correctness is preserved.
|
|
2127
|
+
*
|
|
2128
|
+
* Pure-static-spine fast path (`shortCircuit`): when the request
|
|
2129
|
+
* walks a static spine with no param/splat/prefix siblings on any
|
|
2130
|
+
* traversed node, the leaf's `exactRoutes` (filtered to the request
|
|
2131
|
+
* method's buckets) is the full answer — no need to walk the param
|
|
2132
|
+
* branch or collect prefix candidates at intermediate nodes.
|
|
2133
|
+
*/
|
|
2134
|
+
var TrieRouter = class TrieRouter {
|
|
2135
|
+
/**
|
|
2136
|
+
* Monotonic counter assigned as the registration `index` on each
|
|
2137
|
+
* route — the dispatch loop uses it to preserve registration
|
|
2138
|
+
* order across the candidate list. App owns the canonical
|
|
2139
|
+
* `Route<T>[]` list (Plan 019); the trie no longer keeps a
|
|
2140
|
+
* parallel copy.
|
|
2141
|
+
*/
|
|
2142
|
+
_routeCount;
|
|
2143
|
+
root;
|
|
2144
|
+
/**
|
|
2145
|
+
* Routes that bypass the trie — registered with no path, with
|
|
2146
|
+
* the root path `/`, or with a path containing syntax the
|
|
2147
|
+
* parser doesn't recognise. Walked linearly on every lookup,
|
|
2148
|
+
* merged into the result in registration order.
|
|
2149
|
+
*/
|
|
2150
|
+
universal;
|
|
2151
|
+
cache;
|
|
2152
|
+
constructor(options = {}) {
|
|
2153
|
+
this._routeCount = 0;
|
|
2154
|
+
this.root = createTrieNode();
|
|
2155
|
+
this.universal = [];
|
|
2156
|
+
this.cache = options.cache;
|
|
2157
|
+
}
|
|
2158
|
+
add(route) {
|
|
2159
|
+
const index = this._routeCount++;
|
|
2160
|
+
if (typeof route.path !== "string" || route.path === "" || route.path === "/") {
|
|
2161
|
+
this.universal.push({
|
|
2162
|
+
route,
|
|
2163
|
+
index,
|
|
2164
|
+
matcher: buildRoutePathMatcher(route)
|
|
2165
|
+
});
|
|
2166
|
+
this.cache?.clear();
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
const variants = parsePath(route.path);
|
|
2170
|
+
if (variants === null) {
|
|
2171
|
+
this.universal.push({
|
|
2172
|
+
route,
|
|
2173
|
+
index,
|
|
2174
|
+
matcher: buildRoutePathMatcher(route)
|
|
2175
|
+
});
|
|
2176
|
+
this.cache?.clear();
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
for (const segments of variants) this.insertIntoTrie(segments, route, index);
|
|
2180
|
+
this.cache?.clear();
|
|
2181
|
+
}
|
|
2182
|
+
lookup(path, method) {
|
|
2183
|
+
const cacheKey = `${method ?? ""}\t${path}`;
|
|
2184
|
+
const cached = this.cache?.get(cacheKey);
|
|
2185
|
+
if (typeof cached !== "undefined") return cached;
|
|
2186
|
+
const candidates = [];
|
|
2187
|
+
for (const u of this.universal) candidates.push(u);
|
|
2188
|
+
const segments = this.parseRequestPath(path);
|
|
2189
|
+
const shortCircuit = this.shortCircuit(segments, method);
|
|
2190
|
+
if (shortCircuit !== null) for (const c of shortCircuit) candidates.push(c);
|
|
2191
|
+
else this.walk(this.root, segments, 0, candidates, method);
|
|
2192
|
+
candidates.sort((a, b) => {
|
|
2193
|
+
if (a.index !== b.index) return a.index - b.index;
|
|
2194
|
+
const ad = a.matchDepth ?? -1;
|
|
2195
|
+
return (b.matchDepth ?? -1) - ad;
|
|
2196
|
+
});
|
|
2197
|
+
const matches = [];
|
|
2198
|
+
let lastIndex = -1;
|
|
2199
|
+
for (const candidate of candidates) {
|
|
2200
|
+
const { route, index, matcher, paramsIndexMap, matchDepth } = candidate;
|
|
2201
|
+
if (index === lastIndex) continue;
|
|
2202
|
+
if (matcher) {
|
|
2203
|
+
const output = matcher.exec(path);
|
|
2204
|
+
if (typeof output === "undefined") continue;
|
|
2205
|
+
matches.push({
|
|
2206
|
+
route,
|
|
2207
|
+
index,
|
|
2208
|
+
params: this.assignParams(output.params),
|
|
2209
|
+
path: output.path
|
|
2210
|
+
});
|
|
2211
|
+
lastIndex = index;
|
|
2212
|
+
continue;
|
|
2213
|
+
}
|
|
2214
|
+
if (paramsIndexMap && typeof matchDepth === "number") {
|
|
2215
|
+
matches.push({
|
|
2216
|
+
route,
|
|
2217
|
+
index,
|
|
2218
|
+
params: extractTrieParams(segments, paramsIndexMap),
|
|
2219
|
+
path: trieMatchedPath(segments, matchDepth, candidate.splatTerminated === true)
|
|
2220
|
+
});
|
|
2221
|
+
lastIndex = index;
|
|
2222
|
+
continue;
|
|
2223
|
+
}
|
|
2224
|
+
matches.push({
|
|
2225
|
+
route,
|
|
2226
|
+
index,
|
|
2227
|
+
params: Object.create(null)
|
|
2228
|
+
});
|
|
2229
|
+
lastIndex = index;
|
|
2230
|
+
}
|
|
2231
|
+
this.cache?.set(cacheKey, matches);
|
|
2232
|
+
return matches;
|
|
2233
|
+
}
|
|
2234
|
+
clone() {
|
|
2235
|
+
return new TrieRouter({ cache: this.cache?.clone() });
|
|
2236
|
+
}
|
|
2237
|
+
/**
|
|
2238
|
+
* T1: returns the pre-computed candidate list when the request's
|
|
2239
|
+
* static spine has no param sibling, no prefix routes, and no
|
|
2240
|
+
* splats along the way. The leaf node's `exactRoutes` (filtered
|
|
2241
|
+
* to the request method's buckets) is then the complete answer —
|
|
2242
|
+
* no need to walk the param branch or collect prefix/splat
|
|
2243
|
+
* candidates from intermediate nodes. When any branch is
|
|
2244
|
+
* encountered, returns `null` and the caller falls through to
|
|
2245
|
+
* the regular `walk`.
|
|
2246
|
+
*/
|
|
2247
|
+
shortCircuit(segments, method) {
|
|
2248
|
+
let node = this.root;
|
|
2249
|
+
for (const segment of segments) {
|
|
2250
|
+
if (node.paramChild || hasAnyBucket(node.splatRoutes) || node.prefixRoutes.length > 0) return null;
|
|
2251
|
+
const child = node.staticChildren.get(segment);
|
|
2252
|
+
if (!child) return null;
|
|
2253
|
+
node = child;
|
|
2254
|
+
}
|
|
2255
|
+
if (node.paramChild || hasAnyBucket(node.splatRoutes) || node.prefixRoutes.length > 0) return null;
|
|
2256
|
+
const out = [];
|
|
2257
|
+
emitBucket(node.exactRoutes, method, out);
|
|
2258
|
+
return out;
|
|
2259
|
+
}
|
|
2260
|
+
parseRequestPath(path) {
|
|
2261
|
+
const trimmed = path.charAt(0) === "/" ? path.slice(1) : path;
|
|
2262
|
+
if (trimmed === "") return [];
|
|
2263
|
+
const parts = trimmed.split("/");
|
|
2264
|
+
const result = [];
|
|
2265
|
+
for (const part of parts) if (part !== "") result.push(part);
|
|
2266
|
+
return result;
|
|
2267
|
+
}
|
|
2268
|
+
insertIntoTrie(segments, route, index) {
|
|
2269
|
+
let node = this.root;
|
|
2270
|
+
const exact = this.isExactMatchRoute(route);
|
|
2271
|
+
const methodKey = route.method ?? "";
|
|
2272
|
+
const paramsIndexMap = buildParamsIndexMap(segments);
|
|
2273
|
+
for (const [i, segment] of segments.entries()) {
|
|
2274
|
+
const seg = segment;
|
|
2275
|
+
if (seg.kind === "splat") {
|
|
2276
|
+
pushIntoBucket(node.splatRoutes, methodKey, {
|
|
2277
|
+
route,
|
|
2278
|
+
index,
|
|
2279
|
+
paramsIndexMap,
|
|
2280
|
+
matchDepth: i,
|
|
2281
|
+
splatTerminated: true
|
|
2282
|
+
});
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
if (seg.kind === "param") {
|
|
2286
|
+
if (!node.paramChild) node.paramChild = createTrieNode();
|
|
2287
|
+
node = node.paramChild;
|
|
2288
|
+
continue;
|
|
2289
|
+
}
|
|
2290
|
+
let child = node.staticChildren.get(seg.value);
|
|
2291
|
+
if (!child) {
|
|
2292
|
+
child = createTrieNode();
|
|
2293
|
+
node.staticChildren.set(seg.value, child);
|
|
2294
|
+
}
|
|
2295
|
+
node = child;
|
|
2296
|
+
}
|
|
2297
|
+
const indexed = {
|
|
2298
|
+
route,
|
|
2299
|
+
index,
|
|
2300
|
+
paramsIndexMap,
|
|
2301
|
+
matchDepth: segments.length
|
|
2302
|
+
};
|
|
2303
|
+
if (exact) pushIntoBucket(node.exactRoutes, methodKey, indexed);
|
|
2304
|
+
else node.prefixRoutes.push(indexed);
|
|
2305
|
+
}
|
|
2306
|
+
walk(node, segments, depth, collected, method) {
|
|
2307
|
+
emitBucket(node.splatRoutes, method, collected);
|
|
2308
|
+
if (depth === segments.length) {
|
|
2309
|
+
emitBucket(node.exactRoutes, method, collected);
|
|
2310
|
+
for (const p of node.prefixRoutes) collected.push(p);
|
|
2311
|
+
return;
|
|
2312
|
+
}
|
|
2313
|
+
for (const p of node.prefixRoutes) collected.push(p);
|
|
2314
|
+
const seg = segments[depth];
|
|
2315
|
+
const staticChild = node.staticChildren.get(seg);
|
|
2316
|
+
if (staticChild) this.walk(staticChild, segments, depth + 1, collected, method);
|
|
2317
|
+
if (node.paramChild) this.walk(node.paramChild, segments, depth + 1, collected, method);
|
|
2318
|
+
}
|
|
2319
|
+
isExactMatchRoute(route) {
|
|
2320
|
+
return typeof route.method !== "undefined";
|
|
2321
|
+
}
|
|
2322
|
+
/**
|
|
2323
|
+
* T5: copy params onto a prototype-less object so downstream
|
|
2324
|
+
* lookups skip prototype-chain traversal and avoid `__proto__` /
|
|
2325
|
+
* `hasOwnProperty` shadowing from user-controlled segment values.
|
|
2326
|
+
*/
|
|
2327
|
+
assignParams(source) {
|
|
2328
|
+
const out = Object.create(null);
|
|
2329
|
+
for (const k in source) if (Object.prototype.hasOwnProperty.call(source, k)) out[k] = source[k];
|
|
2330
|
+
return out;
|
|
2331
|
+
}
|
|
2332
|
+
};
|
|
2333
|
+
//#endregion
|
|
2334
|
+
//#region src/router/smart/module.ts
|
|
2335
|
+
/**
|
|
2336
|
+
* Default crossover. Empirically `LinearRouter` wins for small route
|
|
2337
|
+
* counts (no per-request trie walk overhead, no static-spine setup);
|
|
2338
|
+
* `TrieRouter` wins past ~30 entries on typical workloads. Override
|
|
2339
|
+
* via `SmartRouterOptions.threshold` when a benchmark says otherwise
|
|
2340
|
+
* for your route shape.
|
|
2341
|
+
*/
|
|
2342
|
+
const DEFAULT_THRESHOLD = 30;
|
|
2343
|
+
/**
|
|
2344
|
+
* Auto-selecting router. Accumulates registered routes in a pending
|
|
2345
|
+
* buffer; on the first `lookup()` call, picks `LinearRouter` or
|
|
2346
|
+
* `TrieRouter` based on the registered route count and replays the
|
|
2347
|
+
* pending list onto the chosen inner router. Every subsequent call
|
|
2348
|
+
* — `add`, `lookup`, `clone` — forwards to the inner.
|
|
2349
|
+
*
|
|
2350
|
+
* Use this when you don't want to commit to a router family up-front
|
|
2351
|
+
* (e.g. a library that registers a variable number of routes
|
|
2352
|
+
* depending on configuration). For known workloads, prefer the
|
|
2353
|
+
* concrete router — `SmartRouter` adds one indirection per call.
|
|
2354
|
+
*
|
|
2355
|
+
* Inspired by Hono's `SmartRouter` (which auto-selects across more
|
|
2356
|
+
* candidates including `RegExpRouter`); ours covers the only choice
|
|
2357
|
+
* that matters in routup today: linear-vs-trie at the registration-
|
|
2358
|
+
* size crossover.
|
|
2359
|
+
*/
|
|
2360
|
+
var SmartRouter = class SmartRouter {
|
|
2361
|
+
inner;
|
|
2362
|
+
pending = [];
|
|
2363
|
+
threshold;
|
|
2364
|
+
/**
|
|
2365
|
+
* Cache handed off to whichever inner router gets chosen. Stays
|
|
2366
|
+
* `undefined` if the user didn't configure one.
|
|
2367
|
+
*/
|
|
2368
|
+
cache;
|
|
2369
|
+
constructor(options = {}) {
|
|
2370
|
+
this.threshold = options.threshold ?? DEFAULT_THRESHOLD;
|
|
2371
|
+
this.cache = options.cache;
|
|
2372
|
+
}
|
|
2373
|
+
add(route) {
|
|
2374
|
+
if (this.inner) {
|
|
2375
|
+
this.inner.add(route);
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2378
|
+
this.pending.push(route);
|
|
2379
|
+
}
|
|
2380
|
+
lookup(path, method) {
|
|
2381
|
+
if (!this.inner) {
|
|
2382
|
+
this.inner = this.choose();
|
|
2383
|
+
for (const r of this.pending) this.inner.add(r);
|
|
2384
|
+
this.pending = [];
|
|
2385
|
+
}
|
|
2386
|
+
return this.inner.lookup(path, method);
|
|
2387
|
+
}
|
|
2388
|
+
clone() {
|
|
2389
|
+
return new SmartRouter({
|
|
2390
|
+
threshold: this.threshold,
|
|
2391
|
+
cache: this.cache?.clone()
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
2394
|
+
/**
|
|
2395
|
+
* Pick the inner router based on the registered route count.
|
|
2396
|
+
* `LinearRouter` for tiny tables, `TrieRouter` past the
|
|
2397
|
+
* configured threshold.
|
|
2398
|
+
*
|
|
2399
|
+
* @protected
|
|
2400
|
+
*/
|
|
2401
|
+
choose() {
|
|
2402
|
+
if (this.pending.length < this.threshold) return new LinearRouter({ cache: this.cache });
|
|
2403
|
+
return new TrieRouter({ cache: this.cache });
|
|
2404
|
+
}
|
|
2405
|
+
};
|
|
2406
|
+
//#endregion
|
|
2407
|
+
//#region src/app/options.ts
|
|
2408
|
+
function normalizeAppOptions(input) {
|
|
2409
|
+
let etag;
|
|
2410
|
+
if (typeof input.etag !== "undefined") if (input.etag === null || input.etag === false) etag = null;
|
|
2411
|
+
else etag = buildEtagFn(input.etag);
|
|
2412
|
+
let trustProxy;
|
|
2413
|
+
if (typeof input.trustProxy !== "undefined") trustProxy = buildTrustProxyFn(input.trustProxy);
|
|
1484
2414
|
if (typeof input.timeout !== "undefined") {
|
|
1485
|
-
if (
|
|
2415
|
+
if (!Number.isFinite(input.timeout) || input.timeout <= 0) delete input.timeout;
|
|
1486
2416
|
}
|
|
1487
2417
|
if (typeof input.handlerTimeout !== "undefined") {
|
|
1488
|
-
if (
|
|
2418
|
+
if (!Number.isFinite(input.handlerTimeout) || input.handlerTimeout <= 0) delete input.handlerTimeout;
|
|
1489
2419
|
}
|
|
1490
|
-
return
|
|
2420
|
+
return {
|
|
2421
|
+
...input,
|
|
2422
|
+
etag,
|
|
2423
|
+
trustProxy
|
|
2424
|
+
};
|
|
1491
2425
|
}
|
|
1492
2426
|
//#endregion
|
|
1493
|
-
//#region src/
|
|
1494
|
-
const
|
|
1495
|
-
const
|
|
2427
|
+
//#region src/app/constants.ts
|
|
2428
|
+
const AppSymbol = Symbol.for("App");
|
|
2429
|
+
const AppPipelineStep = {
|
|
1496
2430
|
START: 0,
|
|
1497
2431
|
LOOKUP: 1,
|
|
1498
2432
|
CHILD_BEFORE: 2,
|
|
@@ -1500,65 +2434,73 @@ const RouterPipelineStep = {
|
|
|
1500
2434
|
CHILD_AFTER: 4,
|
|
1501
2435
|
FINISH: 5
|
|
1502
2436
|
};
|
|
1503
|
-
const
|
|
1504
|
-
|
|
2437
|
+
const RouteEntryType = {
|
|
2438
|
+
APP: "app",
|
|
1505
2439
|
HANDLER: "handler"
|
|
1506
2440
|
};
|
|
1507
2441
|
//#endregion
|
|
1508
|
-
//#region src/
|
|
1509
|
-
function
|
|
1510
|
-
return hasInstanceof(input,
|
|
2442
|
+
//#region src/app/check.ts
|
|
2443
|
+
function isAppInstance(input) {
|
|
2444
|
+
return hasInstanceof(input, AppSymbol);
|
|
1511
2445
|
}
|
|
2446
|
+
//#endregion
|
|
2447
|
+
//#region src/app/module.ts
|
|
1512
2448
|
/**
|
|
1513
|
-
*
|
|
1514
|
-
*
|
|
1515
|
-
*
|
|
1516
|
-
*
|
|
2449
|
+
* Merge resolver-supplied path params into `event.params` *only* when
|
|
2450
|
+
* `match.params` actually has keys. Skipping the object spread on the
|
|
2451
|
+
* empty-params path (every static route, every middleware match) saves
|
|
2452
|
+
* an allocation per match — the hottest path in static-route apps.
|
|
1517
2453
|
*/
|
|
1518
|
-
function
|
|
1519
|
-
|
|
1520
|
-
|
|
2454
|
+
function mergeMatchParams(event, matchParams) {
|
|
2455
|
+
let hasKeys = false;
|
|
2456
|
+
for (const _k in matchParams) {
|
|
2457
|
+
hasKeys = true;
|
|
2458
|
+
break;
|
|
2459
|
+
}
|
|
2460
|
+
if (!hasKeys) return;
|
|
2461
|
+
event.params = {
|
|
2462
|
+
...event.params,
|
|
2463
|
+
...matchParams
|
|
2464
|
+
};
|
|
1521
2465
|
}
|
|
1522
2466
|
/**
|
|
1523
|
-
*
|
|
1524
|
-
*
|
|
1525
|
-
*
|
|
2467
|
+
* Copy `source[key]` into `target[key]` when the target's value is
|
|
2468
|
+
* undefined; return whether a write happened.
|
|
2469
|
+
*
|
|
2470
|
+
* Bound to a single key `K` per call, so TypeScript can prove the
|
|
2471
|
+
* read and write hit the same property's value type — no `as` cast
|
|
2472
|
+
* needed. This is the standard escape hatch for the variance trap
|
|
2473
|
+
* you'd otherwise hit by writing `target[key] = source[key]` inside
|
|
2474
|
+
* a loop where `key: keyof AppOptions` is a *union* (read returns
|
|
2475
|
+
* the union of value types, write requires the intersection, which
|
|
2476
|
+
* collapses to `never`).
|
|
1526
2477
|
*/
|
|
1527
|
-
function
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
return
|
|
1531
|
-
}
|
|
1532
|
-
|
|
1533
|
-
//#region src/router/module.ts
|
|
1534
|
-
const METHOD_TO_REGISTER = {
|
|
1535
|
-
[MethodName.GET]: "get",
|
|
1536
|
-
[MethodName.POST]: "post",
|
|
1537
|
-
[MethodName.PUT]: "put",
|
|
1538
|
-
[MethodName.PATCH]: "patch",
|
|
1539
|
-
[MethodName.DELETE]: "delete",
|
|
1540
|
-
[MethodName.HEAD]: "head",
|
|
1541
|
-
[MethodName.OPTIONS]: "options"
|
|
1542
|
-
};
|
|
1543
|
-
var Router = class Router {
|
|
2478
|
+
function copyOptionIfUnset(target, source, key) {
|
|
2479
|
+
if (typeof target[key] !== "undefined") return false;
|
|
2480
|
+
target[key] = source[key];
|
|
2481
|
+
return true;
|
|
2482
|
+
}
|
|
2483
|
+
var App = class App {
|
|
1544
2484
|
/**
|
|
1545
2485
|
* A label for the router instance.
|
|
1546
2486
|
*/
|
|
1547
2487
|
name;
|
|
1548
2488
|
/**
|
|
1549
|
-
*
|
|
1550
|
-
*
|
|
1551
|
-
* runtime checks.
|
|
2489
|
+
* Registration-time path prefix for entries registered on this
|
|
2490
|
+
* App. Local to this instance — never inherited from a parent.
|
|
1552
2491
|
*
|
|
1553
2492
|
* @protected
|
|
1554
2493
|
*/
|
|
1555
|
-
|
|
2494
|
+
_path;
|
|
1556
2495
|
/**
|
|
1557
|
-
*
|
|
2496
|
+
* Pluggable router (route table) — owns the "which entries match
|
|
2497
|
+
* this path?" lookup. Defaults to `LinearRouter` (walks entries
|
|
2498
|
+
* linearly per request); swap in via `AppContext.router`
|
|
2499
|
+
* for a radix/trie implementation on apps with many routes.
|
|
1558
2500
|
*
|
|
1559
2501
|
* @protected
|
|
1560
2502
|
*/
|
|
1561
|
-
|
|
2503
|
+
router;
|
|
1562
2504
|
/**
|
|
1563
2505
|
* Lifecycle hook registry.
|
|
1564
2506
|
*
|
|
@@ -1566,7 +2508,14 @@ var Router = class Router {
|
|
|
1566
2508
|
*/
|
|
1567
2509
|
hooks;
|
|
1568
2510
|
/**
|
|
1569
|
-
* Normalized options for this
|
|
2511
|
+
* Normalized options for this App instance.
|
|
2512
|
+
*
|
|
2513
|
+
* Frozen on construction and on every `extendOptions` update —
|
|
2514
|
+
* once published to `event.appOptions` it is shared across all
|
|
2515
|
+
* requests, and a handler must not be able to mutate
|
|
2516
|
+
* router-global state. `extendOptions` therefore uses a
|
|
2517
|
+
* functional update (build a new object, freeze it, replace
|
|
2518
|
+
* the slot) rather than mutating in place.
|
|
1570
2519
|
*/
|
|
1571
2520
|
_options;
|
|
1572
2521
|
/**
|
|
@@ -1575,18 +2524,51 @@ var Router = class Router {
|
|
|
1575
2524
|
* @protected
|
|
1576
2525
|
*/
|
|
1577
2526
|
plugins = /* @__PURE__ */ new Map();
|
|
2527
|
+
/**
|
|
2528
|
+
* Every route registered on this App, in registration order.
|
|
2529
|
+
*
|
|
2530
|
+
* App owns the canonical list — the `IRouter` contract has no
|
|
2531
|
+
* `routes` field, so cascades / clones / `setRouter` replay
|
|
2532
|
+
* read from here instead of asking the router. Routes are
|
|
2533
|
+
* pushed alongside every `this.router.add()` via the `register`
|
|
2534
|
+
* helper.
|
|
2535
|
+
*
|
|
2536
|
+
* @protected
|
|
2537
|
+
*/
|
|
2538
|
+
_routes = [];
|
|
1578
2539
|
constructor(input = {}) {
|
|
1579
2540
|
this.name = input.name;
|
|
1580
|
-
|
|
1581
|
-
this.hooks = hooks;
|
|
1582
|
-
this.plugins = new Map(plugins);
|
|
1583
|
-
this.
|
|
1584
|
-
this.
|
|
1585
|
-
markInstanceof(this,
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
2541
|
+
this._path = input.path;
|
|
2542
|
+
this.hooks = input.hooks ?? new Hooks();
|
|
2543
|
+
this.plugins = new Map(input.plugins);
|
|
2544
|
+
this.router = input.router ?? new LinearRouter();
|
|
2545
|
+
this._options = Object.freeze(normalizeAppOptions(input.options ?? {}));
|
|
2546
|
+
markInstanceof(this, AppSymbol);
|
|
2547
|
+
}
|
|
2548
|
+
/**
|
|
2549
|
+
* Register a route with the active router and record it on the
|
|
2550
|
+
* App so we can replay it onto a different router later (see
|
|
2551
|
+
* `setRouter`) and so cascades / clones have a source of truth
|
|
2552
|
+
* independent of the router instance.
|
|
2553
|
+
*
|
|
2554
|
+
* @protected
|
|
2555
|
+
*/
|
|
2556
|
+
register(route) {
|
|
2557
|
+
this.router.add(route);
|
|
2558
|
+
this._routes.push(route);
|
|
2559
|
+
}
|
|
2560
|
+
/**
|
|
2561
|
+
* Swap the active router. Replays every previously-registered
|
|
2562
|
+
* route onto the new router so lookups stay correct.
|
|
2563
|
+
*
|
|
2564
|
+
* Useful for picking a router after route shape is known (e.g.
|
|
2565
|
+
* a SmartRouter-style decision), or for testing alternatives
|
|
2566
|
+
* mid-flight without rebuilding the App. Any cache the previous
|
|
2567
|
+
* router carried is dropped along with it.
|
|
2568
|
+
*/
|
|
2569
|
+
setRouter(router) {
|
|
2570
|
+
for (const route of this._routes) router.add(route);
|
|
2571
|
+
this.router = router;
|
|
1590
2572
|
}
|
|
1591
2573
|
/**
|
|
1592
2574
|
* Public entry point — creates a DispatcherEvent from the request,
|
|
@@ -1640,136 +2622,188 @@ var Router = class Router {
|
|
|
1640
2622
|
headers
|
|
1641
2623
|
});
|
|
1642
2624
|
}
|
|
2625
|
+
/**
|
|
2626
|
+
* Mount-time option inheritance — fill in any of this App's
|
|
2627
|
+
* unset option keys from the supplied parent options. Called by
|
|
2628
|
+
* `App.use(child)` after narrowing to `App` via `isAppInstance`.
|
|
2629
|
+
*
|
|
2630
|
+
* Public so callers don't need to reach into another App's
|
|
2631
|
+
* protected fields: the parent passes its options by value and
|
|
2632
|
+
* the child decides what to do with them.
|
|
2633
|
+
*
|
|
2634
|
+
* Shallow per-key merge. App-local concerns — `name`, `path`,
|
|
2635
|
+
* `hooks`, `plugins`, `router`, and the router's cache — are
|
|
2636
|
+
* deliberately not propagated; they sit on `AppContext`, not
|
|
2637
|
+
* inside `AppOptions`.
|
|
2638
|
+
*
|
|
2639
|
+
* Cascades to any Apps already mounted on this one — a deeper
|
|
2640
|
+
* grandchild gets the new keys too. Without this, mounting
|
|
2641
|
+
* grandchild → child → parent in that order would leave the
|
|
2642
|
+
* grandchild without parent's options (it adopted from child
|
|
2643
|
+
* before child had them).
|
|
2644
|
+
*
|
|
2645
|
+
* Late mutation of the parent's options after this call does NOT
|
|
2646
|
+
* propagate. `AppOptions` is configured at construction-and-mount;
|
|
2647
|
+
* later changes are not a supported workflow.
|
|
2648
|
+
*/
|
|
2649
|
+
extendOptions(incoming) {
|
|
2650
|
+
let next;
|
|
2651
|
+
const keys = Object.keys(incoming);
|
|
2652
|
+
for (const key of keys) {
|
|
2653
|
+
if (typeof this._options[key] !== "undefined") continue;
|
|
2654
|
+
if (typeof incoming[key] === "undefined") continue;
|
|
2655
|
+
next ??= { ...this._options };
|
|
2656
|
+
copyOptionIfUnset(next, incoming, key);
|
|
2657
|
+
}
|
|
2658
|
+
if (!next) return;
|
|
2659
|
+
this._options = Object.freeze(next);
|
|
2660
|
+
for (const route of this._routes) if (route.data.type === RouteEntryType.APP && isAppInstance(route.data.data)) route.data.data.extendOptions(this._options);
|
|
2661
|
+
}
|
|
1643
2662
|
async executePipelineStep(context) {
|
|
1644
|
-
while (context.step !==
|
|
1645
|
-
case
|
|
2663
|
+
while (context.step !== AppPipelineStep.FINISH) switch (context.step) {
|
|
2664
|
+
case AppPipelineStep.START:
|
|
1646
2665
|
await this.executePipelineStepStart(context);
|
|
1647
2666
|
break;
|
|
1648
|
-
case
|
|
2667
|
+
case AppPipelineStep.LOOKUP:
|
|
1649
2668
|
await this.executePipelineStepLookup(context);
|
|
1650
2669
|
break;
|
|
1651
|
-
case
|
|
2670
|
+
case AppPipelineStep.CHILD_BEFORE:
|
|
1652
2671
|
await this.executePipelineStepChildBefore(context);
|
|
1653
2672
|
break;
|
|
1654
|
-
case
|
|
2673
|
+
case AppPipelineStep.CHILD_DISPATCH:
|
|
1655
2674
|
await this.executePipelineStepChildDispatch(context);
|
|
1656
2675
|
break;
|
|
1657
|
-
case
|
|
2676
|
+
case AppPipelineStep.CHILD_AFTER:
|
|
1658
2677
|
await this.executePipelineStepChildAfter(context);
|
|
1659
2678
|
break;
|
|
1660
2679
|
default:
|
|
1661
|
-
context.step =
|
|
2680
|
+
context.step = AppPipelineStep.FINISH;
|
|
1662
2681
|
break;
|
|
1663
2682
|
}
|
|
1664
2683
|
await this.executePipelineStepFinish(context);
|
|
1665
2684
|
}
|
|
1666
2685
|
async executePipelineStepStart(context) {
|
|
1667
|
-
await this.hooks.trigger(HookName.
|
|
1668
|
-
if (context.event.dispatched) context.step =
|
|
1669
|
-
else context.step =
|
|
2686
|
+
if (this.hooks.hasListeners(HookName.START)) await this.hooks.trigger(HookName.START, context.event);
|
|
2687
|
+
if (context.event.dispatched) context.step = AppPipelineStep.FINISH;
|
|
2688
|
+
else context.step = AppPipelineStep.LOOKUP;
|
|
1670
2689
|
}
|
|
1671
2690
|
async executePipelineStepLookup(context) {
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
2691
|
+
if (typeof context.matches === "undefined" || context.matchesPath !== context.event.path) {
|
|
2692
|
+
context.matches = this.router.lookup(context.event.path, context.event.method);
|
|
2693
|
+
context.matchesPath = context.event.path;
|
|
2694
|
+
}
|
|
2695
|
+
const { matches } = context;
|
|
2696
|
+
while (!context.event.dispatched && context.matchIndex < matches.length) {
|
|
2697
|
+
const { route } = matches[context.matchIndex];
|
|
2698
|
+
if (route.data.type === RouteEntryType.HANDLER) {
|
|
2699
|
+
const handler = route.data.data;
|
|
1676
2700
|
if (context.event.error && handler.type === HandlerType.CORE || !context.event.error && handler.type === HandlerType.ERROR) {
|
|
1677
|
-
context.
|
|
2701
|
+
context.matchIndex++;
|
|
1678
2702
|
continue;
|
|
1679
2703
|
}
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
if (context.event.dispatched) context.step = RouterPipelineStep.FINISH;
|
|
1686
|
-
else context.step = RouterPipelineStep.CHILD_BEFORE;
|
|
1687
|
-
return;
|
|
1688
|
-
}
|
|
2704
|
+
const { method } = route;
|
|
2705
|
+
if (method) context.event.methodsAllowed.add(method);
|
|
2706
|
+
if (!matchHandlerMethod(method, context.event.method)) {
|
|
2707
|
+
context.matchIndex++;
|
|
2708
|
+
continue;
|
|
1689
2709
|
}
|
|
1690
|
-
context.stackIndex++;
|
|
1691
|
-
continue;
|
|
1692
2710
|
}
|
|
1693
|
-
if (
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
else context.step = RouterPipelineStep.CHILD_BEFORE;
|
|
2711
|
+
if (this.hooks.hasListeners(HookName.CHILD_MATCH)) await this.hooks.trigger(HookName.CHILD_MATCH, context.event);
|
|
2712
|
+
if (context.event.dispatched) {
|
|
2713
|
+
context.step = AppPipelineStep.FINISH;
|
|
1697
2714
|
return;
|
|
1698
2715
|
}
|
|
1699
|
-
context.
|
|
2716
|
+
if (context.event.path !== context.matchesPath) {
|
|
2717
|
+
context.matches = void 0;
|
|
2718
|
+
context.matchIndex = 0;
|
|
2719
|
+
context.step = AppPipelineStep.LOOKUP;
|
|
2720
|
+
return;
|
|
2721
|
+
}
|
|
2722
|
+
context.step = AppPipelineStep.CHILD_BEFORE;
|
|
2723
|
+
return;
|
|
1700
2724
|
}
|
|
1701
|
-
context.step =
|
|
2725
|
+
context.step = AppPipelineStep.FINISH;
|
|
1702
2726
|
}
|
|
1703
2727
|
async executePipelineStepChildBefore(context) {
|
|
1704
|
-
await this.hooks.trigger(HookName.CHILD_DISPATCH_BEFORE, context.event);
|
|
1705
|
-
if (context.event.dispatched)
|
|
1706
|
-
|
|
2728
|
+
if (this.hooks.hasListeners(HookName.CHILD_DISPATCH_BEFORE)) await this.hooks.trigger(HookName.CHILD_DISPATCH_BEFORE, context.event);
|
|
2729
|
+
if (context.event.dispatched) {
|
|
2730
|
+
context.step = AppPipelineStep.FINISH;
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2733
|
+
if (context.event.path !== context.matchesPath) {
|
|
2734
|
+
context.matches = void 0;
|
|
2735
|
+
context.matchIndex = 0;
|
|
2736
|
+
context.step = AppPipelineStep.LOOKUP;
|
|
2737
|
+
return;
|
|
2738
|
+
}
|
|
2739
|
+
context.step = AppPipelineStep.CHILD_DISPATCH;
|
|
1707
2740
|
}
|
|
1708
2741
|
async executePipelineStepChildAfter(context) {
|
|
1709
|
-
await this.hooks.trigger(HookName.CHILD_DISPATCH_AFTER, context.event);
|
|
1710
|
-
if (context.event.dispatched) context.step =
|
|
1711
|
-
else context.step =
|
|
2742
|
+
if (this.hooks.hasListeners(HookName.CHILD_DISPATCH_AFTER)) await this.hooks.trigger(HookName.CHILD_DISPATCH_AFTER, context.event);
|
|
2743
|
+
if (context.event.dispatched) context.step = AppPipelineStep.FINISH;
|
|
2744
|
+
else context.step = AppPipelineStep.LOOKUP;
|
|
1712
2745
|
}
|
|
1713
2746
|
async executePipelineStepChildDispatch(context) {
|
|
1714
|
-
const
|
|
1715
|
-
if (context.event.dispatched || typeof
|
|
1716
|
-
context.step =
|
|
2747
|
+
const match = context.matches?.[context.matchIndex];
|
|
2748
|
+
if (context.event.dispatched || typeof match === "undefined") {
|
|
2749
|
+
context.step = AppPipelineStep.FINISH;
|
|
1717
2750
|
return;
|
|
1718
2751
|
}
|
|
2752
|
+
const { route } = match;
|
|
1719
2753
|
const { event } = context;
|
|
1720
2754
|
const savedPath = event.path;
|
|
1721
2755
|
const savedMountPath = event.mountPath;
|
|
1722
2756
|
const savedParams = event.params;
|
|
1723
|
-
if (
|
|
1724
|
-
|
|
1725
|
-
if (
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
event.params = {
|
|
1730
|
-
...event.params,
|
|
1731
|
-
...output.params
|
|
1732
|
-
};
|
|
1733
|
-
}
|
|
1734
|
-
} else if (entry.type === RouterStackEntryType.HANDLER && entry.pathMatcher) {
|
|
1735
|
-
const output = entry.pathMatcher.exec(event.path);
|
|
1736
|
-
if (typeof output !== "undefined") event.params = {
|
|
1737
|
-
...event.params,
|
|
1738
|
-
...output.params
|
|
1739
|
-
};
|
|
1740
|
-
}
|
|
2757
|
+
if (route.data.type === RouteEntryType.APP && typeof match.path === "string") {
|
|
2758
|
+
event.mountPath = cleanDoubleSlashes(`${event.mountPath}/${match.path}`);
|
|
2759
|
+
if (event.path === match.path) event.path = "/";
|
|
2760
|
+
else event.path = withLeadingSlash(event.path.substring(match.path.length));
|
|
2761
|
+
mergeMatchParams(event, match.params);
|
|
2762
|
+
} else if (route.data.type === RouteEntryType.HANDLER && typeof match.path === "string") mergeMatchParams(event, match.params);
|
|
1741
2763
|
try {
|
|
2764
|
+
const parentMatches = context.matches;
|
|
2765
|
+
const parentMatchesPath = context.matchesPath;
|
|
2766
|
+
const nextMatchIndex = context.matchIndex + 1;
|
|
1742
2767
|
event.setNext(async (error) => {
|
|
1743
2768
|
if (error) event.error = createError(error);
|
|
1744
|
-
const
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
2769
|
+
const pathChanged = event.path !== parentMatchesPath;
|
|
2770
|
+
const savedStep = context.step;
|
|
2771
|
+
const savedMatchIndex = context.matchIndex;
|
|
2772
|
+
const savedMatches = context.matches;
|
|
2773
|
+
const savedMatchesPath = context.matchesPath;
|
|
2774
|
+
context.step = AppPipelineStep.LOOKUP;
|
|
2775
|
+
context.matchIndex = pathChanged ? 0 : nextMatchIndex;
|
|
2776
|
+
context.matches = pathChanged ? void 0 : parentMatches;
|
|
2777
|
+
context.matchesPath = pathChanged ? void 0 : parentMatchesPath;
|
|
2778
|
+
try {
|
|
2779
|
+
await this.executePipelineStep(context);
|
|
2780
|
+
} finally {
|
|
2781
|
+
context.step = savedStep;
|
|
2782
|
+
context.matchIndex = savedMatchIndex;
|
|
2783
|
+
context.matches = savedMatches;
|
|
2784
|
+
context.matchesPath = savedMatchesPath;
|
|
2785
|
+
}
|
|
2786
|
+
return context.response;
|
|
1752
2787
|
});
|
|
1753
|
-
const response = await
|
|
2788
|
+
const response = await route.data.data.dispatch(event);
|
|
1754
2789
|
if (response) {
|
|
1755
2790
|
context.response = response;
|
|
1756
2791
|
event.dispatched = true;
|
|
1757
2792
|
}
|
|
1758
2793
|
} catch (e) {
|
|
1759
2794
|
event.error = createError(e);
|
|
1760
|
-
await this.hooks.trigger(HookName.ERROR, event);
|
|
2795
|
+
if (this.hooks.hasListeners(HookName.ERROR)) await this.hooks.trigger(HookName.ERROR, event);
|
|
1761
2796
|
}
|
|
1762
2797
|
if (!event.dispatched) {
|
|
1763
2798
|
event.path = savedPath;
|
|
1764
2799
|
event.mountPath = savedMountPath;
|
|
1765
2800
|
event.params = savedParams;
|
|
1766
2801
|
}
|
|
1767
|
-
context.
|
|
1768
|
-
context.step =
|
|
2802
|
+
context.matchIndex++;
|
|
2803
|
+
context.step = AppPipelineStep.CHILD_AFTER;
|
|
1769
2804
|
}
|
|
1770
2805
|
async executePipelineStepFinish(context) {
|
|
1771
|
-
if (context.event.error
|
|
1772
|
-
if (!context.event.dispatched && context.event.routerPath.length === 1 && context.event.method && context.event.method === MethodName.OPTIONS) {
|
|
2806
|
+
if (!context.event.error && !context.event.dispatched && context.isRoot && context.event.method === MethodName.OPTIONS) {
|
|
1773
2807
|
if (context.event.methodsAllowed.has(MethodName.GET)) context.event.methodsAllowed.add(MethodName.HEAD);
|
|
1774
2808
|
const options = [...context.event.methodsAllowed].map((key) => key.toUpperCase()).join(",");
|
|
1775
2809
|
const optionsHeaders = new Headers(context.event.response.headers);
|
|
@@ -1780,37 +2814,28 @@ var Router = class Router {
|
|
|
1780
2814
|
});
|
|
1781
2815
|
context.event.dispatched = true;
|
|
1782
2816
|
}
|
|
1783
|
-
return this.hooks.trigger(HookName.RESPONSE, context.event);
|
|
1784
2817
|
}
|
|
1785
2818
|
async dispatch(event) {
|
|
1786
2819
|
const savedPath = event.path;
|
|
1787
2820
|
const savedMountPath = event.mountPath;
|
|
1788
2821
|
const savedParams = event.params;
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
event.mountPath = cleanDoubleSlashes(`${event.mountPath}/${output.path}`);
|
|
1793
|
-
if (event.path === output.path) event.path = "/";
|
|
1794
|
-
else event.path = withLeadingSlash(event.path.substring(output.path.length));
|
|
1795
|
-
event.params = {
|
|
1796
|
-
...event.params,
|
|
1797
|
-
...output.params
|
|
1798
|
-
};
|
|
1799
|
-
}
|
|
1800
|
-
}
|
|
2822
|
+
const savedAppOptions = event.appOptions;
|
|
2823
|
+
const wasDispatching = event.isDispatching;
|
|
2824
|
+
const isRoot = !wasDispatching;
|
|
1801
2825
|
const context = {
|
|
1802
|
-
step:
|
|
2826
|
+
step: AppPipelineStep.START,
|
|
1803
2827
|
event,
|
|
1804
|
-
|
|
2828
|
+
isRoot,
|
|
2829
|
+
matchIndex: 0
|
|
1805
2830
|
};
|
|
1806
|
-
event.
|
|
1807
|
-
|
|
1808
|
-
options: this._options
|
|
1809
|
-
});
|
|
2831
|
+
event.appOptions = this._options;
|
|
2832
|
+
event.isDispatching = true;
|
|
1810
2833
|
try {
|
|
1811
2834
|
await this.executePipelineStep(context);
|
|
2835
|
+
if (this.hooks.hasListeners(HookName.END)) await this.hooks.trigger(HookName.END, event);
|
|
1812
2836
|
} finally {
|
|
1813
|
-
event.
|
|
2837
|
+
event.appOptions = savedAppOptions;
|
|
2838
|
+
event.isDispatching = wasDispatching;
|
|
1814
2839
|
if (!event.dispatched) {
|
|
1815
2840
|
event.path = savedPath;
|
|
1816
2841
|
event.mountPath = savedMountPath;
|
|
@@ -1855,19 +2880,19 @@ var Router = class Router {
|
|
|
1855
2880
|
continue;
|
|
1856
2881
|
}
|
|
1857
2882
|
let handler;
|
|
1858
|
-
if (
|
|
2883
|
+
if (isHandler(element)) handler = element;
|
|
2884
|
+
else if (isHandlerOptions(element)) handler = new Handler({
|
|
1859
2885
|
...element,
|
|
1860
|
-
method
|
|
1861
|
-
path: path ?? element.path
|
|
2886
|
+
method
|
|
1862
2887
|
});
|
|
1863
|
-
else if (isHandler(element)) handler = element;
|
|
1864
2888
|
else continue;
|
|
1865
|
-
this.
|
|
1866
|
-
|
|
1867
|
-
data: handler,
|
|
2889
|
+
this.register({
|
|
2890
|
+
path: joinPaths(this._path, path, handler.path),
|
|
1868
2891
|
method,
|
|
1869
|
-
|
|
1870
|
-
|
|
2892
|
+
data: {
|
|
2893
|
+
type: RouteEntryType.HANDLER,
|
|
2894
|
+
data: handler
|
|
2895
|
+
}
|
|
1871
2896
|
});
|
|
1872
2897
|
}
|
|
1873
2898
|
}
|
|
@@ -1878,33 +2903,37 @@ var Router = class Router {
|
|
|
1878
2903
|
path = withLeadingSlash(item);
|
|
1879
2904
|
continue;
|
|
1880
2905
|
}
|
|
1881
|
-
if (
|
|
1882
|
-
this.
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
2906
|
+
if (isAppInstance(item)) {
|
|
2907
|
+
item.extendOptions(this._options);
|
|
2908
|
+
this.register({
|
|
2909
|
+
path: joinPaths(this._path, path),
|
|
2910
|
+
data: {
|
|
2911
|
+
type: RouteEntryType.APP,
|
|
2912
|
+
data: item
|
|
2913
|
+
}
|
|
1887
2914
|
});
|
|
1888
2915
|
continue;
|
|
1889
2916
|
}
|
|
1890
|
-
if (
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
path
|
|
2917
|
+
if (isHandler(item)) {
|
|
2918
|
+
this.register({
|
|
2919
|
+
path: joinPaths(this._path, path, item.path),
|
|
2920
|
+
method: item.method,
|
|
2921
|
+
data: {
|
|
2922
|
+
type: RouteEntryType.HANDLER,
|
|
2923
|
+
data: item
|
|
2924
|
+
}
|
|
1899
2925
|
});
|
|
1900
2926
|
continue;
|
|
1901
2927
|
}
|
|
1902
|
-
if (
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
2928
|
+
if (isHandlerOptions(item)) {
|
|
2929
|
+
const handler = new Handler({ ...item });
|
|
2930
|
+
this.register({
|
|
2931
|
+
path: joinPaths(this._path, path, handler.path),
|
|
2932
|
+
method: handler.method,
|
|
2933
|
+
data: {
|
|
2934
|
+
type: RouteEntryType.HANDLER,
|
|
2935
|
+
data: handler
|
|
2936
|
+
}
|
|
1908
2937
|
});
|
|
1909
2938
|
continue;
|
|
1910
2939
|
}
|
|
@@ -1928,7 +2957,10 @@ var Router = class Router {
|
|
|
1928
2957
|
}
|
|
1929
2958
|
install(plugin, context = {}) {
|
|
1930
2959
|
if (this.plugins.has(plugin.name)) throw new PluginAlreadyInstalledError(plugin.name);
|
|
1931
|
-
const router = new
|
|
2960
|
+
const router = new App({
|
|
2961
|
+
name: plugin.name,
|
|
2962
|
+
router: this.router.clone()
|
|
2963
|
+
});
|
|
1932
2964
|
plugin.install(router);
|
|
1933
2965
|
if (context.path) this.use(context.path, router);
|
|
1934
2966
|
else this.use(router);
|
|
@@ -1936,7 +2968,7 @@ var Router = class Router {
|
|
|
1936
2968
|
return this;
|
|
1937
2969
|
}
|
|
1938
2970
|
/**
|
|
1939
|
-
* Return a new `
|
|
2971
|
+
* Return a new `App` that mirrors this one but owns independent
|
|
1940
2972
|
* mountable state.
|
|
1941
2973
|
*
|
|
1942
2974
|
* The new router has:
|
|
@@ -1952,26 +2984,33 @@ var Router = class Router {
|
|
|
1952
2984
|
* mutations on one mount do not bleed into the others.
|
|
1953
2985
|
*/
|
|
1954
2986
|
clone() {
|
|
1955
|
-
const next = new
|
|
1956
|
-
|
|
2987
|
+
const next = new App({
|
|
2988
|
+
name: this.name,
|
|
2989
|
+
path: this._path,
|
|
2990
|
+
options: { ...this._options },
|
|
1957
2991
|
hooks: this.hooks.clone(),
|
|
1958
|
-
plugins: this.plugins
|
|
2992
|
+
plugins: this.plugins,
|
|
2993
|
+
router: this.router.clone()
|
|
1959
2994
|
});
|
|
1960
|
-
for (const
|
|
1961
|
-
if (
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
if (entry.path) next[register](entry.path, entry.data);
|
|
1970
|
-
else next[register](entry.data);
|
|
2995
|
+
for (const route of this._routes) {
|
|
2996
|
+
if (route.data.type === RouteEntryType.APP) {
|
|
2997
|
+
next.register({
|
|
2998
|
+
path: route.path,
|
|
2999
|
+
data: {
|
|
3000
|
+
type: RouteEntryType.APP,
|
|
3001
|
+
data: route.data.data.clone()
|
|
3002
|
+
}
|
|
3003
|
+
});
|
|
1971
3004
|
continue;
|
|
1972
3005
|
}
|
|
1973
|
-
|
|
1974
|
-
|
|
3006
|
+
next.register({
|
|
3007
|
+
path: route.path,
|
|
3008
|
+
method: route.method,
|
|
3009
|
+
data: {
|
|
3010
|
+
type: RouteEntryType.HANDLER,
|
|
3011
|
+
data: route.data.data
|
|
3012
|
+
}
|
|
3013
|
+
});
|
|
1975
3014
|
}
|
|
1976
3015
|
return next;
|
|
1977
3016
|
}
|
|
@@ -1988,6 +3027,6 @@ var Router = class Router {
|
|
|
1988
3027
|
}
|
|
1989
3028
|
};
|
|
1990
3029
|
//#endregion
|
|
1991
|
-
export {
|
|
3030
|
+
export { isError as $, fromWebHandler as A, DispatcherEvent as B, getRequestAcceptableEncodings as C, matchHandlerMethod as D, isRequestCacheable as E, defineErrorHandler as F, getRequestAcceptableContentTypes as G, sendRedirect as H, defineCoreHandler as I, sendFile as J, useRequestNegotiator as K, Handler as L, isWebHandlerProvider as M, fromNodeHandler as N, isHandler as O, fromNodeMiddleware as P, createError as Q, HandlerSymbol as R, getRequestAcceptableEncoding as S, getRequestAcceptableCharsets as T, sendFormat as U, sendStream as V, getRequestAcceptableContentType as W, sendAccepted as X, sendCreated as Y, toResponse as Z, getRequestIP as _, LinearRouter as a, appendResponseHeaderDirective as at, getRequestAcceptableLanguage as b, PluginNotInstalledError as c, AppError as ct, PluginError as d, AppEvent as dt, setResponseHeaderContentType as et, isPluginError as f, HeaderName as ft, getRequestProtocol as g, PathMatcher as h, TrieRouter as i, appendResponseHeader as it, isWebHandler as j, isHandlerOptions as k, PluginInstallError as l, ErrorSymbol as lt, isPath as m, LruCache as mt, normalizeAppOptions as n, setResponseHeaderInline as nt, buildRoutePathMatcher as o, createEventStream as ot, PluginErrorCode as p, MethodName as pt, getRequestHeader as q, SmartRouter as r, setResponseContentTypeByFileName as rt, isPlugin as s, serializeEventStreamMessage as st, App as t, setResponseHeaderAttachment as tt, PluginAlreadyInstalledError as u, setResponseCacheHeaders as ut, getRequestHostName as v, getRequestAcceptableCharset as w, getRequestAcceptableLanguages as x, matchRequestContentType as y, HandlerType as z };
|
|
1992
3031
|
|
|
1993
|
-
//# sourceMappingURL=src-
|
|
3032
|
+
//# sourceMappingURL=src-gmPicCWT.mjs.map
|