@orkestrel/router 0.0.12 → 0.0.14

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.
@@ -1,10 +1,10 @@
1
- import { EmitterErrorHandler } from '@orkestrel/emitter';
2
- import { EmitterHooks } from '@orkestrel/emitter';
3
- import { EmitterInterface } from '@orkestrel/emitter';
1
+ import type { EmitterErrorHandler } from '@orkestrel/emitter';
2
+ import type { EmitterHooks } from '@orkestrel/emitter';
3
+ import type { EmitterInterface } from '@orkestrel/emitter';
4
4
 
5
5
  /**
6
- * The native-override seam — a predicate deciding whether an entry's `meta`
7
- * ANSWERS a given `match` call, beyond path matching.
6
+ * Represents the native-override seam — a predicate deciding whether an entry's
7
+ * `meta` answers a given `match` call, beyond path matching.
8
8
  *
9
9
  * @typeParam Meta - The entry payload the predicate reads
10
10
  *
@@ -14,12 +14,12 @@ import { EmitterInterface } from '@orkestrel/emitter';
14
14
  * === requestMethod`), the `Navigator` omits the predicate entirely (every
15
15
  * path match always answers). Passed per-call to {@link RouterInterface.match};
16
16
  * when omitted, every entry whose path matches is eligible. Total — it never
17
- * throws (a consumer keeps it pure, per AGENTS §14 guard totality).
17
+ * throws, so a consumer keeps it pure.
18
18
  */
19
19
  export declare type AnswerHandler<Meta> = (meta: Meta) => boolean;
20
20
 
21
21
  /**
22
- * Canonicalize a route path for REGISTRY IDENTITYstrip a single trailing
22
+ * Canonicalizes a route path for registry identitystrips a single trailing
23
23
  * slash, except the root `/` (and the empty pattern). The trailing-slash fold
24
24
  * {@link compilePath} normalizes a pattern through, so identity agrees with the
25
25
  * matcher.
@@ -45,22 +45,23 @@ export declare type AnswerHandler<Meta> = (meta: Meta) => boolean;
45
45
  export declare function canonicalizePath(path: string): string;
46
46
 
47
47
  /**
48
- * Classify one path segment into its specificity TIER — the SAME syntax
49
- * {@link compilePath} rewrites: a syntactically valid `:name` head is a PARAM
50
- * segment, a final `*name` is a WILDCARD segment, everything else (including a
51
- * literal segment that merely CONTAINS a `:` mid-string, e.g. `a:b`) is a
52
- * LITERAL segment.
48
+ * Classifies one path segment into its specificity tier — the same syntax
49
+ * {@link compilePath} rewrites: a syntactically valid `:name` head is a param
50
+ * segment, a final `*name` is a wildcard segment, and everything else (including a
51
+ * literal segment that merely contains a `:` mid-string, for example `a:b`) is a
52
+ * literal segment.
53
53
  *
54
54
  * @remarks
55
55
  * This is the fix over the old engine's bug: the old classifier ranked any
56
56
  * segment `includes(':')` as a param, so a literal segment like `a:b` was
57
57
  * mis-tiered even though {@link compilePath} compiles it literally. Sharing one
58
58
  * segment parser between compilation and classification keeps the two in
59
- * agreement (§4 fixes). Pure and total.
59
+ * agreement. Pure and total.
60
60
  *
61
61
  * @param segment - One `/`-split path segment
62
- * @param isFinal - Whether `segment` is the last segment of its path (only the
63
- * final segment may be classified as a wildcard)
62
+ * @param isFinal - If `true`, `segment` is the path's last segment and may
63
+ * classify as a wildcard; if `false`, a wildcard-shaped segment classifies as
64
+ * a literal
64
65
  * @returns The segment's specificity tier — {@link import('./constants.js').TIER_LITERAL},
65
66
  * {@link import('./constants.js').TIER_PARAM}, or
66
67
  * {@link import('./constants.js').TIER_WILDCARD}
@@ -76,7 +77,7 @@ export declare function canonicalizePath(path: string): string;
76
77
  export declare function classifySegment(segment: string, isFinal: boolean): number;
77
78
 
78
79
  /**
79
- * Compare two route paths by SPECIFICITY — the comparator that picks the
80
+ * Compares two route paths by specificity — the comparator that picks the
80
81
  * most-specific matching route (literal-over-param-over-wildcard,
81
82
  * registration-order-independent).
82
83
  *
@@ -104,12 +105,12 @@ export declare function classifySegment(segment: string, isFinal: boolean): numb
104
105
  export declare function compareSpecificity(a: string, b: string): number;
105
106
 
106
107
  /**
107
- * A compiled route path — the anchored regex plus its ordered param names.
108
+ * Represents a compiled route path — the anchored regex plus its ordered param names.
108
109
  *
109
110
  * @remarks
110
- * The once-per-path compile output of `compilePath` (U1 `helpers.ts`):
111
+ * The once-per-path compile output of `compilePath` (the path compiler in `helpers.ts`):
111
112
  * `regex` is anchored (`^…$`) and matches the WHOLE pathname (with an
112
- * optional trailing slash, §4), and `params` lists each captured segment's
113
+ * optional trailing slash), and `params` lists each captured segment's
113
114
  * name (`:name` or the final `*name`) in order, so a `regex.exec` result's
114
115
  * capture groups line up with `params` positionally (the walk `matchPath`
115
116
  * performs). Plain data — no behavior.
@@ -120,7 +121,7 @@ export declare interface CompiledPath {
120
121
  }
121
122
 
122
123
  /**
123
- * Compile a route path pattern into an anchored regex and its ordered param
124
+ * Compiles a route path pattern into an anchored regex and its ordered param
124
125
  * names.
125
126
  *
126
127
  * @remarks
@@ -128,7 +129,7 @@ export declare interface CompiledPath {
128
129
  * `([^/]+)` capture group; the FINAL segment may instead be `*name`, which
129
130
  * becomes a `(.+)` capture spanning the REST of the path including slashes — a
130
131
  * wildcard segment anywhere but last is a registration-time programmer error
131
- * and throws `TypeError` (§14 construction/registration boundary). Every regex
132
+ * and throws a `ContractError` at the construction/registration boundary. Every regex
132
133
  * metacharacter in a literal segment is escaped first ({@link escapeRegExp}),
133
134
  * so a path like `/files/:name.json` matches the `.` literally apart from the
134
135
  * param. The regex is anchored (`^…$`), so it matches the whole pathname, not
@@ -146,942 +147,1073 @@ export declare interface CompiledPath {
146
147
  * regex flag, so `/Users` matches `/users`. The pattern's own casing is never
147
148
  * altered — only the matching behavior.
148
149
  *
149
- * @param path - The route path pattern (e.g. `/users/:id`, `/files/*rest`)
150
- * @param sensitive - Case-sensitive matching (default `true`)
150
+ * @param path - The route path pattern (for example `/users/:id`, `/files/*rest`)
151
+ * @param sensitive - If `true`, matching is case-sensitive; if `false`, case is
152
+ * folded during matching. Default: `true`
151
153
  * @returns The {@link CompiledPath} — its `regex` + ordered `params`
152
- * @throws {TypeError} When a `*name` wildcard segment is not the FINAL segment
153
- *
154
- * @example
155
- * ```ts
156
- * const { regex, params } = compilePath('/users/:id/posts/:slug')
157
- * params // ['id', 'slug']
158
- * regex.exec('/users/7/posts/hello') // ['', '7', 'hello']
159
- * regex.test('/users/7/posts/hello/') // true the trailing slash is optional
160
- *
161
- * compilePath('/files/*rest').regex.test('/files/a/b.png') // true
162
- * compilePath('/Users', false).regex.test('/users') // true — case-insensitive
163
- * ```
164
- */
165
- export declare function compilePath(path: string, sensitive?: boolean): CompiledPath;
154
+ * @throws {@link import('@orkestrel/contract').ContractError} Thrown when a
155
+ * `*name` wildcard segment is not the FINAL segment
156
+ *
157
+ * @example
158
+ * ```ts
159
+ * const { regex, params } = compilePath('/users/:id/posts/:slug')
160
+ * params // ['id', 'slug']
161
+ * regex.exec('/users/7/posts/hello') // ['…', '7', 'hello']
162
+ * regex.test('/users/7/posts/hello/') // true — the trailing slash is optional
163
+ *
164
+ * compilePath('/files/*rest').regex.test('/files/a/b.png') // true
165
+ * compilePath('/Users', false).regex.test('/users') // true — case-insensitive
166
+ * ```
167
+ */
168
+ export declare function compilePath(path: string, sensitive?: boolean): CompiledPath;
166
169
 
167
- /**
168
- * Compute the registry key for a method-dimensioned dispatcher route.
169
- *
170
- * @remarks
171
- * Combines the route record's HTTP method with the outer entry's canonical
172
- * path, so registrations that differ only by one trailing slash replace each
173
- * other while registrations for different methods remain distinct.
174
- *
175
- * @param entry - The dispatcher route entry to identify
176
- * @returns The canonical `METHOD /path` registry key
177
- *
178
- * @example
179
- * ```ts
180
- * computeDispatchKey({ path: '/health/', meta: { method: 'GET' } }) // 'GET /health'
181
- * ```
182
- */
183
- export declare function computeDispatchKey(entry: RouteEntry<{
184
- readonly method: Method;
185
- }>): string;
170
+ /**
171
+ * Computes the canonical `METHOD /path` registry key for a method-dimensioned
172
+ * dispatcher route.
173
+ *
174
+ * @remarks
175
+ * Combines the route record's HTTP method with the outer entry's canonical
176
+ * path, so registrations that differ only by one trailing slash replace each
177
+ * other while registrations for different methods remain distinct.
178
+ *
179
+ * @param entry - The dispatcher route entry to identify
180
+ * @returns The canonical `METHOD /path` registry key
181
+ *
182
+ * @example
183
+ * ```ts
184
+ * computeDispatchKey({ path: '/health/', meta: { method: 'GET' } }) // 'GET /health'
185
+ * ```
186
+ */
187
+ export declare function computeDispatchKey(entry: RouteEntry<{
188
+ readonly method: Method;
189
+ }>): string;
186
190
 
187
- /**
188
- * Compute a route path's SPECIFICITY VECTOR — the per-segment type ranking
189
- * that breaks a tie when several registered routes match the same concrete
190
- * pathname.
191
- *
192
- * @remarks
193
- * Splits the CANONICALIZED path into segments (on `/`) and maps each to its
194
- * specificity tier via {@link classifySegment} — the same segment parser
195
- * {@link compilePath} uses, so a literal segment that merely contains a `:`
196
- * (e.g. `a:b`) is correctly tiered as literal rather than param (the old
197
- * engine's bug, fixed here). The standard route-precedence rule compares two
198
- * matching routes' vectors LEFT-TO-RIGHT: at the first index where the tiers
199
- * differ, the HIGHER tier (a literal over a param over a wildcard) is MORE
200
- * SPECIFIC and wins — so `/users/me` (`[2, 2]`) beats `/users/:id` (`[2, 1]`)
201
- * beats `/users/*rest` (`[2, 0]`) regardless of registration order. Two routes
202
- * that match the SAME concrete pathname necessarily have the same segment
203
- * count in the common case; {@link compareSpecificity} handles the general
204
- * case for totality.
205
- *
206
- * @param path - The route path pattern (e.g. `/users/:id`)
207
- * @returns The per-segment specificity tiers, in order
208
- *
209
- * @example
210
- * ```ts
211
- * computeSpecificity('/users/me') // [2, 2]
212
- * computeSpecificity('/users/:id') // [2, 1]
213
- * computeSpecificity('/files/*rest') // [2, 0]
214
- * computeSpecificity('/a:b') // [2] — literal, not param — the classification fix
215
- * ```
216
- */
217
- export declare function computeSpecificity(path: string): readonly number[];
191
+ /**
192
+ * Computes a route path's specificity vector — the per-segment type ranking
193
+ * that breaks a tie when several registered routes match the same concrete
194
+ * pathname.
195
+ *
196
+ * @remarks
197
+ * Splits the CANONICALIZED path into segments (on `/`) and maps each to its
198
+ * specificity tier through {@link classifySegment} — the same segment parser
199
+ * {@link compilePath} uses, so a literal segment that merely contains a `:`
200
+ * (for example `a:b`) is correctly tiered as literal rather than param (the old
201
+ * engine's bug, fixed here). The standard route-precedence rule compares two
202
+ * matching routes' vectors LEFT-TO-RIGHT: at the first index where the tiers
203
+ * differ, the HIGHER tier (a literal over a param over a wildcard) is MORE
204
+ * SPECIFIC and wins — so `/users/me` (`[2, 2]`) beats `/users/:id` (`[2, 1]`)
205
+ * beats `/users/*rest` (`[2, 0]`) regardless of registration order. Two routes
206
+ * that match the SAME concrete pathname necessarily have the same segment
207
+ * count in the common case; {@link compareSpecificity} handles the general
208
+ * case for totality.
209
+ *
210
+ * @param path - The route path pattern (for example `/users/:id`)
211
+ * @returns The per-segment specificity tiers, in order
212
+ *
213
+ * @example
214
+ * ```ts
215
+ * computeSpecificity('/users/me') // [2, 2]
216
+ * computeSpecificity('/users/:id') // [2, 1]
217
+ * computeSpecificity('/files/*rest') // [2, 0]
218
+ * computeSpecificity('/a:b') // [2] — literal, not param — the classification fix
219
+ * ```
220
+ */
221
+ export declare function computeSpecificity(path: string): readonly number[];
218
222
 
