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