routup 5.0.0-beta.2 → 5.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,12 @@
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 { distinctArray, 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
11
  let MethodName = /* @__PURE__ */ function(MethodName) {
12
12
  MethodName["GET"] = "GET";
@@ -53,6 +53,89 @@ let HeaderName = /* @__PURE__ */ function(HeaderName) {
53
53
  return HeaderName;
54
54
  }({});
55
55
  //#endregion
56
+ //#region src/response/helpers/cache.ts
57
+ function setResponseCacheHeaders(event, options) {
58
+ options = options || {};
59
+ const cacheControls = ["public"].concat(options.cacheControls || []);
60
+ if (options.maxAge !== void 0) cacheControls.push(`max-age=${+options.maxAge}`, `s-maxage=${+options.maxAge}`);
61
+ if (options.modifiedTime) {
62
+ const modifiedTime = typeof options.modifiedTime === "string" ? new Date(options.modifiedTime) : options.modifiedTime;
63
+ event.response.headers.set("last-modified", modifiedTime.toUTCString());
64
+ }
65
+ event.response.headers.set("cache-control", cacheControls.join(", "));
66
+ }
67
+ //#endregion
68
+ //#region src/error/module.ts
69
+ var RoutupError = class extends HTTPError {
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;
115
+ if (typeof message === "string") {
116
+ handle.write({ data: message });
117
+ return;
118
+ }
119
+ const serialized = serializeEventStreamMessage(message);
120
+ if (options?.maxMessageSize !== void 0) {
121
+ if (encoder.encode(serialized).byteLength > options.maxMessageSize) return;
122
+ }
123
+ controller.enqueue(encoder.encode(serialized));
124
+ },
125
+ end() {
126
+ if (closed) return;
127
+ closed = true;
128
+ controller.close();
129
+ },
130
+ response: new Response(stream, {
131
+ status: event.response.status,
132
+ statusText: event.response.statusText,
133
+ headers
134
+ })
135
+ };
136
+ return handle;
137
+ }
138
+ //#endregion
56
139
  //#region src/utils/header.ts