219
- /**
220
- * Create a {@link DispatcherInterface} — the fetch-standard, method-
221
- * dimensioned dispatch entity over one internal `Router<RouteRecord<TState>>`.
222
- *
223
- * @remarks
224
- * Prefer this over `new Dispatcher(...)` at call sites that only need the
225
- * interface.
226
- *
227
- * @typeParam TState - The consumer's opaque per-request state type (default
228
- * `undefined` for stateless use)
229
- * @param options - Optional initial `routes`, the `sensitive` case toggle,
230
- * the `unmatched`/`unmethoded` default-responder overrides, and the AGENTS
231
- * §13 emitter `on`/`error` wiring
232
- * @returns A {@link DispatcherInterface}
233
- *
234
- * @example
235
- * ```ts
236
- * import { createDispatcher } from '@src/core'
237
- *
238
- * const dispatcher = createDispatcher<{ readonly userId: string }>({
239
- * routes: [
240
- * { method: 'GET', path: '/health', handler: () => new Response('ok') },
241
- * ],
242
- * })
243
- * const response = await dispatcher.handle(new Request('http://x/health'), { userId: 'me' })
244
- * ```
245
- */
246
- export declare function createDispatcher<TState = undefined>(options?: DispatcherOptions<TState>): DispatcherInterface<TState>;
223
+ /**
224
+ * Creates a {@link DispatcherInterface} — the fetch-standard,
225
+ * method-dimensioned dispatch entity over one internal `Router<RouteRecord<TState>>`.
226
+ *
227
+ * @remarks
228
+ * Prefer this over `new Dispatcher(...)` at call sites that only need the
229
+ * interface.
230
+ *
231
+ * @typeParam TState - The consumer's opaque per-request state type (default
232
+ * `undefined` for stateless use)
233
+ * @param options - Optional initial `routes`, the `sensitive` case toggle,
234
+ * the `unmatched`/`unmethoded` default-responder overrides, and the
235
+ * Emitter pattern's `on`/`error` wiring
236
+ * @returns A {@link DispatcherInterface}
237
+ *
238
+ * @example
239
+ * ```ts
240
+ * import { createDispatcher } from '@src/core'
241
+ *
242
+ * const dispatcher = createDispatcher<{ readonly userId: string }>({
243
+ * routes: [
244
+ * { method: 'GET', path: '/health', handler: () => new Response('ok') },
245
+ * ],
246
+ * })
247
+ * const response = await dispatcher.handle(new Request('http://x/health'), { userId: 'me' })
248
+ * ```
249
+ */
250
+ export declare function createDispatcher<TState = undefined>(options?: DispatcherOptions<TState>): DispatcherInterface<TState>;
247
251
 
248
- /**
249
- * Create a {@link RouterInterface} — the pure path-matching + registry engine
250
- * shared by the browser `Navigator` and the core `Dispatcher`.
251
- *
252
- * @remarks
253
- * Prefer this over `new Router(...)` at call sites that only need the
254
- * interface; an entity that OWNS a `Router` internally (like `Dispatcher`)
255
- * still constructs `new Router(...)` directly.
256
- *
257
- * @typeParam Meta - The opaque payload each entry carries and a match returns
258
- * @param options - Optional initial `entries`, the `sensitive` case toggle
259
- * (default `true`), and a `key` dedup identity function
260
- * @returns A {@link RouterInterface}
261
- *
262
- * @example
263
- * ```ts
264
- * import { createRouter } from '@src/core'
265
- *
266
- * const router = createRouter<{ readonly page: string }>()
267
- * router.add({ path: '/users/:id', meta: { page: 'profile' } })
268
- * router.match('/users/7') // { path: '/users/:id', params: { id: '7' }, meta: { page: 'profile' } }
269
- * ```
270
- */
271
- export declare function createRouter<Meta>(options?: RouterOptions<Meta>): RouterInterface<Meta>;
252
+ /**
253
+ * Creates a {@link RouterInterface} — the pure path-matching + registry engine
254
+ * shared by the browser `Navigator` and the core `Dispatcher`.
255
+ *
256
+ * @remarks
257
+ * Prefer this over `new Router(...)` at call sites that only need the
258
+ * interface; an entity that OWNS a `Router` internally (like `Dispatcher`)
259
+ * still constructs `new Router(...)` directly.
260
+ *
261
+ * @typeParam Meta - The opaque payload each entry carries and a match returns
262
+ * @param options - Optional initial `entries`, the `sensitive` case toggle
263
+ * (default `true`), and a `key` dedup identity function
264
+ * @returns A {@link RouterInterface}
265
+ *
266
+ * @example Register and match
267
+ * ```ts
268
+ * import { createDispatcher, createRouter } from '@orkestrel/router'
269
+ *
270
+ * const router = createRouter<{ readonly page: string }>()
271
+ * router.add({ path: '/users/:id', meta: { page: 'profile' } })
272
+ * router.match('/users/7') // { path: '/users/:id', params: { id: '7' }, meta: { page: 'profile' } }
273
+ *
274
+ * const dispatcher = createDispatcher<{ readonly userId: string }>({
275
+ * routes: [
276
+ * {
277
+ * method: 'GET',
278
+ * path: '/users/:id',
279
+ * handler: (_request, context) => Response.json(context.params),
280
+ * },
281
+ * ],
282
+ * })
283
+ * const response = await dispatcher.handle(new Request('http://x/users/7'), { userId: 'me' })
284
+ * ```
285
+ */
286
+ export declare function createRouter<Meta>(options?: RouterOptions<Meta>): RouterInterface<Meta>;
272
287
 
273
- /**
274
- * URL-decode one captured param value, tolerating a malformed percent-escape —
275
- * the decode {@link matchPath} applies to each captured group.
276
- *
277
- * @remarks
278
- * A bad `%` sequence is not a reason to reject an otherwise-matching route, so
279
- * a `decodeURIComponent` that would throw falls back to the raw value
280
- * (mirroring the cookie / token boundary readers, AGENTS §14). Total — never
281
- * throws.
282
- *
283
- * @param value - The raw captured param value
284
- * @returns The URL-decoded value, or the raw value when decoding would throw
285
- *
286
- * @example
287
- * ```ts
288
- * decodeParam('a%2Fb') // 'a/b'
289
- * decodeParam('100%25') // '100%'
290
- * decodeParam('%') // '%' — malformed escape stays literal
291
- * ```
292
- */
293
- export declare function decodeParam(value: string): string;
288
+ /**
289
+ * Decodes one captured param value from a URL, tolerating a malformed percent-escape —
290
+ * the decode {@link matchPath} applies to each captured group.
291
+ *
292
+ * @remarks
293
+ * A bad `%` sequence is not a reason to reject an otherwise-matching route, so
294
+ * a `decodeURIComponent` that would throw falls back to the raw value
295
+ * (mirroring the cookie / token boundary readers). Total — never
296
+ * throws.
297
+ *
298
+ * @param value - The raw captured param value
299
+ * @returns The URL-decoded value, or the raw value when decoding would throw
300
+ *
301
+ * @example
302
+ * ```ts
303
+ * decodeParam('a%2Fb') // 'a/b'
304
+ * decodeParam('100%25') // '100%'
305
+ * decodeParam('%') // '%' — malformed escape stays literal
306
+ * ```
307
+ */
308
+ export declare function decodeParam(value: string): string;
294
309
 
295
- /**
296
- * The fetch-standard, method-dimensioned dispatch entity layers HTTP method
297
- * dispatch and web-standard `Request`/`Response` handling over one internal
298
- * `Router<RouteRecord<TState>>`. The core machine the eventual server face
299
- * (§7) and any fetch-native runtime consumes directly.
300
- *
301
- * @typeParam TState - The consumer's opaque per-request state type
302
- *
303
- * @remarks
304
- * - **Dedup by `method + canonicalizePath`.** The underlying `Router` is
305
- * constructed with a `key` function so registering the same method+path
306
- * twice REPLACES the prior route in place (§5.1).
307
- * - **Registration boundary guard (§14).** `add` validates each input's
308
- * `handler` (`isFunction`) and `method` (must be in {@link METHODS})
309
- * throws `TypeError` on a malformed registration; path validation is
310
- * delegated to the underlying `Router`'s own guard. `match`/`handle` stay
311
- * guard-free.
312
- * - **Auto-`HEAD` / auto-`OPTIONS`.** A `HEAD` request with no registered
313
- * `HEAD` route runs the matching `GET` handler and strips the response
314
- * body; an `OPTIONS` request with no registered `OPTIONS` route answers
315
- * `204` with a derived `Allow` header.
316
- * - **Handler throws propagate.** `handle` never invents an error boundary —
317
- * a handler throw reaches the caller uncaught (§5.1).
318
- * - **Emitter (§13).** Owns a `#emitter` for {@link DispatcherEventMap};
319
- * `match`/`miss` fire AFTER resolution, before the handler/responder runs.
320
- *
321
- * @example
322
- * ```ts
323
- * const dispatcher = new Dispatcher<{ readonly userId: string }>()
324
- * dispatcher.add({
325
- * method: 'GET',
326
- * path: '/users/:id',
327
- * handler: (request, context) => Response.json({ id: context.params.id }),
328
- * })
329
- * const response = await dispatcher.handle(new Request('http://x/users/7'), { userId: 'me' })
330
- * ```
331
- */
332
- export declare class Dispatcher<TState = undefined> implements DispatcherInterface<TState> {
333
- #private;
334
- readonly router: RouterInterface<RouteRecord<TState>>;
335
- constructor(options?: DispatcherOptions<TState>);
336
- get emitter(): EmitterInterface<DispatcherEventMap>;
337
- add<Path extends string>(input: RouteInput<Path, TState>): void;
338
- add(inputs: ReadonlyArray<RouteInput<string, TState>>): void;
339
- group(prefix: string): DispatchGroupInterface<TState>;
340
- match(method: Method, pathname: string): DispatchResult<TState>;
341
- handle(request: Request, state: TState): Promise<Response>;
342
- destroy(): void;
343
- }
310
+ /**
311
+ * Provides an identity pass-through for a {@link RouteInput} that pins its `Path`
312
+ * generic to the literal registration-site string, so `context.params` types
313
+ * correctly through {@link PathParams} without an explicit type argument.
314
+ *
315
+ * @remarks
316
+ * A bare object literal handed straight to {@link import('./types.js').DispatcherInterface}'s
317
+ * `add` already infers `Path` as a literal at that call site — but the moment
318
+ * the object is built through an intermediate binding (a local `const route =
319
+ * { method, path, handler }`) TypeScript widens `path` to `string` unless the
320
+ * binding's own type is pinned. Wrapping the literal in `defineRoute(...)`
321
+ * supplies that pin: its `const Path extends string` type parameter infers the
322
+ * NARROW literal from the call, and the function returns its input completely
323
+ * unchanged (same reference, no cloning, no validation) this is a
324
+ * compile-time typing aid only, not a construction step (contrast
325
+ * {@link import('./factories.js')} `create*` entity factories). A
326
+ * heterogeneous `RouteInput[]` built from several `defineRoute(...)` calls
327
+ * still widens each element's `Path` to `string` after collection into one array —
328
+ * the realistic ceiling this helper raises is PER-CALL typing at the
329
+ * registration site, not a stored, still-literal-typed record.
330
+ *
331
+ * @typeParam Path - The route path pattern literal (drives `context.params`
332
+ * through {@link PathParams})
333
+ * @typeParam TState - The consumer's opaque per-request state type
334
+ * @param input - The {@link RouteInput} to pass through unchanged
335
+ * @returns `input`, unchanged (same reference)
336
+ *
337
+ * @example
338
+ * ```ts
339
+ * const input = defineRoute({
340
+ * method: 'GET',
341
+ * path: '/users/:id',
342
+ * handler: (_request, context) => new Response(context.params.id), // typed string
343
+ * })
344
+ * dispatcher.add(input)
345
+ * ```
346
+ */
347
+ export declare function defineRoute<const Path extends string, TState = undefined>(input: RouteInput<Path, TState>): RouteInput<Path, TState>;
344
348
 
