@orkestrel/router 0.0.17 → 0.0.18

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.
@@ -2,7 +2,7 @@ import { canonicalizePath, createRouter, joinPaths } from "../core/index.js";
2
2
  import { createAbort } from "@orkestrel/abort";
3
3
  import { Emitter } from "@orkestrel/emitter";
4
4
  import { ContractError, isFunction, isString, preview } from "@orkestrel/contract";
5
- //#region src/browser/helpers.ts
5
+ // src/browser/helpers.ts
6
6
  /**
7
7
  * Computes the canonical path key a `Navigator` registers a browser navigation
8
8
  * route under.
@@ -115,8 +115,7 @@ function resolveLocationPath(location, history, base) {
115
115
  function findAnchor(event) {
116
116
  for (const node of event.composedPath()) if (node instanceof HTMLAnchorElement) return node;
117
117
  }
118
- //#endregion
119
- //#region src/browser/Navigator.ts
118
+ // src/browser/Navigator.ts
120
119
  /**
121
120
  * Represents the headless History/hash navigation entity — composes one core
122
121
  * `Router<Meta>`, resolving the current location on `start()` and
@@ -300,15 +299,17 @@ var Navigator = class {
300
299
  this.#emitter.emit("navigate", to);
301
300
  }
302
301
  async #guarded(guard, to, from, handle) {
303
- let verdict;
302
+ const verdict = await this.#verdict(guard, to, from, handle);
303
+ if (handle.signal.aborted || !verdict) return;
304
+ this.#commit(to);
305
+ }
306
+ async #verdict(guard, to, from, handle) {
304
307
  try {
305
- verdict = await guard(to, from, handle.signal);
308
+ return await guard(to, from, handle.signal);
306
309
  } catch (error) {
307
310
  this.#surface(error);
308
- return;
311
+ return false;
309
312
  }
310
- if (handle.signal.aborted || !verdict) return;
311
- this.#commit(to);
312
313
  }
313
314
  #surface(error) {
314
315
  const handler = this.#error;
@@ -333,8 +334,7 @@ var Navigator = class {
333
334
  }, true, this.#base));
334
335
  }
335
336
  };
336
- //#endregion
337
- //#region src/browser/factories.ts
337
+ // src/browser/factories.ts
338
338
  /**
339
339
  * Creates a {@link NavigatorInterface} — the headless History/hash navigation
340
340
  * entity composing one core `Router<Meta>`.
@@ -369,7 +369,6 @@ var Navigator = class {
369
369
  function createNavigator(options) {
370
370
  return new Navigator(options);
371
371
  }
372
- //#endregion
373
372
  export { Navigator, computeNavigationKey, createNavigator, extractHashPath, findAnchor, resolveLocationPath };
374
373
 
375
374
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/browser/helpers.ts","../../../src/browser/Navigator.ts","../../../src/browser/factories.ts"],"sourcesContent":["// The PURE browser-navigation primitives (self-describing helper naming —\n// module scope, no entity context). Every one is exported (the centralized-file\n// rule): 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 isolation check.\n\nimport type { RouteEntry } from '@src/core'\nimport { canonicalizePath } from '@src/core'\n\n/**\n * Computes the canonical path key a `Navigator` registers a browser navigation\n * route under.\n *\n * @remarks\n * Projects the route's path through the core engine's canonical trailing-slash\n * identity, so `/users` and `/users/` replace one another in the Navigator's\n * shared Router. The entry's `meta` payload is never read, so any payload type\n * is accepted.\n *\n * @param entry - The Router entry carrying the Navigator route\n * @returns The route's canonical path\n *\n * @example\n * ```ts\n * computeNavigationKey({ path: '/users/', meta: {} }) // '/users'\n * ```\n */\nexport function computeNavigationKey(entry: RouteEntry<unknown>): string {\n\treturn canonicalizePath(entry.path)\n}\n\n/**\n * Extracts the `/`-prefixed pathname from a `location.hash` value — strips 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, so a hash-mode\n * 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 (for example `'#/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 * Resolves 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 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 - If `true`, the pathname is read from `location.pathname`\n * with `base` stripped; if `false`, it is read from `location.hash`\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 * Finds 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 { RouterInterface, RouterMatch } from '@src/core'\nimport { createAbort } from '@orkestrel/abort'\nimport { Emitter } from '@orkestrel/emitter'\nimport { ContractError, isFunction, isString, preview } from '@orkestrel/contract'\nimport { createRouter, joinPaths } from '@src/core'\nimport { computeNavigationKey, findAnchor, resolveLocationPath } from './helpers.js'\n\n/**\n * Represents the headless History/hash navigation entity — composes one core\n * `Router<Meta>`, resolving the current location on `start()` and\n * every subsequent navigation event, tracking `active`, and emitting\n * `navigate` through the core {@link Emitter}. 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 shared engine.\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 * honest to the one-shared-engine rule: 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<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 #listener: () => 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 ContractError('a navigator guard must be a function when defined', {\n\t\t\t\tcode: 'literal',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['options', 'guard'],\n\t\t\t\t\tlimit: 'function or undefined',\n\t\t\t\t\treceived: preview(options.guard),\n\t\t\t\t},\n\t\t\t})\n\t\tif (options.fallback !== undefined && !isString(options.fallback))\n\t\t\tthrow new ContractError('a navigator fallback must be a string when defined', {\n\t\t\t\tcode: 'literal',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['options', 'fallback'],\n\t\t\t\t\tlimit: 'string or undefined',\n\t\t\t\t\treceived: preview(options.fallback),\n\t\t\t\t},\n\t\t\t})\n\t\tif (options.base !== undefined && !isString(options.base))\n\t\t\tthrow new ContractError('a navigator base must be a string when defined', {\n\t\t\t\tcode: 'literal',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['options', 'base'],\n\t\t\t\t\tlimit: 'string or undefined',\n\t\t\t\t\treceived: preview(options.base),\n\t\t\t\t},\n\t\t\t})\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<Meta>({\n\t\t\tentries: options.routes,\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.#listener = this.#resolve.bind(this)\n\t\tthis.#clickListener = this.#intercepted.bind(this)\n\t}\n\n\tget router(): RouterInterface<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.#listener)\n\t\t} else {\n\t\t\twindow.addEventListener('popstate', this.#listener)\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.#listener)\n\t\t} else {\n\t\t\twindow.removeEventListener('popstate', this.#listener)\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\treturn this.#router.match(path)\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, honest to the one-shared-engine rule: 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 through `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 (the Emitter pattern'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 through `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 * Creates a {@link NavigatorInterface} — the headless History/hash navigation\n * entity composing one core `Router<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 Emitter pattern's\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":[],"sources":["../../../src/browser/helpers.ts","../../../src/browser/Navigator.ts","../../../src/browser/factories.ts"],"sourcesContent":["// The PURE browser-navigation primitives (self-describing helper naming —\n// module scope, no entity context). Every one is exported (the centralized-file\n// rule): 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 isolation check.\n\nimport type { RouteEntry } from '@src/core'\nimport { canonicalizePath } from '@src/core'\n\n/**\n * Computes the canonical path key a `Navigator` registers a browser navigation\n * route under.\n *\n * @remarks\n * Projects the route's path through the core engine's canonical trailing-slash\n * identity, so `/users` and `/users/` replace one another in the Navigator's\n * shared Router. The entry's `meta` payload is never read, so any payload type\n * is accepted.\n *\n * @param entry - The Router entry carrying the Navigator route\n * @returns The route's canonical path\n *\n * @example\n * ```ts\n * computeNavigationKey({ path: '/users/', meta: {} }) // '/users'\n * ```\n */\nexport function computeNavigationKey(entry: RouteEntry<unknown>): string {\n\treturn canonicalizePath(entry.path)\n}\n\n/**\n * Extracts the `/`-prefixed pathname from a `location.hash` value — strips 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, so a hash-mode\n * 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 (for example `'#/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 * Resolves 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 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 - If `true`, the pathname is read from `location.pathname`\n * with `base` stripped; if `false`, it is read from `location.hash`\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 * Finds 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 { RouterInterface, RouterMatch } from '@src/core'\nimport { createAbort } from '@orkestrel/abort'\nimport { Emitter } from '@orkestrel/emitter'\nimport { ContractError, isFunction, isString, preview } from '@orkestrel/contract'\nimport { createRouter, joinPaths } from '@src/core'\nimport { computeNavigationKey, findAnchor, resolveLocationPath } from './helpers.js'\n\n/**\n * Represents the headless History/hash navigation entity — composes one core\n * `Router<Meta>`, resolving the current location on `start()` and\n * every subsequent navigation event, tracking `active`, and emitting\n * `navigate` through the core {@link Emitter}. 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 shared engine.\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 * honest to the one-shared-engine rule: 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<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 #listener: () => 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 ContractError('a navigator guard must be a function when defined', {\n\t\t\t\tcode: 'literal',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['options', 'guard'],\n\t\t\t\t\tlimit: 'function or undefined',\n\t\t\t\t\treceived: preview(options.guard),\n\t\t\t\t},\n\t\t\t})\n\t\tif (options.fallback !== undefined && !isString(options.fallback))\n\t\t\tthrow new ContractError('a navigator fallback must be a string when defined', {\n\t\t\t\tcode: 'literal',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['options', 'fallback'],\n\t\t\t\t\tlimit: 'string or undefined',\n\t\t\t\t\treceived: preview(options.fallback),\n\t\t\t\t},\n\t\t\t})\n\t\tif (options.base !== undefined && !isString(options.base))\n\t\t\tthrow new ContractError('a navigator base must be a string when defined', {\n\t\t\t\tcode: 'literal',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['options', 'base'],\n\t\t\t\t\tlimit: 'string or undefined',\n\t\t\t\t\treceived: preview(options.base),\n\t\t\t\t},\n\t\t\t})\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<Meta>({\n\t\t\tentries: options.routes,\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.#listener = this.#resolve.bind(this)\n\t\tthis.#clickListener = this.#intercepted.bind(this)\n\t}\n\n\tget router(): RouterInterface<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.#listener)\n\t\t} else {\n\t\t\twindow.addEventListener('popstate', this.#listener)\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.#listener)\n\t\t} else {\n\t\t\twindow.removeEventListener('popstate', this.#listener)\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\treturn this.#router.match(path)\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, honest to the one-shared-engine rule: 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 through `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\tconst verdict = await this.#verdict(guard, to, from, handle)\n\t\tif (handle.signal.aborted || !verdict) return\n\t\tthis.#commit(to)\n\t}\n\n\tasync #verdict(\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<boolean> {\n\t\ttry {\n\t\t\treturn await guard(to, from, handle.signal)\n\t\t} catch (error) {\n\t\t\tthis.#surface(error)\n\t\t\treturn false\n\t\t}\n\t}\n\n\t// Route a guard throw to the `error` handler (the Emitter pattern'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 through `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 * Creates a {@link NavigatorInterface} — the headless History/hash navigation\n * entity composing one core `Router<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 Emitter pattern's\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,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let _orkestrel_contract = require("@orkestrel/contract");
3
3
  let _orkestrel_emitter = require("@orkestrel/emitter");
4
- //#region src/core/constants.ts
4
+ // src/core/constants.ts
5
5
  /**
6
6
  * Lists the HTTP methods a {@link import('./types.js').DispatcherInterface} registers
7
7
  * routes under, in canonical order — a frozen literal tuple, and the single source the
@@ -92,8 +92,7 @@ var TIER_PARAM = 1;
92
92
  * ```
93
93
  */
