@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
package/src/helpers.ts ADDED
@@ -0,0 +1,194 @@
1
+ // packages/real-router/modules/helpers.ts
2
+
3
+ import { DEFAULT_LIMITS } from "./constants";
4
+
5
+ import type { Limits } from "./types";
6
+ import type { State, LimitsConfig } from "@real-router/types";
7
+
8
+ export { getTypeDescription } from "type-guards";
9
+
10
+ // =============================================================================
11
+ // State Helpers
12
+ // =============================================================================
13
+
14
+ /**
15
+ * Structural type guard for State object.
16
+ * Only checks required fields exist with correct types.
17
+ * Does NOT validate params serializability (allows circular refs).
18
+ *
19
+ * Use `isState` from type-guards for full validation (serializable params).
20
+ * Use this for internal operations like deepFreezeState that handle any object structure.
21
+ *
22
+ * @param value - Value to check
23
+ * @returns true if value has State structure
24
+ * @internal
25
+ */
26
+ function isStateStructural(value: unknown): value is State {
27
+ if (value === null || typeof value !== "object") {
28
+ return false;
29
+ }
30
+
31
+ const obj = value as Record<string, unknown>;
32
+
33
+ return (
34
+ typeof obj.name === "string" &&
35
+ typeof obj.path === "string" &&
36
+ typeof obj.params === "object" &&
37
+ obj.params !== null
38
+ );
39
+ }
40
+
41
+ /**
42
+ * Deep freezes State object to prevent mutations.
43
+ * Creates a deep clone first, then recursively freezes the clone and all nested objects.
44
+ * Uses simple recursive freezing after cloning (no need for WeakSet since clone has no circular refs).
45
+ *
46
+ * @param state - The State object to freeze
47
+ * @returns A frozen deep clone of the state
48
+ * @throws {TypeError} If state is not a valid State object
49
+ *
50
+ * @example
51
+ * const state = { name: 'home', params: {}, path: '/' };
52
+ * const frozen = deepFreezeState(state);
53
+ * // frozen.params is now immutable
54
+ * // original state is unchanged
55
+ */
56
+ export function deepFreezeState<T extends State>(state: T): T {
57
+ // Early return for null/undefined
58
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
59
+ if (!state) {
60
+ return state;
61
+ }
62
+
63
+ // Validate State structure (structural check, allows circular refs)
64
+ if (!isStateStructural(state)) {
65
+ throw new TypeError(
66
+ `[deepFreezeState] Expected valid State object, got: ${typeof state}`,
67
+ );
68
+ }
69
+
70
+ // Create a deep clone to avoid mutating the original
71
+ // structuredClone preserves circular references, so we need to track visited objects
72
+ const clonedState = structuredClone(state);
73
+
74
+ // WeakSet to track visited objects (prevent infinite recursion with circular refs)
75
+ const visited = new WeakSet<object>();
76
+
77
+ // Recursive freeze function with circular reference protection
78
+ function freezeClonedRecursive(obj: unknown): void {
79
+ // Skip primitives, null, undefined
80
+ // Note: typeof undefined === "undefined" !== "object", so checking undefined is redundant
81
+ if (obj === null || typeof obj !== "object") {
82
+ return;
83
+ }
84
+
85
+ // Skip already visited objects (circular reference protection)
86
+ if (visited.has(obj)) {
87
+ return;
88
+ }
89
+
90
+ // Mark as visited
91
+ visited.add(obj);
92
+
93
+ // Freeze the object/array itself
94
+ Object.freeze(obj);
95
+
96
+ // Get all values to freeze recursively
97
+ const values = Array.isArray(obj) ? obj : Object.values(obj);
98
+
99
+ // Recursively freeze nested values
100
+ for (const value of values) {
101
+ freezeClonedRecursive(value);
102
+ }
103
+ }
104
+
105
+ // Freeze the entire cloned state tree
106
+ freezeClonedRecursive(clonedState);
107
+
108
+ return clonedState;
109
+ }
110
+
111
+ // WeakSet to track already frozen root objects for O(1) re-freeze check
112
+ const frozenRoots = new WeakSet<object>();
113
+
114
+ // Module-scope recursive freeze function - better JIT optimization, no allocation per call
115
+ function freezeRecursive(obj: unknown): void {
116
+ // Skip primitives, null
117
+ if (obj === null || typeof obj !== "object") {
118
+ return;
119
+ }
120
+
121
+ // Skip already frozen objects (handles potential shared refs)
122
+ if (Object.isFrozen(obj)) {
123
+ return;
124
+ }
125
+
126
+ // Freeze the object/array
127
+ Object.freeze(obj);
128
+
129
+ // Iterate without Object.values() allocation
130
+ if (Array.isArray(obj)) {
131
+ for (const item of obj) {
132
+ freezeRecursive(item);
133
+ }
134
+ } else {
135
+ for (const key in obj) {
136
+ freezeRecursive((obj as Record<string, unknown>)[key]);
137
+ }
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Freezes State object in-place without cloning.
143
+ * Optimized for hot paths where state is known to be a fresh object.
144
+ *
145
+ * IMPORTANT: Only use this when you know the state is a fresh object
146
+ * that hasn't been exposed to external code yet (e.g., from makeState()).
147
+ *
148
+ * @param state - The State object to freeze (must be a fresh object)
149
+ * @returns The same state object, now frozen
150
+ * @internal
151
+ */
152
+ export function freezeStateInPlace<T extends State>(state: T): T {
153
+ // Early return for null/undefined - state from makeState() is never null
154
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
155
+ if (!state) {
156
+ return state;
157
+ }
158
+
159
+ // Fast path: already processed root object - O(1) check
160
+ if (frozenRoots.has(state)) {
161
+ return state;
162
+ }
163
+
164
+ // Freeze the entire state tree
165
+ freezeRecursive(state);
166
+
167
+ // Mark root as processed for future calls
168
+ frozenRoots.add(state);
169
+
170
+ return state;
171
+ }
172
+
173
+ /**
174
+ * Computes warning and error thresholds for a given limit.
175
+ * WARN threshold: 20% of limit
176
+ * ERROR threshold: 50% of limit
177
+ */
178
+ export function computeThresholds(limit: number): {
179
+ warn: number;
180
+ error: number;
181
+ } {
182
+ return {
183
+ warn: Math.floor(limit * 0.2),
184
+ error: Math.floor(limit * 0.5),
185
+ };
186
+ }
187
+
188
+ /**
189
+ * Merges user limits with defaults.
190
+ * Returns frozen object for immutability.
191
+ */
192
+ export function createLimits(userLimits: Partial<LimitsConfig> = {}): Limits {
193
+ return { ...DEFAULT_LIMITS, ...userLimits };
194
+ }
package/src/index.ts ADDED
@@ -0,0 +1,46 @@
1
+ // packages/real-router/modules/index.ts
2
+
3
+ // Router-dependent types (defined in core)
4
+ export type {
5
+ ActivationFnFactory,
6
+ BuildStateResultWithSegments,
7
+ MiddlewareFactory,
8
+ PluginFactory,
9
+ Route,
10
+ RouteConfigUpdate,
11
+ } from "./types";
12
+
13
+ // Router class (replaces Router interface from core-types)
14
+ export { Router } from "./Router";
15
+
16
+ // Types (re-exported from core-types - no Router dependency)
17
+ export type {
18
+ ActivationFn,
19
+ Config,
20
+ DefaultDependencies,
21
+ Listener,
22
+ Middleware,
23
+ Navigator,
24
+ NavigationOptions,
25
+ Options,
26
+ Params,
27
+ Plugin,
28
+ SimpleState,
29
+ State,
30
+ StateMeta,
31
+ SubscribeFn,
32
+ SubscribeState,
33
+ Subscription,
34
+ Unsubscribe,
35
+ } from "@real-router/types";
36
+
37
+ export type { ErrorCodes, Constants } from "./constants";
38
+
39
+ export { events, constants, errorCodes } from "./constants";
40
+
41
+ // RouterError class (migrated from router-error package)
42
+ export { RouterError } from "./RouterError";
43
+
44
+ export { createRouter } from "./createRouter";
45
+
46
+ export { getNavigator } from "./getNavigator";
@@ -0,0 +1,120 @@
1
+ // packages/core/src/namespaces/CloneNamespace/CloneNamespace.ts
2
+
3
+ import { getTypeDescription } from "type-guards";
4
+
5
+ import type { ApplyConfigFn, CloneData, RouterFactory } from "./types";
6
+ import type { Router } from "../../Router";
7
+ import type { DefaultDependencies } from "@real-router/types";
8
+
9
+ /**
10
+ * Independent namespace for router cloning operations.
11
+ *
12
+ * This namespace handles the logic of collecting data from a source router
13
+ * and creating a configured clone. It requires a factory function to create
14
+ * the new router instance.
15
+ */
16
+ export class CloneNamespace<
17
+ Dependencies extends DefaultDependencies = DefaultDependencies,
18
+ > {
19
+ // =========================================================================
20
+ // Instance fields
21
+ // =========================================================================
22
+
23
+ /**
24
+ * Function to get cloning data from the source router.
25
+ */
26
+ #getCloneData!: () => CloneData<Dependencies>;
27
+
28
+ // =========================================================================
29
+ // Static validation methods (called by facade before instance methods)
30
+ // =========================================================================
31
+
32
+ /**
33
+ * Validates clone arguments.
34
+ * Dependencies can be undefined or a plain object without getters.
35
+ */
36
+ static validateCloneArgs(dependencies: unknown): void {
37
+ // undefined is valid (no new dependencies)
38
+ if (dependencies === undefined) {
39
+ return;
40
+ }
41
+
42
+ // Must be a plain object
43
+ if (
44
+ !(
45
+ dependencies &&
46
+ typeof dependencies === "object" &&
47
+ dependencies.constructor === Object
48
+ )
49
+ ) {
50
+ throw new TypeError(
51
+ `[router.clone] Invalid dependencies: expected plain object or undefined, received ${getTypeDescription(dependencies)}`,
52
+ );
53
+ }
54
+
55
+ // Getters can throw, return different values, or have side effects
56
+ for (const key in dependencies) {
57
+ if (Object.getOwnPropertyDescriptor(dependencies, key)?.get) {
58
+ throw new TypeError(
59
+ `[router.clone] Getters not allowed in dependencies: "${key}"`,
60
+ );
61
+ }
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Sets the function to collect clone data.
67
+ */
68
+ setGetCloneData(getCloneData: () => CloneData<Dependencies>): void {
69
+ this.#getCloneData = getCloneData;
70
+ }
71
+
72
+ /**
73
+ * Creates a clone of the router with optional new dependencies.
74
+ *
75
+ * @param dependencies - Optional new dependencies for the cloned router
76
+ * @param factory - Factory function to create the new router instance
77
+ * @param applyConfig - Function to apply route config to the new router
78
+ */
79
+ clone(
80
+ dependencies: Dependencies | undefined,
81
+ factory: RouterFactory<Dependencies>,
82
+ applyConfig: ApplyConfigFn<Dependencies>,
83
+ ): Router<Dependencies> {
84
+ // Collect all data from source router
85
+ const data = this.#getCloneData();
86
+
87
+ // Merge dependencies
88
+ const mergedDeps = {
89
+ ...data.dependencies,
90
+ ...dependencies,
91
+ } as Dependencies;
92
+
93
+ // Create new router instance
94
+ const newRouter = factory(data.routes, data.options, mergedDeps);
95
+
96
+ // Copy lifecycle factories
97
+ for (const [name, handler] of Object.entries(data.canDeactivateFactories)) {
98
+ newRouter.addDeactivateGuard(name, handler);
99
+ }
100
+
101
+ for (const [name, handler] of Object.entries(data.canActivateFactories)) {
102
+ newRouter.addActivateGuard(name, handler);
103
+ }
104
+
105
+ // Copy middleware factories
106
+ if (data.middlewareFactories.length > 0) {
107
+ newRouter.useMiddleware(...data.middlewareFactories);
108
+ }
109
+
110
+ // Copy plugin factories
111
+ if (data.pluginFactories.length > 0) {
112
+ newRouter.usePlugin(...data.pluginFactories);
113
+ }
114
+
115
+ // Apply route config (decoders, encoders, defaultParams, forwardMap)
116
+ applyConfig(newRouter, data.routeConfig, data.resolvedForwardMap);
117
+
118
+ return newRouter;
119
+ }
120
+ }
@@ -0,0 +1,3 @@
1
+ export { CloneNamespace } from "./CloneNamespace";
2
+
3
+ export type { ApplyConfigFn, CloneData, RouterFactory } from "./types";
@@ -0,0 +1,46 @@
1
+ // packages/core/src/namespaces/CloneNamespace/types.ts
2
+
3
+ import type { Router } from "../../Router";
4
+ import type {
5
+ ActivationFnFactory,
6
+ MiddlewareFactory,
7
+ PluginFactory,
8
+ Route,
9
+ } from "../../types";
10
+ import type { RouteConfig } from "../RoutesNamespace";
11
+ import type { DefaultDependencies, Options } from "@real-router/types";
12
+
13
+ /**
14
+ * Data collected from source router for cloning.
15
+ */
16
+ export interface CloneData<Dependencies extends DefaultDependencies> {
17
+ routes: Route<Dependencies>[];
18
+ options: Options;
19
+ dependencies: Partial<Dependencies>;
20
+ canDeactivateFactories: Record<string, ActivationFnFactory<Dependencies>>;
21
+ canActivateFactories: Record<string, ActivationFnFactory<Dependencies>>;
22
+ middlewareFactories: MiddlewareFactory<Dependencies>[];
23
+ pluginFactories: PluginFactory<Dependencies>[];
24
+ routeConfig: RouteConfig;
25
+ resolvedForwardMap: Record<string, string>;
26
+ }
27
+
28
+ /**
29
+ * Factory function to create a new router instance.
30
+ */
31
+ export type RouterFactory<Dependencies extends DefaultDependencies> = (
32
+ routes: Route<Dependencies>[],
33
+ options: Partial<Options>,
34
+ dependencies: Dependencies,
35
+ ) => Router<Dependencies>;
36
+
37
+ /**
38
+ * Function to apply route config to a new router.
39
+ */
40
+ export type ApplyConfigFn<
41
+ Dependencies extends DefaultDependencies = DefaultDependencies,
42
+ > = (
43
+ router: Router<Dependencies>,
44
+ config: RouteConfig,
45
+ resolvedForwardMap: Record<string, string>,
46
+ ) => void;
@@ -0,0 +1,250 @@
1
+ // packages/core/src/namespaces/DependenciesNamespace/DependenciesNamespace.ts
2
+
3
+ import { logger } from "@real-router/logger";
4
+ import { getTypeDescription } from "type-guards";
5
+
6
+ import {
7
+ validateDependencyExists,
8
+ validateDependencyLimit,
9
+ validateDependencyName,
10
+ validateDependenciesObject,
11
+ validateSetDependencyArgs,
12
+ } from "./validators";
13
+ import { DEFAULT_LIMITS } from "../../constants";
14
+ import { computeThresholds } from "../../helpers";
15
+
16
+ import type { Limits } from "../../types";
17
+ import type { DefaultDependencies } from "@real-router/types";
18
+
19
+ /**
20
+ * Independent namespace for managing router dependencies.
21
+ *
22
+ * Static methods handle validation (called by facade).
23
+ * Instance methods handle storage and business logic (limits, warnings).
24
+ */
25
+ export class DependenciesNamespace<
26
+ Dependencies extends DefaultDependencies = DefaultDependencies,
27
+ > {
28
+ readonly #dependencies: Partial<Dependencies> = Object.create(
29
+ null,
30
+ ) as Partial<Dependencies>;
31
+
32
+ #limits: Limits = DEFAULT_LIMITS;
33
+
34
+ constructor(initialDependencies: Partial<Dependencies> = {} as Dependencies) {
35
+ this.setMultiple(initialDependencies);
36
+ }
37
+
38
+ // =========================================================================
39
+ // Static validation methods (called by facade before instance methods)
40
+ // Proxy to functions in validators.ts for separation of concerns
41
+ // =========================================================================
42
+
43
+ static validateName(
44
+ name: unknown,
45
+ methodName: string,
46
+ ): asserts name is string {
47
+ validateDependencyName(name, methodName);
48
+ }
49
+
50
+ static validateSetDependencyArgs(name: unknown): asserts name is string {
51
+ validateSetDependencyArgs(name);
52
+ }
53
+
54
+ static validateDependenciesObject(
55
+ deps: unknown,
56
+ methodName: string,
57
+ ): asserts deps is Record<string, unknown> {
58
+ validateDependenciesObject(deps, methodName);
59
+ }
60
+
61
+ static validateDependencyExists(
62
+ value: unknown,
63
+ dependencyName: string,
64
+ ): asserts value is NonNullable<unknown> {
65
+ validateDependencyExists(value, dependencyName);
66
+ }
67
+
68
+ static validateDependencyLimit(
69
+ currentCount: number,
70
+ newCount: number,
71
+ methodName: string,
72
+ maxDependencies?: number,
73
+ ): void {
74
+ validateDependencyLimit(
75
+ currentCount,
76
+ newCount,
77
+ methodName,
78
+ maxDependencies,
79
+ );
80
+ }
81
+
82
+ setLimits(limits: Limits): void {
83
+ this.#limits = limits;
84
+ }
85
+
86
+ // =========================================================================
87
+ // Instance methods (trust input - already validated by facade)
88
+ // =========================================================================
89
+
90
+ /**
91
+ * Sets a single dependency.
92
+ * Returns true if set, false if value was undefined (no-op).
93
+ *
94
+ * @param dependencyName - Already validated by facade
95
+ * @param dependencyValue - Value to set
96
+ */
97
+ set<K extends keyof Dependencies & string>(
98
+ dependencyName: K,
99
+ dependencyValue: Dependencies[K],
100
+ ): boolean {
101
+ // undefined = "don't set" (feature for conditional setting)
102
+ if (dependencyValue === undefined) {
103
+ return false;
104
+ }
105
+
106
+ const isNewKey = !Object.hasOwn(this.#dependencies, dependencyName);
107
+
108
+ if (isNewKey) {
109
+ // Only check limit when adding new keys (overwrites don't increase count)
110
+ this.#checkDependencyCount("setDependency");
111
+ } else {
112
+ const oldValue = this.#dependencies[dependencyName];
113
+ const isChanging = oldValue !== dependencyValue;
114
+ // Special case for NaN idempotency (NaN !== NaN is always true)
115
+ const bothAreNaN =
116
+ Number.isNaN(oldValue) && Number.isNaN(dependencyValue);
117
+
118
+ if (isChanging && !bothAreNaN) {
119
+ logger.warn(
120
+ "router.setDependency",
121
+ "Router dependency already exists and is being overwritten:",
122
+ dependencyName,
123
+ );
124
+ }
125
+ }
126
+
127
+ this.#dependencies[dependencyName] = dependencyValue;
128
+
129
+ return true;
130
+ }
131
+
132
+ /**
133
+ * Sets multiple dependencies at once.
134
+ * Limit check should be done by facade before calling this method.
135
+ *
136
+ * @param deps - Already validated by facade
137
+ */
138
+ setMultiple(deps: Partial<Dependencies>): void {
139
+ const overwrittenKeys: string[] = [];
140
+
141
+ for (const key in deps) {
142
+ if (deps[key] !== undefined) {
143
+ if (Object.hasOwn(this.#dependencies, key)) {
144
+ overwrittenKeys.push(key);
145
+ }
146
+
147
+ this.#dependencies[key] = deps[key];
148
+ }
149
+ }
150
+
151
+ if (overwrittenKeys.length > 0) {
152
+ logger.warn(
153
+ "router.setDependencies",
154
+ "Overwritten:",
155
+ overwrittenKeys.join(", "),
156
+ );
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Gets a single dependency.
162
+ * Throws if not found.
163
+ *
164
+ * @param dependencyName - Already validated by facade
165
+ */
166
+ get<K extends keyof Dependencies>(dependencyName: K): Dependencies[K] {
167
+ return this.#dependencies[dependencyName] as Dependencies[K];
168
+ }
169
+
170
+ /**
171
+ * Gets all dependencies as a shallow copy.
172
+ */
173
+ getAll(): Partial<Dependencies> {
174
+ return { ...this.#dependencies };
175
+ }
176
+
177
+ /**
178
+ * Gets the current number of dependencies.
179
+ */
180
+ count(): number {
181
+ return Object.keys(this.#dependencies).length;
182
+ }
183
+
184
+ /**
185
+ * Removes a dependency by name.
186
+ * Logs warning if dependency doesn't exist.
187
+ */
188
+ remove(dependencyName: keyof Dependencies): void {
189
+ if (!Object.hasOwn(this.#dependencies, dependencyName)) {
190
+ logger.warn(
191
+ `router.removeDependency`,
192
+ `Attempted to remove non-existent dependency: "${getTypeDescription(dependencyName)}"`,
193
+ );
194
+ }
195
+
196
+ delete this.#dependencies[dependencyName];
197
+ }
198
+
199
+ /**
200
+ * Checks if a dependency exists.
201
+ */
202
+ has(dependencyName: keyof Dependencies): boolean {
203
+ return Object.hasOwn(this.#dependencies, dependencyName);
204
+ }
205
+
206
+ /**
207
+ * Removes all dependencies.
208
+ */
209
+ reset(): void {
210
+ for (const key in this.#dependencies) {
211
+ delete this.#dependencies[key];
212
+ }
213
+ }
214
+
215
+ // =========================================================================
216
+ // Private methods (business logic)
217
+ // =========================================================================
218
+
219
+ #checkDependencyCount(methodName: string): void {
220
+ const maxDependencies = this.#limits.maxDependencies;
221
+
222
+ if (maxDependencies === 0) {
223
+ return;
224
+ }
225
+
226
+ const currentCount = Object.keys(this.#dependencies).length;
227
+
228
+ const { warn, error } = computeThresholds(maxDependencies);
229
+
230
+ if (currentCount === warn) {
231
+ logger.warn(
232
+ `router.${methodName}`,
233
+ `${warn} dependencies registered. ` + `Consider if all are necessary.`,
234
+ );
235
+ } else if (currentCount === error) {
236
+ logger.error(
237
+ `router.${methodName}`,
238
+ `${error} dependencies registered! ` +
239
+ `This indicates architectural problems. ` +
240
+ `Hard limit at ${maxDependencies}.`,
241
+ );
242
+ } else if (currentCount >= maxDependencies) {
243
+ throw new Error(
244
+ `[router.${methodName}] Dependency limit exceeded (${maxDependencies}). ` +
245
+ `Current: ${currentCount}. This is likely a bug in your code. ` +
246
+ `If you genuinely need more dependencies, your architecture needs refactoring.`,
247
+ );
248
+ }
249
+ }
250
+ }
@@ -0,0 +1,3 @@
1
+ // packages/core/src/namespaces/DependenciesNamespace/index.ts
2
+
3
+ export { DependenciesNamespace } from "./DependenciesNamespace";