routup 5.0.0-beta.2 → 5.0.0-beta.4

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.
@@ -1,57 +1,137 @@
1
+ import { FastURL } from "srvx";
1
2
  import { HTTPError, isHTTPError } from "@ebec/http";
2
3
  import { Buffer } from "node:buffer";
3
4
  import { subtle } from "uncrypto";
4
- import { distinctArray, hasOwnProperty, merge } from "smob";
5
+ import { merge } from "smob";
5
6
  import { compile } from "proxy-addr";
6
7
  import { get, getType } from "mime-explorer";
7
- import { FastURL } from "srvx";
8
- import { pathToRegexp } from "path-to-regexp";
9
8
  import Negotiator from "negotiator";
9
+ import { pathToRegexp } from "path-to-regexp";
10
10
  //#region src/constants.ts
11
- let MethodName = /* @__PURE__ */ function(MethodName) {
12
- MethodName["GET"] = "GET";
13
- MethodName["POST"] = "POST";
14
- MethodName["PUT"] = "PUT";
15
- MethodName["PATCH"] = "PATCH";
16
- MethodName["DELETE"] = "DELETE";
17
- MethodName["OPTIONS"] = "OPTIONS";
18
- MethodName["HEAD"] = "HEAD";
19
- return MethodName;
20
- }({});
21
- let HeaderName = /* @__PURE__ */ function(HeaderName) {
22
- HeaderName["ACCEPT"] = "accept";
23
- HeaderName["ACCEPT_CHARSET"] = "accept-charset";
24
- HeaderName["ACCEPT_ENCODING"] = "accept-encoding";
25
- HeaderName["ACCEPT_LANGUAGE"] = "accept-language";
26
- HeaderName["ACCEPT_RANGES"] = "accept-ranges";
27
- HeaderName["ALLOW"] = "allow";
28
- HeaderName["CACHE_CONTROL"] = "cache-control";
29
- HeaderName["CONTENT_DISPOSITION"] = "content-disposition";
30
- HeaderName["CONTENT_ENCODING"] = "content-encoding";
31
- HeaderName["CONTENT_LENGTH"] = "content-length";
32
- HeaderName["CONTENT_RANGE"] = "content-range";
33
- HeaderName["CONTENT_TYPE"] = "content-type";
34
- HeaderName["CONNECTION"] = "connection";
35
- HeaderName["COOKIE"] = "cookie";
36
- HeaderName["ETag"] = "etag";
37
- HeaderName["HOST"] = "host";
38
- HeaderName["IF_MODIFIED_SINCE"] = "if-modified-since";
39
- HeaderName["IF_NONE_MATCH"] = "if-none-match";
40
- HeaderName["LAST_MODIFIED"] = "last-modified";
41
- HeaderName["LOCATION"] = "location";
42
- HeaderName["RANGE"] = "range";
43
- HeaderName["RATE_LIMIT_LIMIT"] = "ratelimit-limit";
44
- HeaderName["RATE_LIMIT_REMAINING"] = "ratelimit-remaining";
45
- HeaderName["RATE_LIMIT_RESET"] = "ratelimit-reset";
46
- HeaderName["RETRY_AFTER"] = "retry-after";
47
- HeaderName["SET_COOKIE"] = "set-cookie";
48
- HeaderName["TRANSFER_ENCODING"] = "transfer-encoding";
49
- HeaderName["X_ACCEL_BUFFERING"] = "x-accel-buffering";
50
- HeaderName["X_FORWARDED_HOST"] = "x-forwarded-host";
51
- HeaderName["X_FORWARDED_FOR"] = "x-forwarded-for";
52
- HeaderName["X_FORWARDED_PROTO"] = "x-forwarded-proto";
53
- return HeaderName;
54
- }({});
11
+ const MethodName = {
12
+ GET: "GET",
13
+ POST: "POST",
14
+ PUT: "PUT",
15
+ PATCH: "PATCH",
16
+ DELETE: "DELETE",
17
+ OPTIONS: "OPTIONS",
18
+ HEAD: "HEAD"
19
+ };
20
+ const HeaderName = {
21
+ ACCEPT: "accept",
22
+ ACCEPT_CHARSET: "accept-charset",
23
+ ACCEPT_ENCODING: "accept-encoding",
24
+ ACCEPT_LANGUAGE: "accept-language",
25
+ ACCEPT_RANGES: "accept-ranges",
26
+ ALLOW: "allow",
27
+ CACHE_CONTROL: "cache-control",
28
+ CONTENT_DISPOSITION: "content-disposition",
29
+ CONTENT_ENCODING: "content-encoding",
30
+ CONTENT_LENGTH: "content-length",
31
+ CONTENT_RANGE: "content-range",
32
+ CONTENT_TYPE: "content-type",
33
+ CONNECTION: "connection",
34
+ COOKIE: "cookie",
35
+ ETag: "etag",
36
+ HOST: "host",
37
+ IF_MODIFIED_SINCE: "if-modified-since",
38
+ IF_NONE_MATCH: "if-none-match",
39
+ LAST_MODIFIED: "last-modified",
40
+ LOCATION: "location",
41
+ RANGE: "range",
42
+ RATE_LIMIT_LIMIT: "ratelimit-limit",
43
+ RATE_LIMIT_REMAINING: "ratelimit-remaining",
44
+ RATE_LIMIT_RESET: "ratelimit-reset",
45
+ RETRY_AFTER: "retry-after",
46
+ SET_COOKIE: "set-cookie",
47
+ TRANSFER_ENCODING: "transfer-encoding",
48
+ X_ACCEL_BUFFERING: "x-accel-buffering",
49
+ X_FORWARDED_HOST: "x-forwarded-host",
50
+ X_FORWARDED_FOR: "x-forwarded-for",
51
+ X_FORWARDED_PROTO: "x-forwarded-proto"
52
+ };
53
+ //#endregion
54
+ //#region src/response/helpers/cache.ts
55
+ function setResponseCacheHeaders(event, options) {
56
+ options = options || {};
57
+ const cacheControls = ["public"].concat(options.cacheControls || []);
58
+ if (options.maxAge !== void 0) cacheControls.push(`max-age=${+options.maxAge}`, `s-maxage=${+options.maxAge}`);
59
+ if (options.modifiedTime) {
60
+ const modifiedTime = typeof options.modifiedTime === "string" ? new Date(options.modifiedTime) : options.modifiedTime;
61
+ event.response.headers.set("last-modified", modifiedTime.toUTCString());
62
+ }
63
+ event.response.headers.set("cache-control", cacheControls.join(", "));
64
+ }
65
+ //#endregion
66
+ //#region src/error/module.ts
67
+ const ErrorSymbol = Symbol.for("RoutupError");
68
+ var RoutupError = class extends HTTPError {
69
+ "@instanceof" = ErrorSymbol;
70
+ constructor(input = {}) {
71
+ super(input);
72
+ this.name = "RoutupError";
73
+ }
74
+ };
75
+ //#endregion
76
+ //#region src/response/helpers/event-stream/utils.ts
77
+ function stripNewlines(value) {
78
+ return value.replace(/[\r\n]/g, "");
79
+ }
80
+ function serializeEventStreamMessage(message) {
81
+ let result = "";
82
+ if (message.id) result += `id: ${stripNewlines(message.id)}\n`;
83
+ if (message.event) result += `event: ${stripNewlines(message.event)}\n`;
84
+ if (typeof message.retry === "number" && Number.isInteger(message.retry)) result += `retry: ${message.retry}\n`;
85
+ const lines = message.data.replace(/\r/g, "").split("\n");
86
+ for (const line of lines) result += `data: ${line}\n`;
87
+ result += "\n";
88
+ return result;
89
+ }
90
+ //#endregion
91
+ //#region src/response/helpers/event-stream/module.ts
92
+ function createEventStream(event, options) {
93
+ if (options?.maxMessageSize !== void 0) {
94
+ if (!Number.isInteger(options.maxMessageSize) || options.maxMessageSize < 0) throw new RoutupError("maxMessageSize must be a non-negative integer.");
95
+ }
96
+ let controller;
97
+ let closed = false;
98
+ const encoder = new TextEncoder();
99
+ const stream = new ReadableStream({
100
+ start(ctrl) {
101
+ controller = ctrl;
102
+ },
103
+ cancel() {
104
+ closed = true;
105
+ }
106
+ });
107
+ const headers = new Headers(event.response.headers);
108
+ headers.set(HeaderName.CONTENT_TYPE, "text/event-stream");
109
+ headers.set(HeaderName.CACHE_CONTROL, "private, no-cache, no-store, no-transform, must-revalidate, max-age=0");
110
+ headers.set(HeaderName.X_ACCEL_BUFFERING, "no");
111
+ headers.set(HeaderName.CONNECTION, "keep-alive");
112
+ const handle = {
113
+ write(message) {
114
+ if (closed) return false;
115
+ if (typeof message === "string") return handle.write({ data: message });
116
+ const serialized = serializeEventStreamMessage(message);
117
+ if (options?.maxMessageSize !== void 0) {
118
+ if (encoder.encode(serialized).byteLength > options.maxMessageSize) return false;
119
+ }
120
+ controller.enqueue(encoder.encode(serialized));
121
+ return true;
122
+ },
123
+ end() {
124
+ if (closed) return;
125
+ closed = true;
126
+ controller.close();
127
+ },
128
+ response: new Response(stream, {
129
+ status: event.response.status,
130
+ headers
131
+ })
132
+ };
133
+ return handle;
134
+ }
55
135
  //#endregion
56
136
  //#region src/utils/header.ts
