@real-router/core 0.56.0 → 0.57.1

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.
Files changed (112) hide show
  1. package/dist/cjs/Router-Brp6_4FE.js +6 -0
  2. package/dist/cjs/Router-Brp6_4FE.js.map +1 -0
  3. package/dist/cjs/api.d.ts +1 -1
  4. package/dist/cjs/api.js +1 -1
  5. package/dist/cjs/{cloneRouter-DRieJvam.js → cloneRouter-CZx0T0RQ.js} +2 -2
  6. package/dist/cjs/{cloneRouter-DRieJvam.js.map → cloneRouter-CZx0T0RQ.js.map} +1 -1
  7. package/dist/cjs/{index-C-i6vx5Y.d.ts → index-BWUmnecT.d.ts} +1 -2
  8. package/dist/cjs/index-BWUmnecT.d.ts.map +1 -0
  9. package/dist/cjs/index-CYpAZCoc.d.ts.map +1 -1
  10. package/dist/cjs/index.d.ts +1 -1
  11. package/dist/cjs/index.js +1 -1
  12. package/dist/cjs/utils.js +1 -1
  13. package/dist/cjs/utils.js.map +1 -1
  14. package/dist/cjs/validation.d.ts +1 -1
  15. package/dist/esm/Router-LT61erYH.mjs +6 -0
  16. package/dist/esm/Router-LT61erYH.mjs.map +1 -0
  17. package/dist/esm/api.d.mts +1 -1
  18. package/dist/esm/api.mjs +1 -1
  19. package/dist/esm/{cloneRouter-DHrH6D_z.mjs → cloneRouter-DAscsmmF.mjs} +2 -2
  20. package/dist/esm/{cloneRouter-DHrH6D_z.mjs.map → cloneRouter-DAscsmmF.mjs.map} +1 -1
  21. package/dist/esm/{index-C-i6vx5Y.d.mts → index-BWUmnecT.d.mts} +1 -2
  22. package/dist/esm/index-BWUmnecT.d.mts.map +1 -0
  23. package/dist/esm/index-CYpAZCoc.d.mts.map +1 -1
  24. package/dist/esm/index.d.mts +1 -1
  25. package/dist/esm/index.mjs +1 -1
  26. package/dist/esm/utils.mjs +1 -1
  27. package/dist/esm/utils.mjs.map +1 -1
  28. package/dist/esm/validation.d.mts +1 -1
  29. package/package.json +4 -5
  30. package/dist/cjs/Router-IEGavTKk.js +0 -6
  31. package/dist/cjs/Router-IEGavTKk.js.map +0 -1
  32. package/dist/cjs/index-C-i6vx5Y.d.ts.map +0 -1
  33. package/dist/esm/Router-B3aeavRb.mjs +0 -6
  34. package/dist/esm/Router-B3aeavRb.mjs.map +0 -1
  35. package/dist/esm/index-C-i6vx5Y.d.mts.map +0 -1
  36. package/src/Router.ts +0 -737
  37. package/src/RouterError.ts +0 -324
  38. package/src/api/cloneRouter.ts +0 -159
  39. package/src/api/getDependenciesApi.ts +0 -160
  40. package/src/api/getLifecycleApi.ts +0 -65
  41. package/src/api/getPluginApi.ts +0 -228
  42. package/src/api/getRoutesApi.ts +0 -831
  43. package/src/api/helpers.ts +0 -10
  44. package/src/api/index.ts +0 -16
  45. package/src/api/types.ts +0 -12
  46. package/src/constants.ts +0 -101
  47. package/src/createRouter.ts +0 -32
  48. package/src/fsm/index.ts +0 -5
  49. package/src/fsm/routerFSM.ts +0 -130
  50. package/src/getNavigator.ts +0 -30
  51. package/src/guards.ts +0 -46
  52. package/src/helpers.ts +0 -197
  53. package/src/index.ts +0 -66
  54. package/src/internals.ts +0 -228
  55. package/src/namespaces/DependenciesNamespace/dependenciesStore.ts +0 -30
  56. package/src/namespaces/DependenciesNamespace/index.ts +0 -5
  57. package/src/namespaces/EventBusNamespace/EventBusNamespace.ts +0 -522
  58. package/src/namespaces/EventBusNamespace/index.ts +0 -5
  59. package/src/namespaces/EventBusNamespace/types.ts +0 -11
  60. package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +0 -552
  61. package/src/namespaces/NavigationNamespace/constants.ts +0 -55
  62. package/src/namespaces/NavigationNamespace/index.ts +0 -5
  63. package/src/namespaces/NavigationNamespace/transition/completeTransition.ts +0 -108
  64. package/src/namespaces/NavigationNamespace/transition/errorHandling.ts +0 -124
  65. package/src/namespaces/NavigationNamespace/transition/guardPhase.ts +0 -283
  66. package/src/namespaces/NavigationNamespace/types.ts +0 -110
  67. package/src/namespaces/OptionsNamespace/OptionsNamespace.ts +0 -28
  68. package/src/namespaces/OptionsNamespace/constants.ts +0 -19
  69. package/src/namespaces/OptionsNamespace/helpers.ts +0 -50
  70. package/src/namespaces/OptionsNamespace/index.ts +0 -7
  71. package/src/namespaces/OptionsNamespace/validators.ts +0 -13
  72. package/src/namespaces/PluginsNamespace/PluginsNamespace.ts +0 -291
  73. package/src/namespaces/PluginsNamespace/constants.ts +0 -34
  74. package/src/namespaces/PluginsNamespace/index.ts +0 -7
  75. package/src/namespaces/PluginsNamespace/types.ts +0 -22
  76. package/src/namespaces/PluginsNamespace/validators.ts +0 -28
  77. package/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts +0 -558
  78. package/src/namespaces/RouteLifecycleNamespace/index.ts +0 -5
  79. package/src/namespaces/RouteLifecycleNamespace/types.ts +0 -10
  80. package/src/namespaces/RouterLifecycleNamespace/RouterLifecycleNamespace.ts +0 -81
  81. package/src/namespaces/RouterLifecycleNamespace/constants.ts +0 -25
  82. package/src/namespaces/RouterLifecycleNamespace/index.ts +0 -5
  83. package/src/namespaces/RouterLifecycleNamespace/types.ts +0 -30
  84. package/src/namespaces/RoutesNamespace/RoutesNamespace.ts +0 -582
  85. package/src/namespaces/RoutesNamespace/constants.ts +0 -6
  86. package/src/namespaces/RoutesNamespace/forwardChain.ts +0 -34
  87. package/src/namespaces/RoutesNamespace/helpers.ts +0 -204
  88. package/src/namespaces/RoutesNamespace/index.ts +0 -11
  89. package/src/namespaces/RoutesNamespace/routeGuards.ts +0 -62
  90. package/src/namespaces/RoutesNamespace/routesStore.ts +0 -566
  91. package/src/namespaces/RoutesNamespace/types.ts +0 -81
  92. package/src/namespaces/StateNamespace/StateNamespace.ts +0 -224
  93. package/src/namespaces/StateNamespace/helpers.ts +0 -24
  94. package/src/namespaces/StateNamespace/index.ts +0 -5
  95. package/src/namespaces/StateNamespace/types.ts +0 -15
  96. package/src/namespaces/index.ts +0 -35
  97. package/src/stateMetaStore.ts +0 -15
  98. package/src/transitionPath.ts +0 -440
  99. package/src/typeGuards.ts +0 -59
  100. package/src/types/RouterValidator.ts +0 -156
  101. package/src/types.ts +0 -77
  102. package/src/utils/createRequestScope.ts +0 -174
  103. package/src/utils/getStaticPaths.ts +0 -50
  104. package/src/utils/hydrateRouter.ts +0 -89
  105. package/src/utils/index.ts +0 -27
  106. package/src/utils/serializeRouterState.ts +0 -120
  107. package/src/utils/serializeState.ts +0 -63
  108. package/src/validation.ts +0 -12
  109. package/src/wiring/RouterWiringBuilder.ts +0 -275
  110. package/src/wiring/index.ts +0 -7
  111. package/src/wiring/types.ts +0 -47
  112. package/src/wiring/wireRouter.ts +0 -26
