@real-router/core 0.56.0 → 0.57.0

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 (108) hide show
  1. package/dist/cjs/Router-BSGzVINO.js +6 -0
  2. package/dist/cjs/Router-BSGzVINO.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-7z-60z_f.js} +2 -2
  6. package/dist/cjs/{cloneRouter-DRieJvam.js.map → cloneRouter-7z-60z_f.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.d.ts +1 -1
  10. package/dist/cjs/index.js +1 -1
  11. package/dist/cjs/utils.js +1 -1
  12. package/dist/cjs/validation.d.ts +1 -1
  13. package/dist/esm/Router-B7txWo9N.mjs +6 -0
  14. package/dist/esm/Router-B7txWo9N.mjs.map +1 -0
  15. package/dist/esm/api.d.mts +1 -1
  16. package/dist/esm/api.mjs +1 -1
  17. package/dist/esm/{cloneRouter-DHrH6D_z.mjs → cloneRouter-BNCQ7tIa.mjs} +2 -2
  18. package/dist/esm/{cloneRouter-DHrH6D_z.mjs.map → cloneRouter-BNCQ7tIa.mjs.map} +1 -1
  19. package/dist/esm/{index-C-i6vx5Y.d.mts → index-BWUmnecT.d.mts} +1 -2
  20. package/dist/esm/index-BWUmnecT.d.mts.map +1 -0
  21. package/dist/esm/index.d.mts +1 -1
  22. package/dist/esm/index.mjs +1 -1
  23. package/dist/esm/utils.mjs +1 -1
  24. package/dist/esm/validation.d.mts +1 -1
  25. package/package.json +2 -3
  26. package/dist/cjs/Router-IEGavTKk.js +0 -6
  27. package/dist/cjs/Router-IEGavTKk.js.map +0 -1
  28. package/dist/cjs/index-C-i6vx5Y.d.ts.map +0 -1
  29. package/dist/esm/Router-B3aeavRb.mjs +0 -6
  30. package/dist/esm/Router-B3aeavRb.mjs.map +0 -1
  31. package/dist/esm/index-C-i6vx5Y.d.mts.map +0 -1
  32. package/src/Router.ts +0 -737
  33. package/src/RouterError.ts +0 -324
  34. package/src/api/cloneRouter.ts +0 -159
  35. package/src/api/getDependenciesApi.ts +0 -160
  36. package/src/api/getLifecycleApi.ts +0 -65
  37. package/src/api/getPluginApi.ts +0 -228
  38. package/src/api/getRoutesApi.ts +0 -831
  39. package/src/api/helpers.ts +0 -10
  40. package/src/api/index.ts +0 -16
  41. package/src/api/types.ts +0 -12
  42. package/src/constants.ts +0 -101
  43. package/src/createRouter.ts +0 -32
  44. package/src/fsm/index.ts +0 -5
  45. package/src/fsm/routerFSM.ts +0 -130
  46. package/src/getNavigator.ts +0 -30
  47. package/src/guards.ts +0 -46
  48. package/src/helpers.ts +0 -197
  49. package/src/index.ts +0 -66
  50. package/src/internals.ts +0 -228
  51. package/src/namespaces/DependenciesNamespace/dependenciesStore.ts +0 -30
  52. package/src/namespaces/DependenciesNamespace/index.ts +0 -5
  53. package/src/namespaces/EventBusNamespace/EventBusNamespace.ts +0 -522
  54. package/src/namespaces/EventBusNamespace/index.ts +0 -5
  55. package/src/namespaces/EventBusNamespace/types.ts +0 -11
  56. package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +0 -552
  57. package/src/namespaces/NavigationNamespace/constants.ts +0 -55
  58. package/src/namespaces/NavigationNamespace/index.ts +0 -5
  59. package/src/namespaces/NavigationNamespace/transition/completeTransition.ts +0 -108
  60. package/src/namespaces/NavigationNamespace/transition/errorHandling.ts +0 -124
  61. package/src/namespaces/NavigationNamespace/transition/guardPhase.ts +0 -283
  62. package/src/namespaces/NavigationNamespace/types.ts +0 -110
  63. package/src/namespaces/OptionsNamespace/OptionsNamespace.ts +0 -28
  64. package/src/namespaces/OptionsNamespace/constants.ts +0 -19
  65. package/src/namespaces/OptionsNamespace/helpers.ts +0 -50
  66. package/src/namespaces/OptionsNamespace/index.ts +0 -7
  67. package/src/namespaces/OptionsNamespace/validators.ts +0 -13
  68. package/src/namespaces/PluginsNamespace/PluginsNamespace.ts +0 -291
  69. package/src/namespaces/PluginsNamespace/constants.ts +0 -34
  70. package/src/namespaces/PluginsNamespace/index.ts +0 -7
  71. package/src/namespaces/PluginsNamespace/types.ts +0 -22
  72. package/src/namespaces/PluginsNamespace/validators.ts +0 -28
  73. package/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts +0 -558
  74. package/src/namespaces/RouteLifecycleNamespace/index.ts +0 -5
  75. package/src/namespaces/RouteLifecycleNamespace/types.ts +0 -10
  76. package/src/namespaces/RouterLifecycleNamespace/RouterLifecycleNamespace.ts +0 -81
  77. package/src/namespaces/RouterLifecycleNamespace/constants.ts +0 -25
  78. package/src/namespaces/RouterLifecycleNamespace/index.ts +0 -5
  79. package/src/namespaces/RouterLifecycleNamespace/types.ts +0 -30
  80. package/src/namespaces/RoutesNamespace/RoutesNamespace.ts +0 -582
  81. package/src/namespaces/RoutesNamespace/constants.ts +0 -6
  82. package/src/namespaces/RoutesNamespace/forwardChain.ts +0 -34
  83. package/src/namespaces/RoutesNamespace/helpers.ts +0 -204
  84. package/src/namespaces/RoutesNamespace/index.ts +0 -11
  85. package/src/namespaces/RoutesNamespace/routeGuards.ts +0 -62
  86. package/src/namespaces/RoutesNamespace/routesStore.ts +0 -566
  87. package/src/namespaces/RoutesNamespace/types.ts +0 -81
  88. package/src/namespaces/StateNamespace/StateNamespace.ts +0 -224
  89. package/src/namespaces/StateNamespace/helpers.ts +0 -24
  90. package/src/namespaces/StateNamespace/index.ts +0 -5
  91. package/src/namespaces/StateNamespace/types.ts +0 -15
  92. package/src/namespaces/index.ts +0 -35
  93. package/src/stateMetaStore.ts +0 -15
  94. package/src/transitionPath.ts +0 -440
  95. package/src/typeGuards.ts +0 -59
  96. package/src/types/RouterValidator.ts +0 -156
  97. package/src/types.ts +0 -77
  98. package/src/utils/createRequestScope.ts +0 -174
  99. package/src/utils/getStaticPaths.ts +0 -50
  100. package/src/utils/hydrateRouter.ts +0 -89
  101. package/src/utils/index.ts +0 -27
  102. package/src/utils/serializeRouterState.ts +0 -120
  103. package/src/utils/serializeState.ts +0 -63
  104. package/src/validation.ts +0 -12
  105. package/src/wiring/RouterWiringBuilder.ts +0 -275
  106. package/src/wiring/index.ts +0 -7
  107. package/src/wiring/types.ts +0 -47
  108. 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
- }