345
- /**
346
- * The `Dispatcher`'s event map (AGENTS §13)the two dispatch-outcome
347
- * signals a consumer can observe alongside the return value of `handle`.
348
- *
349
- * @remarks
350
- * - `match` — emitted on every dispatch that resolves to a handler
351
- * (including the auto-`HEAD`/auto-`OPTIONS` derived cases): the request
352
- * `method` and the winning `pattern`.
353
- * - `miss` — emitted on every non-matching dispatch: the RAW request
354
- * `method` (a plain `string`, not narrowed to {@link Method} — an unknown
355
- * verb like `PURGE` is observable here exactly as sent, never coerced),
356
- * the raw `pathname`, and which tier missed (`'unmatched'` — nothing
357
- * matched the path at all; `'unmethoded'` the path matched but not the
358
- * method, including an unknown verb against a path with other registered
359
- * methods).
360
- */
361
- export declare type DispatcherEventMap = {
362
- readonly match: readonly [method: Method, pattern: string];
363
- readonly miss: readonly [method: string, pathname: string, reason: 'unmatched' | 'unmethoded'];
364
- };
349
+ /**
350
+ * Represents the fetch-standard, method-dimensioned dispatch entitylayers HTTP
351
+ * method dispatch and web-standard `Request`/`Response` handling over one internal
352
+ * `Router<RouteRecord<TState>>`. The core machine the server face and any
353
+ * fetch-native runtime consume directly.
354
+ *
355
+ * @typeParam TState - The consumer's opaque per-request state type
356
+ *
357
+ * @remarks
358
+ * - **Dedup by `method + canonicalizePath`.** The underlying `Router` is
359
+ * constructed with a `key` function so registering the same method+path
360
+ * twice REPLACES the prior route in place.
361
+ * - **Registration boundary guard.** `add` validates each input's
362
+ * `handler` (`isFunction`) and `method` (must be in {@link METHODS})
363
+ * throws a `ContractError` on a malformed registration; path validation is
364
+ * delegated to the underlying `Router`'s own guard. `match`/`handle` stay
365
+ * guard-free.
366
+ * - **Auto-`HEAD` / auto-`OPTIONS`.** A `HEAD` request with no registered
367
+ * `HEAD` route runs the matching `GET` handler and strips the response
368
+ * body; an `OPTIONS` request with no registered `OPTIONS` route answers
369
+ * `204` with a derived `Allow` header.
370
+ * - **Handler throws propagate.** `handle` never invents an error boundary —
371
+ * a handler throw reaches the caller uncaught.
372
+ * - **Emitter.** Owns a `#emitter` for {@link DispatcherEventMap};
373
+ * `match`/`miss` fire AFTER resolution, before the handler/responder runs.
374
+ *
375
+ * @example
376
+ * ```ts
377
+ * const dispatcher = new Dispatcher<{ readonly userId: string }>()
378
+ * dispatcher.add({
379
+ * method: 'GET',
380
+ * path: '/users/:id',
381
+ * handler: (request, context) => Response.json({ id: context.params.id }),
382
+ * })
383
+ * const response = await dispatcher.handle(new Request('http://x/users/7'), { userId: 'me' })
384
+ * ```
385
+ */
386
+ export declare class Dispatcher<TState = undefined> implements DispatcherInterface<TState> {
387
+ #private;
388
+ constructor(options?: DispatcherOptions<TState>);
389
+ get router(): RouterInterface<RouteRecord<TState>>;
390
+ get emitter(): EmitterInterface<DispatcherEventMap>;
391
+ add<Path extends string>(input: RouteInput<Path, TState>): void;
392
+ add(inputs: ReadonlyArray<RouteInput<string, TState>>): void;
393
+ group(prefix: string): DispatchGroupInterface<TState>;
394
+ match(method: Method, pathname: string): DispatchResult<TState>;
395
+ handle(request: Request, state: TState): Promise<Response>;
396
+ destroy(): void;
397
+ }
365
398
 
366
- /**
367
- * The fetch-standard, method-dimensioned dispatch entity contract (the §4.5
368
- * behavioral-interface role for the one-class-per-file `Dispatcher`). Layers
369
- * HTTP method dispatch and web-standard `Request`/`Response` handling over a
370
- * single internal `Router<RouteRecord<TState>>`.
371
- *
372
- * @typeParam TState - The consumer's opaque per-request state type, threaded
373
- * into every {@link RouteContext} (default `undefined` for stateless use)
374
- *
375
- * @remarks
376
- * - `router` the underlying registry, exposed READONLY for introspection
377
- * (the same object `add`/`group`/`match` operate on).
378
- * - `emitter` the AGENTS §13 observable surface for {@link DispatcherEventMap}.
379
- * - `add(input)` / `add(inputs)` — register ONE / MANY {@link RouteInput}s
380
- * (§9.2 batch); throws `TypeError` on a malformed registration (a
381
- * non-`/`-prefixed path, a non-function handler, or a method outside
382
- * {@link import('./constants.js').METHODS}) — the construction/registration
383
- * boundary guard (§14); `match`/`handle` hot paths carry zero guards.
384
- * - `group(prefix)` a {@link DispatchGroupInterface} scoped under `prefix`.
385
- * - `match(method, pathname)` the raw {@link DispatchResult} for a method +
386
- * pathname pair, with no `Request`/`Response` involvement — the pure
387
- * decision `handle` builds its response from.
388
- * - `handle(request, state)` — the full dispatch: parses `request.url`,
389
- * calls `match`, and either invokes the winning handler (auto-stripping
390
- * the body for a derived `HEAD`, auto-answering a derived `OPTIONS` with
391
- * the `Allow` set), or invokes the `unmatched`/`unmethoded` responder.
392
- * Emits `match`/`miss` accordingly. A handler throw propagates uncaught.
393
- * - `destroy()` — tears down the `#emitter` (AGENTS §13); the underlying
394
- * router is left registered (not cleared) so introspection remains valid
395
- * after destroy.
396
- */
397
- export declare interface DispatcherInterface<TState = undefined> {
398
- readonly router: RouterInterface<RouteRecord<TState>>;
399
- readonly emitter: EmitterInterface<DispatcherEventMap>;
400
- add<Path extends string>(input: RouteInput<Path, TState>): void;
401
- add(inputs: ReadonlyArray<RouteInput<string, TState>>): void;
402
- group(prefix: string): DispatchGroupInterface<TState>;
403
- match(method: Method, pathname: string): DispatchResult<TState>;
404
- handle(request: Request, state: TState): Promise<Response>;
405
- destroy(): void;
406
- }
399
+ /**
400
+ * Represents the `Dispatcher`'s event map the dispatch-outcome
401
+ * signals a consumer can observe alongside the return value of `handle`.
402
+ *
403
+ * @remarks
404
+ * - `match` — emitted on every dispatch the dispatcher answers, the derived
405
+ * `HEAD` and `OPTIONS` cases included: the request `method` and the winning
406
+ * `pattern` (for a derived `OPTIONS` answer, the most-specific pattern the
407
+ * pathname resolved to).
408
+ * - `miss` — emitted on every non-matching dispatch: the RAW request
409
+ * `method` (a plain `string`, not narrowed to {@link Method} an unknown
410
+ * verb like `PURGE` is observable here exactly as sent, never coerced),
411
+ * the raw `pathname`, and which tier missed (`'unmatched'` nothing
412
+ * matched the path at all; `'unmethoded'` — the path matched but not the
413
+ * method, including an unknown verb against a path with other registered
414
+ * methods).
415
+ */
416
+ export declare type DispatcherEventMap = {
417
+ readonly match: readonly [method: Method, pattern: string];
418
+ readonly miss: readonly [method: string, pathname: string, status: 'unmatched' | 'unmethoded'];
419
+ };
407
420
 
408
- /**
409
- * Options for `createDispatcher` initial routes, case sensitivity, the two
410
- * default-responder overrides, and the AGENTS §13 emitter wiring.
411
- *
412
- * @typeParam TState - The consumer's opaque per-request state type
413
- *
414
- * @remarks
415
- * - `routes` the initial route inputs to register, equivalent to a bare
416
- * `createDispatcher()` followed by `add(routes)`. Omitted ⇒ no routes.
417
- * - `sensitive` — forwarded to the underlying `Router` (default `true`, §4).
418
- * - `unmatched` the responder invoked when nothing matches the pathname at
419
- * all (default: a `404` `Response`).
420
- * - `unmethoded` — the responder invoked when the pathname matches but not
421
- * the method, given the derived `Allow` set (default: a `405` `Response`
422
- * with an `Allow` header).
423
- * - `on` initial `DispatcherEventMap` listeners (AGENTS §8/§13).
424
- * - `error` the emitter's own listener-error handler (AGENTS §13),
425
- * forwarded alongside `on`.
426
- */
427
- export declare interface DispatcherOptions<TState> {
428
- readonly routes?: ReadonlyArray<RouteInput<string, TState>>;
429
- readonly sensitive?: boolean;
430
- readonly unmatched?: (request: Request) => Response | Promise<Response>;
431
- readonly unmethoded?: (request: Request, allow: readonly Method[]) => Response | Promise<Response>;
432
- readonly on?: EmitterHooks<DispatcherEventMap>;
433
- readonly error?: EmitterErrorHandler;
434
- }
421
+ /**
422
+ * Represents the fetch-standard, method-dimensioned dispatch entity contract (the
423
+ * behavioral-interface role for the one-class-per-file `Dispatcher`). Layers
424
+ * HTTP method dispatch and web-standard `Request`/`Response` handling over a
425
+ * single internal `Router<RouteRecord<TState>>`.
426
+ *
427
+ * @typeParam TState - The consumer's opaque per-request state type, threaded
428
+ * into every {@link RouteContext} (default `undefined` for stateless use)
429
+ *
430
+ * @remarks
431
+ * Registration is the guarded boundary: `add` throws on a malformed registration,
432
+ * while the `match` and `handle` hot paths carry no guard of their own.
433
+ */
434
+ export declare interface DispatcherInterface<TState = undefined> {
435
+ /**
436
+ * Holds the underlying registry, exposed readonly for introspection — the same
437
+ * object `add`, `group`, and `match` operate on.
438
+ */
439
+ readonly router: RouterInterface<RouteRecord<TState>>;
440
+ /** Holds the observable surface for {@link DispatcherEventMap}. */
441
+ readonly emitter: EmitterInterface<DispatcherEventMap>;
442
+ /**
443
+ * Registers one route input, or many in one call (batch registration); throws a
444
+ * `ContractError` on a malformed registration.
445
+ *
446
+ * @remarks
447
+ * A registration is malformed when its path is not `/`-prefixed, its handler is not
448
+ * a function, or its method sits outside {@link import('./constants.js').METHODS}.
449
+ * Path validation is delegated to the underlying router's own guard.
450
+ */
451
+ add<Path extends string>(input: RouteInput<Path, TState>): void;
452
+ add(inputs: ReadonlyArray<RouteInput<string, TState>>): void;
453
+ /** Returns a prefix-scoped registration handle over this dispatcher. */
454
+ group(prefix: string): DispatchGroupInterface<TState>;
455
+ /**
456
+ * Decides the raw {@link DispatchResult} for a method and pathname pair, with no
457
+ * `Request` or `Response` involvement — the pure decision `handle` builds its
458
+ * response from.
459
+ */
460
+ match(method: Method, pathname: string): DispatchResult<TState>;
461
+ /**
462
+ * Runs the full dispatch: parses the request URL, matches, and invokes either the
463
+ * winning handler or the `unmatched`/`unmethoded` responder.
464
+ *
465
+ * @remarks
466
+ * A derived `HEAD` runs the matching `GET` handler with the response body stripped,
467
+ * and a derived `OPTIONS` answers with the `Allow` set. Emits `match` or `miss`
468
+ * accordingly. A handler throw propagates uncaught.
469
+ */
470
+ handle(request: Request, state: TState): Promise<Response>;
471
+ /**
472
+ * Tears down the emitter; the underlying router is left registered rather than
473
+ * cleared, so introspection stays valid afterwards.
474
+ */
475
+ destroy(): void;
476
+ }
435
477
 
436
- /**
437
- * A prefix-scoped registration handle over a
438
- * {@link import('./Dispatcher.js').Dispatcher} the method-dimensioned
439
- * counterpart of `Group` (`Group.ts`).
440
- *
441
- * @typeParam TState - The consumer's opaque per-request state type, matching
442
- * the owning dispatcher
443
- *
444
- * @remarks
445
- * Every `add` composes `input.path` via {@link joinPaths} against
446
- * `this.prefix` and forwards to the OWNING dispatcher's `add` (its own §14
447
- * boundary guard still applies). Pure string composition (§4.2.2) — no
448
- * independent state or storage.
449
- *
450
- * @example
451
- * ```ts
452
- * import { Dispatcher } from '@src/core'
453
- *
454
- * const dispatcher = new Dispatcher()
455
- * const api = dispatcher.group('/api')
456
- * api.add({ method: 'GET', path: '/users', handler: () => new Response('ok') })
457
- * ```
458
- */
459
- export declare class DispatchGroup<TState> implements DispatchGroupInterface<TState> {
460
- #private;
461
- readonly prefix: string;
462
- constructor(parent: DispatcherInterface<TState>, prefix: string);
463
- add<Path extends string>(input: RouteInput<Path, TState>): void;
464
- add(inputs: ReadonlyArray<RouteInput<string, TState>>): void;
465
- group(prefix: string): DispatchGroupInterface<TState>;
466
- }
478
+ /**
479
+ * Represents the options for `createDispatcher` — initial routes, case sensitivity, the
480
+ * default-responder overrides, and the Emitter pattern's wiring.
481
+ *
482
+ * @typeParam TState - The consumer's opaque per-request state type
483
+ *
484
+ * @remarks
485
+ * - `routes` — the initial route inputs to register, equivalent to a bare
486
+ * `createDispatcher()` followed by `add(routes)`. Omitted ⇒ no routes.
487
+ * - `sensitive` forwarded to the underlying `Router` (default `true`).
488
+ * - `unmatched` the responder invoked when nothing matches the pathname at
489
+ * all (default: a `404` `Response`).
490
+ * - `unmethoded` the responder invoked when the pathname matches but not
491
+ * the method, given the derived `Allow` set (default: a `405` `Response`
492
+ * with an `Allow` header).
493
+ * - `on` — initial `DispatcherEventMap` listeners (the Emitter pattern's
494
+ * similar-surface pin).
495
+ * - `error` — the emitter's own listener-error handler, forwarded alongside
496
+ * `on`.
497
+ */
498
+ export declare interface DispatcherOptions<TState> {
499
+ readonly routes?: ReadonlyArray<RouteInput<string, TState>>;
500
+ readonly sensitive?: boolean;
501
+ readonly unmatched?: (request: Request) => Response | Promise<Response>;
502
+ readonly unmethoded?: (request: Request, allow: readonly Method[]) => Response | Promise<Response>;
503
+ readonly on?: EmitterHooks<DispatcherEventMap>;
504
+ readonly error?: EmitterErrorHandler;
505
+ }
467
506
 
