@real-router/core 0.22.0 → 0.23.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.
- package/README.md +1 -3
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/metafile-cjs.json +1 -1
- package/dist/esm/index.d.mts +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/metafile-esm.json +1 -1
- package/package.json +7 -5
- package/src/Router.ts +1174 -0
- package/src/RouterError.ts +324 -0
- package/src/constants.ts +112 -0
- package/src/createRouter.ts +32 -0
- package/src/fsm/index.ts +5 -0
- package/src/fsm/routerFSM.ts +129 -0
- package/src/getNavigator.ts +15 -0
- package/src/helpers.ts +194 -0
- package/src/index.ts +46 -0
- package/src/namespaces/CloneNamespace/CloneNamespace.ts +120 -0
- package/src/namespaces/CloneNamespace/index.ts +3 -0
- package/src/namespaces/CloneNamespace/types.ts +46 -0
- package/src/namespaces/DependenciesNamespace/DependenciesNamespace.ts +250 -0
- package/src/namespaces/DependenciesNamespace/index.ts +3 -0
- package/src/namespaces/DependenciesNamespace/validators.ts +105 -0
- package/src/namespaces/EventBusNamespace/EventBusNamespace.ts +272 -0
- package/src/namespaces/EventBusNamespace/index.ts +5 -0
- package/src/namespaces/EventBusNamespace/types.ts +11 -0
- package/src/namespaces/MiddlewareNamespace/MiddlewareNamespace.ts +206 -0
- package/src/namespaces/MiddlewareNamespace/index.ts +5 -0
- package/src/namespaces/MiddlewareNamespace/types.ts +28 -0
- package/src/namespaces/MiddlewareNamespace/validators.ts +96 -0
- package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +308 -0
- package/src/namespaces/NavigationNamespace/index.ts +5 -0
- package/src/namespaces/NavigationNamespace/transition/executeLifecycleHooks.ts +84 -0
- package/src/namespaces/NavigationNamespace/transition/executeMiddleware.ts +56 -0
- package/src/namespaces/NavigationNamespace/transition/index.ts +107 -0
- package/src/namespaces/NavigationNamespace/transition/makeError.ts +37 -0
- package/src/namespaces/NavigationNamespace/transition/mergeStates.ts +54 -0
- package/src/namespaces/NavigationNamespace/transition/processLifecycleResult.ts +81 -0
- package/src/namespaces/NavigationNamespace/transition/wrapSyncError.ts +82 -0
- package/src/namespaces/NavigationNamespace/types.ts +129 -0
- package/src/namespaces/NavigationNamespace/validators.ts +87 -0
- package/src/namespaces/OptionsNamespace/OptionsNamespace.ts +50 -0
- package/src/namespaces/OptionsNamespace/constants.ts +41 -0
- package/src/namespaces/OptionsNamespace/helpers.ts +51 -0
- package/src/namespaces/OptionsNamespace/index.ts +11 -0
- package/src/namespaces/OptionsNamespace/validators.ts +252 -0
- package/src/namespaces/PluginsNamespace/PluginsNamespace.ts +325 -0
- package/src/namespaces/PluginsNamespace/constants.ts +35 -0
- package/src/namespaces/PluginsNamespace/index.ts +7 -0
- package/src/namespaces/PluginsNamespace/types.ts +32 -0
- package/src/namespaces/PluginsNamespace/validators.ts +79 -0
- package/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts +389 -0
- package/src/namespaces/RouteLifecycleNamespace/index.ts +5 -0
- package/src/namespaces/RouteLifecycleNamespace/types.ts +17 -0
- package/src/namespaces/RouteLifecycleNamespace/validators.ts +65 -0
- package/src/namespaces/RouterLifecycleNamespace/RouterLifecycleNamespace.ts +140 -0
- package/src/namespaces/RouterLifecycleNamespace/constants.ts +25 -0
- package/src/namespaces/RouterLifecycleNamespace/index.ts +5 -0
- package/src/namespaces/RouterLifecycleNamespace/types.ts +23 -0
- package/src/namespaces/RoutesNamespace/RoutesNamespace.ts +1482 -0
- package/src/namespaces/RoutesNamespace/constants.ts +14 -0
- package/src/namespaces/RoutesNamespace/helpers.ts +532 -0
- package/src/namespaces/RoutesNamespace/index.ts +9 -0
- package/src/namespaces/RoutesNamespace/stateBuilder.ts +70 -0
- package/src/namespaces/RoutesNamespace/types.ts +82 -0
- package/src/namespaces/RoutesNamespace/validators.ts +331 -0
- package/src/namespaces/StateNamespace/StateNamespace.ts +317 -0
- package/src/namespaces/StateNamespace/helpers.ts +43 -0
- package/src/namespaces/StateNamespace/index.ts +5 -0
- package/src/namespaces/StateNamespace/types.ts +15 -0
- package/src/namespaces/index.ts +42 -0
- package/src/transitionPath.ts +441 -0
- package/src/typeGuards.ts +74 -0
- package/src/types.ts +194 -0
- package/src/wiring/RouterWiringBuilder.ts +235 -0
- package/src/wiring/index.ts +7 -0
- package/src/wiring/types.ts +53 -0
- package/src/wiring/wireRouter.ts +29 -0
package/src/Router.ts
ADDED
|
@@ -0,0 +1,1174 @@
|
|
|
1
|
+
// packages/core/src/Router.ts
|
|
2
|
+
/* eslint-disable unicorn/prefer-event-target -- custom EventEmitter package, not Node.js EventEmitter */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Router class - facade with integrated namespaces.
|
|
6
|
+
*
|
|
7
|
+
* All functionality is now provided by namespace classes.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { logger } from "@real-router/logger";
|
|
11
|
+
import { EventEmitter } from "event-emitter";
|
|
12
|
+
import { validateRouteName } from "type-guards";
|
|
13
|
+
|
|
14
|
+
import { errorCodes } from "./constants";
|
|
15
|
+
import { createRouterFSM } from "./fsm";
|
|
16
|
+
import { createLimits } from "./helpers";
|
|
17
|
+
import {
|
|
18
|
+
CloneNamespace,
|
|
19
|
+
DependenciesNamespace,
|
|
20
|
+
EventBusNamespace,
|
|
21
|
+
MiddlewareNamespace,
|
|
22
|
+
NavigationNamespace,
|
|
23
|
+
OptionsNamespace,
|
|
24
|
+
PluginsNamespace,
|
|
25
|
+
RouteLifecycleNamespace,
|
|
26
|
+
RouterLifecycleNamespace,
|
|
27
|
+
RoutesNamespace,
|
|
28
|
+
StateNamespace,
|
|
29
|
+
} from "./namespaces";
|
|
30
|
+
import { CACHED_ALREADY_STARTED_ERROR } from "./namespaces/RouterLifecycleNamespace/constants";
|
|
31
|
+
import { RouterError } from "./RouterError";
|
|
32
|
+
import { getTransitionPath } from "./transitionPath";
|
|
33
|
+
import { isLoggerConfig } from "./typeGuards";
|
|
34
|
+
import { RouterWiringBuilder, wireRouter } from "./wiring";
|
|
35
|
+
|
|
36
|
+
import type {
|
|
37
|
+
ActivationFnFactory,
|
|
38
|
+
EventMethodMap,
|
|
39
|
+
Limits,
|
|
40
|
+
MiddlewareFactory,
|
|
41
|
+
PluginFactory,
|
|
42
|
+
Route,
|
|
43
|
+
RouteConfigUpdate,
|
|
44
|
+
RouterEventMap,
|
|
45
|
+
} from "./types";
|
|
46
|
+
import type {
|
|
47
|
+
DefaultDependencies,
|
|
48
|
+
EventName,
|
|
49
|
+
NavigationOptions,
|
|
50
|
+
Options,
|
|
51
|
+
Params,
|
|
52
|
+
Plugin,
|
|
53
|
+
RouteTreeState,
|
|
54
|
+
SimpleState,
|
|
55
|
+
State,
|
|
56
|
+
StateMetaInput,
|
|
57
|
+
SubscribeFn,
|
|
58
|
+
Unsubscribe,
|
|
59
|
+
} from "@real-router/types";
|
|
60
|
+
import type { CreateMatcherOptions } from "route-tree";
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Router class with integrated namespace architecture.
|
|
64
|
+
*
|
|
65
|
+
* All functionality is provided by namespace classes:
|
|
66
|
+
* - OptionsNamespace: getOptions (immutable)
|
|
67
|
+
* - DependenciesNamespace: get/set/remove dependencies
|
|
68
|
+
* - EventEmitter: event listeners, subscribe
|
|
69
|
+
* - StateNamespace: state storage (getState, setState, getPreviousState)
|
|
70
|
+
* - RoutesNamespace: route tree operations
|
|
71
|
+
* - RouteLifecycleNamespace: canActivate/canDeactivate guards
|
|
72
|
+
* - MiddlewareNamespace: middleware chain
|
|
73
|
+
* - PluginsNamespace: plugin lifecycle
|
|
74
|
+
* - NavigationNamespace: navigate, navigateToState
|
|
75
|
+
* - RouterLifecycleNamespace: start, stop, isStarted
|
|
76
|
+
*
|
|
77
|
+
* @internal This class implementation is internal. Use createRouter() instead.
|
|
78
|
+
*/
|
|
79
|
+
export class Router<
|
|
80
|
+
Dependencies extends DefaultDependencies = DefaultDependencies,
|
|
81
|
+
> {
|
|
82
|
+
// Index signatures to satisfy interface
|
|
83
|
+
[key: string]: unknown;
|
|
84
|
+
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// Namespaces
|
|
87
|
+
// ============================================================================
|
|
88
|
+
|
|
89
|
+
readonly #options: OptionsNamespace;
|
|
90
|
+
readonly #limits: Limits;
|
|
91
|
+
readonly #dependencies: DependenciesNamespace<Dependencies>;
|
|
92
|
+
readonly #state: StateNamespace;
|
|
93
|
+
readonly #routes: RoutesNamespace<Dependencies>;
|
|
94
|
+
readonly #routeLifecycle: RouteLifecycleNamespace<Dependencies>;
|
|
95
|
+
readonly #middleware: MiddlewareNamespace<Dependencies>;
|
|
96
|
+
readonly #plugins: PluginsNamespace<Dependencies>;
|
|
97
|
+
readonly #navigation: NavigationNamespace;
|
|
98
|
+
readonly #lifecycle: RouterLifecycleNamespace;
|
|
99
|
+
readonly #clone: CloneNamespace<Dependencies>;
|
|
100
|
+
|
|
101
|
+
readonly #eventBus: EventBusNamespace;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* When true, skips argument validation in public methods for production performance.
|
|
105
|
+
* Constructor options are always validated (needed to validate noValidate itself).
|
|
106
|
+
*/
|
|
107
|
+
readonly #noValidate: boolean;
|
|
108
|
+
|
|
109
|
+
// ============================================================================
|
|
110
|
+
// Constructor
|
|
111
|
+
// ============================================================================
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param routes - Route definitions
|
|
115
|
+
* @param options - Router options
|
|
116
|
+
* @param dependencies - DI dependencies
|
|
117
|
+
*/
|
|
118
|
+
constructor(
|
|
119
|
+
routes: Route<Dependencies>[] = [],
|
|
120
|
+
options: Partial<Options> = {},
|
|
121
|
+
dependencies: Dependencies = {} as Dependencies,
|
|
122
|
+
) {
|
|
123
|
+
// Configure logger if provided
|
|
124
|
+
if (options.logger && isLoggerConfig(options.logger)) {
|
|
125
|
+
logger.configure(options.logger);
|
|
126
|
+
delete options.logger;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// =========================================================================
|
|
130
|
+
// Validate inputs before creating namespaces
|
|
131
|
+
// =========================================================================
|
|
132
|
+
|
|
133
|
+
// Always validate options (needed to validate noValidate itself)
|
|
134
|
+
OptionsNamespace.validateOptions(options, "constructor");
|
|
135
|
+
|
|
136
|
+
// Extract noValidate BEFORE creating namespaces
|
|
137
|
+
const noValidate = options.noValidate ?? false;
|
|
138
|
+
|
|
139
|
+
// Conditional validation for dependencies
|
|
140
|
+
if (!noValidate) {
|
|
141
|
+
DependenciesNamespace.validateDependenciesObject(
|
|
142
|
+
dependencies,
|
|
143
|
+
"constructor",
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Conditional validation for initial routes - structure and batch duplicates
|
|
148
|
+
// Validation happens BEFORE tree is built, so tree is not passed
|
|
149
|
+
if (!noValidate && routes.length > 0) {
|
|
150
|
+
RoutesNamespace.validateAddRouteArgs(routes);
|
|
151
|
+
RoutesNamespace.validateRoutes(routes);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// =========================================================================
|
|
155
|
+
// Create Namespaces
|
|
156
|
+
// =========================================================================
|
|
157
|
+
|
|
158
|
+
this.#options = new OptionsNamespace(options);
|
|
159
|
+
this.#limits = createLimits(options.limits);
|
|
160
|
+
this.#dependencies = new DependenciesNamespace<Dependencies>(dependencies);
|
|
161
|
+
this.#state = new StateNamespace();
|
|
162
|
+
this.#routes = new RoutesNamespace<Dependencies>(
|
|
163
|
+
routes,
|
|
164
|
+
noValidate,
|
|
165
|
+
deriveMatcherOptions(this.#options.get()),
|
|
166
|
+
);
|
|
167
|
+
this.#routeLifecycle = new RouteLifecycleNamespace<Dependencies>();
|
|
168
|
+
this.#middleware = new MiddlewareNamespace<Dependencies>();
|
|
169
|
+
this.#plugins = new PluginsNamespace<Dependencies>();
|
|
170
|
+
this.#navigation = new NavigationNamespace();
|
|
171
|
+
this.#lifecycle = new RouterLifecycleNamespace();
|
|
172
|
+
this.#clone = new CloneNamespace<Dependencies>();
|
|
173
|
+
this.#noValidate = noValidate;
|
|
174
|
+
|
|
175
|
+
// =========================================================================
|
|
176
|
+
// Initialize EventBus
|
|
177
|
+
// =========================================================================
|
|
178
|
+
|
|
179
|
+
const routerFSM = createRouterFSM();
|
|
180
|
+
const emitter = new EventEmitter<RouterEventMap>({
|
|
181
|
+
onListenerError: (eventName, error) => {
|
|
182
|
+
logger.error("Router", `Error in listener for ${eventName}:`, error);
|
|
183
|
+
},
|
|
184
|
+
onListenerWarn: (eventName, count) => {
|
|
185
|
+
logger.warn(
|
|
186
|
+
"router.addEventListener",
|
|
187
|
+
`Event "${eventName}" has ${count} listeners — possible memory leak`,
|
|
188
|
+
);
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
this.#eventBus = new EventBusNamespace({ routerFSM, emitter });
|
|
193
|
+
|
|
194
|
+
// =========================================================================
|
|
195
|
+
// Wire Dependencies
|
|
196
|
+
// =========================================================================
|
|
197
|
+
|
|
198
|
+
wireRouter(
|
|
199
|
+
new RouterWiringBuilder<Dependencies>({
|
|
200
|
+
router: this,
|
|
201
|
+
options: this.#options,
|
|
202
|
+
limits: this.#limits,
|
|
203
|
+
dependencies: this.#dependencies,
|
|
204
|
+
state: this.#state,
|
|
205
|
+
routes: this.#routes,
|
|
206
|
+
routeLifecycle: this.#routeLifecycle,
|
|
207
|
+
middleware: this.#middleware,
|
|
208
|
+
plugins: this.#plugins,
|
|
209
|
+
navigation: this.#navigation,
|
|
210
|
+
lifecycle: this.#lifecycle,
|
|
211
|
+
clone: this.#clone,
|
|
212
|
+
eventBus: this.#eventBus,
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// =========================================================================
|
|
217
|
+
// Bind Public Methods
|
|
218
|
+
// =========================================================================
|
|
219
|
+
// All public methods that access private fields must be bound to preserve
|
|
220
|
+
// `this` context when methods are extracted as references.
|
|
221
|
+
// See: https://github.com/nicolo-ribaudo/tc39-proposal-bind-operator
|
|
222
|
+
// =========================================================================
|
|
223
|
+
|
|
224
|
+
// Route Management
|
|
225
|
+
this.addRoute = this.addRoute.bind(this);
|
|
226
|
+
this.removeRoute = this.removeRoute.bind(this);
|
|
227
|
+
this.clearRoutes = this.clearRoutes.bind(this);
|
|
228
|
+
this.getRoute = this.getRoute.bind(this);
|
|
229
|
+
this.hasRoute = this.hasRoute.bind(this);
|
|
230
|
+
this.updateRoute = this.updateRoute.bind(this);
|
|
231
|
+
|
|
232
|
+
// Path & State Building
|
|
233
|
+
this.isActiveRoute = this.isActiveRoute.bind(this);
|
|
234
|
+
this.buildPath = this.buildPath.bind(this);
|
|
235
|
+
this.matchPath = this.matchPath.bind(this);
|
|
236
|
+
this.setRootPath = this.setRootPath.bind(this);
|
|
237
|
+
this.getRootPath = this.getRootPath.bind(this);
|
|
238
|
+
|
|
239
|
+
// State Management
|
|
240
|
+
this.makeState = this.makeState.bind(this);
|
|
241
|
+
this.getState = this.getState.bind(this);
|
|
242
|
+
this.getPreviousState = this.getPreviousState.bind(this);
|
|
243
|
+
this.areStatesEqual = this.areStatesEqual.bind(this);
|
|
244
|
+
this.forwardState = this.forwardState.bind(this);
|
|
245
|
+
this.buildState = this.buildState.bind(this);
|
|
246
|
+
this.buildNavigationState = this.buildNavigationState.bind(this);
|
|
247
|
+
this.shouldUpdateNode = this.shouldUpdateNode.bind(this);
|
|
248
|
+
|
|
249
|
+
// Options
|
|
250
|
+
this.getOptions = this.getOptions.bind(this);
|
|
251
|
+
|
|
252
|
+
// Router Lifecycle
|
|
253
|
+
this.isActive = this.isActive.bind(this);
|
|
254
|
+
this.start = this.start.bind(this);
|
|
255
|
+
this.stop = this.stop.bind(this);
|
|
256
|
+
this.dispose = this.dispose.bind(this);
|
|
257
|
+
|
|
258
|
+
// Route Lifecycle (Guards)
|
|
259
|
+
this.addActivateGuard = this.addActivateGuard.bind(this);
|
|
260
|
+
this.addDeactivateGuard = this.addDeactivateGuard.bind(this);
|
|
261
|
+
this.removeActivateGuard = this.removeActivateGuard.bind(this);
|
|
262
|
+
this.removeDeactivateGuard = this.removeDeactivateGuard.bind(this);
|
|
263
|
+
this.canNavigateTo = this.canNavigateTo.bind(this);
|
|
264
|
+
|
|
265
|
+
// Plugins
|
|
266
|
+
this.usePlugin = this.usePlugin.bind(this);
|
|
267
|
+
|
|
268
|
+
// Middleware
|
|
269
|
+
this.useMiddleware = this.useMiddleware.bind(this);
|
|
270
|
+
// Dependencies
|
|
271
|
+
this.setDependency = this.setDependency.bind(this);
|
|
272
|
+
this.setDependencies = this.setDependencies.bind(this);
|
|
273
|
+
this.getDependency = this.getDependency.bind(this);
|
|
274
|
+
this.getDependencies = this.getDependencies.bind(this);
|
|
275
|
+
this.removeDependency = this.removeDependency.bind(this);
|
|
276
|
+
this.hasDependency = this.hasDependency.bind(this);
|
|
277
|
+
this.resetDependencies = this.resetDependencies.bind(this);
|
|
278
|
+
|
|
279
|
+
// Events
|
|
280
|
+
this.addEventListener = this.addEventListener.bind(this);
|
|
281
|
+
|
|
282
|
+
// Navigation
|
|
283
|
+
this.navigate = this.navigate.bind(this);
|
|
284
|
+
this.navigateToDefault = this.navigateToDefault.bind(this);
|
|
285
|
+
this.navigateToState = this.navigateToState.bind(this);
|
|
286
|
+
|
|
287
|
+
// Subscription
|
|
288
|
+
this.subscribe = this.subscribe.bind(this);
|
|
289
|
+
|
|
290
|
+
// Cloning
|
|
291
|
+
this.clone = this.clone.bind(this);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ============================================================================
|
|
295
|
+
// Route Management
|
|
296
|
+
// ============================================================================
|
|
297
|
+
|
|
298
|
+
addRoute(
|
|
299
|
+
routes: Route<Dependencies>[] | Route<Dependencies>,
|
|
300
|
+
options?: { parent?: string },
|
|
301
|
+
): this {
|
|
302
|
+
const routeArray = Array.isArray(routes) ? routes : [routes];
|
|
303
|
+
const parentName = options?.parent;
|
|
304
|
+
|
|
305
|
+
if (!this.#noValidate) {
|
|
306
|
+
// 1. Validate parent option format
|
|
307
|
+
if (parentName !== undefined) {
|
|
308
|
+
RoutesNamespace.validateParentOption(parentName);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 2. Static validation (route structure and properties)
|
|
312
|
+
RoutesNamespace.validateAddRouteArgs(routeArray);
|
|
313
|
+
|
|
314
|
+
// 3. State-dependent validation (parent exists, duplicates, forwardTo)
|
|
315
|
+
RoutesNamespace.validateRoutes(
|
|
316
|
+
routeArray,
|
|
317
|
+
this.#routes.getTree(),
|
|
318
|
+
this.#routes.getForwardRecord(),
|
|
319
|
+
parentName,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// 4. Execute (add definitions, register handlers, rebuild tree)
|
|
324
|
+
this.#routes.addRoutes(routeArray, parentName);
|
|
325
|
+
|
|
326
|
+
return this;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
removeRoute(name: string): this {
|
|
330
|
+
// Static validation
|
|
331
|
+
if (!this.#noValidate) {
|
|
332
|
+
RoutesNamespace.validateRemoveRouteArgs(name);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Instance validation (checks active route, navigation state)
|
|
336
|
+
const canRemove = this.#routes.validateRemoveRoute(
|
|
337
|
+
name,
|
|
338
|
+
this.#state.get()?.name,
|
|
339
|
+
this.#eventBus.isTransitioning(),
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
if (!canRemove) {
|
|
343
|
+
return this;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Perform removal
|
|
347
|
+
const wasRemoved = this.#routes.removeRoute(name);
|
|
348
|
+
|
|
349
|
+
if (!wasRemoved) {
|
|
350
|
+
logger.warn(
|
|
351
|
+
"router.removeRoute",
|
|
352
|
+
`Route "${name}" not found. No changes made.`,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return this;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
clearRoutes(): this {
|
|
360
|
+
const isNavigating = this.#eventBus.isTransitioning();
|
|
361
|
+
|
|
362
|
+
// Validate operation can proceed
|
|
363
|
+
const canClear = this.#routes.validateClearRoutes(isNavigating);
|
|
364
|
+
|
|
365
|
+
if (!canClear) {
|
|
366
|
+
return this;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Clear routes config (definitions, decoders, encoders, defaultParams, forwardMap)
|
|
370
|
+
this.#routes.clearRoutes();
|
|
371
|
+
|
|
372
|
+
// Clear all lifecycle handlers
|
|
373
|
+
this.#routeLifecycle.clearAll();
|
|
374
|
+
|
|
375
|
+
// Clear router state since all routes are removed
|
|
376
|
+
this.#state.set(undefined);
|
|
377
|
+
|
|
378
|
+
return this;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
getRoute(name: string): Route<Dependencies> | undefined {
|
|
382
|
+
if (!this.#noValidate) {
|
|
383
|
+
validateRouteName(name, "getRoute");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return this.#routes.getRoute(name);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
hasRoute(name: string): boolean {
|
|
390
|
+
if (!this.#noValidate) {
|
|
391
|
+
validateRouteName(name, "hasRoute");
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return this.#routes.hasRoute(name);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
updateRoute(name: string, updates: RouteConfigUpdate<Dependencies>): this {
|
|
398
|
+
// Validate name and updates object structure (basic checks only)
|
|
399
|
+
if (!this.#noValidate) {
|
|
400
|
+
RoutesNamespace.validateUpdateRouteBasicArgs(name, updates);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Cache all property values upfront to protect against mutating getters.
|
|
404
|
+
// This ensures consistent behavior regardless of getter side effects.
|
|
405
|
+
// Must happen AFTER basic validation but BEFORE property type validation.
|
|
406
|
+
const {
|
|
407
|
+
forwardTo,
|
|
408
|
+
defaultParams,
|
|
409
|
+
decodeParams,
|
|
410
|
+
encodeParams,
|
|
411
|
+
canActivate,
|
|
412
|
+
canDeactivate,
|
|
413
|
+
} = updates;
|
|
414
|
+
|
|
415
|
+
// Validate cached property values
|
|
416
|
+
if (!this.#noValidate) {
|
|
417
|
+
RoutesNamespace.validateUpdateRoutePropertyTypes(
|
|
418
|
+
forwardTo,
|
|
419
|
+
defaultParams,
|
|
420
|
+
decodeParams,
|
|
421
|
+
encodeParams,
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Warn if navigation is in progress
|
|
426
|
+
if (this.#eventBus.isTransitioning()) {
|
|
427
|
+
logger.error(
|
|
428
|
+
"router.updateRoute",
|
|
429
|
+
`Updating route "${name}" while navigation is in progress. This may cause unexpected behavior.`,
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Instance validation (route existence, forwardTo checks) - use cached values
|
|
434
|
+
if (!this.#noValidate) {
|
|
435
|
+
this.#routes.validateUpdateRoute(name, forwardTo);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Update route config
|
|
439
|
+
this.#routes.updateRouteConfig(name, {
|
|
440
|
+
forwardTo,
|
|
441
|
+
defaultParams,
|
|
442
|
+
decodeParams,
|
|
443
|
+
encodeParams,
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// Handle canActivate separately (uses RouteLifecycleNamespace)
|
|
447
|
+
// Use facade method for proper validation
|
|
448
|
+
if (canActivate !== undefined) {
|
|
449
|
+
if (canActivate === null) {
|
|
450
|
+
this.#routeLifecycle.clearCanActivate(name);
|
|
451
|
+
} else {
|
|
452
|
+
this.addActivateGuard(name, canActivate);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Handle canDeactivate separately (uses RouteLifecycleNamespace)
|
|
457
|
+
// Use facade method for proper validation
|
|
458
|
+
if (canDeactivate !== undefined) {
|
|
459
|
+
if (canDeactivate === null) {
|
|
460
|
+
this.#routeLifecycle.clearCanDeactivate(name);
|
|
461
|
+
} else {
|
|
462
|
+
this.addDeactivateGuard(name, canDeactivate);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return this;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ============================================================================
|
|
470
|
+
// Path & State Building
|
|
471
|
+
// ============================================================================
|
|
472
|
+
|
|
473
|
+
isActiveRoute(
|
|
474
|
+
name: string,
|
|
475
|
+
params?: Params,
|
|
476
|
+
strictEquality?: boolean,
|
|
477
|
+
ignoreQueryParams?: boolean,
|
|
478
|
+
): boolean {
|
|
479
|
+
if (!this.#noValidate) {
|
|
480
|
+
RoutesNamespace.validateIsActiveRouteArgs(
|
|
481
|
+
name,
|
|
482
|
+
params,
|
|
483
|
+
strictEquality,
|
|
484
|
+
ignoreQueryParams,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Empty string is special case - warn and return false (root node is not a parent)
|
|
489
|
+
if (name === "") {
|
|
490
|
+
logger.warn(
|
|
491
|
+
"real-router",
|
|
492
|
+
'isActiveRoute("") called with empty string. Root node is not considered a parent of any route.',
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return this.#routes.isActiveRoute(
|
|
499
|
+
name,
|
|
500
|
+
params,
|
|
501
|
+
strictEquality,
|
|
502
|
+
ignoreQueryParams,
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
buildPath(route: string, params?: Params): string {
|
|
507
|
+
if (!this.#noValidate) {
|
|
508
|
+
RoutesNamespace.validateBuildPathArgs(route);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return this.#routes.buildPath(route, params, this.#options.get());
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
matchPath<P extends Params = Params, MP extends Params = Params>(
|
|
515
|
+
path: string,
|
|
516
|
+
): State<P, MP> | undefined {
|
|
517
|
+
if (!this.#noValidate) {
|
|
518
|
+
RoutesNamespace.validateMatchPathArgs(path);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return this.#routes.matchPath<P, MP>(path, this.#options.get());
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
setRootPath(rootPath: string): void {
|
|
525
|
+
if (!this.#noValidate) {
|
|
526
|
+
RoutesNamespace.validateSetRootPathArgs(rootPath);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
this.#routes.setRootPath(rootPath);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
getRootPath(): string {
|
|
533
|
+
return this.#routes.getRootPath();
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// ============================================================================
|
|
537
|
+
// State Management (delegated to StateNamespace)
|
|
538
|
+
// ============================================================================
|
|
539
|
+
|
|
540
|
+
makeState<P extends Params = Params, MP extends Params = Params>(
|
|
541
|
+
name: string,
|
|
542
|
+
params?: P,
|
|
543
|
+
path?: string,
|
|
544
|
+
meta?: StateMetaInput<MP>,
|
|
545
|
+
forceId?: number,
|
|
546
|
+
): State<P, MP> {
|
|
547
|
+
if (!this.#noValidate) {
|
|
548
|
+
StateNamespace.validateMakeStateArgs(name, params, path, forceId);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return this.#state.makeState<P, MP>(name, params, path, meta, forceId);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
getState<P extends Params = Params, MP extends Params = Params>():
|
|
555
|
+
| State<P, MP>
|
|
556
|
+
| undefined {
|
|
557
|
+
return this.#state.get<P, MP>();
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
getPreviousState(): State | undefined {
|
|
561
|
+
return this.#state.getPrevious();
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
areStatesEqual(
|
|
565
|
+
state1: State | undefined,
|
|
566
|
+
state2: State | undefined,
|
|
567
|
+
ignoreQueryParams = true,
|
|
568
|
+
): boolean {
|
|
569
|
+
if (!this.#noValidate) {
|
|
570
|
+
StateNamespace.validateAreStatesEqualArgs(
|
|
571
|
+
state1,
|
|
572
|
+
state2,
|
|
573
|
+
ignoreQueryParams,
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return this.#state.areStatesEqual(state1, state2, ignoreQueryParams);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
forwardState<P extends Params = Params>(
|
|
581
|
+
routeName: string,
|
|
582
|
+
routeParams: P,
|
|
583
|
+
): SimpleState<P> {
|
|
584
|
+
if (!this.#noValidate) {
|
|
585
|
+
RoutesNamespace.validateStateBuilderArgs(
|
|
586
|
+
routeName,
|
|
587
|
+
routeParams,
|
|
588
|
+
"forwardState",
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return this.#routes.forwardState<P>(routeName, routeParams);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
buildState(
|
|
596
|
+
routeName: string,
|
|
597
|
+
routeParams: Params,
|
|
598
|
+
): RouteTreeState | undefined {
|
|
599
|
+
if (!this.#noValidate) {
|
|
600
|
+
RoutesNamespace.validateStateBuilderArgs(
|
|
601
|
+
routeName,
|
|
602
|
+
routeParams,
|
|
603
|
+
"buildState",
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Call forwardState at facade level to allow plugin interception
|
|
608
|
+
const { name, params } = this.forwardState(routeName, routeParams);
|
|
609
|
+
|
|
610
|
+
return this.#routes.buildStateResolved(name, params);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
buildNavigationState(name: string, params: Params = {}): State | undefined {
|
|
614
|
+
if (!this.#noValidate) {
|
|
615
|
+
RoutesNamespace.validateStateBuilderArgs(
|
|
616
|
+
name,
|
|
617
|
+
params,
|
|
618
|
+
"buildNavigationState",
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const routeInfo = this.buildState(name, params);
|
|
623
|
+
|
|
624
|
+
if (!routeInfo) {
|
|
625
|
+
return undefined;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return this.makeState(
|
|
629
|
+
routeInfo.name,
|
|
630
|
+
routeInfo.params,
|
|
631
|
+
this.buildPath(routeInfo.name, routeInfo.params),
|
|
632
|
+
{
|
|
633
|
+
params: routeInfo.meta,
|
|
634
|
+
options: {},
|
|
635
|
+
},
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
shouldUpdateNode(
|
|
640
|
+
nodeName: string,
|
|
641
|
+
): (toState: State, fromState?: State) => boolean {
|
|
642
|
+
if (!this.#noValidate) {
|
|
643
|
+
RoutesNamespace.validateShouldUpdateNodeArgs(nodeName);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return this.#routes.shouldUpdateNode(nodeName);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// ============================================================================
|
|
650
|
+
// Options (backed by OptionsNamespace)
|
|
651
|
+
// ============================================================================
|
|
652
|
+
|
|
653
|
+
getOptions(): Options {
|
|
654
|
+
return this.#options.get();
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// ============================================================================
|
|
658
|
+
// Router Lifecycle
|
|
659
|
+
// ============================================================================
|
|
660
|
+
|
|
661
|
+
isActive(): boolean {
|
|
662
|
+
return this.#eventBus.isActive();
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async start(startPath: string): Promise<State> {
|
|
666
|
+
// Static validation
|
|
667
|
+
if (!this.#noValidate) {
|
|
668
|
+
RouterLifecycleNamespace.validateStartArgs([startPath]);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (!this.#eventBus.canStart()) {
|
|
672
|
+
throw CACHED_ALREADY_STARTED_ERROR;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
this.#eventBus.sendStart();
|
|
676
|
+
|
|
677
|
+
try {
|
|
678
|
+
return await this.#lifecycle.start(startPath);
|
|
679
|
+
} catch (error) {
|
|
680
|
+
if (this.#eventBus.isReady()) {
|
|
681
|
+
this.#lifecycle.stop();
|
|
682
|
+
this.#eventBus.sendStop();
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
throw error;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
stop(): this {
|
|
690
|
+
this.#eventBus.cancelTransitionIfRunning(this.#state.get());
|
|
691
|
+
|
|
692
|
+
if (!this.#eventBus.isReady() && !this.#eventBus.isTransitioning()) {
|
|
693
|
+
return this;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
this.#lifecycle.stop();
|
|
697
|
+
this.#eventBus.sendStop();
|
|
698
|
+
|
|
699
|
+
return this;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
dispose(): void {
|
|
703
|
+
if (this.#eventBus.isDisposed()) {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
this.#eventBus.cancelTransitionIfRunning(this.#state.get());
|
|
708
|
+
|
|
709
|
+
if (this.#eventBus.isReady() || this.#eventBus.isTransitioning()) {
|
|
710
|
+
this.#lifecycle.stop();
|
|
711
|
+
this.#eventBus.sendStop();
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
this.#eventBus.sendDispose();
|
|
715
|
+
this.#eventBus.clearAll();
|
|
716
|
+
|
|
717
|
+
this.#plugins.disposeAll();
|
|
718
|
+
this.#middleware.clearAll();
|
|
719
|
+
this.#routes.clearRoutes();
|
|
720
|
+
this.#routeLifecycle.clearAll();
|
|
721
|
+
this.#state.reset();
|
|
722
|
+
this.#dependencies.reset();
|
|
723
|
+
|
|
724
|
+
this.#markDisposed();
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// ============================================================================
|
|
728
|
+
// Route Lifecycle (Guards)
|
|
729
|
+
// ============================================================================
|
|
730
|
+
|
|
731
|
+
addDeactivateGuard(
|
|
732
|
+
name: string,
|
|
733
|
+
canDeactivateHandler: ActivationFnFactory<Dependencies> | boolean,
|
|
734
|
+
): this {
|
|
735
|
+
if (!this.#noValidate) {
|
|
736
|
+
validateRouteName(name, "addDeactivateGuard");
|
|
737
|
+
RouteLifecycleNamespace.validateHandler(
|
|
738
|
+
canDeactivateHandler,
|
|
739
|
+
"addDeactivateGuard",
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
this.#routeLifecycle.addCanDeactivate(
|
|
744
|
+
name,
|
|
745
|
+
canDeactivateHandler,
|
|
746
|
+
this.#noValidate,
|
|
747
|
+
);
|
|
748
|
+
|
|
749
|
+
return this;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
addActivateGuard(
|
|
753
|
+
name: string,
|
|
754
|
+
canActivateHandler: ActivationFnFactory<Dependencies> | boolean,
|
|
755
|
+
): this {
|
|
756
|
+
if (!this.#noValidate) {
|
|
757
|
+
validateRouteName(name, "addActivateGuard");
|
|
758
|
+
RouteLifecycleNamespace.validateHandler(
|
|
759
|
+
canActivateHandler,
|
|
760
|
+
"addActivateGuard",
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
this.#routeLifecycle.addCanActivate(
|
|
765
|
+
name,
|
|
766
|
+
canActivateHandler,
|
|
767
|
+
this.#noValidate,
|
|
768
|
+
);
|
|
769
|
+
|
|
770
|
+
return this;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
removeActivateGuard(name: string): void {
|
|
774
|
+
if (!this.#noValidate) {
|
|
775
|
+
validateRouteName(name, "removeActivateGuard");
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
this.#routeLifecycle.clearCanActivate(name);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
removeDeactivateGuard(name: string): void {
|
|
782
|
+
if (!this.#noValidate) {
|
|
783
|
+
validateRouteName(name, "removeDeactivateGuard");
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
this.#routeLifecycle.clearCanDeactivate(name);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
canNavigateTo(name: string, params?: Params): boolean {
|
|
790
|
+
if (!this.#noValidate) {
|
|
791
|
+
validateRouteName(name, "canNavigateTo");
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (!this.hasRoute(name)) {
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const { name: resolvedName, params: resolvedParams } = this.forwardState(
|
|
799
|
+
name,
|
|
800
|
+
params ?? {},
|
|
801
|
+
);
|
|
802
|
+
const toState = this.makeState(resolvedName, resolvedParams);
|
|
803
|
+
const fromState = this.getState();
|
|
804
|
+
|
|
805
|
+
const { toDeactivate, toActivate } = getTransitionPath(toState, fromState);
|
|
806
|
+
|
|
807
|
+
for (const segment of toDeactivate) {
|
|
808
|
+
if (
|
|
809
|
+
!this.#routeLifecycle.checkDeactivateGuardSync(
|
|
810
|
+
segment,
|
|
811
|
+
toState,
|
|
812
|
+
fromState,
|
|
813
|
+
)
|
|
814
|
+
) {
|
|
815
|
+
return false;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
for (const segment of toActivate) {
|
|
820
|
+
if (
|
|
821
|
+
!this.#routeLifecycle.checkActivateGuardSync(
|
|
822
|
+
segment,
|
|
823
|
+
toState,
|
|
824
|
+
fromState,
|
|
825
|
+
)
|
|
826
|
+
) {
|
|
827
|
+
return false;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
return true;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// ============================================================================
|
|
835
|
+
// Plugins
|
|
836
|
+
// ============================================================================
|
|
837
|
+
|
|
838
|
+
usePlugin(...plugins: PluginFactory<Dependencies>[]): Unsubscribe {
|
|
839
|
+
if (!this.#noValidate) {
|
|
840
|
+
// 1. Validate input arguments
|
|
841
|
+
PluginsNamespace.validateUsePluginArgs<Dependencies>(plugins);
|
|
842
|
+
|
|
843
|
+
// 2. Validate limit
|
|
844
|
+
PluginsNamespace.validatePluginLimit(
|
|
845
|
+
this.#plugins.count(),
|
|
846
|
+
plugins.length,
|
|
847
|
+
this.#limits.maxPlugins,
|
|
848
|
+
);
|
|
849
|
+
|
|
850
|
+
// 3. Validate no duplicates with existing plugins
|
|
851
|
+
PluginsNamespace.validateNoDuplicatePlugins(
|
|
852
|
+
plugins,
|
|
853
|
+
this.#plugins.has.bind(this.#plugins),
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// 4. Execute (warnings, deduplication, initialization, commit)
|
|
858
|
+
return this.#plugins.use(...plugins);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// ============================================================================
|
|
862
|
+
// Middleware
|
|
863
|
+
// ============================================================================
|
|
864
|
+
|
|
865
|
+
useMiddleware(
|
|
866
|
+
...middlewares: MiddlewareFactory<Dependencies>[]
|
|
867
|
+
): Unsubscribe {
|
|
868
|
+
if (!this.#noValidate) {
|
|
869
|
+
// 1. Validate input arguments
|
|
870
|
+
MiddlewareNamespace.validateUseMiddlewareArgs<Dependencies>(middlewares);
|
|
871
|
+
|
|
872
|
+
// 2. Validate no duplicates
|
|
873
|
+
MiddlewareNamespace.validateNoDuplicates<Dependencies>(
|
|
874
|
+
middlewares,
|
|
875
|
+
this.#middleware.getFactories(),
|
|
876
|
+
);
|
|
877
|
+
|
|
878
|
+
// 3. Validate limit
|
|
879
|
+
MiddlewareNamespace.validateMiddlewareLimit(
|
|
880
|
+
this.#middleware.count(),
|
|
881
|
+
middlewares.length,
|
|
882
|
+
this.#limits.maxMiddleware,
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// 4. Initialize (without committing)
|
|
887
|
+
const initialized = this.#middleware.initialize(...middlewares);
|
|
888
|
+
|
|
889
|
+
// 5. Validate results
|
|
890
|
+
if (!this.#noValidate) {
|
|
891
|
+
for (const { middleware, factory } of initialized) {
|
|
892
|
+
MiddlewareNamespace.validateMiddleware<Dependencies>(
|
|
893
|
+
middleware,
|
|
894
|
+
factory,
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// 6. Commit
|
|
900
|
+
return this.#middleware.commit(initialized);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// ============================================================================
|
|
904
|
+
// Dependencies (backed by DependenciesNamespace)
|
|
905
|
+
// ============================================================================
|
|
906
|
+
|
|
907
|
+
setDependency<K extends keyof Dependencies & string>(
|
|
908
|
+
dependencyName: K,
|
|
909
|
+
dependency: Dependencies[K],
|
|
910
|
+
): this {
|
|
911
|
+
if (!this.#noValidate) {
|
|
912
|
+
DependenciesNamespace.validateSetDependencyArgs(dependencyName);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
this.#dependencies.set(dependencyName, dependency);
|
|
916
|
+
|
|
917
|
+
return this;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
setDependencies(deps: Dependencies): this {
|
|
921
|
+
if (!this.#noValidate) {
|
|
922
|
+
DependenciesNamespace.validateDependenciesObject(deps, "setDependencies");
|
|
923
|
+
DependenciesNamespace.validateDependencyLimit(
|
|
924
|
+
this.#dependencies.count(),
|
|
925
|
+
Object.keys(deps).length,
|
|
926
|
+
"setDependencies",
|
|
927
|
+
this.#limits.maxDependencies,
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
this.#dependencies.setMultiple(deps);
|
|
932
|
+
|
|
933
|
+
return this;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
getDependency<K extends keyof Dependencies>(key: K): Dependencies[K] {
|
|
937
|
+
if (!this.#noValidate) {
|
|
938
|
+
DependenciesNamespace.validateName(key, "getDependency");
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const value = this.#dependencies.get(key);
|
|
942
|
+
|
|
943
|
+
if (!this.#noValidate) {
|
|
944
|
+
DependenciesNamespace.validateDependencyExists(value, key as string);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
return value;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
getDependencies(): Partial<Dependencies> {
|
|
951
|
+
return this.#dependencies.getAll();
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
removeDependency(dependencyName: keyof Dependencies): this {
|
|
955
|
+
if (!this.#noValidate) {
|
|
956
|
+
DependenciesNamespace.validateName(dependencyName, "removeDependency");
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
this.#dependencies.remove(dependencyName);
|
|
960
|
+
|
|
961
|
+
return this;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
hasDependency(dependencyName: keyof Dependencies): boolean {
|
|
965
|
+
if (!this.#noValidate) {
|
|
966
|
+
DependenciesNamespace.validateName(dependencyName, "hasDependency");
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
return this.#dependencies.has(dependencyName);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
resetDependencies(): this {
|
|
973
|
+
this.#dependencies.reset();
|
|
974
|
+
|
|
975
|
+
return this;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// ============================================================================
|
|
979
|
+
// Events (backed by EventEmitter)
|
|
980
|
+
// ============================================================================
|
|
981
|
+
|
|
982
|
+
addEventListener<E extends EventName>(
|
|
983
|
+
eventName: E,
|
|
984
|
+
cb: Plugin[EventMethodMap[E]],
|
|
985
|
+
): Unsubscribe {
|
|
986
|
+
if (!this.#noValidate) {
|
|
987
|
+
EventBusNamespace.validateListenerArgs(eventName, cb);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
return this.#eventBus.addEventListener(eventName, cb);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// ============================================================================
|
|
994
|
+
// Subscription (backed by EventEmitter)
|
|
995
|
+
// ============================================================================
|
|
996
|
+
|
|
997
|
+
subscribe(listener: SubscribeFn): Unsubscribe {
|
|
998
|
+
if (!this.#noValidate) {
|
|
999
|
+
EventBusNamespace.validateSubscribeListener(listener);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
return this.#eventBus.subscribe(listener);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// ============================================================================
|
|
1006
|
+
// Navigation
|
|
1007
|
+
// ============================================================================
|
|
1008
|
+
|
|
1009
|
+
navigate(routeName: string): Promise<State>;
|
|
1010
|
+
navigate(routeName: string, routeParams: Params): Promise<State>;
|
|
1011
|
+
navigate(
|
|
1012
|
+
routeName: string,
|
|
1013
|
+
routeParams: Params,
|
|
1014
|
+
options: NavigationOptions,
|
|
1015
|
+
): Promise<State>;
|
|
1016
|
+
navigate(
|
|
1017
|
+
routeName: string,
|
|
1018
|
+
routeParams?: Params,
|
|
1019
|
+
options?: NavigationOptions,
|
|
1020
|
+
): Promise<State> {
|
|
1021
|
+
// 1. Validate route name
|
|
1022
|
+
if (!this.#noValidate) {
|
|
1023
|
+
NavigationNamespace.validateNavigateArgs(routeName);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// 2. Validate parsed options
|
|
1027
|
+
const opts = options ?? {};
|
|
1028
|
+
|
|
1029
|
+
if (!this.#noValidate) {
|
|
1030
|
+
NavigationNamespace.validateNavigationOptions(opts, "navigate");
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// 3. Execute navigation with parsed arguments
|
|
1034
|
+
const promiseState = this.#navigation.navigate(
|
|
1035
|
+
routeName,
|
|
1036
|
+
routeParams ?? {},
|
|
1037
|
+
opts,
|
|
1038
|
+
);
|
|
1039
|
+
|
|
1040
|
+
Router.#suppressUnhandledRejection(promiseState);
|
|
1041
|
+
|
|
1042
|
+
return promiseState;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
navigateToDefault(): Promise<State>;
|
|
1046
|
+
navigateToDefault(options: NavigationOptions): Promise<State>;
|
|
1047
|
+
navigateToDefault(options?: NavigationOptions): Promise<State> {
|
|
1048
|
+
// 1. Validate arguments
|
|
1049
|
+
if (!this.#noValidate) {
|
|
1050
|
+
NavigationNamespace.validateNavigateToDefaultArgs(options);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// 2. Validate parsed options
|
|
1054
|
+
const opts = options ?? {};
|
|
1055
|
+
|
|
1056
|
+
if (!this.#noValidate) {
|
|
1057
|
+
NavigationNamespace.validateNavigationOptions(opts, "navigateToDefault");
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// 3. Execute navigation with parsed arguments
|
|
1061
|
+
const promiseState = this.#navigation.navigateToDefault(opts);
|
|
1062
|
+
|
|
1063
|
+
Router.#suppressUnhandledRejection(promiseState);
|
|
1064
|
+
|
|
1065
|
+
return promiseState;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
navigateToState(
|
|
1069
|
+
toState: State,
|
|
1070
|
+
fromState: State | undefined,
|
|
1071
|
+
opts: NavigationOptions,
|
|
1072
|
+
): Promise<State> {
|
|
1073
|
+
if (!this.#noValidate) {
|
|
1074
|
+
NavigationNamespace.validateNavigateToStateArgs(toState, fromState, opts);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
return this.#navigation.navigateToState(toState, fromState, opts);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// ============================================================================
|
|
1081
|
+
// Cloning
|
|
1082
|
+
// ============================================================================
|
|
1083
|
+
|
|
1084
|
+
clone(dependencies?: Dependencies): Router<Dependencies> {
|
|
1085
|
+
if (!this.#noValidate) {
|
|
1086
|
+
CloneNamespace.validateCloneArgs(dependencies);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
return this.#clone.clone(
|
|
1090
|
+
dependencies,
|
|
1091
|
+
(routes, options, deps) =>
|
|
1092
|
+
new Router<Dependencies>(routes, options, deps),
|
|
1093
|
+
(newRouter, config, resolvedForwardMap) => {
|
|
1094
|
+
const typedRouter = newRouter as unknown as Router<Dependencies>;
|
|
1095
|
+
|
|
1096
|
+
typedRouter.#routes.applyClonedConfig(config, resolvedForwardMap);
|
|
1097
|
+
},
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* Pre-allocated callback for #suppressUnhandledRejection.
|
|
1103
|
+
* Avoids creating a new closure on every navigate() call.
|
|
1104
|
+
*/
|
|
1105
|
+
static readonly #onSuppressedError = (error: unknown): void => {
|
|
1106
|
+
if (
|
|
1107
|
+
error instanceof RouterError &&
|
|
1108
|
+
(error.code === errorCodes.SAME_STATES ||
|
|
1109
|
+
error.code === errorCodes.TRANSITION_CANCELLED ||
|
|
1110
|
+
error.code === errorCodes.ROUTER_NOT_STARTED ||
|
|
1111
|
+
error.code === errorCodes.ROUTE_NOT_FOUND)
|
|
1112
|
+
) {
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
logger.error("router.navigate", "Unexpected navigation error", error);
|
|
1117
|
+
};
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Fire-and-forget safety: prevents unhandled rejection warnings
|
|
1121
|
+
* when navigate/navigateToDefault is called without await.
|
|
1122
|
+
* Expected errors are silently suppressed; unexpected ones are logged.
|
|
1123
|
+
*/
|
|
1124
|
+
static #suppressUnhandledRejection(promise: Promise<State>): void {
|
|
1125
|
+
promise.catch(Router.#onSuppressedError);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
#markDisposed(): void {
|
|
1129
|
+
this.navigate = throwDisposed as never;
|
|
1130
|
+
this.navigateToDefault = throwDisposed as never;
|
|
1131
|
+
this.navigateToState = throwDisposed as never;
|
|
1132
|
+
this.start = throwDisposed as never;
|
|
1133
|
+
this.stop = throwDisposed as never;
|
|
1134
|
+
this.addRoute = throwDisposed as never;
|
|
1135
|
+
this.removeRoute = throwDisposed as never;
|
|
1136
|
+
this.clearRoutes = throwDisposed as never;
|
|
1137
|
+
this.updateRoute = throwDisposed as never;
|
|
1138
|
+
this.addActivateGuard = throwDisposed as never;
|
|
1139
|
+
this.addDeactivateGuard = throwDisposed as never;
|
|
1140
|
+
this.removeActivateGuard = throwDisposed as never;
|
|
1141
|
+
this.removeDeactivateGuard = throwDisposed as never;
|
|
1142
|
+
this.usePlugin = throwDisposed as never;
|
|
1143
|
+
this.useMiddleware = throwDisposed as never;
|
|
1144
|
+
this.setDependency = throwDisposed as never;
|
|
1145
|
+
this.setDependencies = throwDisposed as never;
|
|
1146
|
+
this.removeDependency = throwDisposed as never;
|
|
1147
|
+
this.resetDependencies = throwDisposed as never;
|
|
1148
|
+
this.addEventListener = throwDisposed as never;
|
|
1149
|
+
this.subscribe = throwDisposed as never;
|
|
1150
|
+
this.setRootPath = throwDisposed as never;
|
|
1151
|
+
this.clone = throwDisposed as never;
|
|
1152
|
+
this.canNavigateTo = throwDisposed as never;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function throwDisposed(): never {
|
|
1157
|
+
throw new RouterError(errorCodes.ROUTER_DISPOSED);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* Derives CreateMatcherOptions from router Options.
|
|
1162
|
+
* Maps core option names to matcher option names.
|
|
1163
|
+
*/
|
|
1164
|
+
function deriveMatcherOptions(
|
|
1165
|
+
options: Readonly<Options>,
|
|
1166
|
+
): CreateMatcherOptions {
|
|
1167
|
+
return {
|
|
1168
|
+
strictTrailingSlash: options.trailingSlash === "strict",
|
|
1169
|
+
strictQueryParams: options.queryParamsMode === "strict",
|
|
1170
|
+
urlParamsEncoding: options.urlParamsEncoding,
|
|
1171
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1172
|
+
queryParams: options.queryParams!,
|
|
1173
|
+
};
|
|
1174
|
+
}
|