@orkestrel/router 0.0.11 → 0.0.13
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 +13 -10
- package/dist/src/browser/index.d.ts +43 -44
- package/dist/src/browser/index.js +60 -52
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +189 -117
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +945 -911
- package/dist/src/core/index.d.ts +945 -911
- package/dist/src/core/index.js +188 -117
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +25 -21
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +21 -21
- package/dist/src/server/index.d.ts +21 -21
- package/dist/src/server/index.js +25 -21
- package/dist/src/server/index.js.map +1 -1
- package/package.json +19 -15
package/dist/src/core/index.js
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
|
+
import { ContractError, isFunction, isString, preview } from "@orkestrel/contract";
|
|
1
2
|
import { Emitter } from "@orkestrel/emitter";
|
|
2
|
-
import { isFunction, isString } from "@orkestrel/contract";
|
|
3
3
|
//#region src/core/constants.ts
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
* registers routes under
|
|
7
|
-
*
|
|
5
|
+
* Lists the HTTP methods a {@link import('./types.js').DispatcherInterface}
|
|
6
|
+
* registers routes under, in canonical order — the single source the
|
|
7
|
+
* {@link import('./types.js').Method} type, {@link METHODS}, and
|
|
8
|
+
* `parseMethod` are all derived from.
|
|
8
9
|
*
|
|
9
10
|
* @remarks
|
|
10
|
-
* A
|
|
11
|
-
* `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* A frozen tuple of the verbs: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`,
|
|
12
|
+
* `HEAD`, `OPTIONS`. Adding a verb here widens the `Method` type, the
|
|
13
|
+
* {@link METHODS} membership set, and the `parseMethod` narrowing together, so
|
|
14
|
+
* the method set cannot drift between them. Prefer {@link METHODS} for a
|
|
15
|
+
* membership test; use this tuple where order or literal typing matters.
|
|
14
16
|
*
|
|
15
17
|
* @example
|
|
16
18
|
* ```ts
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
+
* METHOD_LIST[0] // 'GET'
|
|
20
|
+
* METHOD_LIST.includes('GET') // true
|
|
19
21
|
* ```
|
|
20
22
|
*/
|
|
21
|
-
var
|
|
23
|
+
var METHOD_LIST = Object.freeze([
|
|
22
24
|
"GET",
|
|
23
25
|
"POST",
|
|
24
26
|
"PUT",
|
|
@@ -26,14 +28,35 @@ var METHODS = Object.freeze(/* @__PURE__ */ new Set([
|
|
|
26
28
|
"DELETE",
|
|
27
29
|
"HEAD",
|
|
28
30
|
"OPTIONS"
|
|
29
|
-
])
|
|
31
|
+
]);
|
|
30
32
|
/**
|
|
31
|
-
*
|
|
33
|
+
* Holds the complete set of HTTP methods a
|
|
34
|
+
* {@link import('./types.js').DispatcherInterface} registers routes under —
|
|
35
|
+
* backs the registration guard (`add` rejects any `method` outside this set)
|
|
36
|
+
* and the auto-`OPTIONS` `Allow` derivation.
|
|
37
|
+
*
|
|
38
|
+
* @remarks
|
|
39
|
+
* A `ReadonlySet` built from {@link METHOD_LIST}, so it carries exactly the
|
|
40
|
+
* {@link import('./types.js').Method} literals: `GET`, `POST`, `PUT`,
|
|
41
|
+
* `PATCH`, `DELETE`, `HEAD`, `OPTIONS`. `HEAD` is included even though it is
|
|
42
|
+
* never required at registration (a `GET` route auto-answers `HEAD`) — it is
|
|
43
|
+
* still a valid method to register explicitly. The element type stays `string`
|
|
44
|
+
* so a raw, unnarrowed `request.method` can be tested directly.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* METHODS.has('GET') // true
|
|
49
|
+
* METHODS.has('TRACE') // false
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
var METHODS = Object.freeze(new Set(METHOD_LIST));
|
|
53
|
+
/**
|
|
54
|
+
* Names the specificity tier for a **literal** path segment (`/users`) — the highest
|
|
32
55
|
* tier, always outranking a param or wildcard segment at the same position.
|
|
33
56
|
*
|
|
34
57
|
* @remarks
|
|
35
|
-
* Consumed by `computeSpecificity` (
|
|
36
|
-
* matches left-to-right at the earliest differing segment
|
|
58
|
+
* Consumed by `computeSpecificity` (the path compiler in `helpers.ts`) when ranking candidate
|
|
59
|
+
* matches left-to-right at the earliest differing segment.
|
|
37
60
|
*
|
|
38
61
|
* @example
|
|
39
62
|
* ```ts
|
|
@@ -42,11 +65,11 @@ var METHODS = Object.freeze(/* @__PURE__ */ new Set([
|
|
|
42
65
|
*/
|
|
43
66
|
var TIER_LITERAL = 2;
|
|
44
67
|
/**
|
|
45
|
-
*
|
|
68
|
+
* Names the specificity tier for a **param** path segment (`:name`) — ranks below a
|
|
46
69
|
* literal segment and above a wildcard segment at the same position.
|
|
47
70
|
*
|
|
48
71
|
* @remarks
|
|
49
|
-
* Consumed by `computeSpecificity` (
|
|
72
|
+
* Consumed by `computeSpecificity` (the path compiler in `helpers.ts`) alongside {@link TIER_LITERAL}
|
|
50
73
|
* and {@link TIER_WILDCARD}.
|
|
51
74
|
*
|
|
52
75
|
* @example
|
|
@@ -56,12 +79,12 @@ var TIER_LITERAL = 2;
|
|
|
56
79
|
*/
|
|
57
80
|
var TIER_PARAM = 1;
|
|
58
81
|
/**
|
|
59
|
-
*
|
|
82
|
+
* Names the specificity tier for a **wildcard** path segment (`*name`) — the lowest
|
|
60
83
|
* tier; a wildcard only ever wins against another wildcard shape (an
|
|
61
84
|
* equal-specificity tie resolved by registration order).
|
|
62
85
|
*
|
|
63
86
|
* @remarks
|
|
64
|
-
* Consumed by `computeSpecificity` (
|
|
87
|
+
* Consumed by `computeSpecificity` (the path compiler in `helpers.ts`).
|
|
65
88
|
*
|
|
66
89
|
* @example
|
|
67
90
|
* ```ts
|
|
@@ -72,7 +95,7 @@ var TIER_WILDCARD = 0;
|
|
|
72
95
|
//#endregion
|
|
73
96
|
//#region src/core/helpers.ts
|
|
74
97
|
/**
|
|
75
|
-
*
|
|
98
|
+
* Escapes every regex metacharacter in a literal string so it can be embedded
|
|
76
99
|
* inside a larger `RegExp` source without being interpreted as syntax.
|
|
77
100
|
*
|
|
78
101
|
* @remarks
|
|
@@ -95,7 +118,7 @@ function escapeRegExp(value) {
|
|
|
95
118
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
96
119
|
}
|
|
97
120
|
/**
|
|
98
|
-
*
|
|
121
|
+
* Canonicalizes a route path for REGISTRY IDENTITY — strips a single trailing
|
|
99
122
|
* slash, except the root `/` (and the empty pattern). The trailing-slash fold
|
|
100
123
|
* {@link compilePath} normalizes a pattern through, so identity agrees with the
|
|
101
124
|
* matcher.
|
|
@@ -122,7 +145,7 @@ function canonicalizePath(path) {
|
|
|
122
145
|
return path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
|
|
123
146
|
}
|
|
124
147
|
/**
|
|
125
|
-
*
|
|
148
|
+
* Computes the registry key for a method-dimensioned dispatcher route.
|
|
126
149
|
*
|
|
127
150
|
* @remarks
|
|
128
151
|
* Combines the route record's HTTP method with the outer entry's canonical
|
|
@@ -141,7 +164,7 @@ function computeDispatchKey(entry) {
|
|
|
141
164
|
return `${entry.meta.method} ${canonicalizePath(entry.path)}`;
|
|
142
165
|
}
|
|
143
166
|
/**
|
|
144
|
-
*
|
|
167
|
+
* Compiles a route path pattern into an anchored regex and its ordered param
|
|
145
168
|
* names.
|
|
146
169
|
*
|
|
147
170
|
* @remarks
|
|
@@ -149,7 +172,7 @@ function computeDispatchKey(entry) {
|
|
|
149
172
|
* `([^/]+)` capture group; the FINAL segment may instead be `*name`, which
|
|
150
173
|
* becomes a `(.+)` capture spanning the REST of the path including slashes — a
|
|
151
174
|
* wildcard segment anywhere but last is a registration-time programmer error
|
|
152
|
-
* and throws `
|
|
175
|
+
* and throws a `ContractError` at the construction/registration boundary. Every regex
|
|
153
176
|
* metacharacter in a literal segment is escaped first ({@link escapeRegExp}),
|
|
154
177
|
* so a path like `/files/:name.json` matches the `.` literally apart from the
|
|
155
178
|
* param. The regex is anchored (`^…$`), so it matches the whole pathname, not
|
|
@@ -167,10 +190,12 @@ function computeDispatchKey(entry) {
|
|
|
167
190
|
* regex flag, so `/Users` matches `/users`. The pattern's own casing is never
|
|
168
191
|
* altered — only the matching behavior.
|
|
169
192
|
*
|
|
170
|
-
* @param path - The route path pattern (
|
|
171
|
-
* @param sensitive -
|
|
193
|
+
* @param path - The route path pattern (for example `/users/:id`, `/files/*rest`)
|
|
194
|
+
* @param sensitive - If `true`, matching is case-sensitive; if `false`, case is
|
|
195
|
+
* folded during matching. Default: `true`
|
|
172
196
|
* @returns The {@link CompiledPath} — its `regex` + ordered `params`
|
|
173
|
-
* @throws {
|
|
197
|
+
* @throws {@link import('@orkestrel/contract').ContractError} Thrown when a
|
|
198
|
+
* `*name` wildcard segment is not the FINAL segment
|
|
174
199
|
*
|
|
175
200
|
* @example
|
|
176
201
|
* ```ts
|
|
@@ -189,7 +214,14 @@ function compilePath(path, sensitive = true) {
|
|
|
189
214
|
const segments = normalized.split("/");
|
|
190
215
|
const pattern = segments.map((segment, index) => {
|
|
191
216
|
const isFinal = index === segments.length - 1;
|
|
192
|
-
if (!isFinal && /^\*[A-Za-z_]\w*/.test(segment)) throw new
|
|
217
|
+
if (!isFinal && /^\*[A-Za-z_]\w*/.test(segment)) throw new ContractError("a wildcard segment must be the final segment of a path pattern", {
|
|
218
|
+
code: "placement",
|
|
219
|
+
context: {
|
|
220
|
+
path: ["path"],
|
|
221
|
+
limit: `a wildcard only in the final segment, not "${segment}"`,
|
|
222
|
+
received: preview(path)
|
|
223
|
+
}
|
|
224
|
+
});
|
|
193
225
|
const tier = classifySegment(segment, isFinal);
|
|
194
226
|
if (tier === 0) {
|
|
195
227
|
params.push(segment.slice(1));
|
|
@@ -210,13 +242,13 @@ function compilePath(path, sensitive = true) {
|
|
|
210
242
|
};
|
|
211
243
|
}
|
|
212
244
|
/**
|
|
213
|
-
* URL-
|
|
245
|
+
* URL-decodes one captured param value, tolerating a malformed percent-escape —
|
|
214
246
|
* the decode {@link matchPath} applies to each captured group.
|
|
215
247
|
*
|
|
216
248
|
* @remarks
|
|
217
249
|
* A bad `%` sequence is not a reason to reject an otherwise-matching route, so
|
|
218
250
|
* a `decodeURIComponent` that would throw falls back to the raw value
|
|
219
|
-
* (mirroring the cookie / token boundary readers
|
|
251
|
+
* (mirroring the cookie / token boundary readers). Total — never
|
|
220
252
|
* throws.
|
|
221
253
|
*
|
|
222
254
|
* @param value - The raw captured param value
|
|
@@ -237,7 +269,7 @@ function decodeParam(value) {
|
|
|
237
269
|
}
|
|
238
270
|
}
|
|
239
271
|
/**
|
|
240
|
-
*
|
|
272
|
+
* Extracts the URL-decoded params a compiled path captures from a concrete
|
|
241
273
|
* pathname, or `undefined` when the pathname does not match.
|
|
242
274
|
*
|
|
243
275
|
* @remarks
|
|
@@ -248,7 +280,7 @@ function decodeParam(value) {
|
|
|
248
280
|
* for a parameterless path). Total — never throws.
|
|
249
281
|
*
|
|
250
282
|
* @param compiled - The {@link CompiledPath} from {@link compilePath}
|
|
251
|
-
* @param pathname - The concrete request pathname to match (
|
|
283
|
+
* @param pathname - The concrete request pathname to match (for example `/users/7`)
|
|
252
284
|
* @returns The decoded params on a hit, or `undefined` on a miss
|
|
253
285
|
*
|
|
254
286
|
* @example
|
|
@@ -271,10 +303,10 @@ function matchPath(compiled, pathname) {
|
|
|
271
303
|
return Object.freeze(params);
|
|
272
304
|
}
|
|
273
305
|
/**
|
|
274
|
-
*
|
|
306
|
+
* Classifies one path segment into its specificity TIER — the SAME syntax
|
|
275
307
|
* {@link compilePath} rewrites: a syntactically valid `:name` head is a PARAM
|
|
276
308
|
* segment, a final `*name` is a WILDCARD segment, everything else (including a
|
|
277
|
-
* literal segment that merely CONTAINS a `:` mid-string,
|
|
309
|
+
* literal segment that merely CONTAINS a `:` mid-string, for example `a:b`) is a
|
|
278
310
|
* LITERAL segment.
|
|
279
311
|
*
|
|
280
312
|
* @remarks
|
|
@@ -282,11 +314,12 @@ function matchPath(compiled, pathname) {
|
|
|
282
314
|
* segment `includes(':')` as a param, so a literal segment like `a:b` was
|
|
283
315
|
* mis-tiered even though {@link compilePath} compiles it literally. Sharing one
|
|
284
316
|
* segment parser between compilation and classification keeps the two in
|
|
285
|
-
* agreement
|
|
317
|
+
* agreement. Pure and total.
|
|
286
318
|
*
|
|
287
319
|
* @param segment - One `/`-split path segment
|
|
288
|
-
* @param isFinal -
|
|
289
|
-
*
|
|
320
|
+
* @param isFinal - If `true`, `segment` is the path's last segment and may
|
|
321
|
+
* classify as a wildcard; if `false`, a wildcard-shaped segment classifies as
|
|
322
|
+
* a literal
|
|
290
323
|
* @returns The segment's specificity tier — {@link import('./constants.js').TIER_LITERAL},
|
|
291
324
|
* {@link import('./constants.js').TIER_PARAM}, or
|
|
292
325
|
* {@link import('./constants.js').TIER_WILDCARD}
|
|
@@ -305,15 +338,15 @@ function classifySegment(segment, isFinal) {
|
|
|
305
338
|
return 2;
|
|
306
339
|
}
|
|
307
340
|
/**
|
|
308
|
-
*
|
|
341
|
+
* Computes a route path's SPECIFICITY VECTOR — the per-segment type ranking
|
|
309
342
|
* that breaks a tie when several registered routes match the same concrete
|
|
310
343
|
* pathname.
|
|
311
344
|
*
|
|
312
345
|
* @remarks
|
|
313
346
|
* Splits the CANONICALIZED path into segments (on `/`) and maps each to its
|
|
314
|
-
* specificity tier
|
|
347
|
+
* specificity tier through {@link classifySegment} — the same segment parser
|
|
315
348
|
* {@link compilePath} uses, so a literal segment that merely contains a `:`
|
|
316
|
-
* (
|
|
349
|
+
* (for example `a:b`) is correctly tiered as literal rather than param (the old
|
|
317
350
|
* engine's bug, fixed here). The standard route-precedence rule compares two
|
|
318
351
|
* matching routes' vectors LEFT-TO-RIGHT: at the first index where the tiers
|
|
319
352
|
* differ, the HIGHER tier (a literal over a param over a wildcard) is MORE
|
|
@@ -323,7 +356,7 @@ function classifySegment(segment, isFinal) {
|
|
|
323
356
|
* count in the common case; {@link compareSpecificity} handles the general
|
|
324
357
|
* case for totality.
|
|
325
358
|
*
|
|
326
|
-
* @param path - The route path pattern (
|
|
359
|
+
* @param path - The route path pattern (for example `/users/:id`)
|
|
327
360
|
* @returns The per-segment specificity tiers, in order
|
|
328
361
|
*
|
|
329
362
|
* @example
|
|
@@ -339,7 +372,7 @@ function computeSpecificity(path) {
|
|
|
339
372
|
return segments.map((segment, index) => classifySegment(segment, index === segments.length - 1));
|
|
340
373
|
}
|
|
341
374
|
/**
|
|
342
|
-
*
|
|
375
|
+
* Compares two route paths by SPECIFICITY — the comparator that picks the
|
|
343
376
|
* most-specific matching route (literal-over-param-over-wildcard,
|
|
344
377
|
* registration-order-independent).
|
|
345
378
|
*
|
|
@@ -376,45 +409,20 @@ function compareSpecificity(a, b) {
|
|
|
376
409
|
return 0;
|
|
377
410
|
}
|
|
378
411
|
/**
|
|
379
|
-
*
|
|
380
|
-
* never throws.
|
|
381
|
-
*
|
|
382
|
-
* @remarks
|
|
383
|
-
* Guarded via {@link import('./constants.js').METHODS} (the seven registrable
|
|
384
|
-
* HTTP methods); any other value (an unknown verb, non-uppercase casing)
|
|
385
|
-
* resolves to `undefined` rather than throwing (§14 guard totality). Pure
|
|
386
|
-
* leaf shared by the `Dispatcher`'s `handle` (§5.1 unknown-verb honesty) and
|
|
387
|
-
* anywhere else a raw method string needs narrowing.
|
|
388
|
-
*
|
|
389
|
-
* @param value - The raw `request.method` string to narrow
|
|
390
|
-
* @returns The matching {@link Method}, or `undefined` when `value` is not one
|
|
391
|
-
* of the seven registrable methods
|
|
392
|
-
*
|
|
393
|
-
* @example
|
|
394
|
-
* ```ts
|
|
395
|
-
* parseMethod('GET') // 'GET'
|
|
396
|
-
* parseMethod('PURGE') // undefined
|
|
397
|
-
* parseMethod('get') // undefined — case-sensitive
|
|
398
|
-
* ```
|
|
399
|
-
*/
|
|
400
|
-
function parseMethod(value) {
|
|
401
|
-
if (value === "GET" || value === "POST" || value === "PUT" || value === "PATCH" || value === "DELETE" || value === "HEAD" || value === "OPTIONS") return value;
|
|
402
|
-
}
|
|
403
|
-
/**
|
|
404
|
-
* Join a group prefix and a route path into one `/`-prefixed path, normalizing
|
|
412
|
+
* Joins a group prefix and a route path into one `/`-prefixed path, normalizing
|
|
405
413
|
* duplicate or missing joining slashes.
|
|
406
414
|
*
|
|
407
415
|
* @remarks
|
|
408
416
|
* {@link import('./types.js').GroupInterface} / {@link import('./types.js').DispatchGroupInterface}
|
|
409
417
|
* compose a prefix with each registered entry's path this way — pure string
|
|
410
|
-
* composition
|
|
418
|
+
* composition, no independent state. Both a duplicated slash
|
|
411
419
|
* (`'/api/'` + `'/users'`) and a missing one (`'/api'` + `'users'`) normalize
|
|
412
420
|
* to a single joining slash. An empty `prefix` returns `path` unchanged (after
|
|
413
421
|
* ensuring a leading slash); an empty `path` returns `prefix` unchanged.
|
|
414
422
|
* Pure and total.
|
|
415
423
|
*
|
|
416
|
-
* @param prefix - The group prefix (
|
|
417
|
-
* @param path - The route path being joined under the prefix (
|
|
424
|
+
* @param prefix - The group prefix (for example `/api`)
|
|
425
|
+
* @param path - The route path being joined under the prefix (for example `/users`)
|
|
418
426
|
* @returns The joined `/`-prefixed path
|
|
419
427
|
*
|
|
420
428
|
* @example
|
|
@@ -432,7 +440,7 @@ function joinPaths(prefix, path) {
|
|
|
432
440
|
return `${prefix.endsWith("/") ? prefix.slice(0, -1) : prefix}${path.startsWith("/") ? path : `/${path}`}`;
|
|
433
441
|
}
|
|
434
442
|
/**
|
|
435
|
-
*
|
|
443
|
+
* Provides an identity pass-through for a {@link RouteInput} that pins its `Path` generic
|
|
436
444
|
* to the LITERAL registration-site string, so `context.params` types
|
|
437
445
|
* correctly through {@link PathParams} without an explicit type argument.
|
|
438
446
|
*
|
|
@@ -441,26 +449,26 @@ function joinPaths(prefix, path) {
|
|
|
441
449
|
* `add` already infers `Path` as a literal at that call site — but the moment
|
|
442
450
|
* the object is built through an intermediate binding (a local `const route =
|
|
443
451
|
* { method, path, handler }`) TypeScript widens `path` to `string` unless the
|
|
444
|
-
* binding's own type is pinned. Wrapping the literal in `
|
|
445
|
-
* that pin: its `const Path extends string` type parameter infers the
|
|
446
|
-
* literal from the call, and the function returns its input completely
|
|
452
|
+
* binding's own type is pinned. Wrapping the literal in `defineRoute(...)`
|
|
453
|
+
* supplies that pin: its `const Path extends string` type parameter infers the
|
|
454
|
+
* NARROW literal from the call, and the function returns its input completely
|
|
447
455
|
* unchanged (same reference, no cloning, no validation) — this is a
|
|
448
456
|
* compile-time typing aid only, not a construction step (contrast
|
|
449
457
|
* {@link import('./factories.js')} `create*` entity factories). A
|
|
450
|
-
* heterogeneous `RouteInput[]` built from several `
|
|
451
|
-
* widens each element's `Path` to `string`
|
|
452
|
-
*
|
|
458
|
+
* heterogeneous `RouteInput[]` built from several `defineRoute(...)` calls
|
|
459
|
+
* still widens each element's `Path` to `string` after collection into one array —
|
|
460
|
+
* the realistic ceiling this helper raises is PER-CALL typing at the
|
|
453
461
|
* registration site, not a stored, still-literal-typed record.
|
|
454
462
|
*
|
|
455
463
|
* @typeParam Path - The route path pattern literal (drives `context.params`
|
|
456
|
-
*
|
|
464
|
+
* through {@link PathParams})
|
|
457
465
|
* @typeParam TState - The consumer's opaque per-request state type
|
|
458
466
|
* @param input - The {@link RouteInput} to pass through unchanged
|
|
459
467
|
* @returns `input`, unchanged (same reference)
|
|
460
468
|
*
|
|
461
469
|
* @example
|
|
462
470
|
* ```ts
|
|
463
|
-
* const input =
|
|
471
|
+
* const input = defineRoute({
|
|
464
472
|
* method: 'GET',
|
|
465
473
|
* path: '/users/:id',
|
|
466
474
|
* handler: (_request, context) => new Response(context.params.id), // typed string
|
|
@@ -468,21 +476,49 @@ function joinPaths(prefix, path) {
|
|
|
468
476
|
* dispatcher.add(input)
|
|
469
477
|
* ```
|
|
470
478
|
*/
|
|
471
|
-
function
|
|
479
|
+
function defineRoute(input) {
|
|
472
480
|
return input;
|
|
473
481
|
}
|
|
474
482
|
//#endregion
|
|
483
|
+
//#region src/core/parsers.ts
|
|
484
|
+
/**
|
|
485
|
+
* Narrows a raw `request.method` string into a typed {@link Method} — total,
|
|
486
|
+
* never throws.
|
|
487
|
+
*
|
|
488
|
+
* @remarks
|
|
489
|
+
* Consults {@link import('./constants.js').METHOD_LIST} (the one home for the
|
|
490
|
+
* registrable HTTP methods), so a verb added there narrows here without
|
|
491
|
+
* a second list to update; any other value (an unknown verb, non-uppercase
|
|
492
|
+
* casing) resolves to `undefined` rather than throwing (total guard behavior).
|
|
493
|
+
* Pure leaf shared by the `Dispatcher`'s `handle` (honest about an unknown verb)
|
|
494
|
+
* and anywhere else a raw method string needs narrowing.
|
|
495
|
+
*
|
|
496
|
+
* @param value - The raw `request.method` string to narrow
|
|
497
|
+
* @returns The matching {@link Method}, or `undefined` when `value` is not a
|
|
498
|
+
* registrable method
|
|
499
|
+
*
|
|
500
|
+
* @example
|
|
501
|
+
* ```ts
|
|
502
|
+
* parseMethod('GET') // 'GET'
|
|
503
|
+
* parseMethod('PURGE') // undefined
|
|
504
|
+
* parseMethod('get') // undefined — case-sensitive
|
|
505
|
+
* ```
|
|
506
|
+
*/
|
|
507
|
+
function parseMethod(value) {
|
|
508
|
+
return METHOD_LIST.find((method) => method === value);
|
|
509
|
+
}
|
|
510
|
+
//#endregion
|
|
475
511
|
//#region src/core/Group.ts
|
|
476
512
|
/**
|
|
477
|
-
*
|
|
478
|
-
* pure string composition
|
|
513
|
+
* Represents a prefix-scoped registration handle over a {@link import('./Router.js').Router} —
|
|
514
|
+
* pure string composition, no independent state or storage.
|
|
479
515
|
*
|
|
480
516
|
* @typeParam Meta - The entry payload type, matching the owning router
|
|
481
517
|
*
|
|
482
518
|
* @remarks
|
|
483
519
|
* Every `add` composes `entry.path` as `joinPaths(prefix, entry.path)` and
|
|
484
520
|
* forwards to the OWNING router, so grouped routes land in the SAME registry.
|
|
485
|
-
* `group(prefix)` nests, composing prefixes
|
|
521
|
+
* `group(prefix)` nests, composing prefixes through {@link joinPaths}.
|
|
486
522
|
*
|
|
487
523
|
* @example
|
|
488
524
|
* ```ts
|
|
@@ -515,7 +551,7 @@ var Group = class Group {
|
|
|
515
551
|
//#endregion
|
|
516
552
|
//#region src/core/Router.ts
|
|
517
553
|
/**
|
|
518
|
-
*
|
|
554
|
+
* Represents the path-matching + registry engine — registers `{ path, meta, name? }`
|
|
519
555
|
* entries (compiling each path once) and resolves a concrete pathname to the
|
|
520
556
|
* MOST SPECIFIC matching entry. The shared machine both the `Navigator`
|
|
521
557
|
* (browser) and the `Dispatcher` (core, method-dimensioned) compose.
|
|
@@ -523,18 +559,18 @@ var Group = class Group {
|
|
|
523
559
|
* @typeParam Meta - The opaque payload each entry carries and a match returns
|
|
524
560
|
*
|
|
525
561
|
* @remarks
|
|
526
|
-
* - **Registration boundary guard
|
|
527
|
-
* `path` — `isString` plus a leading `/` — and throws `
|
|
562
|
+
* - **Registration boundary guard.** `add` validates each entry's
|
|
563
|
+
* `path` — `isString` plus a leading `/` — and throws a `ContractError` on a
|
|
528
564
|
* malformed registration; `match` stays guard-free (the hot path).
|
|
529
565
|
* - **Compile-once.** Each path is compiled exactly once at registration into
|
|
530
566
|
* a parallel `#compiled` array, so `match` runs only a cached `exec` per
|
|
531
567
|
* candidate.
|
|
532
|
-
* - **Dedup
|
|
568
|
+
* - **Dedup through `key`.** When `options.key` is set, an entry whose computed
|
|
533
569
|
* key already exists REPLACES the prior one IN PLACE (both the `#entries`
|
|
534
570
|
* and `#compiled` arrays, at the existing index) — last write wins, no
|
|
535
571
|
* engine rebuild. Omitted ⇒ every entry is kept, even duplicate paths.
|
|
536
572
|
* - **Groups.** `group(prefix)` returns a {@link GroupInterface} that composes
|
|
537
|
-
* `prefix` onto every entry it registers, nesting
|
|
573
|
+
* `prefix` onto every entry it registers, nesting through {@link joinPaths}.
|
|
538
574
|
*
|
|
539
575
|
* @example
|
|
540
576
|
* ```ts
|
|
@@ -603,7 +639,22 @@ var Router = class {
|
|
|
603
639
|
this.#index.clear();
|
|
604
640
|
}
|
|
605
641
|
#register(entry) {
|
|
606
|
-
if (!isString(entry.path)
|
|
642
|
+
if (!isString(entry.path)) throw new ContractError("a route path must be a string", {
|
|
643
|
+
code: "literal",
|
|
644
|
+
context: {
|
|
645
|
+
path: ["entry", "path"],
|
|
646
|
+
limit: "string",
|
|
647
|
+
received: preview(entry.path)
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
if (!entry.path.startsWith("/")) throw new ContractError("a route path must start with \"/\"", {
|
|
651
|
+
code: "pattern",
|
|
652
|
+
context: {
|
|
653
|
+
path: ["entry", "path"],
|
|
654
|
+
limit: "a \"/\"-prefixed path pattern",
|
|
655
|
+
received: preview(entry.path)
|
|
656
|
+
}
|
|
657
|
+
});
|
|
607
658
|
const compiled = compilePath(entry.path, this.#sensitive);
|
|
608
659
|
if (this.#key === void 0) {
|
|
609
660
|
this.#entries.push(entry);
|
|
@@ -625,7 +676,7 @@ var Router = class {
|
|
|
625
676
|
//#endregion
|
|
626
677
|
//#region src/core/DispatchGroup.ts
|
|
627
678
|
/**
|
|
628
|
-
*
|
|
679
|
+
* Represents a prefix-scoped registration handle over a
|
|
629
680
|
* {@link import('./Dispatcher.js').Dispatcher} — the method-dimensioned
|
|
630
681
|
* counterpart of `Group` (`Group.ts`).
|
|
631
682
|
*
|
|
@@ -633,9 +684,9 @@ var Router = class {
|
|
|
633
684
|
* the owning dispatcher
|
|
634
685
|
*
|
|
635
686
|
* @remarks
|
|
636
|
-
* Every `add` composes `input.path`
|
|
637
|
-
* `this.prefix` and forwards to the OWNING dispatcher's `add` (its own
|
|
638
|
-
* boundary guard still applies). Pure string composition
|
|
687
|
+
* Every `add` composes `input.path` through {@link joinPaths} against
|
|
688
|
+
* `this.prefix` and forwards to the OWNING dispatcher's `add` (its own
|
|
689
|
+
* registration boundary guard still applies). Pure string composition — no
|
|
639
690
|
* independent state or storage.
|
|
640
691
|
*
|
|
641
692
|
* @example
|
|
@@ -668,20 +719,20 @@ var DispatchGroup = class DispatchGroup {
|
|
|
668
719
|
//#endregion
|
|
669
720
|
//#region src/core/Dispatcher.ts
|
|
670
721
|
/**
|
|
671
|
-
*
|
|
722
|
+
* Represents the fetch-standard, method-dimensioned dispatch entity — layers HTTP method
|
|
672
723
|
* dispatch and web-standard `Request`/`Response` handling over one internal
|
|
673
724
|
* `Router<RouteRecord<TState>>`. The core machine the eventual server face
|
|
674
|
-
*
|
|
725
|
+
* and any fetch-native runtime consumes directly.
|
|
675
726
|
*
|
|
676
727
|
* @typeParam TState - The consumer's opaque per-request state type
|
|
677
728
|
*
|
|
678
729
|
* @remarks
|
|
679
730
|
* - **Dedup by `method + canonicalizePath`.** The underlying `Router` is
|
|
680
731
|
* constructed with a `key` function so registering the same method+path
|
|
681
|
-
* twice REPLACES the prior route in place
|
|
682
|
-
* - **Registration boundary guard
|
|
732
|
+
* twice REPLACES the prior route in place.
|
|
733
|
+
* - **Registration boundary guard.** `add` validates each input's
|
|
683
734
|
* `handler` (`isFunction`) and `method` (must be in {@link METHODS}) —
|
|
684
|
-
* throws `
|
|
735
|
+
* throws a `ContractError` on a malformed registration; path validation is
|
|
685
736
|
* delegated to the underlying `Router`'s own guard. `match`/`handle` stay
|
|
686
737
|
* guard-free.
|
|
687
738
|
* - **Auto-`HEAD` / auto-`OPTIONS`.** A `HEAD` request with no registered
|
|
@@ -689,8 +740,8 @@ var DispatchGroup = class DispatchGroup {
|
|
|
689
740
|
* body; an `OPTIONS` request with no registered `OPTIONS` route answers
|
|
690
741
|
* `204` with a derived `Allow` header.
|
|
691
742
|
* - **Handler throws propagate.** `handle` never invents an error boundary —
|
|
692
|
-
* a handler throw reaches the caller uncaught
|
|
693
|
-
* - **Emitter
|
|
743
|
+
* a handler throw reaches the caller uncaught.
|
|
744
|
+
* - **Emitter.** Owns a `#emitter` for {@link DispatcherEventMap};
|
|
694
745
|
* `match`/`miss` fire AFTER resolution, before the handler/responder runs.
|
|
695
746
|
*
|
|
696
747
|
* @example
|
|
@@ -705,7 +756,7 @@ var DispatchGroup = class DispatchGroup {
|
|
|
705
756
|
* ```
|
|
706
757
|
*/
|
|
707
758
|
var Dispatcher = class {
|
|
708
|
-
router;
|
|
759
|
+
#router;
|
|
709
760
|
#emitter;
|
|
710
761
|
#unmatched;
|
|
711
762
|
#unmethoded;
|
|
@@ -713,7 +764,7 @@ var Dispatcher = class {
|
|
|
713
764
|
const sensitive = options?.sensitive;
|
|
714
765
|
const on = options?.on;
|
|
715
766
|
const error = options?.error;
|
|
716
|
-
this
|
|
767
|
+
this.#router = new Router({
|
|
717
768
|
...sensitive === void 0 ? {} : { sensitive },
|
|
718
769
|
key: computeDispatchKey
|
|
719
770
|
});
|
|
@@ -725,6 +776,9 @@ var Dispatcher = class {
|
|
|
725
776
|
this.#unmethoded = options?.unmethoded;
|
|
726
777
|
if (options?.routes !== void 0) this.add(options.routes);
|
|
727
778
|
}
|
|
779
|
+
get router() {
|
|
780
|
+
return this.#router;
|
|
781
|
+
}
|
|
728
782
|
get emitter() {
|
|
729
783
|
return this.#emitter;
|
|
730
784
|
}
|
|
@@ -736,13 +790,13 @@ var Dispatcher = class {
|
|
|
736
790
|
return new DispatchGroup(this, prefix);
|
|
737
791
|
}
|
|
738
792
|
match(method, pathname) {
|
|
739
|
-
const hit = this
|
|
793
|
+
const hit = this.#router.match(pathname, (meta) => meta.method === method);
|
|
740
794
|
if (hit !== void 0) return {
|
|
741
795
|
status: "matched",
|
|
742
796
|
match: hit
|
|
743
797
|
};
|
|
744
798
|
if (method === "HEAD") {
|
|
745
|
-
const getHit = this
|
|
799
|
+
const getHit = this.#router.match(pathname, (meta) => meta.method === "GET");
|
|
746
800
|
if (getHit !== void 0) return {
|
|
747
801
|
status: "matched",
|
|
748
802
|
match: getHit
|
|
@@ -772,7 +826,10 @@ var Dispatcher = class {
|
|
|
772
826
|
const result = this.match(method, pathname);
|
|
773
827
|
if (result.status === "matched") return this.#respondMatched(request, state, method, result.match, url);
|
|
774
828
|
if (result.status === "unmethoded") {
|
|
775
|
-
if (method === "OPTIONS")
|
|
829
|
+
if (method === "OPTIONS") {
|
|
830
|
+
const hit = this.#router.match(pathname);
|
|
831
|
+
if (hit !== void 0) return this.#respondAutoOptions(hit.path, result.allow);
|
|
832
|
+
}
|
|
776
833
|
this.#emitter.emit("miss", method, pathname, "unmethoded");
|
|
777
834
|
return this.#respondUnmethoded(request, result.allow);
|
|
778
835
|
}
|
|
@@ -783,10 +840,24 @@ var Dispatcher = class {
|
|
|
783
840
|
this.#emitter.destroy();
|
|
784
841
|
}
|
|
785
842
|
#register(input) {
|
|
786
|
-
if (!isFunction(input.handler)) throw new
|
|
787
|
-
|
|
843
|
+
if (!isFunction(input.handler)) throw new ContractError("a route handler must be a function", {
|
|
844
|
+
code: "literal",
|
|
845
|
+
context: {
|
|
846
|
+
path: ["input", "handler"],
|
|
847
|
+
limit: "function",
|
|
848
|
+
received: preview(input.handler)
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
if (!isString(input.method) || !METHODS.has(input.method)) throw new ContractError("a route method must be a registrable HTTP method", {
|
|
852
|
+
code: "literal",
|
|
853
|
+
context: {
|
|
854
|
+
path: ["input", "method"],
|
|
855
|
+
limit: [...METHODS].join(", "),
|
|
856
|
+
received: preview(input.method)
|
|
857
|
+
}
|
|
858
|
+
});
|
|
788
859
|
const name = input.name;
|
|
789
|
-
this
|
|
860
|
+
this.#router.add({
|
|
790
861
|
path: input.path,
|
|
791
862
|
...name === void 0 ? {} : { name },
|
|
792
863
|
meta: {
|
|
@@ -797,7 +868,7 @@ var Dispatcher = class {
|
|
|
797
868
|
});
|
|
798
869
|
}
|
|
799
870
|
#allow(pathname) {
|
|
800
|
-
const entries = this
|
|
871
|
+
const entries = this.#router.entries(pathname);
|
|
801
872
|
const methods = /* @__PURE__ */ new Set();
|
|
802
873
|
for (const entry of entries) methods.add(entry.meta.method);
|
|
803
874
|
if (methods.has("GET")) methods.add("HEAD");
|
|
@@ -832,8 +903,8 @@ var Dispatcher = class {
|
|
|
832
903
|
});
|
|
833
904
|
return response;
|
|
834
905
|
}
|
|
835
|
-
#respondAutoOptions(
|
|
836
|
-
this.#emitter.emit("match", "OPTIONS",
|
|
906
|
+
#respondAutoOptions(pattern, allow) {
|
|
907
|
+
this.#emitter.emit("match", "OPTIONS", pattern);
|
|
837
908
|
const headers = new Headers({ Allow: [...allow, "OPTIONS"].join(", ") });
|
|
838
909
|
return new Response(null, {
|
|
839
910
|
status: 204,
|
|
@@ -844,7 +915,7 @@ var Dispatcher = class {
|
|
|
844
915
|
//#endregion
|
|
845
916
|
//#region src/core/factories.ts
|
|
846
917
|
/**
|
|
847
|
-
*
|
|
918
|
+
* Creates a {@link RouterInterface} — the pure path-matching + registry engine
|
|
848
919
|
* shared by the browser `Navigator` and the core `Dispatcher`.
|
|
849
920
|
*
|
|
850
921
|
* @remarks
|
|
@@ -870,7 +941,7 @@ function createRouter(options) {
|
|
|
870
941
|
return new Router(options);
|
|
871
942
|
}
|
|
872
943
|
/**
|
|
873
|
-
*
|
|
944
|
+
* Creates a {@link DispatcherInterface} — the fetch-standard, method-
|
|
874
945
|
* dimensioned dispatch entity over one internal `Router<RouteRecord<TState>>`.
|
|
875
946
|
*
|
|
876
947
|
* @remarks
|
|
@@ -880,8 +951,8 @@ function createRouter(options) {
|
|
|
880
951
|
* @typeParam TState - The consumer's opaque per-request state type (default
|
|
881
952
|
* `undefined` for stateless use)
|
|
882
953
|
* @param options - Optional initial `routes`, the `sensitive` case toggle,
|
|
883
|
-
* the `unmatched`/`unmethoded` default-responder overrides, and the
|
|
884
|
-
*
|
|
954
|
+
* the `unmatched`/`unmethoded` default-responder overrides, and the
|
|
955
|
+
* Emitter pattern's `on`/`error` wiring
|
|
885
956
|
* @returns A {@link DispatcherInterface}
|
|
886
957
|
*
|
|
887
958
|
* @example
|
|
@@ -900,6 +971,6 @@ function createDispatcher(options) {
|
|
|
900
971
|
return new Dispatcher(options);
|
|
901
972
|
}
|
|
902
973
|
//#endregion
|
|
903
|
-
export { DispatchGroup, Dispatcher, Group, METHODS, Router, TIER_LITERAL, TIER_PARAM, TIER_WILDCARD, canonicalizePath, classifySegment, compareSpecificity, compilePath, computeDispatchKey, computeSpecificity, createDispatcher, createRouter, decodeParam, escapeRegExp, joinPaths, matchPath, parseMethod
|
|
974
|
+
export { DispatchGroup, Dispatcher, Group, METHODS, METHOD_LIST, Router, TIER_LITERAL, TIER_PARAM, TIER_WILDCARD, canonicalizePath, classifySegment, compareSpecificity, compilePath, computeDispatchKey, computeSpecificity, createDispatcher, createRouter, decodeParam, defineRoute, escapeRegExp, joinPaths, matchPath, parseMethod };
|
|
904
975
|
|
|
905
976
|
//# sourceMappingURL=index.js.map
|