468
- /**
469
- * A prefix-scoped registration handle over a {@link DispatcherInterface} —
470
- * the method-dimensioned counterpart of {@link GroupInterface}.
471
- *
472
- * @typeParam TState - The consumer's opaque per-request state type, matching
473
- * the owning dispatcher
474
- *
475
- * @remarks
476
- * - `prefix` — the path prefix this group prepends to every route it
477
- * registers (and to every nested group's own prefix).
478
- * - `add(input)` / `add(inputs)` register ONE / MANY {@link RouteInput}s on
479
- * the OWNING dispatcher, each input's `path` composed as
480
- * `prefix + input.path` (§9.2 batch, mirroring
481
- * {@link DispatcherInterface.add}).
482
- * - `group(prefix)` — a nested group whose prefix is `this.prefix + prefix`.
483
- */
484
- export declare interface DispatchGroupInterface<TState> {
485
- readonly prefix: string;
486
- add<Path extends string>(input: RouteInput<Path, TState>): void;
487
- add(inputs: ReadonlyArray<RouteInput<string, TState>>): void;
488
- group(prefix: string): DispatchGroupInterface<TState>;
489
- }
507
+ /**
508
+ * Represents a prefix-scoped registration handle over a
509
+ * {@link import('./Dispatcher.js').Dispatcher} — the method-dimensioned counterpart of
510
+ * `Group`.
511
+ *
512
+ * @typeParam TState - The consumer's opaque per-request state type, matching
513
+ * the owning dispatcher
514
+ *
515
+ * @remarks
516
+ * Every `add` composes `input.path` through {@link joinPaths} against
517
+ * `this.prefix` and forwards to the OWNING dispatcher's `add` (its own
518
+ * registration boundary guard still applies). Pure string composition — no
519
+ * independent state or storage.
520
+ *
521
+ * @example
522
+ * ```ts
523
+ * import { Dispatcher } from '@src/core'
524
+ *
525
+ * const dispatcher = new Dispatcher()
526
+ * const api = dispatcher.group('/api')
527
+ * api.add({ method: 'GET', path: '/users', handler: () => new Response('ok') })
528
+ * ```
529
+ */
530
+ export declare class DispatchGroup<TState> implements DispatchGroupInterface<TState> {
531
+ #private;
532
+ readonly prefix: string;
533
+ constructor(parent: DispatcherInterface<TState>, prefix: string);
534
+ add<Path extends string>(input: RouteInput<Path, TState>): void;
535
+ add(inputs: ReadonlyArray<RouteInput<string, TState>>): void;
536
+ group(prefix: string): DispatchGroupInterface<TState>;
537
+ }
490
538
 
491
- /**
492
- * The outcome of {@link DispatcherInterface.match} — a discriminated union
493
- * over the three dispatch tiers: a full hit, a path-matches-but-method-
494
- * doesn't (405 territory), or nothing matched at all (404 territory).
495
- *
496
- * @typeParam TState - The consumer's opaque per-request state type
497
- *
498
- * @remarks
499
- * - `'matched'` carries the winning {@link RouterMatch} (its `meta` is a
500
- * {@link RouteRecord}).
501
- * - `'unmethoded'` the pathname matched at least one entry but none for
502
- * the requested method; `allow` is the derived `Allow` method set (from
503
- * `router.entries(pathname)`).
504
- * - `'unmatched'` no registered pattern matches the pathname at all.
505
- */
506
- export declare type DispatchResult<TState> = {
507
- readonly status: 'matched';
508
- readonly match: RouterMatch<RouteRecord<TState>>;
509
- } | {
510
- readonly status: 'unmethoded';
511
- readonly allow: readonly Method[];
512
- } | {
513
- readonly status: 'unmatched';
514
- };
539
+ /**
540
+ * Represents a prefix-scoped registration handle over a {@link DispatcherInterface} —
541
+ * the method-dimensioned counterpart of {@link GroupInterface}.
542
+ *
543
+ * @typeParam TState - The consumer's opaque per-request state type, matching
544
+ * the owning dispatcher
545
+ *
546
+ * @remarks
547
+ * Nesting composes prefixes left to right with no depth limit.
548
+ */
549
+ export declare interface DispatchGroupInterface<TState> {
550
+ /**
551
+ * Holds the path prefix this group prepends to every route it registers, and to
552
+ * every nested group's own prefix.
553
+ */
554
+ readonly prefix: string;
555
+ /**
556
+ * Registers one route input, or many in one call, on the owning dispatcher with this
557
+ * group's prefix composed onto each path.
558
+ *
559
+ * @remarks
560
+ * Batch registration mirrors {@link DispatcherInterface.add}, and the owning
561
+ * dispatcher's own registration guard still applies.
562
+ */
563
+ add<Path extends string>(input: RouteInput<Path, TState>): void;
564
+ add(inputs: ReadonlyArray<RouteInput<string, TState>>): void;
565
+ /** Returns a nested group whose prefix is this prefix followed by the given one. */
566
+ group(prefix: string): DispatchGroupInterface<TState>;
567
+ }
515
568
 
516
- /**
517
- * Escape every regex metacharacter in a literal string so it can be embedded
518
- * inside a larger `RegExp` source without being interpreted as syntax.
519
- *
520
- * @remarks
521
- * {@link compilePath} escapes the literal segments of a route pattern with this
522
- * before splicing in `:name` / `*name` capture groups, so a path like
523
- * `/files/:name.json` matches the `.` literally rather than as "any character".
524
- * Pure and total never throws.
525
- *
526
- * @param value - The literal string to escape
527
- * @returns `value` with every regex metacharacter backslash-escaped
528
- *
529
- * @example
530
- * ```ts
531
- * escapeRegExp('a.b+c') // 'a\\.b\\+c'
532
- * new RegExp(`^${escapeRegExp('a.b')}$`).test('a.b') // true
533
- * new RegExp(`^${escapeRegExp('a.b')}$`).test('axb') // false
534
- * ```
535
- */
536
- export declare function escapeRegExp(value: string): string;
569
+ /**
570
+ * Represents the outcome of {@link DispatcherInterface.match} a discriminated
571
+ * union over the dispatch tiers: a full hit, a path that matches with no route for
572
+ * the method (405 territory), or nothing matched at all (404 territory).
573
+ *
574
+ * @typeParam TState - The consumer's opaque per-request state type
575
+ *
576
+ * @remarks
577
+ * - `'matched'`carries the winning {@link RouterMatch} (its `meta` is a
578
+ * {@link RouteRecord}).
579
+ * - `'unmethoded'` the pathname matched at least one entry but none for
580
+ * the requested method; `allow` is the derived `Allow` method set (from
581
+ * `router.entries(pathname)`).
582
+ * - `'unmatched'` — no registered pattern matches the pathname at all.
583
+ */
584
+ export declare type DispatchResult<TState> = {
585
+ readonly status: 'matched';
586
+ readonly match: RouterMatch<RouteRecord<TState>>;
587
+ } | {
588
+ readonly status: 'unmethoded';
589
+ readonly allow: readonly Method[];
590
+ } | {
591
+ readonly status: 'unmatched';
592
+ };
537
593
 
538
- /**
539
- * A prefix-scoped registration handle over a {@link import('./Router.js').Router}
540
- * pure string composition (AGENTS §4.2.2), no independent state or storage.
541
- *
542
- * @typeParam Meta - The entry payload type, matching the owning router
543
- *
544
- * @remarks
545
- * Every `add` composes `entry.path` as `joinPaths(prefix, entry.path)` and
546
- * forwards to the OWNING router, so grouped routes land in the SAME registry.
547
- * `group(prefix)` nests, composing prefixes via {@link joinPaths}.
548
- *
549
- * @example
550
- * ```ts
551
- * import { Router } from '@src/core'
552
- *
553
- * const router = new Router<{ readonly page: string }>()
554
- * const api = router.group('/api')
555
- * api.add({ path: '/users', meta: { page: 'list' } })
556
- * router.match('/api/users')?.path // '/api/users'
557
- * ```
558
- */
559
- export declare class Group<Meta> implements GroupInterface<Meta> {
560
- #private;
561
- readonly prefix: string;
562
- constructor(parent: RouterInterface<Meta>, prefix: string);
563
- add(entry: RouteEntry<Meta>): void;
564
- add(entries: ReadonlyArray<RouteEntry<Meta>>): void;
565
- group(prefix: string): GroupInterface<Meta>;
566
- }
594
+ /**
595
+ * Escapes every regex metacharacter in a literal string so it can be embedded
596
+ * inside a larger `RegExp` source without being interpreted as syntax.
597
+ *
598
+ * @remarks
599
+ * {@link compilePath} escapes the literal segments of a route pattern with this
600
+ * before splicing in `:name` / `*name` capture groups, so a path like
601
+ * `/files/:name.json` matches the `.` literally rather than as "any character".
602
+ * Pure and total never throws.
603
+ *
604
+ * @param value - The literal string to escape
605
+ * @returns `value` with every regex metacharacter backslash-escaped
606
+ *
607
+ * @example
608
+ * ```ts
609
+ * escapeRegExp('a.b+c') // 'a\\.b\\+c'
610
+ * new RegExp(`^${escapeRegExp('a.b')}$`).test('a.b') // true
611
+ * new RegExp(`^${escapeRegExp('a.b')}$`).test('axb') // false
612
+ * ```
613
+ */
614
+ export declare function escapeRegExp(value: string): string;
567
615
 
568
- /**
569
- * A prefix-scoped registration handle over a {@link RouterInterface} — pure
570
- * string composition (§4.2.2), no independent state or storage.
571
- *
572
- * @typeParam Meta - The entry payload type, matching the owning router
573
- *
574
- * @remarks
575
- * - `prefix` the path prefix this group prepends to every entry it
576
- * registers (and to every nested group's own prefix).
577
- * - `add(entry)` / `add(entries)` register ONE / MANY entries on the
578
- * OWNING router, each entry's `path` composed as `prefix + entry.path`
579
- * (§9.2 batch, mirroring {@link RouterInterface.add}).
580
- * - `group(prefix)` — a nested group whose prefix is `this.prefix + prefix`;
581
- * nesting composes prefixes left to right with no depth limit.
582
- */
583
- export declare interface GroupInterface<Meta> {
584
- readonly prefix: string;
585
- add(entry: RouteEntry<Meta>): void;
586
- add(entries: ReadonlyArray<RouteEntry<Meta>>): void;
587
- group(prefix: string): GroupInterface<Meta>;
588
- }
616
+ /**
617
+ * Represents a prefix-scoped registration handle over a {@link import('./Router.js').Router} —
618
+ * pure string composition, no independent state or storage.
619
+ *
620
+ * @typeParam Meta - The entry payload type, matching the owning router
621
+ *
622
+ * @remarks
623
+ * Every `add` composes `entry.path` as `joinPaths(prefix, entry.path)` and
624
+ * forwards to the OWNING router, so grouped routes land in the SAME registry.
625
+ * `group(prefix)` nests, composing prefixes through {@link joinPaths}.
626
+ *
627
+ * @example
628
+ * ```ts
629
+ * import { Router } from '@src/core'
630
+ *
631
+ * const router = new Router<{ readonly page: string }>()
632
+ * const api = router.group('/api')
633
+ * api.add({ path: '/users', meta: { page: 'list' } })
634
+ * router.match('/api/users')?.path // '/api/users'
635
+ * ```
636
+ */
637
+ export declare class Group<Meta> implements GroupInterface<Meta> {
638
+ #private;
639
+ readonly prefix: string;
640
+ constructor(parent: RouterInterface<Meta>, prefix: string);
641
+ add(entry: RouteEntry<Meta>): void;
642
+ add(entries: ReadonlyArray<RouteEntry<Meta>>): void;
643
+ group(prefix: string): GroupInterface<Meta>;
644
+ }
589
645
 
