@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.
package/README.md CHANGED
@@ -1,9 +1,12 @@
1
1
  # @orkestrel/router
2
2
 
3
- A typed request router for the `@orkestrel` line the first `@orkestrel`
4
- package to ship both server and browser environments alongside its shared
5
- core. Built to sit beside `@orkestrel/contract` (validation) and
6
- `@orkestrel/emitter` (observable lifecycle), reusing both as it takes shape.
3
+ A typed request router for the `@orkestrel` line. One matching engine compiles
4
+ route patterns, extracts URL-decoded params, and resolves the most specific
5
+ match; a `Dispatcher` layers fetch-standard, method-dimensioned dispatch over
6
+ it; a headless `Navigator` drives History or hash navigation in the browser;
7
+ and a `node:http` adapter converts messages in both directions. Built on
8
+ `@orkestrel/contract` for validation, `@orkestrel/emitter` for the observable
9
+ surface, and `@orkestrel/abort` for cancellation.
7
10
 
8
11
  ## Install
9
12
 
@@ -13,8 +16,8 @@ npm install @orkestrel/router
13
16
 
14
17
  ## Requirements
15
18
 
16
- - Node.js >= 24
17
- - ESM-only (no CommonJS build)
19
+ - Node.js >= 22.12.0
20
+ - ESM and CommonJS for the core and `./server` entries; the `./browser` entry is ESM only.
18
21
  - Server and browser environments both supported
19
22
 
20
23
  ## Usage
