@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
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// packages/core/src/namespaces/PluginsNamespace/validators.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Static validation functions for PluginsNamespace.
|
|
5
|
+
* Called by Router facade before instance methods.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getTypeDescription, isObjKey } from "type-guards";
|
|
9
|
+
|
|
10
|
+
import { EVENTS_MAP } from "./constants";
|
|
11
|
+
import { DEFAULT_LIMITS } from "../../constants";
|
|
12
|
+
|
|
13
|
+
import type { PluginFactory } from "../../types";
|
|
14
|
+
import type { DefaultDependencies, Plugin } from "@real-router/types";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Validates usePlugin arguments - all must be functions.
|
|
18
|
+
*/
|
|
19
|
+
export function validateUsePluginArgs<D extends DefaultDependencies>(
|
|
20
|
+
plugins: unknown[],
|
|
21
|
+
): asserts plugins is PluginFactory<D>[] {
|
|
22
|
+
for (const plugin of plugins) {
|
|
23
|
+
if (typeof plugin !== "function") {
|
|
24
|
+
throw new TypeError(
|
|
25
|
+
`[router.usePlugin] Expected plugin factory function, got ${typeof plugin}`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Validates that a plugin factory returned a valid plugin object.
|
|
33
|
+
*/
|
|
34
|
+
export function validatePlugin(plugin: Plugin): void {
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
36
|
+
if (!(plugin && typeof plugin === "object") || Array.isArray(plugin)) {
|
|
37
|
+
throw new TypeError(
|
|
38
|
+
`[router.usePlugin] Plugin factory must return an object, got ${getTypeDescription(
|
|
39
|
+
plugin,
|
|
40
|
+
)}`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Detect async factory (returns Promise)
|
|
45
|
+
if (typeof (plugin as unknown as { then?: unknown }).then === "function") {
|
|
46
|
+
throw new TypeError(
|
|
47
|
+
`[router.usePlugin] Async plugin factories are not supported. ` +
|
|
48
|
+
`Factory returned a Promise instead of a plugin object.`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const key in plugin) {
|
|
53
|
+
if (!(key === "teardown" || isObjKey<typeof EVENTS_MAP>(key, EVENTS_MAP))) {
|
|
54
|
+
throw new TypeError(
|
|
55
|
+
`[router.usePlugin] Unknown property '${key}'. ` +
|
|
56
|
+
`Plugin must only contain event handlers and optional teardown.`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Validates that adding new plugins won't exceed the hard limit.
|
|
64
|
+
*/
|
|
65
|
+
export function validatePluginLimit(
|
|
66
|
+
currentCount: number,
|
|
67
|
+
newCount: number,
|
|
68
|
+
maxPlugins: number = DEFAULT_LIMITS.maxPlugins,
|
|
69
|
+
): void {
|
|
70
|
+
if (maxPlugins === 0) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const totalCount = currentCount + newCount;
|
|
75
|
+
|
|
76
|
+
if (totalCount > maxPlugins) {
|
|
77
|
+
throw new Error(`[router.usePlugin] Plugin limit exceeded (${maxPlugins})`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
// packages/core/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts
|
|
2
|
+
|
|
3
|
+
import { logger } from "@real-router/logger";
|
|
4
|
+
import { isBoolean, isPromise, getTypeDescription } from "type-guards";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
validateHandler,
|
|
8
|
+
validateHandlerLimit,
|
|
9
|
+
validateNotRegistering,
|
|
10
|
+
} from "./validators";
|
|
11
|
+
import { DEFAULT_LIMITS } from "../../constants";
|
|
12
|
+
import { computeThresholds } from "../../helpers";
|
|
13
|
+
|
|
14
|
+
import type { RouteLifecycleDependencies } from "./types";
|
|
15
|
+
import type { Router } from "../../Router";
|
|
16
|
+
import type { ActivationFnFactory, Limits } from "../../types";
|
|
17
|
+
import type {
|
|
18
|
+
ActivationFn,
|
|
19
|
+
DefaultDependencies,
|
|
20
|
+
State,
|
|
21
|
+
} from "@real-router/types";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Converts a boolean value to an activation function factory.
|
|
25
|
+
* Used for the shorthand syntax where true/false is passed instead of a function.
|
|
26
|
+
*/
|
|
27
|
+
function booleanToFactory<Dependencies extends DefaultDependencies>(
|
|
28
|
+
value: boolean,
|
|
29
|
+
): ActivationFnFactory<Dependencies> {
|
|
30
|
+
const activationFn: ActivationFn = () => value;
|
|
31
|
+
|
|
32
|
+
return () => activationFn;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Independent namespace for managing route lifecycle handlers.
|
|
37
|
+
*
|
|
38
|
+
* Static methods handle input validation (called by facade).
|
|
39
|
+
* Instance methods handle state-dependent validation, storage and business logic.
|
|
40
|
+
*/
|
|
41
|
+
export class RouteLifecycleNamespace<
|
|
42
|
+
Dependencies extends DefaultDependencies = DefaultDependencies,
|
|
43
|
+
> {
|
|
44
|
+
readonly #canDeactivateFactories = new Map<
|
|
45
|
+
string,
|
|
46
|
+
ActivationFnFactory<Dependencies>
|
|
47
|
+
>();
|
|
48
|
+
readonly #canActivateFactories = new Map<
|
|
49
|
+
string,
|
|
50
|
+
ActivationFnFactory<Dependencies>
|
|
51
|
+
>();
|
|
52
|
+
readonly #canDeactivateFunctions = new Map<string, ActivationFn>();
|
|
53
|
+
readonly #canActivateFunctions = new Map<string, ActivationFn>();
|
|
54
|
+
|
|
55
|
+
readonly #registering = new Set<string>();
|
|
56
|
+
|
|
57
|
+
#router!: Router<Dependencies>;
|
|
58
|
+
#deps!: RouteLifecycleDependencies<Dependencies>;
|
|
59
|
+
#limits: Limits = DEFAULT_LIMITS;
|
|
60
|
+
|
|
61
|
+
// =========================================================================
|
|
62
|
+
// Static validation methods (called by facade for input validation)
|
|
63
|
+
// =========================================================================
|
|
64
|
+
|
|
65
|
+
static validateHandler<D extends DefaultDependencies>(
|
|
66
|
+
handler: unknown,
|
|
67
|
+
methodName: string,
|
|
68
|
+
): asserts handler is ActivationFnFactory<D> | boolean {
|
|
69
|
+
validateHandler<D>(handler, methodName);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
setRouter(router: Router<Dependencies>): void {
|
|
73
|
+
this.#router = router;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
setDependencies(deps: RouteLifecycleDependencies<Dependencies>): void {
|
|
77
|
+
this.#deps = deps;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
setLimits(limits: Limits): void {
|
|
81
|
+
this.#limits = limits;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// =========================================================================
|
|
85
|
+
// Instance methods
|
|
86
|
+
// =========================================================================
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Adds a canActivate guard for a route.
|
|
90
|
+
* Handles state-dependent validation, overwrite detection, and registration.
|
|
91
|
+
*
|
|
92
|
+
* @param name - Route name (input-validated by facade)
|
|
93
|
+
* @param handler - Guard function or boolean (input-validated by facade)
|
|
94
|
+
* @param skipValidation - True when called during route config building (#noValidate)
|
|
95
|
+
*/
|
|
96
|
+
addCanActivate(
|
|
97
|
+
name: string,
|
|
98
|
+
handler: ActivationFnFactory<Dependencies> | boolean,
|
|
99
|
+
skipValidation: boolean,
|
|
100
|
+
): void {
|
|
101
|
+
if (!skipValidation) {
|
|
102
|
+
validateNotRegistering(
|
|
103
|
+
this.#registering.has(name),
|
|
104
|
+
name,
|
|
105
|
+
"addActivateGuard",
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const isOverwrite = this.#canActivateFactories.has(name);
|
|
110
|
+
|
|
111
|
+
if (!isOverwrite && !skipValidation) {
|
|
112
|
+
validateHandlerLimit(
|
|
113
|
+
this.#canActivateFactories.size + 1,
|
|
114
|
+
"addActivateGuard",
|
|
115
|
+
this.#limits.maxLifecycleHandlers,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.#registerHandler(
|
|
120
|
+
"activate",
|
|
121
|
+
name,
|
|
122
|
+
handler,
|
|
123
|
+
this.#canActivateFactories,
|
|
124
|
+
this.#canActivateFunctions,
|
|
125
|
+
"canActivate",
|
|
126
|
+
isOverwrite,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Adds a canDeactivate guard for a route.
|
|
132
|
+
* Handles state-dependent validation, overwrite detection, and registration.
|
|
133
|
+
*
|
|
134
|
+
* @param name - Route name (input-validated by facade)
|
|
135
|
+
* @param handler - Guard function or boolean (input-validated by facade)
|
|
136
|
+
* @param skipValidation - True when called during route config building (#noValidate)
|
|
137
|
+
*/
|
|
138
|
+
addCanDeactivate(
|
|
139
|
+
name: string,
|
|
140
|
+
handler: ActivationFnFactory<Dependencies> | boolean,
|
|
141
|
+
skipValidation: boolean,
|
|
142
|
+
): void {
|
|
143
|
+
if (!skipValidation) {
|
|
144
|
+
validateNotRegistering(
|
|
145
|
+
this.#registering.has(name),
|
|
146
|
+
name,
|
|
147
|
+
"addDeactivateGuard",
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const isOverwrite = this.#canDeactivateFactories.has(name);
|
|
152
|
+
|
|
153
|
+
if (!isOverwrite && !skipValidation) {
|
|
154
|
+
validateHandlerLimit(
|
|
155
|
+
this.#canDeactivateFactories.size + 1,
|
|
156
|
+
"addDeactivateGuard",
|
|
157
|
+
this.#limits.maxLifecycleHandlers,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.#registerHandler(
|
|
162
|
+
"deactivate",
|
|
163
|
+
name,
|
|
164
|
+
handler,
|
|
165
|
+
this.#canDeactivateFactories,
|
|
166
|
+
this.#canDeactivateFunctions,
|
|
167
|
+
"canDeactivate",
|
|
168
|
+
isOverwrite,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Removes a canActivate guard for a route.
|
|
174
|
+
* Input already validated by facade (not registering).
|
|
175
|
+
*
|
|
176
|
+
* @param name - Route name (already validated by facade)
|
|
177
|
+
*/
|
|
178
|
+
clearCanActivate(name: string): void {
|
|
179
|
+
this.#canActivateFactories.delete(name);
|
|
180
|
+
this.#canActivateFunctions.delete(name);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Removes a canDeactivate guard for a route.
|
|
185
|
+
* Input already validated by facade (not registering).
|
|
186
|
+
*
|
|
187
|
+
* @param name - Route name (already validated by facade)
|
|
188
|
+
*/
|
|
189
|
+
clearCanDeactivate(name: string): void {
|
|
190
|
+
this.#canDeactivateFactories.delete(name);
|
|
191
|
+
this.#canDeactivateFunctions.delete(name);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Clears all lifecycle handlers (canActivate and canDeactivate).
|
|
196
|
+
* Used by clearRoutes to reset all lifecycle state.
|
|
197
|
+
*/
|
|
198
|
+
clearAll(): void {
|
|
199
|
+
this.#canActivateFactories.clear();
|
|
200
|
+
this.#canActivateFunctions.clear();
|
|
201
|
+
this.#canDeactivateFactories.clear();
|
|
202
|
+
this.#canDeactivateFunctions.clear();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Returns lifecycle factories as records for cloning.
|
|
207
|
+
*
|
|
208
|
+
* @returns Tuple of [canDeactivateFactories, canActivateFactories]
|
|
209
|
+
*/
|
|
210
|
+
getFactories(): [
|
|
211
|
+
Record<string, ActivationFnFactory<Dependencies>>,
|
|
212
|
+
Record<string, ActivationFnFactory<Dependencies>>,
|
|
213
|
+
] {
|
|
214
|
+
const deactivateRecord: Record<
|
|
215
|
+
string,
|
|
216
|
+
ActivationFnFactory<Dependencies>
|
|
217
|
+
> = {};
|
|
218
|
+
const activateRecord: Record<
|
|
219
|
+
string,
|
|
220
|
+
ActivationFnFactory<Dependencies>
|
|
221
|
+
> = {};
|
|
222
|
+
|
|
223
|
+
for (const [name, factory] of this.#canDeactivateFactories) {
|
|
224
|
+
deactivateRecord[name] = factory;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for (const [name, factory] of this.#canActivateFactories) {
|
|
228
|
+
activateRecord[name] = factory;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return [deactivateRecord, activateRecord];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Returns compiled lifecycle functions for transition execution.
|
|
236
|
+
*
|
|
237
|
+
* @returns Tuple of [canDeactivateFunctions, canActivateFunctions] as Maps
|
|
238
|
+
*/
|
|
239
|
+
getFunctions(): [Map<string, ActivationFn>, Map<string, ActivationFn>] {
|
|
240
|
+
return [this.#canDeactivateFunctions, this.#canActivateFunctions];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
checkActivateGuardSync(
|
|
244
|
+
name: string,
|
|
245
|
+
toState: State,
|
|
246
|
+
fromState: State | undefined,
|
|
247
|
+
): boolean {
|
|
248
|
+
return this.#checkGuardSync(
|
|
249
|
+
this.#canActivateFunctions,
|
|
250
|
+
name,
|
|
251
|
+
toState,
|
|
252
|
+
fromState,
|
|
253
|
+
"checkActivateGuardSync",
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
checkDeactivateGuardSync(
|
|
258
|
+
name: string,
|
|
259
|
+
toState: State,
|
|
260
|
+
fromState: State | undefined,
|
|
261
|
+
): boolean {
|
|
262
|
+
return this.#checkGuardSync(
|
|
263
|
+
this.#canDeactivateFunctions,
|
|
264
|
+
name,
|
|
265
|
+
toState,
|
|
266
|
+
fromState,
|
|
267
|
+
"checkDeactivateGuardSync",
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// =========================================================================
|
|
272
|
+
// Private methods (business logic)
|
|
273
|
+
// =========================================================================
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Registers a handler.
|
|
277
|
+
* Handles overwrite warning, count threshold warnings, and factory compilation.
|
|
278
|
+
*/
|
|
279
|
+
#registerHandler(
|
|
280
|
+
type: "activate" | "deactivate",
|
|
281
|
+
name: string,
|
|
282
|
+
handler: ActivationFnFactory<Dependencies> | boolean,
|
|
283
|
+
factories: Map<string, ActivationFnFactory<Dependencies>>,
|
|
284
|
+
functions: Map<string, ActivationFn>,
|
|
285
|
+
methodName: string,
|
|
286
|
+
isOverwrite: boolean,
|
|
287
|
+
): void {
|
|
288
|
+
// Emit warnings
|
|
289
|
+
if (isOverwrite) {
|
|
290
|
+
logger.warn(
|
|
291
|
+
`router.${methodName}`,
|
|
292
|
+
`Overwriting existing ${type} handler for route "${name}"`,
|
|
293
|
+
);
|
|
294
|
+
} else {
|
|
295
|
+
this.#checkCountThresholds(factories.size + 1, methodName);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Convert boolean to factory if needed
|
|
299
|
+
const factory = isBoolean(handler)
|
|
300
|
+
? booleanToFactory<Dependencies>(handler)
|
|
301
|
+
: handler;
|
|
302
|
+
|
|
303
|
+
// Store factory
|
|
304
|
+
factories.set(name, factory);
|
|
305
|
+
|
|
306
|
+
// Mark route as being registered before calling user factory
|
|
307
|
+
this.#registering.add(name);
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
// Lifecycle factories receive full router as part of their public API
|
|
311
|
+
const fn = factory(this.#router, this.#deps.getDependency);
|
|
312
|
+
|
|
313
|
+
if (typeof fn !== "function") {
|
|
314
|
+
throw new TypeError(
|
|
315
|
+
`[router.${methodName}] Factory must return a function, got ${getTypeDescription(fn)}`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
functions.set(name, fn);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
// Rollback on failure to maintain consistency
|
|
322
|
+
factories.delete(name);
|
|
323
|
+
|
|
324
|
+
throw error;
|
|
325
|
+
} finally {
|
|
326
|
+
this.#registering.delete(name);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
#checkGuardSync(
|
|
331
|
+
functions: Map<string, ActivationFn>,
|
|
332
|
+
name: string,
|
|
333
|
+
toState: State,
|
|
334
|
+
fromState: State | undefined,
|
|
335
|
+
methodName: string,
|
|
336
|
+
): boolean {
|
|
337
|
+
const guardFn = functions.get(name);
|
|
338
|
+
|
|
339
|
+
if (!guardFn) {
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
const result = guardFn(toState, fromState);
|
|
345
|
+
|
|
346
|
+
if (typeof result === "boolean") {
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (isPromise(result)) {
|
|
351
|
+
logger.warn(
|
|
352
|
+
`router.${methodName}`,
|
|
353
|
+
`Guard for "${name}" returned a Promise. Sync check cannot resolve async guards — returning false.`,
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Guard returned void/State — permissive default
|
|
360
|
+
return true;
|
|
361
|
+
} catch {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
#checkCountThresholds(currentSize: number, methodName: string): void {
|
|
367
|
+
const maxLifecycleHandlers = this.#limits.maxLifecycleHandlers;
|
|
368
|
+
|
|
369
|
+
if (maxLifecycleHandlers === 0) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const { warn, error } = computeThresholds(maxLifecycleHandlers);
|
|
374
|
+
|
|
375
|
+
if (currentSize >= error) {
|
|
376
|
+
logger.error(
|
|
377
|
+
`router.${methodName}`,
|
|
378
|
+
`${currentSize} lifecycle handlers registered! ` +
|
|
379
|
+
`This is excessive. Hard limit at ${maxLifecycleHandlers}.`,
|
|
380
|
+
);
|
|
381
|
+
} else if (currentSize >= warn) {
|
|
382
|
+
logger.warn(
|
|
383
|
+
`router.${methodName}`,
|
|
384
|
+
`${currentSize} lifecycle handlers registered. ` +
|
|
385
|
+
`Consider consolidating logic.`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// packages/core/src/namespaces/RouteLifecycleNamespace/types.ts
|
|
2
|
+
|
|
3
|
+
import type { DefaultDependencies } from "@real-router/types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Dependencies injected into RouteLifecycleNamespace.
|
|
7
|
+
*
|
|
8
|
+
* Note: Lifecycle factories still receive the router object directly
|
|
9
|
+
* as they need access to various router methods. This interface
|
|
10
|
+
* only covers the internal namespace operations.
|
|
11
|
+
*/
|
|
12
|
+
export interface RouteLifecycleDependencies<
|
|
13
|
+
Dependencies extends DefaultDependencies = DefaultDependencies,
|
|
14
|
+
> {
|
|
15
|
+
/** Get dependency value for lifecycle factory */
|
|
16
|
+
getDependency: <K extends keyof Dependencies>(key: K) => Dependencies[K];
|
|
17
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// packages/core/src/namespaces/RouteLifecycleNamespace/validators.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Static validation functions for RouteLifecycleNamespace.
|
|
5
|
+
* Called by Router facade before instance methods.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { isBoolean, getTypeDescription } from "type-guards";
|
|
9
|
+
|
|
10
|
+
import { DEFAULT_LIMITS } from "../../constants";
|
|
11
|
+
|
|
12
|
+
import type { ActivationFnFactory } from "../../types";
|
|
13
|
+
import type { DefaultDependencies } from "@real-router/types";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Validates that a handler is either a boolean or a factory function.
|
|
17
|
+
*/
|
|
18
|
+
export function validateHandler<D extends DefaultDependencies>(
|
|
19
|
+
handler: unknown,
|
|
20
|
+
methodName: string,
|
|
21
|
+
): asserts handler is ActivationFnFactory<D> | boolean {
|
|
22
|
+
if (!isBoolean(handler) && typeof handler !== "function") {
|
|
23
|
+
throw new TypeError(
|
|
24
|
+
`[router.${methodName}] Handler must be a boolean or factory function, ` +
|
|
25
|
+
`got ${getTypeDescription(handler)}`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validates that a route is not currently being registered.
|
|
32
|
+
* Prevents self-modification during factory compilation.
|
|
33
|
+
*/
|
|
34
|
+
export function validateNotRegistering(
|
|
35
|
+
isRegistering: boolean,
|
|
36
|
+
name: string,
|
|
37
|
+
methodName: string,
|
|
38
|
+
): void {
|
|
39
|
+
if (isRegistering) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`[router.${methodName}] Cannot modify route "${name}" during its own registration`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validates that adding a new handler won't exceed the hard limit.
|
|
48
|
+
*/
|
|
49
|
+
export function validateHandlerLimit(
|
|
50
|
+
currentCount: number,
|
|
51
|
+
methodName: string,
|
|
52
|
+
maxLifecycleHandlers: number = DEFAULT_LIMITS.maxLifecycleHandlers,
|
|
53
|
+
): void {
|
|
54
|
+
if (maxLifecycleHandlers === 0) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (currentCount >= maxLifecycleHandlers) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`[router.${methodName}] Lifecycle handler limit exceeded (${maxLifecycleHandlers}). ` +
|
|
61
|
+
`This indicates too many routes with individual handlers. ` +
|
|
62
|
+
`Consider using middleware for cross-cutting concerns.`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// packages/core/src/namespaces/RouterLifecycleNamespace/RouterLifecycleNamespace.ts
|
|
2
|
+
|
|
3
|
+
import { errorCodes } from "../../constants";
|
|
4
|
+
import { RouterError } from "../../RouterError";
|
|
5
|
+
|
|
6
|
+
import type { RouterLifecycleDependencies } from "./types";
|
|
7
|
+
import type { NavigationOptions, State } from "@real-router/types";
|
|
8
|
+
|
|
9
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
10
|
+
// CYCLIC DEPENDENCIES
|
|
11
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
12
|
+
// RouterLifecycle → Navigation.navigateToState() (for start transitions)
|
|
13
|
+
//
|
|
14
|
+
// Solution: functional references configured in Router.#setupDependencies()
|
|
15
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Independent namespace for managing router lifecycle.
|
|
19
|
+
*
|
|
20
|
+
* Handles start() and stop(). Lifecycle state (isActive, isStarted) is managed
|
|
21
|
+
* by RouterFSM in the facade (Router.ts).
|
|
22
|
+
*/
|
|
23
|
+
export class RouterLifecycleNamespace {
|
|
24
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
25
|
+
// Functional references for cyclic dependencies
|
|
26
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
27
|
+
|
|
28
|
+
// Dependencies injected via setDependencies (replaces full router reference)
|
|
29
|
+
#navigateToState!: (
|
|
30
|
+
toState: State,
|
|
31
|
+
fromState: State | undefined,
|
|
32
|
+
opts: NavigationOptions,
|
|
33
|
+
) => Promise<State>;
|
|
34
|
+
|
|
35
|
+
#deps!: RouterLifecycleDependencies;
|
|
36
|
+
|
|
37
|
+
// =========================================================================
|
|
38
|
+
// Static validation methods (called by facade before instance methods)
|
|
39
|
+
// =========================================================================
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validates start() arguments.
|
|
43
|
+
*/
|
|
44
|
+
static validateStartArgs(args: unknown[]): void {
|
|
45
|
+
/* v8 ignore next 4 -- @preserve: facade enforces 1 arg via TypeScript signature */
|
|
46
|
+
if (args.length !== 1 || typeof args[0] !== "string") {
|
|
47
|
+
throw new Error(
|
|
48
|
+
"[router.start] Expected exactly 1 string argument (startPath).",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// =========================================================================
|
|
54
|
+
// Dependency injection
|
|
55
|
+
// =========================================================================
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Sets the navigateToState reference (cyclic dependency on NavigationNamespace).
|
|
59
|
+
* Must be called before using start().
|
|
60
|
+
*/
|
|
61
|
+
setNavigateToState(
|
|
62
|
+
fn: (
|
|
63
|
+
toState: State,
|
|
64
|
+
fromState: State | undefined,
|
|
65
|
+
opts: NavigationOptions,
|
|
66
|
+
) => Promise<State>,
|
|
67
|
+
): void {
|
|
68
|
+
this.#navigateToState = fn;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Sets dependencies for lifecycle operations.
|
|
73
|
+
* Must be called before using lifecycle methods.
|
|
74
|
+
*/
|
|
75
|
+
setDependencies(deps: RouterLifecycleDependencies): void {
|
|
76
|
+
this.#deps = deps;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// =========================================================================
|
|
80
|
+
// Instance methods
|
|
81
|
+
// =========================================================================
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Starts the router with the given path.
|
|
85
|
+
*
|
|
86
|
+
* Guards (concurrent start, already started) are handled by the facade via
|
|
87
|
+
* RouterFSM state checks before this method is called.
|
|
88
|
+
*/
|
|
89
|
+
async start(startPath: string): Promise<State> {
|
|
90
|
+
const deps = this.#deps;
|
|
91
|
+
const options = deps.getOptions();
|
|
92
|
+
|
|
93
|
+
const startOptions: NavigationOptions = {
|
|
94
|
+
replace: true,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const matchedState = deps.matchPath(startPath);
|
|
98
|
+
|
|
99
|
+
if (!matchedState && !options.allowNotFound) {
|
|
100
|
+
const err = new RouterError(errorCodes.ROUTE_NOT_FOUND, {
|
|
101
|
+
path: startPath,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
deps.emitTransitionError(undefined, undefined, err);
|
|
105
|
+
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
deps.completeStart();
|
|
110
|
+
|
|
111
|
+
let finalState: State;
|
|
112
|
+
|
|
113
|
+
if (matchedState) {
|
|
114
|
+
finalState = await this.#navigateToState(
|
|
115
|
+
matchedState,
|
|
116
|
+
undefined,
|
|
117
|
+
startOptions,
|
|
118
|
+
);
|
|
119
|
+
} else {
|
|
120
|
+
const notFoundState = deps.makeNotFoundState(startPath, startOptions);
|
|
121
|
+
|
|
122
|
+
finalState = await this.#navigateToState(
|
|
123
|
+
notFoundState,
|
|
124
|
+
undefined,
|
|
125
|
+
startOptions,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return finalState;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Stops the router and resets state.
|
|
134
|
+
*
|
|
135
|
+
* Called only for READY/TRANSITIONING states (facade handles STARTING/IDLE/DISPOSED).
|
|
136
|
+
*/
|
|
137
|
+
stop(): void {
|
|
138
|
+
this.#deps.setState();
|
|
139
|
+
}
|
|
140
|
+
}
|