@real-router/core 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -3
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/metafile-cjs.json +1 -1
- package/dist/esm/index.d.mts +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/metafile-esm.json +1 -1
- package/package.json +7 -5
- package/src/Router.ts +1174 -0
- package/src/RouterError.ts +324 -0
- package/src/constants.ts +112 -0
- package/src/createRouter.ts +32 -0
- package/src/fsm/index.ts +5 -0
- package/src/fsm/routerFSM.ts +129 -0
- package/src/getNavigator.ts +15 -0
- package/src/helpers.ts +194 -0
- package/src/index.ts +46 -0
- package/src/namespaces/CloneNamespace/CloneNamespace.ts +120 -0
- package/src/namespaces/CloneNamespace/index.ts +3 -0
- package/src/namespaces/CloneNamespace/types.ts +46 -0
- package/src/namespaces/DependenciesNamespace/DependenciesNamespace.ts +250 -0
- package/src/namespaces/DependenciesNamespace/index.ts +3 -0
- package/src/namespaces/DependenciesNamespace/validators.ts +105 -0
- package/src/namespaces/EventBusNamespace/EventBusNamespace.ts +272 -0
- package/src/namespaces/EventBusNamespace/index.ts +5 -0
- package/src/namespaces/EventBusNamespace/types.ts +11 -0
- package/src/namespaces/MiddlewareNamespace/MiddlewareNamespace.ts +206 -0
- package/src/namespaces/MiddlewareNamespace/index.ts +5 -0
- package/src/namespaces/MiddlewareNamespace/types.ts +28 -0
- package/src/namespaces/MiddlewareNamespace/validators.ts +96 -0
- package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +308 -0
- package/src/namespaces/NavigationNamespace/index.ts +5 -0
- package/src/namespaces/NavigationNamespace/transition/executeLifecycleHooks.ts +84 -0
- package/src/namespaces/NavigationNamespace/transition/executeMiddleware.ts +56 -0
- package/src/namespaces/NavigationNamespace/transition/index.ts +107 -0
- package/src/namespaces/NavigationNamespace/transition/makeError.ts +37 -0
- package/src/namespaces/NavigationNamespace/transition/mergeStates.ts +54 -0
- package/src/namespaces/NavigationNamespace/transition/processLifecycleResult.ts +81 -0
- package/src/namespaces/NavigationNamespace/transition/wrapSyncError.ts +82 -0
- package/src/namespaces/NavigationNamespace/types.ts +129 -0
- package/src/namespaces/NavigationNamespace/validators.ts +87 -0
- package/src/namespaces/OptionsNamespace/OptionsNamespace.ts +50 -0
- package/src/namespaces/OptionsNamespace/constants.ts +41 -0
- package/src/namespaces/OptionsNamespace/helpers.ts +51 -0
- package/src/namespaces/OptionsNamespace/index.ts +11 -0
- package/src/namespaces/OptionsNamespace/validators.ts +252 -0
- package/src/namespaces/PluginsNamespace/PluginsNamespace.ts +325 -0
- package/src/namespaces/PluginsNamespace/constants.ts +35 -0
- package/src/namespaces/PluginsNamespace/index.ts +7 -0
- package/src/namespaces/PluginsNamespace/types.ts +32 -0
- package/src/namespaces/PluginsNamespace/validators.ts +79 -0
- package/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts +389 -0
- package/src/namespaces/RouteLifecycleNamespace/index.ts +5 -0
- package/src/namespaces/RouteLifecycleNamespace/types.ts +17 -0
- package/src/namespaces/RouteLifecycleNamespace/validators.ts +65 -0
- package/src/namespaces/RouterLifecycleNamespace/RouterLifecycleNamespace.ts +140 -0
- package/src/namespaces/RouterLifecycleNamespace/constants.ts +25 -0
- package/src/namespaces/RouterLifecycleNamespace/index.ts +5 -0
- package/src/namespaces/RouterLifecycleNamespace/types.ts +23 -0
- package/src/namespaces/RoutesNamespace/RoutesNamespace.ts +1482 -0
- package/src/namespaces/RoutesNamespace/constants.ts +14 -0
- package/src/namespaces/RoutesNamespace/helpers.ts +532 -0
- package/src/namespaces/RoutesNamespace/index.ts +9 -0
- package/src/namespaces/RoutesNamespace/stateBuilder.ts +70 -0
- package/src/namespaces/RoutesNamespace/types.ts +82 -0
- package/src/namespaces/RoutesNamespace/validators.ts +331 -0
- package/src/namespaces/StateNamespace/StateNamespace.ts +317 -0
- package/src/namespaces/StateNamespace/helpers.ts +43 -0
- package/src/namespaces/StateNamespace/index.ts +5 -0
- package/src/namespaces/StateNamespace/types.ts +15 -0
- package/src/namespaces/index.ts +42 -0
- package/src/transitionPath.ts +441 -0
- package/src/typeGuards.ts +74 -0
- package/src/types.ts +194 -0
- package/src/wiring/RouterWiringBuilder.ts +235 -0
- package/src/wiring/index.ts +7 -0
- package/src/wiring/types.ts +53 -0
- package/src/wiring/wireRouter.ts +29 -0
package/src/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,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
|
+
}
|