590
- /**
591
- * The identifier CONTINUATION characters after the firstmirrors the
592
- * runtime classifier's `[A-Za-z0-9_]*` tail class.
593
- */
594
- export declare type IdentifierChar = IdentifierStartChar | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
646
+ /**
647
+ * Represents a prefix-scoped registration handle over a {@link RouterInterface} pure
648
+ * string composition, no independent state or storage.
649
+ *
650
+ * @typeParam Meta - The entry payload type, matching the owning router
651
+ *
652
+ * @remarks
653
+ * Nesting composes prefixes left to right with no depth limit.
654
+ */
655
+ export declare interface GroupInterface<Meta> {
656
+ /**
657
+ * Holds the path prefix this group prepends to every entry it registers, and to
658
+ * every nested group's own prefix.
659
+ */
660
+ readonly prefix: string;
661
+ /**
662
+ * Registers one entry, or many in one call, on the owning router with this group's
663
+ * prefix composed onto each path.
664
+ *
665
+ * @remarks
666
+ * Batch registration mirrors {@link RouterInterface.add}, and the owning router's own
667
+ * registration guard still applies.
668
+ */
669
+ add(entry: RouteEntry<Meta>): void;
670
+ add(entries: ReadonlyArray<RouteEntry<Meta>>): void;
671
+ /** Returns a nested group whose prefix is this prefix followed by the given one. */
672
+ group(prefix: string): GroupInterface<Meta>;
673
+ }
595
674
 
596
- export declare type IdentifierHead<S extends string> = S extends `${infer Head}${infer Tail}` ? Head extends IdentifierStartChar ? TakeIdentifierTail<Tail, Head> : '' : '';
675
+ /**
676
+ * Names the identifier continuation characters after the first — mirrors the
677
+ * runtime classifier's `[A-Za-z0-9_]*` tail class.
678
+ */
679
+ export declare type IdentifierChar = IdentifierStartChar | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
597
680
 
598
- /**
599
- * The identifier START characters an identifier-grammar param name may
600
- * begin with mirrors the runtime classifier's `[A-Za-z_]` head class
601
- * (`classifySegment` / `compilePath`, `helpers.ts`).
602
- */
603
- export declare type IdentifierStartChar = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z' | '_';
681
+ /**
682
+ * Captures the identifier at the front of a string literal, or an empty string when the
683
+ * literal does not begin with an identifier-start char.
684
+ *
685
+ * @typeParam S - The string literal to read the head from
686
+ *
687
+ * @remarks
688
+ * The type-level mirror of the runtime head match anchored on an identifier-start char:
689
+ * an {@link IdentifierStartChar} opens the run and {@link TakeIdentifierTail} consumes
690
+ * the rest of it.
691
+ */
692
+ export declare type IdentifierHead<S extends string> = S extends `${infer Head}${infer Tail}` ? Head extends IdentifierStartChar ? TakeIdentifierTail<Tail, Head> : '' : '';
604
693
 
605
- /**
606
- * Join a group prefix and a route path into one `/`-prefixed path, normalizing
607
- * duplicate or missing joining slashes.
608
- *
609
- * @remarks
610
- * {@link import('./types.js').GroupInterface} / {@link import('./types.js').DispatchGroupInterface}
611
- * compose a prefix with each registered entry's path this way — pure string
612
- * composition (§4.2.2), no independent state. Both a duplicated slash
613
- * (`'/api/'` + `'/users'`) and a missing one (`'/api'` + `'users'`) normalize
614
- * to a single joining slash. An empty `prefix` returns `path` unchanged (after
615
- * ensuring a leading slash); an empty `path` returns `prefix` unchanged.
616
- * Pure and total.
617
- *
618
- * @param prefix - The group prefix (e.g. `/api`)
619
- * @param path - The route path being joined under the prefix (e.g. `/users`)
620
- * @returns The joined `/`-prefixed path
621
- *
622
- * @example
623
- * ```ts
624
- * joinPaths('/api', '/users') // '/api/users'
625
- * joinPaths('/api/', '/users') // '/api/users'
626
- * joinPaths('/api', 'users') // '/api/users'
627
- * joinPaths('', '/users') // '/users'
628
- * joinPaths('/api', '') // '/api'
629
- * ```
630
- */
631
- export declare function joinPaths(prefix: string, path: string): string;
694
+ /**
695
+ * Names the identifier start characters an identifier-grammar param name may begin
696
+ * with mirrors the runtime classifier's `[A-Za-z_]` head class, the one
697
+ * `classifySegment` and `compilePath` share.
698
+ */
699
+ export declare type IdentifierStartChar = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z' | '_';
632
700
 
633
- /**
634
- * Extract the URL-decoded params a compiled path captures from a concrete
635
- * pathname, or `undefined` when the pathname does not match.
636
- *
637
- * @remarks
638
- * Runs the {@link CompiledPath} `regex` against `pathname` (a single `exec`); a
639
- * miss returns `undefined`. On a hit it walks `params` POSITIONALLYthe
640
- * `n`-th param name pairs with the `n`-th capture group — and URL-decodes each
641
- * value with {@link decodeParam}. Returns a frozen `name value` record (empty
642
- * for a parameterless path). Total never throws.
643
- *
644
- * @param compiled - The {@link CompiledPath} from {@link compilePath}
645
- * @param pathname - The concrete request pathname to match (e.g. `/users/7`)
646
- * @returns The decoded params on a hit, or `undefined` on a miss
647
- *
648
- * @example
649
- * ```ts
650
- * const compiled = compilePath('/users/:id')
651
- * matchPath(compiled, '/users/7') // { id: '7' }
652
- * matchPath(compiled, '/users/a%2Fb') // { id: 'a/b' } — decoded
653
- * matchPath(compiled, '/posts/7') // undefined
654
- * ```
655
- */
656
- export declare function matchPath(compiled: CompiledPath, pathname: string): Readonly<Record<string, string>> | undefined;
701
+ /**
702
+ * Joins a group prefix and a route path into one `/`-prefixed path, normalizing
703
+ * duplicate or missing joining slashes.
704
+ *
705
+ * @remarks
706
+ * {@link import('./types.js').GroupInterface} / {@link import('./types.js').DispatchGroupInterface}
707
+ * compose a prefix with each registered entry's path this waypure string
708
+ * composition, no independent state. Both a duplicated slash
709
+ * (`'/api/'` + `'/users'`) and a missing one (`'/api'` + `'users'`) normalize
710
+ * to a single joining slash. An empty `prefix` returns `path` unchanged (after
711
+ * ensuring a leading slash); an empty `path` returns `prefix` unchanged.
712
+ * Pure and total.
713
+ *
714
+ * @param prefix - The group prefix (for example `/api`)
715
+ * @param path - The route path being joined under the prefix (for example `/users`)
716
+ * @returns The joined `/`-prefixed path
717
+ *
718
+ * @example
719
+ * ```ts
720
+ * joinPaths('/api', '/users') // '/api/users'
721
+ * joinPaths('/api/', '/users') // '/api/users'
722
+ * joinPaths('/api', 'users') // '/api/users'
723
+ * joinPaths('', '/users') // '/users'
724
+ * joinPaths('/api', '') // '/api'
725
+ * ```
726
+ */
727
+ export declare function joinPaths(prefix: string, path: string): string;
657
728
 
658
- /**
659
- * The seven HTTP methods a {@link DispatcherInterface} dimensions dispatch
660
- * over the value-level counterpart is {@link import('./constants.js').METHODS}.
661
- *
662
- * @remarks
663
- * `HEAD` is a valid explicit registration even though a `GET` route already
664
- * auto-answers `HEAD` (§5.1 dispatch semantics) an explicit `HEAD` handler
665
- * always takes precedence over the derived one.
666
- */
667
- export declare type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
729
+ /**
730
+ * Extracts the URL-decoded params a compiled path captures from a concrete
731
+ * pathname, or `undefined` when the pathname does not match.
732
+ *
733
+ * @remarks
734
+ * Runs the {@link CompiledPath} `regex` against `pathname` (a single `exec`); a
735
+ * miss returns `undefined`. On a hit it walks `params` POSITIONALLY — the
736
+ * `n`-th param name pairs with the `n`-th capture group — and URL-decodes each
737
+ * value with {@link decodeParam}. Returns a frozen `name → value` record (empty
738
+ * for a parameterless path). Total never throws.
739
+ *
740
+ * @param compiled - The {@link CompiledPath} from {@link compilePath}
741
+ * @param pathname - The concrete request pathname to match (for example `/users/7`)
742
+ * @returns The decoded params on a hit, or `undefined` on a miss
743
+ *
744
+ * @example
745
+ * ```ts
746
+ * const compiled = compilePath('/users/:id')
747
+ * matchPath(compiled, '/users/7') // { id: '7' }
748
+ * matchPath(compiled, '/users/a%2Fb') // { id: 'a/b' } — decoded
749
+ * matchPath(compiled, '/posts/7') // undefined
750
+ * ```
751
+ */
752
+ export declare function matchPath(compiled: CompiledPath, pathname: string): Readonly<Record<string, string>> | undefined;
668
753
 
669
- /**
670
- * The complete set of HTTP methods a {@link import('./types.js').Dispatcher}
671
- * registers routes under — backs the registration guard (`add` rejects any
672
- * `method` outside this set) and the auto-`OPTIONS` `Allow` derivation.
673
- *
674
- * @remarks
675
- * A `ReadonlySet` of the seven {@link import('./types.js').Method} literals:
676
- * `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`. `HEAD` is
677
- * included even though it is never required at registration (a `GET` route
678
- * auto-answers `HEAD`) — it is still a valid method to register explicitly.
679
- *
680
- * @example
681
- * ```ts
682
- * METHODS.has('GET') // true
683
- * METHODS.has('TRACE') // false
684
- * ```
685
- */
686
- export declare const METHODS: ReadonlySet<string>;
754
+ /**
755
+ * Names the HTTP methods a {@link DispatcherInterface} dimensions dispatch over —
756
+ * derived from {@link import('./constants.js').METHOD_LIST}, whose membership
757
+ * counterpart is {@link import('./constants.js').METHODS}.
758
+ *
759
+ * @remarks
760
+ * Resolves to `'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' |
761
+ * 'OPTIONS'`. Deriving the union from the constant keeps one home for the
762
+ * method set: a verb added there widens this type, the membership set, and the
763
+ * `parseMethod` narrowing together. `HEAD` is a valid explicit registration
764
+ * even though a `GET` route already auto-answers `HEAD` — an explicit
765
+ * `HEAD` handler always takes precedence over the derived one.
766
+ */
767
+ export declare type Method = (typeof METHOD_LIST)[number];
687
768
 
688
- /**
689
- * Narrow a raw `request.method` string into a typed {@link Method} — total,
690
- * never throws.
691
- *
692
- * @remarks
693
- * Guarded via {@link import('./constants.js').METHODS} (the seven registrable
694
- * HTTP methods); any other value (an unknown verb, non-uppercase casing)
695
- * resolves to `undefined` rather than throwing (§14 guard totality). Pure
696
- * leaf shared by the `Dispatcher`'s `handle` (§5.1 unknown-verb honesty) and
697
- * anywhere else a raw method string needs narrowing.
698
- *
699
- * @param value - The raw `request.method` string to narrow
700
- * @returns The matching {@link Method}, or `undefined` when `value` is not one
701
- * of the seven registrable methods
702
- *
703
- * @example
704
- * ```ts
705
- * parseMethod('GET') // 'GET'
706
- * parseMethod('PURGE') // undefined
707
- * parseMethod('get') // undefined case-sensitive
708
- * ```
709
- */
710
- export declare function parseMethod(value: string): Method | undefined;
769
+ /**
770
+ * Lists the HTTP methods a {@link import('./types.js').DispatcherInterface} registers
771
+ * routes under, in canonical order — a frozen literal tuple, and the single source the
772
+ * {@link import('./types.js').Method} type, {@link METHODS}, and `parseMethod` are all
773
+ * derived from.
774
+ *
775
+ * @remarks
776
+ * A frozen tuple of the verbs: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`,
777
+ * `HEAD`, `OPTIONS`. Adding a verb here widens the `Method` type, the
778
+ * {@link METHODS} membership set, and the `parseMethod` narrowing together, so
779
+ * the method set cannot drift between them. Prefer {@link METHODS} for a
780
+ * membership test; use this tuple where order or literal typing matters.
781
+ *
782
+ * @example
783
+ * ```ts
784
+ * METHOD_LIST[0] // 'GET'
785
+ * METHOD_LIST.includes('GET') // true
786
+ * ```
787
+ */
788
+ export declare const METHOD_LIST: readonly ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
711
789
 
712
- /**
713
- * Extracts `{ name: string }` param records from a path pattern at the type
714
- * level the typed half of the path grammar (§4).
715
- *
716
- * @typeParam Path - A route path pattern literal (`/users/:id/posts/:slug`,
717
- * `/files/*rest`, or a parameterless literal path)
718
- *
719
- * @remarks
720
- * A `:name` segment contributes `{ name: string }`; a trailing `*name`
721
- * wildcard (the grammar's only allowed wildcard position) contributes
722
- * `{ name: string }` capturing the rest of the path; a parameterless pattern
723
- * resolves to an empty record. Built over {@link PathParamsRaw} and flattened
724
- * through an identity-mapped type so editor hovers show the resolved shape
725
- * (`{ id: string; slug: string }`) rather than an unresolved intersection.
726
- *
727
- * @example
728
- * ```ts
729
- * type A = PathParams<'/users/:id/posts/:slug'> // { readonly id: string; readonly slug: string }
730
- * type B = PathParams<'/files/*rest'> // { readonly rest: string }
731
- * type C = PathParams<'/health'> // Record<string, never>
732
- * ```
733
- */
734
- export declare type PathParams<Path extends string> = {
735
- readonly [K in keyof PathParamsRaw<Path>]: PathParamsRaw<Path>[K];
736
- };
790
+ /**
791
+ * Holds every HTTP method a {@link import('./types.js').DispatcherInterface} registers
792
+ * routes under as a `ReadonlySet` backs the registration guard (`add` rejects any
793
+ * `method` outside this set) and the auto-`OPTIONS` `Allow` derivation.
794
+ *
795
+ * @remarks
796
+ * A `ReadonlySet` built from {@link METHOD_LIST}, so it carries exactly the
797
+ * {@link import('./types.js').Method} literals: `GET`, `POST`, `PUT`,
798
+ * `PATCH`, `DELETE`, `HEAD`, `OPTIONS`. `HEAD` is included even though it is
799
+ * never required at registration (a `GET` route auto-answers `HEAD`) — it is
800
+ * still a valid method to register explicitly. The element type stays `string`
801
+ * so a raw, unnarrowed `request.method` can be tested directly.
802
+ *
803
+ * @example
804
+ * ```ts
805
+ * METHODS.has('GET') // true
806
+ * METHODS.has('TRACE') // false
807
+ * ```
808
+ */
809
+ export declare const METHODS: ReadonlySet<string>;
737
810
 
