@orkestrel/router 0.0.9 → 0.0.11
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.
- package/dist/src/browser/index.d.ts +4 -4
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +14 -14
- package/dist/src/core/index.d.ts +14 -14
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.d.cts +1 -1
- package/dist/src/server/index.d.ts +1 -1
- package/package.json +18 -14
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { EmitterErrorHandler } from '@orkestrel/emitter';
|
|
2
2
|
import { EmitterHooks } from '@orkestrel/emitter';
|
|
3
3
|
import { EmitterInterface } from '@orkestrel/emitter';
|
|
4
|
-
import { RouteEntry } from '
|
|
5
|
-
import { RouterInterface } from '
|
|
6
|
-
import { RouterMatch } from '
|
|
4
|
+
import { RouteEntry } from '@orkestrel/router';
|
|
5
|
+
import { RouterInterface } from '@orkestrel/router';
|
|
6
|
+
import { RouterMatch } from '@orkestrel/router';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Compute the registry key for a browser navigation route.
|
|
@@ -258,7 +258,7 @@ export declare interface NavigatorInterface<Meta> {
|
|
|
258
258
|
* not a listener throw, so it is surfaced through the same channel).
|
|
259
259
|
*/
|
|
260
260
|
export declare interface NavigatorOptions<Meta> {
|
|
261
|
-
readonly routes:
|
|
261
|
+
readonly routes: ReadonlyArray<RouteEntry<Meta>>;
|
|
262
262
|
readonly history?: boolean;
|
|
263
263
|
readonly base?: string;
|
|
264
264
|
readonly fallback?: string;
|
|
@@ -1 +1 @@
|
|
|
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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"}
|
|
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: ReadonlyArray<RouteEntry<Meta>>): void\n\tadd(input: RouteEntry<Meta> | ReadonlyArray<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: Array<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: ReadonlyArray<RouteEntry<Meta>>): void\n\tadd(input: RouteEntry<Meta> | ReadonlyArray<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(): ReadonlyArray<RouteEntry<Meta>>\n\tentries(pathname: string): ReadonlyArray<RouteEntry<Meta>>\n\tentries(pathname?: string): ReadonlyArray<RouteEntry<Meta>> {\n\t\tif (pathname === undefined) return [...this.#entries]\n\t\tconst out: Array<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: ReadonlyArray<RouteInput<string, TState>>): void\n\tadd(input: RouteInput<string, TState> | ReadonlyArray<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: ReadonlyArray<RouteInput<string, TState>>): void\n\tadd(input: RouteInput<string, TState> | ReadonlyArray<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: ReadonlyArray<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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"}
|
|
@@ -335,7 +335,7 @@ export declare class Dispatcher<TState = undefined> implements DispatcherInterfa
|
|
|
335
335
|
constructor(options?: DispatcherOptions<TState>);
|
|
336
336
|
get emitter(): EmitterInterface<DispatcherEventMap>;
|
|
337
337
|
add<Path extends string>(input: RouteInput<Path, TState>): void;
|
|
338
|
-
add(inputs:
|
|
338
|
+
add(inputs: ReadonlyArray<RouteInput<string, TState>>): void;
|
|
339
339
|
group(prefix: string): DispatchGroupInterface<TState>;
|
|
340
340
|
match(method: Method, pathname: string): DispatchResult<TState>;
|
|
341
341
|
handle(request: Request, state: TState): Promise<Response>;
|
|
@@ -398,7 +398,7 @@ export declare interface DispatcherInterface<TState = undefined> {
|
|
|
398
398
|
readonly router: RouterInterface<RouteRecord<TState>>;
|
|
399
399
|
readonly emitter: EmitterInterface<DispatcherEventMap>;
|
|
400
400
|
add<Path extends string>(input: RouteInput<Path, TState>): void;
|
|
401
|
-
add(inputs:
|
|
401
|
+
add(inputs: ReadonlyArray<RouteInput<string, TState>>): void;
|
|
402
402
|
group(prefix: string): DispatchGroupInterface<TState>;
|
|
403
403
|
match(method: Method, pathname: string): DispatchResult<TState>;
|
|
404
404
|
handle(request: Request, state: TState): Promise<Response>;
|
|
@@ -425,7 +425,7 @@ export declare interface DispatcherInterface<TState = undefined> {
|
|
|
425
425
|
* forwarded alongside `on`.
|
|
426
426
|
*/
|
|
427
427
|
export declare interface DispatcherOptions<TState> {
|
|
428
|
-
readonly routes?:
|
|
428
|
+
readonly routes?: ReadonlyArray<RouteInput<string, TState>>;
|
|
429
429
|
readonly sensitive?: boolean;
|
|
430
430
|
readonly unmatched?: (request: Request) => Response | Promise<Response>;
|
|
431
431
|
readonly unmethoded?: (request: Request, allow: readonly Method[]) => Response | Promise<Response>;
|
|
@@ -461,7 +461,7 @@ export declare class DispatchGroup<TState> implements DispatchGroupInterface<TSt
|
|
|
461
461
|
readonly prefix: string;
|
|
462
462
|
constructor(parent: DispatcherInterface<TState>, prefix: string);
|
|
463
463
|
add<Path extends string>(input: RouteInput<Path, TState>): void;
|
|
464
|
-
add(inputs:
|
|
464
|
+
add(inputs: ReadonlyArray<RouteInput<string, TState>>): void;
|
|
465
465
|
group(prefix: string): DispatchGroupInterface<TState>;
|
|
466
466
|
}
|
|
467
467
|
|
|
@@ -484,7 +484,7 @@ export declare class DispatchGroup<TState> implements DispatchGroupInterface<TSt
|
|
|
484
484
|
export declare interface DispatchGroupInterface<TState> {
|
|
485
485
|
readonly prefix: string;
|
|
486
486
|
add<Path extends string>(input: RouteInput<Path, TState>): void;
|
|
487
|
-
add(inputs:
|
|
487
|
+
add(inputs: ReadonlyArray<RouteInput<string, TState>>): void;
|
|
488
488
|
group(prefix: string): DispatchGroupInterface<TState>;
|
|
489
489
|
}
|
|
490
490
|
|
|
@@ -561,7 +561,7 @@ export declare class Group<Meta> implements GroupInterface<Meta> {
|
|
|
561
561
|
readonly prefix: string;
|
|
562
562
|
constructor(parent: RouterInterface<Meta>, prefix: string);
|
|
563
563
|
add(entry: RouteEntry<Meta>): void;
|
|
564
|
-
add(entries:
|
|
564
|
+
add(entries: ReadonlyArray<RouteEntry<Meta>>): void;
|
|
565
565
|
group(prefix: string): GroupInterface<Meta>;
|
|
566
566
|
}
|
|
567
567
|
|
|
@@ -583,7 +583,7 @@ export declare class Group<Meta> implements GroupInterface<Meta> {
|
|
|
583
583
|
export declare interface GroupInterface<Meta> {
|
|
584
584
|
readonly prefix: string;
|
|
585
585
|
add(entry: RouteEntry<Meta>): void;
|
|
586
|
-
add(entries:
|
|
586
|
+
add(entries: ReadonlyArray<RouteEntry<Meta>>): void;
|
|
587
587
|
group(prefix: string): GroupInterface<Meta>;
|
|
588
588
|
}
|
|
589
589
|
|
|
@@ -918,10 +918,10 @@ export declare class Router<Meta> implements RouterInterface<Meta> {
|
|
|
918
918
|
constructor(options?: RouterOptions<Meta>);
|
|
919
919
|
get count(): number;
|
|
920
920
|
add(entry: RouteEntry<Meta>): void;
|
|
921
|
-
add(entries:
|
|
921
|
+
add(entries: ReadonlyArray<RouteEntry<Meta>>): void;
|
|
922
922
|
match(pathname: string, answers?: AnswerHandler<Meta>): RouterMatch<Meta> | undefined;
|
|
923
|
-
entries():
|
|
924
|
-
entries(pathname: string):
|
|
923
|
+
entries(): ReadonlyArray<RouteEntry<Meta>>;
|
|
924
|
+
entries(pathname: string): ReadonlyArray<RouteEntry<Meta>>;
|
|
925
925
|
group(prefix: string): GroupInterface<Meta>;
|
|
926
926
|
clear(): void;
|
|
927
927
|
}
|
|
@@ -977,10 +977,10 @@ export declare interface RouteRecord<TState> {
|
|
|
977
977
|
export declare interface RouterInterface<Meta> {
|
|
978
978
|
readonly count: number;
|
|
979
979
|
add(entry: RouteEntry<Meta>): void;
|
|
980
|
-
add(entries:
|
|
980
|
+
add(entries: ReadonlyArray<RouteEntry<Meta>>): void;
|
|
981
981
|
match(pathname: string, answers?: AnswerHandler<Meta>): RouterMatch<Meta> | undefined;
|
|
982
|
-
entries():
|
|
983
|
-
entries(pathname: string):
|
|
982
|
+
entries(): ReadonlyArray<RouteEntry<Meta>>;
|
|
983
|
+
entries(pathname: string): ReadonlyArray<RouteEntry<Meta>>;
|
|
984
984
|
group(prefix: string): GroupInterface<Meta>;
|
|
985
985
|
clear(): void;
|
|
986
986
|
}
|
|
@@ -1026,7 +1026,7 @@ export declare interface RouterMatch<Meta> {
|
|
|
1026
1026
|
* registered entry is kept, even duplicate paths.
|
|
1027
1027
|
*/
|
|
1028
1028
|
export declare interface RouterOptions<Meta> {
|
|
1029
|
-
readonly entries?:
|
|
1029
|
+
readonly entries?: ReadonlyArray<RouteEntry<Meta>>;
|
|
1030
1030
|
readonly sensitive?: boolean;
|
|
1031
1031
|
readonly key?: (entry: RouteEntry<Meta>) => string;
|
|
1032
1032
|
}
|
package/dist/src/core/index.d.ts
CHANGED
|
@@ -335,7 +335,7 @@ export declare class Dispatcher<TState = undefined> implements DispatcherInterfa
|
|
|
335
335
|
constructor(options?: DispatcherOptions<TState>);
|
|
336
336
|
get emitter(): EmitterInterface<DispatcherEventMap>;
|
|
337
337
|
add<Path extends string>(input: RouteInput<Path, TState>): void;
|
|
338
|
-
add(inputs:
|
|
338
|
+
add(inputs: ReadonlyArray<RouteInput<string, TState>>): void;
|
|
339
339
|
group(prefix: string): DispatchGroupInterface<TState>;
|
|
340
340
|
match(method: Method, pathname: string): DispatchResult<TState>;
|
|
341
341
|
handle(request: Request, state: TState): Promise<Response>;
|
|
@@ -398,7 +398,7 @@ export declare interface DispatcherInterface<TState = undefined> {
|
|
|
398
398
|
readonly router: RouterInterface<RouteRecord<TState>>;
|
|
399
399
|
readonly emitter: EmitterInterface<DispatcherEventMap>;
|
|
400
400
|
add<Path extends string>(input: RouteInput<Path, TState>): void;
|
|
401
|
-
add(inputs:
|
|
401
|
+
add(inputs: ReadonlyArray<RouteInput<string, TState>>): void;
|
|
402
402
|
group(prefix: string): DispatchGroupInterface<TState>;
|
|
403
403
|
match(method: Method, pathname: string): DispatchResult<TState>;
|
|
404
404
|
handle(request: Request, state: TState): Promise<Response>;
|
|
@@ -425,7 +425,7 @@ export declare interface DispatcherInterface<TState = undefined> {
|
|
|
425
425
|
* forwarded alongside `on`.
|
|
426
426
|
*/
|
|
427
427
|
export declare interface DispatcherOptions<TState> {
|
|
428
|
-
readonly routes?:
|
|
428
|
+
readonly routes?: ReadonlyArray<RouteInput<string, TState>>;
|
|
429
429
|
readonly sensitive?: boolean;
|
|
430
430
|
readonly unmatched?: (request: Request) => Response | Promise<Response>;
|
|
431
431
|
readonly unmethoded?: (request: Request, allow: readonly Method[]) => Response | Promise<Response>;
|
|
@@ -461,7 +461,7 @@ export declare class DispatchGroup<TState> implements DispatchGroupInterface<TSt
|
|
|
461
461
|
readonly prefix: string;
|
|
462
462
|
constructor(parent: DispatcherInterface<TState>, prefix: string);
|
|
463
463
|
add<Path extends string>(input: RouteInput<Path, TState>): void;
|
|
464
|
-
add(inputs:
|
|
464
|
+
add(inputs: ReadonlyArray<RouteInput<string, TState>>): void;
|
|
465
465
|
group(prefix: string): DispatchGroupInterface<TState>;
|
|
466
466
|
}
|
|
467
467
|
|
|
@@ -484,7 +484,7 @@ export declare class DispatchGroup<TState> implements DispatchGroupInterface<TSt
|
|
|
484
484
|
export declare interface DispatchGroupInterface<TState> {
|
|
485
485
|
readonly prefix: string;
|
|
486
486
|
add<Path extends string>(input: RouteInput<Path, TState>): void;
|
|
487
|
-
add(inputs:
|
|
487
|
+
add(inputs: ReadonlyArray<RouteInput<string, TState>>): void;
|
|
488
488
|
group(prefix: string): DispatchGroupInterface<TState>;
|
|
489
489
|
}
|
|
490
490
|
|
|
@@ -561,7 +561,7 @@ export declare class Group<Meta> implements GroupInterface<Meta> {
|
|
|
561
561
|
readonly prefix: string;
|
|
562
562
|
constructor(parent: RouterInterface<Meta>, prefix: string);
|
|
563
563
|
add(entry: RouteEntry<Meta>): void;
|
|
564
|
-
add(entries:
|
|
564
|
+
add(entries: ReadonlyArray<RouteEntry<Meta>>): void;
|
|
565
565
|
group(prefix: string): GroupInterface<Meta>;
|
|
566
566
|
}
|
|
567
567
|
|
|
@@ -583,7 +583,7 @@ export declare class Group<Meta> implements GroupInterface<Meta> {
|
|
|
583
583
|
export declare interface GroupInterface<Meta> {
|
|
584
584
|
readonly prefix: string;
|
|
585
585
|
add(entry: RouteEntry<Meta>): void;
|
|
586
|
-
add(entries:
|
|
586
|
+
add(entries: ReadonlyArray<RouteEntry<Meta>>): void;
|
|
587
587
|
group(prefix: string): GroupInterface<Meta>;
|
|
588
588
|
}
|
|
589
589
|
|
|
@@ -918,10 +918,10 @@ export declare class Router<Meta> implements RouterInterface<Meta> {
|
|
|
918
918
|
constructor(options?: RouterOptions<Meta>);
|
|
919
919
|
get count(): number;
|
|
920
920
|
add(entry: RouteEntry<Meta>): void;
|
|
921
|
-
add(entries:
|
|
921
|
+
add(entries: ReadonlyArray<RouteEntry<Meta>>): void;
|
|
922
922
|
match(pathname: string, answers?: AnswerHandler<Meta>): RouterMatch<Meta> | undefined;
|
|
923
|
-
entries():
|
|
924
|
-
entries(pathname: string):
|
|
923
|
+
entries(): ReadonlyArray<RouteEntry<Meta>>;
|
|
924
|
+
entries(pathname: string): ReadonlyArray<RouteEntry<Meta>>;
|
|
925
925
|
group(prefix: string): GroupInterface<Meta>;
|
|
926
926
|
clear(): void;
|
|
927
927
|
}
|
|
@@ -977,10 +977,10 @@ export declare interface RouteRecord<TState> {
|
|
|
977
977
|
export declare interface RouterInterface<Meta> {
|
|
978
978
|
readonly count: number;
|
|
979
979
|
add(entry: RouteEntry<Meta>): void;
|
|
980
|
-
add(entries:
|
|
980
|
+
add(entries: ReadonlyArray<RouteEntry<Meta>>): void;
|
|
981
981
|
match(pathname: string, answers?: AnswerHandler<Meta>): RouterMatch<Meta> | undefined;
|
|
982
|
-
entries():
|
|
983
|
-
entries(pathname: string):
|
|
982
|
+
entries(): ReadonlyArray<RouteEntry<Meta>>;
|
|
983
|
+
entries(pathname: string): ReadonlyArray<RouteEntry<Meta>>;
|
|
984
984
|
group(prefix: string): GroupInterface<Meta>;
|
|
985
985
|
clear(): void;
|
|
986
986
|
}
|
|
@@ -1026,7 +1026,7 @@ export declare interface RouterMatch<Meta> {
|
|
|
1026
1026
|
* registered entry is kept, even duplicate paths.
|
|
1027
1027
|
*/
|
|
1028
1028
|
export declare interface RouterOptions<Meta> {
|
|
1029
|
-
readonly entries?:
|
|
1029
|
+
readonly entries?: ReadonlyArray<RouteEntry<Meta>>;
|
|
1030
1030
|
readonly sensitive?: boolean;
|
|
1031
1031
|
readonly key?: (entry: RouteEntry<Meta>) => string;
|
|
1032
1032
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","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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"}
|
|
1
|
+
{"version":3,"file":"index.js","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: ReadonlyArray<RouteEntry<Meta>>): void\n\tadd(input: RouteEntry<Meta> | ReadonlyArray<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: Array<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: ReadonlyArray<RouteEntry<Meta>>): void\n\tadd(input: RouteEntry<Meta> | ReadonlyArray<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(): ReadonlyArray<RouteEntry<Meta>>\n\tentries(pathname: string): ReadonlyArray<RouteEntry<Meta>>\n\tentries(pathname?: string): ReadonlyArray<RouteEntry<Meta>> {\n\t\tif (pathname === undefined) return [...this.#entries]\n\t\tconst out: Array<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: ReadonlyArray<RouteInput<string, TState>>): void\n\tadd(input: RouteInput<string, TState> | ReadonlyArray<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: ReadonlyArray<RouteInput<string, TState>>): void\n\tadd(input: RouteInput<string, TState> | ReadonlyArray<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: ReadonlyArray<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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orkestrel/router",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"description": "A typed request router for the @orkestrel line — server and browser environments. Part of the @orkestrel line.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"browser",
|
|
@@ -70,38 +70,42 @@
|
|
|
70
70
|
"format": "oxfmt --config .oxfmtrc.json --write .",
|
|
71
71
|
"format:check": "oxfmt --config .oxfmtrc.json --check .",
|
|
72
72
|
"lint:check": "oxlint --config .oxlintrc.json --deny-warnings .",
|
|
73
|
-
"test": "npm run test:src && npm run test:policy && npm run test:guides",
|
|
73
|
+
"test": "npm run test:src && npm run test:policy && npm run test:config && npm run test:guides",
|
|
74
74
|
"test:src": "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:core --project src:browser --project src:server",
|
|
75
75
|
"test:src:core": "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:core",
|
|
76
76
|
"test:src:browser": "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:browser",
|
|
77
77
|
"test:src:server": "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:server",
|
|
78
78
|
"test:policy": "vitest run --config vite.config.ts --no-cache --reporter=dot --project policy",
|
|
79
|
+
"test:config": "vitest run --config vite.config.ts --no-cache --reporter=dot --project config",
|
|
79
80
|
"test:guides": "vitest run --config vite.config.ts --reporter=dot --project guides",
|
|
80
81
|
"build": "npm run clean && npm run build:src",
|
|
81
82
|
"build:src": "npm run build:src:core && npm run build:src:browser && npm run build:src:server",
|
|
82
83
|
"build:src:core": "vite build --config configs/src/vite.core.config.ts && npm run copy dist/src/core/index.d.ts dist/src/core/index.d.cts",
|
|
83
84
|
"build:src:browser": "vite build --config configs/src/vite.browser.config.ts",
|
|
84
85
|
"build:src:server": "vite build --config configs/src/vite.server.config.ts && npm run copy dist/src/server/index.d.ts dist/src/server/index.d.cts",
|
|
86
|
+
"prepack": "npm run build",
|
|
85
87
|
"prepublishOnly": "npm run format:check && npm run lint:check && npm run check && npm run build && npm test"
|
|
86
88
|
},
|
|
87
89
|
"dependencies": {
|
|
88
|
-
"@orkestrel/abort": "^0.0.
|
|
89
|
-
"@orkestrel/contract": "^0.0.
|
|
90
|
-
"@orkestrel/emitter": "^0.0.
|
|
90
|
+
"@orkestrel/abort": "^0.0.7",
|
|
91
|
+
"@orkestrel/contract": "^0.0.13",
|
|
92
|
+
"@orkestrel/emitter": "^0.0.7"
|
|
91
93
|
},
|
|
92
94
|
"devDependencies": {
|
|
93
|
-
"@microsoft/api-extractor": "^7.
|
|
94
|
-
"@orkestrel/guide": "^0.0.
|
|
95
|
-
"@orkestrel/
|
|
96
|
-
"@
|
|
97
|
-
"@
|
|
98
|
-
"
|
|
99
|
-
"
|
|
95
|
+
"@microsoft/api-extractor": "^7.59.0",
|
|
96
|
+
"@orkestrel/guide": "^0.0.12",
|
|
97
|
+
"@orkestrel/probe": "^0.0.2",
|
|
98
|
+
"@orkestrel/scaffold": "^0.0.47",
|
|
99
|
+
"@orkestrel/test": "^0.0.8",
|
|
100
|
+
"@types/node": "^26.2.0",
|
|
101
|
+
"@vitest/browser-playwright": "^4.1.11",
|
|
102
|
+
"oxfmt": "^0.64.0",
|
|
103
|
+
"oxlint": "^1.79.0",
|
|
100
104
|
"playwright": "^1.62.1",
|
|
101
105
|
"typescript": "^6.0.3",
|
|
102
|
-
"vite": "
|
|
106
|
+
"vite": "^8.2.2",
|
|
103
107
|
"vite-plugin-dts": "^5.0.3",
|
|
104
|
-
"vitest": "^4.1.
|
|
108
|
+
"vitest": "^4.1.11"
|
|
105
109
|
},
|
|
106
110
|
"engines": {
|
|
107
111
|
"node": ">=22.12.0"
|