@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.
Files changed (80) hide show
  1. package/README.md +1 -3
  2. package/dist/cjs/index.d.ts +1 -1
  3. package/dist/cjs/index.js +1 -1
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/cjs/metafile-cjs.json +1 -1
  6. package/dist/esm/index.d.mts +1 -1
  7. package/dist/esm/index.mjs +1 -1
  8. package/dist/esm/index.mjs.map +1 -1
  9. package/dist/esm/metafile-esm.json +1 -1
  10. package/package.json +7 -5
  11. package/src/Router.ts +1174 -0
  12. package/src/RouterError.ts +324 -0
  13. package/src/constants.ts +112 -0
  14. package/src/createRouter.ts +32 -0
  15. package/src/fsm/index.ts +5 -0
  16. package/src/fsm/routerFSM.ts +129 -0
  17. package/src/getNavigator.ts +15 -0
  18. package/src/helpers.ts +194 -0
  19. package/src/index.ts +46 -0
  20. package/src/namespaces/CloneNamespace/CloneNamespace.ts +120 -0
  21. package/src/namespaces/CloneNamespace/index.ts +3 -0
  22. package/src/namespaces/CloneNamespace/types.ts +46 -0
  23. package/src/namespaces/DependenciesNamespace/DependenciesNamespace.ts +250 -0
  24. package/src/namespaces/DependenciesNamespace/index.ts +3 -0
  25. package/src/namespaces/DependenciesNamespace/validators.ts +105 -0
  26. package/src/namespaces/EventBusNamespace/EventBusNamespace.ts +272 -0
  27. package/src/namespaces/EventBusNamespace/index.ts +5 -0
  28. package/src/namespaces/EventBusNamespace/types.ts +11 -0
  29. package/src/namespaces/MiddlewareNamespace/MiddlewareNamespace.ts +206 -0
  30. package/src/namespaces/MiddlewareNamespace/index.ts +5 -0
  31. package/src/namespaces/MiddlewareNamespace/types.ts +28 -0
  32. package/src/namespaces/MiddlewareNamespace/validators.ts +96 -0
  33. package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +308 -0
  34. package/src/namespaces/NavigationNamespace/index.ts +5 -0
  35. package/src/namespaces/NavigationNamespace/transition/executeLifecycleHooks.ts +84 -0
  36. package/src/namespaces/NavigationNamespace/transition/executeMiddleware.ts +56 -0
  37. package/src/namespaces/NavigationNamespace/transition/index.ts +107 -0
  38. package/src/namespaces/NavigationNamespace/transition/makeError.ts +37 -0
  39. package/src/namespaces/NavigationNamespace/transition/mergeStates.ts +54 -0
  40. package/src/namespaces/NavigationNamespace/transition/processLifecycleResult.ts +81 -0
  41. package/src/namespaces/NavigationNamespace/transition/wrapSyncError.ts +82 -0
  42. package/src/namespaces/NavigationNamespace/types.ts +129 -0
  43. package/src/namespaces/NavigationNamespace/validators.ts +87 -0
  44. package/src/namespaces/OptionsNamespace/OptionsNamespace.ts +50 -0
  45. package/src/namespaces/OptionsNamespace/constants.ts +41 -0
  46. package/src/namespaces/OptionsNamespace/helpers.ts +51 -0
  47. package/src/namespaces/OptionsNamespace/index.ts +11 -0
  48. package/src/namespaces/OptionsNamespace/validators.ts +252 -0
  49. package/src/namespaces/PluginsNamespace/PluginsNamespace.ts +325 -0
  50. package/src/namespaces/PluginsNamespace/constants.ts +35 -0
  51. package/src/namespaces/PluginsNamespace/index.ts +7 -0
  52. package/src/namespaces/PluginsNamespace/types.ts +32 -0
  53. package/src/namespaces/PluginsNamespace/validators.ts +79 -0
  54. package/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts +389 -0
  55. package/src/namespaces/RouteLifecycleNamespace/index.ts +5 -0
  56. package/src/namespaces/RouteLifecycleNamespace/types.ts +17 -0
  57. package/src/namespaces/RouteLifecycleNamespace/validators.ts +65 -0
  58. package/src/namespaces/RouterLifecycleNamespace/RouterLifecycleNamespace.ts +140 -0
  59. package/src/namespaces/RouterLifecycleNamespace/constants.ts +25 -0
  60. package/src/namespaces/RouterLifecycleNamespace/index.ts +5 -0
  61. package/src/namespaces/RouterLifecycleNamespace/types.ts +23 -0
  62. package/src/namespaces/RoutesNamespace/RoutesNamespace.ts +1482 -0
  63. package/src/namespaces/RoutesNamespace/constants.ts +14 -0
  64. package/src/namespaces/RoutesNamespace/helpers.ts +532 -0
  65. package/src/namespaces/RoutesNamespace/index.ts +9 -0
  66. package/src/namespaces/RoutesNamespace/stateBuilder.ts +70 -0
  67. package/src/namespaces/RoutesNamespace/types.ts +82 -0
  68. package/src/namespaces/RoutesNamespace/validators.ts +331 -0
  69. package/src/namespaces/StateNamespace/StateNamespace.ts +317 -0
  70. package/src/namespaces/StateNamespace/helpers.ts +43 -0
  71. package/src/namespaces/StateNamespace/index.ts +5 -0
  72. package/src/namespaces/StateNamespace/types.ts +15 -0
  73. package/src/namespaces/index.ts +42 -0
  74. package/src/transitionPath.ts +441 -0
  75. package/src/typeGuards.ts +74 -0
  76. package/src/types.ts +194 -0
  77. package/src/wiring/RouterWiringBuilder.ts +235 -0
  78. package/src/wiring/index.ts +7 -0
  79. package/src/wiring/types.ts +53 -0
  80. 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,5 @@
1
+ // packages/core/src/namespaces/RouteLifecycleNamespace/index.ts
2
+
3
+ export { RouteLifecycleNamespace } from "./RouteLifecycleNamespace";
4
+
5
+ export type { RouteLifecycleDependencies } from "./types";
@@ -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
+ }