57
137
  function sanitizeHeaderValue(value) {
@@ -193,20 +273,77 @@ function cleanDoubleSlashes(input = "") {
193
273
  return input.replace(/\/+/g, "/");
194
274
  }
195
275
  //#endregion
276
+ //#region src/response/helpers/header.ts
277
+ function appendResponseHeader(event, name, value) {
278
+ const { headers } = event.response;
279
+ if (Array.isArray(value)) for (const v of value) headers.append(name, sanitizeHeaderValue(v));
280
+ else headers.append(name, sanitizeHeaderValue(value));
281
+ }
282
+ function appendResponseHeaderDirective(event, name, value) {
283
+ const { headers } = event.response;
284
+ const existing = headers.get(name);
285
+ if (!existing) {
286
+ if (Array.isArray(value)) headers.set(name, sanitizeHeaderValue(value.join("; ")));
287
+ else headers.set(name, sanitizeHeaderValue(value));
288
+ return;
289
+ }
290
+ const directives = existing.split("; ");
291
+ if (Array.isArray(value)) directives.push(...value);
292
+ else directives.push(value);
293
+ const unique = [...new Set(directives)];
294
+ headers.set(name, sanitizeHeaderValue(unique.join("; ")));
295
+ }
296
+ //#endregion
297
+ //#region src/response/helpers/utils.ts
298
+ function setResponseContentTypeByFileName(event, fileName) {
299
+ const ext = extname(fileName);
300
+ if (ext) {
301
+ let type = getMimeType(ext.substring(1));
302
+ if (type) {
303
+ const charset = getCharsetForMimeType(type);
304
+ if (charset) type += `; charset=${charset}`;
305
+ event.response.headers.set(HeaderName.CONTENT_TYPE, type);
306
+ }
307
+ }
308
+ }
309
+ //#endregion
310
+ //#region src/response/helpers/header-attachment.ts
311
+ function sanitizeFilename(filename) {
312
+ return filename.replace(/[\r\n]/g, "");
313
+ }
314
+ function toAsciiFilename(filename) {
315
+ return filename.replace(/[^\x20-\x7E]/g, "").replace(/"/g, "\\\"");
316
+ }
317
+ function encodeRfc5987(filename) {
318
+ return encodeURIComponent(filename).replace(/['()]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`).replace(/\*/g, "%2A");
319
+ }
320
+ function setResponseHeaderAttachment(event, filename) {
321
+ if (typeof filename === "string") setResponseContentTypeByFileName(event, filename);
322
+ let disposition = "attachment";
323
+ if (filename) {
324
+ const sanitized = sanitizeFilename(filename);
325
+ const ascii = toAsciiFilename(sanitized);
326
+ disposition += `; filename="${ascii}"`;
327
+ disposition += `; filename*=UTF-8''${encodeRfc5987(sanitized)}`;
328
+ }
329
+ event.response.headers.set(HeaderName.CONTENT_DISPOSITION, disposition);
330
+ }
331
+ //#endregion
332
+ //#region src/response/helpers/header-content-type.ts
333
+ function setResponseHeaderContentType(event, input, ifNotExists) {
334
+ if (ifNotExists) {
335
+ if (event.response.headers.get(HeaderName.CONTENT_TYPE)) return;
336
+ }
337
+ const contentType = getMimeType(input);
338
+ if (contentType) event.response.headers.set(HeaderName.CONTENT_TYPE, contentType);
339
+ }
340
+ //#endregion
196
341
  //#region src/error/is.ts
197
342
  function isError(input) {
198
343
  if (!isHTTPError(input)) return false;
199
- return input.name === "RoutupError";
344
+ return isInstance(input, ErrorSymbol);
200
345
  }
201
346
  //#endregion
202
- //#region src/error/module.ts
203
- var RoutupError = class extends HTTPError {
204
- constructor(input = {}) {
205
- super(input);
206
- this.name = "RoutupError";
207
- }
208
- };
209
- //#endregion
210
347
  //#region src/error/create.ts
211
348
  function isNativeError(input) {
212
349
  return isObject(input) && typeof input.message === "string" && typeof input.name === "string";
@@ -216,7 +353,7 @@ function isNativeError(input) {
216
353
  * - an existing RoutupError (returned as-is)
217
354
  * - an HTTPError (wrapped into a RoutupError preserving status)
218
355
  * - an Error (wrapped preserving message and cause)
219
- * - an options object (statusCode, statusMessage, etc.)
356
+ * - an options object (status, message, etc.)
220
357
  * - a message string
221
358
  *
222
359
  * @param input
@@ -227,8 +364,7 @@ function createError(input) {
227
364
  if (isHTTPError(input)) return new RoutupError({
228
365
  message: input.message,
229
366
  code: input.code,
230
- statusCode: input.statusCode,
231
- statusMessage: input.statusMessage,
367
+ status: input.status,
232
368
  redirectURL: input.redirectURL,
233
369
  cause: input
234
370
  });
@@ -242,440 +378,72 @@ function createError(input) {
242
378
  return new RoutupError(options);
243
379
  }
244
380
  //#endregion
245
- //#region src/event/module.ts
246
- var RoutupEvent = class {
247
- request;
248
- params;
249
- path;
250
- method;
251
- mountPath;
252
- error;
253
- routerPath;
254
- /**
255
- * Collected allowed methods (for OPTIONS).
256
- */
257
- methodsAllowed;
258
- store;
259
- _dispatched;
260
- _response;
261
- /**
262
- * Cached parsed URL (avoids double-parsing).
263
- */
264
- _url;
265
- _searchParams;
266
- /**
267
- * Continuation function for middleware onion model.
268
- */
269
- _next;
270
- /**
271
- * Whether _next has already been called (guard against double-invocation).
272
- */
273
- _nextCalled;
274
- /**
275
- * The cached result of the next handler.
276
- */
277
- _nextResult;
278
- constructor(request) {
279
- this.request = request;
280
- this._url = new FastURL(request.url);
281
- this.method = request.method;
282
- this.path = this._url.pathname;
283
- this.mountPath = "/";
284
- this.params = {};
285
- this.routerPath = [];
286
- this.methodsAllowed = [];
287
- this.store = Object.create(null);
288
- this._dispatched = false;
289
- this._nextCalled = false;
290
- }
291
- get headers() {
292
- return this.request.headers;
381
+ //#region src/response/to-response.ts
382
+ function stripWeakPrefix(etag) {
383
+ return etag.startsWith("W/") ? etag.slice(2) : etag;
384
+ }
385
+ async function applyEtag(body, event, headers) {
386
+ const etagFn = event.routerOptions.etag;
387
+ if (!etagFn) return void 0;
388
+ const etag = await etagFn(body);
389
+ if (!etag) return void 0;
390
+ headers.set("etag", etag);
391
+ const ifNoneMatch = event.headers.get("if-none-match");
392
+ if (ifNoneMatch && (ifNoneMatch === "*" || ifNoneMatch.split(",").some((t) => stripWeakPrefix(t.trim()) === stripWeakPrefix(etag)))) return new Response(null, {
393
+ status: 304,
394
+ headers
395
+ });
396
+ }
397
+ async function toResponse(value, event) {
398
+ if (value === void 0) return;
399
+ if (value === null) return new Response(null, {
400
+ status: event.response.status,
401
+ headers: event.response.headers
402
+ });
403
+ if (value instanceof Response) return value;
404
+ const { status, headers } = event.response;
405
+ if (typeof value === "string") {
406
+ if (!headers.has("content-type")) headers.set("content-type", "text/plain; charset=utf-8");
407
+ const cached = await applyEtag(value, event, headers);
408
+ if (cached) return cached;
409
+ return new Response(value, {
410
+ status,
411
+ headers
412
+ });
293
413
  }
294
- get searchParams() {
295
- if (!this._searchParams) this._searchParams = new URLSearchParams(this._url.search);
296
- return this._searchParams;
414
+ if (value instanceof ArrayBuffer || value instanceof Uint8Array) {
415
+ if (!headers.has("content-type")) headers.set("content-type", "application/octet-stream");
416
+ return new Response(value, {
417
+ status,
418
+ headers
419
+ });
297
420
  }
298
- get response() {
299
- if (!this._response) this._response = {
300
- status: 200,
301
- headers: new Headers()
302
- };
303
- return this._response;
421
+ if (value instanceof ReadableStream) return new Response(value, {
422
+ status,
423
+ headers
424
+ });
425
+ if (value instanceof Blob) {
426
+ if (!headers.has("content-type")) headers.set("content-type", value.type || "application/octet-stream");
427
+ return new Response(value, {
428
+ status,
429
+ headers
430
+ });
304
431
  }
305
- get dispatched() {
306
- return this._dispatched;
432
+ if (!headers.has("content-type")) headers.set("content-type", "application/json; charset=utf-8");
433
+ let json;
434
+ try {
435
+ json = JSON.stringify(value);
436
+ } catch (e) {
437
+ throw createError({
438
+ message: "JSON serialization failed",
439
+ status: 500,
440
+ cause: e
441
+ });
307
442
  }
308
- set dispatched(value) {
309
- this._dispatched = value;
310
- }
311
- async next() {
312
- if (this._nextCalled) return this._nextResult;
313
- this._nextCalled = true;
314
- if (this._next) this._nextResult = this._next();
315
- return this._nextResult;
316
- }
317
- };
318
- //#endregion
319
- //#region src/handler/constants.ts
320
- let HandlerType = /* @__PURE__ */ function(HandlerType) {
321
- HandlerType["CORE"] = "core";
322
- HandlerType["ERROR"] = "error";
323
- return HandlerType;
324
- }({});
325
- const HandlerSymbol = /* @__PURE__ */ Symbol.for("Handler");
326
- //#endregion
327
- //#region src/hook/constants.ts
328
- let HookName = /* @__PURE__ */ function(HookName) {
329
- HookName["REQUEST"] = "request";
330
- HookName["RESPONSE"] = "response";
331
- HookName["ERROR"] = "error";
332
- HookName["CHILD_MATCH"] = "childMatch";
333
- HookName["CHILD_DISPATCH_BEFORE"] = "childDispatchBefore";
334
- HookName["CHILD_DISPATCH_AFTER"] = "childDispatchAfter";
335
- return HookName;
336
- }({});
337
- //#endregion
338
- //#region src/hook/module.ts
339
- var HookManager = class {
340
- items;
341
- constructor() {
342
- this.items = {};
343
- }
344
- addListener(name, fn) {
345
- this.items[name] = this.items[name] || [];
346
- this.items[name].push(fn);
347
- return () => {
348
- this.removeListener(name, fn);
349
- };
350
- }
351
- removeListener(name, fn) {
352
- if (!this.items[name]) return;
353
- if (typeof fn === "undefined") {
354
- delete this.items[name];
355
- return;
356
- }
357
- if (typeof fn === "function") {
358
- const index = this.items[name].indexOf(fn);
359
- if (index !== -1) this.items[name].splice(index, 1);
360
- }
361
- if (this.items[name].length === 0) delete this.items[name];
362
- }
363
- async trigger(name, event) {
364
- if (!this.items[name] || this.items[name].length === 0) return;
365
- try {
366
- for (let i = 0; i < this.items[name].length; i++) {
367
- const listener = this.items[name][i];
368
- await this.triggerListener(name, event, listener);
369
- if (event.dispatched) {
370
- if (event.error) event.error = void 0;
371
- return;
372
- }
373
- }
374
- } catch (e) {
375
- event.error = e;
376
- if (!this.isErrorListenerHook(name)) {
377
- await this.trigger(HookName.ERROR, event);
378
- if (event.dispatched) {
379
- if (event.error) event.error = void 0;
380
- }
381
- }
382
- }
383
- }
384
- triggerListener(name, event, listener) {
385
- if (this.isErrorListenerHook(name)) {
386
- if (event.error) return listener(event);
387
- return;
388
- }
389
- return listener(event);
390
- }
391
- isErrorListenerHook(input) {
392
- return input === HookName.ERROR;
393
- }
394
- };
395
- //#endregion
396
- //#region src/path/matcher.ts
397
- function decodeParam(val) {
398
- /* istanbul ignore next */
399
- if (typeof val !== "string" || val.length === 0) return val;
400
- try {
401
- return decodeURIComponent(val);
402
- } catch {
403
- return val;
404
- }
405
- }
406
- var PathMatcher = class {
407
- path;
408
- regexp;
409
- regexpKeys = [];
410
- regexpOptions;
411
- constructor(path, options) {
412
- this.path = path;
413
- this.regexpOptions = options || {};
414
- const regexp = pathToRegexp(path, options);
415
- this.regexp = regexp.regexp;
416
- this.regexpKeys = regexp.keys;
417
- }
418
- test(path) {
419
- return this.regexp.test(path);
420
- }
421
- exec(path) {
422
- if (this.path === "/" && this.regexpOptions.end === false) return {
423
- path: "/",
424
- params: Object.create(null)
425
- };
426
- const match = this.regexp.exec(path);
427
- if (!match) return;
428
- const params = Object.create(null);
429
- for (let i = 1; i < match.length; i++) {
430
- const key = this.regexpKeys[i - 1];
431
- if (!key) continue;
432
- const prop = key.name;
433
- const val = decodeParam(match[i]);
434
- if (typeof val !== "undefined") params[prop] = val;
435
- }
436
- return {
437
- path: match[0],
438
- params
439
- };
440
- }
441
- };
442
- //#endregion
443
- //#region src/path/utils.ts
444
- function isPath(input) {
445
- return typeof input === "string";
446
- }
447
- //#endregion
448
- //#region src/response/helpers/cache.ts
449
- function setResponseCacheHeaders(event, options) {
450
- options = options || {};
451
- const cacheControls = ["public"].concat(options.cacheControls || []);
452
- if (options.maxAge !== void 0) cacheControls.push(`max-age=${+options.maxAge}`, `s-maxage=${+options.maxAge}`);
453
- if (options.modifiedTime) {
454
- const modifiedTime = typeof options.modifiedTime === "string" ? new Date(options.modifiedTime) : options.modifiedTime;
455
- event.response.headers.set("last-modified", modifiedTime.toUTCString());
456
- }
457
- event.response.headers.set("cache-control", cacheControls.join(", "));
458
- }
459
- //#endregion
460
- //#region src/response/helpers/event-stream/utils.ts
461
- function stripNewlines(value) {
462
- return value.replace(/[\r\n]/g, "");
463
- }
464
- function serializeEventStreamMessage(message) {
465
- let result = "";
466
- if (message.id) result += `id: ${stripNewlines(message.id)}\n`;
467
- if (message.event) result += `event: ${stripNewlines(message.event)}\n`;
468
- if (typeof message.retry === "number" && Number.isInteger(message.retry)) result += `retry: ${message.retry}\n`;
469
- const lines = message.data.replace(/\r/g, "").split("\n");
470
- for (const line of lines) result += `data: ${line}\n`;
471
- result += "\n";
472
- return result;
473
- }
474
- //#endregion
475
- //#region src/response/helpers/event-stream/module.ts
476
- function createEventStream(event, options) {
477
- if (options?.maxMessageSize !== void 0) {
478
- if (!Number.isInteger(options.maxMessageSize) || options.maxMessageSize < 0) throw new RoutupError("maxMessageSize must be a non-negative integer.");
479
- }
480
- let controller;
481
- let closed = false;
482
- const encoder = new TextEncoder();
483
- const stream = new ReadableStream({
484
- start(ctrl) {
485
- controller = ctrl;
486
- },
487
- cancel() {
488
- closed = true;
489
- }
490
- });
491
- const headers = new Headers(event.response.headers);
492
- headers.set(HeaderName.CONTENT_TYPE, "text/event-stream");
493
- headers.set(HeaderName.CACHE_CONTROL, "private, no-cache, no-store, no-transform, must-revalidate, max-age=0");
494
- headers.set(HeaderName.X_ACCEL_BUFFERING, "no");
495
- headers.set(HeaderName.CONNECTION, "keep-alive");
496
- const handle = {
497
- write(message) {
498
- if (closed) return;
499
- if (typeof message === "string") {
500
- handle.write({ data: message });
501
- return;
502
- }
503
- const serialized = serializeEventStreamMessage(message);
504
- if (options?.maxMessageSize !== void 0) {
505
- if (encoder.encode(serialized).byteLength > options.maxMessageSize) return;
506
- }
507
- controller.enqueue(encoder.encode(serialized));
508
- },
509
- end() {
510
- if (closed) return;
511
- closed = true;
512
- controller.close();
513
- },
514
- response: new Response(stream, {
515
- status: event.response.status,
516
- statusText: event.response.statusText,
517
- headers
518
- })
519
- };
520
- return handle;
521
- }
522
- //#endregion
523
- //#region src/response/helpers/gone.ts
524
- function isResponseGone(event) {
525
- return event.dispatched;
526
- }
527
- function setResponseGone(event) {
528
- event.dispatched = true;
529
- }
530
- //#endregion
531
- //#region src/response/helpers/header.ts
532
- function appendResponseHeader(event, name, value) {
533
- const { headers } = event.response;
534
- if (Array.isArray(value)) for (const v of value) headers.append(name, sanitizeHeaderValue(v));
535
- else headers.append(name, sanitizeHeaderValue(value));
536
- }
537
- function appendResponseHeaderDirective(event, name, value) {
538
- const { headers } = event.response;
539
- const existing = headers.get(name);
540
- if (!existing) {
541
- if (Array.isArray(value)) headers.set(name, sanitizeHeaderValue(value.join("; ")));
542
- else headers.set(name, sanitizeHeaderValue(value));
543
- return;
544
- }
545
- const directives = existing.split("; ");
546
- if (Array.isArray(value)) directives.push(...value);
547
- else directives.push(value);
548
- const unique = [...new Set(directives)];
549
- headers.set(name, sanitizeHeaderValue(unique.join("; ")));
550
- }
551
- //#endregion
552
- //#region src/response/helpers/utils.ts
553
- function setResponseContentTypeByFileName(event, fileName) {
554
- const ext = extname(fileName);
555
- if (ext) {
556
- let type = getMimeType(ext.substring(1));
557
- if (type) {
558
- const charset = getCharsetForMimeType(type);
559
- if (charset) type += `; charset=${charset}`;
560
- event.response.headers.set(HeaderName.CONTENT_TYPE, type);
561
- }
562
- }
563
- }
564
- //#endregion
565
- //#region src/response/helpers/header-attachment.ts
566
- function sanitizeFilename(filename) {
567
- return filename.replace(/[\r\n]/g, "");
568
- }
569
- function toAsciiFilename(filename) {
570
- return filename.replace(/[^\x20-\x7E]/g, "").replace(/"/g, "\\\"");
571
- }
572
- function encodeRfc5987(filename) {
573
- return encodeURIComponent(filename).replace(/['()]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`).replace(/\*/g, "%2A");
574
- }
575
- function setResponseHeaderAttachment(event, filename) {
576
- if (typeof filename === "string") setResponseContentTypeByFileName(event, filename);
577
- let disposition = "attachment";
578
- if (filename) {
579
- const sanitized = sanitizeFilename(filename);
580
- const ascii = toAsciiFilename(sanitized);
581
- disposition += `; filename="${ascii}"`;
582
- disposition += `; filename*=UTF-8''${encodeRfc5987(sanitized)}`;
583
- }
584
- event.response.headers.set(HeaderName.CONTENT_DISPOSITION, disposition);
585
- }
586
- //#endregion
587
- //#region src/response/helpers/header-content-type.ts
588
- function setResponseHeaderContentType(event, input, ifNotExists) {
589
- if (ifNotExists) {
590
- if (event.response.headers.get(HeaderName.CONTENT_TYPE)) return;
591
- }
592
- const contentType = getMimeType(input);
593
- if (contentType) event.response.headers.set(HeaderName.CONTENT_TYPE, contentType);
594
- }
595
- //#endregion
596
- //#region src/router-options/module.ts
597
- const defaults = {
598
- trustProxy: () => false,
599
- subdomainOffset: 2,
600
- etag: buildEtagFn(),
601
- proxyIpMax: 0
602
- };
603
- const instances = {};
604
- function setRouterOptions(id, input) {
605
- instances[id] = input;
606
- }
607
- function findRouterOption(key, path) {
608
- if (!path || path.length === 0) return defaults[key];
609
- if (path.length > 0) for (let i = path.length; i >= 0; i--) {
610
- const segment = path[i];
611
- if (segment !== void 0 && hasOwnProperty(instances, segment) && typeof instances[segment][key] !== "undefined") return instances[segment][key];
612
- }
613
- return defaults[key];
614
- }
615
- //#endregion
616
- //#region src/response/to-response.ts
617
- function stripWeakPrefix(etag) {
618
- return etag.startsWith("W/") ? etag.slice(2) : etag;
619
- }
620
- async function applyEtag(body, event, headers) {
621
- const etagFn = findRouterOption("etag", event.routerPath);
622
- if (!etagFn) return void 0;
623
- const etag = await etagFn(body);
624
- if (!etag) return void 0;
625
- headers.set("etag", etag);
626
- const ifNoneMatch = event.headers.get("if-none-match");
627
- if (ifNoneMatch && (ifNoneMatch === "*" || ifNoneMatch.split(",").some((t) => stripWeakPrefix(t.trim()) === stripWeakPrefix(etag)))) return new Response(null, {
628
- status: 304,
629
- headers
630
- });
631
- }
632
- async function toResponse(value, event) {
633
- if (value === void 0) return;
634
- if (value === null) return new Response(null, {
635
- status: event.response.status,
636
- statusText: event.response.statusText,
637
- headers: event.response.headers
638
- });
639
- if (value instanceof Response) return value;
640
- const { status, headers, statusText } = event.response;
641
- if (typeof value === "string") {
642
- if (!headers.has("content-type")) headers.set("content-type", "text/plain; charset=utf-8");
643
- const cached = await applyEtag(value, event, headers);
644
- if (cached) return cached;
645
- return new Response(value, {
646
- status,
647
- statusText,
648
- headers
649
- });
650
- }
651
- if (value instanceof ArrayBuffer || value instanceof Uint8Array) {
652
- if (!headers.has("content-type")) headers.set("content-type", "application/octet-stream");
653
- return new Response(value, {
654
- status,
655
- statusText,
656
- headers
657
- });
658
- }
659
- if (value instanceof ReadableStream) return new Response(value, {
660
- status,
661
- statusText,
662
- headers
663
- });
664
- if (value instanceof Blob) {
665
- if (!headers.has("content-type")) headers.set("content-type", value.type || "application/octet-stream");
666
- return new Response(value, {
667
- status,
668
- statusText,
669
- headers
670
- });
671
- }
672
- if (!headers.has("content-type")) headers.set("content-type", "application/json; charset=utf-8");
673
- const json = JSON.stringify(value);
674
443
  const cached = await applyEtag(json, event, headers);
675
444
  if (cached) return cached;
676
445
  return new Response(json, {
677
446
  status,
678
- statusText,
679
447
  headers
680
448
  });
681
449
  }
@@ -683,16 +451,12 @@ async function toResponse(value, event) {
683
451
  //#region src/response/helpers/send-accepted.ts
684
452
  async function sendAccepted(event, data) {
685
453
  event.response.status = 202;
686
- event.response.statusText = "Accepted";
687
- event.dispatched = true;
688
454
  return await toResponse(data ?? "", event);
689
455
  }
690
456
  //#endregion
691
457
  //#region src/response/helpers/send-created.ts
692
458
  async function sendCreated(event, data) {
693
459
  event.response.status = 201;
694
- event.response.statusText = "Created";
695
- event.dispatched = true;
696
460
  return await toResponse(data ?? "", event);
697
461
  }
698
462
  //#endregion
@@ -718,133 +482,438 @@ async function sendFile(event, options) {
718
482
  contentOptions.start = Number.isFinite(parsedStart) && parsedStart >= 0 ? parsedStart : 0;
719
483
  contentOptions.end = Number.isFinite(parsedEnd) && parsedEnd >= 0 ? Math.min(parsedEnd, stats.size - 1) : stats.size - 1;
720
484
  if (contentOptions.start >= stats.size || contentOptions.start > contentOptions.end) {
721
- event.dispatched = true;
722
485
  const rangeHeaders = new Headers(headers);
723
486
  rangeHeaders.set(HeaderName.CONTENT_RANGE, `bytes */${stats.size}`);
724
487
  return new Response(null, {
725
488
  status: 416,
726
- statusText: event.response.statusText,
727
489
  headers: rangeHeaders
728
490
  });
729
491
  }
730
- headers.set(HeaderName.CONTENT_RANGE, `bytes ${contentOptions.start}-${contentOptions.end}/${stats.size}`);
731
- headers.set(HeaderName.CONTENT_LENGTH, `${contentOptions.end - contentOptions.start + 1}`);
732
- statusCode = 206;
733
- } else headers.set(HeaderName.CONTENT_LENGTH, `${stats.size}`);
734
- headers.set(HeaderName.ACCEPT_RANGES, "bytes");
735
- if (stats.mtime) {
736
- const mtime = new Date(stats.mtime);
737
- headers.set(HeaderName.LAST_MODIFIED, mtime.toUTCString());
738
- headers.set(HeaderName.ETag, `W/"${stats.size}-${mtime.getTime()}"`);
492
+ headers.set(HeaderName.CONTENT_RANGE, `bytes ${contentOptions.start}-${contentOptions.end}/${stats.size}`);
493
+ headers.set(HeaderName.CONTENT_LENGTH, `${contentOptions.end - contentOptions.start + 1}`);
494
+ statusCode = 206;
495
+ } else headers.set(HeaderName.CONTENT_LENGTH, `${stats.size}`);
496
+ headers.set(HeaderName.ACCEPT_RANGES, "bytes");
497
+ if (stats.mtime) {
498
+ const mtime = new Date(stats.mtime);
499
+ headers.set(HeaderName.LAST_MODIFIED, mtime.toUTCString());
500
+ headers.set(HeaderName.ETag, `W/"${stats.size}-${mtime.getTime()}"`);
501
+ }
502
+ }
503
+ const content = await options.content(contentOptions);
504
+ return new Response(content, {
505
+ status: statusCode,
506
+ headers
507
+ });
508
+ }
509
+ //#endregion
510
+ //#region src/request/helpers/header.ts
511
+ function getRequestHeader(event, name) {
512
+ return event.headers.get(name);
513
+ }
514
+ //#endregion
515
+ //#region src/request/helpers/negotiator.ts
516
+ const NEGOTIATOR_KEY = Symbol.for("routup:negotiator");
517
+ function headersToPlainObject(headers) {
518
+ const result = {};
519
+ headers.forEach((value, key) => {
520
+ result[key] = value;
521
+ });
522
+ return result;
523
+ }
524
+ function useRequestNegotiator(event) {
525
+ let value = event.store[NEGOTIATOR_KEY];
526
+ if (value) return value;
527
+ value = new Negotiator({ headers: headersToPlainObject(event.headers) });
528
+ event.store[NEGOTIATOR_KEY] = value;
529
+ return value;
530
+ }
531
+ //#endregion
532
+ //#region src/request/helpers/header-accept.ts
533
+ function getRequestAcceptableContentTypes(event) {
534
+ return useRequestNegotiator(event).mediaTypes();
535
+ }
536
+ function getRequestAcceptableContentType(event, input) {
537
+ input = input || [];
538
+ const items = Array.isArray(input) ? input : [input];
539
+ if (items.length === 0) return getRequestAcceptableContentTypes(event).shift();
540
+ if (!getRequestHeader(event, HeaderName.ACCEPT)) return items[0];
541
+ let polluted = false;
542
+ const mimeTypes = [];
543
+ for (const item of items) {
544
+ const mimeType = getMimeType(item);
545
+ if (mimeType) mimeTypes.push(mimeType);
546
+ else polluted = true;
547
+ }
548
+ const matches = useRequestNegotiator(event).mediaTypes(mimeTypes);
549
+ if (matches.length > 0) {
550
+ if (polluted) return items[0];
551
+ return items[mimeTypes.indexOf(matches[0])];
552
+ }
553
+ }
554
+ //#endregion
555
+ //#region src/response/helpers/send-format.ts
556
+ function sendFormat(event, input) {
557
+ const { default: formatDefault, ...formats } = input;
558
+ const contentTypes = Object.keys(formats);
559
+ if (contentTypes.length === 0) return formatDefault();
560
+ const contentType = getRequestAcceptableContentType(event, contentTypes);
561
+ if (contentType && formats[contentType]) return formats[contentType]();
562
+ return formatDefault();
563
+ }
564
+ //#endregion
565
+ //#region src/response/helpers/send-redirect.ts
566
+ function escapeHtml(str) {
567
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
568
+ }
569
+ function isAllowedRedirectUrl(location) {
570
+ if (location.startsWith("//")) return false;
571
+ if (location.startsWith("/") || location.startsWith(".")) return true;
572
+ try {
573
+ const url = new URL(location);
574
+ return url.protocol === "http:" || url.protocol === "https:";
575
+ } catch {
576
+ return true;
577
+ }
578
+ }
579
+ function sendRedirect(event, location, statusCode = 302) {
580
+ if (!isAllowedRedirectUrl(location)) throw new RoutupError({
581
+ status: 400,
582
+ message: "Invalid redirect URL scheme."
583
+ });
584
+ const sanitizedLocation = sanitizeHeaderValue(location);
585
+ const html = `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${escapeHtml(location)}"></head></html>`;
586
+ const headers = new Headers(event.response.headers);
587
+ headers.set("location", sanitizedLocation);
588
+ headers.set("content-type", "text/html; charset=utf-8");
589
+ headers.delete("content-length");
590
+ return new Response(html, {
591
+ status: statusCode,
592
+ headers
593
+ });
594
+ }
595
+ //#endregion
596
+ //#region src/response/helpers/send-stream.ts
597
+ function sendStream(event, stream) {
598
+ const { status, headers } = event.response;
599
+ return new Response(stream, {
600
+ status,
601
+ headers
602
+ });
603
+ }
604
+ //#endregion
605
+ //#region src/event/module.ts
606
+ var RoutupEvent = class {
607
+ request;
608
+ params;
609
+ path;
610
+ method;
611
+ mountPath;
612
+ headers;
613
+ searchParams;
614
+ response;
615
+ store;
616
+ signal;
617
+ _context;
618
+ _routerOptions;
619
+ constructor(context) {
620
+ this._context = context;
621
+ this.request = context.request;
622
+ this.params = context.params;
623
+ this.path = context.path;
624
+ this.method = context.method;
625
+ this.mountPath = context.mountPath;
626
+ this.headers = context.headers;
627
+ this.searchParams = context.searchParams;
628
+ this.response = context.response;
629
+ this.store = context.store;
630
+ this.signal = context.signal;
631
+ }
632
+ get routerOptions() {
633
+ if (!this._routerOptions) this._routerOptions = this._context.routerOptions();
634
+ return this._routerOptions;
635
+ }
636
+ async next(error) {
637
+ return this._context.next(this, error);
638
+ }
639
+ };
640
+ //#endregion
641
+ //#region src/dispatcher/module.ts
642
+ var DispatcherEvent = class {
643
+ request;
644
+ params;
645
+ path;
646
+ method;
647
+ /**
648
+ * Collected allowed methods (for OPTIONS).
649
+ */
650
+ methodsAllowed;
651
+ mountPath;
652
+ error;
653
+ routerPath;
654
+ _dispatched;
655
+ _response;
656
+ _store;
657
+ /**
658
+ * Cached parsed URL (avoids double-parsing).
659
+ */
660
+ _url;
661
+ /**
662
+ * Continuation function for middleware onion model.
663
+ */
664
+ _next;
665
+ _signal;
666
+ _signalCleanup;
667
+ /**
668
+ * Whether _next has already been called (guard against double-invocation).
669
+ */
670
+ _nextCalled;
671
+ /**
672
+ * The cached result of the next handler.
673
+ */
674
+ _nextResult;
675
+ constructor(request) {
676
+ this.request = request;
677
+ this._url = new FastURL(request.url);
678
+ this.method = request.method;
679
+ this.path = this._url.pathname;
680
+ this.mountPath = "/";
681
+ this.params = {};
682
+ this.routerPath = [];
683
+ this.methodsAllowed = /* @__PURE__ */ new Set();
684
+ this._dispatched = false;
685
+ this._nextCalled = false;
686
+ }
687
+ get response() {
688
+ if (!this._response) this._response = {
689
+ status: 200,
690
+ headers: new Headers()
691
+ };
692
+ return this._response;
693
+ }
694
+ get signal() {
695
+ if (!this._signal) this._signal = this.request.signal;
696
+ return this._signal;
697
+ }
698
+ set signal(value) {
699
+ if (this._signalCleanup) {
700
+ this._signalCleanup();
701
+ this._signalCleanup = void 0;
702
+ }
703
+ if (value === this.request.signal) {
704
+ this._signal = value;
705
+ return;
706
+ }
707
+ const controller = new AbortController();
708
+ const abort = (e) => {
709
+ const reason = e?.target instanceof AbortSignal ? e.target.reason : void 0;
710
+ this.request.signal.removeEventListener("abort", abort);
711
+ value.removeEventListener("abort", abort);
712
+ controller.abort(reason);
713
+ };
714
+ if (this.request.signal.aborted || value.aborted) {
715
+ const reason = this.request.signal.aborted ? this.request.signal.reason : value.reason;
716
+ controller.abort(reason);
717
+ } else {
718
+ this.request.signal.addEventListener("abort", abort, { once: true });
719
+ value.addEventListener("abort", abort, { once: true });
720
+ this._signalCleanup = () => {
721
+ this.request.signal.removeEventListener("abort", abort);
722
+ value.removeEventListener("abort", abort);
723
+ };
724
+ }
725
+ this._signal = controller.signal;
726
+ }
727
+ get dispatched() {
728
+ return this._dispatched;
729
+ }
730
+ set dispatched(value) {
731
+ this._dispatched = value;
732
+ }
733
+ async next(event, error) {
734
+ if (this._nextCalled) return this._nextResult;
735
+ this._nextCalled = true;
736
+ if (this._next) this._nextResult = this._next(event, error);
737
+ return this._nextResult;
738
+ }
739
+ setNext(fn) {
740
+ if (fn) this._next = async (event, error) => {
741
+ return toResponse(await fn(error), event);
742
+ };
743
+ else this._next = void 0;
744
+ this._nextCalled = false;
745
+ this._nextResult = void 0;
746
+ }
747
+ build(signal) {
748
+ return new RoutupEvent({
749
+ request: this.request,
750
+ params: this.params,
751
+ path: this.path,
752
+ method: this.method,
753
+ mountPath: this.mountPath,
754
+ headers: this.request.headers,
755
+ searchParams: new URLSearchParams(this._url.search),
756
+ response: this.response,
757
+ store: this.store,
758
+ signal: signal ?? this.signal,
759
+ routerOptions: () => this.resolveOptions(),
760
+ next: (event, error) => this.next(event, error)
761
+ });
762
+ }
763
+ get store() {
764
+ if (!this._store) this._store = Object.create(null);
765
+ return this._store;
766
+ }
767
+ resolveOptions() {
768
+ const resolved = {
769
+ trustProxy: () => false,
770
+ subdomainOffset: 2,
771
+ etag: buildEtagFn(),
772
+ proxyIpMax: 0
773
+ };
774
+ for (let i = 0; i < this.routerPath.length; i++) {
775
+ const node = this.routerPath[i];
776
+ const entries = Object.entries(node.options);
777
+ for (const entry of entries) {
778
+ const [key, value] = entry;
779
+ if (typeof value !== "undefined") resolved[key] = value;
780
+ }
739
781
  }
782
+ return resolved;
740
783
  }
741
- const content = await options.content(contentOptions);
742
- event.dispatched = true;
743
- return new Response(content, {
744
- status: statusCode,
745
- statusText: event.response.statusText,
746
- headers
747
- });
748
- }
784
+ };
749
785
  //#endregion
750
- //#region src/request/helpers/header.ts
751
- function getRequestHeader(event, name) {
752
- return event.headers.get(name);
753
- }
786
+ //#region src/handler/constants.ts
787
+ const HandlerType = {
788
+ CORE: "core",
789
+ ERROR: "error"
790
+ };
791
+ const HandlerSymbol = Symbol.for("Handler");
754
792
  //#endregion
755
- //#region src/request/helpers/negotiator.ts
756
- const NEGOTIATOR_KEY = /* @__PURE__ */ Symbol.for("routup:negotiator");
757
- function headersToPlainObject(headers) {
758
- const result = {};
759
- headers.forEach((value, key) => {
760
- result[key] = value;
761
- });
762
- return result;
763
- }
764
- function useRequestNegotiator(event) {
765
- let value = event.store[NEGOTIATOR_KEY];
766
- if (value) return value;
767
- value = new Negotiator({ headers: headersToPlainObject(event.headers) });
768
- event.store[NEGOTIATOR_KEY] = value;
769
- return value;
770
- }
793
+ //#region src/hook/constants.ts
794
+ const HookName = {
795
+ REQUEST: "request",
796
+ RESPONSE: "response",
797
+ ERROR: "error",
798
+ CHILD_MATCH: "childMatch",
799
+ CHILD_DISPATCH_BEFORE: "childDispatchBefore",
800
+ CHILD_DISPATCH_AFTER: "childDispatchAfter"
801
+ };
771
802
  //#endregion
772
- //#region src/request/helpers/header-accept.ts
773
- function getRequestAcceptableContentTypes(event) {
774
- return useRequestNegotiator(event).mediaTypes();
775
- }
776
- function getRequestAcceptableContentType(event, input) {
777
- input = input || [];
778
- const items = Array.isArray(input) ? input : [input];
779
- if (items.length === 0) return getRequestAcceptableContentTypes(event).shift();
780
- if (!getRequestHeader(event, HeaderName.ACCEPT)) return items[0];
781
- let polluted = false;
782
- const mimeTypes = [];
783
- for (const item of items) {
784
- const mimeType = getMimeType(item);
785
- if (mimeType) mimeTypes.push(mimeType);
786
- else polluted = true;
803
+ //#region src/hook/module.ts
804
+ var HookManager = class {
805
+ items;
806
+ constructor() {
807
+ this.items = {};
787
808
  }
788
- const matches = useRequestNegotiator(event).mediaTypes(mimeTypes);
789
- if (matches.length > 0) {
790
- if (polluted) return items[0];
791
- return items[mimeTypes.indexOf(matches[0])];
809
+ addListener(name, fn, priority = 0) {
810
+ this.items[name] = this.items[name] || [];
811
+ const entry = {
812
+ fn,
813
+ priority
814
+ };
815
+ let i = 0;
816
+ while (i < this.items[name].length && this.items[name][i].priority >= priority) i++;
817
+ this.items[name].splice(i, 0, entry);
818
+ return () => {
819
+ this.removeListener(name, fn);
820
+ };
792
821
  }
793
- }
794
- //#endregion
795
- //#region src/response/helpers/send-format.ts
796
- function sendFormat(event, input) {
797
- const { default: formatDefault, ...formats } = input;
798
- const contentTypes = Object.keys(formats);
799
- if (contentTypes.length === 0) return formatDefault();
800
- const contentType = getRequestAcceptableContentType(event, contentTypes);
801
- if (contentType && formats[contentType]) return formats[contentType]();
802
- return formatDefault();
803
- }
822
+ removeListener(name, fn) {
823
+ if (!this.items[name]) return;
824
+ if (typeof fn === "undefined") {
825
+ delete this.items[name];
826
+ return;
827
+ }
828
+ if (typeof fn === "function") {
829
+ const index = this.items[name].findIndex((entry) => entry.fn === fn);
830
+ if (index !== -1) this.items[name].splice(index, 1);
831
+ }
832
+ if (this.items[name].length === 0) delete this.items[name];
833
+ }
834
+ async trigger(name, event) {
835
+ if (!this.items[name] || this.items[name].length === 0) return;
836
+ try {
837
+ for (let i = 0; i < this.items[name].length; i++) {
838
+ const { fn } = this.items[name][i];
839
+ await this.triggerListener(name, event, fn);
840
+ if (event.dispatched) {
841
+ if (event.error) event.error = void 0;
842
+ return;
843
+ }
844
+ }
845
+ } catch (e) {
846
+ if (!event.error) event.error = createError(e);
847
+ if (!this.isErrorListenerHook(name)) {
848
+ await this.trigger(HookName.ERROR, event);
849
+ if (event.dispatched) {
850
+ if (event.error) event.error = void 0;
851
+ }
852
+ }
853
+ }
854
+ }
855
+ triggerListener(name, event, listener) {
856
+ if (this.isErrorListenerHook(name)) {
857
+ if (event.error) return listener(event);
858
+ return;
859
+ }
860
+ return listener(event);
861
+ }
862
+ isErrorListenerHook(input) {
863
+ return input === HookName.ERROR;
864
+ }
865
+ };
804
866
  //#endregion
805
- //#region src/response/helpers/send-redirect.ts
806
- function escapeHtml(str) {
807
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
808
- }
809
- function isAllowedRedirectUrl(location) {
810
- if (location.startsWith("//")) return false;
811
- if (location.startsWith("/") || location.startsWith(".")) return true;
867
+ //#region src/path/matcher.ts
868
+ function decodeParam(val) {
869
+ /* istanbul ignore next */
870
+ if (typeof val !== "string" || val.length === 0) return val;
812
871
  try {
813
- const url = new URL(location);
814
- return url.protocol === "http:" || url.protocol === "https:";
872
+ return decodeURIComponent(val);
815
873
  } catch {
816
- return true;
874
+ return val;
817
875
  }
818
876
  }
819
- function sendRedirect(event, location, statusCode = 302) {
820
- if (!isAllowedRedirectUrl(location)) throw new RoutupError({
821
- statusCode: 400,
822
- statusMessage: "Invalid redirect URL scheme."
823
- });
824
- const sanitizedLocation = sanitizeHeaderValue(location);
825
- const html = `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${escapeHtml(location)}"></head></html>`;
826
- const headers = new Headers(event.response.headers);
827
- headers.set("location", sanitizedLocation);
828
- headers.set("content-type", "text/html; charset=utf-8");
829
- headers.delete("content-length");
830
- const response = new Response(html, {
831
- status: statusCode,
832
- statusText: event.response.statusText,
833
- headers
834
- });
835
- event.dispatched = true;
836
- return response;
837
- }
877
+ var PathMatcher = class {
878
+ path;
879
+ regexp;
880
+ regexpKeys = [];
881
+ regexpOptions;
882
+ constructor(path, options) {
883
+ this.path = path;
884
+ this.regexpOptions = options || {};
885
+ const regexp = pathToRegexp(path, options);
886
+ this.regexp = regexp.regexp;
887
+ this.regexpKeys = regexp.keys;
888
+ }
889
+ test(path) {
890
+ return this.regexp.test(path);
891
+ }
892
+ exec(path) {
893
+ if (this.path === "/" && this.regexpOptions.end === false) return {
894
+ path: "/",
895
+ params: Object.create(null)
896
+ };
897
+ const match = this.regexp.exec(path);
898
+ if (!match) return;
899
+ const params = Object.create(null);
900
+ for (let i = 1; i < match.length; i++) {
901
+ const key = this.regexpKeys[i - 1];
902
+ if (!key) continue;
903
+ const prop = key.name;
904
+ const val = decodeParam(match[i]);
905
+ if (typeof val !== "undefined") params[prop] = val;
906
+ }
907
+ return {
908
+ path: match[0],
909
+ params
910
+ };
911
+ }
912
+ };
838
913
  //#endregion
839
- //#region src/response/helpers/send-stream.ts
840
- function sendStream(event, stream) {
841
- event.dispatched = true;
842
- const { status, statusText, headers } = event.response;
843
- return new Response(stream, {
844
- status,
845
- statusText,
846
- headers
847
- });
914
+ //#region src/path/utils.ts
915
+ function isPath(input) {
916
+ return typeof input === "string";
848
917
  }
849
918
  //#endregion
850
919
  //#region src/handler/module.ts
@@ -884,10 +953,37 @@ var Handler = class {
884
953
  let response;
885
954
  try {
886
955
  let result;
887
- if (this.config.type === HandlerType.ERROR) {
888
- if (event.error) result = await this.config.fn(event.error, event);
889
- } else result = await this.config.fn(event);
890
- response = await toResponse(result, event);
956
+ const previewEvent = event.build();
957
+ const effectiveTimeout = this.resolveTimeout(previewEvent.routerOptions);
958
+ let childController;
959
+ let cleanupParentListener;
960
+ let handlerEvent = previewEvent;
961
+ if (effectiveTimeout) {
962
+ const parentSignal = event.signal;
963
+ childController = new AbortController();
964
+ if (parentSignal.aborted) childController.abort(parentSignal.reason);
965
+ else {
966
+ const onAbort = () => childController.abort(parentSignal.reason);
967
+ parentSignal.addEventListener("abort", onAbort, { once: true });
968
+ cleanupParentListener = () => parentSignal.removeEventListener("abort", onAbort);
969
+ }
970
+ handlerEvent = event.build(childController.signal);
971
+ }
972
+ try {
973
+ if (this.config.type === HandlerType.ERROR) {
974
+ if (event.error) {
975
+ const { fn } = this.config;
976
+ const { error } = event;
977
+ result = await this.executeWithTimeout(() => fn(error, handlerEvent), handlerEvent.routerOptions, childController);
978
+ }
979
+ } else {
980
+ const { fn } = this.config;
981
+ result = await this.executeWithTimeout(() => fn(handlerEvent), handlerEvent.routerOptions, childController);
982
+ }
983
+ } finally {
984
+ if (cleanupParentListener) cleanupParentListener();
985
+ }
986
+ response = await toResponse(result, handlerEvent);
891
987
  if (response) event.dispatched = true;
892
988
  } catch (e) {
893
989
  event.error = isError(e) ? e : createError(e);
@@ -919,6 +1015,33 @@ var Handler = class {
919
1015
  this.config.method = method;
920
1016
  this._method = method;
921
1017
  }
1018
+ async executeWithTimeout(fn, routerOptions, controller) {
1019
+ const effectiveTimeout = this.resolveTimeout(routerOptions);
1020
+ if (!effectiveTimeout) return fn();
1021
+ let timerId;
1022
+ try {
1023
+ return await Promise.race([fn(), new Promise((_, reject) => {
1024
+ timerId = setTimeout(() => {
1025
+ if (controller) controller.abort();
1026
+ reject(createError({
1027
+ status: 408,
1028
+ message: "Request Timeout"
1029
+ }));
1030
+ }, effectiveTimeout);
1031
+ })]);
1032
+ } finally {
1033
+ clearTimeout(timerId);
1034
+ }
1035
+ }
1036
+ resolveTimeout(routerOptions) {
1037
+ const routerDefault = routerOptions.handlerTimeout;
1038
+ const handlerOverride = this.config.timeout;
1039
+ if (!routerDefault && !handlerOverride) return;
1040
+ if (!routerDefault) return handlerOverride;
1041
+ if (!handlerOverride) return routerDefault;
1042
+ if (routerOptions.handlerTimeoutOverridable) return handlerOverride;
1043
+ return Math.min(routerDefault, handlerOverride);
1044
+ }
922
1045
  mountHooks() {
923
1046
  if (this.config.onBefore) this.hookManager.addListener(HookName.CHILD_DISPATCH_BEFORE, this.config.onBefore);
924
1047
  if (this.config.onAfter) this.hookManager.addListener(HookName.CHILD_DISPATCH_AFTER, this.config.onAfter);
@@ -1028,7 +1151,7 @@ function createNodeBridge(handler, isMiddleware) {
1028
1151
  if (!node?.req || !node?.res) throw new RoutupError("fromNodeHandler/fromNodeMiddleware requires a Node.js runtime.");
1029
1152
  const req = node.req;
1030
1153
  const res = node.res;
1031
- if ((isMiddleware ? await callMiddleware(handler, req, res) : await callHandler(handler, req, res)) === kHandled) event.dispatched = true;
1154
+ if ((isMiddleware ? await callMiddleware(handler, req, res) : await callHandler(handler, req, res)) === kHandled) return null;
1032
1155
  }) });
1033
1156
  }
1034
1157
  /**
@@ -1084,47 +1207,6 @@ function isHandler(input) {
1084
1207
  return isInstance(input, HandlerSymbol);
1085
1208
  }
1086
1209
  //#endregion
1087
- //#region src/request/helpers/body.ts
1088
- const BODY_KEY = /* @__PURE__ */ Symbol.for("routup:body");
1089
- /**
1090
- * Read and parse the request body.
1091
- *
1092
- * - `application/x-www-form-urlencoded` → plain object (duplicate keys become arrays)
1093
- * - `application/json` or other → attempts JSON parse, returns undefined on failure
1094
- *
1095
- * The result is cached on the event store — calling `readBody()` multiple
1096
- * times returns the same parsed result.
1097
- *
1098
- * For binary or streaming access, use `event.request.arrayBuffer()`,
1099
- * `event.request.blob()`, or `event.request.body` directly.
1100
- *
1101
- * @experimental
1102
- */
1103
- async function readBody(event) {
1104
- if (BODY_KEY in event.store) return event.store[BODY_KEY];
1105
- const text = await event.request.text();
1106
- let result;
1107
- if ((event.headers.get("content-type") || "").includes("application/x-www-form-urlencoded")) result = parseURLEncodedBody(text);
1108
- else try {
1109
- result = JSON.parse(text);
1110
- } catch {
1111
- result = void 0;
1112
- }
1113
- event.store[BODY_KEY] = result;
1114
- return result;
1115
- }
1116
- function parseURLEncodedBody(body) {
1117
- const form = new URLSearchParams(body);
1118
- const parsed = Object.create(null);
1119
- for (const [key, value] of form.entries()) {
1120
- const existing = parsed[key];
1121
- if (existing !== void 0) if (Array.isArray(existing)) existing.push(value);
1122
- else parsed[key] = [existing, value];
1123
- else parsed[key] = value;
1124
- }
1125
- return parsed;
1126
- }
1127
- //#endregion
1128
1210
  //#region src/request/helpers/cache.ts
1129
1211
  function isRequestCacheable(event, modifiedTime) {
1130
1212
  const modifiedSince = event.headers.get(HeaderName.IF_MODIFIED_SINCE);
@@ -1179,7 +1261,7 @@ function matchRequestContentType(event, contentType) {
1179
1261
  function getRequestHostName(event, options = {}) {
1180
1262
  let trustProxy;
1181
1263
  if (typeof options.trustProxy !== "undefined") trustProxy = buildTrustProxyFn(options.trustProxy);
1182
- else trustProxy = findRouterOption("trustProxy", event.routerPath);
1264
+ else trustProxy = event.routerOptions.trustProxy;
1183
1265
  let hostname = event.headers.get(HeaderName.X_FORWARDED_HOST);
1184
1266
  if (!hostname || !event.request.ip || !trustProxy(event.request.ip, 0)) hostname = event.headers.get(HeaderName.HOST);
1185
1267
  else if (hostname && hostname.includes(",")) hostname = hostname.substring(0, hostname.indexOf(",")).trimEnd();
@@ -1202,7 +1284,7 @@ function getRequestHostName(event, options = {}) {
1202
1284
  function getRequestIP(event, options = {}) {
1203
1285
  let trustProxy;
1204
1286
  if (typeof options.trustProxy !== "undefined") trustProxy = buildTrustProxyFn(options.trustProxy);
1205
- else trustProxy = findRouterOption("trustProxy", event.routerPath);
1287
+ else trustProxy = event.routerOptions.trustProxy;
1206
1288
  const socketAddr = event.request.ip;
1207
1289
  if (!socketAddr) return;
1208
1290
  const forwarded = event.headers.get(HeaderName.X_FORWARDED_FOR);
@@ -1222,7 +1304,7 @@ function getRequestIP(event, options = {}) {
1222
1304
  function getRequestProtocol(event, options = {}) {
1223
1305
  let trustProxy;
1224
1306
  if (typeof options.trustProxy !== "undefined") trustProxy = buildTrustProxyFn(options.trustProxy);
1225
- else trustProxy = findRouterOption("trustProxy", event.routerPath);
1307
+ else trustProxy = event.routerOptions.trustProxy;
1226
1308
  let protocol;
1227
1309
  try {
1228
1310
  if (new URL(event.request.url).protocol === "https:") protocol = "https";
@@ -1239,6 +1321,59 @@ function getRequestProtocol(event, options = {}) {
1239
1321
  return protocol;
1240
1322
  }
1241
1323
  //#endregion
1324
+ //#region src/plugin/error/constants.ts
1325
+ const PluginErrorCode = {
1326
+ PLUGIN: "PLUGIN",
1327
+ NOT_INSTALLED: "PLUGIN_NOT_INSTALLED",
1328
+ INSTALL: "PLUGIN_INSTALL"
1329
+ };
1330
+ //#endregion
1331
+ //#region src/plugin/error/is.ts
1332
+ const PLUGIN_ERROR_CODES = new Set(Object.values(PluginErrorCode));
1333
+ function isPluginError(input) {
1334
+ if (!isError(input)) return false;
1335
+ return PLUGIN_ERROR_CODES.has(input.code);
1336
+ }
1337
+ //#endregion
1338
+ //#region src/plugin/error/module.ts
1339
+ var PluginError = class extends RoutupError {
1340
+ constructor(input = {}) {
1341
+ const options = typeof input === "string" ? { message: input } : { ...input };
1342
+ if (!("code" in options) || !options.code) options.code = PluginErrorCode.PLUGIN;
1343
+ super(options);
1344
+ this.name = "PluginError";
1345
+ }
1346
+ };
1347
+ //#endregion
1348
+ //#region src/plugin/error/sub/install.ts
1349
+ var PluginInstallError = class extends PluginError {
1350
+ pluginName;
1351
+ constructor(pluginName, cause) {
1352
+ super({
1353
+ message: `Failed to install plugin "${pluginName}".`,
1354
+ code: PluginErrorCode.INSTALL,
1355
+ cause
1356
+ });
1357
+ this.name = "PluginInstallError";
1358
+ this.pluginName = pluginName;
1359
+ }
1360
+ };
1361
+ //#endregion
1362
+ //#region src/plugin/error/sub/not-installed.ts
1363
+ var PluginNotInstalledError = class extends PluginError {
1364
+ pluginName;
1365
+ helperName;
1366
+ constructor(pluginName, helperName) {
1367
+ super({
1368
+ message: `${helperName}() requires the "${pluginName}" plugin. Register it with: router.use(${pluginName}())`,
1369
+ code: PluginErrorCode.NOT_INSTALLED
1370
+ });
1371
+ this.name = "PluginNotInstalledError";
1372
+ this.pluginName = pluginName;
1373
+ this.helperName = helperName;
1374
+ }
1375
+ };
1376
+ //#endregion
1242
1377
  //#region src/plugin/is.ts
1243
1378
  function isPlugin(input) {
1244
1379
  if (!isObject(input)) return false;
@@ -1246,30 +1381,31 @@ function isPlugin(input) {
1246
1381
  return typeof input.install === "function" && input.install.length === 1;
1247
1382
  }
1248
1383
  //#endregion
1249
- //#region src/router-options/normalize.ts
1384
+ //#region src/router/options.ts
1250
1385
  function normalizeRouterOptions(input) {
1251
1386
  if (typeof input.etag !== "undefined") input.etag = buildEtagFn(input.etag);
1252
1387
  if (typeof input.trustProxy !== "undefined") input.trustProxy = buildTrustProxyFn(input.trustProxy);
1388
+ if (typeof input.timeout !== "undefined") {
1389
+ if (typeof input.timeout !== "number" || !Number.isFinite(input.timeout) || input.timeout <= 0) delete input.timeout;
1390
+ }
1391
+ if (typeof input.handlerTimeout !== "undefined") {
1392
+ if (typeof input.handlerTimeout !== "number" || !Number.isFinite(input.handlerTimeout) || input.handlerTimeout <= 0) delete input.handlerTimeout;
1393
+ }
1253
1394
  return input;
1254
1395
  }
1255
1396
  //#endregion
1256
1397
  //#region src/router/constants.ts
1257
- const RouterSymbol = /* @__PURE__ */ Symbol.for("Router");
1258
- let RouterPipelineStep = /* @__PURE__ */ function(RouterPipelineStep) {
1259
- RouterPipelineStep[RouterPipelineStep["START"] = 0] = "START";
1260
- RouterPipelineStep[RouterPipelineStep["LOOKUP"] = 1] = "LOOKUP";
1261
- RouterPipelineStep[RouterPipelineStep["CHILD_BEFORE"] = 2] = "CHILD_BEFORE";
1262
- RouterPipelineStep[RouterPipelineStep["CHILD_DISPATCH"] = 3] = "CHILD_DISPATCH";
1263
- RouterPipelineStep[RouterPipelineStep["CHILD_AFTER"] = 4] = "CHILD_AFTER";
1264
- RouterPipelineStep[RouterPipelineStep["FINISH"] = 5] = "FINISH";
1265
- return RouterPipelineStep;
1266
- }({});
1398
+ const RouterSymbol = Symbol.for("Router");
1399
+ const RouterPipelineStep = {
1400
+ START: 0,
1401
+ LOOKUP: 1,
1402
+ CHILD_BEFORE: 2,
1403
+ CHILD_DISPATCH: 3,
1404
+ CHILD_AFTER: 4,
1405
+ FINISH: 5
1406
+ };
1267
1407
  //#endregion
1268
1408
  //#region src/router/utils.ts
1269
- let nextId = 0;
1270
- function generateRouterID() {
1271
- return ++nextId;
1272
- }
1273
1409
  function isRouterInstance(input) {
1274
1410
  return isInstance(input, RouterSymbol);
1275
1411
  }
@@ -1288,10 +1424,6 @@ function acceptsJson(request) {
1288
1424
  var Router = class Router {
1289
1425
  "@instanceof" = RouterSymbol;
1290
1426
  /**
1291
- * An identifier for the router instance.
1292
- */
1293
- id;
1294
- /**
1295
1427
  * A label for the router instance.
1296
1428
  */
1297
1429
  name;
@@ -1313,12 +1445,15 @@ var Router = class Router {
1313
1445
  * @protected
1314
1446
  */
1315
1447
  hookManager;
1316
- constructor(options = {}) {
1317
- this.id = generateRouterID();
1318
- this.name = options.name;
1448
+ /**
1449
+ * Normalized options for this router instance.
1450
+ */
1451
+ _options;
1452
+ constructor(input = {}) {
1453
+ this.name = input.name;
1319
1454
  this.hookManager = new HookManager();
1320
- this.setPath(options.path);
1321
- setRouterOptions(this.id, normalizeRouterOptions(options));
1455
+ this._options = normalizeRouterOptions(input);
1456
+ this.setPath(input.path);
1322
1457
  }
1323
1458
  matchPath(path) {
1324
1459
  if (this.pathMatcher) return this.pathMatcher.test(path);
@@ -1332,23 +1467,37 @@ var Router = class Router {
1332
1467
  this.pathMatcher = new PathMatcher(withLeadingSlash(withoutTrailingSlash(`${value}`)), { end: false });
1333
1468
  }
1334
1469
  /**
1335
- * Public entry point — creates a RoutupEvent from the request,
1470
+ * Public entry point — creates a DispatcherEvent from the request,
1336
1471
  * runs the pipeline, and returns a Response (with 404/500 fallbacks).
1337
1472
  */
1338
1473
  async fetch(request) {
1339
- const event = new RoutupEvent(request);
1474
+ const event = new DispatcherEvent(request);
1340
1475
  let response;
1341
1476
  try {
1342
- response = await this.dispatch(event);
1477
+ const timeoutMs = this._options.timeout;
1478
+ if (timeoutMs) {
1479
+ const controller = new AbortController();
1480
+ event.signal = controller.signal;
1481
+ let timerId;
1482
+ try {
1483
+ response = await Promise.race([this.dispatch(event), new Promise((_, reject) => {
1484
+ timerId = setTimeout(() => {
1485
+ controller.abort();
1486
+ reject(createError({
1487
+ status: 408,
1488
+ message: "Request Timeout"
1489
+ }));
1490
+ }, timeoutMs);
1491
+ })]);
1492
+ } finally {
1493
+ clearTimeout(timerId);
1494
+ }
1495
+ } else response = await this.dispatch(event);
1343
1496
  } catch (e) {
1344
1497
  event.error = createError(e);
1345
1498
  }
1346
1499
  if (response) return response;
1347
- if (event.error) {
1348
- const status = event.error.statusCode || 500;
1349
- const message = event.error.statusMessage || "Internal Server Error";
1350
- return this.buildFallbackResponse(request, event, status, message);
1351
- }
1500
+ if (event.error) return this.buildFallbackResponse(request, event, event.error.status || 500, event.error.message);
1352
1501
  return this.buildFallbackResponse(request, event, 404, "Not Found");
1353
1502
  }
1354
1503
  buildFallbackResponse(request, event, status, message) {
@@ -1360,33 +1509,42 @@ var Router = class Router {
1360
1509
  message
1361
1510
  }), {
1362
1511
  status,
1363
- statusText: message,
1364
1512
  headers
1365
1513
  });
1366
1514
  }
1367
1515
  headers.set("content-type", "text/plain; charset=utf-8");
1368
1516
  return new Response(message, {
1369
1517
  status,
1370
- statusText: message,
1371
1518
  headers
1372
1519
  });
1373
1520
  }
1374
1521
  async executePipelineStep(context) {
1375
- switch (context.step) {
1376
- case RouterPipelineStep.START: return this.executePipelineStepStart(context);
1377
- case RouterPipelineStep.LOOKUP: return this.executePipelineStepLookup(context);
1378
- case RouterPipelineStep.CHILD_BEFORE: return this.executePipelineStepChildBefore(context);
1379
- case RouterPipelineStep.CHILD_DISPATCH: return this.executePipelineStepChildDispatch(context);
1380
- case RouterPipelineStep.CHILD_AFTER: return this.executePipelineStepChildAfter(context);
1381
- case RouterPipelineStep.FINISH:
1382
- default: return this.executePipelineStepFinish(context);
1522
+ while (context.step !== RouterPipelineStep.FINISH) switch (context.step) {
1523
+ case RouterPipelineStep.START:
1524
+ await this.executePipelineStepStart(context);
1525
+ break;
1526
+ case RouterPipelineStep.LOOKUP:
1527
+ await this.executePipelineStepLookup(context);
1528
+ break;
1529
+ case RouterPipelineStep.CHILD_BEFORE:
1530
+ await this.executePipelineStepChildBefore(context);
1531
+ break;
1532
+ case RouterPipelineStep.CHILD_DISPATCH:
1533
+ await this.executePipelineStepChildDispatch(context);
1534
+ break;
1535
+ case RouterPipelineStep.CHILD_AFTER:
1536
+ await this.executePipelineStepChildAfter(context);
1537
+ break;
1538
+ default:
1539
+ context.step = RouterPipelineStep.FINISH;
1540
+ break;
1383
1541
  }
1542
+ await this.executePipelineStepFinish(context);
1384
1543
  }
1385
1544
  async executePipelineStepStart(context) {
1386
1545
  await this.hookManager.trigger(HookName.REQUEST, context.event);
1387
1546
  if (context.event.dispatched) context.step = RouterPipelineStep.FINISH;
1388
1547
  else context.step = RouterPipelineStep.LOOKUP;
1389
- return this.executePipelineStep(context);
1390
1548
  }
1391
1549
  async executePipelineStepLookup(context) {
1392
1550
  while (!context.event.dispatched && context.stackIndex < this.stack.length) {
@@ -1397,12 +1555,12 @@ var Router = class Router {
1397
1555
  continue;
1398
1556
  }
1399
1557
  if (item.matchPath(context.event.path)) {
1400
- if (item.method) context.event.methodsAllowed.push(item.method);
1558
+ if (item.method) context.event.methodsAllowed.add(item.method);
1401
1559
  if (item.matchMethod(context.event.method)) {
1402
1560
  await this.hookManager.trigger(HookName.CHILD_MATCH, context.event);
1403
1561
  if (context.event.dispatched) context.step = RouterPipelineStep.FINISH;
1404
1562
  else context.step = RouterPipelineStep.CHILD_BEFORE;
1405
- return this.executePipelineStep(context);
1563
+ return;
1406
1564
  }
1407
1565
  }
1408
1566
  context.stackIndex++;
@@ -1412,51 +1570,41 @@ var Router = class Router {
1412
1570
  await this.hookManager.trigger(HookName.CHILD_MATCH, context.event);
1413
1571
  if (context.event.dispatched) context.step = RouterPipelineStep.FINISH;
1414
1572
  else context.step = RouterPipelineStep.CHILD_BEFORE;
1415
- return this.executePipelineStep(context);
1573
+ return;
1416
1574
  }
1417
1575
  context.stackIndex++;
1418
1576
  }
1419
1577
  context.step = RouterPipelineStep.FINISH;
1420
- return this.executePipelineStep(context);
1421
1578
  }
1422
1579
  async executePipelineStepChildBefore(context) {
1423
1580
  await this.hookManager.trigger(HookName.CHILD_DISPATCH_BEFORE, context.event);
1424
1581
  if (context.event.dispatched) context.step = RouterPipelineStep.FINISH;
1425
1582
  else context.step = RouterPipelineStep.CHILD_DISPATCH;
1426
- return this.executePipelineStep(context);
1427
1583
  }
1428
1584
  async executePipelineStepChildAfter(context) {
1429
1585
  await this.hookManager.trigger(HookName.CHILD_DISPATCH_AFTER, context.event);
1430
1586
  if (context.event.dispatched) context.step = RouterPipelineStep.FINISH;
1431
1587
  else context.step = RouterPipelineStep.LOOKUP;
1432
- return this.executePipelineStep(context);
1433
1588
  }
1434
1589
  async executePipelineStepChildDispatch(context) {
1435
1590
  if (context.event.dispatched || typeof this.stack[context.stackIndex] === "undefined") {
1436
1591
  context.step = RouterPipelineStep.FINISH;
1437
- return this.executePipelineStep(context);
1592
+ return;
1438
1593
  }
1439
1594
  const item = this.stack[context.stackIndex];
1440
1595
  const { event } = context;
1441
- const savedNext = event._next;
1442
- const savedNextCalled = event._nextCalled;
1443
1596
  try {
1444
- event._nextCalled = false;
1445
- event._next = async () => {
1597
+ event.setNext(async (error) => {
1598
+ if (error) event.error = createError(error);
1446
1599
  const nextContext = {
1447
1600
  step: RouterPipelineStep.LOOKUP,
1448
1601
  event,
1449
1602
  stackIndex: context.stackIndex + 1,
1450
1603
  response: void 0
1451
1604
  };
1452
- event.routerPath.push(this.id);
1453
- try {
1454
- await this.executePipelineStep(nextContext);
1455
- } finally {
1456
- event.routerPath.pop();
1457
- }
1605
+ await this.executePipelineStep(nextContext);
1458
1606
  return nextContext.response;
1459
- };
1607
+ });
1460
1608
  const response = await item.dispatch(event);
1461
1609
  if (response) {
1462
1610
  context.response = response;
@@ -1465,25 +1613,19 @@ var Router = class Router {
1465
1613
  } catch (e) {
1466
1614
  event.error = createError(e);
1467
1615
  await this.hookManager.trigger(HookName.ERROR, event);
1468
- } finally {
1469
- event._next = savedNext;
1470
- event._nextCalled = savedNextCalled;
1471
1616
  }
1472
1617
  context.stackIndex++;
1473
1618
  context.step = RouterPipelineStep.CHILD_AFTER;
1474
- return this.executePipelineStep(context);
1475
1619
  }
1476
1620
  async executePipelineStepFinish(context) {
1477
1621
  if (context.event.error || context.event.dispatched) return this.hookManager.trigger(HookName.RESPONSE, context.event);
1478
1622
  if (!context.event.dispatched && context.event.routerPath.length === 1 && context.event.method && context.event.method === MethodName.OPTIONS) {
1479
- if (context.event.methodsAllowed.includes(MethodName.GET)) context.event.methodsAllowed.push(MethodName.HEAD);
1480
- distinctArray(context.event.methodsAllowed);
1481
- const options = context.event.methodsAllowed.map((key) => key.toUpperCase()).join(",");
1623
+ if (context.event.methodsAllowed.has(MethodName.GET)) context.event.methodsAllowed.add(MethodName.HEAD);
1624
+ const options = [...context.event.methodsAllowed].map((key) => key.toUpperCase()).join(",");
1482
1625
  const optionsHeaders = new Headers(context.event.response.headers);
1483
1626
  optionsHeaders.set(HeaderName.ALLOW, options);
1484
1627
  context.response = new Response(options, {
1485
1628
  status: context.event.response.status || 200,
1486
- statusText: context.event.response.statusText,
1487
1629
  headers: optionsHeaders
1488
1630
  });
1489
1631
  context.event.dispatched = true;
@@ -1508,7 +1650,10 @@ var Router = class Router {
1508
1650
  event,
1509
1651
  stackIndex: 0
1510
1652
  };
1511
- event.routerPath.push(this.id);
1653
+ event.routerPath.push({
1654
+ name: this.name,
1655
+ options: this._options
1656
+ });
1512
1657
  try {
1513
1658
  await this.executePipelineStep(context);
1514
1659
  } finally {
@@ -1598,8 +1743,8 @@ var Router = class Router {
1598
1743
  else this.use(router);
1599
1744
  return this;
1600
1745
  }
1601
- on(name, fn) {
1602
- return this.hookManager.addListener(name, fn);
1746
+ on(name, fn, priority) {
1747
+ return this.hookManager.addListener(name, fn, priority);
1603
1748
  }
1604
1749
  off(name, fn) {
1605
1750
  if (typeof fn === "undefined") {
@@ -1611,6 +1756,6 @@ var Router = class Router {
1611
1756
  }
1612
1757
  };
1613
1758
  //#endregion
1614
- export { isError as $, useRequestNegotiator as A, appendResponseHeaderDirective as B, defineCoreHandler as C, sendFormat as D, sendRedirect as E, toResponse as F, setResponseCacheHeaders as G, setResponseGone as H, setResponseHeaderContentType as I, HandlerSymbol as J, isPath as K, setResponseHeaderAttachment as L, sendFile as M, sendCreated as N, getRequestAcceptableContentType as O, sendAccepted as P, RoutupError as Q, setResponseContentTypeByFileName as R, defineErrorHandler as S, sendStream as T, createEventStream as U, isResponseGone as V, serializeEventStreamMessage as W, RoutupEvent as X, HandlerType as Y, createError as Z, fromWebHandler as _, getRequestHostName as a, fromNodeHandler as b, getRequestAcceptableLanguages as c, getRequestAcceptableCharset as d, HeaderName as et, getRequestAcceptableCharsets as f, isHandlerOptions as g, isHandler as h, getRequestIP as i, getRequestHeader as j, getRequestAcceptableContentTypes as k, getRequestAcceptableEncoding as l, readBody as m, isPlugin as n, matchRequestContentType as o, isRequestCacheable as p, PathMatcher as q, getRequestProtocol as r, getRequestAcceptableLanguage as s, Router as t, MethodName as tt, getRequestAcceptableEncodings as u, isWebHandler as v, Handler as w, fromNodeMiddleware as x, isWebHandlerProvider as y, appendResponseHeader as z };
1759
+ export { appendResponseHeaderDirective as $, isPath as A, getRequestAcceptableContentTypes as B, isWebHandler as C, defineErrorHandler as D, fromNodeMiddleware as E, RoutupEvent as F, sendAccepted as G, getRequestHeader as H, sendStream as I, isError as J, toResponse as K, sendRedirect as L, HandlerSymbol as M, HandlerType as N, defineCoreHandler as O, DispatcherEvent as P, appendResponseHeader as Q, sendFormat as R, fromWebHandler as S, fromNodeHandler as T, sendFile as U, useRequestNegotiator as V, sendCreated as W, setResponseHeaderAttachment as X, setResponseHeaderContentType as Y, setResponseContentTypeByFileName as Z, getRequestAcceptableCharset as _, PluginInstallError as a, HeaderName as at, isHandler as b, PluginErrorCode as c, getRequestHostName as d, createEventStream as et, matchRequestContentType as f, getRequestAcceptableEncodings as g, getRequestAcceptableEncoding as h, PluginNotInstalledError as i, setResponseCacheHeaders as it, PathMatcher as j, Handler as k, getRequestProtocol as l, getRequestAcceptableLanguages as m, normalizeRouterOptions as n, ErrorSymbol as nt, PluginError as o, MethodName as ot, getRequestAcceptableLanguage as p, createError as q, isPlugin as r, RoutupError as rt, isPluginError as s, Router as t, serializeEventStreamMessage as tt, getRequestIP as u, getRequestAcceptableCharsets as v, isWebHandlerProvider as w, isHandlerOptions as x, isRequestCacheable as y, getRequestAcceptableContentType as z };
1615
1760
 
1616
- //# sourceMappingURL=src-CNoRH9eg.mjs.map
1761
+ //# sourceMappingURL=src-Ck8GklBr.mjs.map