57
140
  function sanitizeHeaderValue(value) {
58
141
  return value.replace(/[\r\n]/g, "");
@@ -193,432 +276,77 @@ function cleanDoubleSlashes(input = "") {
193
276
  return input.replace(/\/+/g, "/");
194
277
  }
195
278
  //#endregion
196
- //#region src/error/is.ts
197
- function isError(input) {
198
- if (!isHTTPError(input)) return false;
199
- return input.name === "RoutupError";
279
+ //#region src/response/helpers/header.ts
280
+ function appendResponseHeader(event, name, value) {
281
+ const { headers } = event.response;
282
+ if (Array.isArray(value)) for (const v of value) headers.append(name, sanitizeHeaderValue(v));
283
+ else headers.append(name, sanitizeHeaderValue(value));
284
+ }
285
+ function appendResponseHeaderDirective(event, name, value) {
286
+ const { headers } = event.response;
287
+ const existing = headers.get(name);
288
+ if (!existing) {
289
+ if (Array.isArray(value)) headers.set(name, sanitizeHeaderValue(value.join("; ")));
290
+ else headers.set(name, sanitizeHeaderValue(value));
291
+ return;
292
+ }
293
+ const directives = existing.split("; ");
294
+ if (Array.isArray(value)) directives.push(...value);
295
+ else directives.push(value);
296
+ const unique = [...new Set(directives)];
297
+ headers.set(name, sanitizeHeaderValue(unique.join("; ")));
200
298
  }
201
299
  //#endregion
202
- //#region src/error/module.ts
203
- var RoutupError = class extends HTTPError {
204
- constructor(input = {}) {
205
- super(input);
206
- this.name = "RoutupError";
300
+ //#region src/response/helpers/utils.ts
301
+ function setResponseContentTypeByFileName(event, fileName) {
302
+ const ext = extname(fileName);
303
+ if (ext) {
304
+ let type = getMimeType(ext.substring(1));
305
+ if (type) {
306
+ const charset = getCharsetForMimeType(type);
307
+ if (charset) type += `; charset=${charset}`;
308
+ event.response.headers.set(HeaderName.CONTENT_TYPE, type);
309
+ }
207
310
  }
208
- };
311
+ }
209
312
  //#endregion
210
- //#region src/error/create.ts
211
- function isNativeError(input) {
212
- return isObject(input) && typeof input.message === "string" && typeof input.name === "string";
313
+ //#region src/response/helpers/header-attachment.ts
314
+ function sanitizeFilename(filename) {
315
+ return filename.replace(/[\r\n]/g, "");
213
316
  }
214
- /**
215
- * Create an internal error object by
216
- * - an existing RoutupError (returned as-is)
217
- * - an HTTPError (wrapped into a RoutupError preserving status)
218
- * - an Error (wrapped preserving message and cause)
219
- * - an options object (statusCode, statusMessage, etc.)
220
- * - a message string
221
- *
222
- * @param input
223
- */
224
- function createError(input) {
225
- if (isError(input)) return input;
226
- if (typeof input === "string") return new RoutupError(input);
227
- if (isHTTPError(input)) return new RoutupError({
228
- message: input.message,
229
- code: input.code,
230
- statusCode: input.statusCode,
231
- statusMessage: input.statusMessage,
232
- redirectURL: input.redirectURL,
233
- cause: input
234
- });
235
- if (isNativeError(input)) return new RoutupError({
236
- message: input.message,
237
- cause: input
238
- });
239
- if (!isObject(input)) return new RoutupError();
240
- const options = { ...input };
241
- if (options.cause === void 0) options.cause = input;
242
- return new RoutupError(options);
317
+ function toAsciiFilename(filename) {
318
+ return filename.replace(/[^\x20-\x7E]/g, "").replace(/"/g, "\\\"");
243
319
  }
244
- //#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;
293
- }
294
- get searchParams() {
295
- if (!this._searchParams) this._searchParams = new URLSearchParams(this._url.search);
296
- return this._searchParams;
297
- }
298
- get response() {
299
- if (!this._response) this._response = {
300
- status: 200,
301
- headers: new Headers()
302
- };
303
- return this._response;
304
- }
305
- get dispatched() {
306
- return this._dispatched;
307
- }
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;
320
+ function encodeRfc5987(filename) {
321
+ return encodeURIComponent(filename).replace(/['()]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`).replace(/\*/g, "%2A");
322
+ }
323
+ function setResponseHeaderAttachment(event, filename) {
324
+ if (typeof filename === "string") setResponseContentTypeByFileName(event, filename);
325
+ let disposition = "attachment";
326
+ if (filename) {
327
+ const sanitized = sanitizeFilename(filename);
328
+ const ascii = toAsciiFilename(sanitized);
329
+ disposition += `; filename="${ascii}"`;
330
+ disposition += `; filename*=UTF-8''${encodeRfc5987(sanitized)}`;
316
331
  }
317
- };
332
+ event.response.headers.set(HeaderName.CONTENT_DISPOSITION, disposition);
333
+ }
318
334
  //#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
- }
335
+ //#region src/response/helpers/header-content-type.ts
336
+ function setResponseHeaderContentType(event, input, ifNotExists) {
337
+ if (ifNotExists) {
338
+ if (event.response.headers.get(HeaderName.CONTENT_TYPE)) return;
339
+ }
340
+ const contentType = getMimeType(input);
341
+ if (contentType) event.response.headers.set(HeaderName.CONTENT_TYPE, contentType);
342
+ }
615
343
  //#endregion
616
344
  //#region src/response/to-response.ts
617
345
  function stripWeakPrefix(etag) {
618
346
  return etag.startsWith("W/") ? etag.slice(2) : etag;
619
347
  }
620
348
  async function applyEtag(body, event, headers) {
621
- const etagFn = findRouterOption("etag", event.routerPath);
349
+ const etagFn = event.routerOptions.etag;
622
350
  if (!etagFn) return void 0;
623
351
  const etag = await etagFn(body);
624
352
  if (!etag) return void 0;
@@ -684,7 +412,6 @@ async function toResponse(value, event) {
684
412
  async function sendAccepted(event, data) {
685
413
  event.response.status = 202;
686
414
  event.response.statusText = "Accepted";
687
- event.dispatched = true;
688
415
  return await toResponse(data ?? "", event);
689
416
  }
690
417
  //#endregion
@@ -692,7 +419,6 @@ async function sendAccepted(event, data) {
692
419
  async function sendCreated(event, data) {
693
420
  event.response.status = 201;
694
421
  event.response.statusText = "Created";
695
- event.dispatched = true;
696
422
  return await toResponse(data ?? "", event);
697
423
  }
698
424
  //#endregion
@@ -718,7 +444,6 @@ async function sendFile(event, options) {
718
444
  contentOptions.start = Number.isFinite(parsedStart) && parsedStart >= 0 ? parsedStart : 0;
719
445
  contentOptions.end = Number.isFinite(parsedEnd) && parsedEnd >= 0 ? Math.min(parsedEnd, stats.size - 1) : stats.size - 1;
720
446
  if (contentOptions.start >= stats.size || contentOptions.start > contentOptions.end) {
721
- event.dispatched = true;
722
447
  const rangeHeaders = new Headers(headers);
723
448
  rangeHeaders.set(HeaderName.CONTENT_RANGE, `bytes */${stats.size}`);
724
449
  return new Response(null, {
@@ -738,113 +463,422 @@ async function sendFile(event, options) {
738
463
  headers.set(HeaderName.ETag, `W/"${stats.size}-${mtime.getTime()}"`);
739
464
  }
740
465
  }
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
- }
466
+ const content = await options.content(contentOptions);
467
+ return new Response(content, {
468
+ status: statusCode,
469
+ statusText: event.response.statusText,
470
+ headers
471
+ });
472
+ }
473
+ //#endregion
474
+ //#region src/request/helpers/header.ts
475
+ function getRequestHeader(event, name) {
476
+ return event.headers.get(name);
477
+ }
478
+ //#endregion
479
+ //#region src/request/helpers/negotiator.ts
480
+ const NEGOTIATOR_KEY = /* @__PURE__ */ Symbol.for("routup:negotiator");
481
+ function headersToPlainObject(headers) {
482
+ const result = {};
483
+ headers.forEach((value, key) => {
484
+ result[key] = value;
485
+ });
486
+ return result;
487
+ }
488
+ function useRequestNegotiator(event) {
489
+ let value = event.store[NEGOTIATOR_KEY];
490
+ if (value) return value;
491
+ value = new Negotiator({ headers: headersToPlainObject(event.headers) });
492
+ event.store[NEGOTIATOR_KEY] = value;
493
+ return value;
494
+ }
495
+ //#endregion
496
+ //#region src/request/helpers/header-accept.ts
497
+ function getRequestAcceptableContentTypes(event) {
498
+ return useRequestNegotiator(event).mediaTypes();
499
+ }
500
+ function getRequestAcceptableContentType(event, input) {
501
+ input = input || [];
502
+ const items = Array.isArray(input) ? input : [input];
503
+ if (items.length === 0) return getRequestAcceptableContentTypes(event).shift();
504
+ if (!getRequestHeader(event, HeaderName.ACCEPT)) return items[0];
505
+ let polluted = false;
506
+ const mimeTypes = [];
507
+ for (const item of items) {
508
+ const mimeType = getMimeType(item);
509
+ if (mimeType) mimeTypes.push(mimeType);
510
+ else polluted = true;
511
+ }
512
+ const matches = useRequestNegotiator(event).mediaTypes(mimeTypes);
513
+ if (matches.length > 0) {
514
+ if (polluted) return items[0];
515
+ return items[mimeTypes.indexOf(matches[0])];
516
+ }
517
+ }
518
+ //#endregion
519
+ //#region src/response/helpers/send-format.ts
520
+ function sendFormat(event, input) {
521
+ const { default: formatDefault, ...formats } = input;
522
+ const contentTypes = Object.keys(formats);
523
+ if (contentTypes.length === 0) return formatDefault();
524
+ const contentType = getRequestAcceptableContentType(event, contentTypes);
525
+ if (contentType && formats[contentType]) return formats[contentType]();
526
+ return formatDefault();
527
+ }
528
+ //#endregion
529
+ //#region src/response/helpers/send-redirect.ts
530
+ function escapeHtml(str) {
531
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
532
+ }
533
+ function isAllowedRedirectUrl(location) {
534
+ if (location.startsWith("//")) return false;
535
+ if (location.startsWith("/") || location.startsWith(".")) return true;
536
+ try {
537
+ const url = new URL(location);
538
+ return url.protocol === "http:" || url.protocol === "https:";
539
+ } catch {
540
+ return true;
541
+ }
542
+ }
543
+ function sendRedirect(event, location, statusCode = 302) {
544
+ if (!isAllowedRedirectUrl(location)) throw new RoutupError({
545
+ statusCode: 400,
546
+ statusMessage: "Invalid redirect URL scheme."
547
+ });
548
+ const sanitizedLocation = sanitizeHeaderValue(location);
549
+ const html = `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${escapeHtml(location)}"></head></html>`;
550
+ const headers = new Headers(event.response.headers);
551
+ headers.set("location", sanitizedLocation);
552
+ headers.set("content-type", "text/html; charset=utf-8");
553
+ headers.delete("content-length");
554
+ return new Response(html, {
555
+ status: statusCode,
556
+ statusText: event.response.statusText,
557
+ headers
558
+ });
559
+ }
560
+ //#endregion
561
+ //#region src/response/helpers/send-stream.ts
562
+ function sendStream(event, stream) {
563
+ const { status, statusText, headers } = event.response;
564
+ return new Response(stream, {
565
+ status,
566
+ statusText,
567
+ headers
568
+ });
569
+ }
570
+ //#endregion
571
+ //#region src/event/module.ts
572
+ var RoutupEvent = class {
573
+ request;
574
+ params;
575
+ path;
576
+ method;
577
+ mountPath;
578
+ headers;
579
+ searchParams;
580
+ response;
581
+ store;
582
+ _context;
583
+ _routerOptions;
584
+ constructor(context) {
585
+ this._context = context;
586
+ this.request = context.request;
587
+ this.params = context.params;
588
+ this.path = context.path;
589
+ this.method = context.method;
590
+ this.mountPath = context.mountPath;
591
+ this.headers = context.headers;
592
+ this.searchParams = context.searchParams;
593
+ this.response = context.response;
594
+ this.store = context.store;
595
+ }
596
+ get routerOptions() {
597
+ if (!this._routerOptions) this._routerOptions = this._context.routerOptions();
598
+ return this._routerOptions;
599
+ }
600
+ async next(error) {
601
+ return this._context.next(this, error);
602
+ }
603
+ };
604
+ //#endregion
605
+ //#region src/dispatcher/module.ts
606
+ var DispatcherEvent = class {
607
+ request;
608
+ params;
609
+ path;
610
+ method;
611
+ /**
612
+ * Collected allowed methods (for OPTIONS).
613
+ */
614
+ methodsAllowed;
615
+ mountPath;
616
+ error;
617
+ routerPath;
618
+ _dispatched;
619
+ _response;
620
+ _store;
621
+ /**
622
+ * Cached parsed URL (avoids double-parsing).
623
+ */
624
+ _url;
625
+ /**
626
+ * Continuation function for middleware onion model.
627
+ */
628
+ _next;
629
+ /**
630
+ * Whether _next has already been called (guard against double-invocation).
631
+ */
632
+ _nextCalled;
633
+ /**
634
+ * The cached result of the next handler.
635
+ */
636
+ _nextResult;
637
+ constructor(request) {
638
+ this.request = request;
639
+ this._url = new FastURL(request.url);
640
+ this.method = request.method;
641
+ this.path = this._url.pathname;
642
+ this.mountPath = "/";
643
+ this.params = {};
644
+ this.routerPath = [];
645
+ this.methodsAllowed = [];
646
+ this._dispatched = false;
647
+ this._nextCalled = false;
648
+ }
649
+ get response() {
650
+ if (!this._response) this._response = {
651
+ status: 200,
652
+ headers: new Headers()
653
+ };
654
+ return this._response;
655
+ }
656
+ get dispatched() {
657
+ return this._dispatched;
658
+ }
659
+ set dispatched(value) {
660
+ this._dispatched = value;
661
+ }
662
+ async next(event, error) {
663
+ if (this._nextCalled) return this._nextResult;
664
+ this._nextCalled = true;
665
+ if (this._next) this._nextResult = this._next(event, error);
666
+ return this._nextResult;
667
+ }
668
+ setNext(fn) {
669
+ if (fn) this._next = async (event, error) => {
670
+ return toResponse(await fn(error), event);
671
+ };
672
+ else this._next = void 0;
673
+ this._nextCalled = false;
674
+ this._nextResult = void 0;
675
+ }
676
+ build() {
677
+ return new RoutupEvent({
678
+ request: this.request,
679
+ params: this.params,
680
+ path: this.path,
681
+ method: this.method,
682
+ mountPath: this.mountPath,
683
+ headers: this.request.headers,
684
+ searchParams: new URLSearchParams(this._url.search),
685
+ response: this.response,
686
+ store: this.store,
687
+ routerOptions: () => this.resolveOptions(),
688
+ next: (event, error) => this.next(event, error)
689
+ });
690
+ }
691
+ get store() {
692
+ if (!this._store) this._store = Object.create(null);
693
+ return this._store;
694
+ }
695
+ resolveOptions() {
696
+ const resolved = {
697
+ trustProxy: () => false,
698
+ subdomainOffset: 2,
699
+ etag: buildEtagFn(),
700
+ proxyIpMax: 0
701
+ };
702
+ for (let i = 0; i < this.routerPath.length; i++) {
703
+ const node = this.routerPath[i];
704
+ const entries = Object.entries(node.options);
705
+ for (const entry of entries) {
706
+ const [key, value] = entry;
707
+ if (typeof value !== "undefined") resolved[key] = value;
708
+ }
709
+ }
710
+ return resolved;
711
+ }
712
+ };
749
713
  //#endregion
750
- //#region src/request/helpers/header.ts
751
- function getRequestHeader(event, name) {
752
- return event.headers.get(name);
714
+ //#region src/error/is.ts
715
+ function isError(input) {
716
+ if (!isHTTPError(input)) return false;
717
+ return input.name === "RoutupError";
753
718
  }
754
719
  //#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;
720
+ //#region src/error/create.ts
721
+ function isNativeError(input) {
722
+ return isObject(input) && typeof input.message === "string" && typeof input.name === "string";
763
723
  }
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;
724
+ /**
725
+ * Create an internal error object by
726
+ * - an existing RoutupError (returned as-is)
727
+ * - an HTTPError (wrapped into a RoutupError preserving status)
728
+ * - an Error (wrapped preserving message and cause)
729
+ * - an options object (statusCode, statusMessage, etc.)
730
+ * - a message string
731
+ *
732
+ * @param input
733
+ */
734
+ function createError(input) {
735
+ if (isError(input)) return input;
736
+ if (typeof input === "string") return new RoutupError(input);
737
+ if (isHTTPError(input)) return new RoutupError({
738
+ message: input.message,
739
+ code: input.code,
740
+ statusCode: input.statusCode,
741
+ statusMessage: input.statusMessage,
742
+ redirectURL: input.redirectURL,
743
+ cause: input
744
+ });
745
+ if (isNativeError(input)) return new RoutupError({
746
+ message: input.message,
747
+ cause: input
748
+ });
749
+ if (!isObject(input)) return new RoutupError();
750
+ const options = { ...input };
751
+ if (options.cause === void 0) options.cause = input;
752
+ return new RoutupError(options);
770
753
  }
771
754
  //#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;
755
+ //#region src/handler/constants.ts
756
+ let HandlerType = /* @__PURE__ */ function(HandlerType) {
757
+ HandlerType["CORE"] = "core";
758
+ HandlerType["ERROR"] = "error";
759
+ return HandlerType;
760
+ }({});
761
+ const HandlerSymbol = /* @__PURE__ */ Symbol.for("Handler");
762
+ //#endregion
763
+ //#region src/hook/constants.ts
764
+ let HookName = /* @__PURE__ */ function(HookName) {
765
+ HookName["REQUEST"] = "request";
766
+ HookName["RESPONSE"] = "response";
767
+ HookName["ERROR"] = "error";
768
+ HookName["CHILD_MATCH"] = "childMatch";
769
+ HookName["CHILD_DISPATCH_BEFORE"] = "childDispatchBefore";
770
+ HookName["CHILD_DISPATCH_AFTER"] = "childDispatchAfter";
771
+ return HookName;
772
+ }({});
773
+ //#endregion
774
+ //#region src/hook/module.ts
775
+ var HookManager = class {
776
+ items;
777
+ constructor() {
778
+ this.items = {};
787
779
  }
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])];
780
+ addListener(name, fn) {
781
+ this.items[name] = this.items[name] || [];
782
+ this.items[name].push(fn);
783
+ return () => {
784
+ this.removeListener(name, fn);
785
+ };
792
786
  }
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
- }
787
+ removeListener(name, fn) {
788
+ if (!this.items[name]) return;
789
+ if (typeof fn === "undefined") {
790
+ delete this.items[name];
791
+ return;
792
+ }
793
+ if (typeof fn === "function") {
794
+ const index = this.items[name].indexOf(fn);
795
+ if (index !== -1) this.items[name].splice(index, 1);
796
+ }
797
+ if (this.items[name].length === 0) delete this.items[name];
798
+ }
799
+ async trigger(name, event) {
800
+ if (!this.items[name] || this.items[name].length === 0) return;
801
+ try {
802
+ for (let i = 0; i < this.items[name].length; i++) {
803
+ const listener = this.items[name][i];
804
+ await this.triggerListener(name, event, listener);
805
+ if (event.dispatched) {
806
+ if (event.error) event.error = void 0;
807
+ return;
808
+ }
809
+ }
810
+ } catch (e) {
811
+ event.error = e;
812
+ if (!this.isErrorListenerHook(name)) {
813
+ await this.trigger(HookName.ERROR, event);
814
+ if (event.dispatched) {
815
+ if (event.error) event.error = void 0;
816
+ }
817
+ }
818
+ }
819
+ }
820
+ triggerListener(name, event, listener) {
821
+ if (this.isErrorListenerHook(name)) {
822
+ if (event.error) return listener(event);
823
+ return;
824
+ }
825
+ return listener(event);
826
+ }
827
+ isErrorListenerHook(input) {
828
+ return input === HookName.ERROR;
829
+ }
830
+ };
804
831
  //#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;
832
+ //#region src/path/matcher.ts
833
+ function decodeParam(val) {
834
+ /* istanbul ignore next */
835
+ if (typeof val !== "string" || val.length === 0) return val;
812
836
  try {
813
- const url = new URL(location);
814
- return url.protocol === "http:" || url.protocol === "https:";
837
+ return decodeURIComponent(val);
815
838
  } catch {
816
- return true;
839
+ return val;
817
840
  }
818
841
  }
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
- }
842
+ var PathMatcher = class {
843
+ path;
844
+ regexp;
845
+ regexpKeys = [];
846
+ regexpOptions;
847
+ constructor(path, options) {
848
+ this.path = path;
849
+ this.regexpOptions = options || {};
850
+ const regexp = pathToRegexp(path, options);
851
+ this.regexp = regexp.regexp;
852
+ this.regexpKeys = regexp.keys;
853
+ }
854
+ test(path) {
855
+ return this.regexp.test(path);
856
+ }
857
+ exec(path) {
858
+ if (this.path === "/" && this.regexpOptions.end === false) return {
859
+ path: "/",
860
+ params: Object.create(null)
861
+ };
862
+ const match = this.regexp.exec(path);
863
+ if (!match) return;
864
+ const params = Object.create(null);
865
+ for (let i = 1; i < match.length; i++) {
866
+ const key = this.regexpKeys[i - 1];
867
+ if (!key) continue;
868
+ const prop = key.name;
869
+ const val = decodeParam(match[i]);
870
+ if (typeof val !== "undefined") params[prop] = val;
871
+ }
872
+ return {
873
+ path: match[0],
874
+ params
875
+ };
876
+ }
877
+ };
838
878
  //#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
- });
879
+ //#region src/path/utils.ts
880
+ function isPath(input) {
881
+ return typeof input === "string";
848
882
  }
849
883
  //#endregion
850
884
  //#region src/handler/module.ts
@@ -884,10 +918,11 @@ var Handler = class {
884
918
  let response;
885
919
  try {
886
920
  let result;
921
+ const handlerEvent = event.build();
887
922
  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);
923
+ if (event.error) result = await this.config.fn(event.error, handlerEvent);
924
+ } else result = await this.config.fn(handlerEvent);
925
+ response = await toResponse(result, handlerEvent);
891
926
  if (response) event.dispatched = true;
892
927
  } catch (e) {
893
928
  event.error = isError(e) ? e : createError(e);
@@ -1028,7 +1063,7 @@ function createNodeBridge(handler, isMiddleware) {
1028
1063
  if (!node?.req || !node?.res) throw new RoutupError("fromNodeHandler/fromNodeMiddleware requires a Node.js runtime.");
1029
1064
  const req = node.req;
1030
1065
  const res = node.res;
1031
- if ((isMiddleware ? await callMiddleware(handler, req, res) : await callHandler(handler, req, res)) === kHandled) event.dispatched = true;
1066
+ if ((isMiddleware ? await callMiddleware(handler, req, res) : await callHandler(handler, req, res)) === kHandled) return null;
1032
1067
  }) });