package/src/Router.ts DELETED
@@ -1,737 +0,0 @@
1
- // packages/core/src/Router.ts
2
-
3
- /**
4
- * Router class - facade with integrated namespaces.
5
- *
6
- * All functionality is now provided by namespace classes.
7
- */
8
-
9
- import { logger } from "@real-router/logger";
10
- import { EventEmitter } from "event-emitter";
11
-
12
- import { EMPTY_PARAMS, errorCodes } from "./constants";
13
- import { createRouterFSM } from "./fsm";
14
- import { guardDependencies, guardRouteStructure } from "./guards";
15
- import { createLimits, normalizeParams } from "./helpers";
16
- import {
17
- createBinaryInterceptable,
18
- createInterceptable,
19
- getInternals,
20
- registerInternals,
21
- } from "./internals";
22
- import {
23
- EventBusNamespace,
24
- NavigationNamespace,
25
- OptionsNamespace,
26
- PluginsNamespace,
27
- RouteLifecycleNamespace,
28
- RouterLifecycleNamespace,
29
- RoutesNamespace,
30
- StateNamespace,
31
- createDependenciesStore,
32
- } from "./namespaces";
33
- import { CACHED_ALREADY_STARTED_ERROR } from "./namespaces/RouterLifecycleNamespace/constants";
34
- import { RouterError } from "./RouterError";
35
- import { getTransitionPath } from "./transitionPath";
36
- import { isLoggerConfig } from "./typeGuards";
37
- import { RouterWiringBuilder, wireRouter } from "./wiring";
38
-
39
- import type { RouterInternals } from "./internals";
40
- import type { DependenciesStore } from "./namespaces";
41
- import type { Limits, PluginFactory, Route, RouterEventMap } from "./types";
42
- import type {
43
- DefaultDependencies,
44
- LeaveFn,
45
- NavigationOptions,
46
- Options,
47
- Params,
48
- Router as RouterInterface,
49
- State,
50
- SubscribeFn,
51
- Unsubscribe,
52
- } from "@real-router/types";
53
- import type { CreateMatcherOptions } from "route-tree";
54
-
55
- const EMPTY_OPTS: Readonly<NavigationOptions> = Object.freeze({});
56
-
57
- // Module-level so #onSuppressedError allocates nothing per navigate() call.
58
- const SUPPRESSED_ERROR_CODES: ReadonlySet<string> = new Set([
59
- errorCodes.SAME_STATES,
60
- errorCodes.TRANSITION_CANCELLED,
61
- errorCodes.ROUTER_NOT_STARTED,
62
- errorCodes.ROUTE_NOT_FOUND,
63
- ]);
64
-
65
- /**
66
- * Router class with integrated namespace architecture.
67
- *
68
- * All functionality is provided by namespace classes:
69
- * - OptionsNamespace: getOptions (immutable)
70
- * - DependenciesStore: get/set/remove dependencies
71
- * - EventEmitter: subscribe
72
- * - StateNamespace: state storage (getState, setState, getPreviousState)
73
- * - RoutesNamespace: route tree operations
74
- * - RouteLifecycleNamespace: canActivate/canDeactivate guards
75
- * - PluginsNamespace: plugin lifecycle
76
- * - NavigationNamespace: navigate
77
- * - RouterLifecycleNamespace: start, stop, isStarted
78
- *
79
- * @internal This class implementation is internal. Use createRouter() instead.
80
- */
81
- export class Router<
82
- Dependencies extends DefaultDependencies = DefaultDependencies,
83
- > implements RouterInterface<Dependencies> {
84
- [key: string]: unknown;
85
-
86
- // ============================================================================
87
- // Namespaces
88
- // ============================================================================
89
-
90
- readonly #options: OptionsNamespace;
91
- readonly #limits: Limits;
92
- readonly #dependenciesStore: DependenciesStore<Dependencies>;
93
- readonly #state: StateNamespace;
94
- readonly #routes: RoutesNamespace<Dependencies>;
95
- readonly #routeLifecycle: RouteLifecycleNamespace<Dependencies>;
96
- readonly #plugins: PluginsNamespace<Dependencies>;
97
- readonly #navigation: NavigationNamespace;
98
- readonly #lifecycle: RouterLifecycleNamespace;
99
-
100
- readonly #eventBus: EventBusNamespace;
101
-
102
- // ============================================================================
103
- // Constructor
104
- // ============================================================================
105
-
106
- /**
107
- * @param routes - Route definitions
108
- * @param options - Router options
109
- * @param dependencies - DI dependencies
110
- */
111
- constructor(
112
- routes: Route<Dependencies>[] = [],
113
- options: Partial<Options> = {},
114
- dependencies: Dependencies = {} as Dependencies,
115
- ) {
116
- // Configure logger if provided
117
- if (options.logger && isLoggerConfig(options.logger)) {
118
- logger.configure(options.logger);
119
- delete options.logger;
120
- }
121
-
122
- // =========================================================================
123
- // Validate inputs before creating namespaces
124
- // =========================================================================
125
-
126
- // Always validate options
127
- OptionsNamespace.validateOptionsIsObject(options);
128
-
129
- // Unconditional guard-level validation before creating namespaces
130
- guardDependencies(dependencies);
131
-
132
- if (routes.length > 0) {
133
- guardRouteStructure(routes);
134
- }
135
-
136
- // =========================================================================
137
- // Create Namespaces
138
- // =========================================================================
139
-
140
- this.#options = new OptionsNamespace(options);
141
- this.#limits = createLimits(options.limits);
142
- this.#dependenciesStore =
143
- createDependenciesStore<Dependencies>(dependencies);
144
- this.#state = new StateNamespace();
145
- this.#routes = new RoutesNamespace<Dependencies>(
146
- routes,
147
- deriveMatcherOptions(this.#options.get()),
148
- );
149
- this.#routeLifecycle = new RouteLifecycleNamespace<Dependencies>();
150
- this.#plugins = new PluginsNamespace<Dependencies>();
151
- this.#navigation = new NavigationNamespace();
152
- this.#lifecycle = new RouterLifecycleNamespace();
153
-
154
- // =========================================================================
155
- // Initialize EventBus
156
- // =========================================================================
157
-
158
- const routerFSM = createRouterFSM();
159
- // eslint-disable-next-line unicorn/prefer-event-target
160
- const emitter = new EventEmitter<RouterEventMap>({
161
- onListenerError: (eventName, error) => {
162
- logger.error("Router", `Error in listener for ${eventName}:`, error);
163
- },
164
- onListenerWarn: (eventName, count) => {
165
- logger.warn(
166
- "router.addEventListener",
167
- `Event "${eventName}" has ${count} listeners — possible memory leak`,
168
- );
169
- },
170
- });
171
-
172
- this.#eventBus = new EventBusNamespace({ routerFSM, emitter });
173
-
174
- // =========================================================================
175
- // Wire Dependencies
176
- // =========================================================================
177
-
178
- wireRouter(
179
- new RouterWiringBuilder<Dependencies>({
180
- router: this,
181
- options: this.#options,
182
- limits: this.#limits,
183
- dependenciesStore: this.#dependenciesStore,
184
- state: this.#state,
185
- routes: this.#routes,
186
- routeLifecycle: this.#routeLifecycle,
187
- plugins: this.#plugins,
188
- navigation: this.#navigation,
189
- lifecycle: this.#lifecycle,
190
- eventBus: this.#eventBus,
191
- }),
192
- );
193
-
194
- // =========================================================================
195
- // Register Internals (WeakMap for plugin/infrastructure access)
196
- // =========================================================================
197
-
198
- const interceptorsMap: RouterInternals["interceptors"] = new Map();
199
-
200
- registerInternals(this, {
201
- makeState: (name, params, path, meta) =>
202
- this.#state.makeState(name, params, path, meta),
203
- // `as unknown as` is required: createBinaryInterceptable returns a
204
- // non-generic `(a: A, b: B) => R`, but RouterInternals["forwardState"]
205
- // is declared with a generic parameter `<P extends Params = Params>`,
206
- // which tsc will not infer from the non-generic source. Sonar S4325
207
- // misclassifies this as a redundant cast.
208
- forwardState: createBinaryInterceptable(
209
- "forwardState",
210
- (name: string, params: Params) =>
211
- this.#routes.forwardState(name, params),
212
- interceptorsMap,
213
- ) as unknown as RouterInternals["forwardState"],
214
- buildStateResolved: (name, params) =>
215
- this.#routes.buildStateResolved(name, params),
216
- matchPath: (path, matchOptions) =>
217
- this.#routes.matchPath(path, matchOptions),
218
- getOptions: () => this.#options.get(),
219
- addEventListener: (eventName, cb) =>
220
- this.#eventBus.addEventListener(eventName, cb),
221
- treeChanged: {
222
- emit: (event) => {
223
- this.#eventBus.emitTreeChanged(event);
224
- },
225
- subscribe: (handler) => this.#eventBus.subscribeTreeChanged(handler),
226
- listenerCount: () => this.#eventBus.treeChangedListenerCount(),
227
- },
228
- buildPath: createBinaryInterceptable(
229
- "buildPath",
230
- (route: string, params?: Params) =>
231
- this.#routes.buildPath(
232
- route,
233
- params ?? EMPTY_PARAMS,
234
- this.#options.get(),
235
- ),
236
- interceptorsMap,
237
- ),
238
- emitTransitionError: (error) => {
239
- this.#eventBus.sendFailSafe(undefined, this.#state.get(), error);
240
- },
241
- start: createInterceptable(
242
- "start",
243
- (path: string) => {
244
- return this.#lifecycle.start(path);
245
- },
246
- interceptorsMap,
247
- ),
248
- navigateToState: (state, navOpts) => {
249
- // Plugin-only navigation primitive (#525). Mirrors the same
250
- // unhandled-rejection suppression and lastSync* bookkeeping used by
251
- // the public Router.navigate facade so plugin call-sites can
252
- // fire-and-forget the returned promise (popstate handlers do).
253
- const promiseState = this.#navigation.navigateToState(
254
- state,
255
- navOpts ?? EMPTY_OPTS,
256
- );
257
-
258
- if (this.#navigation.lastSyncResolved) {
259
- this.#navigation.lastSyncResolved = false;
260
- } else if (this.#navigation.lastSyncRejected) {
261
- this.#navigation.lastSyncRejected = false;
262
- } else {
263
- Router.#suppressUnhandledRejection(promiseState);
264
- }
265
-
266
- return promiseState;
267
- },
268
- interceptors: interceptorsMap,
269
- setRootPath: (rootPath) => {
270
- this.#routes.setRootPath(rootPath);
271
- },
272
- getRootPath: () => this.#routes.getStore().rootPath,
273
- getTree: () => this.#routes.getStore().tree,
274
- isDisposed: () => this.#eventBus.isDisposed(),
275
- validator: null,
276
- // Dependencies (issue #172)
277
- dependenciesGetStore: () => this.#dependenciesStore,
278
- // Clone support (issue #173)
279
- cloneOptions: () => ({ ...this.#options.get() }),
280
- cloneDependencies: () => ({ ...this.#dependenciesStore.dependencies }),
281
- getLifecycleFactories: () => this.#routeLifecycle.getFactories(),
282
- getPluginFactories: () => this.#plugins.getAll(),
283
- routeGetStore: () => this.#routes.getStore(),
284
- // Cross-namespace state (issue #174)
285
- getStateName: () => this.#state.get()?.name,
286
- isTransitioning: () => this.#eventBus.isTransitioning(),
287
- clearState: () => {
288
- this.#state.set(undefined);
289
- },
290
- setState: (state) => {
291
- this.#state.set(state);
292
- },
293
- routerExtensions: [],
294
- contextClaimRecords: new Set(),
295
- hydrationState: null,
296
- });
297
-
298
- // =========================================================================
299
- // Bind Public Methods
300
- // =========================================================================
301
- // All public methods that access private fields must be bound to preserve
302
- // `this` context when methods are extracted as references.
303
- // See: https://github.com/tc39/proposal-bind-operator
304
- // =========================================================================
305
-
306
- // Path & State Building
307
- this.isActiveRoute = this.isActiveRoute.bind(this);
308
- this.buildPath = this.buildPath.bind(this);
309
-
310
- // State Management
311
- this.getState = this.getState.bind(this);
312
- this.getPreviousState = this.getPreviousState.bind(this);
313
- this.areStatesEqual = this.areStatesEqual.bind(this);
314
- this.shouldUpdateNode = this.shouldUpdateNode.bind(this);
315
-
316
- // Router Lifecycle
317
- this.isActive = this.isActive.bind(this);
318
- this.start = this.start.bind(this);
319
- this.stop = this.stop.bind(this);
320
- this.dispose = this.dispose.bind(this);
321
-
322
- // Route Lifecycle (Guards)
323
- this.canNavigateTo = this.canNavigateTo.bind(this);
324
-
325
- // Plugins
326
- this.usePlugin = this.usePlugin.bind(this);
327
-
328
- // Navigation
329
- this.navigate = this.navigate.bind(this);
330
- this.navigateToDefault = this.navigateToDefault.bind(this);
331
- this.navigateToNotFound = this.navigateToNotFound.bind(this);
332
-
333
- // Subscription
334
- this.subscribe = this.subscribe.bind(this);
335
- this.subscribeLeave = this.subscribeLeave.bind(this);
336
- this.isLeaveApproved = this.isLeaveApproved.bind(this);
337
- }
338
-
339
- // ============================================================================
340
- // Path & State Building
341
- // ============================================================================
342
-
343
- isActiveRoute(
344
- name: string,
345
- params?: Params,
346
- strictEquality?: boolean,
347
- ignoreQueryParams?: boolean,
348
- ): boolean {
349
- getInternals(this).validator?.routes.validateIsActiveRouteArgs(
350
- name,
351
- params,
352
- strictEquality,
353
- ignoreQueryParams,
354
- );
355
-
356
- getInternals(this).validator?.routes.validateRouteName(
357
- name,
358
- "isActiveRoute",
359
- );
360
-
361
- // Empty string is special case - warn and return false (root node is not a parent)
362
- if (name === "") {
363
- logger.warn(
364
- "real-router",
365
- 'isActiveRoute("") called with empty string. Root node is not considered a parent of any route.',
366
- );
367
-
368
- return false;
369
- }
370
-
371
- return this.#routes.isActiveRoute(
372
- name,
373
- params,
374
- strictEquality,
375
- ignoreQueryParams,
376
- );
377
- }
378
-
379
- buildPath(route: string, params?: Params): string {
380
- const ctx = getInternals(this);
381
-
382
- ctx.validator?.routes.validateBuildPathArgs(route);
383
- ctx.validator?.navigation.validateParams(params, "buildPath");
384
-
385
- return ctx.buildPath(route, normalizeParams(params));
386
- }
387
-
388
- // ============================================================================
389
- // State Management (delegated to StateNamespace)
390
- // ============================================================================
391
-
392
- getState<P extends Params = Params>(): State<P> | undefined {
393
- return this.#state.get<P>();
394
- }
395
-
396
- getPreviousState(): State | undefined {
397
- return this.#state.getPrevious();
398
- }
399
-
400
- areStatesEqual(
401
- state1: State | undefined,
402
- state2: State | undefined,
403
- ignoreQueryParams = true,
404
- ): boolean {
405
- getInternals(this).validator?.state.validateAreStatesEqualArgs(
406
- state1,
407
- state2,
408
- ignoreQueryParams,
409
- );
410
-
411
- return this.#state.areStatesEqual(state1, state2, ignoreQueryParams);
412
- }
413
-
414
- shouldUpdateNode(
415
- nodeName: string,
416
- ): (toState: State, fromState?: State) => boolean {
417
- getInternals(this).validator?.routes.validateShouldUpdateNodeArgs(nodeName);
418
-
419
- return RoutesNamespace.shouldUpdateNode(nodeName);
420
- }
421
-
422
- // ============================================================================
423
- // Router Lifecycle
424
- // ============================================================================
425
-
426
- isActive(): boolean {
427
- return this.#eventBus.isActive();
428
- }
429
-
430
- start(startPath: string): Promise<State> {
431
- if (!this.#eventBus.canStart()) {
432
- return Promise.reject(CACHED_ALREADY_STARTED_ERROR);
433
- }
434
-
435
- getInternals(this).validator?.navigation.validateStartArgs(startPath);
436
-
437
- this.#eventBus.sendStart();
438
-
439
- // Convert sync interceptor throws to rejections so the recovery branch in
440
- // .catch is reachable; otherwise the throw escapes synchronously, FSM is
441
- // left in STARTING, and the router is permanently bricked (#668).
442
- let internalStart: Promise<State>;
443
-
444
- try {
445
- internalStart = getInternals(this).start(startPath);
446
- } catch (syncError: unknown) {
447
- // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- preserve original throw shape from user-provided start interceptor
448
- internalStart = Promise.reject(syncError);
449
- }
450
-
451
- const promiseState = internalStart.catch((error: unknown) => {
452
- if (this.#eventBus.isReady()) {
453
- this.#lifecycle.stop();
454
- this.#eventBus.sendStop();
455
- } else if (this.#eventBus.isStarting()) {
456
- this.#eventBus.sendFail(undefined, undefined, error);
457
- }
458
-
459
- throw error;
460
- });
461
-
462
- Router.#suppressUnhandledRejection(promiseState);
463
-
464
- return promiseState;
465
- }
466
-
467
- stop(): this {
468
- this.#navigation.abortCurrentNavigation();
469
- this.#eventBus.sendCancelIfPossible(this.#state.get());
470
-
471
- if (!this.#eventBus.isReady() && !this.#eventBus.isTransitioning()) {
472
- return this;
473
- }
474
-
475
- this.#lifecycle.stop();
476
- this.#eventBus.sendStop();
477
-
478
- return this;
479
- }
480
-
481
- dispose(): void {
482
- if (this.#eventBus.isDisposed()) {
483
- return;
484
- }
485
-
486
- this.#navigation.abortCurrentNavigation();
487
- this.#eventBus.sendCancelIfPossible(this.#state.get());
488
-
489
- if (this.#eventBus.isReady() || this.#eventBus.isTransitioning()) {
490
- this.#lifecycle.stop();
491
- this.#eventBus.sendStop();
492
- }
493
-
494
- this.#eventBus.sendDispose();
495
- this.#eventBus.clearAll();
496
-
497
- this.#plugins.disposeAll();
498
-
499
- // Safety net: clean up extensions plugins failed to remove in teardown
500
- const ctx = getInternals(this);
501
-
502
- for (const extension of ctx.routerExtensions) {
503
- for (const key of extension.keys) {
504
- delete (this as Record<string, unknown>)[key];
505
- }
506
- }
507
-
508
- ctx.routerExtensions.length = 0;
509
-
510
- // Safety net: release context namespace claims plugins failed to release in teardown
511
- ctx.contextClaimRecords.clear();
512
-
513
- this.#routes.clearRoutes();
514
- this.#routeLifecycle.clearAll();
515
- this.#state.reset();
516
- this.#dependenciesStore.dependencies = Object.create(
517
- null,
518
- ) as Partial<Dependencies>;
519
-
520
- this.#markDisposed();
521
- }
522
-
523
- // ============================================================================
524
- // Route Lifecycle (Guards)
525
- // ============================================================================
526
-
527
- canNavigateTo(name: string, params?: Params): boolean {
528
- const ctx = getInternals(this);
529
-
530
- ctx.validator?.routes.validateRouteName(name, "canNavigateTo");
531
- ctx.validator?.navigation.validateParams(params, "canNavigateTo");
532
-
533
- if (!this.#routes.hasRoute(name)) {
534
- return false;
535
- }
536
-
537
- const { name: resolvedName, params: resolvedParams } = ctx.forwardState(
538
- name,
539
- params ?? {},
540
- );
541
- const toState = this.#state.makeState(resolvedName, resolvedParams);
542
- const fromState = this.#state.get();
543
-
544
- const { toDeactivate, toActivate } = getTransitionPath(toState, fromState);
545
-
546
- return this.#routeLifecycle.canNavigateTo(
547
- toDeactivate,
548
- toActivate,
549
- toState,
550
- fromState,
551
- );
552
- }
553
-
554
- // ============================================================================
555
- // Plugins
556
- // ============================================================================
557
-
558
- usePlugin(
559
- ...plugins: (PluginFactory<Dependencies> | false | null | undefined)[]
560
- ): Unsubscribe {
561
- const filtered = plugins.filter(Boolean) as PluginFactory<Dependencies>[];
562
-
563
- if (filtered.length === 0) {
564
- return () => {};
565
- }
566
-
567
- const ctx = getInternals(this);
568
-
569
- ctx.validator?.plugins.validatePluginLimit(
570
- this.#plugins.count(),
571
- this.#limits,
572
- );
573
- for (const plugin of filtered) {
574
- ctx.validator?.plugins.validateNoDuplicatePlugins(
575
- plugin,
576
- this.#plugins.getAll(),
577
- );
578
- }
579
-
580
- return this.#plugins.use(...filtered);
581
- }
582
-
583
- // ============================================================================
584
- // Subscription (backed by EventEmitter)
585
- // ============================================================================
586
-
587
- subscribe(listener: SubscribeFn): Unsubscribe {
588
- EventBusNamespace.validateSubscribeListener(listener);
589
-
590
- return this.#eventBus.subscribe(listener);
591
- }
592
-
593
- subscribeLeave(listener: LeaveFn): Unsubscribe {
594
- EventBusNamespace.validateSubscribeLeaveListener(listener);
595
-
596
- return this.#eventBus.subscribeLeave(listener);
597
- }
598
-
599
- isLeaveApproved(): boolean {
600
- return this.#eventBus.isLeaveApproved();
601
- }
602
-
603
- // ============================================================================
604
- // Navigation
605
- // ============================================================================
606
-
607
- navigate(
608
- routeName: string,
609
- routeParams?: Params,
610
- options?: NavigationOptions,
611
- ): Promise<State> {
612
- const ctx = getInternals(this);
613
-
614
- ctx.validator?.navigation.validateNavigateArgs(routeName);
615
- ctx.validator?.navigation.validateParams(routeParams, "navigate");
616
-
617
- const opts = options ?? EMPTY_OPTS;
618
-
619
- ctx.validator?.navigation.validateNavigationOptions(opts, "navigate");
620
-
621
- const promiseState = this.#navigation.navigate(
622
- routeName,
623
- routeParams ?? EMPTY_PARAMS,
624
- opts,
625
- );
626
-
627
- if (this.#navigation.lastSyncResolved) {
628
- this.#navigation.lastSyncResolved = false;
629
- } else if (this.#navigation.lastSyncRejected) {
630
- // Cached rejection — already pre-suppressed at module load, skip .catch()
631
- this.#navigation.lastSyncRejected = false;
632
- } else {
633
- Router.#suppressUnhandledRejection(promiseState);
634
- }
635
-
636
- return promiseState;
637
- }
638
-
639
- navigateToDefault(options?: NavigationOptions): Promise<State> {
640
- const ctx = getInternals(this);
641
-
642
- ctx.validator?.navigation.validateNavigateToDefaultArgs(options);
643
-
644
- const opts = options ?? EMPTY_OPTS;
645
-
646
- ctx.validator?.navigation.validateNavigationOptions(
647
- opts,
648
- "navigateToDefault",
649
- );
650
-
651
- const promiseState = this.#navigation.navigateToDefault(opts);
652
-
653
- if (this.#navigation.lastSyncResolved) {
654
- this.#navigation.lastSyncResolved = false;
655
- } else if (this.#navigation.lastSyncRejected) {
656
- this.#navigation.lastSyncRejected = false;
657
- } else {
658
- Router.#suppressUnhandledRejection(promiseState);
659
- }
660
-
661
- return promiseState;
662
- }
663
-
664
- navigateToNotFound(path?: string): State {
665
- if (!this.#eventBus.isActive()) {
666
- throw new RouterError(errorCodes.ROUTER_NOT_STARTED);
667
- }
668
-
669
- if (path !== undefined && typeof path !== "string") {
670
- throw new TypeError(
671
- `[router.navigateToNotFound] path must be a string, got ${typeof path}`,
672
- );
673
- }
674
-
675
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- isActive() guarantees state exists
676
- const resolvedPath = path ?? this.#state.get()!.path;
677
-
678
- return this.#navigation.navigateToNotFound(resolvedPath);
679
- }
680
-
681
- /**
682
- * Pre-allocated callback for #suppressUnhandledRejection.
683
- * Avoids creating a new closure on every navigate() call.
684
- */
685
- static readonly #onSuppressedError = (error: unknown): void => {
686
- if (
687
- error instanceof RouterError &&
688
- SUPPRESSED_ERROR_CODES.has(error.code)
689
- ) {
690
- return;
691
- }
692
-
693
- logger.error("router.navigate", "Unexpected navigation error", error);
694
- };
695
-
696
- /**
697
- * Fire-and-forget safety: prevents unhandled rejection warnings
698
- * when navigate/navigateToDefault is called without await.
699
- * Expected errors are silently suppressed; unexpected ones are logged.
700
- */
701
- static #suppressUnhandledRejection(promise: Promise<State>): void {
702
- promise.catch(Router.#onSuppressedError);
703
- }
704
-
705
- #markDisposed(): void {
706
- this.navigate = throwDisposed;
707
- this.navigateToDefault = throwDisposed;
708
- this.navigateToNotFound = throwDisposed;
709
- this.start = throwDisposed;
710
- this.stop = throwDisposed;
711
- this.usePlugin = throwDisposed;
712
-
713
- this.subscribe = throwDisposed;
714
- this.subscribeLeave = throwDisposed;
715
- this.canNavigateTo = throwDisposed;
716
- }
717
- }
718
-
719
- function throwDisposed(): never {
720
- throw new RouterError(errorCodes.ROUTER_DISPOSED);
721
- }
722
-
723
- /**
724
- * Derives CreateMatcherOptions from router Options.
725
- * Maps core option names to matcher option names.
726
- */
727
- function deriveMatcherOptions(
728
- options: Readonly<Options>,
729
- ): CreateMatcherOptions {
730
- return {
731
- strictTrailingSlash: options.trailingSlash === "strict",
732
- strictQueryParams: options.queryParamsMode === "strict",
733
- urlParamsEncoding: options.urlParamsEncoding,
734
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
735
- queryParams: options.queryParams!,
736
- };
737
- }