routup 6.0.0-beta.0 → 6.0.0-beta.2

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.
@@ -20,11 +20,9 @@ const DEFAULT_MAX_SIZE = 1024;
20
20
  * `ICache` (e.g. wrapping `lru-cache`) and pass it via the router's
21
21
  * `BaseRouterOptions.cache` slot.
22
22
  */
23
- var LruCache = class LruCache {
24
- options;
23
+ var LruCache = class {
25
24
  inner;
26
25
  constructor(options = {}) {
27
- this.options = options;
28
26
  this.inner = new QuickLRU({ maxSize: options.maxSize ?? DEFAULT_MAX_SIZE });
29
27
  }
30
28
  get(key) {
@@ -39,9 +37,6 @@ var LruCache = class LruCache {
39
37
  clear() {
40
38
  this.inner.clear();
41
39
  }
42
- clone() {
43
- return new LruCache({ ...this.options });
44
- }
45
40
  };
46
41
  //#endregion
47
42
  //#region src/constants.ts
@@ -815,16 +810,17 @@ var DispatcherEvent = class {
815
810
  error;
816
811
  /**
817
812
  * Options of the App currently dispatching this event. Set on
818
- * entry to `App.dispatch` and restored on exit (so nested apps
819
- * temporarily override). Initialized to `{}` so consumers
820
- * reading before any dispatch get a valid (empty) shape.
813
+ * entry to `App.dispatch` and restored on exit so re-entrant
814
+ * dispatch calls leave the caller's view intact. Initialized to
815
+ * `{}` so consumers reading before any dispatch get a valid
816
+ * (empty) shape.
821
817
  */
822
818
  appOptions;
823
819
  /**
824
- * `true` while at least one `App.dispatch` is on the call stack
825
- * for this event. `App.dispatch` reads this on entry to derive
826
- * `isRoot` and writes it on entry/exit so nested calls see it
827
- * already set.
820
+ * `true` while an `App.dispatch` call is on the stack for this
821
+ * event. `App.dispatch` reads this on entry to derive `isRoot`
822
+ * and writes it on entry/exit so re-entrant calls behave
823
+ * correctly.
828
824
  */
829
825
  isDispatching;
830
826
  _dispatched;
@@ -950,148 +946,12 @@ const HandlerType = {
950
946
  };
951
947
  const HandlerSymbol = Symbol.for("Handler");
952
948
  //#endregion
953
- //#region src/hook/constants.ts
954
- const HookName = {
955
- /**
956
- * Fired at the start of `App.dispatch`, before the pipeline walk.
957
- * Once per router per request.
958
- */
959
- START: "start",
960
- /**
961
- * Fired at the end of `App.dispatch`, after the pipeline walk
962
- * (and OPTIONS auto-Allow synthesis) completes. Once per router per
963
- * request.
964
- */
965
- END: "end",
966
- ERROR: "error",
967
- CHILD_MATCH: "childMatch",
968
- CHILD_DISPATCH_BEFORE: "childDispatchBefore",
969
- CHILD_DISPATCH_AFTER: "childDispatchAfter"
970
- };
971
- //#endregion
972
- //#region src/hook/module.ts
973
- var Hooks = class Hooks {
974
- items;
975
- /**
976
- * Derived bit: `true` iff at least one entry exists across all
977
- * hook names. Maintained on every `addListener` / `removeListener`
978
- * so the dispatch hot path can short-circuit on a single boolean
979
- * read rather than per-name lookup. Apps that never register a
980
- * hook (the common case) pay one boolean check per request
981
- * instead of a property access per pipeline step.
982
- */
983
- _hasAny;
984
- constructor() {
985
- this.items = {};
986
- this._hasAny = false;
987
- }
988
- hasAny() {
989
- return this._hasAny;
990
- }
991
- hasListeners(name) {
992
- if (!this._hasAny) return false;
993
- return this.items[name] !== void 0;
994
- }
995
- addListener(name, fn, priority = 0) {
996
- this.items[name] = this.items[name] || [];
997
- const entry = {
998
- fn,
999
- priority
1000
- };
1001
- let i = 0;
1002
- while (i < this.items[name].length && this.items[name][i].priority >= priority) i++;
1003
- this.items[name].splice(i, 0, entry);
1004
- this._hasAny = true;
1005
- return () => {
1006
- this.removeListener(name, fn);
1007
- };
1008
- }
1009
- removeListener(name, fn) {
1010
- if (!this.items[name]) return;
1011
- if (typeof fn === "undefined") {
1012
- delete this.items[name];
1013
- this.recomputeHasAny();
1014
- return;
1015
- }
1016
- if (typeof fn === "function") {
1017
- const index = this.items[name].findIndex((entry) => entry.fn === fn);
1018
- if (index !== -1) this.items[name].splice(index, 1);
1019
- }
1020
- if (this.items[name].length === 0) delete this.items[name];
1021
- this.recomputeHasAny();
1022
- }
1023
- /**
1024
- * Recompute `_hasAny` from the current `items` map. O(k) where k
1025
- * is the number of distinct hook names ever registered (≤ ~6) —
1026
- * effectively O(1). Called from `removeListener` so the fast-path
1027
- * flag stays in sync with registration state.
1028
- */
1029
- recomputeHasAny() {
1030
- for (const name in this.items) if (this.items[name] && this.items[name].length > 0) {
1031
- this._hasAny = true;
1032
- return;
1033
- }
1034
- this._hasAny = false;
1035
- }
1036
- /**
1037
- * Create a new `Hooks` instance seeded with the same listeners as this
1038
- * one.
1039
- *
1040
- * Listener functions are shared by reference; priority and ordering are
1041
- * preserved. Future mutations on the returned instance do not affect this
1042
- * one (and vice versa).
1043
- */
1044
- clone() {
1045
- const next = new Hooks();
1046
- const names = Object.keys(this.items);
1047
- for (const name of names) {
1048
- const entries = this.items[name];
1049
- for (const entry of entries) next.addListener(name, entry.fn, entry.priority);
1050
- }
1051
- return next;
1052
- }
1053
- async trigger(name, event) {
1054
- if (!this.items[name] || this.items[name].length === 0) return;
1055
- try {
1056
- for (let i = 0; i < this.items[name].length; i++) {
1057
- const { fn } = this.items[name][i];
1058
- await this.triggerListener(name, event, fn);
1059
- if (event.dispatched) {
1060
- if (event.error) event.error = void 0;
1061
- return;
1062
- }
1063
- }
1064
- } catch (e) {
1065
- if (!event.error) event.error = createError(e);
1066
- if (!this.isErrorListenerHook(name)) {
1067
- await this.trigger(HookName.ERROR, event);
1068
- if (event.dispatched) {
1069
- if (event.error) event.error = void 0;
1070
- }
1071
- }
1072
- }
1073
- }
1074
- triggerListener(name, event, listener) {
1075
- if (this.isErrorListenerHook(name)) {
1076
- if (event.error) return listener(event);
1077
- return;
1078
- }
1079
- return listener(event);
1080
- }
1081
- isErrorListenerHook(input) {
1082
- return input === HookName.ERROR;
1083
- }
1084
- };
1085
- //#endregion
1086
949
  //#region src/handler/module.ts
1087
950
  var Handler = class {
1088
951
  config;
1089
- hooks;
1090
952
  method;
1091
953
  constructor(handler) {
1092
954
  this.config = handler;
1093
- this.hooks = new Hooks();
1094
- this.mountHooks();
1095
955
  if (typeof handler.path === "string") this.config.path = withLeadingSlash(handler.path);
1096
956
  this.method = this.config.method ? toMethodName(this.config.method) : void 0;
1097
957
  markInstanceof(this, HandlerSymbol);
@@ -1103,15 +963,12 @@ var Handler = class {
1103
963
  return this.config.path;
1104
964
  }
1105
965
  async dispatch(event) {
1106
- if (this.hooks.hasListeners(HookName.CHILD_DISPATCH_BEFORE)) {
1107
- await this.hooks.trigger(HookName.CHILD_DISPATCH_BEFORE, event);
1108
- if (event.dispatched) return;
1109
- }
1110
966
  let response;
967
+ let handlerEvent;
968
+ let cleanupParentListener;
1111
969
  try {
1112
970
  const effectiveTimeout = this.resolveTimeout(event.appOptions);
1113
971
  let childController;
1114
- let cleanupParentListener;
1115
972
  if (effectiveTimeout) {
1116
973
  const parentSignal = event.signal;
1117
974
  childController = new AbortController();
@@ -1122,41 +979,42 @@ var Handler = class {
1122
979
  cleanupParentListener = () => parentSignal.removeEventListener("abort", onAbort);
1123
980
  }
1124
981
  }
1125
- const handlerEvent = childController ? event.build(childController.signal) : event.build();
1126
- let result;
1127
- try {
1128
- let skipFn = false;
1129
- let invocation;
1130
- if (this.config.type === HandlerType.ERROR) if (event.error) {
1131
- const { fn } = this.config;
1132
- invocation = fn(event.error, handlerEvent);
1133
- } else skipFn = true;
1134
- else {
1135
- const { fn } = this.config;
1136
- invocation = fn(handlerEvent);
1137
- }
1138
- if (skipFn) {} else if (effectiveTimeout) result = await this.executeWithTimeout(() => this.resolveHandlerResult(invocation, handlerEvent), effectiveTimeout, childController);
1139
- else if (isPromise(invocation)) {
1140
- const awaited = await invocation;
1141
- result = typeof awaited === "undefined" ? await this.resolveHandlerResult(void 0, handlerEvent) : awaited;
1142
- } else if (typeof invocation === "undefined") result = await this.resolveHandlerResult(void 0, handlerEvent);
1143
- else result = invocation;
1144
- } finally {
1145
- if (cleanupParentListener) cleanupParentListener();
982
+ handlerEvent = childController ? event.build(childController.signal) : event.build();
983
+ const skipFn = this.config.type === HandlerType.ERROR && !event.error;
984
+ if (!skipFn && this.config.onBefore) await this.config.onBefore(handlerEvent);
985
+ let invocation;
986
+ if (skipFn) {} else if (this.config.type === HandlerType.ERROR) {
987
+ const { fn } = this.config;
988
+ invocation = fn(event.error, handlerEvent);
989
+ } else {
990
+ const { fn } = this.config;
991
+ invocation = fn(handlerEvent);
1146
992
  }
993
+ let result;
994
+ if (skipFn) {} else if (effectiveTimeout) result = await this.executeWithTimeout(() => this.resolveHandlerResult(invocation, handlerEvent), effectiveTimeout, childController);
995
+ else if (isPromise(invocation)) {
996
+ const awaited = await invocation;
997
+ result = typeof awaited === "undefined" ? await this.resolveHandlerResult(void 0, handlerEvent) : awaited;
998
+ } else if (typeof invocation === "undefined") result = await this.resolveHandlerResult(void 0, handlerEvent);
999
+ else result = invocation;
1147
1000
  const toResp = toResponse(result, handlerEvent);
1148
1001
  response = isPromise(toResp) ? await toResp : toResp;
1149
1002
  if (response) {
1150
1003
  event.dispatched = true;
1151
1004
  if (this.config.type === HandlerType.ERROR && event.error) event.error = void 0;
1152
1005
  }
1006
+ if (!skipFn && this.config.onAfter) await this.config.onAfter(handlerEvent, response);
1153
1007
  } catch (e) {
1154
1008
  event.error = isError(e) ? e : createError(e);
1155
- if (this.hooks.hasListeners(HookName.ERROR)) await this.hooks.trigger(HookName.ERROR, event);
1156
- if (event.dispatched) event.error = void 0;
1157
- else throw event.error;
1009
+ if (this.config.onError) try {
1010
+ await this.config.onError(event.error, handlerEvent ?? event.build());
1011
+ } catch (innerErr) {
1012
+ event.error = isError(innerErr) ? innerErr : createError(innerErr);
1013
+ }
1014
+ throw event.error;
1015
+ } finally {
1016
+ if (cleanupParentListener) cleanupParentListener();
1158
1017
  }
1159
- if (this.hooks.hasListeners(HookName.CHILD_DISPATCH_AFTER)) await this.hooks.trigger(HookName.CHILD_DISPATCH_AFTER, event);
1160
1018
  return response;
1161
1019
  }
1162
1020
  /**
@@ -1220,11 +1078,6 @@ var Handler = class {
1220
1078
  if (appOptions.handlerTimeoutOverridable) return handlerOverride;
1221
1079
  return Math.min(routerDefault, handlerOverride);
1222
1080
  }
1223
- mountHooks() {
1224
- if (this.config.onBefore) this.hooks.addListener(HookName.CHILD_DISPATCH_BEFORE, this.config.onBefore);
1225
- if (this.config.onAfter) this.hooks.addListener(HookName.CHILD_DISPATCH_AFTER, this.config.onAfter);
1226
- if (this.config.onError) this.hooks.addListener(HookName.ERROR, this.config.onError);
1227
- }
1228
1081
  };
1229
1082
  //#endregion
1230
1083
  //#region src/handler/core/define.ts
@@ -1681,7 +1534,7 @@ function buildRoutePathMatcher(route) {
1681
1534
  * `BaseRouterOptions.cache` to skip the linear walk on repeated
1682
1535
  * requests for the same path. Default is no caching.
1683
1536
  */
1684
- var LinearRouter = class LinearRouter {
1537
+ var LinearRouter = class {
1685
1538
  _routes;
1686
1539
  _matchers;
1687
1540
  cache;
@@ -1722,9 +1575,6 @@ var LinearRouter = class LinearRouter {
1722
1575
  this.cache?.set(path, matches);
1723
1576
  return matches;
1724
1577
  }
1725
- clone() {
1726
- return new LinearRouter({ cache: this.cache?.clone() });
1727
- }
1728
1578
  };
1729
1579
  //#endregion
1730
1580
  //#region src/router/trie/parser.ts
@@ -2131,7 +1981,7 @@ function pushIntoBucket(buckets, methodKey, route) {
2131
1981
  * method's buckets) is the full answer — no need to walk the param
2132
1982
  * branch or collect prefix candidates at intermediate nodes.
2133
1983
  */
2134
- var TrieRouter = class TrieRouter {
1984
+ var TrieRouter = class {
2135
1985
  /**
2136
1986
  * Monotonic counter assigned as the registration `index` on each
2137
1987
  * route — the dispatch loop uses it to preserve registration
@@ -2231,9 +2081,6 @@ var TrieRouter = class TrieRouter {
2231
2081
  this.cache?.set(cacheKey, matches);
2232
2082
  return matches;
2233
2083
  }
2234
- clone() {
2235
- return new TrieRouter({ cache: this.cache?.clone() });
2236
- }
2237
2084
  /**
2238
2085
  * T1: returns the pre-computed candidate list when the request's
2239
2086
  * static spine has no param sibling, no prefix routes, and no
@@ -2345,7 +2192,7 @@ const DEFAULT_THRESHOLD = 30;
2345
2192
  * buffer; on the first `lookup()` call, picks `LinearRouter` or
2346
2193
  * `TrieRouter` based on the registered route count and replays the
2347
2194
  * pending list onto the chosen inner router. Every subsequent call
2348
- * — `add`, `lookup`, `clone` — forwards to the inner.
2195
+ * — `add`, `lookup` — forwards to the inner.
2349
2196
  *
2350
2197
  * Use this when you don't want to commit to a router family up-front
2351
2198
  * (e.g. a library that registers a variable number of routes
@@ -2357,7 +2204,7 @@ const DEFAULT_THRESHOLD = 30;
2357
2204
  * that matters in routup today: linear-vs-trie at the registration-
2358
2205
  * size crossover.
2359
2206
  */
2360
- var SmartRouter = class SmartRouter {
2207
+ var SmartRouter = class {
2361
2208
  inner;
2362
2209
  pending = [];
2363
2210
  threshold;
@@ -2385,12 +2232,6 @@ var SmartRouter = class SmartRouter {
2385
2232
  }
2386
2233
  return this.inner.lookup(path, method);
2387
2234
  }
2388
- clone() {
2389
- return new SmartRouter({
2390
- threshold: this.threshold,
2391
- cache: this.cache?.clone()
2392
- });
2393
- }
2394
2235
  /**
2395
2236
  * Pick the inner router based on the registered route count.
2396
2237
  * `LinearRouter` for tiny tables, `TrieRouter` past the
@@ -2426,18 +2267,6 @@ function normalizeAppOptions(input) {
2426
2267
  //#endregion
2427
2268
  //#region src/app/constants.ts
2428
2269
  const AppSymbol = Symbol.for("App");
2429
- const AppPipelineStep = {
2430
- START: 0,
2431
- LOOKUP: 1,
2432
- CHILD_BEFORE: 2,
2433
- CHILD_DISPATCH: 3,
2434
- CHILD_AFTER: 4,
2435
- FINISH: 5
2436
- };
2437
- const RouteEntryType = {
2438
- APP: "app",
2439
- HANDLER: "handler"
2440
- };
2441
2270
  //#endregion
2442
2271
  //#region src/app/check.ts
2443
2272
  function isAppInstance(input) {
@@ -2463,26 +2292,9 @@ function mergeMatchParams(event, matchParams) {
2463
2292
  ...matchParams
2464
2293
  };
2465
2294
  }
2466
- /**
2467
- * Copy `source[key]` into `target[key]` when the target's value is
2468
- * undefined; return whether a write happened.
2469
- *
2470
- * Bound to a single key `K` per call, so TypeScript can prove the
2471
- * read and write hit the same property's value type — no `as` cast
2472
- * needed. This is the standard escape hatch for the variance trap
2473
- * you'd otherwise hit by writing `target[key] = source[key]` inside
2474
- * a loop where `key: keyof AppOptions` is a *union* (read returns
2475
- * the union of value types, write requires the intersection, which
2476
- * collapses to `never`).
2477
- */
2478
- function copyOptionIfUnset(target, source, key) {
2479
- if (typeof target[key] !== "undefined") return false;
2480
- target[key] = source[key];
2481
- return true;
2482
- }
2483
2295
  var App = class App {
2484
2296
  /**
2485
- * A label for the router instance.
2297
+ * A label for the App instance.
2486
2298
  */
2487
2299
  name;
2488
2300
  /**
@@ -2502,54 +2314,60 @@ var App = class App {
2502
2314
  */
2503
2315
  router;
2504
2316
  /**
2505
- * Lifecycle hook registry.
2506
- *
2507
- * @protected
2508
- */
2509
- hooks;
2510
- /**
2511
2317
  * Normalized options for this App instance.
2512
2318
  *
2513
- * Frozen on construction and on every `extendOptions` update —
2514
- * once published to `event.appOptions` it is shared across all
2515
- * requests, and a handler must not be able to mutate
2516
- * router-global state. `extendOptions` therefore uses a
2517
- * functional update (build a new object, freeze it, replace
2518
- * the slot) rather than mutating in place.
2319
+ * Frozen on construction once published to `event.appOptions`
2320
+ * it is shared across all requests, and a handler must not be
2321
+ * able to mutate router-global state.
2519
2322
  */
2520
2323
  _options;
2521
2324
  /**
2522
- * Registry of installed plugins (name → version) on this router.
2325
+ * Registry of installed plugins (name → version) on this App.
2326
+ *
2327
+ * Read by `use(otherApp)` (via the public `plugins` getter) so
2328
+ * plugin registries merge into the parent at flatten time —
2329
+ * `parent.hasPlugin('foo')` then reflects plugins installed on
2330
+ * apps mounted into it.
2523
2331
  *
2524
2332
  * @protected
2525
2333
  */
2526
- plugins = /* @__PURE__ */ new Map();
2334
+ _plugins;
2527
2335
  /**
2528
2336
  * Every route registered on this App, in registration order.
2529
2337
  *
2530
- * App owns the canonical list the `IRouter` contract has no
2531
- * `routes` field, so cascades / clones / `setRouter` replay
2532
- * read from here instead of asking the router. Routes are
2533
- * pushed alongside every `this.router.add()` via the `register`
2534
- * helper.
2535
- *
2536
- * @protected
2338
+ * Read by `use(otherApp)` to snapshot routes at flatten time.
2339
+ * Late mutations to `_routes` after a flatten do not propagate.
2537
2340
  */
2538
2341
  _routes = [];
2539
2342
  constructor(input = {}) {
2540
2343
  this.name = input.name;
2541
2344
  this._path = input.path;
2542
- this.hooks = input.hooks ?? new Hooks();
2543
- this.plugins = new Map(input.plugins);
2345
+ this._plugins = /* @__PURE__ */ new Map();
2544
2346
  this.router = input.router ?? new LinearRouter();
2545
2347
  this._options = Object.freeze(normalizeAppOptions(input.options ?? {}));
2546
2348
  markInstanceof(this, AppSymbol);
2547
2349
  }
2548
2350
  /**
2351
+ * Public read of the canonical route list. Used by `use(child)`
2352
+ * to snapshot the child's routes at flatten time. Returned
2353
+ * as `readonly` — callers must not mutate.
2354
+ */
2355
+ get routes() {
2356
+ return this._routes;
2357
+ }
2358
+ /**
2359
+ * Public read of the installed-plugin registry. Used by
2360
+ * `use(child)` to merge child plugins into the parent at
2361
+ * flatten time. Returned as `ReadonlyMap` — callers must not
2362
+ * mutate; go through `use(plugin)` to install.
2363
+ */
2364
+ get plugins() {
2365
+ return this._plugins;
2366
+ }
2367
+ /**
2549
2368
  * Register a route with the active router and record it on the
2550
- * App so we can replay it onto a different router later (see
2551
- * `setRouter`) and so cascades / clones have a source of truth
2552
- * independent of the router instance.
2369
+ * App so `setRouter` / `use(child)` can read the canonical list
2370
+ * back.
2553
2371
  *
2554
2372
  * @protected
2555
2373
  */
@@ -2622,199 +2440,6 @@ var App = class App {
2622
2440
  headers
2623
2441
  });
2624
2442
  }
2625
- /**
2626
- * Mount-time option inheritance — fill in any of this App's
2627
- * unset option keys from the supplied parent options. Called by
2628
- * `App.use(child)` after narrowing to `App` via `isAppInstance`.
2629
- *
2630
- * Public so callers don't need to reach into another App's
2631
- * protected fields: the parent passes its options by value and
2632
- * the child decides what to do with them.
2633
- *
2634
- * Shallow per-key merge. App-local concerns — `name`, `path`,
2635
- * `hooks`, `plugins`, `router`, and the router's cache — are
2636
- * deliberately not propagated; they sit on `AppContext`, not
2637
- * inside `AppOptions`.
2638
- *
2639
- * Cascades to any Apps already mounted on this one — a deeper
2640
- * grandchild gets the new keys too. Without this, mounting
2641
- * grandchild → child → parent in that order would leave the
2642
- * grandchild without parent's options (it adopted from child
2643
- * before child had them).
2644
- *
2645
- * Late mutation of the parent's options after this call does NOT
2646
- * propagate. `AppOptions` is configured at construction-and-mount;
2647
- * later changes are not a supported workflow.
2648
- */
2649
- extendOptions(incoming) {
2650
- let next;
2651
- const keys = Object.keys(incoming);
2652
- for (const key of keys) {
2653
- if (typeof this._options[key] !== "undefined") continue;
2654
- if (typeof incoming[key] === "undefined") continue;
2655
- next ??= { ...this._options };
2656
- copyOptionIfUnset(next, incoming, key);
2657
- }
2658
- if (!next) return;
2659
- this._options = Object.freeze(next);
2660
- for (const route of this._routes) if (route.data.type === RouteEntryType.APP && isAppInstance(route.data.data)) route.data.data.extendOptions(this._options);
2661
- }
2662
- async executePipelineStep(context) {
2663
- while (context.step !== AppPipelineStep.FINISH) switch (context.step) {
2664
- case AppPipelineStep.START:
2665
- await this.executePipelineStepStart(context);
2666
- break;
2667
- case AppPipelineStep.LOOKUP:
2668
- await this.executePipelineStepLookup(context);
2669
- break;
2670
- case AppPipelineStep.CHILD_BEFORE:
2671
- await this.executePipelineStepChildBefore(context);
2672
- break;
2673
- case AppPipelineStep.CHILD_DISPATCH:
2674
- await this.executePipelineStepChildDispatch(context);
2675
- break;
2676
- case AppPipelineStep.CHILD_AFTER:
2677
- await this.executePipelineStepChildAfter(context);
2678
- break;
2679
- default:
2680
- context.step = AppPipelineStep.FINISH;
2681
- break;
2682
- }
2683
- await this.executePipelineStepFinish(context);
2684
- }
2685
- async executePipelineStepStart(context) {
2686
- if (this.hooks.hasListeners(HookName.START)) await this.hooks.trigger(HookName.START, context.event);
2687
- if (context.event.dispatched) context.step = AppPipelineStep.FINISH;
2688
- else context.step = AppPipelineStep.LOOKUP;
2689
- }
2690
- async executePipelineStepLookup(context) {
2691
- if (typeof context.matches === "undefined" || context.matchesPath !== context.event.path) {
2692
- context.matches = this.router.lookup(context.event.path, context.event.method);
2693
- context.matchesPath = context.event.path;
2694
- }
2695
- const { matches } = context;
2696
- while (!context.event.dispatched && context.matchIndex < matches.length) {
2697
- const { route } = matches[context.matchIndex];
2698
- if (route.data.type === RouteEntryType.HANDLER) {
2699
- const handler = route.data.data;
2700
- if (context.event.error && handler.type === HandlerType.CORE || !context.event.error && handler.type === HandlerType.ERROR) {
2701
- context.matchIndex++;
2702
- continue;
2703
- }
2704
- const { method } = route;
2705
- if (method) context.event.methodsAllowed.add(method);
2706
- if (!matchHandlerMethod(method, context.event.method)) {
2707
- context.matchIndex++;
2708
- continue;
2709
- }
2710
- }
2711
- if (this.hooks.hasListeners(HookName.CHILD_MATCH)) await this.hooks.trigger(HookName.CHILD_MATCH, context.event);
2712
- if (context.event.dispatched) {
2713
- context.step = AppPipelineStep.FINISH;
2714
- return;
2715
- }
2716
- if (context.event.path !== context.matchesPath) {
2717
- context.matches = void 0;
2718
- context.matchIndex = 0;
2719
- context.step = AppPipelineStep.LOOKUP;
2720
- return;
2721
- }
2722
- context.step = AppPipelineStep.CHILD_BEFORE;
2723
- return;
2724
- }
2725
- context.step = AppPipelineStep.FINISH;
2726
- }
2727
- async executePipelineStepChildBefore(context) {
2728
- if (this.hooks.hasListeners(HookName.CHILD_DISPATCH_BEFORE)) await this.hooks.trigger(HookName.CHILD_DISPATCH_BEFORE, context.event);
2729
- if (context.event.dispatched) {
2730
- context.step = AppPipelineStep.FINISH;
2731
- return;
2732
- }
2733
- if (context.event.path !== context.matchesPath) {
2734
- context.matches = void 0;
2735
- context.matchIndex = 0;
2736
- context.step = AppPipelineStep.LOOKUP;
2737
- return;
2738
- }
2739
- context.step = AppPipelineStep.CHILD_DISPATCH;
2740
- }
2741
- async executePipelineStepChildAfter(context) {
2742
- if (this.hooks.hasListeners(HookName.CHILD_DISPATCH_AFTER)) await this.hooks.trigger(HookName.CHILD_DISPATCH_AFTER, context.event);
2743
- if (context.event.dispatched) context.step = AppPipelineStep.FINISH;
2744
- else context.step = AppPipelineStep.LOOKUP;
2745
- }
2746
- async executePipelineStepChildDispatch(context) {
2747
- const match = context.matches?.[context.matchIndex];
2748
- if (context.event.dispatched || typeof match === "undefined") {
2749
- context.step = AppPipelineStep.FINISH;
2750
- return;
2751
- }
2752
- const { route } = match;
2753
- const { event } = context;
2754
- const savedPath = event.path;
2755
- const savedMountPath = event.mountPath;
2756
- const savedParams = event.params;
2757
- if (route.data.type === RouteEntryType.APP && typeof match.path === "string") {
2758
- event.mountPath = cleanDoubleSlashes(`${event.mountPath}/${match.path}`);
2759
- if (event.path === match.path) event.path = "/";
2760
- else event.path = withLeadingSlash(event.path.substring(match.path.length));
2761
- mergeMatchParams(event, match.params);
2762
- } else if (route.data.type === RouteEntryType.HANDLER && typeof match.path === "string") mergeMatchParams(event, match.params);
2763
- try {
2764
- const parentMatches = context.matches;
2765
- const parentMatchesPath = context.matchesPath;
2766
- const nextMatchIndex = context.matchIndex + 1;
2767
- event.setNext(async (error) => {
2768
- if (error) event.error = createError(error);
2769
- const pathChanged = event.path !== parentMatchesPath;
2770
- const savedStep = context.step;
2771
- const savedMatchIndex = context.matchIndex;
2772
- const savedMatches = context.matches;
2773
- const savedMatchesPath = context.matchesPath;
2774
- context.step = AppPipelineStep.LOOKUP;
2775
- context.matchIndex = pathChanged ? 0 : nextMatchIndex;
2776
- context.matches = pathChanged ? void 0 : parentMatches;
2777
- context.matchesPath = pathChanged ? void 0 : parentMatchesPath;
2778
- try {
2779
- await this.executePipelineStep(context);
2780
- } finally {
2781
- context.step = savedStep;
2782
- context.matchIndex = savedMatchIndex;
2783
- context.matches = savedMatches;
2784
- context.matchesPath = savedMatchesPath;
2785
- }
2786
- return context.response;
2787
- });
2788
- const response = await route.data.data.dispatch(event);
2789
- if (response) {
2790
- context.response = response;
2791
- event.dispatched = true;
2792
- }
2793
- } catch (e) {
2794
- event.error = createError(e);
2795
- if (this.hooks.hasListeners(HookName.ERROR)) await this.hooks.trigger(HookName.ERROR, event);
2796
- }
2797
- if (!event.dispatched) {
2798
- event.path = savedPath;
2799
- event.mountPath = savedMountPath;
2800
- event.params = savedParams;
2801
- }
2802
- context.matchIndex++;
2803
- context.step = AppPipelineStep.CHILD_AFTER;
2804
- }
2805
- async executePipelineStepFinish(context) {
2806
- if (!context.event.error && !context.event.dispatched && context.isRoot && context.event.method === MethodName.OPTIONS) {
2807
- if (context.event.methodsAllowed.has(MethodName.GET)) context.event.methodsAllowed.add(MethodName.HEAD);
2808
- const options = [...context.event.methodsAllowed].map((key) => key.toUpperCase()).join(",");
2809
- const optionsHeaders = new Headers(context.event.response.headers);
2810
- optionsHeaders.set(HeaderName.ALLOW, options);
2811
- context.response = new Response(options, {
2812
- status: context.event.response.status || 200,
2813
- headers: optionsHeaders
2814
- });
2815
- context.event.dispatched = true;
2816
- }
2817
- }
2818
2443
  async dispatch(event) {
2819
2444
  const savedPath = event.path;
2820
2445
  const savedMountPath = event.mountPath;
@@ -2822,17 +2447,23 @@ var App = class App {
2822
2447
  const savedAppOptions = event.appOptions;
2823
2448
  const wasDispatching = event.isDispatching;
2824
2449
  const isRoot = !wasDispatching;
2825
- const context = {
2826
- step: AppPipelineStep.START,
2827
- event,
2828
- isRoot,
2829
- matchIndex: 0
2830
- };
2831
2450
  event.appOptions = this._options;
2832
2451
  event.isDispatching = true;
2452
+ let response;
2833
2453
  try {
2834
- await this.executePipelineStep(context);
2835
- if (this.hooks.hasListeners(HookName.END)) await this.hooks.trigger(HookName.END, event);
2454
+ const matches = this.router.lookup(event.path, event.method);
2455
+ response = await this.runMatches(event, matches, event.path, 0);
2456
+ if (!event.error && !event.dispatched && isRoot && event.method === MethodName.OPTIONS) {
2457
+ if (event.methodsAllowed.has(MethodName.GET)) event.methodsAllowed.add(MethodName.HEAD);
2458
+ const options = [...event.methodsAllowed].map((key) => key.toUpperCase()).join(",");
2459
+ const optionsHeaders = new Headers(event.response.headers);
2460
+ optionsHeaders.set(HeaderName.ALLOW, options);
2461
+ response = new Response(options, {
2462
+ status: event.response.status || 200,
2463
+ headers: optionsHeaders
2464
+ });
2465
+ event.dispatched = true;
2466
+ }
2836
2467
  } finally {
2837
2468
  event.appOptions = savedAppOptions;
2838
2469
  event.isDispatching = wasDispatching;
@@ -2842,7 +2473,57 @@ var App = class App {
2842
2473
  event.params = savedParams;
2843
2474
  }
2844
2475
  }
2845
- return context.response;
2476
+ return response;
2477
+ }
2478
+ /**
2479
+ * Walk the matched routes for the current event, dispatching each
2480
+ * handler in order. Re-entered (recursively) from the `setNext`
2481
+ * continuation so `event.next()` resumes from the next match.
2482
+ */
2483
+ async runMatches(event, matches, matchesPath, startIndex) {
2484
+ let i = startIndex;
2485
+ let response;
2486
+ while (!event.dispatched && i < matches.length) {
2487
+ const match = matches[i];
2488
+ const handler = match.route.data;
2489
+ if (event.error && handler.type === HandlerType.CORE || !event.error && handler.type === HandlerType.ERROR) {
2490
+ i++;
2491
+ continue;
2492
+ }
2493
+ const { method } = match.route;
2494
+ if (method) event.methodsAllowed.add(method);
2495
+ if (!matchHandlerMethod(method, event.method)) {
2496
+ i++;
2497
+ continue;
2498
+ }
2499
+ mergeMatchParams(event, match.params);
2500
+ const savedMountPath = event.mountPath;
2501
+ if (typeof match.path === "string") event.mountPath = match.path;
2502
+ const capturedMatches = matches;
2503
+ const capturedMatchesPath = matchesPath;
2504
+ const nextIndex = i + 1;
2505
+ event.setNext(async (error) => {
2506
+ if (error) event.error = createError(error);
2507
+ const pathChanged = event.path !== capturedMatchesPath;
2508
+ const nextMatches = pathChanged ? this.router.lookup(event.path, event.method) : capturedMatches;
2509
+ const nextMatchesPath = pathChanged ? event.path : capturedMatchesPath;
2510
+ const nextStart = pathChanged ? 0 : nextIndex;
2511
+ return this.runMatches(event, nextMatches, nextMatchesPath, nextStart);
2512
+ });
2513
+ try {
2514
+ const dispatchResponse = await handler.dispatch(event);
2515
+ if (dispatchResponse) {
2516
+ response = dispatchResponse;
2517
+ event.dispatched = true;
2518
+ }
2519
+ } catch (e) {
2520
+ event.error = createError(e);
2521
+ } finally {
2522
+ event.mountPath = savedMountPath;
2523
+ }
2524
+ i++;
2525
+ }
2526
+ return response;
2846
2527
  }
2847
2528
  delete(...input) {
2848
2529
  this.useForMethod(MethodName.DELETE, ...input);
@@ -2889,10 +2570,7 @@ var App = class App {
2889
2570
  this.register({
2890
2571
  path: joinPaths(this._path, path, handler.path),
2891
2572
  method,
2892
- data: {
2893
- type: RouteEntryType.HANDLER,
2894
- data: handler
2895
- }
2573
+ data: handler
2896
2574
  });
2897
2575
  }
2898
2576
  }
@@ -2904,24 +2582,14 @@ var App = class App {
2904
2582
  continue;
2905
2583
  }
2906
2584
  if (isAppInstance(item)) {
2907
- item.extendOptions(this._options);
2908
- this.register({
2909
- path: joinPaths(this._path, path),
2910
- data: {
2911
- type: RouteEntryType.APP,
2912
- data: item
2913
- }
2914
- });
2585
+ this.flatten(item, path);
2915
2586
  continue;
2916
2587
  }
2917
2588
  if (isHandler(item)) {
2918
2589
  this.register({
2919
2590
  path: joinPaths(this._path, path, item.path),
2920
2591
  method: item.method,
2921
- data: {
2922
- type: RouteEntryType.HANDLER,
2923
- data: item
2924
- }
2592
+ data: item
2925
2593
  });
2926
2594
  continue;
2927
2595
  }
@@ -2930,10 +2598,7 @@ var App = class App {
2930
2598
  this.register({
2931
2599
  path: joinPaths(this._path, path, handler.path),
2932
2600
  method: handler.method,
2933
- data: {
2934
- type: RouteEntryType.HANDLER,
2935
- data: handler
2936
- }
2601
+ data: handler
2937
2602
  });
2938
2603
  continue;
2939
2604
  }
@@ -2943,90 +2608,48 @@ var App = class App {
2943
2608
  return this;
2944
2609
  }
2945
2610
  /**
2946
- * Check if a plugin with the given name is installed on this router.
2611
+ * Snapshot a child App's routes and plugin registry into this
2612
+ * one. Each route's path is prefixed with `this._path`, the
2613
+ * supplied mount `path`, and the route's own path (in that
2614
+ * order); the resulting entry is registered on this App's
2615
+ * router. The child app is not retained — late mutations on it
2616
+ * after this call do not propagate.
2617
+ *
2618
+ * @protected
2947
2619
  */
2948
- hasPlugin(name) {
2949
- return this.plugins.has(name);
2620
+ flatten(child, path) {
2621
+ for (const name of child.plugins.keys()) if (this._plugins.has(name)) throw new PluginAlreadyInstalledError(name);
2622
+ for (const [name, version] of child.plugins) this._plugins.set(name, version);
2623
+ for (const route of child.routes) this.register({
2624
+ path: joinPaths(this._path, path, route.path),
2625
+ method: route.method,
2626
+ data: route.data
2627
+ });
2950
2628
  }
2951
2629
  /**
2952
- * Get the version of an installed plugin by name on this router,
2953
- * or `undefined` if the plugin is not installed here.
2630
+ * Check if a plugin with the given name is installed on this App.
2954
2631
  */
2955
- getPluginVersion(name) {
2956
- return this.plugins.get(name);
2957
- }
2958
- install(plugin, context = {}) {
2959
- if (this.plugins.has(plugin.name)) throw new PluginAlreadyInstalledError(plugin.name);
2960
- const router = new App({
2961
- name: plugin.name,
2962
- router: this.router.clone()
2963
- });
2964
- plugin.install(router);
2965
- if (context.path) this.use(context.path, router);
2966
- else this.use(router);
2967
- this.plugins.set(plugin.name, plugin.version);
2968
- return this;
2632
+ hasPlugin(name) {
2633
+ return this._plugins.has(name);
2969
2634
  }
2970
2635
  /**
2971
- * Return a new `App` that mirrors this one but owns independent
2972
- * mountable state.
2973
- *
2974
- * The new router has:
2975
- * - a fresh `stack` array of shallow-copied entries (handlers and child
2976
- * routers are shared by reference; only the wrapping entries are new)
2977
- * - the same `pathMatcher` reference (it is stateless)
2978
- * - a fresh `Hooks` instance seeded with the current listeners
2979
- * - a shallow copy of `_options`
2980
- * - a fresh `plugins` map with the same entries
2981
- *
2982
- * Use this when the same logical router needs to be mounted under
2983
- * multiple paths — each mount can receive its own clone so subsequent
2984
- * mutations on one mount do not bleed into the others.
2636
+ * Get the version of an installed plugin by name, or `undefined`
2637
+ * if the plugin is not installed.
2985
2638
  */
2986
- clone() {
2987
- const next = new App({
2988
- name: this.name,
2989
- path: this._path,
2990
- options: { ...this._options },
2991
- hooks: this.hooks.clone(),
2992
- plugins: this.plugins,
2993
- router: this.router.clone()
2994
- });
2995
- for (const route of this._routes) {
2996
- if (route.data.type === RouteEntryType.APP) {
2997
- next.register({
2998
- path: route.path,
2999
- data: {
3000
- type: RouteEntryType.APP,
3001
- data: route.data.data.clone()
3002
- }
3003
- });
3004
- continue;
3005
- }
3006
- next.register({
3007
- path: route.path,
3008
- method: route.method,
3009
- data: {
3010
- type: RouteEntryType.HANDLER,
3011
- data: route.data.data
3012
- }
3013
- });
3014
- }
3015
- return next;
3016
- }
3017
- on(name, fn, priority) {
3018
- return this.hooks.addListener(name, fn, priority);
2639
+ getPluginVersion(name) {
2640
+ return this._plugins.get(name);
3019
2641
  }
3020
- off(name, fn) {
3021
- if (typeof fn === "undefined") {
3022
- this.hooks.removeListener(name);
3023
- return this;
3024
- }
3025
- this.hooks.removeListener(name, fn);
2642
+ install(plugin, context = {}) {
2643
+ if (this._plugins.has(plugin.name)) throw new PluginAlreadyInstalledError(plugin.name);
2644
+ const scratch = new App({ name: plugin.name });
2645
+ plugin.install(scratch);
2646
+ if (context.path) this.use(context.path, scratch);
2647
+ else this.use(scratch);
2648
+ this._plugins.set(plugin.name, plugin.version);
3026
2649
  return this;
3027
2650
  }
3028
2651
  };
3029
2652
  //#endregion
3030
2653
  export { isError as $, fromWebHandler as A, DispatcherEvent as B, getRequestAcceptableEncodings as C, matchHandlerMethod as D, isRequestCacheable as E, defineErrorHandler as F, getRequestAcceptableContentTypes as G, sendRedirect as H, defineCoreHandler as I, sendFile as J, useRequestNegotiator as K, Handler as L, isWebHandlerProvider as M, fromNodeHandler as N, isHandler as O, fromNodeMiddleware as P, createError as Q, HandlerSymbol as R, getRequestAcceptableEncoding as S, getRequestAcceptableCharsets as T, sendFormat as U, sendStream as V, getRequestAcceptableContentType as W, sendAccepted as X, sendCreated as Y, toResponse as Z, getRequestIP as _, LinearRouter as a, appendResponseHeaderDirective as at, getRequestAcceptableLanguage as b, PluginNotInstalledError as c, AppError as ct, PluginError as d, AppEvent as dt, setResponseHeaderContentType as et, isPluginError as f, HeaderName as ft, getRequestProtocol as g, PathMatcher as h, TrieRouter as i, appendResponseHeader as it, isWebHandler as j, isHandlerOptions as k, PluginInstallError as l, ErrorSymbol as lt, isPath as m, LruCache as mt, normalizeAppOptions as n, setResponseHeaderInline as nt, buildRoutePathMatcher as o, createEventStream as ot, PluginErrorCode as p, MethodName as pt, getRequestHeader as q, SmartRouter as r, setResponseContentTypeByFileName as rt, isPlugin as s, serializeEventStreamMessage as st, App as t, setResponseHeaderAttachment as tt, PluginAlreadyInstalledError as u, setResponseCacheHeaders as ut, getRequestHostName as v, getRequestAcceptableCharset as w, getRequestAcceptableLanguages as x, matchRequestContentType as y, HandlerType as z };
3031
2654
 
3032
- //# sourceMappingURL=src-gmPicCWT.mjs.map
2655
+ //# sourceMappingURL=src-BfsqxIfL.mjs.map