@orkestrel/router 0.0.1 → 0.0.3

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