1033
1068
  }
1034
1069
  /**
@@ -1179,7 +1214,7 @@ function matchRequestContentType(event, contentType) {
1179
1214
  function getRequestHostName(event, options = {}) {
1180
1215
  let trustProxy;
1181
1216
  if (typeof options.trustProxy !== "undefined") trustProxy = buildTrustProxyFn(options.trustProxy);
1182
- else trustProxy = findRouterOption("trustProxy", event.routerPath);
1217
+ else trustProxy = event.routerOptions.trustProxy;
1183
1218
  let hostname = event.headers.get(HeaderName.X_FORWARDED_HOST);
1184
1219
  if (!hostname || !event.request.ip || !trustProxy(event.request.ip, 0)) hostname = event.headers.get(HeaderName.HOST);
1185
1220
  else if (hostname && hostname.includes(",")) hostname = hostname.substring(0, hostname.indexOf(",")).trimEnd();
@@ -1202,7 +1237,7 @@ function getRequestHostName(event, options = {}) {
1202
1237
  function getRequestIP(event, options = {}) {
1203
1238
  let trustProxy;
1204
1239
  if (typeof options.trustProxy !== "undefined") trustProxy = buildTrustProxyFn(options.trustProxy);
1205
- else trustProxy = findRouterOption("trustProxy", event.routerPath);
1240
+ else trustProxy = event.routerOptions.trustProxy;
1206
1241
  const socketAddr = event.request.ip;
1207
1242
  if (!socketAddr) return;
1208
1243
  const forwarded = event.headers.get(HeaderName.X_FORWARDED_FOR);
@@ -1222,7 +1257,7 @@ function getRequestIP(event, options = {}) {
1222
1257
  function getRequestProtocol(event, options = {}) {
1223
1258
  let trustProxy;
1224
1259
  if (typeof options.trustProxy !== "undefined") trustProxy = buildTrustProxyFn(options.trustProxy);
1225
- else trustProxy = findRouterOption("trustProxy", event.routerPath);
1260
+ else trustProxy = event.routerOptions.trustProxy;
1226
1261
  let protocol;
1227
1262
  try {
1228
1263
  if (new URL(event.request.url).protocol === "https:") protocol = "https";
@@ -1246,7 +1281,7 @@ function isPlugin(input) {
1246
1281
  return typeof input.install === "function" && input.install.length === 1;
1247
1282
  }
1248
1283
  //#endregion
1249
- //#region src/router-options/normalize.ts
1284
+ //#region src/router/options.ts
1250
1285
  function normalizeRouterOptions(input) {
1251
1286
  if (typeof input.etag !== "undefined") input.etag = buildEtagFn(input.etag);
1252
1287
  if (typeof input.trustProxy !== "undefined") input.trustProxy = buildTrustProxyFn(input.trustProxy);
@@ -1266,10 +1301,6 @@ let RouterPipelineStep = /* @__PURE__ */ function(RouterPipelineStep) {
1266
1301
  }({});
