@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.
- package/README.md +33 -4
- package/dist/src/browser/index.d.ts +283 -4
- package/dist/src/browser/index.js +4 -300
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +893 -0
- package/dist/src/core/index.cjs.map +1 -0
- package/dist/src/core/index.d.cts +1067 -0
- package/dist/src/core/index.d.ts +1067 -8
- package/dist/src/core/index.js +33 -175
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +4 -210
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/{helpers.d.ts → index.d.cts} +166 -127
- package/dist/src/server/index.d.ts +166 -2
- package/dist/src/server/index.js +207 -0
- package/dist/src/server/index.js.map +1 -0
- package/package.json +38 -25
- package/dist/src/browser/Navigator.d.ts +0 -62
- package/dist/src/browser/factories.d.ts +0 -33
- package/dist/src/browser/helpers.d.ts +0 -76
- package/dist/src/browser/types.d.ts +0 -101
- package/dist/src/core/DispatchGroup.d.ts +0 -32
- package/dist/src/core/Dispatcher.d.ts +0 -51
- package/dist/src/core/Group.d.ts +0 -30
- package/dist/src/core/Router.d.ts +0 -42
- package/dist/src/core/constants.d.ts +0 -60
- package/dist/src/core/factories.d.ts +0 -53
- package/dist/src/core/helpers.d.ts +0 -274
- package/dist/src/core/types.d.ts +0 -445
- package/dist/src/server/types.d.ts +0 -31
package/dist/src/core/index.d.ts
CHANGED
|
@@ -1,8 +1,1067 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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 { }
|