94
94
  var TIER_WILDCARD = 0;
95
- //#endregion
96
- //#region src/core/helpers.ts
95
+ // src/core/helpers.ts
97
96
  /**
98
97
  * Escapes every regex metacharacter in a literal string so it can be embedded
99
98
  * inside a larger `RegExp` source without being interpreted as syntax.
@@ -210,10 +209,9 @@ function computeDispatchKey(entry) {
210
209
  * ```
211
210
  */
212
211
  function compilePath(path, sensitive = true) {
213
- const params = [];
214
212
  const normalized = canonicalizePath(path);
215
213
  const segments = normalized.split("/");
216
- const pattern = segments.map((segment, index) => {
214
+ const compiledSegments = segments.map((segment, index) => {
217
215
  const isFinal = index === segments.length - 1;
218
216
  if (!isFinal && /^\*[A-Za-z_]\w*/.test(segment)) throw new _orkestrel_contract.ContractError("a wildcard segment must be the final segment of a path pattern", {
219
217
  code: "placement",
@@ -224,17 +222,21 @@ function compilePath(path, sensitive = true) {
224
222
  }
225
223
  });
226
224
  const tier = classifySegment(segment, isFinal);
227
- if (tier === 0) {
228
- params.push(segment.slice(1));
229
- return "(.+)";
230
- }
225
+ if (tier === 0) return {
226
+ pattern: "(.+)",
227
+ param: segment.slice(1)
228
+ };
231
229
  if (tier === 1) {
232
230
  const name = /^:([A-Za-z_]\w*)/.exec(segment)?.[1] ?? "";
233
- params.push(name);
234
- return `([^/]+)${escapeRegExp(segment.slice(1 + name.length))}`;
231
+ return {
232
+ pattern: `([^/]+)${escapeRegExp(segment.slice(1 + name.length))}`,
233
+ param: name
234
+ };
235
235
  }
236
- return escapeRegExp(segment);
237
- }).join("/");
236
+ return { pattern: escapeRegExp(segment) };
237
+ });
238
+ const pattern = compiledSegments.map((segment) => segment.pattern).join("/");
239
+ const params = compiledSegments.flatMap((segment) => segment.param === void 0 ? [] : [segment.param]);
238
240
  const suffix = normalized === "/" || normalized === "" ? "" : "/?";
239
241
  const flags = sensitive ? "" : "i";
240
242
  return {
@@ -295,13 +297,13 @@ function decodeParam(value) {
295
297
  function matchPath(compiled, pathname) {
296
298
  const result = compiled.regex.exec(pathname);
297
299
  if (result === null) return void 0;
298
- const params = {};
299
- for (let index = 0; index < compiled.params.length; index += 1) {
300
- const name = compiled.params[index];
300
+ const params = compiled.params.entries().flatMap(([index, name]) => {
301
301
  const value = result[index + 1];
302
- if (name !== void 0 && value !== void 0) params[name] = decodeParam(value);
303
- }
304
- return Object.freeze(params);
302
+ if (name === void 0 || value === void 0) return [];
303
+ const decoded = decodeParam(value);
304
+ return name === "__proto__" ? [] : [[name, decoded]];
305
+ });
306
+ return Object.freeze(Object.fromEntries(params));
305
307
  }
306
308
  /**
307
309
  * Classifies one path segment into its specificity tier — the same syntax
@@ -401,8 +403,8 @@ function computeSpecificity(path) {
401
403
  function compareSpecificity(a, b) {
402
404
  const left = computeSpecificity(a);
403
405
  const right = computeSpecificity(b);
404
- const length = Math.max(left.length, right.length);
405
- for (let index = 0; index < length; index += 1) {
406
+ const longest = left.length >= right.length ? left : right;
407
+ for (const [index] of longest.entries()) {
406
408
  const tierA = left[index] ?? -1;
407
409
  const tierB = right[index] ?? -1;
408
410
  if (tierA !== tierB) return tierB - tierA;
@@ -480,8 +482,7 @@ function joinPaths(prefix, path) {
480
482
  function defineRoute(input) {
481
483
  return input;
482
484
  }
483
- //#endregion
484
- //#region src/core/parsers.ts
485
+ // src/core/parsers.ts
485
486
  /**
486
487
  * Narrows a raw `request.method` string into a typed {@link Method} — total,
487
488
  * never throws.
@@ -508,8 +509,7 @@ function defineRoute(input) {
508
509
  function parseMethod(value) {
509
510
  return METHOD_LIST.find((method) => method === value);
510
511
  }
511
- //#endregion
512
- //#region src/core/Group.ts
512
+ // src/core/Group.ts
513
513
  /**
514
514
  * Represents a prefix-scoped registration handle over a {@link import('./Router.js').Router} —
515
515
  * pure string composition, no independent state or storage.
@@ -549,8 +549,7 @@ var Group = class Group {
549
549
  return new Group(this.#parent, joinPaths(this.prefix, prefix));
550
550
  }
551
551
  };
552
- //#endregion
553
- //#region src/core/Router.ts
552
+ // src/core/Router.ts
554
553
  /**
555
554
  * Represents the path-matching + registry engine — registers `{ path, meta, name? }`
556
555
  * entries (compiling each path once) and resolves a concrete pathname to the most
@@ -599,37 +598,28 @@ var Router = class {
599
598
  for (const entry of inputs) this.#register(entry);
600
599
  }
601
600
  match(pathname, answers) {
602
- let best;
603
- for (let index = 0; index < this.#entries.length; index += 1) {
604
- const entry = this.#entries[index];
601
+ const best = this.#entries.entries().reduce((selected, [index, entry]) => {
605
602
  const compiled = this.#compiled[index];
606
- if (entry === void 0 || compiled === void 0) continue;
607
- if (answers !== void 0 && !answers(entry.meta)) continue;
603
+ if (entry === void 0 || compiled === void 0) return selected;
604
+ if (answers !== void 0 && !answers(entry.meta)) return selected;
608
605
  const params = matchPath(compiled, pathname);
609
- if (params === void 0) continue;
610
- if (best === void 0 || compareSpecificity(entry.path, best.entry.path) < 0) best = {
611
- entry,
612
- params
613
- };
614
- }
606
+ if (params === void 0) return selected;
607
+ return selected === void 0 || compareSpecificity(entry.path, selected[0].path) < 0 ? [entry, params] : selected;
608
+ }, void 0);
615
609
  if (best === void 0) return void 0;
616
610
  return {
617
- path: best.entry.path,
618
- params: best.params,
619
- meta: best.entry.meta,
620
- ...best.entry.name === void 0 ? {} : { name: best.entry.name }
611
+ path: best[0].path,
612
+ params: best[1],
613
+ meta: best[0].meta,
614
+ ...best[0].name === void 0 ? {} : { name: best[0].name }
621
615
  };
622
616
  }
623
617
  entries(pathname) {
624
618
  if (pathname === void 0) return [...this.#entries];
625
- const out = [];
626
- for (let index = 0; index < this.#entries.length; index += 1) {
627
- const entry = this.#entries[index];
619
+ return this.#entries.entries().flatMap(([index, entry]) => {
628
620
  const compiled = this.#compiled[index];
629
- if (entry === void 0 || compiled === void 0) continue;
630
- if (matchPath(compiled, pathname) !== void 0) out.push(entry);
631
- }
632
- return out;
621
+ return entry !== void 0 && compiled !== void 0 && matchPath(compiled, pathname) !== void 0 ? [entry] : [];
622
+ }).toArray();
633
623
  }
634
624
  group(prefix) {
635
625
  return new Group(this, prefix);
@@ -674,8 +664,7 @@ var Router = class {
674
664
  this.#compiled.push(compiled);
675
665
  }
676
666
  };
677
- //#endregion
678
- //#region src/core/DispatchGroup.ts
667
+ // src/core/DispatchGroup.ts
679
668
  /**
680
669
  * Represents a prefix-scoped registration handle over a
681
670
  * {@link import('./Dispatcher.js').Dispatcher} — the method-dimensioned counterpart of
@@ -717,8 +706,7 @@ var DispatchGroup = class DispatchGroup {
717
706
  return new DispatchGroup(this.#parent, joinPaths(this.prefix, prefix));
718
707
  }
719
708
  };
720
- //#endregion
721
- //#region src/core/Dispatcher.ts
709
+ // src/core/Dispatcher.ts
722
710
  /**
723
711
  * Represents the fetch-standard, method-dimensioned dispatch entity — layers HTTP
724
712
  * method dispatch and web-standard `Request`/`Response` handling over one internal
@@ -913,8 +901,7 @@ var Dispatcher = class {
913
901
  });
914
902
  }
915
903
  };
916
- //#endregion
917
- //#region src/core/factories.ts
904
+ // src/core/factories.ts
918
905
  /**
919
906
  * Creates a {@link RouterInterface} — the pure path-matching + registry engine
920
907
  * shared by the browser `Navigator` and the core `Dispatcher`.
@@ -982,7 +969,6 @@ function createRouter(options) {
982
969
  function createDispatcher(options) {
983
970
  return new Dispatcher(options);
984
971
  }
985
- //#endregion
986
972
  exports.DispatchGroup = DispatchGroup;
987
973
  exports.Dispatcher = Dispatcher;
988
974
  exports.Group = Group;