@@ -41,8 +44,8 @@ const response = await dispatcher.handle(new Request('http://x/users/7'), { user
41
44
  `Router` is the shared registry-and-match engine — literal-over-param-over-wildcard
42
45
  precedence, trailing-slash folding, and tolerant percent-decoding — that both `Dispatcher`
43
46
  (fetch-standard, method-dimensioned) and the browser `Navigator` compose. Path params are
44
- inferred at the type level from the literal pattern via `PathParams`, and `route()` pins a
45
- `RouteInput`'s path so literal inference survives across call sites. The `./browser` entry
47
+ inferred at the type level from the literal pattern through `PathParams`, and `defineRoute()`
48
+ pins a `RouteInput`'s path so literal inference survives across call sites. The `./browser` entry
46
49
  adds `createNavigator` for headless History/hash navigation; the `./server` entry adds
47
50
  `buildRequest` / `sendResponse` / `createListener` for `node:http`.
48
51
 
@@ -50,11 +53,11 @@ adds `createNavigator` for headless History/hash navigation; the `./server` entr
50
53
 
51
54
  For the full surface — the core `Router`, the `Dispatcher`, the browser `Navigator`, and the
52
55
  `node:http` server adapter — see
53
- [`guides/src/router.md`](guides/src/router.md).
56
+ [`guides/router.md`](guides/router.md).
54
57
 
55
58
  ## Package
56
59
 
57
- Published as three environment-scoped entry points per the `exports` field in
60
+ Published as environment-scoped entry points per the `exports` field in
58
61
  `package.json`: a shared core, `./browser`, and `./server`.
59
62
 
60
63
  ## License
@@ -6,28 +6,27 @@ import { RouterInterface } from '@orkestrel/router';
6
6
  import { RouterMatch } from '@orkestrel/router';
7
7
 
8
8
  /**
9
- * Compute the registry key for a browser navigation route.
9
+ * Computes the registry key for a browser navigation route.
10
10
  *
11
11
  * @remarks
12
- * Projects the nested route's path through the core engine's canonical
13
- * trailing-slash identity, so `/users` and `/users/` replace one another in
14
- * the Navigator's shared Router.
12
+ * Projects the route's path through the core engine's canonical trailing-slash
13
+ * identity, so `/users` and `/users/` replace one another in the Navigator's
14
+ * shared Router. The entry's `meta` payload is never read, so any payload type
15
+ * is accepted.
15
16
  *
16
- * @param entry - The outer Router entry carrying the Navigator route
17
- * @returns The nested route's canonical path
17
+ * @param entry - The Router entry carrying the Navigator route
18
+ * @returns The route's canonical path
18
19
  *
19
20
  * @example
20
21
  * ```ts
21
- * computeNavigationKey({ path: '/users/', meta: { path: '/users/' } }) // '/users'
22
+ * computeNavigationKey({ path: '/users/', meta: {} }) // '/users'
22
23
  * ```
23
24
  */
24
- export declare function computeNavigationKey(entry: RouteEntry<{
25
- readonly path: string;
26
- }>): string;
25
+ export declare function computeNavigationKey(entry: RouteEntry<unknown>): string;
27
26
 
28
27
  /**
29
- * Create a {@link NavigatorInterface} — the headless History/hash navigation
30
- * entity composing one core `Router<RouteEntry<Meta>>`.
28
+ * Creates a {@link NavigatorInterface} — the headless History/hash navigation
29
+ * entity composing one core `Router<Meta>`.
31
30
  *
32
31
  * @remarks
33
32
  * Prefer this over `new Navigator(...)` at call sites that only need the
@@ -37,7 +36,7 @@ export declare function computeNavigationKey(entry: RouteEntry<{
37
36
  * @param options - The `routes` to register, the `history` toggle (default
38
37
  * `false`, hash mode), an optional `base` (history mode), an optional
39
38
  * `fallback` path, an optional `guard` hook, opt-in link `intercept`
40
- * (history mode), the `sensitive` case toggle, and the AGENTS §13 emitter
39
+ * (history mode), the `sensitive` case toggle, and the Emitter pattern's
41
40
  * `on`/`error` wiring
42
41
  * @returns A live {@link NavigatorInterface} handle — call `start()` to begin
43
42
  * dispatching
@@ -59,17 +58,17 @@ export declare function computeNavigationKey(entry: RouteEntry<{
59
58
  export declare function createNavigator<Meta>(options: NavigatorOptions<Meta>): NavigatorInterface<Meta>;
60
59
 
61
60
  /**
62
- * Extract the `/`-prefixed pathname from a `location.hash` value — strip the
61
+ * Extracts the `/`-prefixed pathname from a `location.hash` value — strips the
63
62
  * leading `#` (keeping the route's own leading `/`) and any `?query` suffix.
64
63
  *
65
64
  * @remarks
66
- * The grammar this package matches everywhere is `/`-prefixed (§4 path
67
- * grammar), so a hash-mode location's `'#/users/7?x'` becomes `'/users/7'`
65
+ * The grammar this package matches everywhere is `/`-prefixed, so a hash-mode
66
+ * location's `'#/users/7?x'` becomes `'/users/7'`
68
67
  * — a hash pattern is expected to start `'#/'`; anything else (an empty hash,
69
68
  * or one that does not begin `'#/'`) yields `''` (the `Navigator` then falls
70
69
  * back). Total — never throws.
71
70
  *
72
- * @param hash - The raw `window.location.hash` value (e.g. `'#/users/7?x'`)
71
+ * @param hash - The raw `window.location.hash` value (for example `'#/users/7?x'`)
73
72
  * @returns The `/`-prefixed pathname to match, or `''` for an empty / non-`#/` hash
74
73
  *
75
74
  * @example
@@ -83,7 +82,7 @@ export declare function createNavigator<Meta>(options: NavigatorOptions<Meta>):
83
82
  export declare function extractHashPath(hash: string): string;
84
83
 
85
84
  /**
86
- * Find the nearest enclosing `<a>` element a DOM event originated from, by
85
+ * Finds the nearest enclosing `<a>` element a DOM event originated from, by
87
86
  * walking its composed path — the pure lookup behind history-mode link
88
87
  * interception.
89
88
  *
@@ -107,10 +106,10 @@ export declare function extractHashPath(hash: string): string;
107
106
  export declare function findAnchor(event: Event): HTMLAnchorElement | undefined;
108
107
 
109
108
  /**
110
- * The headless History/hash navigation entity — composes one core
111
- * `Router<RouteEntry<Meta>>`, resolving the current location on `start()` and
109
+ * Represents the headless History/hash navigation entity — composes one core
110
+ * `Router<Meta>`, resolving the current location on `start()` and
112
111
  * every subsequent navigation event, tracking `active`, and emitting
113
- * `navigate` through the core {@link Emitter} (AGENTS §13). No `render` /
112
+ * `navigate` through the core {@link Emitter}. No `render` /
114
113
  * `outlet` — the consumer owns rendering.
115
114
  *
116
115
  * @typeParam Meta - The opaque per-route payload a match carries back
@@ -120,14 +119,13 @@ export declare function findAnchor(event: Event): HTMLAnchorElement | undefined;
120
119
  * `Router` machine the core `Dispatcher` composes, keyed for dedup by its
121
120
  * {@link canonicalizePath} (last write wins, replace-in-place) — literal-
122
121
  * over-param precedence, trailing-slash insensitivity, and
123
- * `:param`/`*wildcard` extraction all come from that one engine (AGENTS
124
- * §21).
122
+ * `:param`/`*wildcard` extraction all come from that one shared engine.
125
123
  * - **Resolve pipeline.** Compute the `/`-prefixed pathname to match
126
124
  * ({@link resolveLocationPath}) → {@link match} it → on a miss, match the
127
125
  * `fallback` through the SAME engine → a fallback that ALSO matches nothing
128
126
  * aborts any pending guarded navigation (a miss SUPERSEDES it, same as a
129
- * newer navigation) and leaves `active` `undefined`, emitting nothing
130
- * (§21-honest: no phantom match is fabricated) → the optional `guard` may
127
+ * newer navigation) and leaves `active` `undefined`, emitting nothing
128
+ * honest to the one-shared-engine rule: no phantom match is fabricated → the optional `guard` may
131
129
  * veto → on a verdict, `active` is set and `navigate` emitted.
132
130
  * - **Supersede-safe guard.** Every navigation mints an `@orkestrel/abort`
133
131
  * handle, aborting the PREVIOUS navigation's handle first; a guard verdict
@@ -156,7 +154,7 @@ export declare function findAnchor(event: Event): HTMLAnchorElement | undefined;
156
154
  declare class Navigator_2<Meta> implements NavigatorInterface<Meta> {
157
155
  #private;
158
156
  constructor(options: NavigatorOptions<Meta>);
159
- get router(): RouterInterface<RouteEntry<Meta>>;
157
+ get router(): RouterInterface<Meta>;
160
158
  get emitter(): EmitterInterface<NavigatorEventMap<Meta>>;
161
159
  get active(): RouterMatch<Meta> | undefined;
162
160
  start(): void;
@@ -168,7 +166,7 @@ declare class Navigator_2<Meta> implements NavigatorInterface<Meta> {
168
166
  export { Navigator_2 as Navigator }
169
167
 
170
168
  /**
171
- * The `Navigator`'s event map (AGENTS §13) — the single `navigate` signal a
169
+ * Represents the `Navigator`'s event map — the single `navigate` signal a
172
170
  * consumer observes.
173
171
  *
174
172
  * @typeParam Meta - The opaque per-route payload the resolved match carries
@@ -177,25 +175,25 @@ export { Navigator_2 as Navigator }
177
175
  * `navigate` fires once per successful resolution (start, hashchange/popstate,
178
176
  * `navigate()`, link interception) — never for a vetoed or superseded navigation
179
177
  * ({@link NavigatorOptions.guard}), and never when a miss's fallback also
180
- * misses (§21-honest: `active` is left `undefined`, nothing emitted).
178
+ * misses — honest to the one-shared-engine rule: `active` is left `undefined`, nothing emitted.
181
179
  */
182
180
  export declare type NavigatorEventMap<Meta> = {
183
181
  readonly navigate: readonly [match: RouterMatch<Meta>];
184
182
  };
185
183
 
186
184
  /**
187
- * The headless History/hash navigation entity contract (the §4.5 behavioral-
185
+ * Represents the headless History/hash navigation entity contract (the behavioral-
188
186
  * interface role for the one-class-per-file `Navigator`). Composes a core
189
- * `Router<RouteEntry<Meta>>`, resolves the current location on `start()` and
187
+ * `Router<Meta>`, resolves the current location on `start()` and
190
188
  * on every subsequent navigation event, tracks `active`, and emits
191
- * `navigate` through the AGENTS §13 {@link EmitterInterface}.
189
+ * `navigate` through the {@link EmitterInterface}.
192
190
  *
193
191
  * @typeParam Meta - The opaque per-route payload a match carries back
194
192
  *
195
193
  * @remarks
196
194
  * - `router` — the underlying registry, exposed READONLY for introspection
197
195
  * (the same object routes were registered on).
198
- * - `emitter` — the AGENTS §13 observable surface for {@link NavigatorEventMap}.
196
+ * - `emitter` — the observable surface for {@link NavigatorEventMap}.
199
197
  * - `active` — the currently-resolved {@link RouterMatch}, or `undefined`
200
198
  * before the first resolve (or when a miss's fallback also misses).
201
199
  * - `start()` — begin listening (`hashchange` in hash mode; `popstate` +
@@ -208,10 +206,10 @@ export declare type NavigatorEventMap<Meta> = {
208
206
  * no `hashchange` would otherwise fire.
209
207
  * - `match(path)` — a PURE lookup through the underlying `Router`: no
210
208
  * location read, no fallback, no guard, no emit.
211
- * - `destroy()` — `stop()` plus tear down the `#emitter` (AGENTS §13).
209
+ * - `destroy()` — `stop()` plus tear down the `#emitter`.
212
210
  */
213
211
  export declare interface NavigatorInterface<Meta> {
214
- readonly router: RouterInterface<RouteEntry<Meta>>;
212
+ readonly router: RouterInterface<Meta>;
215
213
  readonly emitter: EmitterInterface<NavigatorEventMap<Meta>>;
216
214
  readonly active: RouterMatch<Meta> | undefined;
217
215
  start(): void;
@@ -222,8 +220,8 @@ export declare interface NavigatorInterface<Meta> {
222
220
  }
223
221
 
224
222
  /**
225
- * Options for `createNavigator` — the `routes` to dispatch between, the
226
- * navigation substrate, the optional guard hook, and the AGENTS §13 emitter
223
+ * Represents the options for `createNavigator` — the `routes` to dispatch between, the
224
+ * navigation substrate, the optional guard hook, and the Emitter pattern's
227
225
  * wiring.
228
226
  *
229
227
  * @typeParam Meta - The opaque payload each route may carry
@@ -240,20 +238,21 @@ export declare interface NavigatorInterface<Meta> {
240
238
  * - `fallback` — the route PATTERN to resolve when the current location
241
239
  * matches NOTHING. Omitted ⇒ the first route's path. A `fallback` that
242
240
  * itself matches no registered route leaves `active` `undefined` and emits
243
- * nothing (§21-honest: no phantom match is fabricated).
241
+ * nothing — honest to the one-shared-engine rule: no phantom match is fabricated.
244
242
  * - `guard` — `(to, from, signal) => boolean | Promise<boolean>`, called
245
243
  * before a navigation commits; a `false`/rejected verdict, or one arriving
246
244
  * after the navigation was SUPERSEDED (`signal.aborted`), is discarded —
247
245
  * `active` stays unchanged and nothing is emitted. `signal` fires when a
248
246
  * NEWER navigation starts (or on `stop`/`destroy`), so a slow async guard
249
247
  * can cancel its own work off it. A throw routes to the `error` handler
250
- * below and vetoes the navigation.
248
+ * described later in this list and vetoes the navigation.
251
249
  * - `intercept` — opt-in same-origin `<a>` click interception (history mode
252
250
  * only): a plain left-click on a same-origin link with no modifier keys,
253
251
  * no `target`, and no `download` attribute is intercepted into `navigate`.
254
252
  * - `sensitive` — forwarded to the underlying `Router` (default `true`).
255
- * - `on` — initial `NavigatorEventMap` listeners (AGENTS §8/§13).
256
- * - `error` — the emitter's listener-error handler (AGENTS §13); ALSO the
253
+ * - `on` — initial `NavigatorEventMap` listeners (the Emitter pattern's
254
+ * similar-surface pin).
255
+ * - `error` — the emitter's listener-error handler; ALSO the
257
256
  * handler a thrown {@link guard} routes to (the Navigator's own pipeline,
258
257
  * not a listener throw, so it is surfaced through the same channel).
259
258
  */
@@ -270,7 +269,7 @@ export declare interface NavigatorOptions<Meta> {
270
269
  }
271
270
 
272
271
  /**
273
- * Resolve the `/`-prefixed pathname to match for the CURRENT location, in
272
+ * Resolves the `/`-prefixed pathname to match for the CURRENT location, in
274
273
  * either navigation mode — the one seam `extractHashPath` (hash mode) and
275
274
  * history-mode base-stripping share.
276
275
  *
@@ -280,12 +279,12 @@ export declare interface NavigatorOptions<Meta> {
280
279
  * `location.pathname` and strips a leading `base` prefix when one is
281
280
  * configured: `base` itself maps to the root `'/'`; a pathname that is not
282
281
  * under `base` is returned unchanged (a base mismatch is not this helper's
283
- * concern — the `Navigator`'s match then simply misses). Total — never throws.
282
+ * concern — the `Navigator`'s match then misses). Total — never throws.
284
283
  *
285
284
  * @param location - The `hash` + `pathname` pair to resolve from (accepts a
286
285
  * real `Location` or any object shaped the same, for pure unit testing)
287
- * @param history - The navigation substrate: `false` for hash mode, `true`
288
- * for history mode
286
+ * @param history - If `true`, the pathname is read from `location.pathname`
287
+ * with `base` stripped; if `false`, it is read from `location.hash`
289
288
  * @param base - The history-mode path prefix to strip (ignored in hash mode;
290
289
  * omit for no prefix)
291
290
  * @returns The `/`-prefixed pathname to match
@@ -1,39 +1,40 @@
1
1
  import { canonicalizePath, createRouter, joinPaths } from "../core/index.js";
2
2
  import { createAbort } from "@orkestrel/abort";
3
3
  import { Emitter } from "@orkestrel/emitter";
4
- import { isFunction, isString } from "@orkestrel/contract";
4
+ import { ContractError, isFunction, isString, preview } from "@orkestrel/contract";
5
5
  //#region src/browser/helpers.ts
6
6
  /**
7
- * Compute the registry key for a browser navigation route.
7
+ * Computes the registry key for a browser navigation route.
8
8
  *
9
9
  * @remarks
10
- * Projects the nested route's path through the core engine's canonical
11
- * trailing-slash identity, so `/users` and `/users/` replace one another in
12
- * the Navigator's shared Router.
10
+ * Projects the route's path through the core engine's canonical trailing-slash
11
+ * identity, so `/users` and `/users/` replace one another in the Navigator's
12
+ * shared Router. The entry's `meta` payload is never read, so any payload type
13
+ * is accepted.
13
14
  *
14
- * @param entry - The outer Router entry carrying the Navigator route
15
- * @returns The nested route's canonical path
15
+ * @param entry - The Router entry carrying the Navigator route
16
+ * @returns The route's canonical path
16
17
  *
17
18
  * @example
18
19
  * ```ts
19
- * computeNavigationKey({ path: '/users/', meta: { path: '/users/' } }) // '/users'
20
+ * computeNavigationKey({ path: '/users/', meta: {} }) // '/users'
20
21
  * ```
21
22
  */
22
23
  function computeNavigationKey(entry) {
23
- return canonicalizePath(entry.meta.path);
24
+ return canonicalizePath(entry.path);
24
25
  }
25
26
  /**
26
- * Extract the `/`-prefixed pathname from a `location.hash` value — strip the
27
+ * Extracts the `/`-prefixed pathname from a `location.hash` value — strips the
27
28
  * leading `#` (keeping the route's own leading `/`) and any `?query` suffix.
28
29
  *
29
30
  * @remarks
30
- * The grammar this package matches everywhere is `/`-prefixed (§4 path
31
- * grammar), so a hash-mode location's `'#/users/7?x'` becomes `'/users/7'`
31
+ * The grammar this package matches everywhere is `/`-prefixed, so a hash-mode
32
+ * location's `'#/users/7?x'` becomes `'/users/7'`
32
33
  * — a hash pattern is expected to start `'#/'`; anything else (an empty hash,
33
34
  * or one that does not begin `'#/'`) yields `''` (the `Navigator` then falls
34
35
  * back). Total — never throws.
35
36
  *
36
- * @param hash - The raw `window.location.hash` value (e.g. `'#/users/7?x'`)
37
+ * @param hash - The raw `window.location.hash` value (for example `'#/users/7?x'`)
37
38
  * @returns The `/`-prefixed pathname to match, or `''` for an empty / non-`#/` hash
38
39
  *
39
40
  * @example
@@ -51,7 +52,7 @@ function extractHashPath(hash) {
51
52
  return queryIndex === -1 ? withoutHash : withoutHash.slice(0, queryIndex);
52
53
  }
53
54
  /**
54
- * Resolve the `/`-prefixed pathname to match for the CURRENT location, in
55
+ * Resolves the `/`-prefixed pathname to match for the CURRENT location, in
55
56
  * either navigation mode — the one seam `extractHashPath` (hash mode) and
56
57
  * history-mode base-stripping share.
57
58
  *
@@ -61,12 +62,12 @@ function extractHashPath(hash) {
61
62
  * `location.pathname` and strips a leading `base` prefix when one is
62
63
  * configured: `base` itself maps to the root `'/'`; a pathname that is not
63
64
  * under `base` is returned unchanged (a base mismatch is not this helper's
64
- * concern — the `Navigator`'s match then simply misses). Total — never throws.
65
+ * concern — the `Navigator`'s match then misses). Total — never throws.
65
66
  *
66
67
  * @param location - The `hash` + `pathname` pair to resolve from (accepts a
67
68
  * real `Location` or any object shaped the same, for pure unit testing)
68
- * @param history - The navigation substrate: `false` for hash mode, `true`
69
- * for history mode
69
+ * @param history - If `true`, the pathname is read from `location.pathname`
70
+ * with `base` stripped; if `false`, it is read from `location.hash`
70
71
  * @param base - The history-mode path prefix to strip (ignored in hash mode;
71
72
  * omit for no prefix)
72
73
  * @returns The `/`-prefixed pathname to match
@@ -89,7 +90,7 @@ function resolveLocationPath(location, history, base) {
89
90
  return pathname;
90
91
  }
91
92
  /**
92
- * Find the nearest enclosing `<a>` element a DOM event originated from, by
93
+ * Finds the nearest enclosing `<a>` element a DOM event originated from, by
93
94
  * walking its composed path — the pure lookup behind history-mode link
94
95
  * interception.
95
96
  *
@@ -116,10 +117,10 @@ function findAnchor(event) {
116
117
  //#endregion
117
118
  //#region src/browser/Navigator.ts
118
119
  /**
119
- * The headless History/hash navigation entity — composes one core
120
- * `Router<RouteEntry<Meta>>`, resolving the current location on `start()` and
120
+ * Represents the headless History/hash navigation entity — composes one core
121
+ * `Router<Meta>`, resolving the current location on `start()` and
121
122
  * every subsequent navigation event, tracking `active`, and emitting
122
- * `navigate` through the core {@link Emitter} (AGENTS §13). No `render` /
123
+ * `navigate` through the core {@link Emitter}. No `render` /
123
124
  * `outlet` — the consumer owns rendering.
124
125
  *
125
126
  * @typeParam Meta - The opaque per-route payload a match carries back
@@ -129,14 +130,13 @@ function findAnchor(event) {
129
130
  * `Router` machine the core `Dispatcher` composes, keyed for dedup by its
130
131
  * {@link canonicalizePath} (last write wins, replace-in-place) — literal-
131
132
  * over-param precedence, trailing-slash insensitivity, and
132
- * `:param`/`*wildcard` extraction all come from that one engine (AGENTS
133
- * §21).
133
+ * `:param`/`*wildcard` extraction all come from that one shared engine.
134
134
  * - **Resolve pipeline.** Compute the `/`-prefixed pathname to match
135
135
  * ({@link resolveLocationPath}) → {@link match} it → on a miss, match the
136
136
  * `fallback` through the SAME engine → a fallback that ALSO matches nothing
137
137
  * aborts any pending guarded navigation (a miss SUPERSEDES it, same as a
138
- * newer navigation) and leaves `active` `undefined`, emitting nothing
139
- * (§21-honest: no phantom match is fabricated) → the optional `guard` may
138
+ * newer navigation) and leaves `active` `undefined`, emitting nothing
139
+ * honest to the one-shared-engine rule: no phantom match is fabricated → the optional `guard` may
140
140
  * veto → on a verdict, `active` is set and `navigate` emitted.
141
141
  * - **Supersede-safe guard.** Every navigation mints an `@orkestrel/abort`
142
142
  * handle, aborting the PREVIOUS navigation's handle first; a guard verdict
@@ -171,16 +171,36 @@ var Navigator = class {
171
171
  #guard;
172
172
  #error;
173
173
  #intercept;
174
- #hashListener;
175
- #popListener;
174
+ #listener;
176
175
  #clickListener;
177
176
  #active;
178
177
  #started = false;
179
178
  #current;
180
179
  constructor(options) {
181
- if (options.guard !== void 0 && !isFunction(options.guard)) throw new TypeError(`a navigator guard must be a function, got ${JSON.stringify(options.guard)}`);
182
- if (options.fallback !== void 0 && !isString(options.fallback)) throw new TypeError(`a navigator fallback must be a string, got ${JSON.stringify(options.fallback)}`);
183
- if (options.base !== void 0 && !isString(options.base)) throw new TypeError(`a navigator base must be a string, got ${JSON.stringify(options.base)}`);
180
+ if (options.guard !== void 0 && !isFunction(options.guard)) throw new ContractError("a navigator guard must be a function when defined", {
181
+ code: "literal",
182
+ context: {
183
+ path: ["options", "guard"],
184
+ limit: "function or undefined",
185
+ received: preview(options.guard)
186
+ }
187
+ });
188
+ if (options.fallback !== void 0 && !isString(options.fallback)) throw new ContractError("a navigator fallback must be a string when defined", {
189
+ code: "literal",
190
+ context: {
191
+ path: ["options", "fallback"],
192
+ limit: "string or undefined",
193
+ received: preview(options.fallback)
194
+ }
195
+ });
196
+ if (options.base !== void 0 && !isString(options.base)) throw new ContractError("a navigator base must be a string when defined", {
197
+ code: "literal",
198
+ context: {
199
+ path: ["options", "base"],
200
+ limit: "string or undefined",
201
+ received: preview(options.base)
202
+ }
203
+ });
184
204
  this.#history = options.history ?? false;
185
205
  this.#base = options.base;
186
206
  this.#intercept = options.intercept ?? false;
@@ -191,17 +211,12 @@ var Navigator = class {
191
211
  ...options.error === void 0 ? {} : { error: options.error }
192
212
  });
193
213
  this.#router = createRouter({
194
- entries: options.routes.map((route) => ({
195
- path: route.path,
196
- meta: route,
197
- ...route.name === void 0 ? {} : { name: route.name }
198
- })),
214
+ entries: options.routes,
199
215
  ...options.sensitive === void 0 ? {} : { sensitive: options.sensitive },
200
216
  key: computeNavigationKey
201
217
  });
202
218
  this.#fallback = options.fallback ?? options.routes[0]?.path;
203
- this.#hashListener = this.#resolve.bind(this);
204
- this.#popListener = this.#resolve.bind(this);
219
+ this.#listener = this.#resolve.bind(this);
205
220
  this.#clickListener = this.#intercepted.bind(this);
206
221
  }
207
222
  get router() {
@@ -216,9 +231,9 @@ var Navigator = class {
216
231
  start() {
217
232
  if (this.#started) return;
218
233
  this.#started = true;
219
- if (!this.#history) window.addEventListener("hashchange", this.#hashListener);
234
+ if (!this.#history) window.addEventListener("hashchange", this.#listener);
220
235
  else {
221
- window.addEventListener("popstate", this.#popListener);
236
+ window.addEventListener("popstate", this.#listener);
222
237
  if (this.#intercept) document.addEventListener("click", this.#clickListener);
223
238
  }
224
239
  this.#resolve();
@@ -226,9 +241,9 @@ var Navigator = class {
226
241
  stop() {
227
242
  if (!this.#started) return;
228
243
  this.#started = false;
229
- if (!this.#history) window.removeEventListener("hashchange", this.#hashListener);
244
+ if (!this.#history) window.removeEventListener("hashchange", this.#listener);
230
245
  else {
231
- window.removeEventListener("popstate", this.#popListener);
246
+ window.removeEventListener("popstate", this.#listener);
232
247
  if (this.#intercept) document.removeEventListener("click", this.#clickListener);
233
248
  }
234
249
  this.#current?.abort();
@@ -245,14 +260,7 @@ var Navigator = class {
245
260
  this.#resolve();
246
261
  }
247
262
  match(path) {
248
- const hit = this.#router.match(path);
249
- if (hit === void 0) return void 0;
250
- return {
251
- path: hit.path,
252
- params: hit.params,
253
- meta: hit.meta.meta,
254
- ...hit.meta.name === void 0 ? {} : { name: hit.meta.name }
255
- };
263
+ return this.#router.match(path);
256
264
  }
257
265
  destroy() {
258
266
  this.stop();
@@ -327,8 +335,8 @@ var Navigator = class {
327
335
  //#endregion
328
336
  //#region src/browser/factories.ts
329
337
  /**
330
- * Create a {@link NavigatorInterface} — the headless History/hash navigation
331
- * entity composing one core `Router<RouteEntry<Meta>>`.
338
+ * Creates a {@link NavigatorInterface} — the headless History/hash navigation
339
+ * entity composing one core `Router<Meta>`.
332
340
  *
333
341
  * @remarks
334
342
  * Prefer this over `new Navigator(...)` at call sites that only need the
@@ -338,7 +346,7 @@ var Navigator = class {
338
346
  * @param options - The `routes` to register, the `history` toggle (default
339
347
  * `false`, hash mode), an optional `base` (history mode), an optional
340
348
  * `fallback` path, an optional `guard` hook, opt-in link `intercept`
341
- * (history mode), the `sensitive` case toggle, and the AGENTS §13 emitter
349
+ * (history mode), the `sensitive` case toggle, and the Emitter pattern's
342
350
  * `on`/`error` wiring
343
351
  * @returns A live {@link NavigatorInterface} handle — call `start()` to begin
344
352
  * dispatching
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["#router","#emitter","#history","#base","#fallback","#guard","#error","#intercept","#hashListener","#popListener","#clickListener","#resolve","#intercepted","#active","#started","#current","#matchFallback","#navigate","#commit","#guarded","#surface"],"sources":["../../../src/browser/helpers.ts","../../../src/browser/Navigator.ts","../../../src/browser/factories.ts"],"sourcesContent":["// The PURE browser-navigation primitives (AGENTS §4.3 multi-word names — module\n// scope, no entity context). Every one is exported (the centralized-file rule,\n// §5): the `Navigator` composes them, and each has its own unit test. NO `node:*`\n// — DOM-typed only (`Location`, `Event`, `HTMLAnchorElement`), valid under the\n// `src:browser` scoped check (AGENTS §17.7).\n\nimport type { RouteEntry } from '@src/core'\nimport { canonicalizePath } from '@src/core'\n\n/**\n * Compute the registry key for a browser navigation route.\n *\n * @remarks\n * Projects the nested route's path through the core engine's canonical\n * trailing-slash identity, so `/users` and `/users/` replace one another in\n * the Navigator's shared Router.\n *\n * @param entry - The outer Router entry carrying the Navigator route\n * @returns The nested route's canonical path\n *\n * @example\n * ```ts\n * computeNavigationKey({ path: '/users/', meta: { path: '/users/' } }) // '/users'\n * ```\n */\nexport function computeNavigationKey(entry: RouteEntry<{ readonly path: string }>): string {\n\treturn canonicalizePath(entry.meta.path)\n}\n\n/**\n * Extract the `/`-prefixed pathname from a `location.hash` value — strip the\n * leading `#` (keeping the route's own leading `/`) and any `?query` suffix.\n *\n * @remarks\n * The grammar this package matches everywhere is `/`-prefixed (§4 path\n * grammar), so a hash-mode location's `'#/users/7?x'` becomes `'/users/7'`\n * — a hash pattern is expected to start `'#/'`; anything else (an empty hash,\n * or one that does not begin `'#/'`) yields `''` (the `Navigator` then falls\n * back). Total — never throws.\n *\n * @param hash - The raw `window.location.hash` value (e.g. `'#/users/7?x'`)\n * @returns The `/`-prefixed pathname to match, or `''` for an empty / non-`#/` hash\n *\n * @example\n * ```ts\n * extractHashPath('#/users/7?x') // '/users/7'\n * extractHashPath('#/tokens') // '/tokens'\n * extractHashPath('') // '' — the Navigator falls back\n * extractHashPath('#other') // '' — not a `#/` route hash\n * ```\n */\nexport function extractHashPath(hash: string): string {\n\tif (!hash.startsWith('#/')) return ''\n\tconst withoutHash = hash.slice(1)\n\tconst queryIndex = withoutHash.indexOf('?')\n\treturn queryIndex === -1 ? withoutHash : withoutHash.slice(0, queryIndex)\n}\n\n/**\n * Resolve the `/`-prefixed pathname to match for the CURRENT location, in\n * either navigation mode — the one seam `extractHashPath` (hash mode) and\n * history-mode base-stripping share.\n *\n * @remarks\n * Hash mode (`history: false`) reads `location.hash` through\n * {@link extractHashPath}. History mode (`history: true`) reads\n * `location.pathname` and strips a leading `base` prefix when one is\n * configured: `base` itself maps to the root `'/'`; a pathname that is not\n * under `base` is returned unchanged (a base mismatch is not this helper's\n * concern — the `Navigator`'s match then simply misses). Total — never throws.\n *\n * @param location - The `hash` + `pathname` pair to resolve from (accepts a\n * real `Location` or any object shaped the same, for pure unit testing)\n * @param history - The navigation substrate: `false` for hash mode, `true`\n * for history mode\n * @param base - The history-mode path prefix to strip (ignored in hash mode;\n * omit for no prefix)\n * @returns The `/`-prefixed pathname to match\n *\n * @example\n * ```ts\n * resolveLocationPath({ hash: '#/users/7', pathname: '/' }, false) // '/users/7'\n * resolveLocationPath({ hash: '', pathname: '/app/users/7' }, true, '/app') // '/users/7'\n * resolveLocationPath({ hash: '', pathname: '/app' }, true, '/app') // '/'\n * resolveLocationPath({ hash: '', pathname: '/other/users' }, true, '/app') // '/other/users'\n * ```\n */\nexport function resolveLocationPath(\n\tlocation: Pick<Location, 'hash' | 'pathname'>,\n\thistory: boolean,\n\tbase?: string,\n): string {\n\tif (!history) return extractHashPath(location.hash)\n\tconst pathname = location.pathname\n\tif (base === undefined || base === '') return pathname\n\tconst normalizedBase = base.endsWith('/') ? base.slice(0, -1) : base\n\tif (pathname === normalizedBase) return '/'\n\tif (pathname.startsWith(`${normalizedBase}/`)) return pathname.slice(normalizedBase.length)\n\treturn pathname\n}\n\n/**\n * Find the nearest enclosing `<a>` element a DOM event originated from, by\n * walking its composed path — the pure lookup behind history-mode link\n * interception.\n *\n * @remarks\n * Uses `event.composedPath()` (not `event.target`) so a click on a styled\n * child INSIDE an anchor (an icon, a span) still resolves to the anchor.\n * Total — never throws; returns `undefined` when no anchor is found on the\n * path.\n *\n * @param event - The DOM event to search (typically a `click`)\n * @returns The nearest enclosing `HTMLAnchorElement`, or `undefined`\n *\n * @example\n * ```ts\n * document.addEventListener('click', (event) => {\n * \tconst anchor = findAnchor(event)\n * \tif (anchor !== undefined) console.log(anchor.href)\n * })\n * ```\n */\nexport function findAnchor(event: Event): HTMLAnchorElement | undefined {\n\tfor (const node of event.composedPath()) {\n\t\tif (node instanceof HTMLAnchorElement) return node\n\t}\n\treturn undefined\n}\n","import type { NavigatorEventMap, NavigatorInterface, NavigatorOptions } from './types.js'\nimport type { AbortInterface } from '@orkestrel/abort'\nimport type { EmitterErrorHandler, EmitterInterface } from '@orkestrel/emitter'\nimport type { RouteEntry, RouterInterface, RouterMatch } from '@src/core'\nimport { createAbort } from '@orkestrel/abort'\nimport { Emitter } from '@orkestrel/emitter'\nimport { isFunction, isString } from '@orkestrel/contract'\nimport { createRouter, joinPaths } from '@src/core'\nimport { computeNavigationKey, findAnchor, resolveLocationPath } from './helpers.js'\n\n/**\n * The headless History/hash navigation entity — composes one core\n * `Router<RouteEntry<Meta>>`, resolving the current location on `start()` and\n * every subsequent navigation event, tracking `active`, and emitting\n * `navigate` through the core {@link Emitter} (AGENTS §13). No `render` /\n * `outlet` — the consumer owns rendering.\n *\n * @typeParam Meta - The opaque per-route payload a match carries back\n *\n * @remarks\n * - **One shared engine.** Each `route.path` is registered on the SAME\n * `Router` machine the core `Dispatcher` composes, keyed for dedup by its\n * {@link canonicalizePath} (last write wins, replace-in-place) — literal-\n * over-param precedence, trailing-slash insensitivity, and\n * `:param`/`*wildcard` extraction all come from that one engine (AGENTS\n * §21).\n * - **Resolve pipeline.** Compute the `/`-prefixed pathname to match\n * ({@link resolveLocationPath}) → {@link match} it → on a miss, match the\n * `fallback` through the SAME engine → a fallback that ALSO matches nothing\n * aborts any pending guarded navigation (a miss SUPERSEDES it, same as a\n * newer navigation) and leaves `active` `undefined`, emitting nothing\n * (§21-honest: no phantom match is fabricated) → the optional `guard` may\n * veto → on a verdict, `active` is set and `navigate` emitted.\n * - **Supersede-safe guard.** Every navigation mints an `@orkestrel/abort`\n * handle, aborting the PREVIOUS navigation's handle first; a guard verdict\n * that resolves after its navigation was superseded (`signal.aborted`) is\n * discarded, same as a `false`/rejected verdict. A guard throw routes to\n * the `error` handler and vetoes. `stop()`/`destroy()` also abort the\n * pending handle.\n * - **Hash vs history mode.** Hash mode (`history: false`, the default) binds\n * `hashchange`; history mode (`history: true`) binds `popstate` and, when\n * `intercept` is set, same-origin `<a>` click interception (a plain\n * left-click with no modifier keys, `target`, or `download` attribute).\n *\n * @example\n * ```ts\n * const navigator = new Navigator<{ readonly title: string }>({\n * \troutes: [\n * \t\t{ path: '/users/:id', meta: { title: 'User' } },\n * \t\t{ path: '/tokens', meta: { title: 'Tokens' } },\n * \t],\n * })\n * navigator.emitter.on('navigate', (match) => (document.title = match.meta.title))\n * navigator.start() // resolves the current hash now, and on every hashchange\n * navigator.navigate('/tokens')\n * ```\n */\nexport class Navigator<Meta> implements NavigatorInterface<Meta> {\n\treadonly #router: RouterInterface<RouteEntry<Meta>>\n\treadonly #emitter: Emitter<NavigatorEventMap<Meta>>\n\treadonly #history: boolean\n\treadonly #base: string | undefined\n\treadonly #fallback: string | undefined\n\treadonly #guard: NavigatorOptions<Meta>['guard']\n\treadonly #error: EmitterErrorHandler | undefined\n\treadonly #intercept: boolean\n\treadonly #hashListener: () => void\n\treadonly #popListener: () => void\n\treadonly #clickListener: (event: MouseEvent) => void\n\t#active: RouterMatch<Meta> | undefined\n\t#started = false\n\t#current: AbortInterface | undefined\n\n\tconstructor(options: NavigatorOptions<Meta>) {\n\t\tif (options.guard !== undefined && !isFunction(options.guard))\n\t\t\tthrow new TypeError(\n\t\t\t\t`a navigator guard must be a function, got ${JSON.stringify(options.guard)}`,\n\t\t\t)\n\t\tif (options.fallback !== undefined && !isString(options.fallback))\n\t\t\tthrow new TypeError(\n\t\t\t\t`a navigator fallback must be a string, got ${JSON.stringify(options.fallback)}`,\n\t\t\t)\n\t\tif (options.base !== undefined && !isString(options.base))\n\t\t\tthrow new TypeError(`a navigator base must be a string, got ${JSON.stringify(options.base)}`)\n\t\tthis.#history = options.history ?? false\n\t\tthis.#base = options.base\n\t\tthis.#intercept = options.intercept ?? false\n\t\tthis.#guard = options.guard\n\t\tthis.#error = options.error\n\t\tthis.#emitter = new Emitter<NavigatorEventMap<Meta>>({\n\t\t\t...(options.on === undefined ? {} : { on: options.on }),\n\t\t\t...(options.error === undefined ? {} : { error: options.error }),\n\t\t})\n\t\tthis.#router = createRouter<RouteEntry<Meta>>({\n\t\t\tentries: options.routes.map((route) => ({\n\t\t\t\tpath: route.path,\n\t\t\t\tmeta: route,\n\t\t\t\t...(route.name === undefined ? {} : { name: route.name }),\n\t\t\t})),\n\t\t\t...(options.sensitive === undefined ? {} : { sensitive: options.sensitive }),\n\t\t\tkey: computeNavigationKey,\n\t\t})\n\t\tthis.#fallback = options.fallback ?? options.routes[0]?.path\n\t\tthis.#hashListener = this.#resolve.bind(this)\n\t\tthis.#popListener = this.#resolve.bind(this)\n\t\tthis.#clickListener = this.#intercepted.bind(this)\n\t}\n\n\tget router(): RouterInterface<RouteEntry<Meta>> {\n\t\treturn this.#router\n\t}\n\n\tget emitter(): EmitterInterface<NavigatorEventMap<Meta>> {\n\t\treturn this.#emitter\n\t}\n\n\tget active(): RouterMatch<Meta> | undefined {\n\t\treturn this.#active\n\t}\n\n\tstart(): void {\n\t\tif (this.#started) return\n\t\tthis.#started = true\n\t\tif (!this.#history) {\n\t\t\twindow.addEventListener('hashchange', this.#hashListener)\n\t\t} else {\n\t\t\twindow.addEventListener('popstate', this.#popListener)\n\t\t\tif (this.#intercept) document.addEventListener('click', this.#clickListener)\n\t\t}\n\t\tthis.#resolve()\n\t}\n\n\tstop(): void {\n\t\tif (!this.#started) return\n\t\tthis.#started = false\n\t\tif (!this.#history) {\n\t\t\twindow.removeEventListener('hashchange', this.#hashListener)\n\t\t} else {\n\t\t\twindow.removeEventListener('popstate', this.#popListener)\n\t\t\tif (this.#intercept) document.removeEventListener('click', this.#clickListener)\n\t\t}\n\t\tthis.#current?.abort()\n\t}\n\n\tnavigate(path: string): void {\n\t\tif (!this.#history) {\n\t\t\tconst next = `#${path}`\n\t\t\tif (window.location.hash === next) this.#resolve()\n\t\t\telse window.location.hash = next\n\t\t\treturn\n\t\t}\n\t\tconst target = this.#base === undefined ? path : joinPaths(this.#base, path)\n\t\twindow.history.pushState(null, '', target)\n\t\tthis.#resolve()\n\t}\n\n\tmatch(path: string): RouterMatch<Meta> | undefined {\n\t\tconst hit = this.#router.match(path)\n\t\tif (hit === undefined) return undefined\n\t\treturn {\n\t\t\tpath: hit.path,\n\t\t\tparams: hit.params,\n\t\t\tmeta: hit.meta.meta,\n\t\t\t...(hit.meta.name === undefined ? {} : { name: hit.meta.name }),\n\t\t}\n\t}\n\n\tdestroy(): void {\n\t\tthis.stop()\n\t\tthis.#emitter.destroy()\n\t}\n\n\t// === Private\n\n\t// Compute the pathname to match for the CURRENT location, resolve it (falling back to the\n\t// configured fallback through the SAME engine on a miss), and either navigate or — when\n\t// neither the location nor the fallback matches anything — leave `active` `undefined` with\n\t// no emit (§21-honest: no phantom match is fabricated).\n\t#resolve(): void {\n\t\tconst pathname = resolveLocationPath(\n\t\t\t{ hash: window.location.hash, pathname: window.location.pathname },\n\t\t\tthis.#history,\n\t\t\tthis.#base,\n\t\t)\n\t\tconst to = this.match(pathname) ?? this.#matchFallback()\n\t\tif (to === undefined) {\n\t\t\tthis.#current?.abort()\n\t\t\tthis.#active = undefined\n\t\t\treturn\n\t\t}\n\t\tthis.#navigate(to)\n\t}\n\n\t#matchFallback(): RouterMatch<Meta> | undefined {\n\t\tif (this.#fallback === undefined) return undefined\n\t\treturn this.match(this.#fallback)\n\t}\n\n\t// Supersede the previous pending navigation's abort handle, mint a fresh one for this\n\t// navigation, and either commit directly (no guard configured — the synchronous fast path) or\n\t// run the guard pipeline.\n\t#navigate(to: RouterMatch<Meta>): void {\n\t\tthis.#current?.abort()\n\t\tconst handle = createAbort()\n\t\tthis.#current = handle\n\t\tconst guard = this.#guard\n\t\tif (guard === undefined) {\n\t\t\tthis.#commit(to)\n\t\t\treturn\n\t\t}\n\t\tvoid this.#guarded(guard, to, this.#active, handle)\n\t}\n\n\t#commit(to: RouterMatch<Meta>): void {\n\t\tthis.#active = to\n\t\tthis.#emitter.emit('navigate', to)\n\t}\n\n\t// Await the guard's verdict; a throw routes to the `error` handler and vetoes, a discarded\n\t// verdict (superseded via `handle.signal.aborted`, or a plain `false`/rejected verdict) leaves\n\t// `active` unchanged with no emit, and a true verdict commits.\n\tasync #guarded(\n\t\tguard: NonNullable<NavigatorOptions<Meta>['guard']>,\n\t\tto: RouterMatch<Meta>,\n\t\tfrom: RouterMatch<Meta> | undefined,\n\t\thandle: AbortInterface,\n\t): Promise<void> {\n\t\tlet verdict: boolean\n\t\ttry {\n\t\t\tverdict = await guard(to, from, handle.signal)\n\t\t} catch (error) {\n\t\t\tthis.#surface(error)\n\t\t\treturn\n\t\t}\n\t\tif (handle.signal.aborted || !verdict) return\n\t\tthis.#commit(to)\n\t}\n\n\t// Route a guard throw to the `error` handler (AGENTS §13's own channel, not a listener\n\t// throw so it cannot flow through the emitter's `emit`), swallowing a throwing handler itself\n\t// (anti-recursion, mirroring the emitter's own contract).\n\t#surface(error: unknown): void {\n\t\tconst handler = this.#error\n\t\tif (handler === undefined) return\n\t\ttry {\n\t\t\thandler(error, 'navigate')\n\t\t} catch {\n\t\t\t// The error handler itself threw — swallow it (anti-recursion).\n\t\t}\n\t}\n\n\t// Same-origin `<a>` click interception (history mode, opt-in via `intercept`): skip an\n\t// already-handled event, a non-primary button, any modifier key, a targeted or download link,\n\t// or a cross-origin destination — otherwise prevent the native navigation and `navigate` instead.\n\t#intercepted(event: MouseEvent): void {\n\t\tif (event.defaultPrevented || event.button !== 0) return\n\t\tif (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return\n\t\tconst anchor = findAnchor(event)\n\t\tif (anchor === undefined) return\n\t\tif (anchor.target !== '' && anchor.target !== '_self') return\n\t\tif (anchor.hasAttribute('download')) return\n\t\tconst url = new URL(anchor.href, window.location.href)\n\t\tif (url.origin !== window.location.origin) return\n\t\tevent.preventDefault()\n\t\tthis.navigate(resolveLocationPath({ hash: url.hash, pathname: url.pathname }, true, this.#base))\n\t}\n}\n","import type { NavigatorInterface, NavigatorOptions } from './types.js'\nimport { Navigator } from './Navigator.js'\n\n/**\n * Create a {@link NavigatorInterface} — the headless History/hash navigation\n * entity composing one core `Router<RouteEntry<Meta>>`.\n *\n * @remarks\n * Prefer this over `new Navigator(...)` at call sites that only need the\n * interface.\n *\n * @typeParam Meta - The opaque per-route payload a match carries back\n * @param options - The `routes` to register, the `history` toggle (default\n * `false`, hash mode), an optional `base` (history mode), an optional\n * `fallback` path, an optional `guard` hook, opt-in link `intercept`\n * (history mode), the `sensitive` case toggle, and the AGENTS §13 emitter\n * `on`/`error` wiring\n * @returns A live {@link NavigatorInterface} handle — call `start()` to begin\n * dispatching\n *\n * @example\n * ```ts\n * import { createNavigator } from '@src/browser'\n *\n * const navigator = createNavigator({\n * \troutes: [\n * \t\t{ path: '/users/:id', meta: { title: 'User' } },\n * \t\t{ path: '/tokens', meta: { title: 'Tokens' } },\n * \t],\n * \ton: { navigate: (match) => (document.title = match.meta.title) },\n * })\n * navigator.start()\n * ```\n */\nexport function createNavigator<Meta>(options: NavigatorOptions<Meta>): NavigatorInterface<Meta> {\n\treturn new Navigator<Meta>(options)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["#router","#emitter","#history","#base","#fallback","#guard","#error","#intercept","#listener","#clickListener","#resolve","#intercepted","#active","#started","#current","#matchFallback","#navigate","#commit","#guarded","#surface"],"sources":["../../../src/browser/helpers.ts","../../../src/browser/Navigator.ts","../../../src/browser/factories.ts"],"sourcesContent":["// The PURE browser-navigation primitives (self-describing helper naming —\n// module scope, no entity context). Every one is exported (the centralized-file\n// rule): the `Navigator` composes them, and each has its own unit test. NO `node:*`\n// — DOM-typed only (`Location`, `Event`, `HTMLAnchorElement`), valid under the\n// `src:browser` scoped isolation check.\n\nimport type { RouteEntry } from '@src/core'\nimport { canonicalizePath } from '@src/core'\n\n/**\n * Computes the registry key for a browser navigation route.\n *\n * @remarks\n * Projects the route's path through the core engine's canonical trailing-slash\n * identity, so `/users` and `/users/` replace one another in the Navigator's\n * shared Router. The entry's `meta` payload is never read, so any payload type\n * is accepted.\n *\n * @param entry - The Router entry carrying the Navigator route\n * @returns The route's canonical path\n *\n * @example\n * ```ts\n * computeNavigationKey({ path: '/users/', meta: {} }) // '/users'\n * ```\n */\nexport function computeNavigationKey(entry: RouteEntry<unknown>): string {\n\treturn canonicalizePath(entry.path)\n}\n\n/**\n * Extracts the `/`-prefixed pathname from a `location.hash` value — strips the\n * leading `#` (keeping the route's own leading `/`) and any `?query` suffix.\n *\n * @remarks\n * The grammar this package matches everywhere is `/`-prefixed, so a hash-mode\n * location's `'#/users/7?x'` becomes `'/users/7'`\n * — a hash pattern is expected to start `'#/'`; anything else (an empty hash,\n * or one that does not begin `'#/'`) yields `''` (the `Navigator` then falls\n * back). Total — never throws.\n *\n * @param hash - The raw `window.location.hash` value (for example `'#/users/7?x'`)\n * @returns The `/`-prefixed pathname to match, or `''` for an empty / non-`#/` hash\n *\n * @example\n * ```ts\n * extractHashPath('#/users/7?x') // '/users/7'\n * extractHashPath('#/tokens') // '/tokens'\n * extractHashPath('') // '' — the Navigator falls back\n * extractHashPath('#other') // '' — not a `#/` route hash\n * ```\n */\nexport function extractHashPath(hash: string): string {\n\tif (!hash.startsWith('#/')) return ''\n\tconst withoutHash = hash.slice(1)\n\tconst queryIndex = withoutHash.indexOf('?')\n\treturn queryIndex === -1 ? withoutHash : withoutHash.slice(0, queryIndex)\n}\n\n/**\n * Resolves the `/`-prefixed pathname to match for the CURRENT location, in\n * either navigation mode — the one seam `extractHashPath` (hash mode) and\n * history-mode base-stripping share.\n *\n * @remarks\n * Hash mode (`history: false`) reads `location.hash` through\n * {@link extractHashPath}. History mode (`history: true`) reads\n * `location.pathname` and strips a leading `base` prefix when one is\n * configured: `base` itself maps to the root `'/'`; a pathname that is not\n * under `base` is returned unchanged (a base mismatch is not this helper's\n * concern — the `Navigator`'s match then misses). Total — never throws.\n *\n * @param location - The `hash` + `pathname` pair to resolve from (accepts a\n * real `Location` or any object shaped the same, for pure unit testing)\n * @param history - If `true`, the pathname is read from `location.pathname`\n * with `base` stripped; if `false`, it is read from `location.hash`\n * @param base - The history-mode path prefix to strip (ignored in hash mode;\n * omit for no prefix)\n * @returns The `/`-prefixed pathname to match\n *\n * @example\n * ```ts\n * resolveLocationPath({ hash: '#/users/7', pathname: '/' }, false) // '/users/7'\n * resolveLocationPath({ hash: '', pathname: '/app/users/7' }, true, '/app') // '/users/7'\n * resolveLocationPath({ hash: '', pathname: '/app' }, true, '/app') // '/'\n * resolveLocationPath({ hash: '', pathname: '/other/users' }, true, '/app') // '/other/users'\n * ```\n */\nexport function resolveLocationPath(\n\tlocation: Pick<Location, 'hash' | 'pathname'>,\n\thistory: boolean,\n\tbase?: string,\n): string {\n\tif (!history) return extractHashPath(location.hash)\n\tconst pathname = location.pathname\n\tif (base === undefined || base === '') return pathname\n\tconst normalizedBase = base.endsWith('/') ? base.slice(0, -1) : base\n\tif (pathname === normalizedBase) return '/'\n\tif (pathname.startsWith(`${normalizedBase}/`)) return pathname.slice(normalizedBase.length)\n\treturn pathname\n}\n\n/**\n * Finds the nearest enclosing `<a>` element a DOM event originated from, by\n * walking its composed path — the pure lookup behind history-mode link\n * interception.\n *\n * @remarks\n * Uses `event.composedPath()` (not `event.target`) so a click on a styled\n * child INSIDE an anchor (an icon, a span) still resolves to the anchor.\n * Total — never throws; returns `undefined` when no anchor is found on the\n * path.\n *\n * @param event - The DOM event to search (typically a `click`)\n * @returns The nearest enclosing `HTMLAnchorElement`, or `undefined`\n *\n * @example\n * ```ts\n * document.addEventListener('click', (event) => {\n * \tconst anchor = findAnchor(event)\n * \tif (anchor !== undefined) console.log(anchor.href)\n * })\n * ```\n */\nexport function findAnchor(event: Event): HTMLAnchorElement | undefined {\n\tfor (const node of event.composedPath()) {\n\t\tif (node instanceof HTMLAnchorElement) return node\n\t}\n\treturn undefined\n}\n","import type { NavigatorEventMap, NavigatorInterface, NavigatorOptions } from './types.js'\nimport type { AbortInterface } from '@orkestrel/abort'\nimport type { EmitterErrorHandler, EmitterInterface } from '@orkestrel/emitter'\nimport type { RouterInterface, RouterMatch } from '@src/core'\nimport { createAbort } from '@orkestrel/abort'\nimport { Emitter } from '@orkestrel/emitter'\nimport { ContractError, isFunction, isString, preview } from '@orkestrel/contract'\nimport { createRouter, joinPaths } from '@src/core'\nimport { computeNavigationKey, findAnchor, resolveLocationPath } from './helpers.js'\n\n/**\n * Represents the headless History/hash navigation entity — composes one core\n * `Router<Meta>`, resolving the current location on `start()` and\n * every subsequent navigation event, tracking `active`, and emitting\n * `navigate` through the core {@link Emitter}. No `render` /\n * `outlet` — the consumer owns rendering.\n *\n * @typeParam Meta - The opaque per-route payload a match carries back\n *\n * @remarks\n * - **One shared engine.** Each `route.path` is registered on the SAME\n * `Router` machine the core `Dispatcher` composes, keyed for dedup by its\n * {@link canonicalizePath} (last write wins, replace-in-place) — literal-\n * over-param precedence, trailing-slash insensitivity, and\n * `:param`/`*wildcard` extraction all come from that one shared engine.\n * - **Resolve pipeline.** Compute the `/`-prefixed pathname to match\n * ({@link resolveLocationPath}) → {@link match} it → on a miss, match the\n * `fallback` through the SAME engine → a fallback that ALSO matches nothing\n * aborts any pending guarded navigation (a miss SUPERSEDES it, same as a\n * newer navigation) and leaves `active` `undefined`, emitting nothing —\n * honest to the one-shared-engine rule: no phantom match is fabricated → the optional `guard` may\n * veto → on a verdict, `active` is set and `navigate` emitted.\n * - **Supersede-safe guard.** Every navigation mints an `@orkestrel/abort`\n * handle, aborting the PREVIOUS navigation's handle first; a guard verdict\n * that resolves after its navigation was superseded (`signal.aborted`) is\n * discarded, same as a `false`/rejected verdict. A guard throw routes to\n * the `error` handler and vetoes. `stop()`/`destroy()` also abort the\n * pending handle.\n * - **Hash vs history mode.** Hash mode (`history: false`, the default) binds\n * `hashchange`; history mode (`history: true`) binds `popstate` and, when\n * `intercept` is set, same-origin `<a>` click interception (a plain\n * left-click with no modifier keys, `target`, or `download` attribute).\n *\n * @example\n * ```ts\n * const navigator = new Navigator<{ readonly title: string }>({\n * \troutes: [\n * \t\t{ path: '/users/:id', meta: { title: 'User' } },\n * \t\t{ path: '/tokens', meta: { title: 'Tokens' } },\n * \t],\n * })\n * navigator.emitter.on('navigate', (match) => (document.title = match.meta.title))\n * navigator.start() // resolves the current hash now, and on every hashchange\n * navigator.navigate('/tokens')\n * ```\n */\nexport class Navigator<Meta> implements NavigatorInterface<Meta> {\n\treadonly #router: RouterInterface<Meta>\n\treadonly #emitter: Emitter<NavigatorEventMap<Meta>>\n\treadonly #history: boolean\n\treadonly #base: string | undefined\n\treadonly #fallback: string | undefined\n\treadonly #guard: NavigatorOptions<Meta>['guard']\n\treadonly #error: EmitterErrorHandler | undefined\n\treadonly #intercept: boolean\n\treadonly #listener: () => void\n\treadonly #clickListener: (event: MouseEvent) => void\n\t#active: RouterMatch<Meta> | undefined\n\t#started = false\n\t#current: AbortInterface | undefined\n\n\tconstructor(options: NavigatorOptions<Meta>) {\n\t\tif (options.guard !== undefined && !isFunction(options.guard))\n\t\t\tthrow new ContractError('a navigator guard must be a function when defined', {\n\t\t\t\tcode: 'literal',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['options', 'guard'],\n\t\t\t\t\tlimit: 'function or undefined',\n\t\t\t\t\treceived: preview(options.guard),\n\t\t\t\t},\n\t\t\t})\n\t\tif (options.fallback !== undefined && !isString(options.fallback))\n\t\t\tthrow new ContractError('a navigator fallback must be a string when defined', {\n\t\t\t\tcode: 'literal',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['options', 'fallback'],\n\t\t\t\t\tlimit: 'string or undefined',\n\t\t\t\t\treceived: preview(options.fallback),\n\t\t\t\t},\n\t\t\t})\n\t\tif (options.base !== undefined && !isString(options.base))\n\t\t\tthrow new ContractError('a navigator base must be a string when defined', {\n\t\t\t\tcode: 'literal',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['options', 'base'],\n\t\t\t\t\tlimit: 'string or undefined',\n\t\t\t\t\treceived: preview(options.base),\n\t\t\t\t},\n\t\t\t})\n\t\tthis.#history = options.history ?? false\n\t\tthis.#base = options.base\n\t\tthis.#intercept = options.intercept ?? false\n\t\tthis.#guard = options.guard\n\t\tthis.#error = options.error\n\t\tthis.#emitter = new Emitter<NavigatorEventMap<Meta>>({\n\t\t\t...(options.on === undefined ? {} : { on: options.on }),\n\t\t\t...(options.error === undefined ? {} : { error: options.error }),\n\t\t})\n\t\tthis.#router = createRouter<Meta>({\n\t\t\tentries: options.routes,\n\t\t\t...(options.sensitive === undefined ? {} : { sensitive: options.sensitive }),\n\t\t\tkey: computeNavigationKey,\n\t\t})\n\t\tthis.#fallback = options.fallback ?? options.routes[0]?.path\n\t\tthis.#listener = this.#resolve.bind(this)\n\t\tthis.#clickListener = this.#intercepted.bind(this)\n\t}\n\n\tget router(): RouterInterface<Meta> {\n\t\treturn this.#router\n\t}\n\n\tget emitter(): EmitterInterface<NavigatorEventMap<Meta>> {\n\t\treturn this.#emitter\n\t}\n\n\tget active(): RouterMatch<Meta> | undefined {\n\t\treturn this.#active\n\t}\n\n\tstart(): void {\n\t\tif (this.#started) return\n\t\tthis.#started = true\n\t\tif (!this.#history) {\n\t\t\twindow.addEventListener('hashchange', this.#listener)\n\t\t} else {\n\t\t\twindow.addEventListener('popstate', this.#listener)\n\t\t\tif (this.#intercept) document.addEventListener('click', this.#clickListener)\n\t\t}\n\t\tthis.#resolve()\n\t}\n\n\tstop(): void {\n\t\tif (!this.#started) return\n\t\tthis.#started = false\n\t\tif (!this.#history) {\n\t\t\twindow.removeEventListener('hashchange', this.#listener)\n\t\t} else {\n\t\t\twindow.removeEventListener('popstate', this.#listener)\n\t\t\tif (this.#intercept) document.removeEventListener('click', this.#clickListener)\n\t\t}\n\t\tthis.#current?.abort()\n\t}\n\n\tnavigate(path: string): void {\n\t\tif (!this.#history) {\n\t\t\tconst next = `#${path}`\n\t\t\tif (window.location.hash === next) this.#resolve()\n\t\t\telse window.location.hash = next\n\t\t\treturn\n\t\t}\n\t\tconst target = this.#base === undefined ? path : joinPaths(this.#base, path)\n\t\twindow.history.pushState(null, '', target)\n\t\tthis.#resolve()\n\t}\n\n\tmatch(path: string): RouterMatch<Meta> | undefined {\n\t\treturn this.#router.match(path)\n\t}\n\n\tdestroy(): void {\n\t\tthis.stop()\n\t\tthis.#emitter.destroy()\n\t}\n\n\t// === Private\n\n\t// Compute the pathname to match for the CURRENT location, resolve it (falling back to the\n\t// configured fallback through the SAME engine on a miss), and either navigate or — when\n\t// neither the location nor the fallback matches anything — leave `active` `undefined` with\n\t// no emit, honest to the one-shared-engine rule: no phantom match is fabricated.\n\t#resolve(): void {\n\t\tconst pathname = resolveLocationPath(\n\t\t\t{ hash: window.location.hash, pathname: window.location.pathname },\n\t\t\tthis.#history,\n\t\t\tthis.#base,\n\t\t)\n\t\tconst to = this.match(pathname) ?? this.#matchFallback()\n\t\tif (to === undefined) {\n\t\t\tthis.#current?.abort()\n\t\t\tthis.#active = undefined\n\t\t\treturn\n\t\t}\n\t\tthis.#navigate(to)\n\t}\n\n\t#matchFallback(): RouterMatch<Meta> | undefined {\n\t\tif (this.#fallback === undefined) return undefined\n\t\treturn this.match(this.#fallback)\n\t}\n\n\t// Supersede the previous pending navigation's abort handle, mint a fresh one for this\n\t// navigation, and either commit directly (no guard configured — the synchronous fast path) or\n\t// run the guard pipeline.\n\t#navigate(to: RouterMatch<Meta>): void {\n\t\tthis.#current?.abort()\n\t\tconst handle = createAbort()\n\t\tthis.#current = handle\n\t\tconst guard = this.#guard\n\t\tif (guard === undefined) {\n\t\t\tthis.#commit(to)\n\t\t\treturn\n\t\t}\n\t\tvoid this.#guarded(guard, to, this.#active, handle)\n\t}\n\n\t#commit(to: RouterMatch<Meta>): void {\n\t\tthis.#active = to\n\t\tthis.#emitter.emit('navigate', to)\n\t}\n\n\t// Await the guard's verdict; a throw routes to the `error` handler and vetoes, a discarded\n\t// verdict (superseded through `handle.signal.aborted`, or a plain `false`/rejected verdict) leaves\n\t// `active` unchanged with no emit, and a true verdict commits.\n\tasync #guarded(\n\t\tguard: NonNullable<NavigatorOptions<Meta>['guard']>,\n\t\tto: RouterMatch<Meta>,\n\t\tfrom: RouterMatch<Meta> | undefined,\n\t\thandle: AbortInterface,\n\t): Promise<void> {\n\t\tlet verdict: boolean\n\t\ttry {\n\t\t\tverdict = await guard(to, from, handle.signal)\n\t\t} catch (error) {\n\t\t\tthis.#surface(error)\n\t\t\treturn\n\t\t}\n\t\tif (handle.signal.aborted || !verdict) return\n\t\tthis.#commit(to)\n\t}\n\n\t// Route a guard throw to the `error` handler (the Emitter pattern's own channel, not a listener\n\t// throw so it cannot flow through the emitter's `emit`), swallowing a throwing handler itself\n\t// (anti-recursion, mirroring the emitter's own contract).\n\t#surface(error: unknown): void {\n\t\tconst handler = this.#error\n\t\tif (handler === undefined) return\n\t\ttry {\n\t\t\thandler(error, 'navigate')\n\t\t} catch {\n\t\t\t// The error handler itself threw — swallow it (anti-recursion).\n\t\t}\n\t}\n\n\t// Same-origin `<a>` click interception (history mode, opt-in through `intercept`): skip an\n\t// already-handled event, a non-primary button, any modifier key, a targeted or download link,\n\t// or a cross-origin destination — otherwise prevent the native navigation and `navigate` instead.\n\t#intercepted(event: MouseEvent): void {\n\t\tif (event.defaultPrevented || event.button !== 0) return\n\t\tif (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return\n\t\tconst anchor = findAnchor(event)\n\t\tif (anchor === undefined) return\n\t\tif (anchor.target !== '' && anchor.target !== '_self') return\n\t\tif (anchor.hasAttribute('download')) return\n\t\tconst url = new URL(anchor.href, window.location.href)\n\t\tif (url.origin !== window.location.origin) return\n\t\tevent.preventDefault()\n\t\tthis.navigate(resolveLocationPath({ hash: url.hash, pathname: url.pathname }, true, this.#base))\n\t}\n}\n","import type { NavigatorInterface, NavigatorOptions } from './types.js'\nimport { Navigator } from './Navigator.js'\n\n/**\n * Creates a {@link NavigatorInterface} — the headless History/hash navigation\n * entity composing one core `Router<Meta>`.\n *\n * @remarks\n * Prefer this over `new Navigator(...)` at call sites that only need the\n * interface.\n *\n * @typeParam Meta - The opaque per-route payload a match carries back\n * @param options - The `routes` to register, the `history` toggle (default\n * `false`, hash mode), an optional `base` (history mode), an optional\n * `fallback` path, an optional `guard` hook, opt-in link `intercept`\n * (history mode), the `sensitive` case toggle, and the Emitter pattern's\n * `on`/`error` wiring\n * @returns A live {@link NavigatorInterface} handle — call `start()` to begin\n * dispatching\n *\n * @example\n * ```ts\n * import { createNavigator } from '@src/browser'\n *\n * const navigator = createNavigator({\n * \troutes: [\n * \t\t{ path: '/users/:id', meta: { title: 'User' } },\n * \t\t{ path: '/tokens', meta: { title: 'Tokens' } },\n * \t],\n * \ton: { navigate: (match) => (document.title = match.meta.title) },\n * })\n * navigator.start()\n * ```\n */\nexport function createNavigator<Meta>(options: NavigatorOptions<Meta>): NavigatorInterface<Meta> {\n\treturn new Navigator<Meta>(options)\n}\n"],"mappings":"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"}