738
- /**
739
- * Recursive, unflattened param extraction for {@link PathParams} — walks a
740
- * path pattern segment by segment (split on `/`), extracting each segment's
741
- * {@link SegmentParam} contribution and intersecting the rest.
742
- *
743
- * @typeParam Path - The path pattern literal being decomposed
744
- *
745
- * @remarks
746
- * Splits `Path` at its first `/` into `Segment` + `Rest`, intersects
747
- * `SegmentParam<Segment>` with the recursive walk of `Rest`, and once no
748
- * further `/` remains resolves the final segment's own `SegmentParam`
749
- * directly. `SegmentParam` mirrors the runtime `classifySegment` grammar
750
- * exactly: a `:name` HEAD (identifier-char run, stopping at the first
751
- * non-identifier char) captures a param; a segment whose `:` is not at the
752
- * segment START (`a:b`) contributes nothing (the classification fix, §4); a
753
- * final `*name` wildcard captures the rest-of-path param the same way. A
754
- * fully parameterless `Path` resolves to `unknown` here (the intersection
755
- * identity every non-capturing segment contributes) — {@link PathParams}
756
- * flattens that down to a clean empty record. This type is exported so
757
- * {@link PathParams} (which flattens it into a clean mapped type for IDE
758
- * hovers) has a documented, testable recursive step; consumers reach for the
759
- * flattened {@link PathParams} form, never this raw recursion.
760
- */
761
- export declare type PathParamsRaw<Path extends string> = string extends Path ? Readonly<Record<string, string>> : Path extends `${infer Segment}/${infer Rest}` ? SegmentParam<Segment> & PathParamsRaw<Rest> : SegmentParam<Path>;
811
+ /**
812
+ * Narrows a raw `request.method` string into a typed {@link Method} — total,
813
+ * never throws.
814
+ *
815
+ * @remarks
816
+ * Consults {@link import('./constants.js').METHOD_LIST} (the one home for the
817
+ * registrable HTTP methods), so a verb added there narrows here without
818
+ * a second list to update; any other value (an unknown verb, non-uppercase
819
+ * casing) resolves to `undefined` rather than throwing (total guard behavior).
820
+ * Pure leaf shared by the `Dispatcher`'s `handle` (honest about an unknown verb)
821
+ * and anywhere else a raw method string needs narrowing.
822
+ *
823
+ * @param value - The raw `request.method` string to narrow
824
+ * @returns The matching {@link Method}, or `undefined` when `value` is not a
825
+ * registrable method
826
+ *
827
+ * @example
828
+ * ```ts
829
+ * parseMethod('GET') // 'GET'
830
+ * parseMethod('PURGE') // undefined
831
+ * parseMethod('get') // undefined case-sensitive
832
+ * ```
833
+ */
834
+ export declare function parseMethod(value: string): Method | undefined;
762
835
 
763
- /**
764
- * Identity pass-through for a {@link RouteInput} that pins its `Path` generic
765
- * to the LITERAL registration-site string, so `context.params` types
766
- * correctly through {@link PathParams} without an explicit type argument.
767
- *
768
- * @remarks
769
- * A bare object literal handed straight to {@link import('./types.js').DispatcherInterface}'s
770
- * `add` already infers `Path` as a literal at that call site — but the moment
771
- * the object is built through an intermediate binding (a local `const route =
772
- * { method, path, handler }`) TypeScript widens `path` to `string` unless the
773
- * binding's own type is pinned. Wrapping the literal in `route(...)` supplies
774
- * that pin: its `const Path extends string` type parameter infers the NARROW
775
- * literal from the call, and the function returns its input completely
776
- * unchanged (same reference, no cloning, no validation) this is a
777
- * compile-time typing aid only, not a construction step (contrast
778
- * {@link import('./factories.js')} `create*` entity factories). A
779
- * heterogeneous `RouteInput[]` built from several `route(...)` calls still
780
- * widens each element's `Path` to `string` once collected into one array
781
- * (§14) the realistic ceiling this helper raises is PER-CALL typing at the
782
- * registration site, not a stored, still-literal-typed record.
783
- *
784
- * @typeParam Path - The route path pattern literal (drives `context.params`
785
- * via {@link PathParams})
786
- * @typeParam TState - The consumer's opaque per-request state type
787
- * @param input - The {@link RouteInput} to pass through unchanged
788
- * @returns `input`, unchanged (same reference)
789
- *
790
- * @example
791
- * ```ts
792
- * const input = route({
793
- * method: 'GET',
794
- * path: '/users/:id',
795
- * handler: (_request, context) => new Response(context.params.id), // typed string
796
- * })
797
- * dispatcher.add(input)
798
- * ```
799
- */
800
- export declare function route<const Path extends string, TState = undefined>(input: RouteInput<Path, TState>): RouteInput<Path, TState>;
836
+ /**
837
+ * Extracts `{ name: string }` param records from a path pattern at the type
838
+ * level the typed half of the path grammar.
839
+ *
840
+ * @typeParam Path - A route path pattern literal (`/users/:id/posts/:slug`,
841
+ * `/files/*rest`, or a parameterless literal path)
842
+ *
843
+ * @remarks
844
+ * A `:name` segment contributes `{ name: string }`; a trailing `*name`
845
+ * wildcard (the grammar's only allowed wildcard position) contributes
846
+ * `{ name: string }` capturing the rest of the path; a parameterless pattern
847
+ * resolves to an empty record. Built over {@link PathParamsRaw} and flattened
848
+ * through an identity-mapped type so editor hovers show the resolved shape
849
+ * (`{ id: string; slug: string }`) rather than an unresolved intersection.
850
+ *
851
+ * @example
852
+ * ```ts
853
+ * type A = PathParams<'/users/:id/posts/:slug'> // { readonly id: string; readonly slug: string }
854
+ * type B = PathParams<'/files/*rest'> // { readonly rest: string }
855
+ * type C = PathParams<'/health'> // Record<string, never>
856
+ * ```
857
+ */
858
+ export declare type PathParams<Path extends string> = {
859
+ readonly [K in keyof PathParamsRaw<Path>]: PathParamsRaw<Path>[K];
860
+ };
801
861
 
802
- /**
803
- * The ambient context a {@link RouteHandler} receives alongside the raw
804
- * `Request` decoded params, the winning pattern, the parsed URL, and the
805
- * consumer's opaque per-request state.
806
- *
807
- * @typeParam Path - The route path pattern the handler was registered under
808
- * (drives the typed shape of `params` via {@link PathParams})
809
- * @typeParam TState - The consumer's opaque per-request state type
810
- *
811
- * @remarks
812
- * - `params` — the decoded param record, typed from `Path` via
813
- * {@link PathParams} (empty for a parameterless path).
814
- * - `pattern` the winning REGISTERED pattern (matches
815
- * {@link RouterMatch.path}), useful for logging/metrics.
816
- * - `url` the request URL, already parsed once by the dispatcher.
817
- * - `state` the consumer's per-request payload (logger, session, DI bag),
818
- * threaded opaquely through `handle()` the router never inspects it.
819
- */
820
- export declare interface RouteContext<Path extends string = string, TState = undefined> {
821
- readonly params: PathParams<Path>;
822
- readonly pattern: string;
823
- readonly url: URL;
824
- readonly state: TState;
825
- }
862
+ /**
863
+ * Performs recursive, unflattened param extraction for {@link PathParams} walks a
864
+ * path pattern segment by segment (split on `/`), extracting each segment's
865
+ * {@link SegmentParam} contribution and intersecting the rest.
866
+ *
867
+ * @typeParam Path - The path pattern literal being decomposed
868
+ *
869
+ * @remarks
870
+ * Splits `Path` at its first `/` into `Segment` + `Rest`, intersects
871
+ * `SegmentParam<Segment>` with the recursive walk of `Rest`, and — after no
872
+ * further `/` remains resolves the final segment's own `SegmentParam`
873
+ * directly. `SegmentParam` mirrors the runtime `classifySegment` grammar
874
+ * exactly: a `:name` HEAD (identifier-char run, stopping at the first
875
+ * non-identifier char) captures a param; a segment whose `:` is not at the
876
+ * segment START (`a:b`) contributes nothing (the classification fix); a
877
+ * final `*name` wildcard captures the rest-of-path param the same way. A
878
+ * fully parameterless `Path` resolves to `unknown` here (the intersection
879
+ * identity every non-capturing segment contributes) — {@link PathParams}
880
+ * flattens that down to a clean empty record. This type is exported so
881
+ * {@link PathParams} (which flattens it into a clean mapped type for IDE
882
+ * hovers) has a documented, testable recursive step; consumers reach for the
883
+ * flattened {@link PathParams} form, never this raw recursion.
884
+ */
885
+ export declare type PathParamsRaw<Path extends string> = string extends Path ? Readonly<Record<string, string>> : Path extends `${infer Segment}/${infer Rest}` ? SegmentParam<Segment> & PathParamsRaw<Rest> : SegmentParam<Path>;
826
886
 
827
- /**
828
- * One registered route in a {@link RouterInterface} the `path` pattern plus
829
- * the opaque `meta` payload to return on a match, with an optional `name`.
830
- *
831
- * @typeParam Meta - The payload to carry on a match (opaque to the engine —
832
- * a route handler + method on the `Dispatcher`, a component/loader
833
- * reference on the `Navigator`)
834
- *
835
- * @remarks
836
- * - `path` — the `/`-prefixed route path pattern (`/users/:id`); compiled
837
- * once at registration.
838
- * - `meta` the opaque payload returned (as {@link RouterMatch.meta}) when
839
- * this entry is the most-specific match. The engine never inspects it
840
- * the consumer's {@link AnswerHandler} predicate (the override seam)
841
- * decides eligibility from it.
842
- * - `name` — an optional route identifier, carried through onto a match for
843
- * consumers that build named links or debug output.
844
- */
845
- export declare interface RouteEntry<Meta> {
846
- readonly path: string;
847
- readonly meta: Meta;
848
- readonly name?: string;
849
- }
887
+ /**
888
+ * Represents the ambient context a {@link RouteHandler} receives alongside the raw
889
+ * `Request` decoded params, the winning pattern, the parsed URL, and the
890
+ * consumer's opaque per-request state.
891
+ *
892
+ * @typeParam Path - The route path pattern the handler was registered under
893
+ * (drives the typed shape of `params` through {@link PathParams})
894
+ * @typeParam TState - The consumer's opaque per-request state type
895
+ *
896
+ * @remarks
897
+ * - `params` — the decoded param record, typed from `Path` through
898
+ * {@link PathParams} (empty for a parameterless path).
899
+ * - `pattern` the winning REGISTERED pattern (matches
900
+ * {@link RouterMatch.path}), useful for logging/metrics.
901
+ * - `url` — the request URL, already parsed once by the dispatcher.
902
+ * - `state` — the consumer's per-request payload (logger, session, DI bag),
903
+ * threaded opaquely through `handle()` the router never inspects it.
904
+ */
905
+ export declare interface RouteContext<Path extends string = string, TState = undefined> {
906
+ readonly params: PathParams<Path>;
907
+ readonly pattern: string;
908
+ readonly url: URL;
909
+ readonly state: TState;
910
+ }
850
911
 
851
- /**
852
- * A route handler receives the raw fetch `Request` plus its typed
853
- * {@link RouteContext} and returns (or resolves) a fetch `Response`.
854
- *
855
- * @typeParam Path - The route path pattern the handler is registered under
856
- * @typeParam TState - The consumer's opaque per-request state type
857
- *
858
- * @remarks
859
- * A handler throw propagates to the caller of `dispatcher.handle` — the
860
- * dispatcher never invents an error boundary; mapping throws to responses is
861
- * the consuming server's policy (§5.1).
862
- */
863
- export declare type RouteHandler<Path extends string = string, TState = undefined> = (request: Request, context: RouteContext<Path, TState>) => Response | Promise<Response>;
912
+ /**
913
+ * Represents one registered route in a {@link RouterInterface} the `path` pattern plus
914
+ * the opaque `meta` payload to return on a match, with an optional `name`.
915
+ *
916
+ * @typeParam Meta - The payload to carry on a match (opaque to the engine —
917
+ * a route handler + method on the `Dispatcher`, a component/loader
918
+ * reference on the `Navigator`)
919
+ *
920
+ * @remarks
921
+ * - `path` the `/`-prefixed route path pattern (`/users/:id`); compiled
922
+ * once at registration.
923
+ * - `meta` — the opaque payload returned (as {@link RouterMatch.meta}) when
924
+ * this entry is the most-specific match. The engine never inspects it
925
+ * the consumer's {@link AnswerHandler} predicate (the override seam)
926
+ * decides eligibility from it.
927
+ * - `name` — an optional route identifier, carried through onto a match for
928
+ * consumers that build named links or debug output.
929
+ */
930
+ export declare interface RouteEntry<Meta> {
931
+ readonly path: string;
932
+ readonly meta: Meta;
933
+ readonly name?: string;
934
+ }
864
935
 
