/** * Copyright (c) 2026 AzerothJS. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ /** * The radix-tree route matcher. * * Routing is a pure data structure with no HTTP in it: insert (method, pattern, value) pairs * at startup, then match (method, path) per request. Three design decisions do the heavy * lifting: * * - NO USER REGEX. Express's path-to-regexp compiles every route into a regex and scans * them linearly - O(routes) per request and a ReDoS CVE history. Here a path walks one * radix tree in O(segments), and nothing user-supplied is ever compiled into a pattern. * - CONFLICTS FAIL LOUDLY AT INSERT. Express lets a later, more specific route be silently * shadowed by an earlier one. Here a duplicate (method, pattern) or two different param * names at the same position throw at registration - boot fails with the exact pair. * - METHOD MISMATCH IS DISTINGUISHED FROM NO MATCH. A path that exists under other methods * reports which ones, so the app layer can emit a correct 405 + Allow instead of a 404. * * Matching precedence per segment is static > parameter > wildcard, the ecosystem-standard * specificity order, applied with BACKTRACKING: if the static branch dead-ends deeper in the * tree, the walk retries the param branch, then the wildcard - so `/users/me` and * `/users/:id/posts` coexist with `/users/me/settings` without registration-order tricks. * * Parameter typing is compile-time: `PathParams<'/users/:id/files/*rest'>` infers * `{ id: string; rest: string }` from the pattern string itself, so handlers receive typed * params with zero runtime cost and no codegen. */ /** Infers the param object type from a route pattern string. */ export type PathParams = Path extends `${infer Head}/${infer Rest}` ? PathParams & PathParams : Path extends `:${infer Name}` ? Name extends '' ? object : { [K in Name]: string; } : Path extends `*${infer Name}` ? Name extends '' ? object : { [K in Name]: string; } : object; /** A successful match: the stored value plus the decoded path parameters. */ export interface RouteMatch { kind: 'match'; value: T; params: Record; } /** The path exists under other methods - the app layer turns this into 405 + Allow. */ export interface RouteMethodMismatch { kind: 'method-mismatch'; /** The methods registered for this path, for the Allow header. */ allowed: string[]; } /** Nothing registered anywhere under this path. */ export interface RouteMiss { kind: 'miss'; } /** * The path is not a valid URI - a percent escape without two hex digits after it. Distinct from * a miss because the request-target is malformed rather than unknown; the app layer turns this * into 400, not 404. */ export interface RouteDecodeError { kind: 'decode-error'; } /** * Everything {@link RadixRouter.route} can answer, as a discriminated union on `kind`: a match * carrying the value and decoded params, a method mismatch carrying the Allow set (405), a * plain miss (404), or a decode error for malformed percent-escapes (400). One union so a * dispatcher must handle all four - there is no throwing path. */ export type RouteResult = RouteMatch | RouteMethodMismatch | RouteMiss | RouteDecodeError; /** * Splits a path into segments, collapsing duplicate slashes and one trailing slash so * `/a//b/` and `/a/b` are the same route. The empty path and `/` both yield []. */ export declare function segmentsOf(path: string): string[]; /** * A radix-tree router mapping (method, pattern) to a value of the caller's choosing. * Patterns are `/literal/:param/*wildcard` - no regex form exists. */ export declare class RadixRouter { #private; /** * Registers a route. Throws (synchronously, at startup) on: a duplicate (method, pattern); * two different param names at the same tree position; a wildcard that is not the final * segment; or an empty param/wildcard name. Failing the boot loudly is the point - a * conflicting route table must never reach traffic. */ insert(method: string, pattern: string, value: T): void; /** * Matches a request path. Segments are percent-decoded individually; a malformed escape * returns a miss (the app layer's 400 lives above this pure structure). HEAD falls back * to a GET registration, mirroring what every origin server does. */ match(method: string, path: string): RouteResult; /** * The registered route table, one line per (method, pattern), in registration order - * printed at boot so the served surface is never a mystery. */ table(): string[]; }