@orkestrel/router 0.0.4 → 0.0.6

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.
@@ -5,6 +5,26 @@ import { RouteEntry } from '../core/index.ts';
5
5
  import { RouterInterface } from '../core/index.ts';
6
6
  import { RouterMatch } from '../core/index.ts';
7
7
 
8
+ /**
9
+ * Compute the registry key for a browser navigation route.
10
+ *
11
+ * @remarks
12
+ * Projects the nested route's path through the core engine's canonical
13
+ * trailing-slash identity, so `/users` and `/users/` replace one another in
14
+ * the Navigator's shared Router.
15
+ *
16
+ * @param entry - The outer Router entry carrying the Navigator route
17
+ * @returns The nested route's canonical path
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * computeNavigationKey({ path: '/users/', meta: { path: '/users/' } }) // '/users'
22
+ * ```
23
+ */
24
+ export declare function computeNavigationKey(entry: RouteEntry<{
25
+ readonly path: string;
26
+ }>): string;
27
+
8
28
  /**
9
29
  * Create a {@link NavigatorInterface} — the headless History/hash navigation
10
30
  * entity composing one core `Router<RouteEntry<Meta>>`.
@@ -1,9 +1,28 @@
1
+ import { canonicalizePath, createRouter, joinPaths } from "../core/index.js";
1
2
  import { createAbort } from "@orkestrel/abort";
2
3
  import { Emitter } from "@orkestrel/emitter";
3
4
  import { isFunction, isString } from "@orkestrel/contract";
4
- import { canonicalizePath, createRouter, joinPaths } from "../core/index.js";
5
5
  //#region src/browser/helpers.ts
6
6
  /**
7
+ * Compute the registry key for a browser navigation route.
8
+ *
9
+ * @remarks
10
+ * Projects the nested route's path through the core engine's canonical
11
+ * trailing-slash identity, so `/users` and `/users/` replace one another in
12
+ * the Navigator's shared Router.
13
+ *
14
+ * @param entry - The outer Router entry carrying the Navigator route
15
+ * @returns The nested route's canonical path
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * computeNavigationKey({ path: '/users/', meta: { path: '/users/' } }) // '/users'
20
+ * ```
21
+ */
22
+ function computeNavigationKey(entry) {
23
+ return canonicalizePath(entry.meta.path);
24
+ }
25
+ /**
7
26
  * Extract the `/`-prefixed pathname from a `location.hash` value — strip the
8
27
  * leading `#` (keeping the route's own leading `/`) and any `?query` suffix.
9
28
  *
@@ -168,22 +187,22 @@ var Navigator = class {
168
187
  this.#guard = options.guard;
169
188
  this.#error = options.error;
170
189
  this.#emitter = new Emitter({
171
- on: options.on,
172
- error: options.error
190
+ ...options.on === void 0 ? {} : { on: options.on },
191
+ ...options.error === void 0 ? {} : { error: options.error }
173
192
  });
174
193
  this.#router = createRouter({
175
194
  entries: options.routes.map((route) => ({
176
195
  path: route.path,
177
196
  meta: route,
178
- name: route.name
197
+ ...route.name === void 0 ? {} : { name: route.name }
179
198
  })),
180
- sensitive: options.sensitive,
181
- key: (entry) => canonicalizePath(entry.meta.path)
199
+ ...options.sensitive === void 0 ? {} : { sensitive: options.sensitive },
200
+ key: computeNavigationKey
182
201
  });
183
202
  this.#fallback = options.fallback ?? options.routes[0]?.path;
184
- this.#hashListener = () => this.#resolve();
185
- this.#popListener = () => this.#resolve();
186
- this.#clickListener = (event) => this.#intercepted(event);
203
+ this.#hashListener = this.#resolve.bind(this);
204
+ this.#popListener = this.#resolve.bind(this);
205
+ this.#clickListener = this.#intercepted.bind(this);
187
206
  }
188
207
  get router() {
189
208
  return this.#router;
@@ -232,7 +251,7 @@ var Navigator = class {
232
251
  path: hit.path,
233
252
  params: hit.params,
234
253
  meta: hit.meta.meta,
235
- name: hit.meta.name
254
+ ...hit.meta.name === void 0 ? {} : { name: hit.meta.name }
236
255
  };
237
256
  }
238
257
  destroy() {
@@ -342,6 +361,6 @@ function createNavigator(options) {
342
361
  return new Navigator(options);
343
362
  }
344
363
  //#endregion
345
- export { Navigator, createNavigator, extractHashPath, findAnchor, resolveLocationPath };
364
+ export { Navigator, computeNavigationKey, createNavigator, extractHashPath, findAnchor, resolveLocationPath };
346
365
 
347
366
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["#router","#emitter","#history","#base","#fallback","#guard","#error","#intercept","#hashListener","#popListener","#clickListener","#resolve","#intercepted","#active","#started","#current","#matchFallback","#navigate","#commit","#guarded","#surface"],"sources":["../../../src/browser/helpers.ts","../../../src/browser/Navigator.ts","../../../src/browser/factories.ts"],"sourcesContent":["// The PURE browser-navigation primitives (AGENTS §4.3 multi-word names — module\n// scope, no entity context). Every one is exported (the centralized-file rule,\n// §5): the `Navigator` composes them, and each has its own unit test. NO `node:*`\n// — DOM-typed only (`Location`, `Event`, `HTMLAnchorElement`), valid under the\n// `src:browser` scoped check (AGENTS §17.7).\n\n/**\n * Extract the `/`-prefixed pathname from a `location.hash` value — strip the\n * leading `#` (keeping the route's own leading `/`) and any `?query` suffix.\n *\n * @remarks\n * The grammar this package matches everywhere is `/`-prefixed (§4 path\n * grammar), so a hash-mode location's `'#/users/7?x'` becomes `'/users/7'`\n * — a hash pattern is expected to start `'#/'`; anything else (an empty hash,\n * or one that does not begin `'#/'`) yields `''` (the `Navigator` then falls\n * back). Total — never throws.\n *\n * @param hash - The raw `window.location.hash` value (e.g. `'#/users/7?x'`)\n * @returns The `/`-prefixed pathname to match, or `''` for an empty / non-`#/` hash\n *\n * @example\n * ```ts\n * extractHashPath('#/users/7?x') // '/users/7'\n * extractHashPath('#/tokens') // '/tokens'\n * extractHashPath('') // '' — the Navigator falls back\n * extractHashPath('#other') // '' — not a `#/` route hash\n * ```\n */\nexport function extractHashPath(hash: string): string {\n\tif (!hash.startsWith('#/')) return ''\n\tconst withoutHash = hash.slice(1)\n\tconst queryIndex = withoutHash.indexOf('?')\n\treturn queryIndex === -1 ? withoutHash : withoutHash.slice(0, queryIndex)\n}\n\n/**\n * Resolve the `/`-prefixed pathname to match for the CURRENT location, in\n * either navigation mode — the one seam `extractHashPath` (hash mode) and\n * history-mode base-stripping share.\n *\n * @remarks\n * Hash mode (`history: false`) reads `location.hash` through\n * {@link extractHashPath}. History mode (`history: true`) reads\n * `location.pathname` and strips a leading `base` prefix when one is\n * configured: `base` itself maps to the root `'/'`; a pathname that is not\n * under `base` is returned unchanged (a base mismatch is not this helper's\n * concern — the `Navigator`'s match then simply misses). Total — never throws.\n *\n * @param location - The `hash` + `pathname` pair to resolve from (accepts a\n * real `Location` or any object shaped the same, for pure unit testing)\n * @param history - The navigation substrate: `false` for hash mode, `true`\n * for history mode\n * @param base - The history-mode path prefix to strip (ignored in hash mode;\n * omit for no prefix)\n * @returns The `/`-prefixed pathname to match\n *\n * @example\n * ```ts\n * resolveLocationPath({ hash: '#/users/7', pathname: '/' }, false) // '/users/7'\n * resolveLocationPath({ hash: '', pathname: '/app/users/7' }, true, '/app') // '/users/7'\n * resolveLocationPath({ hash: '', pathname: '/app' }, true, '/app') // '/'\n * resolveLocationPath({ hash: '', pathname: '/other/users' }, true, '/app') // '/other/users'\n * ```\n */\nexport function resolveLocationPath(\n\tlocation: Pick<Location, 'hash' | 'pathname'>,\n\thistory: boolean,\n\tbase?: string,\n): string {\n\tif (!history) return extractHashPath(location.hash)\n\tconst pathname = location.pathname\n\tif (base === undefined || base === '') return pathname\n\tconst normalizedBase = base.endsWith('/') ? base.slice(0, -1) : base\n\tif (pathname === normalizedBase) return '/'\n\tif (pathname.startsWith(`${normalizedBase}/`)) return pathname.slice(normalizedBase.length)\n\treturn pathname\n}\n\n/**\n * Find the nearest enclosing `<a>` element a DOM event originated from, by\n * walking its composed path — the pure lookup behind history-mode link\n * interception.\n *\n * @remarks\n * Uses `event.composedPath()` (not `event.target`) so a click on a styled\n * child INSIDE an anchor (an icon, a span) still resolves to the anchor.\n * Total — never throws; returns `undefined` when no anchor is found on the\n * path.\n *\n * @param event - The DOM event to search (typically a `click`)\n * @returns The nearest enclosing `HTMLAnchorElement`, or `undefined`\n *\n * @example\n * ```ts\n * document.addEventListener('click', (event) => {\n * \tconst anchor = findAnchor(event)\n * \tif (anchor !== undefined) console.log(anchor.href)\n * })\n * ```\n */\nexport function findAnchor(event: Event): HTMLAnchorElement | undefined {\n\tfor (const node of event.composedPath()) {\n\t\tif (node instanceof HTMLAnchorElement) return node\n\t}\n\treturn undefined\n}\n","import type { NavigatorEventMap, NavigatorInterface, NavigatorOptions } from './types.js'\nimport type { AbortInterface } from '@orkestrel/abort'\nimport type { EmitterErrorHandler, EmitterInterface } from '@orkestrel/emitter'\nimport type { RouteEntry, RouterInterface, RouterMatch } from '@src/core'\nimport { createAbort } from '@orkestrel/abort'\nimport { Emitter } from '@orkestrel/emitter'\nimport { isFunction, isString } from '@orkestrel/contract'\nimport { canonicalizePath, createRouter, joinPaths } from '@src/core'\nimport { findAnchor, resolveLocationPath } from './helpers.js'\n\n/**\n * The headless History/hash navigation entity — composes one core\n * `Router<RouteEntry<Meta>>`, resolving the current location on `start()` and\n * every subsequent navigation event, tracking `active`, and emitting\n * `navigate` through the core {@link Emitter} (AGENTS §13). No `render` /\n * `outlet` — the consumer owns rendering.\n *\n * @typeParam Meta - The opaque per-route payload a match carries back\n *\n * @remarks\n * - **One shared engine.** Each `route.path` is registered on the SAME\n * `Router` machine the core `Dispatcher` composes, keyed for dedup by its\n * {@link canonicalizePath} (last write wins, replace-in-place) — literal-\n * over-param precedence, trailing-slash insensitivity, and\n * `:param`/`*wildcard` extraction all come from that one engine (AGENTS\n * §21).\n * - **Resolve pipeline.** Compute the `/`-prefixed pathname to match\n * ({@link resolveLocationPath}) → {@link match} it → on a miss, match the\n * `fallback` through the SAME engine → a fallback that ALSO matches nothing\n * aborts any pending guarded navigation (a miss SUPERSEDES it, same as a\n * newer navigation) and leaves `active` `undefined`, emitting nothing\n * (§21-honest: no phantom match is fabricated) → the optional `guard` may\n * veto → on a verdict, `active` is set and `navigate` emitted.\n * - **Supersede-safe guard.** Every navigation mints an `@orkestrel/abort`\n * handle, aborting the PREVIOUS navigation's handle first; a guard verdict\n * that resolves after its navigation was superseded (`signal.aborted`) is\n * discarded, same as a `false`/rejected verdict. A guard throw routes to\n * the `error` handler and vetoes. `stop()`/`destroy()` also abort the\n * pending handle.\n * - **Hash vs history mode.** Hash mode (`history: false`, the default) binds\n * `hashchange`; history mode (`history: true`) binds `popstate` and, when\n * `intercept` is set, same-origin `<a>` click interception (a plain\n * left-click with no modifier keys, `target`, or `download` attribute).\n *\n * @example\n * ```ts\n * const navigator = new Navigator<{ readonly title: string }>({\n * \troutes: [\n * \t\t{ path: '/users/:id', meta: { title: 'User' } },\n * \t\t{ path: '/tokens', meta: { title: 'Tokens' } },\n * \t],\n * })\n * navigator.emitter.on('navigate', (match) => (document.title = match.meta.title))\n * navigator.start() // resolves the current hash now, and on every hashchange\n * navigator.navigate('/tokens')\n * ```\n */\nexport class Navigator<Meta> implements NavigatorInterface<Meta> {\n\treadonly #router: RouterInterface<RouteEntry<Meta>>\n\treadonly #emitter: Emitter<NavigatorEventMap<Meta>>\n\treadonly #history: boolean\n\treadonly #base: string | undefined\n\treadonly #fallback: string | undefined\n\treadonly #guard: NavigatorOptions<Meta>['guard']\n\treadonly #error: EmitterErrorHandler | undefined\n\treadonly #intercept: boolean\n\treadonly #hashListener: () => void\n\treadonly #popListener: () => void\n\treadonly #clickListener: (event: MouseEvent) => void\n\t#active: RouterMatch<Meta> | undefined\n\t#started = false\n\t#current: AbortInterface | undefined\n\n\tconstructor(options: NavigatorOptions<Meta>) {\n\t\tif (options.guard !== undefined && !isFunction(options.guard))\n\t\t\tthrow new TypeError(\n\t\t\t\t`a navigator guard must be a function, got ${JSON.stringify(options.guard)}`,\n\t\t\t)\n\t\tif (options.fallback !== undefined && !isString(options.fallback))\n\t\t\tthrow new TypeError(\n\t\t\t\t`a navigator fallback must be a string, got ${JSON.stringify(options.fallback)}`,\n\t\t\t)\n\t\tif (options.base !== undefined && !isString(options.base))\n\t\t\tthrow new TypeError(`a navigator base must be a string, got ${JSON.stringify(options.base)}`)\n\t\tthis.#history = options.history ?? false\n\t\tthis.#base = options.base\n\t\tthis.#intercept = options.intercept ?? false\n\t\tthis.#guard = options.guard\n\t\tthis.#error = options.error\n\t\tthis.#emitter = new Emitter<NavigatorEventMap<Meta>>({ on: options.on, error: options.error })\n\t\tthis.#router = createRouter<RouteEntry<Meta>>({\n\t\t\tentries: options.routes.map((route) => ({ path: route.path, meta: route, name: route.name })),\n\t\t\tsensitive: options.sensitive,\n\t\t\tkey: (entry) => canonicalizePath(entry.meta.path),\n\t\t})\n\t\tthis.#fallback = options.fallback ?? options.routes[0]?.path\n\t\tthis.#hashListener = () => this.#resolve()\n\t\tthis.#popListener = () => this.#resolve()\n\t\tthis.#clickListener = (event) => this.#intercepted(event)\n\t}\n\n\tget router(): RouterInterface<RouteEntry<Meta>> {\n\t\treturn this.#router\n\t}\n\n\tget emitter(): EmitterInterface<NavigatorEventMap<Meta>> {\n\t\treturn this.#emitter\n\t}\n\n\tget active(): RouterMatch<Meta> | undefined {\n\t\treturn this.#active\n\t}\n\n\tstart(): void {\n\t\tif (this.#started) return\n\t\tthis.#started = true\n\t\tif (!this.#history) {\n\t\t\twindow.addEventListener('hashchange', this.#hashListener)\n\t\t} else {\n\t\t\twindow.addEventListener('popstate', this.#popListener)\n\t\t\tif (this.#intercept) document.addEventListener('click', this.#clickListener)\n\t\t}\n\t\tthis.#resolve()\n\t}\n\n\tstop(): void {\n\t\tif (!this.#started) return\n\t\tthis.#started = false\n\t\tif (!this.#history) {\n\t\t\twindow.removeEventListener('hashchange', this.#hashListener)\n\t\t} else {\n\t\t\twindow.removeEventListener('popstate', this.#popListener)\n\t\t\tif (this.#intercept) document.removeEventListener('click', this.#clickListener)\n\t\t}\n\t\tthis.#current?.abort()\n\t}\n\n\tnavigate(path: string): void {\n\t\tif (!this.#history) {\n\t\t\tconst next = `#${path}`\n\t\t\tif (window.location.hash === next) this.#resolve()\n\t\t\telse window.location.hash = next\n\t\t\treturn\n\t\t}\n\t\tconst target = this.#base === undefined ? path : joinPaths(this.#base, path)\n\t\twindow.history.pushState(null, '', target)\n\t\tthis.#resolve()\n\t}\n\n\tmatch(path: string): RouterMatch<Meta> | undefined {\n\t\tconst hit = this.#router.match(path)\n\t\tif (hit === undefined) return undefined\n\t\treturn { path: hit.path, params: hit.params, meta: hit.meta.meta, name: hit.meta.name }\n\t}\n\n\tdestroy(): void {\n\t\tthis.stop()\n\t\tthis.#emitter.destroy()\n\t}\n\n\t// === Private\n\n\t// Compute the pathname to match for the CURRENT location, resolve it (falling back to the\n\t// configured fallback through the SAME engine on a miss), and either navigate or — when\n\t// neither the location nor the fallback matches anything — leave `active` `undefined` with\n\t// no emit (§21-honest: no phantom match is fabricated).\n\t#resolve(): void {\n\t\tconst pathname = resolveLocationPath(\n\t\t\t{ hash: window.location.hash, pathname: window.location.pathname },\n\t\t\tthis.#history,\n\t\t\tthis.#base,\n\t\t)\n\t\tconst to = this.match(pathname) ?? this.#matchFallback()\n\t\tif (to === undefined) {\n\t\t\tthis.#current?.abort()\n\t\t\tthis.#active = undefined\n\t\t\treturn\n\t\t}\n\t\tthis.#navigate(to)\n\t}\n\n\t#matchFallback(): RouterMatch<Meta> | undefined {\n\t\tif (this.#fallback === undefined) return undefined\n\t\treturn this.match(this.#fallback)\n\t}\n\n\t// Supersede the previous pending navigation's abort handle, mint a fresh one for this\n\t// navigation, and either commit directly (no guard configured — the synchronous fast path) or\n\t// run the guard pipeline.\n\t#navigate(to: RouterMatch<Meta>): void {\n\t\tthis.#current?.abort()\n\t\tconst handle = createAbort()\n\t\tthis.#current = handle\n\t\tconst guard = this.#guard\n\t\tif (guard === undefined) {\n\t\t\tthis.#commit(to)\n\t\t\treturn\n\t\t}\n\t\tvoid this.#guarded(guard, to, this.#active, handle)\n\t}\n\n\t#commit(to: RouterMatch<Meta>): void {\n\t\tthis.#active = to\n\t\tthis.#emitter.emit('navigate', to)\n\t}\n\n\t// Await the guard's verdict; a throw routes to the `error` handler and vetoes, a discarded\n\t// verdict (superseded via `handle.signal.aborted`, or a plain `false`/rejected verdict) leaves\n\t// `active` unchanged with no emit, and a true verdict commits.\n\tasync #guarded(\n\t\tguard: NonNullable<NavigatorOptions<Meta>['guard']>,\n\t\tto: RouterMatch<Meta>,\n\t\tfrom: RouterMatch<Meta> | undefined,\n\t\thandle: AbortInterface,\n\t): Promise<void> {\n\t\tlet verdict: boolean\n\t\ttry {\n\t\t\tverdict = await guard(to, from, handle.signal)\n\t\t} catch (error) {\n\t\t\tthis.#surface(error)\n\t\t\treturn\n\t\t}\n\t\tif (handle.signal.aborted || !verdict) return\n\t\tthis.#commit(to)\n\t}\n\n\t// Route a guard throw to the `error` handler (AGENTS §13's own channel, not a listener\n\t// throw so it cannot flow through the emitter's `emit`), swallowing a throwing handler itself\n\t// (anti-recursion, mirroring the emitter's own contract).\n\t#surface(error: unknown): void {\n\t\tconst handler = this.#error\n\t\tif (handler === undefined) return\n\t\ttry {\n\t\t\thandler(error, 'navigate')\n\t\t} catch {\n\t\t\t// The error handler itself threw — swallow it (anti-recursion).\n\t\t}\n\t}\n\n\t// Same-origin `<a>` click interception (history mode, opt-in via `intercept`): skip an\n\t// already-handled event, a non-primary button, any modifier key, a targeted or download link,\n\t// or a cross-origin destination — otherwise prevent the native navigation and `navigate` instead.\n\t#intercepted(event: MouseEvent): void {\n\t\tif (event.defaultPrevented || event.button !== 0) return\n\t\tif (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return\n\t\tconst anchor = findAnchor(event)\n\t\tif (anchor === undefined) return\n\t\tif (anchor.target !== '' && anchor.target !== '_self') return\n\t\tif (anchor.hasAttribute('download')) return\n\t\tconst url = new URL(anchor.href, window.location.href)\n\t\tif (url.origin !== window.location.origin) return\n\t\tevent.preventDefault()\n\t\tthis.navigate(resolveLocationPath({ hash: url.hash, pathname: url.pathname }, true, this.#base))\n\t}\n}\n","import type { NavigatorInterface, NavigatorOptions } from './types.js'\nimport { Navigator } from './Navigator.js'\n\n/**\n * Create a {@link NavigatorInterface} — the headless History/hash navigation\n * entity composing one core `Router<RouteEntry<Meta>>`.\n *\n * @remarks\n * Prefer this over `new Navigator(...)` at call sites that only need the\n * interface.\n *\n * @typeParam Meta - The opaque per-route payload a match carries back\n * @param options - The `routes` to register, the `history` toggle (default\n * `false`, hash mode), an optional `base` (history mode), an optional\n * `fallback` path, an optional `guard` hook, opt-in link `intercept`\n * (history mode), the `sensitive` case toggle, and the AGENTS §13 emitter\n * `on`/`error` wiring\n * @returns A live {@link NavigatorInterface} handle — call `start()` to begin\n * dispatching\n *\n * @example\n * ```ts\n * import { createNavigator } from '@src/browser'\n *\n * const navigator = createNavigator({\n * \troutes: [\n * \t\t{ path: '/users/:id', meta: { title: 'User' } },\n * \t\t{ path: '/tokens', meta: { title: 'Tokens' } },\n * \t],\n * \ton: { navigate: (match) => (document.title = match.meta.title) },\n * })\n * navigator.start()\n * ```\n */\nexport function createNavigator<Meta>(options: NavigatorOptions<Meta>): NavigatorInterface<Meta> {\n\treturn new Navigator<Meta>(options)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["#router","#emitter","#history","#base","#fallback","#guard","#error","#intercept","#hashListener","#popListener","#clickListener","#resolve","#intercepted","#active","#started","#current","#matchFallback","#navigate","#commit","#guarded","#surface"],"sources":["../../../src/browser/helpers.ts","../../../src/browser/Navigator.ts","../../../src/browser/factories.ts"],"sourcesContent":["// The PURE browser-navigation primitives (AGENTS §4.3 multi-word names — module\n// scope, no entity context). Every one is exported (the centralized-file rule,\n// §5): the `Navigator` composes them, and each has its own unit test. NO `node:*`\n// — DOM-typed only (`Location`, `Event`, `HTMLAnchorElement`), valid under the\n// `src:browser` scoped check (AGENTS §17.7).\n\nimport type { RouteEntry } from '@src/core'\nimport { canonicalizePath } from '@src/core'\n\n/**\n * Compute the registry key for a browser navigation route.\n *\n * @remarks\n * Projects the nested route's path through the core engine's canonical\n * trailing-slash identity, so `/users` and `/users/` replace one another in\n * the Navigator's shared Router.\n *\n * @param entry - The outer Router entry carrying the Navigator route\n * @returns The nested route's canonical path\n *\n * @example\n * ```ts\n * computeNavigationKey({ path: '/users/', meta: { path: '/users/' } }) // '/users'\n * ```\n */\nexport function computeNavigationKey(entry: RouteEntry<{ readonly path: string }>): string {\n\treturn canonicalizePath(entry.meta.path)\n}\n\n/**\n * Extract the `/`-prefixed pathname from a `location.hash` value — strip the\n * leading `#` (keeping the route's own leading `/`) and any `?query` suffix.\n *\n * @remarks\n * The grammar this package matches everywhere is `/`-prefixed (§4 path\n * grammar), so a hash-mode location's `'#/users/7?x'` becomes `'/users/7'`\n * — a hash pattern is expected to start `'#/'`; anything else (an empty hash,\n * or one that does not begin `'#/'`) yields `''` (the `Navigator` then falls\n * back). Total — never throws.\n *\n * @param hash - The raw `window.location.hash` value (e.g. `'#/users/7?x'`)\n * @returns The `/`-prefixed pathname to match, or `''` for an empty / non-`#/` hash\n *\n * @example\n * ```ts\n * extractHashPath('#/users/7?x') // '/users/7'\n * extractHashPath('#/tokens') // '/tokens'\n * extractHashPath('') // '' — the Navigator falls back\n * extractHashPath('#other') // '' — not a `#/` route hash\n * ```\n */\nexport function extractHashPath(hash: string): string {\n\tif (!hash.startsWith('#/')) return ''\n\tconst withoutHash = hash.slice(1)\n\tconst queryIndex = withoutHash.indexOf('?')\n\treturn queryIndex === -1 ? withoutHash : withoutHash.slice(0, queryIndex)\n}\n\n/**\n * Resolve the `/`-prefixed pathname to match for the CURRENT location, in\n * either navigation mode — the one seam `extractHashPath` (hash mode) and\n * history-mode base-stripping share.\n *\n * @remarks\n * Hash mode (`history: false`) reads `location.hash` through\n * {@link extractHashPath}. History mode (`history: true`) reads\n * `location.pathname` and strips a leading `base` prefix when one is\n * configured: `base` itself maps to the root `'/'`; a pathname that is not\n * under `base` is returned unchanged (a base mismatch is not this helper's\n * concern — the `Navigator`'s match then simply misses). Total — never throws.\n *\n * @param location - The `hash` + `pathname` pair to resolve from (accepts a\n * real `Location` or any object shaped the same, for pure unit testing)\n * @param history - The navigation substrate: `false` for hash mode, `true`\n * for history mode\n * @param base - The history-mode path prefix to strip (ignored in hash mode;\n * omit for no prefix)\n * @returns The `/`-prefixed pathname to match\n *\n * @example\n * ```ts\n * resolveLocationPath({ hash: '#/users/7', pathname: '/' }, false) // '/users/7'\n * resolveLocationPath({ hash: '', pathname: '/app/users/7' }, true, '/app') // '/users/7'\n * resolveLocationPath({ hash: '', pathname: '/app' }, true, '/app') // '/'\n * resolveLocationPath({ hash: '', pathname: '/other/users' }, true, '/app') // '/other/users'\n * ```\n */\nexport function resolveLocationPath(\n\tlocation: Pick<Location, 'hash' | 'pathname'>,\n\thistory: boolean,\n\tbase?: string,\n): string {\n\tif (!history) return extractHashPath(location.hash)\n\tconst pathname = location.pathname\n\tif (base === undefined || base === '') return pathname\n\tconst normalizedBase = base.endsWith('/') ? base.slice(0, -1) : base\n\tif (pathname === normalizedBase) return '/'\n\tif (pathname.startsWith(`${normalizedBase}/`)) return pathname.slice(normalizedBase.length)\n\treturn pathname\n}\n\n/**\n * Find the nearest enclosing `<a>` element a DOM event originated from, by\n * walking its composed path — the pure lookup behind history-mode link\n * interception.\n *\n * @remarks\n * Uses `event.composedPath()` (not `event.target`) so a click on a styled\n * child INSIDE an anchor (an icon, a span) still resolves to the anchor.\n * Total — never throws; returns `undefined` when no anchor is found on the\n * path.\n *\n * @param event - The DOM event to search (typically a `click`)\n * @returns The nearest enclosing `HTMLAnchorElement`, or `undefined`\n *\n * @example\n * ```ts\n * document.addEventListener('click', (event) => {\n * \tconst anchor = findAnchor(event)\n * \tif (anchor !== undefined) console.log(anchor.href)\n * })\n * ```\n */\nexport function findAnchor(event: Event): HTMLAnchorElement | undefined {\n\tfor (const node of event.composedPath()) {\n\t\tif (node instanceof HTMLAnchorElement) return node\n\t}\n\treturn undefined\n}\n","import type { NavigatorEventMap, NavigatorInterface, NavigatorOptions } from './types.js'\nimport type { AbortInterface } from '@orkestrel/abort'\nimport type { EmitterErrorHandler, EmitterInterface } from '@orkestrel/emitter'\nimport type { RouteEntry, RouterInterface, RouterMatch } from '@src/core'\nimport { createAbort } from '@orkestrel/abort'\nimport { Emitter } from '@orkestrel/emitter'\nimport { isFunction, isString } from '@orkestrel/contract'\nimport { createRouter, joinPaths } from '@src/core'\nimport { computeNavigationKey, findAnchor, resolveLocationPath } from './helpers.js'\n\n/**\n * The headless History/hash navigation entity — composes one core\n * `Router<RouteEntry<Meta>>`, resolving the current location on `start()` and\n * every subsequent navigation event, tracking `active`, and emitting\n * `navigate` through the core {@link Emitter} (AGENTS §13). No `render` /\n * `outlet` — the consumer owns rendering.\n *\n * @typeParam Meta - The opaque per-route payload a match carries back\n *\n * @remarks\n * - **One shared engine.** Each `route.path` is registered on the SAME\n * `Router` machine the core `Dispatcher` composes, keyed for dedup by its\n * {@link canonicalizePath} (last write wins, replace-in-place) — literal-\n * over-param precedence, trailing-slash insensitivity, and\n * `:param`/`*wildcard` extraction all come from that one engine (AGENTS\n * §21).\n * - **Resolve pipeline.** Compute the `/`-prefixed pathname to match\n * ({@link resolveLocationPath}) → {@link match} it → on a miss, match the\n * `fallback` through the SAME engine → a fallback that ALSO matches nothing\n * aborts any pending guarded navigation (a miss SUPERSEDES it, same as a\n * newer navigation) and leaves `active` `undefined`, emitting nothing\n * (§21-honest: no phantom match is fabricated) → the optional `guard` may\n * veto → on a verdict, `active` is set and `navigate` emitted.\n * - **Supersede-safe guard.** Every navigation mints an `@orkestrel/abort`\n * handle, aborting the PREVIOUS navigation's handle first; a guard verdict\n * that resolves after its navigation was superseded (`signal.aborted`) is\n * discarded, same as a `false`/rejected verdict. A guard throw routes to\n * the `error` handler and vetoes. `stop()`/`destroy()` also abort the\n * pending handle.\n * - **Hash vs history mode.** Hash mode (`history: false`, the default) binds\n * `hashchange`; history mode (`history: true`) binds `popstate` and, when\n * `intercept` is set, same-origin `<a>` click interception (a plain\n * left-click with no modifier keys, `target`, or `download` attribute).\n *\n * @example\n * ```ts\n * const navigator = new Navigator<{ readonly title: string }>({\n * \troutes: [\n * \t\t{ path: '/users/:id', meta: { title: 'User' } },\n * \t\t{ path: '/tokens', meta: { title: 'Tokens' } },\n * \t],\n * })\n * navigator.emitter.on('navigate', (match) => (document.title = match.meta.title))\n * navigator.start() // resolves the current hash now, and on every hashchange\n * navigator.navigate('/tokens')\n * ```\n */\nexport class Navigator<Meta> implements NavigatorInterface<Meta> {\n\treadonly #router: RouterInterface<RouteEntry<Meta>>\n\treadonly #emitter: Emitter<NavigatorEventMap<Meta>>\n\treadonly #history: boolean\n\treadonly #base: string | undefined\n\treadonly #fallback: string | undefined\n\treadonly #guard: NavigatorOptions<Meta>['guard']\n\treadonly #error: EmitterErrorHandler | undefined\n\treadonly #intercept: boolean\n\treadonly #hashListener: () => void\n\treadonly #popListener: () => void\n\treadonly #clickListener: (event: MouseEvent) => void\n\t#active: RouterMatch<Meta> | undefined\n\t#started = false\n\t#current: AbortInterface | undefined\n\n\tconstructor(options: NavigatorOptions<Meta>) {\n\t\tif (options.guard !== undefined && !isFunction(options.guard))\n\t\t\tthrow new TypeError(\n\t\t\t\t`a navigator guard must be a function, got ${JSON.stringify(options.guard)}`,\n\t\t\t)\n\t\tif (options.fallback !== undefined && !isString(options.fallback))\n\t\t\tthrow new TypeError(\n\t\t\t\t`a navigator fallback must be a string, got ${JSON.stringify(options.fallback)}`,\n\t\t\t)\n\t\tif (options.base !== undefined && !isString(options.base))\n\t\t\tthrow new TypeError(`a navigator base must be a string, got ${JSON.stringify(options.base)}`)\n\t\tthis.#history = options.history ?? false\n\t\tthis.#base = options.base\n\t\tthis.#intercept = options.intercept ?? false\n\t\tthis.#guard = options.guard\n\t\tthis.#error = options.error\n\t\tthis.#emitter = new Emitter<NavigatorEventMap<Meta>>({\n\t\t\t...(options.on === undefined ? {} : { on: options.on }),\n\t\t\t...(options.error === undefined ? {} : { error: options.error }),\n\t\t})\n\t\tthis.#router = createRouter<RouteEntry<Meta>>({\n\t\t\tentries: options.routes.map((route) => ({\n\t\t\t\tpath: route.path,\n\t\t\t\tmeta: route,\n\t\t\t\t...(route.name === undefined ? {} : { name: route.name }),\n\t\t\t})),\n\t\t\t...(options.sensitive === undefined ? {} : { sensitive: options.sensitive }),\n\t\t\tkey: computeNavigationKey,\n\t\t})\n\t\tthis.#fallback = options.fallback ?? options.routes[0]?.path\n\t\tthis.#hashListener = this.#resolve.bind(this)\n\t\tthis.#popListener = this.#resolve.bind(this)\n\t\tthis.#clickListener = this.#intercepted.bind(this)\n\t}\n\n\tget router(): RouterInterface<RouteEntry<Meta>> {\n\t\treturn this.#router\n\t}\n\n\tget emitter(): EmitterInterface<NavigatorEventMap<Meta>> {\n\t\treturn this.#emitter\n\t}\n\n\tget active(): RouterMatch<Meta> | undefined {\n\t\treturn this.#active\n\t}\n\n\tstart(): void {\n\t\tif (this.#started) return\n\t\tthis.#started = true\n\t\tif (!this.#history) {\n\t\t\twindow.addEventListener('hashchange', this.#hashListener)\n\t\t} else {\n\t\t\twindow.addEventListener('popstate', this.#popListener)\n\t\t\tif (this.#intercept) document.addEventListener('click', this.#clickListener)\n\t\t}\n\t\tthis.#resolve()\n\t}\n\n\tstop(): void {\n\t\tif (!this.#started) return\n\t\tthis.#started = false\n\t\tif (!this.#history) {\n\t\t\twindow.removeEventListener('hashchange', this.#hashListener)\n\t\t} else {\n\t\t\twindow.removeEventListener('popstate', this.#popListener)\n\t\t\tif (this.#intercept) document.removeEventListener('click', this.#clickListener)\n\t\t}\n\t\tthis.#current?.abort()\n\t}\n\n\tnavigate(path: string): void {\n\t\tif (!this.#history) {\n\t\t\tconst next = `#${path}`\n\t\t\tif (window.location.hash === next) this.#resolve()\n\t\t\telse window.location.hash = next\n\t\t\treturn\n\t\t}\n\t\tconst target = this.#base === undefined ? path : joinPaths(this.#base, path)\n\t\twindow.history.pushState(null, '', target)\n\t\tthis.#resolve()\n\t}\n\n\tmatch(path: string): RouterMatch<Meta> | undefined {\n\t\tconst hit = this.#router.match(path)\n\t\tif (hit === undefined) return undefined\n\t\treturn {\n\t\t\tpath: hit.path,\n\t\t\tparams: hit.params,\n\t\t\tmeta: hit.meta.meta,\n\t\t\t...(hit.meta.name === undefined ? {} : { name: hit.meta.name }),\n\t\t}\n\t}\n\n\tdestroy(): void {\n\t\tthis.stop()\n\t\tthis.#emitter.destroy()\n\t}\n\n\t// === Private\n\n\t// Compute the pathname to match for the CURRENT location, resolve it (falling back to the\n\t// configured fallback through the SAME engine on a miss), and either navigate or — when\n\t// neither the location nor the fallback matches anything — leave `active` `undefined` with\n\t// no emit (§21-honest: no phantom match is fabricated).\n\t#resolve(): void {\n\t\tconst pathname = resolveLocationPath(\n\t\t\t{ hash: window.location.hash, pathname: window.location.pathname },\n\t\t\tthis.#history,\n\t\t\tthis.#base,\n\t\t)\n\t\tconst to = this.match(pathname) ?? this.#matchFallback()\n\t\tif (to === undefined) {\n\t\t\tthis.#current?.abort()\n\t\t\tthis.#active = undefined\n\t\t\treturn\n\t\t}\n\t\tthis.#navigate(to)\n\t}\n\n\t#matchFallback(): RouterMatch<Meta> | undefined {\n\t\tif (this.#fallback === undefined) return undefined\n\t\treturn this.match(this.#fallback)\n\t}\n\n\t// Supersede the previous pending navigation's abort handle, mint a fresh one for this\n\t// navigation, and either commit directly (no guard configured — the synchronous fast path) or\n\t// run the guard pipeline.\n\t#navigate(to: RouterMatch<Meta>): void {\n\t\tthis.#current?.abort()\n\t\tconst handle = createAbort()\n\t\tthis.#current = handle\n\t\tconst guard = this.#guard\n\t\tif (guard === undefined) {\n\t\t\tthis.#commit(to)\n\t\t\treturn\n\t\t}\n\t\tvoid this.#guarded(guard, to, this.#active, handle)\n\t}\n\n\t#commit(to: RouterMatch<Meta>): void {\n\t\tthis.#active = to\n\t\tthis.#emitter.emit('navigate', to)\n\t}\n\n\t// Await the guard's verdict; a throw routes to the `error` handler and vetoes, a discarded\n\t// verdict (superseded via `handle.signal.aborted`, or a plain `false`/rejected verdict) leaves\n\t// `active` unchanged with no emit, and a true verdict commits.\n\tasync #guarded(\n\t\tguard: NonNullable<NavigatorOptions<Meta>['guard']>,\n\t\tto: RouterMatch<Meta>,\n\t\tfrom: RouterMatch<Meta> | undefined,\n\t\thandle: AbortInterface,\n\t): Promise<void> {\n\t\tlet verdict: boolean\n\t\ttry {\n\t\t\tverdict = await guard(to, from, handle.signal)\n\t\t} catch (error) {\n\t\t\tthis.#surface(error)\n\t\t\treturn\n\t\t}\n\t\tif (handle.signal.aborted || !verdict) return\n\t\tthis.#commit(to)\n\t}\n\n\t// Route a guard throw to the `error` handler (AGENTS §13's own channel, not a listener\n\t// throw so it cannot flow through the emitter's `emit`), swallowing a throwing handler itself\n\t// (anti-recursion, mirroring the emitter's own contract).\n\t#surface(error: unknown): void {\n\t\tconst handler = this.#error\n\t\tif (handler === undefined) return\n\t\ttry {\n\t\t\thandler(error, 'navigate')\n\t\t} catch {\n\t\t\t// The error handler itself threw — swallow it (anti-recursion).\n\t\t}\n\t}\n\n\t// Same-origin `<a>` click interception (history mode, opt-in via `intercept`): skip an\n\t// already-handled event, a non-primary button, any modifier key, a targeted or download link,\n\t// or a cross-origin destination — otherwise prevent the native navigation and `navigate` instead.\n\t#intercepted(event: MouseEvent): void {\n\t\tif (event.defaultPrevented || event.button !== 0) return\n\t\tif (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return\n\t\tconst anchor = findAnchor(event)\n\t\tif (anchor === undefined) return\n\t\tif (anchor.target !== '' && anchor.target !== '_self') return\n\t\tif (anchor.hasAttribute('download')) return\n\t\tconst url = new URL(anchor.href, window.location.href)\n\t\tif (url.origin !== window.location.origin) return\n\t\tevent.preventDefault()\n\t\tthis.navigate(resolveLocationPath({ hash: url.hash, pathname: url.pathname }, true, this.#base))\n\t}\n}\n","import type { NavigatorInterface, NavigatorOptions } from './types.js'\nimport { Navigator } from './Navigator.js'\n\n/**\n * Create a {@link NavigatorInterface} — the headless History/hash navigation\n * entity composing one core `Router<RouteEntry<Meta>>`.\n *\n * @remarks\n * Prefer this over `new Navigator(...)` at call sites that only need the\n * interface.\n *\n * @typeParam Meta - The opaque per-route payload a match carries back\n * @param options - The `routes` to register, the `history` toggle (default\n * `false`, hash mode), an optional `base` (history mode), an optional\n * `fallback` path, an optional `guard` hook, opt-in link `intercept`\n * (history mode), the `sensitive` case toggle, and the AGENTS §13 emitter\n * `on`/`error` wiring\n * @returns A live {@link NavigatorInterface} handle — call `start()` to begin\n * dispatching\n *\n * @example\n * ```ts\n * import { createNavigator } from '@src/browser'\n *\n * const navigator = createNavigator({\n * \troutes: [\n * \t\t{ path: '/users/:id', meta: { title: 'User' } },\n * \t\t{ path: '/tokens', meta: { title: 'Tokens' } },\n * \t],\n * \ton: { navigate: (match) => (document.title = match.meta.title) },\n * })\n * navigator.start()\n * ```\n */\nexport function createNavigator<Meta>(options: NavigatorOptions<Meta>): NavigatorInterface<Meta> {\n\treturn new Navigator<Meta>(options)\n}\n"],"mappings":"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"}
@@ -123,6 +123,25 @@ function canonicalizePath(path) {
123
123
  return path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
124
124
  }
125
125
  /**
126
+ * Compute the registry key for a method-dimensioned dispatcher route.
127
+ *
128
+ * @remarks
129
+ * Combines the route record's HTTP method with the outer entry's canonical
130
+ * path, so registrations that differ only by one trailing slash replace each
131
+ * other while registrations for different methods remain distinct.
132
+ *
133
+ * @param entry - The dispatcher route entry to identify
134
+ * @returns The canonical `METHOD /path` registry key
135
+ *
136
+ * @example
137
+ * ```ts
138
+ * computeDispatchKey({ path: '/health/', meta: { method: 'GET' } }) // 'GET /health'
139
+ * ```
140
+ */
141
+ function computeDispatchKey(entry) {
142
+ return `${entry.meta.method} ${canonicalizePath(entry.path)}`;
143
+ }
144
+ /**
126
145
  * Compile a route path pattern into an anchored regex and its ordered param
127
146
  * names.
128
147
  *
@@ -562,7 +581,7 @@ var Router = class {
562
581
  path: best.entry.path,
563
582
  params: best.params,
564
583
  meta: best.entry.meta,
565
- name: best.entry.name
584
+ ...best.entry.name === void 0 ? {} : { name: best.entry.name }
566
585
  };
567
586
  }
568
587
  entries(pathname) {
@@ -692,19 +711,19 @@ var Dispatcher = class {
692
711
  #unmatched;
693
712
  #unmethoded;
694
713
  constructor(options) {
714
+ const sensitive = options?.sensitive;
715
+ const on = options?.on;
716
+ const error = options?.error;
695
717
  this.router = new Router({
696
- sensitive: options?.sensitive,
697
- key: (entry) => `${entry.meta.method} ${canonicalizePath(entry.path)}`
718
+ ...sensitive === void 0 ? {} : { sensitive },
719
+ key: computeDispatchKey
698
720
  });
699
721
  this.#emitter = new _orkestrel_emitter.Emitter({
700
- on: options?.on,
701
- error: options?.error
722
+ ...on === void 0 ? {} : { on },
723
+ ...error === void 0 ? {} : { error }
702
724
  });
703
- this.#unmatched = options?.unmatched ?? ((_request) => new Response("Not Found", { status: 404 }));
704
- this.#unmethoded = options?.unmethoded ?? ((_request, allow) => new Response("Method Not Allowed", {
705
- status: 405,
706
- headers: { Allow: allow.join(", ") }
707
- }));
725
+ this.#unmatched = options?.unmatched;
726
+ this.#unmethoded = options?.unmethoded;
708
727
  if (options?.routes !== void 0) this.add(options.routes);
709
728
  }
710
729
  get emitter() {
@@ -746,20 +765,20 @@ var Dispatcher = class {
746
765
  const allow = this.#allow(pathname);
747
766
  if (allow.length === 0) {
748
767
  this.#emitter.emit("miss", requested, pathname, "unmatched");
749
- return this.#unmatched(request);
768
+ return this.#respondUnmatched(request);
750
769
  }
751
770
  this.#emitter.emit("miss", requested, pathname, "unmethoded");
752
- return this.#unmethoded(request, allow);
771
+ return this.#respondUnmethoded(request, allow);
753
772
  }
754
773
  const result = this.match(method, pathname);
755
774
  if (result.status === "matched") return this.#respondMatched(request, state, method, result.match, url);
756
775
  if (result.status === "unmethoded") {
757
776
  if (method === "OPTIONS") return this.#respondAutoOptions(pathname, result.allow);
758
777
  this.#emitter.emit("miss", method, pathname, "unmethoded");
759
- return this.#unmethoded(request, result.allow);
778
+ return this.#respondUnmethoded(request, result.allow);
760
779
  }
761
780
  this.#emitter.emit("miss", method, pathname, "unmatched");
762
- return this.#unmatched(request);
781
+ return this.#respondUnmatched(request);
763
782
  }
764
783
  destroy() {
765
784
  this.#emitter.destroy();
@@ -767,13 +786,14 @@ var Dispatcher = class {
767
786
  #register(input) {
768
787
  if (!(0, _orkestrel_contract.isFunction)(input.handler)) throw new TypeError(`a route handler must be a function, got ${JSON.stringify(input.handler)}`);
769
788
  if (!(0, _orkestrel_contract.isString)(input.method) || !METHODS.has(input.method)) throw new TypeError(`a route method must be one of ${[...METHODS].join(", ")}, got ${JSON.stringify(input.method)}`);
789
+ const name = input.name;
770
790
  this.router.add({
771
791
  path: input.path,
772
- name: input.name,
792
+ ...name === void 0 ? {} : { name },
773
793
  meta: {
774
794
  method: input.method,
775
795
  handler: input.handler,
776
- name: input.name
796
+ ...name === void 0 ? {} : { name }
777
797
  }
778
798
  });
779
799
  }
@@ -784,6 +804,19 @@ var Dispatcher = class {
784
804
  if (methods.has("GET")) methods.add("HEAD");
785
805
  return [...methods];
786
806
  }
807
+ #respondUnmatched(request) {
808
+ const responder = this.#unmatched;
809
+ if (responder !== void 0) return responder(request);
810
+ return new Response("Not Found", { status: 404 });
811
+ }
812
+ #respondUnmethoded(request, allow) {
813
+ const responder = this.#unmethoded;
814
+ if (responder !== void 0) return responder(request, allow);
815
+ return new Response("Method Not Allowed", {
816
+ status: 405,
817
+ headers: { Allow: allow.join(", ") }
818
+ });
819
+ }
787
820
  async #respondMatched(request, state, method, match, url) {
788
821
  this.#emitter.emit("match", method, match.path);
789
822
  const context = {
@@ -880,6 +913,7 @@ exports.canonicalizePath = canonicalizePath;
880
913
  exports.classifySegment = classifySegment;
881
914
  exports.compareSpecificity = compareSpecificity;
882
915
  exports.compilePath = compilePath;
916
+ exports.computeDispatchKey = computeDispatchKey;
883
917
  exports.computeSpecificity = computeSpecificity;
884
918
  exports.createDispatcher = createDispatcher;
885
919
  exports.createRouter = createRouter;
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["#parent","#entries","#compiled","#sensitive","#key","#index","#register","#parent","#emitter","#unmatched","#unmethoded","#register","#allow","#respondMatched","#respondAutoOptions"],"sources":["../../../src/core/constants.ts","../../../src/core/helpers.ts","../../../src/core/Group.ts","../../../src/core/Router.ts","../../../src/core/DispatchGroup.ts","../../../src/core/Dispatcher.ts","../../../src/core/factories.ts"],"sourcesContent":["// ============================================================================\n// Core constants — the §5 centralized home for module-scope data used by the\n// matching engine and the fetch dispatcher. Every declaration here is frozen\n// and `export`ed per AGENTS §5.\n// ============================================================================\n\n/**\n * The complete set of HTTP methods a {@link import('./types.js').Dispatcher}\n * registers routes under — backs the registration guard (`add` rejects any\n * `method` outside this set) and the auto-`OPTIONS` `Allow` derivation.\n *\n * @remarks\n * A `ReadonlySet` of the seven {@link import('./types.js').Method} literals:\n * `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`. `HEAD` is\n * included even though it is never required at registration (a `GET` route\n * auto-answers `HEAD`) — it is still a valid method to register explicitly.\n *\n * @example\n * ```ts\n * METHODS.has('GET') // true\n * METHODS.has('TRACE') // false\n * ```\n */\nexport const METHODS: ReadonlySet<string> = Object.freeze(\n\tnew Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']),\n)\n\n/**\n * Specificity tier for a **literal** path segment (`/users`) — the highest\n * tier, always outranking a param or wildcard segment at the same position.\n *\n * @remarks\n * Consumed by `computeSpecificity` (U1 `helpers.ts`) when ranking candidate\n * matches left-to-right at the earliest differing segment (§4 precedence).\n *\n * @example\n * ```ts\n * TIER_LITERAL > TIER_PARAM // true\n * ```\n */\nexport const TIER_LITERAL = 2\n\n/**\n * Specificity tier for a **param** path segment (`:name`) — ranks below a\n * literal segment and above a wildcard segment at the same position.\n *\n * @remarks\n * Consumed by `computeSpecificity` (U1 `helpers.ts`) alongside {@link TIER_LITERAL}\n * and {@link TIER_WILDCARD}.\n *\n * @example\n * ```ts\n * TIER_PARAM > TIER_WILDCARD // true\n * ```\n */\nexport const TIER_PARAM = 1\n\n/**\n * Specificity tier for a **wildcard** path segment (`*name`) — the lowest\n * tier; a wildcard only ever wins against another wildcard shape (an\n * equal-specificity tie resolved by registration order).\n *\n * @remarks\n * Consumed by `computeSpecificity` (U1 `helpers.ts`).\n *\n * @example\n * ```ts\n * TIER_WILDCARD // 0\n * ```\n */\nexport const TIER_WILDCARD = 0\n","import type { CompiledPath, Method, RouteInput } from './types.js'\nimport { TIER_LITERAL, TIER_PARAM, TIER_WILDCARD } from './constants.js'\n\n// The PURE path-matching primitives (AGENTS §4.3 multi-word names — module scope,\n// no entity context). Every one is exported (the centralized-file rule, §5): a\n// consumer composes them directly, or reaches them through the `Router` engine.\n// They speak ONLY `string` / `RegExp` / `Record` and `decodeURIComponent` (a\n// platform global valid in Node and the browser alike) — NO DOM, NO `node:*` — so\n// the SAME engine drives a method-dimensioned server dispatcher and a method-less\n// browser navigator.\n\n/**\n * Escape every regex metacharacter in a literal string so it can be embedded\n * inside a larger `RegExp` source without being interpreted as syntax.\n *\n * @remarks\n * {@link compilePath} escapes the literal segments of a route pattern with this\n * before splicing in `:name` / `*name` capture groups, so a path like\n * `/files/:name.json` matches the `.` literally rather than as \"any character\".\n * Pure and total — never throws.\n *\n * @param value - The literal string to escape\n * @returns `value` with every regex metacharacter backslash-escaped\n *\n * @example\n * ```ts\n * escapeRegExp('a.b+c') // 'a\\\\.b\\\\+c'\n * new RegExp(`^${escapeRegExp('a.b')}$`).test('a.b') // true\n * new RegExp(`^${escapeRegExp('a.b')}$`).test('axb') // false\n * ```\n */\nexport function escapeRegExp(value: string): string {\n\treturn value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/**\n * Canonicalize a route path for REGISTRY IDENTITY — strip a single trailing\n * slash, except the root `/` (and the empty pattern). The trailing-slash fold\n * {@link compilePath} normalizes a pattern through, so identity agrees with the\n * matcher.\n *\n * @remarks\n * Mirrors {@link compilePath}'s trailing-slash folding: `/users/` canonicalizes\n * to `/users` (the two compile to the same regex and match the same\n * pathnames), while the root `/` and the empty `''` are EXEMPT (a bare `/`\n * already matches `/`; stripping it would break that). Pure and total — a path\n * without a trailing slash returns unchanged.\n *\n * @param path - The route path pattern\n * @returns The canonical path (one trailing slash removed, except `/` and `''`)\n *\n * @example\n * ```ts\n * canonicalizePath('/users/') // '/users'\n * canonicalizePath('/users') // '/users'\n * canonicalizePath('/') // '/'\n * canonicalizePath('') // ''\n * ```\n */\nexport function canonicalizePath(path: string): string {\n\treturn path.length > 1 && path.endsWith('/') ? path.slice(0, -1) : path\n}\n\n/**\n * Compile a route path pattern into an anchored regex and its ordered param\n * names.\n *\n * @remarks\n * Splits the CANONICALIZED path into segments. Each `:name` segment becomes a\n * `([^/]+)` capture group; the FINAL segment may instead be `*name`, which\n * becomes a `(.+)` capture spanning the REST of the path including slashes — a\n * wildcard segment anywhere but last is a registration-time programmer error\n * and throws `TypeError` (§14 construction/registration boundary). Every regex\n * metacharacter in a literal segment is escaped first ({@link escapeRegExp}),\n * so a path like `/files/:name.json` matches the `.` literally apart from the\n * param. The regex is anchored (`^…$`), so it matches the whole pathname, not\n * a prefix.\n *\n * **Trailing slash is INSENSITIVE** (Express's `strict: false` default): a\n * single trailing slash on the request path is OPTIONAL, so `/users` matches\n * both `/users` and `/users/`, and `/users/:id` matches both `/users/me` and\n * `/users/me/`. This is NOT prefix matching — a deeper path is still a\n * distinct segment, so `/api` does not match `/api/users`. The ROOT `/` (and\n * the empty pattern `''`) are EXEMPT — they are not stripped, so `/` stays\n * `^/$` and `''` stays `^$`.\n *\n * `sensitive` (default `true`) controls case folding: `false` adds the `i`\n * regex flag, so `/Users` matches `/users`. The pattern's own casing is never\n * altered — only the matching behavior.\n *\n * @param path - The route path pattern (e.g. `/users/:id`, `/files/*rest`)\n * @param sensitive - Case-sensitive matching (default `true`)\n * @returns The {@link CompiledPath} — its `regex` + ordered `params`\n * @throws {TypeError} When a `*name` wildcard segment is not the FINAL segment\n *\n * @example\n * ```ts\n * const { regex, params } = compilePath('/users/:id/posts/:slug')\n * params // ['id', 'slug']\n * regex.exec('/users/7/posts/hello') // ['…', '7', 'hello']\n * regex.test('/users/7/posts/hello/') // true — the trailing slash is optional\n *\n * compilePath('/files/*rest').regex.test('/files/a/b.png') // true\n * compilePath('/Users', false).regex.test('/users') // true — case-insensitive\n * ```\n */\nexport function compilePath(path: string, sensitive = true): CompiledPath {\n\tconst params: string[] = []\n\t// Normalize ONE trailing slash off the pattern so `/users/` compiles like `/users`\n\t// — except the root `/` (and the empty pattern), which must keep matching `/` / `''`.\n\tconst normalized = canonicalizePath(path)\n\tconst segments = normalized.split('/')\n\tconst compiledSegments = segments.map((segment, index) => {\n\t\tconst isFinal = index === segments.length - 1\n\t\t// A wildcard-shaped segment head (`*name`) is only valid as the FINAL segment — a\n\t\t// registration-time programmer error anywhere else (§14 boundary guard).\n\t\tif (!isFinal && /^\\*[A-Za-z_]\\w*/.test(segment))\n\t\t\tthrow new TypeError(\n\t\t\t\t`a wildcard segment (\"${segment}\") must be the final segment of a path pattern, got \"${path}\"`,\n\t\t\t)\n\t\t// Classification and compilation share ONE segment parser (§4 fix) — the tier\n\t\t// {@link classifySegment} assigns is exactly the shape compiled here.\n\t\tconst tier = classifySegment(segment, isFinal)\n\t\tif (tier === TIER_WILDCARD) {\n\t\t\tparams.push(segment.slice(1))\n\t\t\treturn '(.+)'\n\t\t}\n\t\tif (tier === TIER_PARAM) {\n\t\t\tconst match = /^:([A-Za-z_]\\w*)/.exec(segment)\n\t\t\tconst name = match?.[1] ?? ''\n\t\t\tparams.push(name)\n\t\t\treturn `([^/]+)${escapeRegExp(segment.slice(1 + name.length))}`\n\t\t}\n\t\treturn escapeRegExp(segment)\n\t})\n\tconst pattern = compiledSegments.join('/')\n\t// Allow ONE optional trailing slash before the `$` anchor (Express `strict: false`),\n\t// EXCEPT for the root `/` and the empty pattern — those anchor exactly so they keep\n\t// matching only `/` / `''` (a bare `/?` there would let `''` match `/` and vice versa).\n\tconst suffix = normalized === '/' || normalized === '' ? '' : '/?'\n\tconst flags = sensitive ? '' : 'i'\n\treturn { regex: new RegExp(`^${pattern}${suffix}$`, flags), params }\n}\n\n/**\n * URL-decode one captured param value, tolerating a malformed percent-escape —\n * the decode {@link matchPath} applies to each captured group.\n *\n * @remarks\n * A bad `%` sequence is not a reason to reject an otherwise-matching route, so\n * a `decodeURIComponent` that would throw falls back to the raw value\n * (mirroring the cookie / token boundary readers, AGENTS §14). Total — never\n * throws.\n *\n * @param value - The raw captured param value\n * @returns The URL-decoded value, or the raw value when decoding would throw\n *\n * @example\n * ```ts\n * decodeParam('a%2Fb') // 'a/b'\n * decodeParam('100%25') // '100%'\n * decodeParam('%') // '%' — malformed escape stays literal\n * ```\n */\nexport function decodeParam(value: string): string {\n\ttry {\n\t\treturn decodeURIComponent(value)\n\t} catch {\n\t\t// A malformed `%` escape stays literal rather than throwing — the match still succeeds.\n\t\treturn value\n\t}\n}\n\n/**\n * Extract the URL-decoded params a compiled path captures from a concrete\n * pathname, or `undefined` when the pathname does not match.\n *\n * @remarks\n * Runs the {@link CompiledPath} `regex` against `pathname` (a single `exec`); a\n * miss returns `undefined`. On a hit it walks `params` POSITIONALLY — the\n * `n`-th param name pairs with the `n`-th capture group — and URL-decodes each\n * value with {@link decodeParam}. Returns a frozen `name → value` record (empty\n * for a parameterless path). Total — never throws.\n *\n * @param compiled - The {@link CompiledPath} from {@link compilePath}\n * @param pathname - The concrete request pathname to match (e.g. `/users/7`)\n * @returns The decoded params on a hit, or `undefined` on a miss\n *\n * @example\n * ```ts\n * const compiled = compilePath('/users/:id')\n * matchPath(compiled, '/users/7') // { id: '7' }\n * matchPath(compiled, '/users/a%2Fb') // { id: 'a/b' } — decoded\n * matchPath(compiled, '/posts/7') // undefined\n * ```\n */\nexport function matchPath(\n\tcompiled: CompiledPath,\n\tpathname: string,\n): Readonly<Record<string, string>> | undefined {\n\tconst result = compiled.regex.exec(pathname)\n\tif (result === null) return undefined\n\tconst params: Record<string, string> = {}\n\tfor (let index = 0; index < compiled.params.length; index += 1) {\n\t\tconst name = compiled.params[index]\n\t\tconst value = result[index + 1]\n\t\tif (name !== undefined && value !== undefined) params[name] = decodeParam(value)\n\t}\n\treturn Object.freeze(params)\n}\n\n/**\n * Classify one path segment into its specificity TIER — the SAME syntax\n * {@link compilePath} rewrites: a syntactically valid `:name` head is a PARAM\n * segment, a final `*name` is a WILDCARD segment, everything else (including a\n * literal segment that merely CONTAINS a `:` mid-string, e.g. `a:b`) is a\n * LITERAL segment.\n *\n * @remarks\n * This is the fix over the old engine's bug: the old classifier ranked any\n * segment `includes(':')` as a param, so a literal segment like `a:b` was\n * mis-tiered even though {@link compilePath} compiles it literally. Sharing one\n * segment parser between compilation and classification keeps the two in\n * agreement (§4 fixes). Pure and total.\n *\n * @param segment - One `/`-split path segment\n * @param isFinal - Whether `segment` is the last segment of its path (only the\n * final segment may be classified as a wildcard)\n * @returns The segment's specificity tier — {@link import('./constants.js').TIER_LITERAL},\n * {@link import('./constants.js').TIER_PARAM}, or\n * {@link import('./constants.js').TIER_WILDCARD}\n *\n * @example\n * ```ts\n * classifySegment(':id', true) // 1 — TIER_PARAM\n * classifySegment('*rest', true) // 0 — TIER_WILDCARD\n * classifySegment('a:b', true) // 2 — TIER_LITERAL — the old bug's regression case\n * classifySegment('users', false) // 2 — TIER_LITERAL\n * ```\n */\nexport function classifySegment(segment: string, isFinal: boolean): number {\n\tif (isFinal && /^\\*[A-Za-z_]\\w*$/.test(segment)) return TIER_WILDCARD\n\tif (/^:[A-Za-z_]\\w*/.test(segment)) return TIER_PARAM\n\treturn TIER_LITERAL\n}\n\n/**\n * Compute a route path's SPECIFICITY VECTOR — the per-segment type ranking\n * that breaks a tie when several registered routes match the same concrete\n * pathname.\n *\n * @remarks\n * Splits the CANONICALIZED path into segments (on `/`) and maps each to its\n * specificity tier via {@link classifySegment} — the same segment parser\n * {@link compilePath} uses, so a literal segment that merely contains a `:`\n * (e.g. `a:b`) is correctly tiered as literal rather than param (the old\n * engine's bug, fixed here). The standard route-precedence rule compares two\n * matching routes' vectors LEFT-TO-RIGHT: at the first index where the tiers\n * differ, the HIGHER tier (a literal over a param over a wildcard) is MORE\n * SPECIFIC and wins — so `/users/me` (`[2, 2]`) beats `/users/:id` (`[2, 1]`)\n * beats `/users/*rest` (`[2, 0]`) regardless of registration order. Two routes\n * that match the SAME concrete pathname necessarily have the same segment\n * count in the common case; {@link compareSpecificity} handles the general\n * case for totality.\n *\n * @param path - The route path pattern (e.g. `/users/:id`)\n * @returns The per-segment specificity tiers, in order\n *\n * @example\n * ```ts\n * computeSpecificity('/users/me') // [2, 2]\n * computeSpecificity('/users/:id') // [2, 1]\n * computeSpecificity('/files/*rest') // [2, 0]\n * computeSpecificity('/a:b') // [2] — literal, not param — the classification fix\n * ```\n */\nexport function computeSpecificity(path: string): readonly number[] {\n\tconst segments = canonicalizePath(path).split('/')\n\treturn segments.map((segment, index) => classifySegment(segment, index === segments.length - 1))\n}\n\n/**\n * Compare two route paths by SPECIFICITY — the comparator that picks the\n * most-specific matching route (literal-over-param-over-wildcard,\n * registration-order-independent).\n *\n * @remarks\n * Compares the two paths' {@link computeSpecificity} vectors LEFT-TO-RIGHT and\n * returns a standard `Array.sort` ordering: a NEGATIVE number when `a` is MORE\n * specific than `b` (so a descending-specificity sort puts `a` first),\n * positive when `b` is more specific, `0` when neither out-ranks the other\n * across the compared segments. At the first index where the tiers differ,\n * the higher tier wins; if one vector is a prefix of the other (different\n * segment counts), the LONGER, more-segmented path is treated as more\n * specific (a missing segment ranks below any real one).\n *\n * @param a - The first route path\n * @param b - The second route path\n * @returns A negative number when `a` is more specific, positive when `b` is, else `0`\n *\n * @example\n * ```ts\n * compareSpecificity('/users/me', '/users/:id') // negative — literal wins\n * compareSpecificity('/users/:id', '/users/*rest') // negative — param beats wildcard\n * compareSpecificity('/users/:id', '/users/:id') // 0 — equal specificity\n * ```\n */\nexport function compareSpecificity(a: string, b: string): number {\n\tconst left = computeSpecificity(a)\n\tconst right = computeSpecificity(b)\n\tconst length = Math.max(left.length, right.length)\n\tfor (let index = 0; index < length; index += 1) {\n\t\t// A missing segment ranks below any real one (the shorter path is less specific).\n\t\tconst tierA = left[index] ?? -1\n\t\tconst tierB = right[index] ?? -1\n\t\tif (tierA !== tierB) return tierB - tierA\n\t}\n\treturn 0\n}\n\n/**\n * Narrow a raw `request.method` string into a typed {@link Method} — total,\n * never throws.\n *\n * @remarks\n * Guarded via {@link import('./constants.js').METHODS} (the seven registrable\n * HTTP methods); any other value (an unknown verb, non-uppercase casing)\n * resolves to `undefined` rather than throwing (§14 guard totality). Pure\n * leaf shared by the `Dispatcher`'s `handle` (§5.1 unknown-verb honesty) and\n * anywhere else a raw method string needs narrowing.\n *\n * @param value - The raw `request.method` string to narrow\n * @returns The matching {@link Method}, or `undefined` when `value` is not one\n * of the seven registrable methods\n *\n * @example\n * ```ts\n * parseMethod('GET') // 'GET'\n * parseMethod('PURGE') // undefined\n * parseMethod('get') // undefined — case-sensitive\n * ```\n */\nexport function parseMethod(value: string): Method | undefined {\n\tif (\n\t\tvalue === 'GET' ||\n\t\tvalue === 'POST' ||\n\t\tvalue === 'PUT' ||\n\t\tvalue === 'PATCH' ||\n\t\tvalue === 'DELETE' ||\n\t\tvalue === 'HEAD' ||\n\t\tvalue === 'OPTIONS'\n\t)\n\t\treturn value\n\treturn undefined\n}\n\n/**\n * Join a group prefix and a route path into one `/`-prefixed path, normalizing\n * duplicate or missing joining slashes.\n *\n * @remarks\n * {@link import('./types.js').GroupInterface} / {@link import('./types.js').DispatchGroupInterface}\n * compose a prefix with each registered entry's path this way — pure string\n * composition (§4.2.2), no independent state. Both a duplicated slash\n * (`'/api/'` + `'/users'`) and a missing one (`'/api'` + `'users'`) normalize\n * to a single joining slash. An empty `prefix` returns `path` unchanged (after\n * ensuring a leading slash); an empty `path` returns `prefix` unchanged.\n * Pure and total.\n *\n * @param prefix - The group prefix (e.g. `/api`)\n * @param path - The route path being joined under the prefix (e.g. `/users`)\n * @returns The joined `/`-prefixed path\n *\n * @example\n * ```ts\n * joinPaths('/api', '/users') // '/api/users'\n * joinPaths('/api/', '/users') // '/api/users'\n * joinPaths('/api', 'users') // '/api/users'\n * joinPaths('', '/users') // '/users'\n * joinPaths('/api', '') // '/api'\n * ```\n */\nexport function joinPaths(prefix: string, path: string): string {\n\tif (prefix === '') return path.startsWith('/') ? path : `/${path}`\n\tif (path === '') return prefix\n\tconst left = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix\n\tconst right = path.startsWith('/') ? path : `/${path}`\n\treturn `${left}${right}`\n}\n\n/**\n * Identity pass-through for a {@link RouteInput} that pins its `Path` generic\n * to the LITERAL registration-site string, so `context.params` types\n * correctly through {@link PathParams} without an explicit type argument.\n *\n * @remarks\n * A bare object literal handed straight to {@link import('./types.js').DispatcherInterface}'s\n * `add` already infers `Path` as a literal at that call site — but the moment\n * the object is built through an intermediate binding (a local `const route =\n * { method, path, handler }`) TypeScript widens `path` to `string` unless the\n * binding's own type is pinned. Wrapping the literal in `route(...)` supplies\n * that pin: its `const Path extends string` type parameter infers the NARROW\n * literal from the call, and the function returns its input completely\n * unchanged (same reference, no cloning, no validation) — this is a\n * compile-time typing aid only, not a construction step (contrast\n * {@link import('./factories.js')} `create*` entity factories). A\n * heterogeneous `RouteInput[]` built from several `route(...)` calls still\n * widens each element's `Path` to `string` once collected into one array\n * (§14) — the realistic ceiling this helper raises is PER-CALL typing at the\n * registration site, not a stored, still-literal-typed record.\n *\n * @typeParam Path - The route path pattern literal (drives `context.params`\n * via {@link PathParams})\n * @typeParam TState - The consumer's opaque per-request state type\n * @param input - The {@link RouteInput} to pass through unchanged\n * @returns `input`, unchanged (same reference)\n *\n * @example\n * ```ts\n * const input = route({\n * \tmethod: 'GET',\n * \tpath: '/users/:id',\n * \thandler: (_request, context) => new Response(context.params.id), // typed string\n * })\n * dispatcher.add(input)\n * ```\n */\nexport function route<const Path extends string, TState = undefined>(\n\tinput: RouteInput<Path, TState>,\n): RouteInput<Path, TState> {\n\treturn input\n}\n","import type { GroupInterface, RouteEntry, RouterInterface } from './types.js'\nimport { joinPaths } from './helpers.js'\n\n/**\n * A prefix-scoped registration handle over a {@link import('./Router.js').Router} —\n * pure string composition (AGENTS §4.2.2), no independent state or storage.\n *\n * @typeParam Meta - The entry payload type, matching the owning router\n *\n * @remarks\n * Every `add` composes `entry.path` as `joinPaths(prefix, entry.path)` and\n * forwards to the OWNING router, so grouped routes land in the SAME registry.\n * `group(prefix)` nests, composing prefixes via {@link joinPaths}.\n *\n * @example\n * ```ts\n * import { Router } from '@src/core'\n *\n * const router = new Router<{ readonly page: string }>()\n * const api = router.group('/api')\n * api.add({ path: '/users', meta: { page: 'list' } })\n * router.match('/api/users')?.path // '/api/users'\n * ```\n */\nexport class Group<Meta> implements GroupInterface<Meta> {\n\treadonly prefix: string\n\treadonly #parent: RouterInterface<Meta>\n\n\tconstructor(parent: RouterInterface<Meta>, prefix: string) {\n\t\tthis.#parent = parent\n\t\tthis.prefix = prefix\n\t}\n\n\tadd(entry: RouteEntry<Meta>): void\n\tadd(entries: readonly RouteEntry<Meta>[]): void\n\tadd(input: RouteEntry<Meta> | readonly RouteEntry<Meta>[]): void {\n\t\tconst inputs = Array.isArray(input) ? input : [input]\n\t\tthis.#parent.add(\n\t\t\tinputs.map((entry) => ({ ...entry, path: joinPaths(this.prefix, entry.path) })),\n\t\t)\n\t}\n\n\tgroup(prefix: string): GroupInterface<Meta> {\n\t\treturn new Group<Meta>(this.#parent, joinPaths(this.prefix, prefix))\n\t}\n}\n","import type {\n\tAnswerHandler,\n\tCompiledPath,\n\tGroupInterface,\n\tRouteEntry,\n\tRouterInterface,\n\tRouterMatch,\n\tRouterOptions,\n} from './types.js'\nimport { isString } from '@orkestrel/contract'\nimport { compareSpecificity, compilePath, matchPath } from './helpers.js'\nimport { Group } from './Group.js'\n\n/**\n * The path-matching + registry engine — registers `{ path, meta, name? }`\n * entries (compiling each path once) and resolves a concrete pathname to the\n * MOST SPECIFIC matching entry. The shared machine both the `Navigator`\n * (browser) and the `Dispatcher` (core, method-dimensioned) compose.\n *\n * @typeParam Meta - The opaque payload each entry carries and a match returns\n *\n * @remarks\n * - **Registration boundary guard (§14).** `add` validates each entry's\n * `path` — `isString` plus a leading `/` — and throws `TypeError` on a\n * malformed registration; `match` stays guard-free (the hot path).\n * - **Compile-once.** Each path is compiled exactly once at registration into\n * a parallel `#compiled` array, so `match` runs only a cached `exec` per\n * candidate.\n * - **Dedup via `key`.** When `options.key` is set, an entry whose computed\n * key already exists REPLACES the prior one IN PLACE (both the `#entries`\n * and `#compiled` arrays, at the existing index) — last write wins, no\n * engine rebuild. Omitted ⇒ every entry is kept, even duplicate paths.\n * - **Groups.** `group(prefix)` returns a {@link GroupInterface} that composes\n * `prefix` onto every entry it registers, nesting via {@link joinPaths}.\n *\n * @example\n * ```ts\n * const router = new Router<{ readonly page: string }>()\n * router.add({ path: '/users/:id', meta: { page: 'profile' } })\n * router.match('/users/7') // { path: '/users/:id', params: { id: '7' }, meta: { page: 'profile' } }\n * ```\n */\nexport class Router<Meta> implements RouterInterface<Meta> {\n\treadonly #entries: RouteEntry<Meta>[] = []\n\treadonly #compiled: CompiledPath[] = []\n\treadonly #sensitive: boolean\n\treadonly #key: ((entry: RouteEntry<Meta>) => string) | undefined\n\treadonly #index: Map<string, number> = new Map()\n\n\tconstructor(options?: RouterOptions<Meta>) {\n\t\tthis.#sensitive = options?.sensitive ?? true\n\t\tthis.#key = options?.key\n\t\tif (options?.entries !== undefined) this.add(options.entries)\n\t}\n\n\tget count(): number {\n\t\treturn this.#entries.length\n\t}\n\n\tadd(entry: RouteEntry<Meta>): void\n\tadd(entries: readonly RouteEntry<Meta>[]): void\n\tadd(input: RouteEntry<Meta> | readonly RouteEntry<Meta>[]): void {\n\t\tconst inputs = Array.isArray(input) ? input : [input]\n\t\tfor (const entry of inputs) this.#register(entry)\n\t}\n\n\tmatch(pathname: string, answers?: AnswerHandler<Meta>): RouterMatch<Meta> | undefined {\n\t\tlet best: { entry: RouteEntry<Meta>; params: Readonly<Record<string, string>> } | undefined\n\t\tfor (let index = 0; index < this.#entries.length; index += 1) {\n\t\t\tconst entry = this.#entries[index]\n\t\t\tconst compiled = this.#compiled[index]\n\t\t\tif (entry === undefined || compiled === undefined) continue\n\t\t\tif (answers !== undefined && !answers(entry.meta)) continue\n\t\t\tconst params = matchPath(compiled, pathname)\n\t\t\tif (params === undefined) continue\n\t\t\tif (best === undefined || compareSpecificity(entry.path, best.entry.path) < 0)\n\t\t\t\tbest = { entry, params }\n\t\t}\n\t\tif (best === undefined) return undefined\n\t\treturn {\n\t\t\tpath: best.entry.path,\n\t\t\tparams: best.params,\n\t\t\tmeta: best.entry.meta,\n\t\t\tname: best.entry.name,\n\t\t}\n\t}\n\n\tentries(): readonly RouteEntry<Meta>[]\n\tentries(pathname: string): readonly RouteEntry<Meta>[]\n\tentries(pathname?: string): readonly RouteEntry<Meta>[] {\n\t\tif (pathname === undefined) return [...this.#entries]\n\t\tconst out: RouteEntry<Meta>[] = []\n\t\tfor (let index = 0; index < this.#entries.length; index += 1) {\n\t\t\tconst entry = this.#entries[index]\n\t\t\tconst compiled = this.#compiled[index]\n\t\t\tif (entry === undefined || compiled === undefined) continue\n\t\t\tif (matchPath(compiled, pathname) !== undefined) out.push(entry)\n\t\t}\n\t\treturn out\n\t}\n\n\tgroup(prefix: string): GroupInterface<Meta> {\n\t\treturn new Group<Meta>(this, prefix)\n\t}\n\n\tclear(): void {\n\t\tthis.#entries.length = 0\n\t\tthis.#compiled.length = 0\n\t\tthis.#index.clear()\n\t}\n\n\t// Validate the registration boundary (§14: isString + leading '/'), then either replace an\n\t// existing entry IN PLACE (dedup via `#key`, last write wins) or append a new one — the\n\t// engine's compile-once invariant, kept in sync across the `#entries`/`#compiled` pair.\n\t#register(entry: RouteEntry<Meta>): void {\n\t\tif (!isString(entry.path) || !entry.path.startsWith('/'))\n\t\t\tthrow new TypeError(\n\t\t\t\t`a route path must be a string starting with \"/\", got ${JSON.stringify(entry.path)}`,\n\t\t\t)\n\t\tconst compiled = compilePath(entry.path, this.#sensitive)\n\t\tif (this.#key === undefined) {\n\t\t\tthis.#entries.push(entry)\n\t\t\tthis.#compiled.push(compiled)\n\t\t\treturn\n\t\t}\n\t\tconst key = this.#key(entry)\n\t\tconst existing = this.#index.get(key)\n\t\tif (existing !== undefined) {\n\t\t\tthis.#entries[existing] = entry\n\t\t\tthis.#compiled[existing] = compiled\n\t\t\treturn\n\t\t}\n\t\tthis.#index.set(key, this.#entries.length)\n\t\tthis.#entries.push(entry)\n\t\tthis.#compiled.push(compiled)\n\t}\n}\n","import type { DispatchGroupInterface, DispatcherInterface, RouteInput } from './types.js'\nimport { joinPaths } from './helpers.js'\n\n/**\n * A prefix-scoped registration handle over a\n * {@link import('./Dispatcher.js').Dispatcher} — the method-dimensioned\n * counterpart of `Group` (`Group.ts`).\n *\n * @typeParam TState - The consumer's opaque per-request state type, matching\n * the owning dispatcher\n *\n * @remarks\n * Every `add` composes `input.path` via {@link joinPaths} against\n * `this.prefix` and forwards to the OWNING dispatcher's `add` (its own §14\n * boundary guard still applies). Pure string composition (§4.2.2) — no\n * independent state or storage.\n *\n * @example\n * ```ts\n * import { Dispatcher } from '@src/core'\n *\n * const dispatcher = new Dispatcher()\n * const api = dispatcher.group('/api')\n * api.add({ method: 'GET', path: '/users', handler: () => new Response('ok') })\n * ```\n */\nexport class DispatchGroup<TState> implements DispatchGroupInterface<TState> {\n\treadonly prefix: string\n\treadonly #parent: DispatcherInterface<TState>\n\n\tconstructor(parent: DispatcherInterface<TState>, prefix: string) {\n\t\tthis.#parent = parent\n\t\tthis.prefix = prefix\n\t}\n\n\tadd<Path extends string>(input: RouteInput<Path, TState>): void\n\tadd(inputs: readonly RouteInput<string, TState>[]): void\n\tadd(input: RouteInput<string, TState> | readonly RouteInput<string, TState>[]): void {\n\t\tconst inputs = Array.isArray(input) ? input : [input]\n\t\tthis.#parent.add(\n\t\t\tinputs.map((route) => ({ ...route, path: joinPaths(this.prefix, route.path) })),\n\t\t)\n\t}\n\n\tgroup(prefix: string): DispatchGroupInterface<TState> {\n\t\treturn new DispatchGroup<TState>(this.#parent, joinPaths(this.prefix, prefix))\n\t}\n}\n","import type {\n\tDispatchGroupInterface,\n\tDispatcherEventMap,\n\tDispatcherInterface,\n\tDispatcherOptions,\n\tDispatchResult,\n\tMethod,\n\tRouteContext,\n\tRouteEntry,\n\tRouteInput,\n\tRouteRecord,\n\tRouterInterface,\n\tRouterMatch,\n} from './types.js'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport { Emitter } from '@orkestrel/emitter'\nimport { isFunction, isString } from '@orkestrel/contract'\nimport { METHODS } from './constants.js'\nimport { canonicalizePath, parseMethod } from './helpers.js'\nimport { Router } from './Router.js'\nimport { DispatchGroup } from './DispatchGroup.js'\n\n/**\n * The fetch-standard, method-dimensioned dispatch entity — layers HTTP method\n * dispatch and web-standard `Request`/`Response` handling over one internal\n * `Router<RouteRecord<TState>>`. The core machine the eventual server face\n * (§7) and any fetch-native runtime consumes directly.\n *\n * @typeParam TState - The consumer's opaque per-request state type\n *\n * @remarks\n * - **Dedup by `method + canonicalizePath`.** The underlying `Router` is\n * constructed with a `key` function so registering the same method+path\n * twice REPLACES the prior route in place (§5.1).\n * - **Registration boundary guard (§14).** `add` validates each input's\n * `handler` (`isFunction`) and `method` (must be in {@link METHODS}) —\n * throws `TypeError` on a malformed registration; path validation is\n * delegated to the underlying `Router`'s own guard. `match`/`handle` stay\n * guard-free.\n * - **Auto-`HEAD` / auto-`OPTIONS`.** A `HEAD` request with no registered\n * `HEAD` route runs the matching `GET` handler and strips the response\n * body; an `OPTIONS` request with no registered `OPTIONS` route answers\n * `204` with a derived `Allow` header.\n * - **Handler throws propagate.** `handle` never invents an error boundary —\n * a handler throw reaches the caller uncaught (§5.1).\n * - **Emitter (§13).** Owns a `#emitter` for {@link DispatcherEventMap};\n * `match`/`miss` fire AFTER resolution, before the handler/responder runs.\n *\n * @example\n * ```ts\n * const dispatcher = new Dispatcher<{ readonly userId: string }>()\n * dispatcher.add({\n * \tmethod: 'GET',\n * \tpath: '/users/:id',\n * \thandler: (request, context) => Response.json({ id: context.params.id }),\n * })\n * const response = await dispatcher.handle(new Request('http://x/users/7'), { userId: 'me' })\n * ```\n */\nexport class Dispatcher<TState = undefined> implements DispatcherInterface<TState> {\n\treadonly router: RouterInterface<RouteRecord<TState>>\n\treadonly #emitter: Emitter<DispatcherEventMap>\n\treadonly #unmatched: (request: Request) => Response | Promise<Response>\n\treadonly #unmethoded: (request: Request, allow: readonly Method[]) => Response | Promise<Response>\n\n\tconstructor(options?: DispatcherOptions<TState>) {\n\t\tthis.router = new Router<RouteRecord<TState>>({\n\t\t\tsensitive: options?.sensitive,\n\t\t\tkey: (entry) => `${entry.meta.method} ${canonicalizePath(entry.path)}`,\n\t\t})\n\t\tthis.#emitter = new Emitter<DispatcherEventMap>({ on: options?.on, error: options?.error })\n\t\tthis.#unmatched =\n\t\t\toptions?.unmatched ?? ((_request) => new Response('Not Found', { status: 404 }))\n\t\tthis.#unmethoded =\n\t\t\toptions?.unmethoded ??\n\t\t\t((_request, allow) =>\n\t\t\t\tnew Response('Method Not Allowed', {\n\t\t\t\t\tstatus: 405,\n\t\t\t\t\theaders: { Allow: allow.join(', ') },\n\t\t\t\t}))\n\t\tif (options?.routes !== undefined) this.add(options.routes)\n\t}\n\n\tget emitter(): EmitterInterface<DispatcherEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tadd<Path extends string>(input: RouteInput<Path, TState>): void\n\tadd(inputs: readonly RouteInput<string, TState>[]): void\n\tadd(input: RouteInput<string, TState> | readonly RouteInput<string, TState>[]): void {\n\t\tconst inputs = Array.isArray(input) ? input : [input]\n\t\tfor (const route of inputs) this.#register(route)\n\t}\n\n\tgroup(prefix: string): DispatchGroupInterface<TState> {\n\t\treturn new DispatchGroup<TState>(this, prefix)\n\t}\n\n\tmatch(method: Method, pathname: string): DispatchResult<TState> {\n\t\tconst hit = this.router.match(pathname, (meta) => meta.method === method)\n\t\tif (hit !== undefined) return { status: 'matched', match: hit }\n\t\tif (method === 'HEAD') {\n\t\t\tconst getHit = this.router.match(pathname, (meta) => meta.method === 'GET')\n\t\t\tif (getHit !== undefined) return { status: 'matched', match: getHit }\n\t\t}\n\t\tconst allow = this.#allow(pathname)\n\t\tif (allow.length === 0) return { status: 'unmatched' }\n\t\treturn { status: 'unmethoded', allow }\n\t}\n\n\tasync handle(request: Request, state: TState): Promise<Response> {\n\t\tconst url = new URL(request.url)\n\t\tconst pathname = url.pathname\n\t\tconst requested = request.method\n\t\tconst method = parseMethod(requested)\n\t\tif (method === undefined) {\n\t\t\tconst allow = this.#allow(pathname)\n\t\t\tif (allow.length === 0) {\n\t\t\t\tthis.#emitter.emit('miss', requested, pathname, 'unmatched')\n\t\t\t\treturn this.#unmatched(request)\n\t\t\t}\n\t\t\tthis.#emitter.emit('miss', requested, pathname, 'unmethoded')\n\t\t\treturn this.#unmethoded(request, allow)\n\t\t}\n\t\tconst result = this.match(method, pathname)\n\t\tif (result.status === 'matched')\n\t\t\treturn this.#respondMatched(request, state, method, result.match, url)\n\t\tif (result.status === 'unmethoded') {\n\t\t\tif (method === 'OPTIONS') return this.#respondAutoOptions(pathname, result.allow)\n\t\t\tthis.#emitter.emit('miss', method, pathname, 'unmethoded')\n\t\t\treturn this.#unmethoded(request, result.allow)\n\t\t}\n\t\tthis.#emitter.emit('miss', method, pathname, 'unmatched')\n\t\treturn this.#unmatched(request)\n\t}\n\n\tdestroy(): void {\n\t\tthis.#emitter.destroy()\n\t}\n\n\t// Validate the registration boundary (§14: handler function, known method), then delegate\n\t// path validation + dedup to the underlying `Router`'s own guard.\n\t#register(input: RouteInput<string, TState>): void {\n\t\tif (!isFunction(input.handler))\n\t\t\tthrow new TypeError(\n\t\t\t\t`a route handler must be a function, got ${JSON.stringify(input.handler)}`,\n\t\t\t)\n\t\tif (!isString(input.method) || !METHODS.has(input.method))\n\t\t\tthrow new TypeError(\n\t\t\t\t`a route method must be one of ${[...METHODS].join(', ')}, got ${JSON.stringify(input.method)}`,\n\t\t\t)\n\t\tthis.router.add({\n\t\t\tpath: input.path,\n\t\t\tname: input.name,\n\t\t\tmeta: { method: input.method, handler: input.handler, name: input.name },\n\t\t})\n\t}\n\n\t// The derived `Allow` set for a pathname — every distinct registered method, with `HEAD`\n\t// added whenever `GET` is present and `HEAD` is not explicitly registered (§5.1).\n\t#allow(pathname: string): readonly Method[] {\n\t\tconst entries: readonly RouteEntry<RouteRecord<TState>>[] = this.router.entries(pathname)\n\t\tconst methods = new Set<Method>()\n\t\tfor (const entry of entries) methods.add(entry.meta.method)\n\t\tif (methods.has('GET')) methods.add('HEAD')\n\t\treturn [...methods]\n\t}\n\n\t// A matched dispatch — either the winning handler runs directly, or (for a derived `HEAD`\n\t// with no explicit `HEAD` route) the `GET` handler runs and the response body is stripped.\n\tasync #respondMatched(\n\t\trequest: Request,\n\t\tstate: TState,\n\t\tmethod: Method,\n\t\tmatch: RouterMatch<RouteRecord<TState>>,\n\t\turl: URL,\n\t): Promise<Response> {\n\t\tthis.#emitter.emit('match', method, match.path)\n\t\tconst context: RouteContext<string, TState> = {\n\t\t\tparams: match.params,\n\t\t\tpattern: match.path,\n\t\t\turl,\n\t\t\tstate,\n\t\t}\n\t\tconst response = await match.meta.handler(request, context)\n\t\tif (method === 'HEAD' && match.meta.method === 'GET')\n\t\t\treturn new Response(null, {\n\t\t\t\tstatus: response.status,\n\t\t\t\tstatusText: response.statusText,\n\t\t\t\theaders: response.headers,\n\t\t\t})\n\t\treturn response\n\t}\n\n\t// Derived `OPTIONS` — no explicit `OPTIONS` route registered for this pathname: answer\n\t// `204` with the derived `Allow` set (adding `OPTIONS` itself, always answerable).\n\t#respondAutoOptions(pathname: string, allow: readonly Method[]): Response {\n\t\tthis.#emitter.emit('match', 'OPTIONS', pathname)\n\t\tconst headers = new Headers({ Allow: [...allow, 'OPTIONS'].join(', ') })\n\t\treturn new Response(null, { status: 204, headers })\n\t}\n}\n","import type {\n\tDispatcherInterface,\n\tDispatcherOptions,\n\tRouterInterface,\n\tRouterOptions,\n} from './types.js'\nimport { Dispatcher } from './Dispatcher.js'\nimport { Router } from './Router.js'\n\n/**\n * Create a {@link RouterInterface} — the pure path-matching + registry engine\n * shared by the browser `Navigator` and the core `Dispatcher`.\n *\n * @remarks\n * Prefer this over `new Router(...)` at call sites that only need the\n * interface; an entity that OWNS a `Router` internally (like `Dispatcher`)\n * still constructs `new Router(...)` directly.\n *\n * @typeParam Meta - The opaque payload each entry carries and a match returns\n * @param options - Optional initial `entries`, the `sensitive` case toggle\n * (default `true`), and a `key` dedup identity function\n * @returns A {@link RouterInterface}\n *\n * @example\n * ```ts\n * import { createRouter } from '@src/core'\n *\n * const router = createRouter<{ readonly page: string }>()\n * router.add({ path: '/users/:id', meta: { page: 'profile' } })\n * router.match('/users/7') // { path: '/users/:id', params: { id: '7' }, meta: { page: 'profile' } }\n * ```\n */\nexport function createRouter<Meta>(options?: RouterOptions<Meta>): RouterInterface<Meta> {\n\treturn new Router<Meta>(options)\n}\n\n/**\n * Create a {@link DispatcherInterface} — the fetch-standard, method-\n * dimensioned dispatch entity over one internal `Router<RouteRecord<TState>>`.\n *\n * @remarks\n * Prefer this over `new Dispatcher(...)` at call sites that only need the\n * interface.\n *\n * @typeParam TState - The consumer's opaque per-request state type (default\n * `undefined` for stateless use)\n * @param options - Optional initial `routes`, the `sensitive` case toggle,\n * the `unmatched`/`unmethoded` default-responder overrides, and the AGENTS\n * §13 emitter `on`/`error` wiring\n * @returns A {@link DispatcherInterface}\n *\n * @example\n * ```ts\n * import { createDispatcher } from '@src/core'\n *\n * const dispatcher = createDispatcher<{ readonly userId: string }>({\n * \troutes: [\n * \t\t{ method: 'GET', path: '/health', handler: () => new Response('ok') },\n * \t],\n * })\n * const response = await dispatcher.handle(new Request('http://x/health'), { userId: 'me' })\n * ```\n */\nexport function createDispatcher<TState = undefined>(\n\toptions?: DispatcherOptions<TState>,\n): DispatcherInterface<TState> {\n\treturn new Dispatcher<TState>(options)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["#parent","#entries","#compiled","#sensitive","#key","#index","#register","#parent","#emitter","#unmatched","#unmethoded","#register","#allow","#respondUnmatched","#respondUnmethoded","#respondMatched","#respondAutoOptions"],"sources":["../../../src/core/constants.ts","../../../src/core/helpers.ts","../../../src/core/Group.ts","../../../src/core/Router.ts","../../../src/core/DispatchGroup.ts","../../../src/core/Dispatcher.ts","../../../src/core/factories.ts"],"sourcesContent":["// ============================================================================\n// Core constants — the §5 centralized home for module-scope data used by the\n// matching engine and the fetch dispatcher. Every declaration here is frozen\n// and `export`ed per AGENTS §5.\n// ============================================================================\n\n/**\n * The complete set of HTTP methods a {@link import('./types.js').Dispatcher}\n * registers routes under — backs the registration guard (`add` rejects any\n * `method` outside this set) and the auto-`OPTIONS` `Allow` derivation.\n *\n * @remarks\n * A `ReadonlySet` of the seven {@link import('./types.js').Method} literals:\n * `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`. `HEAD` is\n * included even though it is never required at registration (a `GET` route\n * auto-answers `HEAD`) — it is still a valid method to register explicitly.\n *\n * @example\n * ```ts\n * METHODS.has('GET') // true\n * METHODS.has('TRACE') // false\n * ```\n */\nexport const METHODS: ReadonlySet<string> = Object.freeze(\n\tnew Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']),\n)\n\n/**\n * Specificity tier for a **literal** path segment (`/users`) — the highest\n * tier, always outranking a param or wildcard segment at the same position.\n *\n * @remarks\n * Consumed by `computeSpecificity` (U1 `helpers.ts`) when ranking candidate\n * matches left-to-right at the earliest differing segment (§4 precedence).\n *\n * @example\n * ```ts\n * TIER_LITERAL > TIER_PARAM // true\n * ```\n */\nexport const TIER_LITERAL = 2\n\n/**\n * Specificity tier for a **param** path segment (`:name`) — ranks below a\n * literal segment and above a wildcard segment at the same position.\n *\n * @remarks\n * Consumed by `computeSpecificity` (U1 `helpers.ts`) alongside {@link TIER_LITERAL}\n * and {@link TIER_WILDCARD}.\n *\n * @example\n * ```ts\n * TIER_PARAM > TIER_WILDCARD // true\n * ```\n */\nexport const TIER_PARAM = 1\n\n/**\n * Specificity tier for a **wildcard** path segment (`*name`) — the lowest\n * tier; a wildcard only ever wins against another wildcard shape (an\n * equal-specificity tie resolved by registration order).\n *\n * @remarks\n * Consumed by `computeSpecificity` (U1 `helpers.ts`).\n *\n * @example\n * ```ts\n * TIER_WILDCARD // 0\n * ```\n */\nexport const TIER_WILDCARD = 0\n","import type { CompiledPath, Method, RouteEntry, RouteInput } from './types.js'\nimport { TIER_LITERAL, TIER_PARAM, TIER_WILDCARD } from './constants.js'\n\n// The PURE path-matching primitives (AGENTS §4.3 multi-word names — module scope,\n// no entity context). Every one is exported (the centralized-file rule, §5): a\n// consumer composes them directly, or reaches them through the `Router` engine.\n// They speak ONLY `string` / `RegExp` / `Record` and `decodeURIComponent` (a\n// platform global valid in Node and the browser alike) — NO DOM, NO `node:*` — so\n// the SAME engine drives a method-dimensioned server dispatcher and a method-less\n// browser navigator.\n\n/**\n * Escape every regex metacharacter in a literal string so it can be embedded\n * inside a larger `RegExp` source without being interpreted as syntax.\n *\n * @remarks\n * {@link compilePath} escapes the literal segments of a route pattern with this\n * before splicing in `:name` / `*name` capture groups, so a path like\n * `/files/:name.json` matches the `.` literally rather than as \"any character\".\n * Pure and total — never throws.\n *\n * @param value - The literal string to escape\n * @returns `value` with every regex metacharacter backslash-escaped\n *\n * @example\n * ```ts\n * escapeRegExp('a.b+c') // 'a\\\\.b\\\\+c'\n * new RegExp(`^${escapeRegExp('a.b')}$`).test('a.b') // true\n * new RegExp(`^${escapeRegExp('a.b')}$`).test('axb') // false\n * ```\n */\nexport function escapeRegExp(value: string): string {\n\treturn value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/**\n * Canonicalize a route path for REGISTRY IDENTITY — strip a single trailing\n * slash, except the root `/` (and the empty pattern). The trailing-slash fold\n * {@link compilePath} normalizes a pattern through, so identity agrees with the\n * matcher.\n *\n * @remarks\n * Mirrors {@link compilePath}'s trailing-slash folding: `/users/` canonicalizes\n * to `/users` (the two compile to the same regex and match the same\n * pathnames), while the root `/` and the empty `''` are EXEMPT (a bare `/`\n * already matches `/`; stripping it would break that). Pure and total — a path\n * without a trailing slash returns unchanged.\n *\n * @param path - The route path pattern\n * @returns The canonical path (one trailing slash removed, except `/` and `''`)\n *\n * @example\n * ```ts\n * canonicalizePath('/users/') // '/users'\n * canonicalizePath('/users') // '/users'\n * canonicalizePath('/') // '/'\n * canonicalizePath('') // ''\n * ```\n */\nexport function canonicalizePath(path: string): string {\n\treturn path.length > 1 && path.endsWith('/') ? path.slice(0, -1) : path\n}\n\n/**\n * Compute the registry key for a method-dimensioned dispatcher route.\n *\n * @remarks\n * Combines the route record's HTTP method with the outer entry's canonical\n * path, so registrations that differ only by one trailing slash replace each\n * other while registrations for different methods remain distinct.\n *\n * @param entry - The dispatcher route entry to identify\n * @returns The canonical `METHOD /path` registry key\n *\n * @example\n * ```ts\n * computeDispatchKey({ path: '/health/', meta: { method: 'GET' } }) // 'GET /health'\n * ```\n */\nexport function computeDispatchKey(entry: RouteEntry<{ readonly method: Method }>): string {\n\treturn `${entry.meta.method} ${canonicalizePath(entry.path)}`\n}\n\n/**\n * Compile a route path pattern into an anchored regex and its ordered param\n * names.\n *\n * @remarks\n * Splits the CANONICALIZED path into segments. Each `:name` segment becomes a\n * `([^/]+)` capture group; the FINAL segment may instead be `*name`, which\n * becomes a `(.+)` capture spanning the REST of the path including slashes — a\n * wildcard segment anywhere but last is a registration-time programmer error\n * and throws `TypeError` (§14 construction/registration boundary). Every regex\n * metacharacter in a literal segment is escaped first ({@link escapeRegExp}),\n * so a path like `/files/:name.json` matches the `.` literally apart from the\n * param. The regex is anchored (`^…$`), so it matches the whole pathname, not\n * a prefix.\n *\n * **Trailing slash is INSENSITIVE** (Express's `strict: false` default): a\n * single trailing slash on the request path is OPTIONAL, so `/users` matches\n * both `/users` and `/users/`, and `/users/:id` matches both `/users/me` and\n * `/users/me/`. This is NOT prefix matching — a deeper path is still a\n * distinct segment, so `/api` does not match `/api/users`. The ROOT `/` (and\n * the empty pattern `''`) are EXEMPT — they are not stripped, so `/` stays\n * `^/$` and `''` stays `^$`.\n *\n * `sensitive` (default `true`) controls case folding: `false` adds the `i`\n * regex flag, so `/Users` matches `/users`. The pattern's own casing is never\n * altered — only the matching behavior.\n *\n * @param path - The route path pattern (e.g. `/users/:id`, `/files/*rest`)\n * @param sensitive - Case-sensitive matching (default `true`)\n * @returns The {@link CompiledPath} — its `regex` + ordered `params`\n * @throws {TypeError} When a `*name` wildcard segment is not the FINAL segment\n *\n * @example\n * ```ts\n * const { regex, params } = compilePath('/users/:id/posts/:slug')\n * params // ['id', 'slug']\n * regex.exec('/users/7/posts/hello') // ['…', '7', 'hello']\n * regex.test('/users/7/posts/hello/') // true — the trailing slash is optional\n *\n * compilePath('/files/*rest').regex.test('/files/a/b.png') // true\n * compilePath('/Users', false).regex.test('/users') // true — case-insensitive\n * ```\n */\nexport function compilePath(path: string, sensitive = true): CompiledPath {\n\tconst params: string[] = []\n\t// Normalize ONE trailing slash off the pattern so `/users/` compiles like `/users`\n\t// — except the root `/` (and the empty pattern), which must keep matching `/` / `''`.\n\tconst normalized = canonicalizePath(path)\n\tconst segments = normalized.split('/')\n\tconst compiledSegments = segments.map((segment, index) => {\n\t\tconst isFinal = index === segments.length - 1\n\t\t// A wildcard-shaped segment head (`*name`) is only valid as the FINAL segment — a\n\t\t// registration-time programmer error anywhere else (§14 boundary guard).\n\t\tif (!isFinal && /^\\*[A-Za-z_]\\w*/.test(segment))\n\t\t\tthrow new TypeError(\n\t\t\t\t`a wildcard segment (\"${segment}\") must be the final segment of a path pattern, got \"${path}\"`,\n\t\t\t)\n\t\t// Classification and compilation share ONE segment parser (§4 fix) — the tier\n\t\t// {@link classifySegment} assigns is exactly the shape compiled here.\n\t\tconst tier = classifySegment(segment, isFinal)\n\t\tif (tier === TIER_WILDCARD) {\n\t\t\tparams.push(segment.slice(1))\n\t\t\treturn '(.+)'\n\t\t}\n\t\tif (tier === TIER_PARAM) {\n\t\t\tconst match = /^:([A-Za-z_]\\w*)/.exec(segment)\n\t\t\tconst name = match?.[1] ?? ''\n\t\t\tparams.push(name)\n\t\t\treturn `([^/]+)${escapeRegExp(segment.slice(1 + name.length))}`\n\t\t}\n\t\treturn escapeRegExp(segment)\n\t})\n\tconst pattern = compiledSegments.join('/')\n\t// Allow ONE optional trailing slash before the `$` anchor (Express `strict: false`),\n\t// EXCEPT for the root `/` and the empty pattern — those anchor exactly so they keep\n\t// matching only `/` / `''` (a bare `/?` there would let `''` match `/` and vice versa).\n\tconst suffix = normalized === '/' || normalized === '' ? '' : '/?'\n\tconst flags = sensitive ? '' : 'i'\n\treturn { regex: new RegExp(`^${pattern}${suffix}$`, flags), params }\n}\n\n/**\n * URL-decode one captured param value, tolerating a malformed percent-escape —\n * the decode {@link matchPath} applies to each captured group.\n *\n * @remarks\n * A bad `%` sequence is not a reason to reject an otherwise-matching route, so\n * a `decodeURIComponent` that would throw falls back to the raw value\n * (mirroring the cookie / token boundary readers, AGENTS §14). Total — never\n * throws.\n *\n * @param value - The raw captured param value\n * @returns The URL-decoded value, or the raw value when decoding would throw\n *\n * @example\n * ```ts\n * decodeParam('a%2Fb') // 'a/b'\n * decodeParam('100%25') // '100%'\n * decodeParam('%') // '%' — malformed escape stays literal\n * ```\n */\nexport function decodeParam(value: string): string {\n\ttry {\n\t\treturn decodeURIComponent(value)\n\t} catch {\n\t\t// A malformed `%` escape stays literal rather than throwing — the match still succeeds.\n\t\treturn value\n\t}\n}\n\n/**\n * Extract the URL-decoded params a compiled path captures from a concrete\n * pathname, or `undefined` when the pathname does not match.\n *\n * @remarks\n * Runs the {@link CompiledPath} `regex` against `pathname` (a single `exec`); a\n * miss returns `undefined`. On a hit it walks `params` POSITIONALLY — the\n * `n`-th param name pairs with the `n`-th capture group — and URL-decodes each\n * value with {@link decodeParam}. Returns a frozen `name → value` record (empty\n * for a parameterless path). Total — never throws.\n *\n * @param compiled - The {@link CompiledPath} from {@link compilePath}\n * @param pathname - The concrete request pathname to match (e.g. `/users/7`)\n * @returns The decoded params on a hit, or `undefined` on a miss\n *\n * @example\n * ```ts\n * const compiled = compilePath('/users/:id')\n * matchPath(compiled, '/users/7') // { id: '7' }\n * matchPath(compiled, '/users/a%2Fb') // { id: 'a/b' } — decoded\n * matchPath(compiled, '/posts/7') // undefined\n * ```\n */\nexport function matchPath(\n\tcompiled: CompiledPath,\n\tpathname: string,\n): Readonly<Record<string, string>> | undefined {\n\tconst result = compiled.regex.exec(pathname)\n\tif (result === null) return undefined\n\tconst params: Record<string, string> = {}\n\tfor (let index = 0; index < compiled.params.length; index += 1) {\n\t\tconst name = compiled.params[index]\n\t\tconst value = result[index + 1]\n\t\tif (name !== undefined && value !== undefined) params[name] = decodeParam(value)\n\t}\n\treturn Object.freeze(params)\n}\n\n/**\n * Classify one path segment into its specificity TIER — the SAME syntax\n * {@link compilePath} rewrites: a syntactically valid `:name` head is a PARAM\n * segment, a final `*name` is a WILDCARD segment, everything else (including a\n * literal segment that merely CONTAINS a `:` mid-string, e.g. `a:b`) is a\n * LITERAL segment.\n *\n * @remarks\n * This is the fix over the old engine's bug: the old classifier ranked any\n * segment `includes(':')` as a param, so a literal segment like `a:b` was\n * mis-tiered even though {@link compilePath} compiles it literally. Sharing one\n * segment parser between compilation and classification keeps the two in\n * agreement (§4 fixes). Pure and total.\n *\n * @param segment - One `/`-split path segment\n * @param isFinal - Whether `segment` is the last segment of its path (only the\n * final segment may be classified as a wildcard)\n * @returns The segment's specificity tier — {@link import('./constants.js').TIER_LITERAL},\n * {@link import('./constants.js').TIER_PARAM}, or\n * {@link import('./constants.js').TIER_WILDCARD}\n *\n * @example\n * ```ts\n * classifySegment(':id', true) // 1 — TIER_PARAM\n * classifySegment('*rest', true) // 0 — TIER_WILDCARD\n * classifySegment('a:b', true) // 2 — TIER_LITERAL — the old bug's regression case\n * classifySegment('users', false) // 2 — TIER_LITERAL\n * ```\n */\nexport function classifySegment(segment: string, isFinal: boolean): number {\n\tif (isFinal && /^\\*[A-Za-z_]\\w*$/.test(segment)) return TIER_WILDCARD\n\tif (/^:[A-Za-z_]\\w*/.test(segment)) return TIER_PARAM\n\treturn TIER_LITERAL\n}\n\n/**\n * Compute a route path's SPECIFICITY VECTOR — the per-segment type ranking\n * that breaks a tie when several registered routes match the same concrete\n * pathname.\n *\n * @remarks\n * Splits the CANONICALIZED path into segments (on `/`) and maps each to its\n * specificity tier via {@link classifySegment} — the same segment parser\n * {@link compilePath} uses, so a literal segment that merely contains a `:`\n * (e.g. `a:b`) is correctly tiered as literal rather than param (the old\n * engine's bug, fixed here). The standard route-precedence rule compares two\n * matching routes' vectors LEFT-TO-RIGHT: at the first index where the tiers\n * differ, the HIGHER tier (a literal over a param over a wildcard) is MORE\n * SPECIFIC and wins — so `/users/me` (`[2, 2]`) beats `/users/:id` (`[2, 1]`)\n * beats `/users/*rest` (`[2, 0]`) regardless of registration order. Two routes\n * that match the SAME concrete pathname necessarily have the same segment\n * count in the common case; {@link compareSpecificity} handles the general\n * case for totality.\n *\n * @param path - The route path pattern (e.g. `/users/:id`)\n * @returns The per-segment specificity tiers, in order\n *\n * @example\n * ```ts\n * computeSpecificity('/users/me') // [2, 2]\n * computeSpecificity('/users/:id') // [2, 1]\n * computeSpecificity('/files/*rest') // [2, 0]\n * computeSpecificity('/a:b') // [2] — literal, not param — the classification fix\n * ```\n */\nexport function computeSpecificity(path: string): readonly number[] {\n\tconst segments = canonicalizePath(path).split('/')\n\treturn segments.map((segment, index) => classifySegment(segment, index === segments.length - 1))\n}\n\n/**\n * Compare two route paths by SPECIFICITY — the comparator that picks the\n * most-specific matching route (literal-over-param-over-wildcard,\n * registration-order-independent).\n *\n * @remarks\n * Compares the two paths' {@link computeSpecificity} vectors LEFT-TO-RIGHT and\n * returns a standard `Array.sort` ordering: a NEGATIVE number when `a` is MORE\n * specific than `b` (so a descending-specificity sort puts `a` first),\n * positive when `b` is more specific, `0` when neither out-ranks the other\n * across the compared segments. At the first index where the tiers differ,\n * the higher tier wins; if one vector is a prefix of the other (different\n * segment counts), the LONGER, more-segmented path is treated as more\n * specific (a missing segment ranks below any real one).\n *\n * @param a - The first route path\n * @param b - The second route path\n * @returns A negative number when `a` is more specific, positive when `b` is, else `0`\n *\n * @example\n * ```ts\n * compareSpecificity('/users/me', '/users/:id') // negative — literal wins\n * compareSpecificity('/users/:id', '/users/*rest') // negative — param beats wildcard\n * compareSpecificity('/users/:id', '/users/:id') // 0 — equal specificity\n * ```\n */\nexport function compareSpecificity(a: string, b: string): number {\n\tconst left = computeSpecificity(a)\n\tconst right = computeSpecificity(b)\n\tconst length = Math.max(left.length, right.length)\n\tfor (let index = 0; index < length; index += 1) {\n\t\t// A missing segment ranks below any real one (the shorter path is less specific).\n\t\tconst tierA = left[index] ?? -1\n\t\tconst tierB = right[index] ?? -1\n\t\tif (tierA !== tierB) return tierB - tierA\n\t}\n\treturn 0\n}\n\n/**\n * Narrow a raw `request.method` string into a typed {@link Method} — total,\n * never throws.\n *\n * @remarks\n * Guarded via {@link import('./constants.js').METHODS} (the seven registrable\n * HTTP methods); any other value (an unknown verb, non-uppercase casing)\n * resolves to `undefined` rather than throwing (§14 guard totality). Pure\n * leaf shared by the `Dispatcher`'s `handle` (§5.1 unknown-verb honesty) and\n * anywhere else a raw method string needs narrowing.\n *\n * @param value - The raw `request.method` string to narrow\n * @returns The matching {@link Method}, or `undefined` when `value` is not one\n * of the seven registrable methods\n *\n * @example\n * ```ts\n * parseMethod('GET') // 'GET'\n * parseMethod('PURGE') // undefined\n * parseMethod('get') // undefined — case-sensitive\n * ```\n */\nexport function parseMethod(value: string): Method | undefined {\n\tif (\n\t\tvalue === 'GET' ||\n\t\tvalue === 'POST' ||\n\t\tvalue === 'PUT' ||\n\t\tvalue === 'PATCH' ||\n\t\tvalue === 'DELETE' ||\n\t\tvalue === 'HEAD' ||\n\t\tvalue === 'OPTIONS'\n\t)\n\t\treturn value\n\treturn undefined\n}\n\n/**\n * Join a group prefix and a route path into one `/`-prefixed path, normalizing\n * duplicate or missing joining slashes.\n *\n * @remarks\n * {@link import('./types.js').GroupInterface} / {@link import('./types.js').DispatchGroupInterface}\n * compose a prefix with each registered entry's path this way — pure string\n * composition (§4.2.2), no independent state. Both a duplicated slash\n * (`'/api/'` + `'/users'`) and a missing one (`'/api'` + `'users'`) normalize\n * to a single joining slash. An empty `prefix` returns `path` unchanged (after\n * ensuring a leading slash); an empty `path` returns `prefix` unchanged.\n * Pure and total.\n *\n * @param prefix - The group prefix (e.g. `/api`)\n * @param path - The route path being joined under the prefix (e.g. `/users`)\n * @returns The joined `/`-prefixed path\n *\n * @example\n * ```ts\n * joinPaths('/api', '/users') // '/api/users'\n * joinPaths('/api/', '/users') // '/api/users'\n * joinPaths('/api', 'users') // '/api/users'\n * joinPaths('', '/users') // '/users'\n * joinPaths('/api', '') // '/api'\n * ```\n */\nexport function joinPaths(prefix: string, path: string): string {\n\tif (prefix === '') return path.startsWith('/') ? path : `/${path}`\n\tif (path === '') return prefix\n\tconst left = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix\n\tconst right = path.startsWith('/') ? path : `/${path}`\n\treturn `${left}${right}`\n}\n\n/**\n * Identity pass-through for a {@link RouteInput} that pins its `Path` generic\n * to the LITERAL registration-site string, so `context.params` types\n * correctly through {@link PathParams} without an explicit type argument.\n *\n * @remarks\n * A bare object literal handed straight to {@link import('./types.js').DispatcherInterface}'s\n * `add` already infers `Path` as a literal at that call site — but the moment\n * the object is built through an intermediate binding (a local `const route =\n * { method, path, handler }`) TypeScript widens `path` to `string` unless the\n * binding's own type is pinned. Wrapping the literal in `route(...)` supplies\n * that pin: its `const Path extends string` type parameter infers the NARROW\n * literal from the call, and the function returns its input completely\n * unchanged (same reference, no cloning, no validation) — this is a\n * compile-time typing aid only, not a construction step (contrast\n * {@link import('./factories.js')} `create*` entity factories). A\n * heterogeneous `RouteInput[]` built from several `route(...)` calls still\n * widens each element's `Path` to `string` once collected into one array\n * (§14) — the realistic ceiling this helper raises is PER-CALL typing at the\n * registration site, not a stored, still-literal-typed record.\n *\n * @typeParam Path - The route path pattern literal (drives `context.params`\n * via {@link PathParams})\n * @typeParam TState - The consumer's opaque per-request state type\n * @param input - The {@link RouteInput} to pass through unchanged\n * @returns `input`, unchanged (same reference)\n *\n * @example\n * ```ts\n * const input = route({\n * \tmethod: 'GET',\n * \tpath: '/users/:id',\n * \thandler: (_request, context) => new Response(context.params.id), // typed string\n * })\n * dispatcher.add(input)\n * ```\n */\nexport function route<const Path extends string, TState = undefined>(\n\tinput: RouteInput<Path, TState>,\n): RouteInput<Path, TState> {\n\treturn input\n}\n","import type { GroupInterface, RouteEntry, RouterInterface } from './types.js'\nimport { joinPaths } from './helpers.js'\n\n/**\n * A prefix-scoped registration handle over a {@link import('./Router.js').Router} —\n * pure string composition (AGENTS §4.2.2), no independent state or storage.\n *\n * @typeParam Meta - The entry payload type, matching the owning router\n *\n * @remarks\n * Every `add` composes `entry.path` as `joinPaths(prefix, entry.path)` and\n * forwards to the OWNING router, so grouped routes land in the SAME registry.\n * `group(prefix)` nests, composing prefixes via {@link joinPaths}.\n *\n * @example\n * ```ts\n * import { Router } from '@src/core'\n *\n * const router = new Router<{ readonly page: string }>()\n * const api = router.group('/api')\n * api.add({ path: '/users', meta: { page: 'list' } })\n * router.match('/api/users')?.path // '/api/users'\n * ```\n */\nexport class Group<Meta> implements GroupInterface<Meta> {\n\treadonly prefix: string\n\treadonly #parent: RouterInterface<Meta>\n\n\tconstructor(parent: RouterInterface<Meta>, prefix: string) {\n\t\tthis.#parent = parent\n\t\tthis.prefix = prefix\n\t}\n\n\tadd(entry: RouteEntry<Meta>): void\n\tadd(entries: readonly RouteEntry<Meta>[]): void\n\tadd(input: RouteEntry<Meta> | readonly RouteEntry<Meta>[]): void {\n\t\tconst inputs = Array.isArray(input) ? input : [input]\n\t\tthis.#parent.add(\n\t\t\tinputs.map((entry) => ({ ...entry, path: joinPaths(this.prefix, entry.path) })),\n\t\t)\n\t}\n\n\tgroup(prefix: string): GroupInterface<Meta> {\n\t\treturn new Group<Meta>(this.#parent, joinPaths(this.prefix, prefix))\n\t}\n}\n","import type {\n\tAnswerHandler,\n\tCompiledPath,\n\tGroupInterface,\n\tRouteEntry,\n\tRouterInterface,\n\tRouterMatch,\n\tRouterOptions,\n} from './types.js'\nimport { isString } from '@orkestrel/contract'\nimport { compareSpecificity, compilePath, matchPath } from './helpers.js'\nimport { Group } from './Group.js'\n\n/**\n * The path-matching + registry engine — registers `{ path, meta, name? }`\n * entries (compiling each path once) and resolves a concrete pathname to the\n * MOST SPECIFIC matching entry. The shared machine both the `Navigator`\n * (browser) and the `Dispatcher` (core, method-dimensioned) compose.\n *\n * @typeParam Meta - The opaque payload each entry carries and a match returns\n *\n * @remarks\n * - **Registration boundary guard (§14).** `add` validates each entry's\n * `path` — `isString` plus a leading `/` — and throws `TypeError` on a\n * malformed registration; `match` stays guard-free (the hot path).\n * - **Compile-once.** Each path is compiled exactly once at registration into\n * a parallel `#compiled` array, so `match` runs only a cached `exec` per\n * candidate.\n * - **Dedup via `key`.** When `options.key` is set, an entry whose computed\n * key already exists REPLACES the prior one IN PLACE (both the `#entries`\n * and `#compiled` arrays, at the existing index) — last write wins, no\n * engine rebuild. Omitted ⇒ every entry is kept, even duplicate paths.\n * - **Groups.** `group(prefix)` returns a {@link GroupInterface} that composes\n * `prefix` onto every entry it registers, nesting via {@link joinPaths}.\n *\n * @example\n * ```ts\n * const router = new Router<{ readonly page: string }>()\n * router.add({ path: '/users/:id', meta: { page: 'profile' } })\n * router.match('/users/7') // { path: '/users/:id', params: { id: '7' }, meta: { page: 'profile' } }\n * ```\n */\nexport class Router<Meta> implements RouterInterface<Meta> {\n\treadonly #entries: RouteEntry<Meta>[] = []\n\treadonly #compiled: CompiledPath[] = []\n\treadonly #sensitive: boolean\n\treadonly #key: ((entry: RouteEntry<Meta>) => string) | undefined\n\treadonly #index: Map<string, number> = new Map()\n\n\tconstructor(options?: RouterOptions<Meta>) {\n\t\tthis.#sensitive = options?.sensitive ?? true\n\t\tthis.#key = options?.key\n\t\tif (options?.entries !== undefined) this.add(options.entries)\n\t}\n\n\tget count(): number {\n\t\treturn this.#entries.length\n\t}\n\n\tadd(entry: RouteEntry<Meta>): void\n\tadd(entries: readonly RouteEntry<Meta>[]): void\n\tadd(input: RouteEntry<Meta> | readonly RouteEntry<Meta>[]): void {\n\t\tconst inputs = Array.isArray(input) ? input : [input]\n\t\tfor (const entry of inputs) this.#register(entry)\n\t}\n\n\tmatch(pathname: string, answers?: AnswerHandler<Meta>): RouterMatch<Meta> | undefined {\n\t\tlet best: { entry: RouteEntry<Meta>; params: Readonly<Record<string, string>> } | undefined\n\t\tfor (let index = 0; index < this.#entries.length; index += 1) {\n\t\t\tconst entry = this.#entries[index]\n\t\t\tconst compiled = this.#compiled[index]\n\t\t\tif (entry === undefined || compiled === undefined) continue\n\t\t\tif (answers !== undefined && !answers(entry.meta)) continue\n\t\t\tconst params = matchPath(compiled, pathname)\n\t\t\tif (params === undefined) continue\n\t\t\tif (best === undefined || compareSpecificity(entry.path, best.entry.path) < 0)\n\t\t\t\tbest = { entry, params }\n\t\t}\n\t\tif (best === undefined) return undefined\n\t\treturn {\n\t\t\tpath: best.entry.path,\n\t\t\tparams: best.params,\n\t\t\tmeta: best.entry.meta,\n\t\t\t...(best.entry.name === undefined ? {} : { name: best.entry.name }),\n\t\t}\n\t}\n\n\tentries(): readonly RouteEntry<Meta>[]\n\tentries(pathname: string): readonly RouteEntry<Meta>[]\n\tentries(pathname?: string): readonly RouteEntry<Meta>[] {\n\t\tif (pathname === undefined) return [...this.#entries]\n\t\tconst out: RouteEntry<Meta>[] = []\n\t\tfor (let index = 0; index < this.#entries.length; index += 1) {\n\t\t\tconst entry = this.#entries[index]\n\t\t\tconst compiled = this.#compiled[index]\n\t\t\tif (entry === undefined || compiled === undefined) continue\n\t\t\tif (matchPath(compiled, pathname) !== undefined) out.push(entry)\n\t\t}\n\t\treturn out\n\t}\n\n\tgroup(prefix: string): GroupInterface<Meta> {\n\t\treturn new Group<Meta>(this, prefix)\n\t}\n\n\tclear(): void {\n\t\tthis.#entries.length = 0\n\t\tthis.#compiled.length = 0\n\t\tthis.#index.clear()\n\t}\n\n\t// Validate the registration boundary (§14: isString + leading '/'), then either replace an\n\t// existing entry IN PLACE (dedup via `#key`, last write wins) or append a new one — the\n\t// engine's compile-once invariant, kept in sync across the `#entries`/`#compiled` pair.\n\t#register(entry: RouteEntry<Meta>): void {\n\t\tif (!isString(entry.path) || !entry.path.startsWith('/'))\n\t\t\tthrow new TypeError(\n\t\t\t\t`a route path must be a string starting with \"/\", got ${JSON.stringify(entry.path)}`,\n\t\t\t)\n\t\tconst compiled = compilePath(entry.path, this.#sensitive)\n\t\tif (this.#key === undefined) {\n\t\t\tthis.#entries.push(entry)\n\t\t\tthis.#compiled.push(compiled)\n\t\t\treturn\n\t\t}\n\t\tconst key = this.#key(entry)\n\t\tconst existing = this.#index.get(key)\n\t\tif (existing !== undefined) {\n\t\t\tthis.#entries[existing] = entry\n\t\t\tthis.#compiled[existing] = compiled\n\t\t\treturn\n\t\t}\n\t\tthis.#index.set(key, this.#entries.length)\n\t\tthis.#entries.push(entry)\n\t\tthis.#compiled.push(compiled)\n\t}\n}\n","import type { DispatchGroupInterface, DispatcherInterface, RouteInput } from './types.js'\nimport { joinPaths } from './helpers.js'\n\n/**\n * A prefix-scoped registration handle over a\n * {@link import('./Dispatcher.js').Dispatcher} — the method-dimensioned\n * counterpart of `Group` (`Group.ts`).\n *\n * @typeParam TState - The consumer's opaque per-request state type, matching\n * the owning dispatcher\n *\n * @remarks\n * Every `add` composes `input.path` via {@link joinPaths} against\n * `this.prefix` and forwards to the OWNING dispatcher's `add` (its own §14\n * boundary guard still applies). Pure string composition (§4.2.2) — no\n * independent state or storage.\n *\n * @example\n * ```ts\n * import { Dispatcher } from '@src/core'\n *\n * const dispatcher = new Dispatcher()\n * const api = dispatcher.group('/api')\n * api.add({ method: 'GET', path: '/users', handler: () => new Response('ok') })\n * ```\n */\nexport class DispatchGroup<TState> implements DispatchGroupInterface<TState> {\n\treadonly prefix: string\n\treadonly #parent: DispatcherInterface<TState>\n\n\tconstructor(parent: DispatcherInterface<TState>, prefix: string) {\n\t\tthis.#parent = parent\n\t\tthis.prefix = prefix\n\t}\n\n\tadd<Path extends string>(input: RouteInput<Path, TState>): void\n\tadd(inputs: readonly RouteInput<string, TState>[]): void\n\tadd(input: RouteInput<string, TState> | readonly RouteInput<string, TState>[]): void {\n\t\tconst inputs = Array.isArray(input) ? input : [input]\n\t\tthis.#parent.add(\n\t\t\tinputs.map((route) => ({ ...route, path: joinPaths(this.prefix, route.path) })),\n\t\t)\n\t}\n\n\tgroup(prefix: string): DispatchGroupInterface<TState> {\n\t\treturn new DispatchGroup<TState>(this.#parent, joinPaths(this.prefix, prefix))\n\t}\n}\n","import type {\n\tDispatchGroupInterface,\n\tDispatcherEventMap,\n\tDispatcherInterface,\n\tDispatcherOptions,\n\tDispatchResult,\n\tMethod,\n\tRouteContext,\n\tRouteEntry,\n\tRouteInput,\n\tRouteRecord,\n\tRouterInterface,\n\tRouterMatch,\n} from './types.js'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport { Emitter } from '@orkestrel/emitter'\nimport { isFunction, isString } from '@orkestrel/contract'\nimport { METHODS } from './constants.js'\nimport { computeDispatchKey, parseMethod } from './helpers.js'\nimport { Router } from './Router.js'\nimport { DispatchGroup } from './DispatchGroup.js'\n\n/**\n * The fetch-standard, method-dimensioned dispatch entity — layers HTTP method\n * dispatch and web-standard `Request`/`Response` handling over one internal\n * `Router<RouteRecord<TState>>`. The core machine the eventual server face\n * (§7) and any fetch-native runtime consumes directly.\n *\n * @typeParam TState - The consumer's opaque per-request state type\n *\n * @remarks\n * - **Dedup by `method + canonicalizePath`.** The underlying `Router` is\n * constructed with a `key` function so registering the same method+path\n * twice REPLACES the prior route in place (§5.1).\n * - **Registration boundary guard (§14).** `add` validates each input's\n * `handler` (`isFunction`) and `method` (must be in {@link METHODS}) —\n * throws `TypeError` on a malformed registration; path validation is\n * delegated to the underlying `Router`'s own guard. `match`/`handle` stay\n * guard-free.\n * - **Auto-`HEAD` / auto-`OPTIONS`.** A `HEAD` request with no registered\n * `HEAD` route runs the matching `GET` handler and strips the response\n * body; an `OPTIONS` request with no registered `OPTIONS` route answers\n * `204` with a derived `Allow` header.\n * - **Handler throws propagate.** `handle` never invents an error boundary —\n * a handler throw reaches the caller uncaught (§5.1).\n * - **Emitter (§13).** Owns a `#emitter` for {@link DispatcherEventMap};\n * `match`/`miss` fire AFTER resolution, before the handler/responder runs.\n *\n * @example\n * ```ts\n * const dispatcher = new Dispatcher<{ readonly userId: string }>()\n * dispatcher.add({\n * \tmethod: 'GET',\n * \tpath: '/users/:id',\n * \thandler: (request, context) => Response.json({ id: context.params.id }),\n * })\n * const response = await dispatcher.handle(new Request('http://x/users/7'), { userId: 'me' })\n * ```\n */\nexport class Dispatcher<TState = undefined> implements DispatcherInterface<TState> {\n\treadonly router: RouterInterface<RouteRecord<TState>>\n\treadonly #emitter: Emitter<DispatcherEventMap>\n\treadonly #unmatched: DispatcherOptions<TState>['unmatched']\n\treadonly #unmethoded: DispatcherOptions<TState>['unmethoded']\n\n\tconstructor(options?: DispatcherOptions<TState>) {\n\t\tconst sensitive = options?.sensitive\n\t\tconst on = options?.on\n\t\tconst error = options?.error\n\t\tthis.router = new Router<RouteRecord<TState>>({\n\t\t\t...(sensitive === undefined ? {} : { sensitive }),\n\t\t\tkey: computeDispatchKey,\n\t\t})\n\t\tthis.#emitter = new Emitter<DispatcherEventMap>({\n\t\t\t...(on === undefined ? {} : { on }),\n\t\t\t...(error === undefined ? {} : { error }),\n\t\t})\n\t\tthis.#unmatched = options?.unmatched\n\t\tthis.#unmethoded = options?.unmethoded\n\t\tif (options?.routes !== undefined) this.add(options.routes)\n\t}\n\n\tget emitter(): EmitterInterface<DispatcherEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tadd<Path extends string>(input: RouteInput<Path, TState>): void\n\tadd(inputs: readonly RouteInput<string, TState>[]): void\n\tadd(input: RouteInput<string, TState> | readonly RouteInput<string, TState>[]): void {\n\t\tconst inputs = Array.isArray(input) ? input : [input]\n\t\tfor (const route of inputs) this.#register(route)\n\t}\n\n\tgroup(prefix: string): DispatchGroupInterface<TState> {\n\t\treturn new DispatchGroup<TState>(this, prefix)\n\t}\n\n\tmatch(method: Method, pathname: string): DispatchResult<TState> {\n\t\tconst hit = this.router.match(pathname, (meta) => meta.method === method)\n\t\tif (hit !== undefined) return { status: 'matched', match: hit }\n\t\tif (method === 'HEAD') {\n\t\t\tconst getHit = this.router.match(pathname, (meta) => meta.method === 'GET')\n\t\t\tif (getHit !== undefined) return { status: 'matched', match: getHit }\n\t\t}\n\t\tconst allow = this.#allow(pathname)\n\t\tif (allow.length === 0) return { status: 'unmatched' }\n\t\treturn { status: 'unmethoded', allow }\n\t}\n\n\tasync handle(request: Request, state: TState): Promise<Response> {\n\t\tconst url = new URL(request.url)\n\t\tconst pathname = url.pathname\n\t\tconst requested = request.method\n\t\tconst method = parseMethod(requested)\n\t\tif (method === undefined) {\n\t\t\tconst allow = this.#allow(pathname)\n\t\t\tif (allow.length === 0) {\n\t\t\t\tthis.#emitter.emit('miss', requested, pathname, 'unmatched')\n\t\t\t\treturn this.#respondUnmatched(request)\n\t\t\t}\n\t\t\tthis.#emitter.emit('miss', requested, pathname, 'unmethoded')\n\t\t\treturn this.#respondUnmethoded(request, allow)\n\t\t}\n\t\tconst result = this.match(method, pathname)\n\t\tif (result.status === 'matched')\n\t\t\treturn this.#respondMatched(request, state, method, result.match, url)\n\t\tif (result.status === 'unmethoded') {\n\t\t\tif (method === 'OPTIONS') return this.#respondAutoOptions(pathname, result.allow)\n\t\t\tthis.#emitter.emit('miss', method, pathname, 'unmethoded')\n\t\t\treturn this.#respondUnmethoded(request, result.allow)\n\t\t}\n\t\tthis.#emitter.emit('miss', method, pathname, 'unmatched')\n\t\treturn this.#respondUnmatched(request)\n\t}\n\n\tdestroy(): void {\n\t\tthis.#emitter.destroy()\n\t}\n\n\t// Validate the registration boundary (§14: handler function, known method), then delegate\n\t// path validation + dedup to the underlying `Router`'s own guard.\n\t#register(input: RouteInput<string, TState>): void {\n\t\tif (!isFunction(input.handler))\n\t\t\tthrow new TypeError(\n\t\t\t\t`a route handler must be a function, got ${JSON.stringify(input.handler)}`,\n\t\t\t)\n\t\tif (!isString(input.method) || !METHODS.has(input.method))\n\t\t\tthrow new TypeError(\n\t\t\t\t`a route method must be one of ${[...METHODS].join(', ')}, got ${JSON.stringify(input.method)}`,\n\t\t\t)\n\t\tconst name = input.name\n\t\tthis.router.add({\n\t\t\tpath: input.path,\n\t\t\t...(name === undefined ? {} : { name }),\n\t\t\tmeta: {\n\t\t\t\tmethod: input.method,\n\t\t\t\thandler: input.handler,\n\t\t\t\t...(name === undefined ? {} : { name }),\n\t\t\t},\n\t\t})\n\t}\n\n\t// The derived `Allow` set for a pathname — every distinct registered method, with `HEAD`\n\t// added whenever `GET` is present and `HEAD` is not explicitly registered (§5.1).\n\t#allow(pathname: string): readonly Method[] {\n\t\tconst entries: readonly RouteEntry<RouteRecord<TState>>[] = this.router.entries(pathname)\n\t\tconst methods = new Set<Method>()\n\t\tfor (const entry of entries) methods.add(entry.meta.method)\n\t\tif (methods.has('GET')) methods.add('HEAD')\n\t\treturn [...methods]\n\t}\n\n\t#respondUnmatched(request: Request): Response | Promise<Response> {\n\t\tconst responder = this.#unmatched\n\t\tif (responder !== undefined) return responder(request)\n\t\treturn new Response('Not Found', { status: 404 })\n\t}\n\n\t#respondUnmethoded(request: Request, allow: readonly Method[]): Response | Promise<Response> {\n\t\tconst responder = this.#unmethoded\n\t\tif (responder !== undefined) return responder(request, allow)\n\t\treturn new Response('Method Not Allowed', {\n\t\t\tstatus: 405,\n\t\t\theaders: { Allow: allow.join(', ') },\n\t\t})\n\t}\n\n\t// A matched dispatch — either the winning handler runs directly, or (for a derived `HEAD`\n\t// with no explicit `HEAD` route) the `GET` handler runs and the response body is stripped.\n\tasync #respondMatched(\n\t\trequest: Request,\n\t\tstate: TState,\n\t\tmethod: Method,\n\t\tmatch: RouterMatch<RouteRecord<TState>>,\n\t\turl: URL,\n\t): Promise<Response> {\n\t\tthis.#emitter.emit('match', method, match.path)\n\t\tconst context: RouteContext<string, TState> = {\n\t\t\tparams: match.params,\n\t\t\tpattern: match.path,\n\t\t\turl,\n\t\t\tstate,\n\t\t}\n\t\tconst response = await match.meta.handler(request, context)\n\t\tif (method === 'HEAD' && match.meta.method === 'GET')\n\t\t\treturn new Response(null, {\n\t\t\t\tstatus: response.status,\n\t\t\t\tstatusText: response.statusText,\n\t\t\t\theaders: response.headers,\n\t\t\t})\n\t\treturn response\n\t}\n\n\t// Derived `OPTIONS` — no explicit `OPTIONS` route registered for this pathname: answer\n\t// `204` with the derived `Allow` set (adding `OPTIONS` itself, always answerable).\n\t#respondAutoOptions(pathname: string, allow: readonly Method[]): Response {\n\t\tthis.#emitter.emit('match', 'OPTIONS', pathname)\n\t\tconst headers = new Headers({ Allow: [...allow, 'OPTIONS'].join(', ') })\n\t\treturn new Response(null, { status: 204, headers })\n\t}\n}\n","import type {\n\tDispatcherInterface,\n\tDispatcherOptions,\n\tRouterInterface,\n\tRouterOptions,\n} from './types.js'\nimport { Dispatcher } from './Dispatcher.js'\nimport { Router } from './Router.js'\n\n/**\n * Create a {@link RouterInterface} — the pure path-matching + registry engine\n * shared by the browser `Navigator` and the core `Dispatcher`.\n *\n * @remarks\n * Prefer this over `new Router(...)` at call sites that only need the\n * interface; an entity that OWNS a `Router` internally (like `Dispatcher`)\n * still constructs `new Router(...)` directly.\n *\n * @typeParam Meta - The opaque payload each entry carries and a match returns\n * @param options - Optional initial `entries`, the `sensitive` case toggle\n * (default `true`), and a `key` dedup identity function\n * @returns A {@link RouterInterface}\n *\n * @example\n * ```ts\n * import { createRouter } from '@src/core'\n *\n * const router = createRouter<{ readonly page: string }>()\n * router.add({ path: '/users/:id', meta: { page: 'profile' } })\n * router.match('/users/7') // { path: '/users/:id', params: { id: '7' }, meta: { page: 'profile' } }\n * ```\n */\nexport function createRouter<Meta>(options?: RouterOptions<Meta>): RouterInterface<Meta> {\n\treturn new Router<Meta>(options)\n}\n\n/**\n * Create a {@link DispatcherInterface} — the fetch-standard, method-\n * dimensioned dispatch entity over one internal `Router<RouteRecord<TState>>`.\n *\n * @remarks\n * Prefer this over `new Dispatcher(...)` at call sites that only need the\n * interface.\n *\n * @typeParam TState - The consumer's opaque per-request state type (default\n * `undefined` for stateless use)\n * @param options - Optional initial `routes`, the `sensitive` case toggle,\n * the `unmatched`/`unmethoded` default-responder overrides, and the AGENTS\n * §13 emitter `on`/`error` wiring\n * @returns A {@link DispatcherInterface}\n *\n * @example\n * ```ts\n * import { createDispatcher } from '@src/core'\n *\n * const dispatcher = createDispatcher<{ readonly userId: string }>({\n * \troutes: [\n * \t\t{ method: 'GET', path: '/health', handler: () => new Response('ok') },\n * \t],\n * })\n * const response = await dispatcher.handle(new Request('http://x/health'), { userId: 'me' })\n * ```\n */\nexport function createDispatcher<TState = undefined>(\n\toptions?: DispatcherOptions<TState>,\n): DispatcherInterface<TState> {\n\treturn new Dispatcher<TState>(options)\n}\n"],"mappings":"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"}