865
- /**
866
- * One route registration input for {@link DispatcherInterface.add} the
867
- * method-dimensioned counterpart of {@link RouteEntry}.
868
- *
869
- * @typeParam Path - The route path pattern literal (drives the typed
870
- * `handler`'s `context.params` via {@link PathParams})
871
- * @typeParam TState - The consumer's opaque per-request state type
872
- *
873
- * @remarks
874
- * - `method` the HTTP method this route answers.
875
- * - `path` — the `/`-prefixed route path pattern.
876
- * - `handler` — the {@link RouteHandler} invoked on a match.
877
- * - `name` an optional route identifier, carried through onto a
878
- * {@link RouterMatch}.
879
- */
880
- export declare interface RouteInput<Path extends string = string, TState = undefined> {
881
- readonly method: Method;
882
- readonly path: Path;
883
- readonly handler: RouteHandler<Path, TState>;
884
- readonly name?: string;
885
- }
936
+ /**
937
+ * Receives the raw fetch `Request` plus its typed {@link RouteContext} and returns (or resolves)
938
+ * a fetch `Response`.
939
+ *
940
+ * @typeParam Path - The route path pattern the handler is registered under
941
+ * @typeParam TState - The consumer's opaque per-request state type
942
+ *
943
+ * @remarks
944
+ * A handler throw propagates to the caller of `dispatcher.handle` — the
945
+ * dispatcher never invents an error boundary; mapping throws to responses is
946
+ * the consuming server's policy.
947
+ */
948
+ export declare type RouteHandler<Path extends string = string, TState = undefined> = (request: Request, context: RouteContext<Path, TState>) => Response | Promise<Response>;
886
949
 
887
- /**
888
- * The path-matching + registry engine registers `{ path, meta, name? }`
889
- * entries (compiling each path once) and resolves a concrete pathname to the
890
- * MOST SPECIFIC matching entry. The shared machine both the `Navigator`
891
- * (browser) and the `Dispatcher` (core, method-dimensioned) compose.
892
- *
893
- * @typeParam Meta - The opaque payload each entry carries and a match returns
894
- *
895
- * @remarks
896
- * - **Registration boundary guard (§14).** `add` validates each entry's
897
- * `path` `isString` plus a leading `/` and throws `TypeError` on a
898
- * malformed registration; `match` stays guard-free (the hot path).
899
- * - **Compile-once.** Each path is compiled exactly once at registration into
900
- * a parallel `#compiled` array, so `match` runs only a cached `exec` per
901
- * candidate.
902
- * - **Dedup via `key`.** When `options.key` is set, an entry whose computed
903
- * key already exists REPLACES the prior one IN PLACE (both the `#entries`
904
- * and `#compiled` arrays, at the existing index) — last write wins, no
905
- * engine rebuild. Omitted ⇒ every entry is kept, even duplicate paths.
906
- * - **Groups.** `group(prefix)` returns a {@link GroupInterface} that composes
907
- * `prefix` onto every entry it registers, nesting via {@link joinPaths}.
908
- *
909
- * @example
910
- * ```ts
911
- * const router = new Router<{ readonly page: string }>()
912
- * router.add({ path: '/users/:id', meta: { page: 'profile' } })
913
- * router.match('/users/7') // { path: '/users/:id', params: { id: '7' }, meta: { page: 'profile' } }
914
- * ```
915
- */
916
- export declare class Router<Meta> implements RouterInterface<Meta> {
917
- #private;
918
- constructor(options?: RouterOptions<Meta>);
919
- get count(): number;
920
- add(entry: RouteEntry<Meta>): void;
921
- add(entries: ReadonlyArray<RouteEntry<Meta>>): void;
922
- match(pathname: string, answers?: AnswerHandler<Meta>): RouterMatch<Meta> | undefined;
923
- entries(): ReadonlyArray<RouteEntry<Meta>>;
924
- entries(pathname: string): ReadonlyArray<RouteEntry<Meta>>;
925
- group(prefix: string): GroupInterface<Meta>;
926
- clear(): void;
927
- }
950
+ /**
951
+ * Represents one route registration input for {@link DispatcherInterface.add} the
952
+ * method-dimensioned counterpart of {@link RouteEntry}.
953
+ *
954
+ * @typeParam Path - The route path pattern literal (drives the typed
955
+ * `handler`'s `context.params` through {@link PathParams})
956
+ * @typeParam TState - The consumer's opaque per-request state type
957
+ *
958
+ * @remarks
959
+ * - `method` the HTTP method this route answers.
960
+ * - `path` — the `/`-prefixed route path pattern.
961
+ * - `handler` the {@link RouteHandler} invoked on a match.
962
+ * - `name` an optional route identifier, carried through onto a
963
+ * {@link RouterMatch}.
964
+ */
965
+ export declare interface RouteInput<Path extends string = string, TState = undefined> {
966
+ readonly method: Method;
967
+ readonly path: Path;
968
+ readonly handler: RouteHandler<Path, TState>;
969
+ readonly name?: string;
970
+ }
928
971
 
929
- /**
930
- * The `meta` payload a {@link DispatcherInterface} stores in its underlying
931
- * `Router` what {@link RouterInterface.match} returns as
932
- * {@link RouterMatch.meta} on a dispatch hit.
933
- *
934
- * @typeParam TState - The consumer's opaque per-request state type
935
- *
936
- * @remarks
937
- * The handler is typed over `string` (not the original literal `Path`) at
938
- * storage — path-specific param typing is recovered at the call site through
939
- * {@link RouteInput}'s generic `Path`, not preserved in the stored record.
940
- */
941
- export declare interface RouteRecord<TState> {
942
- readonly method: Method;
943
- readonly handler: RouteHandler<string, TState>;
944
- readonly name?: string;
945
- }
972
+ /**
973
+ * Represents the path-matching + registry engine — registers `{ path, meta, name? }`
974
+ * entries (compiling each path once) and resolves a concrete pathname to the most
975
+ * specific matching entry. The shared machine both the `Navigator` (browser) and
976
+ * the `Dispatcher` (core, method-dimensioned) compose.
977
+ *
978
+ * @typeParam Meta - The opaque payload each entry carries and a match returns
979
+ *
980
+ * @remarks
981
+ * - **Registration boundary guard.** `add` validates each entry's
982
+ * `path` `isString` plus a leading `/` and throws a `ContractError` on a
983
+ * malformed registration; `match` stays guard-free (the hot path).
984
+ * - **Compile-once.** Each path is compiled exactly once at registration into
985
+ * a parallel `#compiled` array, so `match` runs only a cached `exec` per
986
+ * candidate.
987
+ * - **Dedup through `key`.** When `options.key` is set, an entry whose computed
988
+ * key already exists REPLACES the prior one IN PLACE (both the `#entries`
989
+ * and `#compiled` arrays, at the existing index) — last write wins, no
990
+ * engine rebuild. Omitted ⇒ every entry is kept, even duplicate paths.
991
+ * - **Groups.** `group(prefix)` returns a {@link GroupInterface} that composes
992
+ * `prefix` onto every entry it registers, nesting through {@link joinPaths}.
993
+ *
994
+ * @example
995
+ * ```ts
996
+ * const router = new Router<{ readonly page: string }>()
997
+ * router.add({ path: '/users/:id', meta: { page: 'profile' } })
998
+ * router.match('/users/7') // { path: '/users/:id', params: { id: '7' }, meta: { page: 'profile' } }
999
+ * ```
1000
+ */
1001
+ export declare class Router<Meta> implements RouterInterface<Meta> {
1002
+ #private;
1003
+ constructor(options?: RouterOptions<Meta>);
1004
+ get count(): number;
1005
+ add(entry: RouteEntry<Meta>): void;
1006
+ add(entries: ReadonlyArray<RouteEntry<Meta>>): void;
1007
+ match(pathname: string, answers?: AnswerHandler<Meta>): RouterMatch<Meta> | undefined;
1008
+ entries(): ReadonlyArray<RouteEntry<Meta>>;
1009
+ entries(pathname: string): ReadonlyArray<RouteEntry<Meta>>;
1010
+ group(prefix: string): GroupInterface<Meta>;
1011
+ clear(): void;
1012
+ }
946
1013
 
947
- /**
948
- * The path-matching + registry engine contract (the §4.5 behavioral-interface
949
- * role for the one-class-per-file `Router`). Registers `{ path, meta, name? }`
950
- * entries (compiling each path once) and resolves a concrete pathname to the
951
- * MOST SPECIFIC matching entry — a literal segment beats a param beats a
952
- * wildcard at the earliest differing segment, registration-order-independent
953
- * (§4). The shared engine both the `Navigator` (browser) and the `Dispatcher`
954
- * (core, method-dimensioned) compose.
955
- *
956
- * @typeParam Meta - The opaque payload each entry carries and a match returns
957
- *
958
- * @remarks
959
- * - `count` the number of registered entries.
960
- * - `add(entry)` / `add(entries)` — register ONE / MANY entries (§9.2 batch);
961
- * each path is compiled once here. When constructed with a `key` option,
962
- * an entry whose key already exists replaces the prior one in place;
963
- * otherwise every entry is kept, even duplicate paths.
964
- * - `match(pathname, answers?)` — the MOST-SPECIFIC matching entry as a
965
- * {@link RouterMatch} (its winning `path`, decoded `params`, `meta`, and
966
- * `name`), or `undefined`. The optional {@link AnswerHandler} predicate
967
- * filters candidates by `meta` first; omitted ⇒ every path match is
968
- * eligible.
969
- * - `entries()` — ALL registered entries in registration order.
970
- * - `entries(pathname)` — only entries whose path matches `pathname` (the
971
- * §9 plural accessor's filtered form; backs a consumer's allow/405 set).
972
- * - `group(prefix)` — a {@link GroupInterface} scoped under `prefix`; entries
973
- * added through the group are registered on this same router with `prefix`
974
- * prepended to each path.
975
- * - `clear()` — drop every entry (§10), leaving the router reusable.
976
- */
977
- export declare interface RouterInterface<Meta> {
978
- readonly count: number;
979
- add(entry: RouteEntry<Meta>): void;
980
- add(entries: ReadonlyArray<RouteEntry<Meta>>): void;
981
- match(pathname: string, answers?: AnswerHandler<Meta>): RouterMatch<Meta> | undefined;
982
- entries(): ReadonlyArray<RouteEntry<Meta>>;
983
- entries(pathname: string): ReadonlyArray<RouteEntry<Meta>>;
984
- group(prefix: string): GroupInterface<Meta>;
985
- clear(): void;
986
- }
1014
+ /**
1015
+ * Represents the `meta` payload a {@link DispatcherInterface} stores in its underlying
1016
+ * `Router` what {@link RouterInterface.match} returns as
1017
+ * {@link RouterMatch.meta} on a dispatch hit.
1018
+ *
1019
+ * @typeParam TState - The consumer's opaque per-request state type
1020
+ *
1021
+ * @remarks
1022
+ * The handler is typed over `string` (not the original literal `Path`) at
1023
+ * storage path-specific param typing is recovered at the call site through
1024
+ * {@link RouteInput}'s generic `Path`, not preserved in the stored record.
1025
+ */
1026
+ export declare interface RouteRecord<TState> {
1027
+ readonly method: Method;
1028
+ readonly handler: RouteHandler<string, TState>;
1029
+ readonly name?: string;
1030
+ }
987
1031
 
