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