1267
1302
  //#endregion
1268
1303
  //#region src/router/utils.ts
1269
- let nextId = 0;
1270
- function generateRouterID() {
1271
- return ++nextId;
1272
- }
1273
1304
  function isRouterInstance(input) {
1274
1305
  return isInstance(input, RouterSymbol);
1275
1306
  }
@@ -1288,10 +1319,6 @@ function acceptsJson(request) {
1288
1319
  var Router = class Router {
1289
1320
  "@instanceof" = RouterSymbol;
1290
1321
  /**
1291
- * An identifier for the router instance.
1292
- */
1293
- id;
1294
- /**
1295
1322
  * A label for the router instance.
1296
1323
  */
1297
1324
  name;
@@ -1313,12 +1340,15 @@ var Router = class Router {
1313
1340
  * @protected
1314
1341
  */
1315
1342
  hookManager;
1316
- constructor(options = {}) {
1317
- this.id = generateRouterID();
1318
- this.name = options.name;
1343
+ /**
1344
+ * Normalized options for this router instance.
1345
+ */
1346
+ _options;
1347
+ constructor(input = {}) {
1348
+ this.name = input.name;
1319
1349
  this.hookManager = new HookManager();
1320
- this.setPath(options.path);
1321
- setRouterOptions(this.id, normalizeRouterOptions(options));
1350
+ this._options = normalizeRouterOptions(input);
1351
+ this.setPath(input.path);
1322
1352
  }
1323
1353
  matchPath(path) {
1324
1354
  if (this.pathMatcher) return this.pathMatcher.test(path);
@@ -1332,11 +1362,11 @@ var Router = class Router {
1332
1362
  this.pathMatcher = new PathMatcher(withLeadingSlash(withoutTrailingSlash(`${value}`)), { end: false });
1333
1363
  }
1334
1364
  /**
1335
- * Public entry point — creates a RoutupEvent from the request,
1365
+ * Public entry point — creates a DispatcherEvent from the request,
1336
1366
  * runs the pipeline, and returns a Response (with 404/500 fallbacks).
1337
1367
  */
1338
1368
  async fetch(request) {
1339
- const event = new RoutupEvent(request);
1369
+ const event = new DispatcherEvent(request);
1340
1370
  let response;
1341
1371
  try {
1342
1372
  response = await this.dispatch(event);
@@ -1438,25 +1468,18 @@ var Router = class Router {
1438
1468
  }
1439
1469
  const item = this.stack[context.stackIndex];
1440
1470
  const { event } = context;
1441
- const savedNext = event._next;
1442
- const savedNextCalled = event._nextCalled;
1443
1471
  try {
1444
- event._nextCalled = false;
1445
- event._next = async () => {
1472
+ event.setNext(async (error) => {
1473
+ if (error) event.error = createError(error);
1446
1474
  const nextContext = {
1447
1475
  step: RouterPipelineStep.LOOKUP,
1448
1476
  event,
1449
1477
  stackIndex: context.stackIndex + 1,
1450
1478
  response: void 0
1451
1479
  };
1452
- event.routerPath.push(this.id);
1453
- try {
1454
- await this.executePipelineStep(nextContext);
1455
- } finally {
1456
- event.routerPath.pop();
1457
- }
1480
+ await this.executePipelineStep(nextContext);
1458
1481
  return nextContext.response;
1459
- };
1482
+ });
1460
1483
  const response = await item.dispatch(event);
1461
1484
  if (response) {
1462
1485
  context.response = response;
@@ -1465,9 +1488,6 @@ var Router = class Router {
1465
1488
  } catch (e) {
1466
1489
  event.error = createError(e);
1467
1490
  await this.hookManager.trigger(HookName.ERROR, event);
1468
- } finally {
1469
- event._next = savedNext;
1470
- event._nextCalled = savedNextCalled;
1471
1491
  }
1472
1492
  context.stackIndex++;
1473
1493
  context.step = RouterPipelineStep.CHILD_AFTER;
@@ -1508,7 +1528,10 @@ var Router = class Router {
1508
1528
  event,
1509
1529
  stackIndex: 0
1510
1530
  };
1511
- event.routerPath.push(this.id);
1531
+ event.routerPath.push({
1532
+ name: this.name,
1533
+ options: this._options
1534
+ });
1512
1535
  try {
1513
1536
  await this.executePipelineStep(context);
1514
1537
  } finally {
@@ -1611,6 +1634,6 @@ var Router = class Router {
1611
1634
  }
1612
1635
  };
1613
1636
  //#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 };
1637
+ export { setResponseCacheHeaders as $, createError as A, getRequestHeader as B, defineErrorHandler as C, PathMatcher as D, isPath as E, sendRedirect as F, setResponseHeaderContentType as G, sendCreated as H, sendFormat as I, appendResponseHeader as J, setResponseHeaderAttachment as K, getRequestAcceptableContentType as L, DispatcherEvent as M, RoutupEvent as N, HandlerSymbol as O, sendStream as P, RoutupError as Q, getRequestAcceptableContentTypes as R, fromNodeMiddleware as S, Handler as T, sendAccepted as U, sendFile as V, toResponse as W, createEventStream as X, appendResponseHeaderDirective as Y, serializeEventStreamMessage as Z, isHandlerOptions as _, getRequestIP as a, isWebHandlerProvider as b, getRequestAcceptableLanguage as c, getRequestAcceptableEncodings as d, HeaderName as et, getRequestAcceptableCharset as f, isHandler as g, readBody as h, getRequestProtocol as i, isError as j, HandlerType as k, getRequestAcceptableLanguages as l, isRequestCacheable as m, normalizeRouterOptions as n, getRequestHostName as o, getRequestAcceptableCharsets as p, setResponseContentTypeByFileName as q, isPlugin as r, matchRequestContentType as s, Router as t, MethodName as tt, getRequestAcceptableEncoding as u, fromWebHandler as v, defineCoreHandler as w, fromNodeHandler as x, isWebHandler as y, useRequestNegotiator as z };
1615
1638
 
1616
- //# sourceMappingURL=src-CNoRH9eg.mjs.map
1639
+ //# sourceMappingURL=src-Tr9cHQaV.mjs.map