988
- /**
989
- * One matched route the winning entry's PATTERN, decoded params, `meta`
990
- * payload, and optional `name`.
991
- *
992
- * @typeParam Meta - The payload the winning entry carries
993
- *
994
- * @remarks
995
- * What {@link RouterInterface.match} returns on a hit (`undefined` on a
996
- * miss). `path` is the winning entry's REGISTERED PATTERN (not the concrete
997
- * pathname that was matched) — useful for consumers that need to know which
998
- * route fired. `params` is a frozen `name → value` record (empty for a
999
- * parameterless path), each value URL-decoded with a malformed `%` escape
1000
- * tolerated as a literal (§4, never throws). Plain data no behavior.
1001
- */
1002
- export declare interface RouterMatch<Meta> {
1003
- readonly path: string;
1004
- readonly params: Readonly<Record<string, string>>;
1005
- readonly meta: Meta;
1006
- readonly name?: string;
1007
- }
1032
+ /**
1033
+ * Represents the path-matching + registry engine contract (the behavioral-interface
1034
+ * role for the one-class-per-file `Router`). Registers `{ path, meta, name? }`
1035
+ * entries (compiling each path once) and resolves a concrete pathname to the most
1036
+ * specific matching entry a literal segment beats a param beats a wildcard at the
1037
+ * earliest differing segment, registration-order-independent. The shared engine both
1038
+ * the `Navigator` (browser) and the `Dispatcher` (core, method-dimensioned) compose.
1039
+ *
1040
+ * @typeParam Meta - The opaque payload each entry carries and a match returns
1041
+ *
1042
+ * @remarks
1043
+ * Registration is the guarded boundary and matching is the hot path: `add` validates
1044
+ * each entry and throws, while `match` carries no guard of its own.
1045
+ */
1046
+ export declare interface RouterInterface<Meta> {
1047
+ /** Holds the number of registered entries. */
1048
+ readonly count: number;
1049
+ /**
1050
+ * Registers one entry, or many in one call (batch registration), compiling each path
1051
+ * once; throws a `ContractError` on a malformed path.
1052
+ *
1053
+ * @remarks
1054
+ * When the router was constructed with a `key` option, an entry whose key already
1055
+ * exists replaces the prior one in place; otherwise every entry is kept, even a
1056
+ * duplicate path.
1057
+ */
1058
+ add(entry: RouteEntry<Meta>): void;
1059
+ add(entries: ReadonlyArray<RouteEntry<Meta>>): void;
1060
+ /**
1061
+ * Resolves the most-specific matching entry for a pathname, or `undefined` when
1062
+ * nothing matches.
1063
+ *
1064
+ * @remarks
1065
+ * A hit is a {@link RouterMatch} carrying the winning `path`, the decoded `params`,
1066
+ * the `meta` payload, and the optional `name`. The optional {@link AnswerHandler}
1067
+ * predicate filters candidates by `meta` first; omitted, every path match is eligible.
1068
+ */
1069
+ match(pathname: string, answers?: AnswerHandler<Meta>): RouterMatch<Meta> | undefined;
1070
+ /**
1071
+ * Lists every registered entry in registration order, or only those whose path
1072
+ * matches a given pathname.
1073
+ *
1074
+ * @remarks
1075
+ * The filtered form is the plural accessor's second shape, and it backs a consumer's
1076
+ * allow set for a 405 answer.
1077
+ */
1078
+ entries(): ReadonlyArray<RouteEntry<Meta>>;
1079
+ entries(pathname: string): ReadonlyArray<RouteEntry<Meta>>;
1080
+ /**
1081
+ * Returns a prefix-scoped registration handle over this router.
1082
+ *
1083
+ * @remarks
1084
+ * Entries added through the group are registered on this same router with `prefix`
1085
+ * prepended to each path.
1086
+ */
1087
+ group(prefix: string): GroupInterface<Meta>;
1088
+ /** Drops every entry, leaving the router reusable. */
1089
+ clear(): void;
1090
+ }
1008
1091
 
1009
- /**
1010
- * Options for `createRouter`an optional initial entry set, the case-
1011
- * sensitivity toggle, and the dedup identity function.
1012
- *
1013
- * @typeParam Meta - The entry payload type
1014
- *
1015
- * @remarks
1016
- * - `entries` the initial `{ path, meta, name? }` entries to register (each
1017
- * path compiled once), equivalent to a bare `createRouter()` followed by
1018
- * `add(entries)`. Omitted an empty router.
1019
- * - `sensitive` case-sensitive path matching (default `true`, §4). Set
1020
- * `false` to fold case during matching (`/Users` matches `/users`);
1021
- * registered patterns are never case-folded in storage, only in matching.
1022
- * - `key` — an optional dedup identity function computed per entry
1023
- * (`RouteEntry<Meta> → string`). When provided, registering an entry whose
1024
- * key already exists REPLACES the prior entry in place (last write wins,
1025
- * no engine rebuild) instead of adding a second candidate. Omitted ⇒ every
1026
- * registered entry is kept, even duplicate paths.
1027
- */
1028
- export declare interface RouterOptions<Meta> {
1029
- readonly entries?: ReadonlyArray<RouteEntry<Meta>>;
1030
- readonly sensitive?: boolean;
1031
- readonly key?: (entry: RouteEntry<Meta>) => string;
1032
- }
1092
+ /**
1093
+ * Represents one matched route the winning entry's registered pattern, its decoded
1094
+ * params, its `meta` payload, and its optional `name`.
1095
+ *
1096
+ * @typeParam Meta - The payload the winning entry carries
1097
+ *
1098
+ * @remarks
1099
+ * What {@link RouterInterface.match} returns on a hit (`undefined` on a
1100
+ * miss). `path` is the winning entry's REGISTERED PATTERN (not the concrete
1101
+ * pathname that was matched) useful for consumers that need to know which
1102
+ * route fired. `params` is a frozen `name value` record (empty for a
1103
+ * parameterless path), each value URL-decoded with a malformed `%` escape
1104
+ * tolerated as a literal, never throws. Plain data no behavior.
1105
+ */
1106
+ export declare interface RouterMatch<Meta> {
1107
+ readonly path: string;
1108
+ readonly params: Readonly<Record<string, string>>;
1109
+ readonly meta: Meta;
1110
+ readonly name?: string;
1111
+ }
1033
1112
 
1034
- export declare type SegmentParam<Segment extends string> = Segment extends `:${infer Rest}` ? IdentifierHead<Rest> extends infer Name extends string ? Name extends '' ? unknown : {
1035
- readonly [K in Name]: string;
1036
- } : unknown : Segment extends `*${infer Rest}` ? IdentifierHead<Rest> extends infer Name extends string ? Name extends '' ? unknown : {
1037
- readonly [K in Name]: string;
1038
- } : unknown : unknown;
1113
+ /**
1114
+ * Represents the options for `createRouter` — an optional initial entry set, the
1115
+ * case-sensitivity toggle, and the dedup identity function.
1116
+ *
1117
+ * @typeParam Meta - The entry payload type
1118
+ *
1119
+ * @remarks
1120
+ * - `entries` — the initial `{ path, meta, name? }` entries to register (each
1121
+ * path compiled once), equivalent to a bare `createRouter()` followed by
1122
+ * `add(entries)`. Omitted ⇒ an empty router.
1123
+ * - `sensitive` — case-sensitive path matching (default `true`). Set
1124
+ * `false` to fold case during matching (`/Users` matches `/users`);
1125
+ * registered patterns are never case-folded in storage, only in matching.
1126
+ * - `key` — an optional dedup identity function computed per entry
1127
+ * (`RouteEntry<Meta> → string`). When provided, registering an entry whose
1128
+ * key already exists REPLACES the prior entry in place (last write wins,
1129
+ * no engine rebuild) instead of adding a second candidate. Omitted ⇒ every
1130
+ * registered entry is kept, even duplicate paths.
1131
+ */
1132
+ export declare interface RouterOptions<Meta> {
1133
+ readonly entries?: ReadonlyArray<RouteEntry<Meta>>;
1134
+ readonly sensitive?: boolean;
1135
+ readonly key?: (entry: RouteEntry<Meta>) => string;
1136
+ }
1039
1137
 
1040
- export declare type TakeIdentifierTail<S extends string, Acc extends string> = S extends `${infer Head}${infer Tail}` ? Head extends IdentifierChar ? TakeIdentifierTail<Tail, `${Acc}${Head}`> : Acc : Acc;
1138
+ /**
1139
+ * Contributes one path segment's type-level param record — the type-level mirror of
1140
+ * the runtime `classifySegment` and `compilePath` segment parser.
1141
+ *
1142
+ * @typeParam Segment - One `/`-split path segment literal
1143
+ *
1144
+ * @remarks
1145
+ * A `:` head followed by an identifier captures that identifier, stopping at the first
1146
+ * non-identifier char, so `:name.json` captures only `name`. A segment whose `:` is not
1147
+ * at the segment start (`a:b`) is literal and captures nothing — the classification fix
1148
+ * this type mirrors. A `*name` segment, valid only as the grammar's final segment,
1149
+ * captures the identifier the same way. A non-capturing segment resolves to `unknown`
1150
+ * (the intersection identity) rather than `Record<string, never>` — an index-signature
1151
+ * type intersected with a later `{ id: string }` would otherwise conflict (`string` is
1152
+ * not assignable to the index signature's `never`); {@link PathParams} normalizes the
1153
+ * eventual `unknown` (parameterless) result down to a clean empty record.
1154
+ */
1155
+ export declare type SegmentParam<Segment extends string> = Segment extends `:${infer Rest}` ? IdentifierHead<Rest> extends infer Name extends string ? Name extends '' ? unknown : {
1156
+ readonly [K in Name]: string;
1157
+ } : unknown : Segment extends `*${infer Rest}` ? IdentifierHead<Rest> extends infer Name extends string ? Name extends '' ? unknown : {
1158
+ readonly [K in Name]: string;
1159
+ } : unknown : unknown;
1041
1160
 
1042
- /**
1043
- * Specificity tier for a **literal** path segment (`/users`) the highest
1044
- * tier, always outranking a param or wildcard segment at the same position.
1045
- *
1046
- * @remarks
1047
- * Consumed by `computeSpecificity` (U1 `helpers.ts`) when ranking candidate
1048
- * matches left-to-right at the earliest differing segment (§4 precedence).
1049
- *
1050
- * @example
1051
- * ```ts
1052
- * TIER_LITERAL > TIER_PARAM // true
1053
- * ```
1054
- */
1055
- export declare const TIER_LITERAL = 2;
1161
+ /**
1162
+ * Consumes the identifier-continuation run at the front of a string literal, char by
1163
+ * char, appending each onto the accumulator.
1164
+ *
1165
+ * @typeParam S - The string literal whose front is being consumed
1166
+ * @typeParam Acc - The identifier characters taken so far
1167
+ *
1168
+ * @remarks
1169
+ * Stops (returning `Acc` unchanged) at the first non-identifier char or at the end of
1170
+ * the string, so the accumulator holds exactly the leading {@link IdentifierChar} run.
1171
+ */
1172
+ export declare type TakeIdentifierTail<S extends string, Acc extends string> = S extends `${infer Head}${infer Tail}` ? Head extends IdentifierChar ? TakeIdentifierTail<Tail, `${Acc}${Head}`> : Acc : Acc;
1056
1173
 
1057
- /**
1058
- * Specificity tier for a **param** path segment (`:name`) — ranks below a
1059
- * literal segment and above a wildcard segment at the same position.
1060
- *
1061
- * @remarks
1062
- * Consumed by `computeSpecificity` (U1 `helpers.ts`) alongside {@link TIER_LITERAL}
1063
- * and {@link TIER_WILDCARD}.
1064
- *
1065
- * @example
1066
- * ```ts
1067
- * TIER_PARAM > TIER_WILDCARD // true
1068
- * ```
1069
- */
1070
- export declare const TIER_PARAM = 1;
1174
+ /**
1175
+ * Names the specificity tier for a **literal** path segment (`/users`) — the highest
1176
+ * tier, always outranking a param or wildcard segment at the same position.
1177
+ *
1178
+ * @remarks
1179
+ * Consumed by `computeSpecificity` (the path compiler in `helpers.ts`) when ranking candidate
1180
+ * matches left-to-right at the earliest differing segment.
1181
+ *
1182
+ * @example
1183
+ * ```ts
1184
+ * TIER_LITERAL > TIER_PARAM // true
1185
+ * ```
1186
+ */
1187
+ export declare const TIER_LITERAL = 2;
1071
1188
 
1072
- /**
1073
- * Specificity tier for a **wildcard** path segment (`*name`) — the lowest
1074
- * tier; a wildcard only ever wins against another wildcard shape (an
1075
- * equal-specificity tie resolved by registration order).
1076
- *
1077
- * @remarks
1078
- * Consumed by `computeSpecificity` (U1 `helpers.ts`).
1079
- *
1080
- * @example
1081
- * ```ts
1082
- * TIER_WILDCARD // 0
1083
- * ```
1084
- */
1085
- export declare const TIER_WILDCARD = 0;
1189
+ /**
1190
+ * Names the specificity tier for a **param** path segment (`:name`) — ranks below a
1191
+ * literal segment and above a wildcard segment at the same position.
1192
+ *
1193
+ * @remarks
1194
+ * Consumed by `computeSpecificity` (the path compiler in `helpers.ts`) alongside {@link TIER_LITERAL}
1195
+ * and {@link TIER_WILDCARD}.
1196
+ *
1197
+ * @example
1198
+ * ```ts
1199
+ * TIER_PARAM > TIER_WILDCARD // true
1200
+ * ```
1201
+ */
1202
+ export declare const TIER_PARAM = 1;
1203
+
1204
+ /**
1205
+ * Names the specificity tier for a **wildcard** path segment (`*name`) — the lowest
1206
+ * tier; a wildcard only ever wins against another wildcard shape (an
1207
+ * equal-specificity tie resolved by registration order).
1208
+ *
1209
+ * @remarks
1210
+ * Consumed by `computeSpecificity` (the path compiler in `helpers.ts`).
1211
+ *
1212
+ * @example
1213
+ * ```ts
1214
+ * TIER_WILDCARD // 0
1215
+ * ```
1216
+ */
1217
+ export declare const TIER_WILDCARD = 0;
1086
1218
 
1087
- export { }
1219
+ export { }