routup 5.1.1 → 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.
@@ -0,0 +1,3032 @@
1
+ import QuickLRU from "quick-lru";
2
+ import { FastURL } from "srvx";
3
+ import { hasInstanceof, markInstanceof } from "@ebec/core";
4
+ import { HTTPError, isHTTPError } from "@ebec/http";
5
+ import { subtle } from "uncrypto";
6
+ import { merge } from "smob";
7
+ import { compile } from "proxy-addr";
8
+ import { get, getType } from "mime-explorer";
9
+ import Negotiator from "negotiator";
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
47
+ //#region src/constants.ts
48
+ const MethodName = {
49
+ GET: "GET",
50
+ POST: "POST",
51
+ PUT: "PUT",
52
+ PATCH: "PATCH",
53
+ DELETE: "DELETE",
54
+ OPTIONS: "OPTIONS",
55
+ HEAD: "HEAD"
56
+ };
57
+ const HeaderName = {
58
+ ACCEPT: "accept",
59
+ ACCEPT_CHARSET: "accept-charset",
60
+ ACCEPT_ENCODING: "accept-encoding",
61
+ ACCEPT_LANGUAGE: "accept-language",
62
+ ACCEPT_RANGES: "accept-ranges",
63
+ ALLOW: "allow",
64
+ CACHE_CONTROL: "cache-control",
65
+ CONTENT_DISPOSITION: "content-disposition",
66
+ CONTENT_ENCODING: "content-encoding",
67
+ CONTENT_LENGTH: "content-length",
68
+ CONTENT_RANGE: "content-range",
69
+ CONTENT_TYPE: "content-type",
70
+ CONNECTION: "connection",
71
+ COOKIE: "cookie",
72
+ ETag: "etag",
73
+ HOST: "host",
74
+ IF_MODIFIED_SINCE: "if-modified-since",
75
+ IF_NONE_MATCH: "if-none-match",
76
+ LAST_MODIFIED: "last-modified",
77
+ LOCATION: "location",
78
+ RANGE: "range",
79
+ RATE_LIMIT_LIMIT: "ratelimit-limit",
80
+ RATE_LIMIT_REMAINING: "ratelimit-remaining",
81
+ RATE_LIMIT_RESET: "ratelimit-reset",
82
+ RETRY_AFTER: "retry-after",
83
+ SET_COOKIE: "set-cookie",
84
+ TRANSFER_ENCODING: "transfer-encoding",
85
+ X_ACCEL_BUFFERING: "x-accel-buffering",
86
+ X_FORWARDED_HOST: "x-forwarded-host",
87
+ X_FORWARDED_FOR: "x-forwarded-for",
88
+ X_FORWARDED_PROTO: "x-forwarded-proto"
89
+ };
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
151
+ //#region src/response/helpers/cache.ts
152
+ function setResponseCacheHeaders(event, options) {
153
+ options = options || {};
154
+ const cacheControls = ["public"].concat(options.cacheControls || []);
155
+ if (options.maxAge !== void 0) cacheControls.push(`max-age=${+options.maxAge}`, `s-maxage=${+options.maxAge}`);
156
+ if (options.modifiedTime) {
157
+ const modifiedTime = typeof options.modifiedTime === "string" ? new Date(options.modifiedTime) : options.modifiedTime;
158
+ event.response.headers.set("last-modified", modifiedTime.toUTCString());
159
+ }
160
+ event.response.headers.set("cache-control", cacheControls.join(", "));
161
+ }
162
+ //#endregion
163
+ //#region src/error/module.ts
164
+ const ErrorSymbol = Symbol.for("AppError");
165
+ var AppError = class extends HTTPError {
166
+ constructor(input = {}) {
167
+ super(input);
168
+ this.name = "AppError";
169
+ markInstanceof(this, ErrorSymbol);
170
+ }
171
+ };
172
+ //#endregion
173
+ //#region src/response/helpers/event-stream/utils.ts
174
+ function stripNewlines(value) {
175
+ return value.replace(/[\r\n]/g, "");
176
+ }
177
+ function serializeEventStreamMessage(message) {
178
+ let result = "";
179
+ if (message.id) result += `id: ${stripNewlines(message.id)}\n`;
180
+ if (message.event) result += `event: ${stripNewlines(message.event)}\n`;
181
+ if (typeof message.retry === "number" && Number.isInteger(message.retry)) result += `retry: ${message.retry}\n`;
182
+ const lines = message.data.replace(/\r/g, "").split("\n");
183
+ for (const line of lines) result += `data: ${line}\n`;
184
+ result += "\n";
185
+ return result;
186
+ }
187
+ //#endregion
188
+ //#region src/response/helpers/event-stream/module.ts
189
+ function createEventStream(event, options) {
190
+ if (options?.maxMessageSize !== void 0) {
191
+ if (!Number.isInteger(options.maxMessageSize) || options.maxMessageSize < 0) throw new AppError("maxMessageSize must be a non-negative integer.");
192
+ }
193
+ let controller;
194
+ let closed = false;
195
+ const encoder = new TextEncoder();
196
+ const stream = new ReadableStream({
197
+ start(ctrl) {
198
+ controller = ctrl;
199
+ },
200
+ cancel() {
201
+ closed = true;
202
+ }
203
+ });
204
+ const headers = new Headers(event.response.headers);
205
+ headers.set(HeaderName.CONTENT_TYPE, "text/event-stream");
206
+ headers.set(HeaderName.CACHE_CONTROL, "private, no-cache, no-store, no-transform, must-revalidate, max-age=0");
207
+ headers.set(HeaderName.X_ACCEL_BUFFERING, "no");
208
+ headers.set(HeaderName.CONNECTION, "keep-alive");
209
+ const handle = {
210
+ write(message) {
211
+ if (closed) return false;
212
+ if (typeof message === "string") return handle.write({ data: message });
213
+ const serialized = serializeEventStreamMessage(message);
214
+ if (options?.maxMessageSize !== void 0) {
215
+ if (encoder.encode(serialized).byteLength > options.maxMessageSize) return false;
216
+ }
217
+ controller.enqueue(encoder.encode(serialized));
218
+ return true;
219
+ },
220
+ end() {
221
+ if (closed) return;
222
+ closed = true;
223
+ controller.close();
224
+ },
225
+ response: new Response(stream, {
226
+ status: event.response.status,
227
+ headers
228
+ })
229
+ };
230
+ return handle;
231
+ }
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
257
+ //#region src/utils/header.ts
258
+ function sanitizeHeaderValue(value) {
259
+ return value.replace(/[\r\n]/g, "");
260
+ }
261
+ //#endregion
262
+ //#region src/utils/etag/module.ts
263
+ async function sha1(str) {
264
+ const enc = new TextEncoder();
265
+ const hash = await subtle.digest("SHA-1", enc.encode(str));
266
+ return btoa(String.fromCharCode(...new Uint8Array(hash)));
267
+ }
268
+ /**
269
+ * Generate an ETag.
270
+ */
271
+ async function generateETag(input) {
272
+ if (input.length === 0) return "\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"";
273
+ const hash = await sha1(input);
274
+ return `"${input.length.toString(16)}-${hash.substring(0, 27)}"`;
275
+ }
276
+ /**
277
+ * Create a simple ETag.
278
+ */
279
+ async function createEtag(input, options = {}) {
280
+ const tag = await generateETag(input);
281
+ return options.weak ? `W/${tag}` : tag;
282
+ }
283
+ //#endregion
284
+ //#region src/utils/object.ts
285
+ function isObject(item) {
286
+ return !!item && typeof item === "object" && !Array.isArray(item);
287
+ }
288
+ //#endregion
289
+ //#region src/utils/etag/utils.ts
290
+ const textEncoder = /* @__PURE__ */ new TextEncoder();
291
+ function buildEtagFn(input) {
292
+ if (typeof input === "function") return input;
293
+ input = input ?? true;
294
+ if (input === false) return () => Promise.resolve(void 0);
295
+ let options = { weak: true };
296
+ if (isObject(input)) options = merge(input, options);
297
+ return async (body, size) => {
298
+ if (typeof options.threshold !== "undefined") {
299
+ if ((size ?? textEncoder.encode(body).byteLength) <= options.threshold) return;
300
+ }
301
+ return createEtag(body, options);
302
+ };
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();
314
+ //#endregion
315
+ //#region src/utils/trust-proxy/module.ts
316
+ function buildTrustProxyFn(input) {
317
+ if (typeof input === "function") return input;
318
+ if (input === true) return () => true;
319
+ if (typeof input === "number") return (_address, hop) => hop < input;
320
+ if (typeof input === "string") input = input.split(",").map((value) => value.trim());
321
+ return compile(input || []);
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();
332
+ //#endregion
333
+ //#region src/utils/mime.ts
334
+ function getMimeType(type) {
335
+ if (type.includes("/")) return type;
336
+ return getType(type);
337
+ }
338
+ function getCharsetForMimeType(type) {
339
+ if (/^text\/|^application\/(javascript|json)/.test(type)) return "utf-8";
340
+ const meta = get(type);
341
+ if (meta && meta.charset) return meta.charset.toLowerCase();
342
+ }
343
+ //#endregion
344
+ //#region src/utils/method.ts
345
+ function toMethodName(input, alt) {
346
+ if (input) return input.toUpperCase();
347
+ return alt;
348
+ }
349
+ //#endregion
350
+ //#region src/utils/path.ts
351
+ /**
352
+ * Based on https://github.com/unjs/pathe v1.1.1 (055f50a6f1131f4e5c56cf259dd8816168fba329)
353
+ */
354
+ function normalizeWindowsPath(input = "") {
355
+ if (!input || !input.includes("\\")) return input;
356
+ return input.replace(/\\/g, "/");
357
+ }
358
+ const EXTNAME_RE = /.(\.[^./]+)$/;
359
+ function extname(input) {
360
+ const match = EXTNAME_RE.exec(normalizeWindowsPath(input));
361
+ return match && match[1] || "";
362
+ }
363
+ function basename(input, extension) {
364
+ const lastSegment = normalizeWindowsPath(input).split("/").pop();
365
+ if (!lastSegment) return input;
366
+ return extension && lastSegment.endsWith(extension) ? lastSegment.slice(0, -extension.length) : lastSegment;
367
+ }
368
+ //#endregion
369
+ //#region src/utils/promise.ts
370
+ function isPromise(p) {
371
+ return isObject(p) && (p instanceof Promise || typeof p.then === "function");
372
+ }
373
+ //#endregion
374
+ //#region src/utils/url.ts
375
+ function hasLeadingSlash(input = "") {
376
+ return input.startsWith("/");
377
+ }
378
+ function withLeadingSlash(input = "") {
379
+ return hasLeadingSlash(input) ? input : `/${input}`;
380
+ }
381
+ function cleanDoubleSlashes(input = "") {
382
+ if (input.includes("://")) return input.split("://").map((str) => cleanDoubleSlashes(str)).join("://");
383
+ return input.replace(/\/+/g, "/");
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
+ }
412
+ //#endregion
413
+ //#region src/response/helpers/header.ts
414
+ function appendResponseHeader(event, name, value) {
415
+ const { headers } = event.response;
416
+ if (Array.isArray(value)) for (const v of value) headers.append(name, sanitizeHeaderValue(v));
417
+ else headers.append(name, sanitizeHeaderValue(value));
418
+ }
419
+ function appendResponseHeaderDirective(event, name, value) {
420
+ const { headers } = event.response;
421
+ const existing = headers.get(name);
422
+ if (!existing) {
423
+ if (Array.isArray(value)) headers.set(name, sanitizeHeaderValue(value.join("; ")));
424
+ else headers.set(name, sanitizeHeaderValue(value));
425
+ return;
426
+ }
427
+ const directives = existing.split("; ");
428
+ if (Array.isArray(value)) directives.push(...value);
429
+ else directives.push(value);
430
+ const unique = [...new Set(directives)];
431
+ headers.set(name, sanitizeHeaderValue(unique.join("; ")));
432
+ }
433
+ //#endregion
434
+ //#region src/response/helpers/utils.ts
435
+ function setResponseContentTypeByFileName(event, fileName) {
436
+ const ext = extname(fileName);
437
+ if (ext) {
438
+ let type = getMimeType(ext.substring(1));
439
+ if (type) {
440
+ const charset = getCharsetForMimeType(type);
441
+ if (charset) type += `; charset=${charset}`;
442
+ event.response.headers.set(HeaderName.CONTENT_TYPE, type);
443
+ }
444
+ }
445
+ }
446
+ //#endregion
447
+ //#region src/response/helpers/header-disposition.ts
448
+ const ENCODE_URL_ATTR_CHAR_REGEXP = /[\x00-\x20"'()*,/:;<=>?@[\\\]{}\x7f]/g;
449
+ const NON_ASCII_REGEXP = /[^\x20-\x7e]/g;
450
+ const QUOTE_REGEXP = /[\\"]/g;
451
+ const HEX_ESCAPE_REGEXP = /%[0-9A-Fa-f]{2}/;
452
+ const ASCII_TEXT_REGEXP = /^[\x20-\x7e]+$/;
453
+ const TOKEN_REGEXP = /^[!#$%&'*+.0-9A-Z^_`a-z|~-]+$/;
454
+ function pencode(char) {
455
+ return `%${char.charCodeAt(0).toString(16).toUpperCase()}`;
456
+ }
457
+ function quoteString(value) {
458
+ return `"${value.replace(QUOTE_REGEXP, "\\$&")}"`;
459
+ }
460
+ function getAscii(value) {
461
+ return value.replace(NON_ASCII_REGEXP, "?");
462
+ }
463
+ function encodeExtended(value) {
464
+ return encodeURIComponent(value).replace(ENCODE_URL_ATTR_CHAR_REGEXP, pencode);
465
+ }
466
+ function formatFilename(value) {
467
+ if (TOKEN_REGEXP.test(value)) return `filename=${value}`;
468
+ return `filename=${quoteString(value)}`;
469
+ }
470
+ function setDisposition(event, type, filename) {
471
+ let disposition = type;
472
+ if (typeof filename === "string") {
473
+ setResponseContentTypeByFileName(event, filename);
474
+ if (ASCII_TEXT_REGEXP.test(filename) && !HEX_ESCAPE_REGEXP.test(filename)) disposition += `; ${formatFilename(filename)}`;
475
+ else {
476
+ disposition += `; ${formatFilename(getAscii(filename))}`;
477
+ disposition += `; filename*=UTF-8''${encodeExtended(filename)}`;
478
+ }
479
+ }
480
+ event.response.headers.set(HeaderName.CONTENT_DISPOSITION, disposition);
481
+ }
482
+ function setResponseHeaderAttachment(event, filename) {
483
+ setDisposition(event, "attachment", filename);
484
+ }
485
+ function setResponseHeaderInline(event, filename) {
486
+ setDisposition(event, "inline", filename);
487
+ }
488
+ //#endregion
489
+ //#region src/response/helpers/header-content-type.ts
490
+ function setResponseHeaderContentType(event, input, ifNotExists) {
491
+ if (ifNotExists) {
492
+ if (event.response.headers.get(HeaderName.CONTENT_TYPE)) return;
493
+ }
494
+ const contentType = getMimeType(input);
495
+ if (contentType) event.response.headers.set(HeaderName.CONTENT_TYPE, contentType);
496
+ }
497
+ //#endregion
498
+ //#region src/error/is.ts
499
+ function isError(input) {
500
+ return hasInstanceof(input, ErrorSymbol);
501
+ }
502
+ //#endregion
503
+ //#region src/error/create.ts
504
+ function isNativeError(input) {
505
+ return isObject(input) && typeof input.message === "string" && typeof input.name === "string";
506
+ }
507
+ /**
508
+ * Create an internal error object by
509
+ * - an existing AppError (returned as-is)
510
+ * - an HTTPError (wrapped into a AppError preserving status)
511
+ * - an Error (wrapped preserving message and cause)
512
+ * - an options object (status, message, etc.)
513
+ * - a message string
514
+ *
515
+ * @param input
516
+ */
517
+ function createError(input) {
518
+ if (isError(input)) return input;
519
+ if (typeof input === "string") return new AppError(input);
520
+ if (isHTTPError(input)) return new AppError({
521
+ message: input.message,
522
+ code: input.code,
523
+ status: input.status,
524
+ redirectURL: input.redirectURL,
525
+ cause: input
526
+ });
527
+ if (isNativeError(input)) return new AppError({
528
+ message: input.message,
529
+ cause: input
530
+ });
531
+ if (!isObject(input)) return new AppError();
532
+ const options = { ...input };
533
+ if (options.cause === void 0) options.cause = input;
534
+ return new AppError(options);
535
+ }
536
+ //#endregion
537
+ //#region src/response/to-response.ts
538
+ function stripWeakPrefix(etag) {
539
+ return etag.startsWith("W/") ? etag.slice(2) : etag;
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
+ */
559
+ async function applyEtag(body, event, headers) {
560
+ const etagFn = effectiveEtagFn(event);
561
+ if (!etagFn) return;
562
+ const etag = await etagFn(body);
563
+ if (!etag) return;
564
+ headers.set("etag", etag);
565
+ const ifNoneMatch = event.headers.get("if-none-match");
566
+ if (ifNoneMatch && (ifNoneMatch === "*" || ifNoneMatch.split(",").some((t) => stripWeakPrefix(t.trim()) === stripWeakPrefix(etag)))) return new Response(null, {
567
+ status: 304,
568
+ headers
569
+ });
570
+ }
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) {
584
+ if (value === void 0) return;
585
+ if (value === null) return new Response(null, {
586
+ status: event.response.status,
587
+ headers: event.response.headers
588
+ });
589
+ const t = typeof value;
590
+ if (t === "string") {
591
+ const { status, headers } = event.response;
592
+ if (!headers.has("content-type")) headers.set("content-type", "text/plain; charset=utf-8");
593
+ if (event.appOptions.etag !== null) return applyEtag(value, event, headers).then((cached) => cached ?? new Response(value, {
594
+ status,
595
+ headers
596
+ }));
597
+ return new Response(value, {
598
+ status,
599
+ headers
600
+ });
601
+ }
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, {
613
+ status,
614
+ headers
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
+ }
623
+ }
624
+ const { status, headers } = event.response;
625
+ if (!headers.has("content-type")) headers.set("content-type", "application/json; charset=utf-8");
626
+ let json;
627
+ try {
628
+ json = JSON.stringify(value);
629
+ } catch (e) {
630
+ throw createError({
631
+ message: "JSON serialization failed",
632
+ status: 500,
633
+ cause: e
634
+ });
635
+ }
636
+ if (event.appOptions.etag !== null) return applyEtag(json, event, headers).then((cached) => cached ?? new Response(json, {
637
+ status,
638
+ headers
639
+ }));
640
+ return new Response(json, {
641
+ status,
642
+ headers
643
+ });
644
+ }
645
+ //#endregion
646
+ //#region src/response/helpers/send-accepted.ts
647
+ async function sendAccepted(event, data) {
648
+ event.response.status = 202;
649
+ return await toResponse(data ?? "", event);
650
+ }
651
+ //#endregion
652
+ //#region src/response/helpers/send-created.ts
653
+ async function sendCreated(event, data) {
654
+ event.response.status = 201;
655
+ return await toResponse(data ?? "", event);
656
+ }
657
+ //#endregion
658
+ //#region src/response/helpers/send-file.ts
659
+ async function sendFile(event, options) {
660
+ let stats;
661
+ if (typeof options.stats === "function") stats = await options.stats();
662
+ else stats = options.stats;
663
+ const name = options.name || stats.name;
664
+ const { headers } = event.response;
665
+ const disposition = options.disposition ?? (options.attachment ? "attachment" : void 0);
666
+ if (name) {
667
+ const fileName = basename(name);
668
+ if (disposition) {
669
+ if (!headers.get(HeaderName.CONTENT_DISPOSITION)) if (disposition === "inline") setResponseHeaderInline(event, fileName);
670
+ else setResponseHeaderAttachment(event, fileName);
671
+ } else setResponseContentTypeByFileName(event, fileName);
672
+ }
673
+ const contentOptions = {};
674
+ let statusCode = event.response.status;
675
+ if (stats.size) {
676
+ const rangeHeader = event.headers.get(HeaderName.RANGE);
677
+ if (rangeHeader) {
678
+ const [x, y] = rangeHeader.replace("bytes=", "").split("-");
679
+ const parsedStart = Number.parseInt(x, 10);
680
+ const parsedEnd = Number.parseInt(y, 10);
681
+ contentOptions.start = Number.isFinite(parsedStart) && parsedStart >= 0 ? parsedStart : 0;
682
+ contentOptions.end = Number.isFinite(parsedEnd) && parsedEnd >= 0 ? Math.min(parsedEnd, stats.size - 1) : stats.size - 1;
683
+ if (contentOptions.start >= stats.size || contentOptions.start > contentOptions.end) {
684
+ const rangeHeaders = new Headers(headers);
685
+ rangeHeaders.set(HeaderName.CONTENT_RANGE, `bytes */${stats.size}`);
686
+ return new Response(null, {
687
+ status: 416,
688
+ headers: rangeHeaders
689
+ });
690
+ }
691
+ headers.set(HeaderName.CONTENT_RANGE, `bytes ${contentOptions.start}-${contentOptions.end}/${stats.size}`);
692
+ headers.set(HeaderName.CONTENT_LENGTH, `${contentOptions.end - contentOptions.start + 1}`);
693
+ statusCode = 206;
694
+ } else headers.set(HeaderName.CONTENT_LENGTH, `${stats.size}`);
695
+ headers.set(HeaderName.ACCEPT_RANGES, "bytes");
696
+ if (stats.mtime) {
697
+ const mtime = new Date(stats.mtime);
698
+ headers.set(HeaderName.LAST_MODIFIED, mtime.toUTCString());
699
+ headers.set(HeaderName.ETag, `W/"${stats.size}-${mtime.getTime()}"`);
700
+ }
701
+ }
702
+ const content = await options.content(contentOptions);
703
+ return new Response(content, {
704
+ status: statusCode,
705
+ headers
706
+ });
707
+ }
708
+ //#endregion
709
+ //#region src/request/helpers/header.ts
710
+ function getRequestHeader(event, name) {
711
+ return event.headers.get(name);
712
+ }
713
+ //#endregion
714
+ //#region src/request/helpers/negotiator.ts
715
+ const NEGOTIATOR_KEY = Symbol.for("routup:negotiator");
716
+ function headersToPlainObject(headers) {
717
+ const result = {};
718
+ headers.forEach((value, key) => {
719
+ result[key] = value;
720
+ });
721
+ return result;
722
+ }
723
+ function useRequestNegotiator(event) {
724
+ let value = event.store[NEGOTIATOR_KEY];
725
+ if (value) return value;
726
+ value = new Negotiator({ headers: headersToPlainObject(event.headers) });
727
+ event.store[NEGOTIATOR_KEY] = value;
728
+ return value;
729
+ }
730
+ //#endregion
731
+ //#region src/request/helpers/header-accept.ts
732
+ function getRequestAcceptableContentTypes(event) {
733
+ return useRequestNegotiator(event).mediaTypes();
734
+ }
735
+ function getRequestAcceptableContentType(event, input) {
736
+ input = input || [];
737
+ const items = Array.isArray(input) ? input : [input];
738
+ if (items.length === 0) return getRequestAcceptableContentTypes(event).shift();
739
+ if (!getRequestHeader(event, HeaderName.ACCEPT)) return items[0];
740
+ let polluted = false;
741
+ const mimeTypes = [];
742
+ for (const item of items) {
743
+ const mimeType = getMimeType(item);
744
+ if (mimeType) mimeTypes.push(mimeType);
745
+ else polluted = true;
746
+ }
747
+ const matches = useRequestNegotiator(event).mediaTypes(mimeTypes);
748
+ if (matches.length > 0) {
749
+ if (polluted) return items[0];
750
+ return items[mimeTypes.indexOf(matches[0])];
751
+ }
752
+ }
753
+ //#endregion
754
+ //#region src/response/helpers/send-format.ts
755
+ function sendFormat(event, input) {
756
+ const { default: formatDefault, ...formats } = input;
757
+ const contentTypes = Object.keys(formats);
758
+ if (contentTypes.length === 0) return formatDefault();
759
+ const contentType = getRequestAcceptableContentType(event, contentTypes);
760
+ if (contentType && formats[contentType]) return formats[contentType]();
761
+ return formatDefault();
762
+ }
763
+ //#endregion
764
+ //#region src/response/helpers/send-redirect.ts
765
+ function escapeHtml(str) {
766
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
767
+ }
768
+ function isAllowedRedirectUrl(location) {
769
+ if (location.startsWith("//")) return false;
770
+ if (location.startsWith("/") || location.startsWith(".")) return true;
771
+ try {
772
+ const url = new URL(location);
773
+ return url.protocol === "http:" || url.protocol === "https:";
774
+ } catch {
775
+ return true;
776
+ }
777
+ }
778
+ function sendRedirect(event, location, statusCode = 302) {
779
+ if (!isAllowedRedirectUrl(location)) throw new AppError({
780
+ status: 400,
781
+ message: "Invalid redirect URL scheme."
782
+ });
783
+ const sanitizedLocation = sanitizeHeaderValue(location);
784
+ const html = `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${escapeHtml(location)}"></head></html>`;
785
+ const headers = new Headers(event.response.headers);
786
+ headers.set("location", sanitizedLocation);
787
+ headers.set("content-type", "text/html; charset=utf-8");
788
+ headers.delete("content-length");
789
+ return new Response(html, {
790
+ status: statusCode,
791
+ headers
792
+ });
793
+ }
794
+ //#endregion
795
+ //#region src/response/helpers/send-stream.ts
796
+ function sendStream(event, stream) {
797
+ const { status, headers } = event.response;
798
+ return new Response(stream, {
799
+ status,
800
+ headers
801
+ });
802
+ }
803
+ //#endregion
804
+ //#region src/dispatcher/module.ts
805
+ var DispatcherEvent = class {
806
+ request;
807
+ params;
808
+ path;
809
+ method;
810
+ /**
811
+ * Collected allowed methods (for OPTIONS).
812
+ */
813
+ methodsAllowed;
814
+ mountPath;
815
+ error;
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;
830
+ _dispatched;
831
+ _response;
832
+ _store;
833
+ /**
834
+ * Cached parsed URL (avoids double-parsing).
835
+ */
836
+ _url;
837
+ /**
838
+ * Continuation function for middleware onion model.
839
+ */
840
+ _next;
841
+ _signal;
842
+ _signalCleanup;
843
+ /**
844
+ * Whether _next has already been called (guard against double-invocation).
845
+ */
846
+ _nextCalled;
847
+ /**
848
+ * The cached result of the next handler.
849
+ */
850
+ _nextResult;
851
+ constructor(request) {
852
+ this.request = request;
853
+ this._url = new FastURL(request.url);
854
+ this.method = request.method;
855
+ this.path = this._url.pathname;
856
+ this.mountPath = "/";
857
+ this.params = {};
858
+ this.appOptions = {};
859
+ this.isDispatching = false;
860
+ this.methodsAllowed = /* @__PURE__ */ new Set();
861
+ this._dispatched = false;
862
+ this._nextCalled = false;
863
+ }
864
+ get response() {
865
+ if (!this._response) this._response = {
866
+ status: 200,
867
+ headers: new Headers()
868
+ };
869
+ return this._response;
870
+ }
871
+ get signal() {
872
+ if (!this._signal) this._signal = this.request.signal;
873
+ return this._signal;
874
+ }
875
+ set signal(value) {
876
+ if (this._signalCleanup) {
877
+ this._signalCleanup();
878
+ this._signalCleanup = void 0;
879
+ }
880
+ if (value === this.request.signal) {
881
+ this._signal = value;
882
+ return;
883
+ }
884
+ const controller = new AbortController();
885
+ const abort = (e) => {
886
+ const reason = e?.target instanceof AbortSignal ? e.target.reason : void 0;
887
+ this.request.signal.removeEventListener("abort", abort);
888
+ value.removeEventListener("abort", abort);
889
+ controller.abort(reason);
890
+ };
891
+ if (this.request.signal.aborted || value.aborted) {
892
+ const reason = this.request.signal.aborted ? this.request.signal.reason : value.reason;
893
+ controller.abort(reason);
894
+ } else {
895
+ this.request.signal.addEventListener("abort", abort, { once: true });
896
+ value.addEventListener("abort", abort, { once: true });
897
+ this._signalCleanup = () => {
898
+ this.request.signal.removeEventListener("abort", abort);
899
+ value.removeEventListener("abort", abort);
900
+ };
901
+ }
902
+ this._signal = controller.signal;
903
+ }
904
+ get dispatched() {
905
+ return this._dispatched;
906
+ }
907
+ set dispatched(value) {
908
+ this._dispatched = value;
909
+ }
910
+ async next(event, error) {
911
+ if (this._nextCalled) return this._nextResult;
912
+ this._nextCalled = true;
913
+ if (this._next) this._nextResult = this._next(event, error);
914
+ return this._nextResult;
915
+ }
916
+ setNext(fn) {
917
+ if (fn) this._next = async (event, error) => {
918
+ return toResponse(await fn(error), event);
919
+ };
920
+ else this._next = void 0;
921
+ this._nextCalled = false;
922
+ this._nextResult = void 0;
923
+ }
924
+ build(signal) {
925
+ return new AppEvent({
926
+ request: this.request,
927
+ params: this.params,
928
+ path: this.path,
929
+ method: this.method,
930
+ mountPath: this.mountPath,
931
+ headers: this.request.headers,
932
+ searchParams: new URLSearchParams(this._url.search),
933
+ response: this.response,
934
+ store: this.store,
935
+ signal: signal ?? this.signal,
936
+ appOptions: this.appOptions,
937
+ next: (event, error) => this.next(event, error)
938
+ });
939
+ }
940
+ get store() {
941
+ if (!this._store) this._store = Object.create(null);
942
+ return this._store;
943
+ }
944
+ };
945
+ //#endregion
946
+ //#region src/handler/constants.ts
947
+ const HandlerType = {
948
+ CORE: "core",
949
+ ERROR: "error"
950
+ };
951
+ const HandlerSymbol = Symbol.for("Handler");
952
+ //#endregion
953
+ //#region src/hook/constants.ts
954
+ const HookName = {
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",
966
+ ERROR: "error",
967
+ CHILD_MATCH: "childMatch",
968
+ CHILD_DISPATCH_BEFORE: "childDispatchBefore",
969
+ CHILD_DISPATCH_AFTER: "childDispatchAfter"
970
+ };
971
+ //#endregion
972
+ //#region src/hook/module.ts
973
+ var Hooks = class Hooks {
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;
984
+ constructor() {
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;
994
+ }
995
+ addListener(name, fn, priority = 0) {
996
+ this.items[name] = this.items[name] || [];
997
+ const entry = {
998
+ fn,
999
+ priority
1000
+ };
1001
+ let i = 0;
1002
+ while (i < this.items[name].length && this.items[name][i].priority >= priority) i++;
1003
+ this.items[name].splice(i, 0, entry);
1004
+ this._hasAny = true;
1005
+ return () => {
1006
+ this.removeListener(name, fn);
1007
+ };
1008
+ }
1009
+ removeListener(name, fn) {
1010
+ if (!this.items[name]) return;
1011
+ if (typeof fn === "undefined") {
1012
+ delete this.items[name];
1013
+ this.recomputeHasAny();
1014
+ return;
1015
+ }
1016
+ if (typeof fn === "function") {
1017
+ const index = this.items[name].findIndex((entry) => entry.fn === fn);
1018
+ if (index !== -1) this.items[name].splice(index, 1);
1019
+ }
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;
1035
+ }
1036
+ /**
1037
+ * Create a new `Hooks` instance seeded with the same listeners as this
1038
+ * one.
1039
+ *
1040
+ * Listener functions are shared by reference; priority and ordering are
1041
+ * preserved. Future mutations on the returned instance do not affect this
1042
+ * one (and vice versa).
1043
+ */
1044
+ clone() {
1045
+ const next = new Hooks();
1046
+ const names = Object.keys(this.items);
1047
+ for (const name of names) {
1048
+ const entries = this.items[name];
1049
+ for (const entry of entries) next.addListener(name, entry.fn, entry.priority);
1050
+ }
1051
+ return next;
1052
+ }
1053
+ async trigger(name, event) {
1054
+ if (!this.items[name] || this.items[name].length === 0) return;
1055
+ try {
1056
+ for (let i = 0; i < this.items[name].length; i++) {
1057
+ const { fn } = this.items[name][i];
1058
+ await this.triggerListener(name, event, fn);
1059
+ if (event.dispatched) {
1060
+ if (event.error) event.error = void 0;
1061
+ return;
1062
+ }
1063
+ }
1064
+ } catch (e) {
1065
+ if (!event.error) event.error = createError(e);
1066
+ if (!this.isErrorListenerHook(name)) {
1067
+ await this.trigger(HookName.ERROR, event);
1068
+ if (event.dispatched) {
1069
+ if (event.error) event.error = void 0;
1070
+ }
1071
+ }
1072
+ }
1073
+ }
1074
+ triggerListener(name, event, listener) {
1075
+ if (this.isErrorListenerHook(name)) {
1076
+ if (event.error) return listener(event);
1077
+ return;
1078
+ }
1079
+ return listener(event);
1080
+ }
1081
+ isErrorListenerHook(input) {
1082
+ return input === HookName.ERROR;
1083
+ }
1084
+ };
1085
+ //#endregion
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);
1098
+ }
1099
+ get type() {
1100
+ return this.config.type;
1101
+ }
1102
+ get path() {
1103
+ return this.config.path;
1104
+ }
1105
+ async dispatch(event) {
1106
+ if (this.hooks.hasListeners(HookName.CHILD_DISPATCH_BEFORE)) {
1107
+ await this.hooks.trigger(HookName.CHILD_DISPATCH_BEFORE, event);
1108
+ if (event.dispatched) return;
1109
+ }
1110
+ let response;
1111
+ try {
1112
+ const effectiveTimeout = this.resolveTimeout(event.appOptions);
1113
+ let childController;
1114
+ let cleanupParentListener;
1115
+ if (effectiveTimeout) {
1116
+ const parentSignal = event.signal;
1117
+ childController = new AbortController();
1118
+ if (parentSignal.aborted) childController.abort(parentSignal.reason);
1119
+ else {
1120
+ const onAbort = () => childController.abort(parentSignal.reason);
1121
+ parentSignal.addEventListener("abort", onAbort, { once: true });
1122
+ cleanupParentListener = () => parentSignal.removeEventListener("abort", onAbort);
1123
+ }
1124
+ }
1125
+ const handlerEvent = childController ? event.build(childController.signal) : event.build();
1126
+ let result;
1127
+ try {
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 {
1135
+ const { fn } = this.config;
1136
+ invocation = fn(handlerEvent);
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;
1144
+ } finally {
1145
+ if (cleanupParentListener) cleanupParentListener();
1146
+ }
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
+ }
1153
+ } catch (e) {
1154
+ event.error = isError(e) ? e : createError(e);
1155
+ if (this.hooks.hasListeners(HookName.ERROR)) await this.hooks.trigger(HookName.ERROR, event);
1156
+ if (event.dispatched) event.error = void 0;
1157
+ else throw event.error;
1158
+ }
1159
+ if (this.hooks.hasListeners(HookName.CHILD_DISPATCH_AFTER)) await this.hooks.trigger(HookName.CHILD_DISPATCH_AFTER, event);
1160
+ return response;
1161
+ }
1162
+ /**
1163
+ * Resolve a handler's return value into the final value handed to `toResponse`.
1164
+ *
1165
+ * Contract:
1166
+ * - non-undefined value → return as-is (becomes the response)
1167
+ * - `undefined` + `event.next()` was called → forward downstream result
1168
+ * - `undefined` + `event.next()` not yet called → wait until either `next()` is
1169
+ * invoked (e.g. from an async callback) or `signal` aborts. A global or
1170
+ * per-handler timeout aborts `signal` and surfaces as 408. With no timeout
1171
+ * configured and no eventual `next()` call, the request hangs by design.
1172
+ */
1173
+ async resolveHandlerResult(invocation, handlerEvent) {
1174
+ const value = await invocation;
1175
+ if (typeof value !== "undefined") return value;
1176
+ if (handlerEvent.nextCalled) return handlerEvent.nextResult;
1177
+ const { signal } = handlerEvent;
1178
+ if (signal.aborted) throw createError({
1179
+ status: 408,
1180
+ message: "Request Timeout"
1181
+ });
1182
+ return new Promise((resolve, reject) => {
1183
+ const onAbort = () => {
1184
+ signal.removeEventListener("abort", onAbort);
1185
+ reject(createError({
1186
+ status: 408,
1187
+ message: "Request Timeout"
1188
+ }));
1189
+ };
1190
+ signal.addEventListener("abort", onAbort, { once: true });
1191
+ handlerEvent.whenNextCalled().then(() => {
1192
+ signal.removeEventListener("abort", onAbort);
1193
+ resolve(handlerEvent.nextResult);
1194
+ });
1195
+ });
1196
+ }
1197
+ async executeWithTimeout(fn, effectiveTimeout, controller) {
1198
+ if (!effectiveTimeout) return fn();
1199
+ let timerId;
1200
+ try {
1201
+ return await Promise.race([fn(), new Promise((_, reject) => {
1202
+ timerId = setTimeout(() => {
1203
+ if (controller) controller.abort();
1204
+ reject(createError({
1205
+ status: 408,
1206
+ message: "Request Timeout"
1207
+ }));
1208
+ }, effectiveTimeout);
1209
+ })]);
1210
+ } finally {
1211
+ clearTimeout(timerId);
1212
+ }
1213
+ }
1214
+ resolveTimeout(appOptions) {
1215
+ const routerDefault = appOptions.handlerTimeout;
1216
+ const handlerOverride = this.config.timeout;
1217
+ if (!routerDefault && !handlerOverride) return;
1218
+ if (!routerDefault) return handlerOverride;
1219
+ if (!handlerOverride) return routerDefault;
1220
+ if (appOptions.handlerTimeoutOverridable) return handlerOverride;
1221
+ return Math.min(routerDefault, handlerOverride);
1222
+ }
1223
+ mountHooks() {
1224
+ if (this.config.onBefore) this.hooks.addListener(HookName.CHILD_DISPATCH_BEFORE, this.config.onBefore);
1225
+ if (this.config.onAfter) this.hooks.addListener(HookName.CHILD_DISPATCH_AFTER, this.config.onAfter);
1226
+ if (this.config.onError) this.hooks.addListener(HookName.ERROR, this.config.onError);
1227
+ }
1228
+ };
1229
+ //#endregion
1230
+ //#region src/handler/core/define.ts
1231
+ function defineCoreHandler(input) {
1232
+ if (typeof input === "function") return new Handler({
1233
+ type: HandlerType.CORE,
1234
+ fn: input
1235
+ });
1236
+ return new Handler({
1237
+ type: HandlerType.CORE,
1238
+ ...input
1239
+ });
1240
+ }
1241
+ //#endregion
1242
+ //#region src/handler/error/define.ts
1243
+ function defineErrorHandler(input) {
1244
+ if (typeof input === "function") return new Handler({
1245
+ type: HandlerType.ERROR,
1246
+ fn: input
1247
+ });
1248
+ return new Handler({
1249
+ type: HandlerType.ERROR,
1250
+ ...input
1251
+ });
1252
+ }
1253
+ //#endregion
1254
+ //#region src/handler/adapters/node/define.ts
1255
+ const kHandled = /* @__PURE__ */ Symbol("handled");
1256
+ function callHandler(handler, req, res) {
1257
+ return new Promise((resolve, reject) => {
1258
+ let settled = false;
1259
+ const onClose = () => settle(kHandled);
1260
+ const onFinish = () => settle(kHandled);
1261
+ const onError = (error) => fail(error);
1262
+ function cleanup() {
1263
+ res.removeListener("close", onClose);
1264
+ res.removeListener("finish", onFinish);
1265
+ res.removeListener("error", onError);
1266
+ }
1267
+ function settle(value) {
1268
+ if (settled) return;
1269
+ settled = true;
1270
+ cleanup();
1271
+ resolve(value);
1272
+ }
1273
+ function fail(error) {
1274
+ if (settled) return;
1275
+ settled = true;
1276
+ cleanup();
1277
+ reject(error);
1278
+ }
1279
+ res.once("close", onClose);
1280
+ res.once("finish", onFinish);
1281
+ res.once("error", onError);
1282
+ try {
1283
+ Promise.resolve(handler(req, res)).then(() => settle(kHandled)).catch(fail);
1284
+ } catch (error) {
1285
+ fail(error);
1286
+ }
1287
+ });
1288
+ }
1289
+ function callMiddleware(handler, req, res) {
1290
+ return new Promise((resolve, reject) => {
1291
+ let settled = false;
1292
+ const onClose = () => settle(kHandled);
1293
+ const onFinish = () => settle(kHandled);
1294
+ const onError = (error) => fail(error);
1295
+ function cleanup() {
1296
+ res.removeListener("close", onClose);
1297
+ res.removeListener("finish", onFinish);
1298
+ res.removeListener("error", onError);
1299
+ }
1300
+ function settle(value) {
1301
+ if (settled) return;
1302
+ settled = true;
1303
+ cleanup();
1304
+ resolve(value);
1305
+ }
1306
+ function fail(error) {
1307
+ if (settled) return;
1308
+ settled = true;
1309
+ cleanup();
1310
+ reject(error);
1311
+ }
1312
+ res.once("close", onClose);
1313
+ res.once("finish", onFinish);
1314
+ res.once("error", onError);
1315
+ try {
1316
+ Promise.resolve(handler(req, res, (error) => {
1317
+ if (error) fail(error);
1318
+ else settle(res.writableEnded || res.destroyed ? kHandled : void 0);
1319
+ })).catch(fail);
1320
+ } catch (error) {
1321
+ fail(error);
1322
+ }
1323
+ });
1324
+ }
1325
+ function createNodeBridge(handler, isMiddleware) {
1326
+ if (typeof handler !== "function") throw new AppError("fromNodeHandler/fromNodeMiddleware expects a function.");
1327
+ return defineCoreHandler({ fn: (async (event) => {
1328
+ const node = event.request.runtime?.node;
1329
+ if (!node?.req || !node?.res) throw new AppError("fromNodeHandler/fromNodeMiddleware requires a Node.js runtime.");
1330
+ const req = node.req;
1331
+ const res = node.res;
1332
+ if ((isMiddleware ? await callMiddleware(handler, req, res) : await callHandler(handler, req, res)) === kHandled) return null;
1333
+ return event.next();
1334
+ }) });
1335
+ }
1336
+ /**
1337
+ * Wraps a Node.js `(req, res)` handler for use in the routup pipeline.
1338
+ *
1339
+ * @example
1340
+ * ```typescript
1341
+ * import { fromNodeHandler } from 'routup/node';
1342
+ *
1343
+ * router.use(fromNodeHandler((req, res) => {
1344
+ * res.end('Hello');
1345
+ * }));
1346
+ * ```
1347
+ */
1348
+ function fromNodeHandler(handler) {
1349
+ return createNodeBridge(handler, false);
1350
+ }
1351
+ /**
1352
+ * Wraps a Node.js `(req, res, next)` middleware for use in the routup pipeline.
1353
+ *
1354
+ * @example
1355
+ * ```typescript
1356
+ * import cors from 'cors';
1357
+ * import { fromNodeMiddleware } from 'routup/node';
1358
+ *
1359
+ * router.use(fromNodeMiddleware(cors()));
1360
+ * ```
1361
+ */
1362
+ function fromNodeMiddleware(handler) {
1363
+ return createNodeBridge(handler, true);
1364
+ }
1365
+ //#endregion
1366
+ //#region src/handler/adapters/web/is.ts
1367
+ function isWebHandlerProvider(input) {
1368
+ return isObject(input) && typeof input.fetch === "function";
1369
+ }
1370
+ function isWebHandler(input) {
1371
+ return typeof input === "function";
1372
+ }
1373
+ //#endregion
1374
+ //#region src/handler/adapters/web/define.ts
1375
+ function fromWebHandler(input) {
1376
+ if (isWebHandlerProvider(input)) return fromWebHandler(input.fetch.bind(input));
1377
+ if (typeof input !== "function") throw new AppError("fromWebHandler expects a function or an object with a fetch method.");
1378
+ return defineCoreHandler({ fn: (event) => input(event.request) });
1379
+ }
1380
+ //#endregion
1381
+ //#region src/handler/is.ts
1382
+ function isHandlerOptions(input) {
1383
+ return isObject(input) && typeof input.fn === "function" && typeof input.type === "string";
1384
+ }
1385
+ function isHandler(input) {
1386
+ return hasInstanceof(input, HandlerSymbol);
1387
+ }
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
1401
+ //#region src/request/helpers/cache.ts
1402
+ function isRequestCacheable(event, modifiedTime) {
1403
+ const modifiedSince = event.headers.get(HeaderName.IF_MODIFIED_SINCE);
1404
+ if (!modifiedSince) return false;
1405
+ modifiedTime = typeof modifiedTime === "string" ? new Date(modifiedTime) : modifiedTime;
1406
+ const sinceDate = new Date(modifiedSince);
1407
+ if (Number.isNaN(sinceDate.getTime()) || Number.isNaN(modifiedTime.getTime())) return false;
1408
+ return sinceDate >= modifiedTime;
1409
+ }
1410
+ //#endregion
1411
+ //#region src/request/helpers/header-accept-charset.ts
1412
+ function getRequestAcceptableCharsets(event) {
1413
+ return useRequestNegotiator(event).charsets();
1414
+ }
1415
+ function getRequestAcceptableCharset(event, input) {
1416
+ input = input || [];
1417
+ const items = Array.isArray(input) ? input : [input];
1418
+ if (items.length === 0) return getRequestAcceptableCharsets(event).shift();
1419
+ return useRequestNegotiator(event).charsets(items).shift() || void 0;
1420
+ }
1421
+ //#endregion
1422
+ //#region src/request/helpers/header-accept-encoding.ts
1423
+ function getRequestAcceptableEncodings(event) {
1424
+ return useRequestNegotiator(event).encodings();
1425
+ }
1426
+ function getRequestAcceptableEncoding(event, input) {
1427
+ input = input || [];
1428
+ const items = Array.isArray(input) ? input : [input];
1429
+ if (items.length === 0) return getRequestAcceptableEncodings(event).shift();
1430
+ return useRequestNegotiator(event).encodings(items).shift() || void 0;
1431
+ }
1432
+ //#endregion
1433
+ //#region src/request/helpers/header-accept-language.ts
1434
+ function getRequestAcceptableLanguages(event) {
1435
+ return useRequestNegotiator(event).languages();
1436
+ }
1437
+ function getRequestAcceptableLanguage(event, input) {
1438
+ input = input || [];
1439
+ const items = Array.isArray(input) ? input : [input];
1440
+ if (items.length === 0) return getRequestAcceptableLanguages(event).shift();
1441
+ return useRequestNegotiator(event).languages(items).shift() || void 0;
1442
+ }
1443
+ //#endregion
1444
+ //#region src/request/helpers/header-content-type.ts
1445
+ function matchRequestContentType(event, contentType) {
1446
+ const header = getRequestHeader(event, HeaderName.CONTENT_TYPE);
1447
+ if (!header) return true;
1448
+ return header.split(";")[0].trim() === getMimeType(contentType);
1449
+ }
1450
+ //#endregion
1451
+ //#region src/request/helpers/hostname.ts
1452
+ function getRequestHostName(event, options = {}) {
1453
+ let trustProxy;
1454
+ if (typeof options.trustProxy !== "undefined") trustProxy = buildTrustProxyFn(options.trustProxy);
1455
+ else trustProxy = event.appOptions.trustProxy ?? DEFAULT_TRUST_PROXY;
1456
+ let hostname = event.headers.get(HeaderName.X_FORWARDED_HOST);
1457
+ if (!hostname || !event.request.ip || !trustProxy(event.request.ip, 0)) hostname = event.headers.get(HeaderName.HOST);
1458
+ else if (hostname && hostname.includes(",")) hostname = hostname.substring(0, hostname.indexOf(",")).trimEnd();
1459
+ if (!hostname) return;
1460
+ const offset = hostname[0] === "[" ? hostname.indexOf("]") + 1 : 0;
1461
+ const index = hostname.indexOf(":", offset);
1462
+ const result = index !== -1 ? hostname.substring(0, index) : hostname;
1463
+ if (/[\x00-\x1F\x7F\s/@\\]/.test(result)) return;
1464
+ return result;
1465
+ }
1466
+ //#endregion
1467
+ //#region src/request/helpers/ip.ts
1468
+ /**
1469
+ * Get the client IP address from the request.
1470
+ *
1471
+ * When `trustProxy` is configured, walks the `X-Forwarded-For` chain
1472
+ * and returns the rightmost untrusted address (the actual client IP).
1473
+ * Falls back to `event.request.ip` (the direct connection IP).
1474
+ */
1475
+ function getRequestIP(event, options = {}) {
1476
+ let trustProxy;
1477
+ if (typeof options.trustProxy !== "undefined") trustProxy = buildTrustProxyFn(options.trustProxy);
1478
+ else trustProxy = event.appOptions.trustProxy ?? DEFAULT_TRUST_PROXY;
1479
+ const socketAddr = event.request.ip;
1480
+ if (!socketAddr) return;
1481
+ const forwarded = event.headers.get(HeaderName.X_FORWARDED_FOR);
1482
+ const addrs = [socketAddr];
1483
+ if (forwarded) {
1484
+ const parts = forwarded.split(",");
1485
+ for (let i = parts.length - 1; i >= 0; i--) {
1486
+ const addr = parts[i].trim();
1487
+ if (addr) addrs.push(addr);
1488
+ }
1489
+ }
1490
+ for (let i = 0; i < addrs.length - 1; i++) if (!trustProxy(addrs[i], i)) return addrs[i];
1491
+ return addrs[addrs.length - 1];
1492
+ }
1493
+ //#endregion
1494
+ //#region src/request/helpers/protocol.ts
1495
+ function getRequestProtocol(event, options = {}) {
1496
+ let trustProxy;
1497
+ if (typeof options.trustProxy !== "undefined") trustProxy = buildTrustProxyFn(options.trustProxy);
1498
+ else trustProxy = event.appOptions.trustProxy ?? DEFAULT_TRUST_PROXY;
1499
+ let protocol;
1500
+ try {
1501
+ if (new URL(event.request.url).protocol === "https:") protocol = "https";
1502
+ else protocol = "http";
1503
+ } catch {
1504
+ protocol = options.default || "http";
1505
+ }
1506
+ if (!event.request.ip || !trustProxy(event.request.ip, 0)) return protocol;
1507
+ const header = event.headers.get(HeaderName.X_FORWARDED_PROTO);
1508
+ if (!header) return protocol;
1509
+ const index = header.indexOf(",");
1510
+ const forwarded = index !== -1 ? header.substring(0, index).trim().toLowerCase() : header.trim().toLowerCase();
1511
+ if (forwarded === "http" || forwarded === "https") return forwarded;
1512
+ return protocol;
1513
+ }
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
1567
+ //#region src/plugin/error/constants.ts
1568
+ const PluginErrorCode = {
1569
+ PLUGIN: "PLUGIN",
1570
+ NOT_INSTALLED: "PLUGIN_NOT_INSTALLED",
1571
+ ALREADY_INSTALLED: "PLUGIN_ALREADY_INSTALLED",
1572
+ INSTALL: "PLUGIN_INSTALL"
1573
+ };
1574
+ //#endregion
1575
+ //#region src/plugin/error/is.ts
1576
+ const PLUGIN_ERROR_CODES = new Set(Object.values(PluginErrorCode));
1577
+ function isPluginError(input) {
1578
+ if (!isError(input)) return false;
1579
+ return PLUGIN_ERROR_CODES.has(input.code);
1580
+ }
1581
+ //#endregion
1582
+ //#region src/plugin/error/module.ts
1583
+ var PluginError = class extends AppError {
1584
+ constructor(input = {}) {
1585
+ const options = typeof input === "string" ? { message: input } : { ...input };
1586
+ if (!("code" in options) || !options.code) options.code = PluginErrorCode.PLUGIN;
1587
+ super(options);
1588
+ this.name = "PluginError";
1589
+ }
1590
+ };
1591
+ //#endregion
1592
+ //#region src/plugin/error/sub/already-installed.ts
1593
+ var PluginAlreadyInstalledError = class extends PluginError {
1594
+ pluginName;
1595
+ constructor(pluginName) {
1596
+ super({
1597
+ message: `Plugin "${pluginName}" is already installed on this router.`,
1598
+ code: PluginErrorCode.ALREADY_INSTALLED
1599
+ });
1600
+ this.name = "PluginAlreadyInstalledError";
1601
+ this.pluginName = pluginName;
1602
+ }
1603
+ };
1604
+ //#endregion
1605
+ //#region src/plugin/error/sub/install.ts
1606
+ var PluginInstallError = class extends PluginError {
1607
+ pluginName;
1608
+ constructor(pluginName, cause) {
1609
+ super({
1610
+ message: `Failed to install plugin "${pluginName}".`,
1611
+ code: PluginErrorCode.INSTALL,
1612
+ cause
1613
+ });
1614
+ this.name = "PluginInstallError";
1615
+ this.pluginName = pluginName;
1616
+ }
1617
+ };
1618
+ //#endregion
1619
+ //#region src/plugin/error/sub/not-installed.ts
1620
+ var PluginNotInstalledError = class extends PluginError {
1621
+ pluginName;
1622
+ helperName;
1623
+ constructor(pluginName, helperName) {
1624
+ super({
1625
+ message: `${helperName}() requires the "${pluginName}" plugin. Register it with: router.use(${pluginName}())`,
1626
+ code: PluginErrorCode.NOT_INSTALLED
1627
+ });
1628
+ this.name = "PluginNotInstalledError";
1629
+ this.pluginName = pluginName;
1630
+ this.helperName = helperName;
1631
+ }
1632
+ };
1633
+ //#endregion
1634
+ //#region src/plugin/is.ts
1635
+ function isPlugin(input) {
1636
+ if (!isObject(input)) return false;
1637
+ if (typeof input.name !== "undefined" && typeof input.name !== "string") return false;
1638
+ return typeof input.install === "function" && input.install.length === 1;
1639
+ }
1640
+ //#endregion
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);
2414
+ if (typeof input.timeout !== "undefined") {
2415
+ if (!Number.isFinite(input.timeout) || input.timeout <= 0) delete input.timeout;
2416
+ }
2417
+ if (typeof input.handlerTimeout !== "undefined") {
2418
+ if (!Number.isFinite(input.handlerTimeout) || input.handlerTimeout <= 0) delete input.handlerTimeout;
2419
+ }
2420
+ return {
2421
+ ...input,
2422
+ etag,
2423
+ trustProxy
2424
+ };
2425
+ }
2426
+ //#endregion
2427
+ //#region src/app/constants.ts
2428
+ const AppSymbol = Symbol.for("App");
2429
+ const AppPipelineStep = {
2430
+ START: 0,
2431
+ LOOKUP: 1,
2432
+ CHILD_BEFORE: 2,
2433
+ CHILD_DISPATCH: 3,
2434
+ CHILD_AFTER: 4,
2435
+ FINISH: 5
2436
+ };
2437
+ const RouteEntryType = {
2438
+ APP: "app",
2439
+ HANDLER: "handler"
2440
+ };
2441
+ //#endregion
2442
+ //#region src/app/check.ts
2443
+ function isAppInstance(input) {
2444
+ return hasInstanceof(input, AppSymbol);
2445
+ }
2446
+ //#endregion
2447
+ //#region src/app/module.ts
2448
+ /**
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.
2453
+ */
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
+ };
2465
+ }
2466
+ /**
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`).
2477
+ */
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 {
2484
+ /**
2485
+ * A label for the router instance.
2486
+ */
2487
+ name;
2488
+ /**
2489
+ * Registration-time path prefix for entries registered on this
2490
+ * App. Local to this instance — never inherited from a parent.
2491
+ *
2492
+ * @protected
2493
+ */
2494
+ _path;
2495
+ /**
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.
2500
+ *
2501
+ * @protected
2502
+ */
2503
+ router;
2504
+ /**
2505
+ * Lifecycle hook registry.
2506
+ *
2507
+ * @protected
2508
+ */
2509
+ hooks;
2510
+ /**
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.
2519
+ */
2520
+ _options;
2521
+ /**
2522
+ * Registry of installed plugins (name → version) on this router.
2523
+ *
2524
+ * @protected
2525
+ */
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 = [];
2539
+ constructor(input = {}) {
2540
+ this.name = input.name;
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;
2572
+ }
2573
+ /**
2574
+ * Public entry point — creates a DispatcherEvent from the request,
2575
+ * runs the pipeline, and returns a Response (with 404/500 fallbacks).
2576
+ */
2577
+ async fetch(request) {
2578
+ const event = new DispatcherEvent(request);
2579
+ let response;
2580
+ try {
2581
+ const timeoutMs = this._options.timeout;
2582
+ if (timeoutMs) {
2583
+ const controller = new AbortController();
2584
+ event.signal = controller.signal;
2585
+ let timerId;
2586
+ try {
2587
+ response = await Promise.race([this.dispatch(event), new Promise((_, reject) => {
2588
+ timerId = setTimeout(() => {
2589
+ controller.abort();
2590
+ reject(createError({
2591
+ status: 408,
2592
+ message: "Request Timeout"
2593
+ }));
2594
+ }, timeoutMs);
2595
+ })]);
2596
+ } finally {
2597
+ clearTimeout(timerId);
2598
+ }
2599
+ } else response = await this.dispatch(event);
2600
+ } catch (e) {
2601
+ event.error = createError(e);
2602
+ }
2603
+ if (response) return response;
2604
+ if (event.error) return this.buildFallbackResponse(request, event, event.error.status || 500, event.error.message);
2605
+ return this.buildFallbackResponse(request, event, 404, "Not Found");
2606
+ }
2607
+ buildFallbackResponse(request, event, status, message) {
2608
+ const headers = new Headers(event.response.headers);
2609
+ if (acceptsJson(request)) {
2610
+ headers.set("content-type", "application/json; charset=utf-8");
2611
+ return new Response(JSON.stringify({
2612
+ status,
2613
+ message
2614
+ }), {
2615
+ status,
2616
+ headers
2617
+ });
2618
+ }
2619
+ headers.set("content-type", "text/plain; charset=utf-8");
2620
+ return new Response(message, {
2621
+ status,
2622
+ headers
2623
+ });
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
+ }
2662
+ async executePipelineStep(context) {
2663
+ while (context.step !== AppPipelineStep.FINISH) switch (context.step) {
2664
+ case AppPipelineStep.START:
2665
+ await this.executePipelineStepStart(context);
2666
+ break;
2667
+ case AppPipelineStep.LOOKUP:
2668
+ await this.executePipelineStepLookup(context);
2669
+ break;
2670
+ case AppPipelineStep.CHILD_BEFORE:
2671
+ await this.executePipelineStepChildBefore(context);
2672
+ break;
2673
+ case AppPipelineStep.CHILD_DISPATCH:
2674
+ await this.executePipelineStepChildDispatch(context);
2675
+ break;
2676
+ case AppPipelineStep.CHILD_AFTER:
2677
+ await this.executePipelineStepChildAfter(context);
2678
+ break;
2679
+ default:
2680
+ context.step = AppPipelineStep.FINISH;
2681
+ break;
2682
+ }
2683
+ await this.executePipelineStepFinish(context);
2684
+ }
2685
+ async executePipelineStepStart(context) {
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;
2689
+ }
2690
+ async executePipelineStepLookup(context) {
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;
2700
+ if (context.event.error && handler.type === HandlerType.CORE || !context.event.error && handler.type === HandlerType.ERROR) {
2701
+ context.matchIndex++;
2702
+ continue;
2703
+ }
2704
+ const { method } = route;
2705
+ if (method) context.event.methodsAllowed.add(method);
2706
+ if (!matchHandlerMethod(method, context.event.method)) {
2707
+ context.matchIndex++;
2708
+ continue;
2709
+ }
2710
+ }
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;
2714
+ return;
2715
+ }
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;
2724
+ }
2725
+ context.step = AppPipelineStep.FINISH;
2726
+ }
2727
+ async executePipelineStepChildBefore(context) {
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;
2740
+ }
2741
+ async executePipelineStepChildAfter(context) {
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;
2745
+ }
2746
+ async executePipelineStepChildDispatch(context) {
2747
+ const match = context.matches?.[context.matchIndex];
2748
+ if (context.event.dispatched || typeof match === "undefined") {
2749
+ context.step = AppPipelineStep.FINISH;
2750
+ return;
2751
+ }
2752
+ const { route } = match;
2753
+ const { event } = context;
2754
+ const savedPath = event.path;
2755
+ const savedMountPath = event.mountPath;
2756
+ const savedParams = event.params;
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);
2763
+ try {
2764
+ const parentMatches = context.matches;
2765
+ const parentMatchesPath = context.matchesPath;
2766
+ const nextMatchIndex = context.matchIndex + 1;
2767
+ event.setNext(async (error) => {
2768
+ if (error) event.error = createError(error);
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;
2787
+ });
2788
+ const response = await route.data.data.dispatch(event);
2789
+ if (response) {
2790
+ context.response = response;
2791
+ event.dispatched = true;
2792
+ }
2793
+ } catch (e) {
2794
+ event.error = createError(e);
2795
+ if (this.hooks.hasListeners(HookName.ERROR)) await this.hooks.trigger(HookName.ERROR, event);
2796
+ }
2797
+ if (!event.dispatched) {
2798
+ event.path = savedPath;
2799
+ event.mountPath = savedMountPath;
2800
+ event.params = savedParams;
2801
+ }
2802
+ context.matchIndex++;
2803
+ context.step = AppPipelineStep.CHILD_AFTER;
2804
+ }
2805
+ async executePipelineStepFinish(context) {
2806
+ if (!context.event.error && !context.event.dispatched && context.isRoot && context.event.method === MethodName.OPTIONS) {
2807
+ if (context.event.methodsAllowed.has(MethodName.GET)) context.event.methodsAllowed.add(MethodName.HEAD);
2808
+ const options = [...context.event.methodsAllowed].map((key) => key.toUpperCase()).join(",");
2809
+ const optionsHeaders = new Headers(context.event.response.headers);
2810
+ optionsHeaders.set(HeaderName.ALLOW, options);
2811
+ context.response = new Response(options, {
2812
+ status: context.event.response.status || 200,
2813
+ headers: optionsHeaders
2814
+ });
2815
+ context.event.dispatched = true;
2816
+ }
2817
+ }
2818
+ async dispatch(event) {
2819
+ const savedPath = event.path;
2820
+ const savedMountPath = event.mountPath;
2821
+ const savedParams = event.params;
2822
+ const savedAppOptions = event.appOptions;
2823
+ const wasDispatching = event.isDispatching;
2824
+ const isRoot = !wasDispatching;
2825
+ const context = {
2826
+ step: AppPipelineStep.START,
2827
+ event,
2828
+ isRoot,
2829
+ matchIndex: 0
2830
+ };
2831
+ event.appOptions = this._options;
2832
+ event.isDispatching = true;
2833
+ try {
2834
+ await this.executePipelineStep(context);
2835
+ if (this.hooks.hasListeners(HookName.END)) await this.hooks.trigger(HookName.END, event);
2836
+ } finally {
2837
+ event.appOptions = savedAppOptions;
2838
+ event.isDispatching = wasDispatching;
2839
+ if (!event.dispatched) {
2840
+ event.path = savedPath;
2841
+ event.mountPath = savedMountPath;
2842
+ event.params = savedParams;
2843
+ }
2844
+ }
2845
+ return context.response;
2846
+ }
2847
+ delete(...input) {
2848
+ this.useForMethod(MethodName.DELETE, ...input);
2849
+ return this;
2850
+ }
2851
+ get(...input) {
2852
+ this.useForMethod(MethodName.GET, ...input);
2853
+ return this;
2854
+ }
2855
+ post(...input) {
2856
+ this.useForMethod(MethodName.POST, ...input);
2857
+ return this;
2858
+ }
2859
+ put(...input) {
2860
+ this.useForMethod(MethodName.PUT, ...input);
2861
+ return this;
2862
+ }
2863
+ patch(...input) {
2864
+ this.useForMethod(MethodName.PATCH, ...input);
2865
+ return this;
2866
+ }
2867
+ head(...input) {
2868
+ this.useForMethod(MethodName.HEAD, ...input);
2869
+ return this;
2870
+ }
2871
+ options(...input) {
2872
+ this.useForMethod(MethodName.OPTIONS, ...input);
2873
+ return this;
2874
+ }
2875
+ useForMethod(method, ...input) {
2876
+ let path;
2877
+ for (const element of input) {
2878
+ if (isPath(element)) {
2879
+ path = element;
2880
+ continue;
2881
+ }
2882
+ let handler;
2883
+ if (isHandler(element)) handler = element;
2884
+ else if (isHandlerOptions(element)) handler = new Handler({
2885
+ ...element,
2886
+ method
2887
+ });
2888
+ else continue;
2889
+ this.register({
2890
+ path: joinPaths(this._path, path, handler.path),
2891
+ method,
2892
+ data: {
2893
+ type: RouteEntryType.HANDLER,
2894
+ data: handler
2895
+ }
2896
+ });
2897
+ }
2898
+ }
2899
+ use(...input) {
2900
+ let path;
2901
+ for (const item of input) {
2902
+ if (isPath(item)) {
2903
+ path = withLeadingSlash(item);
2904
+ continue;
2905
+ }
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
+ }
2914
+ });
2915
+ continue;
2916
+ }
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
+ }
2925
+ });
2926
+ continue;
2927
+ }
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
+ }
2937
+ });
2938
+ continue;
2939
+ }
2940
+ if (isPlugin(item)) if (path) this.install(item, { path });
2941
+ else this.install(item);
2942
+ }
2943
+ return this;
2944
+ }
2945
+ /**
2946
+ * Check if a plugin with the given name is installed on this router.
2947
+ */
2948
+ hasPlugin(name) {
2949
+ return this.plugins.has(name);
2950
+ }
2951
+ /**
2952
+ * Get the version of an installed plugin by name on this router,
2953
+ * or `undefined` if the plugin is not installed here.
2954
+ */
2955
+ getPluginVersion(name) {
2956
+ return this.plugins.get(name);
2957
+ }
2958
+ install(plugin, context = {}) {
2959
+ if (this.plugins.has(plugin.name)) throw new PluginAlreadyInstalledError(plugin.name);
2960
+ const router = new App({
2961
+ name: plugin.name,
2962
+ router: this.router.clone()
2963
+ });
2964
+ plugin.install(router);
2965
+ if (context.path) this.use(context.path, router);
2966
+ else this.use(router);
2967
+ this.plugins.set(plugin.name, plugin.version);
2968
+ return this;
2969
+ }
2970
+ /**
2971
+ * Return a new `App` that mirrors this one but owns independent
2972
+ * mountable state.
2973
+ *
2974
+ * The new router has:
2975
+ * - a fresh `stack` array of shallow-copied entries (handlers and child
2976
+ * routers are shared by reference; only the wrapping entries are new)
2977
+ * - the same `pathMatcher` reference (it is stateless)
2978
+ * - a fresh `Hooks` instance seeded with the current listeners
2979
+ * - a shallow copy of `_options`
2980
+ * - a fresh `plugins` map with the same entries
2981
+ *
2982
+ * Use this when the same logical router needs to be mounted under
2983
+ * multiple paths — each mount can receive its own clone so subsequent
2984
+ * mutations on one mount do not bleed into the others.
2985
+ */
2986
+ clone() {
2987
+ const next = new App({
2988
+ name: this.name,
2989
+ path: this._path,
2990
+ options: { ...this._options },
2991
+ hooks: this.hooks.clone(),
2992
+ plugins: this.plugins,
2993
+ router: this.router.clone()
2994
+ });
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
+ });
3004
+ continue;
3005
+ }
3006
+ next.register({
3007
+ path: route.path,
3008
+ method: route.method,
3009
+ data: {
3010
+ type: RouteEntryType.HANDLER,
3011
+ data: route.data.data
3012
+ }
3013
+ });
3014
+ }
3015
+ return next;
3016
+ }
3017
+ on(name, fn, priority) {
3018
+ return this.hooks.addListener(name, fn, priority);
3019
+ }
3020
+ off(name, fn) {
3021
+ if (typeof fn === "undefined") {
3022
+ this.hooks.removeListener(name);
3023
+ return this;
3024
+ }
3025
+ this.hooks.removeListener(name, fn);
3026
+ return this;
3027
+ }
3028
+ };
3029
+ //#endregion
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 };
3031
+
3032
+ //# sourceMappingURL=src-gmPicCWT.mjs.map