@orkestrel/router 0.0.14 → 0.0.15
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/core/index.cjs +4 -4
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.js +5 -5
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +3 -3
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.js +4 -4
- package/dist/src/server/index.js.map +1 -1
- package/package.json +12 -12
package/dist/src/core/index.cjs
CHANGED
|
@@ -539,7 +539,7 @@ var Group = class Group {
|
|
|
539
539
|
this.prefix = prefix;
|
|
540
540
|
}
|
|
541
541
|
add(input) {
|
|
542
|
-
const inputs =
|
|
542
|
+
const inputs = (0, _orkestrel_contract.isArray)(input) ? input : [input];
|
|
543
543
|
this.#parent.add(inputs.map((entry) => ({
|
|
544
544
|
...entry,
|
|
545
545
|
path: joinPaths(this.prefix, entry.path)
|
|
@@ -595,7 +595,7 @@ var Router = class {
|
|
|
595
595
|
return this.#entries.length;
|
|
596
596
|
}
|
|
597
597
|
add(input) {
|
|
598
|
-
const inputs =
|
|
598
|
+
const inputs = (0, _orkestrel_contract.isArray)(input) ? input : [input];
|
|
599
599
|
for (const entry of inputs) this.#register(entry);
|
|
600
600
|
}
|
|
601
601
|
match(pathname, answers) {
|
|
@@ -707,7 +707,7 @@ var DispatchGroup = class DispatchGroup {
|
|
|
707
707
|
this.prefix = prefix;
|
|
708
708
|
}
|
|
709
709
|
add(input) {
|
|
710
|
-
const inputs =
|
|
710
|
+
const inputs = (0, _orkestrel_contract.isArray)(input) ? input : [input];
|
|
711
711
|
this.#parent.add(inputs.map((route) => ({
|
|
712
712
|
...route,
|
|
713
713
|
path: joinPaths(this.prefix, route.path)
|
|
@@ -784,7 +784,7 @@ var Dispatcher = class {
|
|
|
784
784
|
return this.#emitter;
|
|
785
785
|
}
|
|
786
786
|
add(input) {
|
|
787
|
-
const inputs =
|
|
787
|
+
const inputs = (0, _orkestrel_contract.isArray)(input) ? input : [input];
|
|
788
788
|
for (const route of inputs) this.#register(route);
|
|
789
789
|
}
|
|
790
790
|
group(prefix) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/core/constants.ts","../../../src/core/helpers.ts","../../../src/core/parsers.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 centralized-file rule's home for module-scope data used by\n// the matching engine and the fetch dispatcher. Every declaration here is frozen\n// and `export`ed per the centralized-file rule.\n// ============================================================================\n\n/**\n * Lists the HTTP methods a {@link import('./types.js').DispatcherInterface} registers\n * routes under, in canonical order — a frozen literal tuple, and the single source the\n * {@link import('./types.js').Method} type, {@link METHODS}, and `parseMethod` are all\n * derived from.\n *\n * @remarks\n * A frozen tuple of the verbs: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`,\n * `HEAD`, `OPTIONS`. Adding a verb here widens the `Method` type, the\n * {@link METHODS} membership set, and the `parseMethod` narrowing together, so\n * the method set cannot drift between them. Prefer {@link METHODS} for a\n * membership test; use this tuple where order or literal typing matters.\n *\n * @example\n * ```ts\n * METHOD_LIST[0] // 'GET'\n * METHOD_LIST.includes('GET') // true\n * ```\n */\nexport const METHOD_LIST = Object.freeze([\n\t'GET',\n\t'POST',\n\t'PUT',\n\t'PATCH',\n\t'DELETE',\n\t'HEAD',\n\t'OPTIONS',\n] as const)\n\n/**\n * Holds every HTTP method a {@link import('./types.js').DispatcherInterface} registers\n * routes under as a `ReadonlySet` — backs the registration guard (`add` rejects any\n * `method` outside this set) and the auto-`OPTIONS` `Allow` derivation.\n *\n * @remarks\n * A `ReadonlySet` built from {@link METHOD_LIST}, so it carries exactly the\n * {@link import('./types.js').Method} literals: `GET`, `POST`, `PUT`,\n * `PATCH`, `DELETE`, `HEAD`, `OPTIONS`. `HEAD` is included even though it is\n * never required at registration (a `GET` route auto-answers `HEAD`) — it is\n * still a valid method to register explicitly. The element type stays `string`\n * so a raw, unnarrowed `request.method` can be tested directly.\n *\n * @example\n * ```ts\n * METHODS.has('GET') // true\n * METHODS.has('TRACE') // false\n * ```\n */\nexport const METHODS: ReadonlySet<string> = Object.freeze(new Set<string>(METHOD_LIST))\n\n/**\n * Names the 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` (the path compiler in `helpers.ts`) when ranking candidate\n * matches left-to-right at the earliest differing segment.\n *\n * @example\n * ```ts\n * TIER_LITERAL > TIER_PARAM // true\n * ```\n */\nexport const TIER_LITERAL = 2\n\n/**\n * Names the 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` (the path compiler in `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 * Names the 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` (the path compiler in `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 { ContractError, preview } from '@orkestrel/contract'\nimport { TIER_LITERAL, TIER_PARAM, TIER_WILDCARD } from './constants.js'\n\n// The PURE path-matching primitives (self-describing helper naming — module scope,\n// no entity context). Every one is exported (the centralized-file rule): 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 * Escapes 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 * Canonicalizes a route path for registry identity — strips 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 * Computes the canonical `METHOD /path` registry key for a method-dimensioned\n * 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 * Compiles 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 a `ContractError` at the 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 (for example `/users/:id`, `/files/*rest`)\n * @param sensitive - If `true`, matching is case-sensitive; if `false`, case is\n * folded during matching. Default: `true`\n * @returns The {@link CompiledPath} — its `regex` + ordered `params`\n * @throws {@link import('@orkestrel/contract').ContractError} Thrown when a\n * `*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, guarded at the boundary.\n\t\tif (!isFinal && /^\\*[A-Za-z_]\\w*/.test(segment))\n\t\t\tthrow new ContractError('a wildcard segment must be the final segment of a path pattern', {\n\t\t\t\tcode: 'placement',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['path'],\n\t\t\t\t\tlimit: `a wildcard only in the final segment, not \"${segment}\"`,\n\t\t\t\t\treceived: preview(path),\n\t\t\t\t},\n\t\t\t})\n\t\t// Classification and compilation share ONE segment parser — 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 * Decodes one captured param value from a URL, 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). 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 * Extracts 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 (for example `/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 * Classifies 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, and everything else (including a\n * literal segment that merely contains a `:` mid-string, for example `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. Pure and total.\n *\n * @param segment - One `/`-split path segment\n * @param isFinal - If `true`, `segment` is the path's last segment and may\n * classify as a wildcard; if `false`, a wildcard-shaped segment classifies as\n * a literal\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 * Computes 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 through {@link classifySegment} — the same segment parser\n * {@link compilePath} uses, so a literal segment that merely contains a `:`\n * (for example `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 (for example `/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 * Compares 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 * Joins 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, 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 (for example `/api`)\n * @param path - The route path being joined under the prefix (for example `/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 * Provides an identity pass-through for a {@link RouteInput} that pins its `Path`\n * generic 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 `defineRoute(...)`\n * supplies that pin: its `const Path extends string` type parameter infers the\n * NARROW 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 `defineRoute(...)` calls\n * still widens each element's `Path` to `string` after collection into one array —\n * 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 * through {@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 = defineRoute({\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 defineRoute<const Path extends string, TState = undefined>(\n\tinput: RouteInput<Path, TState>,\n): RouteInput<Path, TState> {\n\treturn input\n}\n","// ============================================================================\n// Core coercers — the centralized-file rule's home for `parse*` narrowing leaves that\n// turn a raw external string into a typed core value or `undefined`. Every\n// declaration here is total and `export`ed per the centralized-file rule.\n// ============================================================================\n\nimport type { Method } from './types.js'\nimport { METHOD_LIST } from './constants.js'\n\n/**\n * Narrows a raw `request.method` string into a typed {@link Method} — total,\n * never throws.\n *\n * @remarks\n * Consults {@link import('./constants.js').METHOD_LIST} (the one home for the\n * registrable HTTP methods), so a verb added there narrows here without\n * a second list to update; any other value (an unknown verb, non-uppercase\n * casing) resolves to `undefined` rather than throwing (total guard behavior).\n * Pure leaf shared by the `Dispatcher`'s `handle` (honest about an unknown verb)\n * and 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 a\n * registrable method\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\treturn METHOD_LIST.find((method) => method === value)\n}\n","import type { GroupInterface, RouteEntry, RouterInterface } from './types.js'\nimport { joinPaths } from './helpers.js'\n\n/**\n * Represents a prefix-scoped registration handle over a {@link import('./Router.js').Router} —\n * pure string composition, 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 through {@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 { ContractError, isString, preview } from '@orkestrel/contract'\nimport { compareSpecificity, compilePath, matchPath } from './helpers.js'\nimport { Group } from './Group.js'\n\n/**\n * Represents the path-matching + registry engine — registers `{ path, meta, name? }`\n * entries (compiling each path once) and resolves a concrete pathname to the most\n * specific matching entry. The shared machine both the `Navigator` (browser) and\n * 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.** `add` validates each entry's\n * `path` — `isString` plus a leading `/` — and throws a `ContractError` 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 through `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 through {@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 (isString + leading '/'), then either replace an\n\t// existing entry IN PLACE (dedup through `#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))\n\t\t\tthrow new ContractError('a route path must be a string', {\n\t\t\t\tcode: 'literal',\n\t\t\t\tcontext: { path: ['entry', 'path'], limit: 'string', received: preview(entry.path) },\n\t\t\t})\n\t\tif (!entry.path.startsWith('/'))\n\t\t\tthrow new ContractError('a route path must start with \"/\"', {\n\t\t\t\tcode: 'pattern',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['entry', 'path'],\n\t\t\t\t\tlimit: 'a \"/\"-prefixed path pattern',\n\t\t\t\t\treceived: preview(entry.path),\n\t\t\t\t},\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 * Represents a prefix-scoped registration handle over a\n * {@link import('./Dispatcher.js').Dispatcher} — the method-dimensioned counterpart of\n * `Group`.\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` through {@link joinPaths} against\n * `this.prefix` and forwards to the OWNING dispatcher's `add` (its own\n * registration boundary guard still applies). Pure string composition — 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 { ContractError, isFunction, isString, preview } from '@orkestrel/contract'\nimport { METHODS } from './constants.js'\nimport { computeDispatchKey } from './helpers.js'\nimport { parseMethod } from './parsers.js'\nimport { Router } from './Router.js'\nimport { DispatchGroup } from './DispatchGroup.js'\n\n/**\n * Represents the fetch-standard, method-dimensioned dispatch entity — layers HTTP\n * method dispatch and web-standard `Request`/`Response` handling over one internal\n * `Router<RouteRecord<TState>>`. The core machine the server face and any\n * fetch-native runtime consume 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.\n * - **Registration boundary guard.** `add` validates each input's\n * `handler` (`isFunction`) and `method` (must be in {@link METHODS}) —\n * throws a `ContractError` 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.\n * - **Emitter.** 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 router(): RouterInterface<RouteRecord<TState>> {\n\t\treturn this.#router\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') {\n\t\t\t\tconst hit = this.#router.match(pathname)\n\t\t\t\tif (hit !== undefined) return this.#respondAutoOptions(hit.path, result.allow)\n\t\t\t}\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 (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 ContractError('a route handler must be a function', {\n\t\t\t\tcode: 'literal',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['input', 'handler'],\n\t\t\t\t\tlimit: 'function',\n\t\t\t\t\treceived: preview(input.handler),\n\t\t\t\t},\n\t\t\t})\n\t\tif (!isString(input.method) || !METHODS.has(input.method))\n\t\t\tthrow new ContractError('a route method must be a registrable HTTP method', {\n\t\t\t\tcode: 'literal',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['input', 'method'],\n\t\t\t\t\tlimit: [...METHODS].join(', '),\n\t\t\t\t\treceived: preview(input.method),\n\t\t\t\t},\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.\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), emitting\n\t// `match` under the most-specific REGISTERED pattern the pathname resolved to, so a consumer\n\t// aggregating by pattern sees bounded cardinality rather than one label per request path.\n\t#respondAutoOptions(pattern: string, allow: readonly Method[]): Response {\n\t\tthis.#emitter.emit('match', 'OPTIONS', pattern)\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 * Creates 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 Register and match\n * ```ts\n * import { createDispatcher, createRouter } from '@orkestrel/router'\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 * const dispatcher = createDispatcher<{ readonly userId: string }>({\n * \troutes: [\n * \t\t{\n * \t\t\tmethod: 'GET',\n * \t\t\tpath: '/users/:id',\n * \t\t\thandler: (_request, context) => Response.json(context.params),\n * \t\t},\n * \t],\n * })\n * const response = await dispatcher.handle(new Request('http://x/users/7'), { userId: 'me' })\n * ```\n */\nexport function createRouter<Meta>(options?: RouterOptions<Meta>): RouterInterface<Meta> {\n\treturn new Router<Meta>(options)\n}\n\n/**\n * Creates a {@link DispatcherInterface} — the fetch-standard,\n * method-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\n * Emitter pattern's `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":[],"sources":["../../../src/core/constants.ts","../../../src/core/helpers.ts","../../../src/core/parsers.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 centralized-file rule's home for module-scope data used by\n// the matching engine and the fetch dispatcher. Every declaration here is frozen\n// and `export`ed per the centralized-file rule.\n// ============================================================================\n\n/**\n * Lists the HTTP methods a {@link import('./types.js').DispatcherInterface} registers\n * routes under, in canonical order — a frozen literal tuple, and the single source the\n * {@link import('./types.js').Method} type, {@link METHODS}, and `parseMethod` are all\n * derived from.\n *\n * @remarks\n * A frozen tuple of the verbs: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`,\n * `HEAD`, `OPTIONS`. Adding a verb here widens the `Method` type, the\n * {@link METHODS} membership set, and the `parseMethod` narrowing together, so\n * the method set cannot drift between them. Prefer {@link METHODS} for a\n * membership test; use this tuple where order or literal typing matters.\n *\n * @example\n * ```ts\n * METHOD_LIST[0] // 'GET'\n * METHOD_LIST.includes('GET') // true\n * ```\n */\nexport const METHOD_LIST = Object.freeze([\n\t'GET',\n\t'POST',\n\t'PUT',\n\t'PATCH',\n\t'DELETE',\n\t'HEAD',\n\t'OPTIONS',\n] as const)\n\n/**\n * Holds every HTTP method a {@link import('./types.js').DispatcherInterface} registers\n * routes under as a `ReadonlySet` — backs the registration guard (`add` rejects any\n * `method` outside this set) and the auto-`OPTIONS` `Allow` derivation.\n *\n * @remarks\n * A `ReadonlySet` built from {@link METHOD_LIST}, so it carries exactly the\n * {@link import('./types.js').Method} literals: `GET`, `POST`, `PUT`,\n * `PATCH`, `DELETE`, `HEAD`, `OPTIONS`. `HEAD` is included even though it is\n * never required at registration (a `GET` route auto-answers `HEAD`) — it is\n * still a valid method to register explicitly. The element type stays `string`\n * so a raw, unnarrowed `request.method` can be tested directly.\n *\n * @example\n * ```ts\n * METHODS.has('GET') // true\n * METHODS.has('TRACE') // false\n * ```\n */\nexport const METHODS: ReadonlySet<string> = Object.freeze(new Set<string>(METHOD_LIST))\n\n/**\n * Names the 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` (the path compiler in `helpers.ts`) when ranking candidate\n * matches left-to-right at the earliest differing segment.\n *\n * @example\n * ```ts\n * TIER_LITERAL > TIER_PARAM // true\n * ```\n */\nexport const TIER_LITERAL = 2\n\n/**\n * Names the 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` (the path compiler in `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 * Names the 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` (the path compiler in `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 { ContractError, preview } from '@orkestrel/contract'\nimport { TIER_LITERAL, TIER_PARAM, TIER_WILDCARD } from './constants.js'\n\n// The PURE path-matching primitives (self-describing helper naming — module scope,\n// no entity context). Every one is exported (the centralized-file rule): 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 * Escapes 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 * Canonicalizes a route path for registry identity — strips 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 * Computes the canonical `METHOD /path` registry key for a method-dimensioned\n * 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 * Compiles 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 a `ContractError` at the 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 (for example `/users/:id`, `/files/*rest`)\n * @param sensitive - If `true`, matching is case-sensitive; if `false`, case is\n * folded during matching. Default: `true`\n * @returns The {@link CompiledPath} — its `regex` + ordered `params`\n * @throws {@link import('@orkestrel/contract').ContractError} Thrown when a\n * `*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, guarded at the boundary.\n\t\tif (!isFinal && /^\\*[A-Za-z_]\\w*/.test(segment))\n\t\t\tthrow new ContractError('a wildcard segment must be the final segment of a path pattern', {\n\t\t\t\tcode: 'placement',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['path'],\n\t\t\t\t\tlimit: `a wildcard only in the final segment, not \"${segment}\"`,\n\t\t\t\t\treceived: preview(path),\n\t\t\t\t},\n\t\t\t})\n\t\t// Classification and compilation share ONE segment parser — 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 * Decodes one captured param value from a URL, 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). 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 * Extracts 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 (for example `/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 * Classifies 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, and everything else (including a\n * literal segment that merely contains a `:` mid-string, for example `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. Pure and total.\n *\n * @param segment - One `/`-split path segment\n * @param isFinal - If `true`, `segment` is the path's last segment and may\n * classify as a wildcard; if `false`, a wildcard-shaped segment classifies as\n * a literal\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 * Computes 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 through {@link classifySegment} — the same segment parser\n * {@link compilePath} uses, so a literal segment that merely contains a `:`\n * (for example `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 (for example `/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 * Compares 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 * Joins 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, 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 (for example `/api`)\n * @param path - The route path being joined under the prefix (for example `/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 * Provides an identity pass-through for a {@link RouteInput} that pins its `Path`\n * generic 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 `defineRoute(...)`\n * supplies that pin: its `const Path extends string` type parameter infers the\n * NARROW 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 `defineRoute(...)` calls\n * still widens each element's `Path` to `string` after collection into one array —\n * 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 * through {@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 = defineRoute({\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 defineRoute<const Path extends string, TState = undefined>(\n\tinput: RouteInput<Path, TState>,\n): RouteInput<Path, TState> {\n\treturn input\n}\n","// ============================================================================\n// Core coercers — the centralized-file rule's home for `parse*` narrowing leaves that\n// turn a raw external string into a typed core value or `undefined`. Every\n// declaration here is total and `export`ed per the centralized-file rule.\n// ============================================================================\n\nimport type { Method } from './types.js'\nimport { METHOD_LIST } from './constants.js'\n\n/**\n * Narrows a raw `request.method` string into a typed {@link Method} — total,\n * never throws.\n *\n * @remarks\n * Consults {@link import('./constants.js').METHOD_LIST} (the one home for the\n * registrable HTTP methods), so a verb added there narrows here without\n * a second list to update; any other value (an unknown verb, non-uppercase\n * casing) resolves to `undefined` rather than throwing (total guard behavior).\n * Pure leaf shared by the `Dispatcher`'s `handle` (honest about an unknown verb)\n * and 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 a\n * registrable method\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\treturn METHOD_LIST.find((method) => method === value)\n}\n","import type { GroupInterface, RouteEntry, RouterInterface } from './types.js'\nimport { isArray } from '@orkestrel/contract'\nimport { joinPaths } from './helpers.js'\n\n/**\n * Represents a prefix-scoped registration handle over a {@link import('./Router.js').Router} —\n * pure string composition, 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 through {@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 = 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 { ContractError, isArray, isString, preview } from '@orkestrel/contract'\nimport { compareSpecificity, compilePath, matchPath } from './helpers.js'\nimport { Group } from './Group.js'\n\n/**\n * Represents the path-matching + registry engine — registers `{ path, meta, name? }`\n * entries (compiling each path once) and resolves a concrete pathname to the most\n * specific matching entry. The shared machine both the `Navigator` (browser) and\n * 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.** `add` validates each entry's\n * `path` — `isString` plus a leading `/` — and throws a `ContractError` 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 through `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 through {@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 = 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 (isString + leading '/'), then either replace an\n\t// existing entry IN PLACE (dedup through `#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))\n\t\t\tthrow new ContractError('a route path must be a string', {\n\t\t\t\tcode: 'literal',\n\t\t\t\tcontext: { path: ['entry', 'path'], limit: 'string', received: preview(entry.path) },\n\t\t\t})\n\t\tif (!entry.path.startsWith('/'))\n\t\t\tthrow new ContractError('a route path must start with \"/\"', {\n\t\t\t\tcode: 'pattern',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['entry', 'path'],\n\t\t\t\t\tlimit: 'a \"/\"-prefixed path pattern',\n\t\t\t\t\treceived: preview(entry.path),\n\t\t\t\t},\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 { isArray } from '@orkestrel/contract'\nimport { joinPaths } from './helpers.js'\n\n/**\n * Represents a prefix-scoped registration handle over a\n * {@link import('./Dispatcher.js').Dispatcher} — the method-dimensioned counterpart of\n * `Group`.\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` through {@link joinPaths} against\n * `this.prefix` and forwards to the OWNING dispatcher's `add` (its own\n * registration boundary guard still applies). Pure string composition — 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 = 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 { ContractError, isArray, isFunction, isString, preview } from '@orkestrel/contract'\nimport { METHODS } from './constants.js'\nimport { computeDispatchKey } from './helpers.js'\nimport { parseMethod } from './parsers.js'\nimport { Router } from './Router.js'\nimport { DispatchGroup } from './DispatchGroup.js'\n\n/**\n * Represents the fetch-standard, method-dimensioned dispatch entity — layers HTTP\n * method dispatch and web-standard `Request`/`Response` handling over one internal\n * `Router<RouteRecord<TState>>`. The core machine the server face and any\n * fetch-native runtime consume 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.\n * - **Registration boundary guard.** `add` validates each input's\n * `handler` (`isFunction`) and `method` (must be in {@link METHODS}) —\n * throws a `ContractError` 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.\n * - **Emitter.** 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 router(): RouterInterface<RouteRecord<TState>> {\n\t\treturn this.#router\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 = 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') {\n\t\t\t\tconst hit = this.#router.match(pathname)\n\t\t\t\tif (hit !== undefined) return this.#respondAutoOptions(hit.path, result.allow)\n\t\t\t}\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 (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 ContractError('a route handler must be a function', {\n\t\t\t\tcode: 'literal',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['input', 'handler'],\n\t\t\t\t\tlimit: 'function',\n\t\t\t\t\treceived: preview(input.handler),\n\t\t\t\t},\n\t\t\t})\n\t\tif (!isString(input.method) || !METHODS.has(input.method))\n\t\t\tthrow new ContractError('a route method must be a registrable HTTP method', {\n\t\t\t\tcode: 'literal',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['input', 'method'],\n\t\t\t\t\tlimit: [...METHODS].join(', '),\n\t\t\t\t\treceived: preview(input.method),\n\t\t\t\t},\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.\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), emitting\n\t// `match` under the most-specific REGISTERED pattern the pathname resolved to, so a consumer\n\t// aggregating by pattern sees bounded cardinality rather than one label per request path.\n\t#respondAutoOptions(pattern: string, allow: readonly Method[]): Response {\n\t\tthis.#emitter.emit('match', 'OPTIONS', pattern)\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 * Creates 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 Register and match\n * ```ts\n * import { createDispatcher, createRouter } from '@orkestrel/router'\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 * const dispatcher = createDispatcher<{ readonly userId: string }>({\n * \troutes: [\n * \t\t{\n * \t\t\tmethod: 'GET',\n * \t\t\tpath: '/users/:id',\n * \t\t\thandler: (_request, context) => Response.json(context.params),\n * \t\t},\n * \t],\n * })\n * const response = await dispatcher.handle(new Request('http://x/users/7'), { userId: 'me' })\n * ```\n */\nexport function createRouter<Meta>(options?: RouterOptions<Meta>): RouterInterface<Meta> {\n\treturn new Router<Meta>(options)\n}\n\n/**\n * Creates a {@link DispatcherInterface} — the fetch-standard,\n * method-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\n * Emitter pattern's `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/dist/src/core/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ContractError, isFunction, isString, preview } from "@orkestrel/contract";
|
|
1
|
+
import { ContractError, isArray, isFunction, isString, preview } from "@orkestrel/contract";
|
|
2
2
|
import { Emitter } from "@orkestrel/emitter";
|
|
3
3
|
//#region src/core/constants.ts
|
|
4
4
|
/**
|
|
@@ -538,7 +538,7 @@ var Group = class Group {
|
|
|
538
538
|
this.prefix = prefix;
|
|
539
539
|
}
|
|
540
540
|
add(input) {
|
|
541
|
-
const inputs =
|
|
541
|
+
const inputs = isArray(input) ? input : [input];
|
|
542
542
|
this.#parent.add(inputs.map((entry) => ({
|
|
543
543
|
...entry,
|
|
544
544
|
path: joinPaths(this.prefix, entry.path)
|
|
@@ -594,7 +594,7 @@ var Router = class {
|
|
|
594
594
|
return this.#entries.length;
|
|
595
595
|
}
|
|
596
596
|
add(input) {
|
|
597
|
-
const inputs =
|
|
597
|
+
const inputs = isArray(input) ? input : [input];
|
|
598
598
|
for (const entry of inputs) this.#register(entry);
|
|
599
599
|
}
|
|
600
600
|
match(pathname, answers) {
|
|
@@ -706,7 +706,7 @@ var DispatchGroup = class DispatchGroup {
|
|
|
706
706
|
this.prefix = prefix;
|
|
707
707
|
}
|
|
708
708
|
add(input) {
|
|
709
|
-
const inputs =
|
|
709
|
+
const inputs = isArray(input) ? input : [input];
|
|
710
710
|
this.#parent.add(inputs.map((route) => ({
|
|
711
711
|
...route,
|
|
712
712
|
path: joinPaths(this.prefix, route.path)
|
|
@@ -783,7 +783,7 @@ var Dispatcher = class {
|
|
|
783
783
|
return this.#emitter;
|
|
784
784
|
}
|
|
785
785
|
add(input) {
|
|
786
|
-
const inputs =
|
|
786
|
+
const inputs = isArray(input) ? input : [input];
|
|
787
787
|
for (const route of inputs) this.#register(route);
|
|
788
788
|
}
|
|
789
789
|
group(prefix) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/core/constants.ts","../../../src/core/helpers.ts","../../../src/core/parsers.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 centralized-file rule's home for module-scope data used by\n// the matching engine and the fetch dispatcher. Every declaration here is frozen\n// and `export`ed per the centralized-file rule.\n// ============================================================================\n\n/**\n * Lists the HTTP methods a {@link import('./types.js').DispatcherInterface} registers\n * routes under, in canonical order — a frozen literal tuple, and the single source the\n * {@link import('./types.js').Method} type, {@link METHODS}, and `parseMethod` are all\n * derived from.\n *\n * @remarks\n * A frozen tuple of the verbs: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`,\n * `HEAD`, `OPTIONS`. Adding a verb here widens the `Method` type, the\n * {@link METHODS} membership set, and the `parseMethod` narrowing together, so\n * the method set cannot drift between them. Prefer {@link METHODS} for a\n * membership test; use this tuple where order or literal typing matters.\n *\n * @example\n * ```ts\n * METHOD_LIST[0] // 'GET'\n * METHOD_LIST.includes('GET') // true\n * ```\n */\nexport const METHOD_LIST = Object.freeze([\n\t'GET',\n\t'POST',\n\t'PUT',\n\t'PATCH',\n\t'DELETE',\n\t'HEAD',\n\t'OPTIONS',\n] as const)\n\n/**\n * Holds every HTTP method a {@link import('./types.js').DispatcherInterface} registers\n * routes under as a `ReadonlySet` — backs the registration guard (`add` rejects any\n * `method` outside this set) and the auto-`OPTIONS` `Allow` derivation.\n *\n * @remarks\n * A `ReadonlySet` built from {@link METHOD_LIST}, so it carries exactly the\n * {@link import('./types.js').Method} literals: `GET`, `POST`, `PUT`,\n * `PATCH`, `DELETE`, `HEAD`, `OPTIONS`. `HEAD` is included even though it is\n * never required at registration (a `GET` route auto-answers `HEAD`) — it is\n * still a valid method to register explicitly. The element type stays `string`\n * so a raw, unnarrowed `request.method` can be tested directly.\n *\n * @example\n * ```ts\n * METHODS.has('GET') // true\n * METHODS.has('TRACE') // false\n * ```\n */\nexport const METHODS: ReadonlySet<string> = Object.freeze(new Set<string>(METHOD_LIST))\n\n/**\n * Names the 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` (the path compiler in `helpers.ts`) when ranking candidate\n * matches left-to-right at the earliest differing segment.\n *\n * @example\n * ```ts\n * TIER_LITERAL > TIER_PARAM // true\n * ```\n */\nexport const TIER_LITERAL = 2\n\n/**\n * Names the 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` (the path compiler in `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 * Names the 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` (the path compiler in `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 { ContractError, preview } from '@orkestrel/contract'\nimport { TIER_LITERAL, TIER_PARAM, TIER_WILDCARD } from './constants.js'\n\n// The PURE path-matching primitives (self-describing helper naming — module scope,\n// no entity context). Every one is exported (the centralized-file rule): 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 * Escapes 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 * Canonicalizes a route path for registry identity — strips 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 * Computes the canonical `METHOD /path` registry key for a method-dimensioned\n * 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 * Compiles 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 a `ContractError` at the 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 (for example `/users/:id`, `/files/*rest`)\n * @param sensitive - If `true`, matching is case-sensitive; if `false`, case is\n * folded during matching. Default: `true`\n * @returns The {@link CompiledPath} — its `regex` + ordered `params`\n * @throws {@link import('@orkestrel/contract').ContractError} Thrown when a\n * `*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, guarded at the boundary.\n\t\tif (!isFinal && /^\\*[A-Za-z_]\\w*/.test(segment))\n\t\t\tthrow new ContractError('a wildcard segment must be the final segment of a path pattern', {\n\t\t\t\tcode: 'placement',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['path'],\n\t\t\t\t\tlimit: `a wildcard only in the final segment, not \"${segment}\"`,\n\t\t\t\t\treceived: preview(path),\n\t\t\t\t},\n\t\t\t})\n\t\t// Classification and compilation share ONE segment parser — 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 * Decodes one captured param value from a URL, 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). 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 * Extracts 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 (for example `/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 * Classifies 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, and everything else (including a\n * literal segment that merely contains a `:` mid-string, for example `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. Pure and total.\n *\n * @param segment - One `/`-split path segment\n * @param isFinal - If `true`, `segment` is the path's last segment and may\n * classify as a wildcard; if `false`, a wildcard-shaped segment classifies as\n * a literal\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 * Computes 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 through {@link classifySegment} — the same segment parser\n * {@link compilePath} uses, so a literal segment that merely contains a `:`\n * (for example `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 (for example `/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 * Compares 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 * Joins 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, 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 (for example `/api`)\n * @param path - The route path being joined under the prefix (for example `/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 * Provides an identity pass-through for a {@link RouteInput} that pins its `Path`\n * generic 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 `defineRoute(...)`\n * supplies that pin: its `const Path extends string` type parameter infers the\n * NARROW 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 `defineRoute(...)` calls\n * still widens each element's `Path` to `string` after collection into one array —\n * 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 * through {@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 = defineRoute({\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 defineRoute<const Path extends string, TState = undefined>(\n\tinput: RouteInput<Path, TState>,\n): RouteInput<Path, TState> {\n\treturn input\n}\n","// ============================================================================\n// Core coercers — the centralized-file rule's home for `parse*` narrowing leaves that\n// turn a raw external string into a typed core value or `undefined`. Every\n// declaration here is total and `export`ed per the centralized-file rule.\n// ============================================================================\n\nimport type { Method } from './types.js'\nimport { METHOD_LIST } from './constants.js'\n\n/**\n * Narrows a raw `request.method` string into a typed {@link Method} — total,\n * never throws.\n *\n * @remarks\n * Consults {@link import('./constants.js').METHOD_LIST} (the one home for the\n * registrable HTTP methods), so a verb added there narrows here without\n * a second list to update; any other value (an unknown verb, non-uppercase\n * casing) resolves to `undefined` rather than throwing (total guard behavior).\n * Pure leaf shared by the `Dispatcher`'s `handle` (honest about an unknown verb)\n * and 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 a\n * registrable method\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\treturn METHOD_LIST.find((method) => method === value)\n}\n","import type { GroupInterface, RouteEntry, RouterInterface } from './types.js'\nimport { joinPaths } from './helpers.js'\n\n/**\n * Represents a prefix-scoped registration handle over a {@link import('./Router.js').Router} —\n * pure string composition, 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 through {@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 { ContractError, isString, preview } from '@orkestrel/contract'\nimport { compareSpecificity, compilePath, matchPath } from './helpers.js'\nimport { Group } from './Group.js'\n\n/**\n * Represents the path-matching + registry engine — registers `{ path, meta, name? }`\n * entries (compiling each path once) and resolves a concrete pathname to the most\n * specific matching entry. The shared machine both the `Navigator` (browser) and\n * 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.** `add` validates each entry's\n * `path` — `isString` plus a leading `/` — and throws a `ContractError` 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 through `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 through {@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 (isString + leading '/'), then either replace an\n\t// existing entry IN PLACE (dedup through `#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))\n\t\t\tthrow new ContractError('a route path must be a string', {\n\t\t\t\tcode: 'literal',\n\t\t\t\tcontext: { path: ['entry', 'path'], limit: 'string', received: preview(entry.path) },\n\t\t\t})\n\t\tif (!entry.path.startsWith('/'))\n\t\t\tthrow new ContractError('a route path must start with \"/\"', {\n\t\t\t\tcode: 'pattern',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['entry', 'path'],\n\t\t\t\t\tlimit: 'a \"/\"-prefixed path pattern',\n\t\t\t\t\treceived: preview(entry.path),\n\t\t\t\t},\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 * Represents a prefix-scoped registration handle over a\n * {@link import('./Dispatcher.js').Dispatcher} — the method-dimensioned counterpart of\n * `Group`.\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` through {@link joinPaths} against\n * `this.prefix` and forwards to the OWNING dispatcher's `add` (its own\n * registration boundary guard still applies). Pure string composition — 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 { ContractError, isFunction, isString, preview } from '@orkestrel/contract'\nimport { METHODS } from './constants.js'\nimport { computeDispatchKey } from './helpers.js'\nimport { parseMethod } from './parsers.js'\nimport { Router } from './Router.js'\nimport { DispatchGroup } from './DispatchGroup.js'\n\n/**\n * Represents the fetch-standard, method-dimensioned dispatch entity — layers HTTP\n * method dispatch and web-standard `Request`/`Response` handling over one internal\n * `Router<RouteRecord<TState>>`. The core machine the server face and any\n * fetch-native runtime consume 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.\n * - **Registration boundary guard.** `add` validates each input's\n * `handler` (`isFunction`) and `method` (must be in {@link METHODS}) —\n * throws a `ContractError` 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.\n * - **Emitter.** 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 router(): RouterInterface<RouteRecord<TState>> {\n\t\treturn this.#router\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') {\n\t\t\t\tconst hit = this.#router.match(pathname)\n\t\t\t\tif (hit !== undefined) return this.#respondAutoOptions(hit.path, result.allow)\n\t\t\t}\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 (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 ContractError('a route handler must be a function', {\n\t\t\t\tcode: 'literal',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['input', 'handler'],\n\t\t\t\t\tlimit: 'function',\n\t\t\t\t\treceived: preview(input.handler),\n\t\t\t\t},\n\t\t\t})\n\t\tif (!isString(input.method) || !METHODS.has(input.method))\n\t\t\tthrow new ContractError('a route method must be a registrable HTTP method', {\n\t\t\t\tcode: 'literal',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['input', 'method'],\n\t\t\t\t\tlimit: [...METHODS].join(', '),\n\t\t\t\t\treceived: preview(input.method),\n\t\t\t\t},\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.\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), emitting\n\t// `match` under the most-specific REGISTERED pattern the pathname resolved to, so a consumer\n\t// aggregating by pattern sees bounded cardinality rather than one label per request path.\n\t#respondAutoOptions(pattern: string, allow: readonly Method[]): Response {\n\t\tthis.#emitter.emit('match', 'OPTIONS', pattern)\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 * Creates 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 Register and match\n * ```ts\n * import { createDispatcher, createRouter } from '@orkestrel/router'\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 * const dispatcher = createDispatcher<{ readonly userId: string }>({\n * \troutes: [\n * \t\t{\n * \t\t\tmethod: 'GET',\n * \t\t\tpath: '/users/:id',\n * \t\t\thandler: (_request, context) => Response.json(context.params),\n * \t\t},\n * \t],\n * })\n * const response = await dispatcher.handle(new Request('http://x/users/7'), { userId: 'me' })\n * ```\n */\nexport function createRouter<Meta>(options?: RouterOptions<Meta>): RouterInterface<Meta> {\n\treturn new Router<Meta>(options)\n}\n\n/**\n * Creates a {@link DispatcherInterface} — the fetch-standard,\n * method-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\n * Emitter pattern's `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":[],"sources":["../../../src/core/constants.ts","../../../src/core/helpers.ts","../../../src/core/parsers.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 centralized-file rule's home for module-scope data used by\n// the matching engine and the fetch dispatcher. Every declaration here is frozen\n// and `export`ed per the centralized-file rule.\n// ============================================================================\n\n/**\n * Lists the HTTP methods a {@link import('./types.js').DispatcherInterface} registers\n * routes under, in canonical order — a frozen literal tuple, and the single source the\n * {@link import('./types.js').Method} type, {@link METHODS}, and `parseMethod` are all\n * derived from.\n *\n * @remarks\n * A frozen tuple of the verbs: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`,\n * `HEAD`, `OPTIONS`. Adding a verb here widens the `Method` type, the\n * {@link METHODS} membership set, and the `parseMethod` narrowing together, so\n * the method set cannot drift between them. Prefer {@link METHODS} for a\n * membership test; use this tuple where order or literal typing matters.\n *\n * @example\n * ```ts\n * METHOD_LIST[0] // 'GET'\n * METHOD_LIST.includes('GET') // true\n * ```\n */\nexport const METHOD_LIST = Object.freeze([\n\t'GET',\n\t'POST',\n\t'PUT',\n\t'PATCH',\n\t'DELETE',\n\t'HEAD',\n\t'OPTIONS',\n] as const)\n\n/**\n * Holds every HTTP method a {@link import('./types.js').DispatcherInterface} registers\n * routes under as a `ReadonlySet` — backs the registration guard (`add` rejects any\n * `method` outside this set) and the auto-`OPTIONS` `Allow` derivation.\n *\n * @remarks\n * A `ReadonlySet` built from {@link METHOD_LIST}, so it carries exactly the\n * {@link import('./types.js').Method} literals: `GET`, `POST`, `PUT`,\n * `PATCH`, `DELETE`, `HEAD`, `OPTIONS`. `HEAD` is included even though it is\n * never required at registration (a `GET` route auto-answers `HEAD`) — it is\n * still a valid method to register explicitly. The element type stays `string`\n * so a raw, unnarrowed `request.method` can be tested directly.\n *\n * @example\n * ```ts\n * METHODS.has('GET') // true\n * METHODS.has('TRACE') // false\n * ```\n */\nexport const METHODS: ReadonlySet<string> = Object.freeze(new Set<string>(METHOD_LIST))\n\n/**\n * Names the 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` (the path compiler in `helpers.ts`) when ranking candidate\n * matches left-to-right at the earliest differing segment.\n *\n * @example\n * ```ts\n * TIER_LITERAL > TIER_PARAM // true\n * ```\n */\nexport const TIER_LITERAL = 2\n\n/**\n * Names the 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` (the path compiler in `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 * Names the 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` (the path compiler in `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 { ContractError, preview } from '@orkestrel/contract'\nimport { TIER_LITERAL, TIER_PARAM, TIER_WILDCARD } from './constants.js'\n\n// The PURE path-matching primitives (self-describing helper naming — module scope,\n// no entity context). Every one is exported (the centralized-file rule): 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 * Escapes 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 * Canonicalizes a route path for registry identity — strips 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 * Computes the canonical `METHOD /path` registry key for a method-dimensioned\n * 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 * Compiles 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 a `ContractError` at the 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 (for example `/users/:id`, `/files/*rest`)\n * @param sensitive - If `true`, matching is case-sensitive; if `false`, case is\n * folded during matching. Default: `true`\n * @returns The {@link CompiledPath} — its `regex` + ordered `params`\n * @throws {@link import('@orkestrel/contract').ContractError} Thrown when a\n * `*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, guarded at the boundary.\n\t\tif (!isFinal && /^\\*[A-Za-z_]\\w*/.test(segment))\n\t\t\tthrow new ContractError('a wildcard segment must be the final segment of a path pattern', {\n\t\t\t\tcode: 'placement',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['path'],\n\t\t\t\t\tlimit: `a wildcard only in the final segment, not \"${segment}\"`,\n\t\t\t\t\treceived: preview(path),\n\t\t\t\t},\n\t\t\t})\n\t\t// Classification and compilation share ONE segment parser — 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 * Decodes one captured param value from a URL, 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). 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 * Extracts 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 (for example `/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 * Classifies 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, and everything else (including a\n * literal segment that merely contains a `:` mid-string, for example `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. Pure and total.\n *\n * @param segment - One `/`-split path segment\n * @param isFinal - If `true`, `segment` is the path's last segment and may\n * classify as a wildcard; if `false`, a wildcard-shaped segment classifies as\n * a literal\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 * Computes 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 through {@link classifySegment} — the same segment parser\n * {@link compilePath} uses, so a literal segment that merely contains a `:`\n * (for example `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 (for example `/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 * Compares 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 * Joins 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, 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 (for example `/api`)\n * @param path - The route path being joined under the prefix (for example `/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 * Provides an identity pass-through for a {@link RouteInput} that pins its `Path`\n * generic 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 `defineRoute(...)`\n * supplies that pin: its `const Path extends string` type parameter infers the\n * NARROW 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 `defineRoute(...)` calls\n * still widens each element's `Path` to `string` after collection into one array —\n * 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 * through {@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 = defineRoute({\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 defineRoute<const Path extends string, TState = undefined>(\n\tinput: RouteInput<Path, TState>,\n): RouteInput<Path, TState> {\n\treturn input\n}\n","// ============================================================================\n// Core coercers — the centralized-file rule's home for `parse*` narrowing leaves that\n// turn a raw external string into a typed core value or `undefined`. Every\n// declaration here is total and `export`ed per the centralized-file rule.\n// ============================================================================\n\nimport type { Method } from './types.js'\nimport { METHOD_LIST } from './constants.js'\n\n/**\n * Narrows a raw `request.method` string into a typed {@link Method} — total,\n * never throws.\n *\n * @remarks\n * Consults {@link import('./constants.js').METHOD_LIST} (the one home for the\n * registrable HTTP methods), so a verb added there narrows here without\n * a second list to update; any other value (an unknown verb, non-uppercase\n * casing) resolves to `undefined` rather than throwing (total guard behavior).\n * Pure leaf shared by the `Dispatcher`'s `handle` (honest about an unknown verb)\n * and 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 a\n * registrable method\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\treturn METHOD_LIST.find((method) => method === value)\n}\n","import type { GroupInterface, RouteEntry, RouterInterface } from './types.js'\nimport { isArray } from '@orkestrel/contract'\nimport { joinPaths } from './helpers.js'\n\n/**\n * Represents a prefix-scoped registration handle over a {@link import('./Router.js').Router} —\n * pure string composition, 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 through {@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 = 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 { ContractError, isArray, isString, preview } from '@orkestrel/contract'\nimport { compareSpecificity, compilePath, matchPath } from './helpers.js'\nimport { Group } from './Group.js'\n\n/**\n * Represents the path-matching + registry engine — registers `{ path, meta, name? }`\n * entries (compiling each path once) and resolves a concrete pathname to the most\n * specific matching entry. The shared machine both the `Navigator` (browser) and\n * 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.** `add` validates each entry's\n * `path` — `isString` plus a leading `/` — and throws a `ContractError` 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 through `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 through {@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 = 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 (isString + leading '/'), then either replace an\n\t// existing entry IN PLACE (dedup through `#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))\n\t\t\tthrow new ContractError('a route path must be a string', {\n\t\t\t\tcode: 'literal',\n\t\t\t\tcontext: { path: ['entry', 'path'], limit: 'string', received: preview(entry.path) },\n\t\t\t})\n\t\tif (!entry.path.startsWith('/'))\n\t\t\tthrow new ContractError('a route path must start with \"/\"', {\n\t\t\t\tcode: 'pattern',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['entry', 'path'],\n\t\t\t\t\tlimit: 'a \"/\"-prefixed path pattern',\n\t\t\t\t\treceived: preview(entry.path),\n\t\t\t\t},\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 { isArray } from '@orkestrel/contract'\nimport { joinPaths } from './helpers.js'\n\n/**\n * Represents a prefix-scoped registration handle over a\n * {@link import('./Dispatcher.js').Dispatcher} — the method-dimensioned counterpart of\n * `Group`.\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` through {@link joinPaths} against\n * `this.prefix` and forwards to the OWNING dispatcher's `add` (its own\n * registration boundary guard still applies). Pure string composition — 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 = 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 { ContractError, isArray, isFunction, isString, preview } from '@orkestrel/contract'\nimport { METHODS } from './constants.js'\nimport { computeDispatchKey } from './helpers.js'\nimport { parseMethod } from './parsers.js'\nimport { Router } from './Router.js'\nimport { DispatchGroup } from './DispatchGroup.js'\n\n/**\n * Represents the fetch-standard, method-dimensioned dispatch entity — layers HTTP\n * method dispatch and web-standard `Request`/`Response` handling over one internal\n * `Router<RouteRecord<TState>>`. The core machine the server face and any\n * fetch-native runtime consume 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.\n * - **Registration boundary guard.** `add` validates each input's\n * `handler` (`isFunction`) and `method` (must be in {@link METHODS}) —\n * throws a `ContractError` 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.\n * - **Emitter.** 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 router(): RouterInterface<RouteRecord<TState>> {\n\t\treturn this.#router\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 = 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') {\n\t\t\t\tconst hit = this.#router.match(pathname)\n\t\t\t\tif (hit !== undefined) return this.#respondAutoOptions(hit.path, result.allow)\n\t\t\t}\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 (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 ContractError('a route handler must be a function', {\n\t\t\t\tcode: 'literal',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['input', 'handler'],\n\t\t\t\t\tlimit: 'function',\n\t\t\t\t\treceived: preview(input.handler),\n\t\t\t\t},\n\t\t\t})\n\t\tif (!isString(input.method) || !METHODS.has(input.method))\n\t\t\tthrow new ContractError('a route method must be a registrable HTTP method', {\n\t\t\t\tcode: 'literal',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['input', 'method'],\n\t\t\t\t\tlimit: [...METHODS].join(', '),\n\t\t\t\t\treceived: preview(input.method),\n\t\t\t\t},\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.\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), emitting\n\t// `match` under the most-specific REGISTERED pattern the pathname resolved to, so a consumer\n\t// aggregating by pattern sees bounded cardinality rather than one label per request path.\n\t#respondAutoOptions(pattern: string, allow: readonly Method[]): Response {\n\t\tthis.#emitter.emit('match', 'OPTIONS', pattern)\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 * Creates 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 Register and match\n * ```ts\n * import { createDispatcher, createRouter } from '@orkestrel/router'\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 * const dispatcher = createDispatcher<{ readonly userId: string }>({\n * \troutes: [\n * \t\t{\n * \t\t\tmethod: 'GET',\n * \t\t\tpath: '/users/:id',\n * \t\t\thandler: (_request, context) => Response.json(context.params),\n * \t\t},\n * \t],\n * })\n * const response = await dispatcher.handle(new Request('http://x/users/7'), { userId: 'me' })\n * ```\n */\nexport function createRouter<Meta>(options?: RouterOptions<Meta>): RouterInterface<Meta> {\n\treturn new Router<Meta>(options)\n}\n\n/**\n * Creates a {@link DispatcherInterface} — the fetch-standard,\n * method-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\n * Emitter pattern's `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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"}
|
|
@@ -77,10 +77,10 @@ function buildRequest(message, options) {
|
|
|
77
77
|
for (const [name, value] of Object.entries(message.headers)) {
|
|
78
78
|
if (value === void 0) continue;
|
|
79
79
|
if (name === "set-cookie") {
|
|
80
|
-
for (const cookie of
|
|
80
|
+
for (const cookie of (0, _orkestrel_contract.isArray)(value) ? value : [value]) headers.append(name, cookie);
|
|
81
81
|
continue;
|
|
82
82
|
}
|
|
83
|
-
headers.set(name,
|
|
83
|
+
headers.set(name, (0, _orkestrel_contract.isArray)(value) ? value.join(", ") : value);
|
|
84
84
|
}
|
|
85
85
|
const abort = (0, _orkestrel_abort.createAbort)();
|
|
86
86
|
message.once("close", () => {
|
|
@@ -210,7 +210,7 @@ async function handleListenerRequest(dispatcher, state, request, response) {
|
|
|
210
210
|
if (!response.headersSent && !response.destroyed) {
|
|
211
211
|
response.writeHead(500);
|
|
212
212
|
response.end();
|
|
213
|
-
} else if (!response.destroyed) response.destroy(error
|
|
213
|
+
} else if (!response.destroyed) response.destroy((0, _orkestrel_contract.isError)(error) ? error : new Error(String(error)));
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
216
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/server/validators.ts","../../../src/server/helpers.ts","../../../src/server/handlers.ts"],"sourcesContent":["// ============================================================================\n// Server guards — the centralized-file rule's home for the total `is*` narrows the\n// `node:http` conversion seam applies to raw connection values. Every\n// declaration here is total and `export`ed per the centralized-file rule.\n// ============================================================================\n\nimport { isRecord } from '@orkestrel/contract'\n\n/**\n * Determines whether a `node:http` connection socket is TLS-encrypted — the\n * total, never-throwing narrow `buildRequest` uses to pick the\n * derived scheme (`https` vs `http`).\n *\n * @param socket - The connection value to test (typically `message.socket`)\n * @returns True if `socket` carries a truthy `encrypted` property (a\n * `tls.TLSSocket`); false otherwise, including for `undefined`\n *\n * @example\n * ```ts\n * import { isEncryptedSocket } from '@src/server'\n *\n * isEncryptedSocket({ encrypted: true }) // true\n * isEncryptedSocket({}) // false\n * ```\n */\nexport function isEncryptedSocket(socket: unknown): socket is { readonly encrypted: true } {\n\treturn isRecord(socket) && socket.encrypted === true\n}\n","// ============================================================================\n// Pure conversion between `node:http` and the fetch vocabulary the core\n// `Dispatcher` speaks — no lifecycle and no listener ownership. Every\n// function is exported per the centralized-file rule.\n// ============================================================================\n\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { RequestOptions } from './types.js'\nimport { once } from 'node:events'\nimport { createAbort } from '@orkestrel/abort'\nimport { isEncryptedSocket } from './validators.js'\n\n/**\n * Builds a fetch-standard `Request` from a `node:http` `IncomingMessage` — the\n * server-adapter half of the fetch/node conversion seam.\n *\n * @remarks\n * - `method` is carried over verbatim (defaulting to `GET` when absent).\n * - The URL is built against `options.origin` when given, otherwise a scheme\n * derived from the connection (`https` when {@link isEncryptedSocket}, else\n * `http`) plus the `Host` header (absent `Host` ⇒ `localhost`).\n * - Every request header is copied; multi-value headers are joined per fetch\n * semantics (`', '`-joined), except `set-cookie`, whose values are each\n * appended individually (fetch `Headers` preserves multiple `set-cookie`\n * entries distinctly).\n * - For a method that carries a body (anything but `GET`/`HEAD`), the message\n * is pumped chunk by chunk into a DOM-compatible `ReadableStream<Uint8Array>`\n * (reconciling the DOM + node type worlds under the root config), with\n * `duplex: 'half'` set as Node's fetch implementation requires for a\n * streamed request body.\n * - A fresh `@orkestrel/abort` handle backs `request.signal`. It aborts when\n * the request connection closes before the message finished\n * (`!message.complete`), or when the paired `options.response` closes before\n * its response finished (`!response.writableEnded`). A handler awaiting the\n * signal therefore observes either side of a client disconnect the\n * fetch-standard way, with zero router-specific API.\n *\n * @param message - The raw `node:http` request\n * @param options - Optional `origin` override and paired `response` for\n * response-side disconnect tracking ({@link RequestOptions})\n * @returns A fetch `Request` whose `signal` fires on an incomplete request, or\n * on a response-side client disconnect when `options.response` is provided\n *\n * @example\n * ```ts\n * import { buildRequest } from '@src/server'\n * import http from 'node:http'\n *\n * const server = http.createServer((incoming, response) => {\n * \tconst request = buildRequest(incoming, { response })\n * \tconsole.log(request.method, request.url)\n * })\n * ```\n */\nexport function buildRequest(message: IncomingMessage, options?: RequestOptions): Request {\n\tconst method = message.method ?? 'GET'\n\tconst host = message.headers.host ?? 'localhost'\n\tconst scheme = isEncryptedSocket(message.socket) ? 'https' : 'http'\n\tconst origin = options?.origin ?? `${scheme}://${host}`\n\tconst url = new URL(message.url ?? '/', origin)\n\n\tconst headers = new Headers()\n\tfor (const [name, value] of Object.entries(message.headers)) {\n\t\tif (value === undefined) continue\n\t\tif (name === 'set-cookie') {\n\t\t\tfor (const cookie of Array.isArray(value) ? value : [value]) headers.append(name, cookie)\n\t\t\tcontinue\n\t\t}\n\t\theaders.set(name, Array.isArray(value) ? value.join(', ') : value)\n\t}\n\n\tconst abort = createAbort()\n\tmessage.once('close', () => {\n\t\tif (!message.complete)\n\t\t\tabort.abort(new Error(`request to ${url.pathname} disconnected before completion`))\n\t})\n\tconst response = options?.response\n\tif (response !== undefined)\n\t\tresponse.once('close', () => {\n\t\t\tif (!response.writableEnded)\n\t\t\t\tabort.abort(new Error(`request to ${url.pathname} disconnected before response completed`))\n\t\t})\n\n\tconst carriesBody = method !== 'GET' && method !== 'HEAD'\n\tconst init: RequestInit = { method, headers, signal: abort.signal }\n\tif (!carriesBody) return new Request(url, init)\n\n\tconst body = new ReadableStream<Uint8Array>({\n\t\tasync start(controller) {\n\t\t\ttry {\n\t\t\t\tfor await (const chunk of message) controller.enqueue(chunk)\n\t\t\t\tcontroller.close()\n\t\t\t} catch (error) {\n\t\t\t\tcontroller.error(error)\n\t\t\t}\n\t\t},\n\t})\n\tconst streamed: RequestInit & { readonly duplex: 'half' } = { ...init, body, duplex: 'half' }\n\treturn new Request(url, streamed)\n}\n\n/**\n * Writes a fetch-standard `Response` back to a `node:http` `ServerResponse` —\n * the reverse half of the fetch/node conversion seam.\n *\n * @remarks\n * Writes `status`/`statusText`, then every response header (`set-cookie`\n * written through {@link Headers.getSetCookie} so multiple cookies stay distinct\n * instead of collapsing into one comma-joined header), then streams the web\n * body to `target` chunk by chunk (`for await` over `response.body`), ending\n * `target` when the stream completes. When a write reports backpressure, the\n * body pump waits for `drain` before pulling again, unless the target closes,\n * errors, or is destroyed first. A `null` body ends `target` immediately with\n * no further writes. Total error posture: if `target` is destroyed mid-stream\n * (the client disconnected), the write loop stops and `target` is left as-is\n * rather than throwing an unhandled rejection — a destroyed target is not\n * this function's error to surface.\n *\n * @param response - The fetch `Response` to write\n * @param target - The `node:http` response to write it to\n * @returns A promise that resolves after `target` has been ended (or the\n * stream stopped because `target` was destroyed)\n *\n * @example\n * ```ts\n * import { sendResponse } from '@src/server'\n * import http from 'node:http'\n *\n * const server = http.createServer(async (_incoming, target) => {\n * \tawait sendResponse(new Response('ok'), target)\n * })\n * ```\n */\nexport async function sendResponse(response: Response, target: ServerResponse): Promise<void> {\n\ttarget.statusCode = response.status\n\ttarget.statusMessage = response.statusText\n\tfor (const [name, value] of response.headers) {\n\t\tif (name === 'set-cookie') continue\n\t\ttarget.setHeader(name, value)\n\t}\n\tconst cookies = response.headers.getSetCookie()\n\tif (cookies.length > 0) target.setHeader('set-cookie', cookies)\n\n\tif (response.body === null) {\n\t\tif (!target.destroyed) target.end()\n\t\treturn\n\t}\n\ttry {\n\t\tfor await (const chunk of response.body) {\n\t\t\tif (target.destroyed) return\n\t\t\tif (!target.write(chunk)) {\n\t\t\t\tif (target.destroyed) return\n\t\t\t\tconst abort = new AbortController()\n\t\t\t\ttry {\n\t\t\t\t\tawait Promise.race([\n\t\t\t\t\t\tonce(target, 'drain', { signal: abort.signal }),\n\t\t\t\t\t\tonce(target, 'close', { signal: abort.signal }),\n\t\t\t\t\t])\n\t\t\t\t} finally {\n\t\t\t\t\tabort.abort()\n\t\t\t\t}\n\t\t\t\tif (target.destroyed) return\n\t\t\t}\n\t\t}\n\t\tif (!target.destroyed) target.end()\n\t} catch {\n\t\tif (!target.destroyed) target.end()\n\t}\n}\n","// ============================================================================\n// Server request handlers — the centralized-file rule's home for the functions that\n// run one `node:http` exchange through a core `Dispatcher`, plus the listener\n// the whole server face hands to `http.createServer`. Every function\n// is exported per the centralized-file rule.\n// ============================================================================\n\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { DispatcherInterface } from '@src/core'\nimport type { ListenerFunction, StateFunction } from './types.js'\nimport { buildRequest, sendResponse } from './helpers.js'\n\n/**\n * Handles one `node:http` request through a core dispatcher and writes its\n * fetch-standard response.\n *\n * @remarks\n * This is the named asynchronous orchestration behind {@link createListener}.\n * A rejected dispatch is treated only as a transport-level last resort: write\n * a bare `500` before headers, or destroy a response whose headers have\n * already started.\n *\n * @typeParam TState - The consumer's opaque per-request state type\n * @param dispatcher - The core dispatcher to run\n * @param state - Derives the consumer state from the incoming message\n * @param request - The raw `node:http` request\n * @param response - The raw `node:http` response\n * @returns A promise that settles after the response is written or closed\n *\n * @example\n * ```ts\n * const server = http.createServer((request, response) => {\n * \tvoid handleListenerRequest(dispatcher, () => undefined, request, response)\n * })\n * ```\n */\nexport async function handleListenerRequest<TState>(\n\tdispatcher: DispatcherInterface<TState>,\n\tstate: StateFunction<TState>,\n\trequest: IncomingMessage,\n\tresponse: ServerResponse,\n): Promise<void> {\n\ttry {\n\t\tconst converted = buildRequest(request, { response })\n\t\tconst result = await dispatcher.handle(converted, state(request))\n\t\tawait sendResponse(result, response)\n\t} catch (error) {\n\t\tif (!response.headersSent && !response.destroyed) {\n\t\t\tresponse.writeHead(500)\n\t\t\tresponse.end()\n\t\t} else if (!response.destroyed) {\n\t\t\tresponse.destroy(error instanceof Error ? error : new Error(String(error)))\n\t\t}\n\t}\n}\n\n/**\n * Creates a `node:http` request listener over a core {@link DispatcherInterface} —\n * the whole server face's entry point: converts the incoming message to\n * a fetch `Request`, hands it to the dispatcher with the consumer's per-request\n * `state`, and writes the resulting `Response` back.\n *\n * @remarks\n * A rejected `dispatcher.handle` (a route handler throw — the dispatcher\n * never invents an error boundary) is this listener's transport-level\n * LAST RESORT, distinct from an application error boundary: when nothing has\n * been sent yet, it destroys the connection with a bare `500` head (never\n * leaking a hanging socket); after headers are already sent, it destroys the\n * connection outright. The router still owns no error POLICY — a consumer\n * that wants mapped error responses installs its own boundary around\n * `dispatcher.handle` (the future `@orkestrel/server` seam).\n *\n * @typeParam TState - The consumer's opaque per-request state type\n * @param dispatcher - The core dispatcher to run each converted request through\n * @param state - Derives the consumer's per-request `state` from the raw message\n * @returns A `(request, response) => void` listener, passable directly to\n * `http.createServer`\n *\n * @example\n * ```ts\n * import { createListener } from '@orkestrel/router/server'\n * import { createDispatcher } from '@orkestrel/router'\n * import http from 'node:http'\n *\n * const dispatcher = createDispatcher<{ readonly requestId: string }>()\n * dispatcher.add({\n * \tmethod: 'GET',\n * \tpath: '/users/:id',\n * \thandler: (_request, context) =>\n * \t\tResponse.json({ id: context.params.id, requestId: context.state.requestId }),\n * })\n *\n * const server = http.createServer(\n * \tcreateListener(dispatcher, () => ({ requestId: crypto.randomUUID() })),\n * )\n * server.listen(0)\n * ```\n */\nexport function createListener<TState>(\n\tdispatcher: DispatcherInterface<TState>,\n\tstate: StateFunction<TState>,\n): ListenerFunction {\n\treturn (request, response) => {\n\t\tvoid handleListenerRequest(dispatcher, state, request, response)\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/server/validators.ts","../../../src/server/helpers.ts","../../../src/server/handlers.ts"],"sourcesContent":["// ============================================================================\n// Server guards — the centralized-file rule's home for the total `is*` narrows the\n// `node:http` conversion seam applies to raw connection values. Every\n// declaration here is total and `export`ed per the centralized-file rule.\n// ============================================================================\n\nimport { isRecord } from '@orkestrel/contract'\n\n/**\n * Determines whether a `node:http` connection socket is TLS-encrypted — the\n * total, never-throwing narrow `buildRequest` uses to pick the\n * derived scheme (`https` vs `http`).\n *\n * @param socket - The connection value to test (typically `message.socket`)\n * @returns True if `socket` carries a truthy `encrypted` property (a\n * `tls.TLSSocket`); false otherwise, including for `undefined`\n *\n * @example\n * ```ts\n * import { isEncryptedSocket } from '@src/server'\n *\n * isEncryptedSocket({ encrypted: true }) // true\n * isEncryptedSocket({}) // false\n * ```\n */\nexport function isEncryptedSocket(socket: unknown): socket is { readonly encrypted: true } {\n\treturn isRecord(socket) && socket.encrypted === true\n}\n","// ============================================================================\n// Pure conversion between `node:http` and the fetch vocabulary the core\n// `Dispatcher` speaks — no lifecycle and no listener ownership. Every\n// function is exported per the centralized-file rule.\n// ============================================================================\n\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { RequestOptions } from './types.js'\nimport { once } from 'node:events'\nimport { createAbort } from '@orkestrel/abort'\nimport { isArray } from '@orkestrel/contract'\nimport { isEncryptedSocket } from './validators.js'\n\n/**\n * Builds a fetch-standard `Request` from a `node:http` `IncomingMessage` — the\n * server-adapter half of the fetch/node conversion seam.\n *\n * @remarks\n * - `method` is carried over verbatim (defaulting to `GET` when absent).\n * - The URL is built against `options.origin` when given, otherwise a scheme\n * derived from the connection (`https` when {@link isEncryptedSocket}, else\n * `http`) plus the `Host` header (absent `Host` ⇒ `localhost`).\n * - Every request header is copied; multi-value headers are joined per fetch\n * semantics (`', '`-joined), except `set-cookie`, whose values are each\n * appended individually (fetch `Headers` preserves multiple `set-cookie`\n * entries distinctly).\n * - For a method that carries a body (anything but `GET`/`HEAD`), the message\n * is pumped chunk by chunk into a DOM-compatible `ReadableStream<Uint8Array>`\n * (reconciling the DOM + node type worlds under the root config), with\n * `duplex: 'half'` set as Node's fetch implementation requires for a\n * streamed request body.\n * - A fresh `@orkestrel/abort` handle backs `request.signal`. It aborts when\n * the request connection closes before the message finished\n * (`!message.complete`), or when the paired `options.response` closes before\n * its response finished (`!response.writableEnded`). A handler awaiting the\n * signal therefore observes either side of a client disconnect the\n * fetch-standard way, with zero router-specific API.\n *\n * @param message - The raw `node:http` request\n * @param options - Optional `origin` override and paired `response` for\n * response-side disconnect tracking ({@link RequestOptions})\n * @returns A fetch `Request` whose `signal` fires on an incomplete request, or\n * on a response-side client disconnect when `options.response` is provided\n *\n * @example\n * ```ts\n * import { buildRequest } from '@src/server'\n * import http from 'node:http'\n *\n * const server = http.createServer((incoming, response) => {\n * \tconst request = buildRequest(incoming, { response })\n * \tconsole.log(request.method, request.url)\n * })\n * ```\n */\nexport function buildRequest(message: IncomingMessage, options?: RequestOptions): Request {\n\tconst method = message.method ?? 'GET'\n\tconst host = message.headers.host ?? 'localhost'\n\tconst scheme = isEncryptedSocket(message.socket) ? 'https' : 'http'\n\tconst origin = options?.origin ?? `${scheme}://${host}`\n\tconst url = new URL(message.url ?? '/', origin)\n\n\tconst headers = new Headers()\n\tfor (const [name, value] of Object.entries(message.headers)) {\n\t\tif (value === undefined) continue\n\t\tif (name === 'set-cookie') {\n\t\t\tfor (const cookie of isArray(value) ? value : [value]) headers.append(name, cookie)\n\t\t\tcontinue\n\t\t}\n\t\theaders.set(name, isArray(value) ? value.join(', ') : value)\n\t}\n\n\tconst abort = createAbort()\n\tmessage.once('close', () => {\n\t\tif (!message.complete)\n\t\t\tabort.abort(new Error(`request to ${url.pathname} disconnected before completion`))\n\t})\n\tconst response = options?.response\n\tif (response !== undefined)\n\t\tresponse.once('close', () => {\n\t\t\tif (!response.writableEnded)\n\t\t\t\tabort.abort(new Error(`request to ${url.pathname} disconnected before response completed`))\n\t\t})\n\n\tconst carriesBody = method !== 'GET' && method !== 'HEAD'\n\tconst init: RequestInit = { method, headers, signal: abort.signal }\n\tif (!carriesBody) return new Request(url, init)\n\n\tconst body = new ReadableStream<Uint8Array>({\n\t\tasync start(controller) {\n\t\t\ttry {\n\t\t\t\tfor await (const chunk of message) controller.enqueue(chunk)\n\t\t\t\tcontroller.close()\n\t\t\t} catch (error) {\n\t\t\t\tcontroller.error(error)\n\t\t\t}\n\t\t},\n\t})\n\tconst streamed: RequestInit & { readonly duplex: 'half' } = { ...init, body, duplex: 'half' }\n\treturn new Request(url, streamed)\n}\n\n/**\n * Writes a fetch-standard `Response` back to a `node:http` `ServerResponse` —\n * the reverse half of the fetch/node conversion seam.\n *\n * @remarks\n * Writes `status`/`statusText`, then every response header (`set-cookie`\n * written through {@link Headers.getSetCookie} so multiple cookies stay distinct\n * instead of collapsing into one comma-joined header), then streams the web\n * body to `target` chunk by chunk (`for await` over `response.body`), ending\n * `target` when the stream completes. When a write reports backpressure, the\n * body pump waits for `drain` before pulling again, unless the target closes,\n * errors, or is destroyed first. A `null` body ends `target` immediately with\n * no further writes. Total error posture: if `target` is destroyed mid-stream\n * (the client disconnected), the write loop stops and `target` is left as-is\n * rather than throwing an unhandled rejection — a destroyed target is not\n * this function's error to surface.\n *\n * @param response - The fetch `Response` to write\n * @param target - The `node:http` response to write it to\n * @returns A promise that resolves after `target` has been ended (or the\n * stream stopped because `target` was destroyed)\n *\n * @example\n * ```ts\n * import { sendResponse } from '@src/server'\n * import http from 'node:http'\n *\n * const server = http.createServer(async (_incoming, target) => {\n * \tawait sendResponse(new Response('ok'), target)\n * })\n * ```\n */\nexport async function sendResponse(response: Response, target: ServerResponse): Promise<void> {\n\ttarget.statusCode = response.status\n\ttarget.statusMessage = response.statusText\n\tfor (const [name, value] of response.headers) {\n\t\tif (name === 'set-cookie') continue\n\t\ttarget.setHeader(name, value)\n\t}\n\tconst cookies = response.headers.getSetCookie()\n\tif (cookies.length > 0) target.setHeader('set-cookie', cookies)\n\n\tif (response.body === null) {\n\t\tif (!target.destroyed) target.end()\n\t\treturn\n\t}\n\ttry {\n\t\tfor await (const chunk of response.body) {\n\t\t\tif (target.destroyed) return\n\t\t\tif (!target.write(chunk)) {\n\t\t\t\tif (target.destroyed) return\n\t\t\t\tconst abort = new AbortController()\n\t\t\t\ttry {\n\t\t\t\t\tawait Promise.race([\n\t\t\t\t\t\tonce(target, 'drain', { signal: abort.signal }),\n\t\t\t\t\t\tonce(target, 'close', { signal: abort.signal }),\n\t\t\t\t\t])\n\t\t\t\t} finally {\n\t\t\t\t\tabort.abort()\n\t\t\t\t}\n\t\t\t\tif (target.destroyed) return\n\t\t\t}\n\t\t}\n\t\tif (!target.destroyed) target.end()\n\t} catch {\n\t\tif (!target.destroyed) target.end()\n\t}\n}\n","// ============================================================================\n// Server request handlers — the centralized-file rule's home for the functions that\n// run one `node:http` exchange through a core `Dispatcher`, plus the listener\n// the whole server face hands to `http.createServer`. Every function\n// is exported per the centralized-file rule.\n// ============================================================================\n\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { DispatcherInterface } from '@src/core'\nimport type { ListenerFunction, StateFunction } from './types.js'\nimport { isError } from '@orkestrel/contract'\nimport { buildRequest, sendResponse } from './helpers.js'\n\n/**\n * Handles one `node:http` request through a core dispatcher and writes its\n * fetch-standard response.\n *\n * @remarks\n * This is the named asynchronous orchestration behind {@link createListener}.\n * A rejected dispatch is treated only as a transport-level last resort: write\n * a bare `500` before headers, or destroy a response whose headers have\n * already started.\n *\n * @typeParam TState - The consumer's opaque per-request state type\n * @param dispatcher - The core dispatcher to run\n * @param state - Derives the consumer state from the incoming message\n * @param request - The raw `node:http` request\n * @param response - The raw `node:http` response\n * @returns A promise that settles after the response is written or closed\n *\n * @example\n * ```ts\n * const server = http.createServer((request, response) => {\n * \tvoid handleListenerRequest(dispatcher, () => undefined, request, response)\n * })\n * ```\n */\nexport async function handleListenerRequest<TState>(\n\tdispatcher: DispatcherInterface<TState>,\n\tstate: StateFunction<TState>,\n\trequest: IncomingMessage,\n\tresponse: ServerResponse,\n): Promise<void> {\n\ttry {\n\t\tconst converted = buildRequest(request, { response })\n\t\tconst result = await dispatcher.handle(converted, state(request))\n\t\tawait sendResponse(result, response)\n\t} catch (error) {\n\t\tif (!response.headersSent && !response.destroyed) {\n\t\t\tresponse.writeHead(500)\n\t\t\tresponse.end()\n\t\t} else if (!response.destroyed) {\n\t\t\tresponse.destroy(isError(error) ? error : new Error(String(error)))\n\t\t}\n\t}\n}\n\n/**\n * Creates a `node:http` request listener over a core {@link DispatcherInterface} —\n * the whole server face's entry point: converts the incoming message to\n * a fetch `Request`, hands it to the dispatcher with the consumer's per-request\n * `state`, and writes the resulting `Response` back.\n *\n * @remarks\n * A rejected `dispatcher.handle` (a route handler throw — the dispatcher\n * never invents an error boundary) is this listener's transport-level\n * LAST RESORT, distinct from an application error boundary: when nothing has\n * been sent yet, it destroys the connection with a bare `500` head (never\n * leaking a hanging socket); after headers are already sent, it destroys the\n * connection outright. The router still owns no error POLICY — a consumer\n * that wants mapped error responses installs its own boundary around\n * `dispatcher.handle` (the future `@orkestrel/server` seam).\n *\n * @typeParam TState - The consumer's opaque per-request state type\n * @param dispatcher - The core dispatcher to run each converted request through\n * @param state - Derives the consumer's per-request `state` from the raw message\n * @returns A `(request, response) => void` listener, passable directly to\n * `http.createServer`\n *\n * @example\n * ```ts\n * import { createListener } from '@orkestrel/router/server'\n * import { createDispatcher } from '@orkestrel/router'\n * import http from 'node:http'\n *\n * const dispatcher = createDispatcher<{ readonly requestId: string }>()\n * dispatcher.add({\n * \tmethod: 'GET',\n * \tpath: '/users/:id',\n * \thandler: (_request, context) =>\n * \t\tResponse.json({ id: context.params.id, requestId: context.state.requestId }),\n * })\n *\n * const server = http.createServer(\n * \tcreateListener(dispatcher, () => ({ requestId: crypto.randomUUID() })),\n * )\n * server.listen(0)\n * ```\n */\nexport function createListener<TState>(\n\tdispatcher: DispatcherInterface<TState>,\n\tstate: StateFunction<TState>,\n): ListenerFunction {\n\treturn (request, response) => {\n\t\tvoid handleListenerRequest(dispatcher, state, request, response)\n\t}\n}\n"],"mappings":"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"}
|
package/dist/src/server/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isRecord } from "@orkestrel/contract";
|
|
1
|
+
import { isArray, isError, isRecord } from "@orkestrel/contract";
|
|
2
2
|
import { once } from "node:events";
|
|
3
3
|
import { createAbort } from "@orkestrel/abort";
|
|
4
4
|
//#region src/server/validators.ts
|
|
@@ -76,10 +76,10 @@ function buildRequest(message, options) {
|
|
|
76
76
|
for (const [name, value] of Object.entries(message.headers)) {
|
|
77
77
|
if (value === void 0) continue;
|
|
78
78
|
if (name === "set-cookie") {
|
|
79
|
-
for (const cookie of
|
|
79
|
+
for (const cookie of isArray(value) ? value : [value]) headers.append(name, cookie);
|
|
80
80
|
continue;
|
|
81
81
|
}
|
|
82
|
-
headers.set(name,
|
|
82
|
+
headers.set(name, isArray(value) ? value.join(", ") : value);
|
|
83
83
|
}
|
|
84
84
|
const abort = createAbort();
|
|
85
85
|
message.once("close", () => {
|
|
@@ -209,7 +209,7 @@ async function handleListenerRequest(dispatcher, state, request, response) {
|
|
|
209
209
|
if (!response.headersSent && !response.destroyed) {
|
|
210
210
|
response.writeHead(500);
|
|
211
211
|
response.end();
|
|
212
|
-
} else if (!response.destroyed) response.destroy(error
|
|
212
|
+
} else if (!response.destroyed) response.destroy(isError(error) ? error : new Error(String(error)));
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/server/validators.ts","../../../src/server/helpers.ts","../../../src/server/handlers.ts"],"sourcesContent":["// ============================================================================\n// Server guards — the centralized-file rule's home for the total `is*` narrows the\n// `node:http` conversion seam applies to raw connection values. Every\n// declaration here is total and `export`ed per the centralized-file rule.\n// ============================================================================\n\nimport { isRecord } from '@orkestrel/contract'\n\n/**\n * Determines whether a `node:http` connection socket is TLS-encrypted — the\n * total, never-throwing narrow `buildRequest` uses to pick the\n * derived scheme (`https` vs `http`).\n *\n * @param socket - The connection value to test (typically `message.socket`)\n * @returns True if `socket` carries a truthy `encrypted` property (a\n * `tls.TLSSocket`); false otherwise, including for `undefined`\n *\n * @example\n * ```ts\n * import { isEncryptedSocket } from '@src/server'\n *\n * isEncryptedSocket({ encrypted: true }) // true\n * isEncryptedSocket({}) // false\n * ```\n */\nexport function isEncryptedSocket(socket: unknown): socket is { readonly encrypted: true } {\n\treturn isRecord(socket) && socket.encrypted === true\n}\n","// ============================================================================\n// Pure conversion between `node:http` and the fetch vocabulary the core\n// `Dispatcher` speaks — no lifecycle and no listener ownership. Every\n// function is exported per the centralized-file rule.\n// ============================================================================\n\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { RequestOptions } from './types.js'\nimport { once } from 'node:events'\nimport { createAbort } from '@orkestrel/abort'\nimport { isEncryptedSocket } from './validators.js'\n\n/**\n * Builds a fetch-standard `Request` from a `node:http` `IncomingMessage` — the\n * server-adapter half of the fetch/node conversion seam.\n *\n * @remarks\n * - `method` is carried over verbatim (defaulting to `GET` when absent).\n * - The URL is built against `options.origin` when given, otherwise a scheme\n * derived from the connection (`https` when {@link isEncryptedSocket}, else\n * `http`) plus the `Host` header (absent `Host` ⇒ `localhost`).\n * - Every request header is copied; multi-value headers are joined per fetch\n * semantics (`', '`-joined), except `set-cookie`, whose values are each\n * appended individually (fetch `Headers` preserves multiple `set-cookie`\n * entries distinctly).\n * - For a method that carries a body (anything but `GET`/`HEAD`), the message\n * is pumped chunk by chunk into a DOM-compatible `ReadableStream<Uint8Array>`\n * (reconciling the DOM + node type worlds under the root config), with\n * `duplex: 'half'` set as Node's fetch implementation requires for a\n * streamed request body.\n * - A fresh `@orkestrel/abort` handle backs `request.signal`. It aborts when\n * the request connection closes before the message finished\n * (`!message.complete`), or when the paired `options.response` closes before\n * its response finished (`!response.writableEnded`). A handler awaiting the\n * signal therefore observes either side of a client disconnect the\n * fetch-standard way, with zero router-specific API.\n *\n * @param message - The raw `node:http` request\n * @param options - Optional `origin` override and paired `response` for\n * response-side disconnect tracking ({@link RequestOptions})\n * @returns A fetch `Request` whose `signal` fires on an incomplete request, or\n * on a response-side client disconnect when `options.response` is provided\n *\n * @example\n * ```ts\n * import { buildRequest } from '@src/server'\n * import http from 'node:http'\n *\n * const server = http.createServer((incoming, response) => {\n * \tconst request = buildRequest(incoming, { response })\n * \tconsole.log(request.method, request.url)\n * })\n * ```\n */\nexport function buildRequest(message: IncomingMessage, options?: RequestOptions): Request {\n\tconst method = message.method ?? 'GET'\n\tconst host = message.headers.host ?? 'localhost'\n\tconst scheme = isEncryptedSocket(message.socket) ? 'https' : 'http'\n\tconst origin = options?.origin ?? `${scheme}://${host}`\n\tconst url = new URL(message.url ?? '/', origin)\n\n\tconst headers = new Headers()\n\tfor (const [name, value] of Object.entries(message.headers)) {\n\t\tif (value === undefined) continue\n\t\tif (name === 'set-cookie') {\n\t\t\tfor (const cookie of Array.isArray(value) ? value : [value]) headers.append(name, cookie)\n\t\t\tcontinue\n\t\t}\n\t\theaders.set(name, Array.isArray(value) ? value.join(', ') : value)\n\t}\n\n\tconst abort = createAbort()\n\tmessage.once('close', () => {\n\t\tif (!message.complete)\n\t\t\tabort.abort(new Error(`request to ${url.pathname} disconnected before completion`))\n\t})\n\tconst response = options?.response\n\tif (response !== undefined)\n\t\tresponse.once('close', () => {\n\t\t\tif (!response.writableEnded)\n\t\t\t\tabort.abort(new Error(`request to ${url.pathname} disconnected before response completed`))\n\t\t})\n\n\tconst carriesBody = method !== 'GET' && method !== 'HEAD'\n\tconst init: RequestInit = { method, headers, signal: abort.signal }\n\tif (!carriesBody) return new Request(url, init)\n\n\tconst body = new ReadableStream<Uint8Array>({\n\t\tasync start(controller) {\n\t\t\ttry {\n\t\t\t\tfor await (const chunk of message) controller.enqueue(chunk)\n\t\t\t\tcontroller.close()\n\t\t\t} catch (error) {\n\t\t\t\tcontroller.error(error)\n\t\t\t}\n\t\t},\n\t})\n\tconst streamed: RequestInit & { readonly duplex: 'half' } = { ...init, body, duplex: 'half' }\n\treturn new Request(url, streamed)\n}\n\n/**\n * Writes a fetch-standard `Response` back to a `node:http` `ServerResponse` —\n * the reverse half of the fetch/node conversion seam.\n *\n * @remarks\n * Writes `status`/`statusText`, then every response header (`set-cookie`\n * written through {@link Headers.getSetCookie} so multiple cookies stay distinct\n * instead of collapsing into one comma-joined header), then streams the web\n * body to `target` chunk by chunk (`for await` over `response.body`), ending\n * `target` when the stream completes. When a write reports backpressure, the\n * body pump waits for `drain` before pulling again, unless the target closes,\n * errors, or is destroyed first. A `null` body ends `target` immediately with\n * no further writes. Total error posture: if `target` is destroyed mid-stream\n * (the client disconnected), the write loop stops and `target` is left as-is\n * rather than throwing an unhandled rejection — a destroyed target is not\n * this function's error to surface.\n *\n * @param response - The fetch `Response` to write\n * @param target - The `node:http` response to write it to\n * @returns A promise that resolves after `target` has been ended (or the\n * stream stopped because `target` was destroyed)\n *\n * @example\n * ```ts\n * import { sendResponse } from '@src/server'\n * import http from 'node:http'\n *\n * const server = http.createServer(async (_incoming, target) => {\n * \tawait sendResponse(new Response('ok'), target)\n * })\n * ```\n */\nexport async function sendResponse(response: Response, target: ServerResponse): Promise<void> {\n\ttarget.statusCode = response.status\n\ttarget.statusMessage = response.statusText\n\tfor (const [name, value] of response.headers) {\n\t\tif (name === 'set-cookie') continue\n\t\ttarget.setHeader(name, value)\n\t}\n\tconst cookies = response.headers.getSetCookie()\n\tif (cookies.length > 0) target.setHeader('set-cookie', cookies)\n\n\tif (response.body === null) {\n\t\tif (!target.destroyed) target.end()\n\t\treturn\n\t}\n\ttry {\n\t\tfor await (const chunk of response.body) {\n\t\t\tif (target.destroyed) return\n\t\t\tif (!target.write(chunk)) {\n\t\t\t\tif (target.destroyed) return\n\t\t\t\tconst abort = new AbortController()\n\t\t\t\ttry {\n\t\t\t\t\tawait Promise.race([\n\t\t\t\t\t\tonce(target, 'drain', { signal: abort.signal }),\n\t\t\t\t\t\tonce(target, 'close', { signal: abort.signal }),\n\t\t\t\t\t])\n\t\t\t\t} finally {\n\t\t\t\t\tabort.abort()\n\t\t\t\t}\n\t\t\t\tif (target.destroyed) return\n\t\t\t}\n\t\t}\n\t\tif (!target.destroyed) target.end()\n\t} catch {\n\t\tif (!target.destroyed) target.end()\n\t}\n}\n","// ============================================================================\n// Server request handlers — the centralized-file rule's home for the functions that\n// run one `node:http` exchange through a core `Dispatcher`, plus the listener\n// the whole server face hands to `http.createServer`. Every function\n// is exported per the centralized-file rule.\n// ============================================================================\n\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { DispatcherInterface } from '@src/core'\nimport type { ListenerFunction, StateFunction } from './types.js'\nimport { buildRequest, sendResponse } from './helpers.js'\n\n/**\n * Handles one `node:http` request through a core dispatcher and writes its\n * fetch-standard response.\n *\n * @remarks\n * This is the named asynchronous orchestration behind {@link createListener}.\n * A rejected dispatch is treated only as a transport-level last resort: write\n * a bare `500` before headers, or destroy a response whose headers have\n * already started.\n *\n * @typeParam TState - The consumer's opaque per-request state type\n * @param dispatcher - The core dispatcher to run\n * @param state - Derives the consumer state from the incoming message\n * @param request - The raw `node:http` request\n * @param response - The raw `node:http` response\n * @returns A promise that settles after the response is written or closed\n *\n * @example\n * ```ts\n * const server = http.createServer((request, response) => {\n * \tvoid handleListenerRequest(dispatcher, () => undefined, request, response)\n * })\n * ```\n */\nexport async function handleListenerRequest<TState>(\n\tdispatcher: DispatcherInterface<TState>,\n\tstate: StateFunction<TState>,\n\trequest: IncomingMessage,\n\tresponse: ServerResponse,\n): Promise<void> {\n\ttry {\n\t\tconst converted = buildRequest(request, { response })\n\t\tconst result = await dispatcher.handle(converted, state(request))\n\t\tawait sendResponse(result, response)\n\t} catch (error) {\n\t\tif (!response.headersSent && !response.destroyed) {\n\t\t\tresponse.writeHead(500)\n\t\t\tresponse.end()\n\t\t} else if (!response.destroyed) {\n\t\t\tresponse.destroy(error instanceof Error ? error : new Error(String(error)))\n\t\t}\n\t}\n}\n\n/**\n * Creates a `node:http` request listener over a core {@link DispatcherInterface} —\n * the whole server face's entry point: converts the incoming message to\n * a fetch `Request`, hands it to the dispatcher with the consumer's per-request\n * `state`, and writes the resulting `Response` back.\n *\n * @remarks\n * A rejected `dispatcher.handle` (a route handler throw — the dispatcher\n * never invents an error boundary) is this listener's transport-level\n * LAST RESORT, distinct from an application error boundary: when nothing has\n * been sent yet, it destroys the connection with a bare `500` head (never\n * leaking a hanging socket); after headers are already sent, it destroys the\n * connection outright. The router still owns no error POLICY — a consumer\n * that wants mapped error responses installs its own boundary around\n * `dispatcher.handle` (the future `@orkestrel/server` seam).\n *\n * @typeParam TState - The consumer's opaque per-request state type\n * @param dispatcher - The core dispatcher to run each converted request through\n * @param state - Derives the consumer's per-request `state` from the raw message\n * @returns A `(request, response) => void` listener, passable directly to\n * `http.createServer`\n *\n * @example\n * ```ts\n * import { createListener } from '@orkestrel/router/server'\n * import { createDispatcher } from '@orkestrel/router'\n * import http from 'node:http'\n *\n * const dispatcher = createDispatcher<{ readonly requestId: string }>()\n * dispatcher.add({\n * \tmethod: 'GET',\n * \tpath: '/users/:id',\n * \thandler: (_request, context) =>\n * \t\tResponse.json({ id: context.params.id, requestId: context.state.requestId }),\n * })\n *\n * const server = http.createServer(\n * \tcreateListener(dispatcher, () => ({ requestId: crypto.randomUUID() })),\n * )\n * server.listen(0)\n * ```\n */\nexport function createListener<TState>(\n\tdispatcher: DispatcherInterface<TState>,\n\tstate: StateFunction<TState>,\n): ListenerFunction {\n\treturn (request, response) => {\n\t\tvoid handleListenerRequest(dispatcher, state, request, response)\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,kBAAkB,QAAyD;CAC1F,OAAO,SAAS,MAAM,KAAK,OAAO,cAAc;AACjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC2BA,SAAgB,aAAa,SAA0B,SAAmC;CACzF,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,OAAO,QAAQ,QAAQ,QAAQ;CACrC,MAAM,SAAS,kBAAkB,QAAQ,MAAM,IAAI,UAAU;CAC7D,MAAM,SAAS,SAAS,UAAU,GAAG,OAAO,KAAK;CACjD,MAAM,MAAM,IAAI,IAAI,QAAQ,OAAO,KAAK,MAAM;CAE9C,MAAM,UAAU,IAAI,QAAQ;CAC5B,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,QAAQ,OAAO,GAAG;EAC5D,IAAI,UAAU,KAAA,GAAW;EACzB,IAAI,SAAS,cAAc;GAC1B,KAAK,MAAM,UAAU,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK,GAAG,QAAQ,OAAO,MAAM,MAAM;GACxF;EACD;EACA,QAAQ,IAAI,MAAM,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK;CAClE;CAEA,MAAM,QAAQ,YAAY;CAC1B,QAAQ,KAAK,eAAe;EAC3B,IAAI,CAAC,QAAQ,UACZ,MAAM,sBAAM,IAAI,MAAM,cAAc,IAAI,SAAS,gCAAgC,CAAC;CACpF,CAAC;CACD,MAAM,WAAW,SAAS;CAC1B,IAAI,aAAa,KAAA,GAChB,SAAS,KAAK,eAAe;EAC5B,IAAI,CAAC,SAAS,eACb,MAAM,sBAAM,IAAI,MAAM,cAAc,IAAI,SAAS,wCAAwC,CAAC;CAC5F,CAAC;CAEF,MAAM,cAAc,WAAW,SAAS,WAAW;CACnD,MAAM,OAAoB;EAAE;EAAQ;EAAS,QAAQ,MAAM;CAAO;CAClE,IAAI,CAAC,aAAa,OAAO,IAAI,QAAQ,KAAK,IAAI;CAE9C,MAAM,OAAO,IAAI,eAA2B,EAC3C,MAAM,MAAM,YAAY;EACvB,IAAI;GACH,WAAW,MAAM,SAAS,SAAS,WAAW,QAAQ,KAAK;GAC3D,WAAW,MAAM;EAClB,SAAS,OAAO;GACf,WAAW,MAAM,KAAK;EACvB;CACD,EACD,CAAC;CACD,MAAM,WAAsD;EAAE,GAAG;EAAM;EAAM,QAAQ;CAAO;CAC5F,OAAO,IAAI,QAAQ,KAAK,QAAQ;AACjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,eAAsB,aAAa,UAAoB,QAAuC;CAC7F,OAAO,aAAa,SAAS;CAC7B,OAAO,gBAAgB,SAAS;CAChC,KAAK,MAAM,CAAC,MAAM,UAAU,SAAS,SAAS;EAC7C,IAAI,SAAS,cAAc;EAC3B,OAAO,UAAU,MAAM,KAAK;CAC7B;CACA,MAAM,UAAU,SAAS,QAAQ,aAAa;CAC9C,IAAI,QAAQ,SAAS,GAAG,OAAO,UAAU,cAAc,OAAO;CAE9D,IAAI,SAAS,SAAS,MAAM;EAC3B,IAAI,CAAC,OAAO,WAAW,OAAO,IAAI;EAClC;CACD;CACA,IAAI;EACH,WAAW,MAAM,SAAS,SAAS,MAAM;GACxC,IAAI,OAAO,WAAW;GACtB,IAAI,CAAC,OAAO,MAAM,KAAK,GAAG;IACzB,IAAI,OAAO,WAAW;IACtB,MAAM,QAAQ,IAAI,gBAAgB;IAClC,IAAI;KACH,MAAM,QAAQ,KAAK,CAClB,KAAK,QAAQ,SAAS,EAAE,QAAQ,MAAM,OAAO,CAAC,GAC9C,KAAK,QAAQ,SAAS,EAAE,QAAQ,MAAM,OAAO,CAAC,CAC/C,CAAC;IACF,UAAU;KACT,MAAM,MAAM;IACb;IACA,IAAI,OAAO,WAAW;GACvB;EACD;EACA,IAAI,CAAC,OAAO,WAAW,OAAO,IAAI;CACnC,QAAQ;EACP,IAAI,CAAC,OAAO,WAAW,OAAO,IAAI;CACnC;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;ACpIA,eAAsB,sBACrB,YACA,OACA,SACA,UACgB;CAChB,IAAI;EACH,MAAM,YAAY,aAAa,SAAS,EAAE,SAAS,CAAC;EAEpD,MAAM,aAAa,MADE,WAAW,OAAO,WAAW,MAAM,OAAO,CAAC,GACrC,QAAQ;CACpC,SAAS,OAAO;EACf,IAAI,CAAC,SAAS,eAAe,CAAC,SAAS,WAAW;GACjD,SAAS,UAAU,GAAG;GACtB,SAAS,IAAI;EACd,OAAO,IAAI,CAAC,SAAS,WACpB,SAAS,QAAQ,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC,CAAC;CAE5E;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA,SAAgB,eACf,YACA,OACmB;CACnB,QAAQ,SAAS,aAAa;EAC7B,sBAA2B,YAAY,OAAO,SAAS,QAAQ;CAChE;AACD"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/server/validators.ts","../../../src/server/helpers.ts","../../../src/server/handlers.ts"],"sourcesContent":["// ============================================================================\n// Server guards — the centralized-file rule's home for the total `is*` narrows the\n// `node:http` conversion seam applies to raw connection values. Every\n// declaration here is total and `export`ed per the centralized-file rule.\n// ============================================================================\n\nimport { isRecord } from '@orkestrel/contract'\n\n/**\n * Determines whether a `node:http` connection socket is TLS-encrypted — the\n * total, never-throwing narrow `buildRequest` uses to pick the\n * derived scheme (`https` vs `http`).\n *\n * @param socket - The connection value to test (typically `message.socket`)\n * @returns True if `socket` carries a truthy `encrypted` property (a\n * `tls.TLSSocket`); false otherwise, including for `undefined`\n *\n * @example\n * ```ts\n * import { isEncryptedSocket } from '@src/server'\n *\n * isEncryptedSocket({ encrypted: true }) // true\n * isEncryptedSocket({}) // false\n * ```\n */\nexport function isEncryptedSocket(socket: unknown): socket is { readonly encrypted: true } {\n\treturn isRecord(socket) && socket.encrypted === true\n}\n","// ============================================================================\n// Pure conversion between `node:http` and the fetch vocabulary the core\n// `Dispatcher` speaks — no lifecycle and no listener ownership. Every\n// function is exported per the centralized-file rule.\n// ============================================================================\n\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { RequestOptions } from './types.js'\nimport { once } from 'node:events'\nimport { createAbort } from '@orkestrel/abort'\nimport { isArray } from '@orkestrel/contract'\nimport { isEncryptedSocket } from './validators.js'\n\n/**\n * Builds a fetch-standard `Request` from a `node:http` `IncomingMessage` — the\n * server-adapter half of the fetch/node conversion seam.\n *\n * @remarks\n * - `method` is carried over verbatim (defaulting to `GET` when absent).\n * - The URL is built against `options.origin` when given, otherwise a scheme\n * derived from the connection (`https` when {@link isEncryptedSocket}, else\n * `http`) plus the `Host` header (absent `Host` ⇒ `localhost`).\n * - Every request header is copied; multi-value headers are joined per fetch\n * semantics (`', '`-joined), except `set-cookie`, whose values are each\n * appended individually (fetch `Headers` preserves multiple `set-cookie`\n * entries distinctly).\n * - For a method that carries a body (anything but `GET`/`HEAD`), the message\n * is pumped chunk by chunk into a DOM-compatible `ReadableStream<Uint8Array>`\n * (reconciling the DOM + node type worlds under the root config), with\n * `duplex: 'half'` set as Node's fetch implementation requires for a\n * streamed request body.\n * - A fresh `@orkestrel/abort` handle backs `request.signal`. It aborts when\n * the request connection closes before the message finished\n * (`!message.complete`), or when the paired `options.response` closes before\n * its response finished (`!response.writableEnded`). A handler awaiting the\n * signal therefore observes either side of a client disconnect the\n * fetch-standard way, with zero router-specific API.\n *\n * @param message - The raw `node:http` request\n * @param options - Optional `origin` override and paired `response` for\n * response-side disconnect tracking ({@link RequestOptions})\n * @returns A fetch `Request` whose `signal` fires on an incomplete request, or\n * on a response-side client disconnect when `options.response` is provided\n *\n * @example\n * ```ts\n * import { buildRequest } from '@src/server'\n * import http from 'node:http'\n *\n * const server = http.createServer((incoming, response) => {\n * \tconst request = buildRequest(incoming, { response })\n * \tconsole.log(request.method, request.url)\n * })\n * ```\n */\nexport function buildRequest(message: IncomingMessage, options?: RequestOptions): Request {\n\tconst method = message.method ?? 'GET'\n\tconst host = message.headers.host ?? 'localhost'\n\tconst scheme = isEncryptedSocket(message.socket) ? 'https' : 'http'\n\tconst origin = options?.origin ?? `${scheme}://${host}`\n\tconst url = new URL(message.url ?? '/', origin)\n\n\tconst headers = new Headers()\n\tfor (const [name, value] of Object.entries(message.headers)) {\n\t\tif (value === undefined) continue\n\t\tif (name === 'set-cookie') {\n\t\t\tfor (const cookie of isArray(value) ? value : [value]) headers.append(name, cookie)\n\t\t\tcontinue\n\t\t}\n\t\theaders.set(name, isArray(value) ? value.join(', ') : value)\n\t}\n\n\tconst abort = createAbort()\n\tmessage.once('close', () => {\n\t\tif (!message.complete)\n\t\t\tabort.abort(new Error(`request to ${url.pathname} disconnected before completion`))\n\t})\n\tconst response = options?.response\n\tif (response !== undefined)\n\t\tresponse.once('close', () => {\n\t\t\tif (!response.writableEnded)\n\t\t\t\tabort.abort(new Error(`request to ${url.pathname} disconnected before response completed`))\n\t\t})\n\n\tconst carriesBody = method !== 'GET' && method !== 'HEAD'\n\tconst init: RequestInit = { method, headers, signal: abort.signal }\n\tif (!carriesBody) return new Request(url, init)\n\n\tconst body = new ReadableStream<Uint8Array>({\n\t\tasync start(controller) {\n\t\t\ttry {\n\t\t\t\tfor await (const chunk of message) controller.enqueue(chunk)\n\t\t\t\tcontroller.close()\n\t\t\t} catch (error) {\n\t\t\t\tcontroller.error(error)\n\t\t\t}\n\t\t},\n\t})\n\tconst streamed: RequestInit & { readonly duplex: 'half' } = { ...init, body, duplex: 'half' }\n\treturn new Request(url, streamed)\n}\n\n/**\n * Writes a fetch-standard `Response` back to a `node:http` `ServerResponse` —\n * the reverse half of the fetch/node conversion seam.\n *\n * @remarks\n * Writes `status`/`statusText`, then every response header (`set-cookie`\n * written through {@link Headers.getSetCookie} so multiple cookies stay distinct\n * instead of collapsing into one comma-joined header), then streams the web\n * body to `target` chunk by chunk (`for await` over `response.body`), ending\n * `target` when the stream completes. When a write reports backpressure, the\n * body pump waits for `drain` before pulling again, unless the target closes,\n * errors, or is destroyed first. A `null` body ends `target` immediately with\n * no further writes. Total error posture: if `target` is destroyed mid-stream\n * (the client disconnected), the write loop stops and `target` is left as-is\n * rather than throwing an unhandled rejection — a destroyed target is not\n * this function's error to surface.\n *\n * @param response - The fetch `Response` to write\n * @param target - The `node:http` response to write it to\n * @returns A promise that resolves after `target` has been ended (or the\n * stream stopped because `target` was destroyed)\n *\n * @example\n * ```ts\n * import { sendResponse } from '@src/server'\n * import http from 'node:http'\n *\n * const server = http.createServer(async (_incoming, target) => {\n * \tawait sendResponse(new Response('ok'), target)\n * })\n * ```\n */\nexport async function sendResponse(response: Response, target: ServerResponse): Promise<void> {\n\ttarget.statusCode = response.status\n\ttarget.statusMessage = response.statusText\n\tfor (const [name, value] of response.headers) {\n\t\tif (name === 'set-cookie') continue\n\t\ttarget.setHeader(name, value)\n\t}\n\tconst cookies = response.headers.getSetCookie()\n\tif (cookies.length > 0) target.setHeader('set-cookie', cookies)\n\n\tif (response.body === null) {\n\t\tif (!target.destroyed) target.end()\n\t\treturn\n\t}\n\ttry {\n\t\tfor await (const chunk of response.body) {\n\t\t\tif (target.destroyed) return\n\t\t\tif (!target.write(chunk)) {\n\t\t\t\tif (target.destroyed) return\n\t\t\t\tconst abort = new AbortController()\n\t\t\t\ttry {\n\t\t\t\t\tawait Promise.race([\n\t\t\t\t\t\tonce(target, 'drain', { signal: abort.signal }),\n\t\t\t\t\t\tonce(target, 'close', { signal: abort.signal }),\n\t\t\t\t\t])\n\t\t\t\t} finally {\n\t\t\t\t\tabort.abort()\n\t\t\t\t}\n\t\t\t\tif (target.destroyed) return\n\t\t\t}\n\t\t}\n\t\tif (!target.destroyed) target.end()\n\t} catch {\n\t\tif (!target.destroyed) target.end()\n\t}\n}\n","// ============================================================================\n// Server request handlers — the centralized-file rule's home for the functions that\n// run one `node:http` exchange through a core `Dispatcher`, plus the listener\n// the whole server face hands to `http.createServer`. Every function\n// is exported per the centralized-file rule.\n// ============================================================================\n\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { DispatcherInterface } from '@src/core'\nimport type { ListenerFunction, StateFunction } from './types.js'\nimport { isError } from '@orkestrel/contract'\nimport { buildRequest, sendResponse } from './helpers.js'\n\n/**\n * Handles one `node:http` request through a core dispatcher and writes its\n * fetch-standard response.\n *\n * @remarks\n * This is the named asynchronous orchestration behind {@link createListener}.\n * A rejected dispatch is treated only as a transport-level last resort: write\n * a bare `500` before headers, or destroy a response whose headers have\n * already started.\n *\n * @typeParam TState - The consumer's opaque per-request state type\n * @param dispatcher - The core dispatcher to run\n * @param state - Derives the consumer state from the incoming message\n * @param request - The raw `node:http` request\n * @param response - The raw `node:http` response\n * @returns A promise that settles after the response is written or closed\n *\n * @example\n * ```ts\n * const server = http.createServer((request, response) => {\n * \tvoid handleListenerRequest(dispatcher, () => undefined, request, response)\n * })\n * ```\n */\nexport async function handleListenerRequest<TState>(\n\tdispatcher: DispatcherInterface<TState>,\n\tstate: StateFunction<TState>,\n\trequest: IncomingMessage,\n\tresponse: ServerResponse,\n): Promise<void> {\n\ttry {\n\t\tconst converted = buildRequest(request, { response })\n\t\tconst result = await dispatcher.handle(converted, state(request))\n\t\tawait sendResponse(result, response)\n\t} catch (error) {\n\t\tif (!response.headersSent && !response.destroyed) {\n\t\t\tresponse.writeHead(500)\n\t\t\tresponse.end()\n\t\t} else if (!response.destroyed) {\n\t\t\tresponse.destroy(isError(error) ? error : new Error(String(error)))\n\t\t}\n\t}\n}\n\n/**\n * Creates a `node:http` request listener over a core {@link DispatcherInterface} —\n * the whole server face's entry point: converts the incoming message to\n * a fetch `Request`, hands it to the dispatcher with the consumer's per-request\n * `state`, and writes the resulting `Response` back.\n *\n * @remarks\n * A rejected `dispatcher.handle` (a route handler throw — the dispatcher\n * never invents an error boundary) is this listener's transport-level\n * LAST RESORT, distinct from an application error boundary: when nothing has\n * been sent yet, it destroys the connection with a bare `500` head (never\n * leaking a hanging socket); after headers are already sent, it destroys the\n * connection outright. The router still owns no error POLICY — a consumer\n * that wants mapped error responses installs its own boundary around\n * `dispatcher.handle` (the future `@orkestrel/server` seam).\n *\n * @typeParam TState - The consumer's opaque per-request state type\n * @param dispatcher - The core dispatcher to run each converted request through\n * @param state - Derives the consumer's per-request `state` from the raw message\n * @returns A `(request, response) => void` listener, passable directly to\n * `http.createServer`\n *\n * @example\n * ```ts\n * import { createListener } from '@orkestrel/router/server'\n * import { createDispatcher } from '@orkestrel/router'\n * import http from 'node:http'\n *\n * const dispatcher = createDispatcher<{ readonly requestId: string }>()\n * dispatcher.add({\n * \tmethod: 'GET',\n * \tpath: '/users/:id',\n * \thandler: (_request, context) =>\n * \t\tResponse.json({ id: context.params.id, requestId: context.state.requestId }),\n * })\n *\n * const server = http.createServer(\n * \tcreateListener(dispatcher, () => ({ requestId: crypto.randomUUID() })),\n * )\n * server.listen(0)\n * ```\n */\nexport function createListener<TState>(\n\tdispatcher: DispatcherInterface<TState>,\n\tstate: StateFunction<TState>,\n): ListenerFunction {\n\treturn (request, response) => {\n\t\tvoid handleListenerRequest(dispatcher, state, request, response)\n\t}\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.15",
|
|
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",
|
|
@@ -91,23 +91,23 @@
|
|
|
91
91
|
"test:setup": "vitest run --config vite.config.ts --no-cache --reporter=dot --project setup"
|
|
92
92
|
},
|
|
93
93
|
"dependencies": {
|
|
94
|
-
"@orkestrel/abort": "^0.0.
|
|
94
|
+
"@orkestrel/abort": "^0.0.11",
|
|
95
95
|
"@orkestrel/contract": "^0.0.17",
|
|
96
96
|
"@orkestrel/emitter": "^0.0.10"
|
|
97
97
|
},
|
|
98
98
|
"devDependencies": {
|
|
99
|
-
"@microsoft/api-extractor": "^7.59.
|
|
100
|
-
"@orkestrel/guide": "^0.0.
|
|
101
|
-
"@orkestrel/probe": "^0.0.
|
|
102
|
-
"@orkestrel/scaffold": "^0.0.
|
|
103
|
-
"@orkestrel/test": "^0.0.
|
|
104
|
-
"@types/node": "^26.
|
|
99
|
+
"@microsoft/api-extractor": "^7.59.1",
|
|
100
|
+
"@orkestrel/guide": "^0.0.19",
|
|
101
|
+
"@orkestrel/probe": "^0.0.15",
|
|
102
|
+
"@orkestrel/scaffold": "^0.0.70",
|
|
103
|
+
"@orkestrel/test": "^0.0.15",
|
|
104
|
+
"@types/node": "^26.5.1",
|
|
105
105
|
"@vitest/browser-playwright": "^4.1.11",
|
|
106
|
-
"oxfmt": "^0.
|
|
107
|
-
"oxlint": "^1.
|
|
108
|
-
"playwright": "^1.
|
|
106
|
+
"oxfmt": "^0.68.0",
|
|
107
|
+
"oxlint": "^1.83.0",
|
|
108
|
+
"playwright": "^1.63.0",
|
|
109
109
|
"typescript": "^6.0.3",
|
|
110
|
-
"vite": "^8.
|
|
110
|
+
"vite": "^8.3.0",
|
|
111
111
|
"vitest": "^4.1.11"
|
|
112
112
|
},
|
|
113
113
|
"engines": {
|