@real-router/core 0.45.1 → 0.45.3
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/dist/cjs/Router-Dh1xgFLI.d.ts.map +1 -0
- package/dist/cjs/RouterValidator-TUi8eT8Q.d.ts.map +1 -0
- package/dist/cjs/api.d.ts.map +1 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/utils.d.ts.map +1 -0
- package/dist/cjs/validation.d.ts.map +1 -0
- package/dist/esm/Router-BPkXwb1J.d.mts.map +1 -0
- package/dist/esm/RouterValidator-DphcVMEp.d.mts.map +1 -0
- package/dist/esm/api.d.mts.map +1 -0
- package/dist/esm/index.d.mts.map +1 -0
- package/dist/esm/utils.d.mts.map +1 -0
- package/dist/esm/validation.d.mts.map +1 -0
- package/package.json +9 -12
- package/src/Router.ts +684 -0
- package/src/RouterError.ts +324 -0
- package/src/api/cloneRouter.ts +77 -0
- package/src/api/getDependenciesApi.ts +168 -0
- package/src/api/getLifecycleApi.ts +65 -0
- package/src/api/getPluginApi.ts +167 -0
- package/src/api/getRoutesApi.ts +573 -0
- package/src/api/helpers.ts +10 -0
- package/src/api/index.ts +16 -0
- package/src/api/types.ts +12 -0
- package/src/constants.ts +87 -0
- package/src/createRouter.ts +32 -0
- package/src/fsm/index.ts +5 -0
- package/src/fsm/routerFSM.ts +120 -0
- package/src/getNavigator.ts +30 -0
- package/src/guards.ts +46 -0
- package/src/helpers.ts +179 -0
- package/src/index.ts +50 -0
- package/src/internals.ts +173 -0
- package/src/namespaces/DependenciesNamespace/dependenciesStore.ts +30 -0
- package/src/namespaces/DependenciesNamespace/index.ts +5 -0
- package/src/namespaces/EventBusNamespace/EventBusNamespace.ts +311 -0
- package/src/namespaces/EventBusNamespace/index.ts +5 -0
- package/src/namespaces/EventBusNamespace/types.ts +11 -0
- package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +405 -0
- package/src/namespaces/NavigationNamespace/constants.ts +55 -0
- package/src/namespaces/NavigationNamespace/index.ts +5 -0
- package/src/namespaces/NavigationNamespace/transition/completeTransition.ts +100 -0
- package/src/namespaces/NavigationNamespace/transition/errorHandling.ts +124 -0
- package/src/namespaces/NavigationNamespace/transition/guardPhase.ts +221 -0
- package/src/namespaces/NavigationNamespace/types.ts +100 -0
- package/src/namespaces/OptionsNamespace/OptionsNamespace.ts +28 -0
- package/src/namespaces/OptionsNamespace/constants.ts +19 -0
- package/src/namespaces/OptionsNamespace/helpers.ts +50 -0
- package/src/namespaces/OptionsNamespace/index.ts +7 -0
- package/src/namespaces/OptionsNamespace/validators.ts +13 -0
- package/src/namespaces/PluginsNamespace/PluginsNamespace.ts +291 -0
- package/src/namespaces/PluginsNamespace/constants.ts +34 -0
- package/src/namespaces/PluginsNamespace/index.ts +7 -0
- package/src/namespaces/PluginsNamespace/types.ts +22 -0
- package/src/namespaces/PluginsNamespace/validators.ts +28 -0
- package/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts +377 -0
- package/src/namespaces/RouteLifecycleNamespace/index.ts +5 -0
- package/src/namespaces/RouteLifecycleNamespace/types.ts +10 -0
- package/src/namespaces/RouterLifecycleNamespace/RouterLifecycleNamespace.ts +81 -0
- package/src/namespaces/RouterLifecycleNamespace/constants.ts +25 -0
- package/src/namespaces/RouterLifecycleNamespace/index.ts +5 -0
- package/src/namespaces/RouterLifecycleNamespace/types.ts +26 -0
- package/src/namespaces/RoutesNamespace/RoutesNamespace.ts +535 -0
- package/src/namespaces/RoutesNamespace/constants.ts +6 -0
- package/src/namespaces/RoutesNamespace/forwardChain.ts +34 -0
- package/src/namespaces/RoutesNamespace/helpers.ts +126 -0
- package/src/namespaces/RoutesNamespace/index.ts +11 -0
- package/src/namespaces/RoutesNamespace/routeGuards.ts +62 -0
- package/src/namespaces/RoutesNamespace/routesStore.ts +346 -0
- package/src/namespaces/RoutesNamespace/types.ts +81 -0
- package/src/namespaces/StateNamespace/StateNamespace.ts +211 -0
- package/src/namespaces/StateNamespace/helpers.ts +24 -0
- package/src/namespaces/StateNamespace/index.ts +5 -0
- package/src/namespaces/StateNamespace/types.ts +15 -0
- package/src/namespaces/index.ts +35 -0
- package/src/stateMetaStore.ts +15 -0
- package/src/transitionPath.ts +436 -0
- package/src/typeGuards.ts +59 -0
- package/src/types/RouterValidator.ts +154 -0
- package/src/types.ts +69 -0
- package/src/utils/getStaticPaths.ts +50 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/serializeState.ts +22 -0
- package/src/validation.ts +12 -0
- package/src/wiring/RouterWiringBuilder.ts +261 -0
- package/src/wiring/index.ts +7 -0
- package/src/wiring/types.ts +47 -0
- package/src/wiring/wireRouter.ts +26 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// packages/core/src/namespaces/StateNamespace/helpers.ts
|
|
2
|
+
|
|
3
|
+
export function areParamValuesEqual(val1: unknown, val2: unknown): boolean {
|
|
4
|
+
if (val1 === val2) {
|
|
5
|
+
return true;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (Array.isArray(val1) && Array.isArray(val2)) {
|
|
9
|
+
if (val1.length !== val2.length) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line unicorn/no-for-loop -- hot path: for-of entries() allocates iterator per recursive call
|
|
14
|
+
for (let i = 0; i < val1.length; i++) {
|
|
15
|
+
if (!areParamValuesEqual(val1[i], val2[i])) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// packages/core/src/namespaces/StateNamespace/types.ts
|
|
2
|
+
|
|
3
|
+
import type { Params } from "@real-router/types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Dependencies injected from Router for state creation.
|
|
7
|
+
*/
|
|
8
|
+
export interface StateNamespaceDependencies {
|
|
9
|
+
/** Get defaultParams config for a route */
|
|
10
|
+
getDefaultParams: () => Record<string, Params>;
|
|
11
|
+
/** Build URL path for a route */
|
|
12
|
+
buildPath: (name: string, params?: Params) => string;
|
|
13
|
+
/** Get URL params for a route (for areStatesEqual) */
|
|
14
|
+
getUrlParams: (name: string) => string[];
|
|
15
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// packages/core/src/namespaces/index.ts
|
|
2
|
+
|
|
3
|
+
export { createDependenciesStore } from "./DependenciesNamespace";
|
|
4
|
+
|
|
5
|
+
export type { DependenciesStore } from "./DependenciesNamespace";
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
deepFreeze,
|
|
9
|
+
defaultOptions,
|
|
10
|
+
OptionsNamespace,
|
|
11
|
+
} from "./OptionsNamespace";
|
|
12
|
+
|
|
13
|
+
export { StateNamespace } from "./StateNamespace";
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
PluginsNamespace,
|
|
17
|
+
EVENTS_MAP,
|
|
18
|
+
EVENT_METHOD_NAMES,
|
|
19
|
+
} from "./PluginsNamespace";
|
|
20
|
+
|
|
21
|
+
export { RouteLifecycleNamespace } from "./RouteLifecycleNamespace";
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
RoutesNamespace,
|
|
25
|
+
DEFAULT_ROUTE_NAME,
|
|
26
|
+
createEmptyConfig,
|
|
27
|
+
} from "./RoutesNamespace";
|
|
28
|
+
|
|
29
|
+
export type { RouteConfig } from "./RoutesNamespace";
|
|
30
|
+
|
|
31
|
+
export { NavigationNamespace } from "./NavigationNamespace";
|
|
32
|
+
|
|
33
|
+
export { RouterLifecycleNamespace } from "./RouterLifecycleNamespace";
|
|
34
|
+
|
|
35
|
+
export { EventBusNamespace } from "./EventBusNamespace";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// packages/core/src/stateMetaStore.ts
|
|
2
|
+
|
|
3
|
+
import type { Params, State } from "@real-router/types";
|
|
4
|
+
|
|
5
|
+
const store = new WeakMap<State, Params>();
|
|
6
|
+
|
|
7
|
+
/** @internal */
|
|
8
|
+
export function getStateMetaParams(state: State): Params | undefined {
|
|
9
|
+
return store.get(state);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** @internal */
|
|
13
|
+
export function setStateMetaParams(state: State, params: Params): void {
|
|
14
|
+
store.set(state, params);
|
|
15
|
+
}
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
// packages/core/src/transitionPath.ts
|
|
2
|
+
|
|
3
|
+
import { getStateMetaParams } from "./stateMetaStore";
|
|
4
|
+
|
|
5
|
+
import type { State } from "@real-router/types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parameters extracted from a route segment.
|
|
9
|
+
* Maps parameter names to their string values.
|
|
10
|
+
*/
|
|
11
|
+
type PrimitiveParam = string | number | boolean;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Represents a transition path between two router states.
|
|
15
|
+
* Contains information about which route segments need to be activated/deactivated.
|
|
16
|
+
*/
|
|
17
|
+
interface TransitionPath {
|
|
18
|
+
/** The common ancestor route segment where paths diverge */
|
|
19
|
+
intersection: string;
|
|
20
|
+
/** Route segments that need to be deactivated (in reverse order) */
|
|
21
|
+
toDeactivate: string[];
|
|
22
|
+
/** Route segments that need to be activated (in order) */
|
|
23
|
+
toActivate: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Constants for better maintainability
|
|
27
|
+
const ROUTE_SEGMENT_SEPARATOR = ".";
|
|
28
|
+
const EMPTY_INTERSECTION = "";
|
|
29
|
+
const DEFAULT_ROUTE_NAME = "";
|
|
30
|
+
const FROZEN_EMPTY_ARRAY: string[] = [];
|
|
31
|
+
|
|
32
|
+
Object.freeze(FROZEN_EMPTY_ARRAY);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Builds a reversed copy of a string array.
|
|
36
|
+
* Optimization: single pass instead of creating intermediate array with .toReversed().
|
|
37
|
+
*
|
|
38
|
+
* @param arr - Source array
|
|
39
|
+
* @returns New array with elements in reverse order
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
function reverseArray(arr: string[]): string[] {
|
|
43
|
+
const length = arr.length;
|
|
44
|
+
const result: string[] = [];
|
|
45
|
+
|
|
46
|
+
for (let i = length - 1; i >= 0; i--) {
|
|
47
|
+
result.push(arr[i]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Handles conversion of route names with many segments (5+).
|
|
55
|
+
* Internal helper for nameToIDs function.
|
|
56
|
+
*
|
|
57
|
+
* Uses optimized hybrid approach: split to get segments, then slice original
|
|
58
|
+
* string to build cumulative paths. This approach is 65-81% faster than
|
|
59
|
+
* string concatenation for typical cases (5-10 segments).
|
|
60
|
+
*
|
|
61
|
+
* @param name - Route name with 5 or more segments
|
|
62
|
+
* @returns Array of cumulative segment IDs
|
|
63
|
+
* @throws {Error} If route depth exceeds maximum allowed
|
|
64
|
+
* @internal
|
|
65
|
+
*/
|
|
66
|
+
function nameToIDsGeneral(name: string): string[] {
|
|
67
|
+
// We know there are at least 5 segments at this point (after fast paths)
|
|
68
|
+
const segments = name.split(ROUTE_SEGMENT_SEPARATOR);
|
|
69
|
+
const segmentCount = segments.length;
|
|
70
|
+
|
|
71
|
+
// First segment is always just itself
|
|
72
|
+
const ids: string[] = [segments[0]];
|
|
73
|
+
|
|
74
|
+
// Calculate cumulative lengths and slice from original string
|
|
75
|
+
// This avoids repeated string concatenation (O(k²) → O(k))
|
|
76
|
+
let cumulativeLength = segments[0].length;
|
|
77
|
+
|
|
78
|
+
for (let i = 1; i < segmentCount - 1; i++) {
|
|
79
|
+
cumulativeLength += 1 + segments[i].length; // +1 for dot separator
|
|
80
|
+
ids.push(name.slice(0, cumulativeLength));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Last segment is always the full route name
|
|
84
|
+
ids.push(name);
|
|
85
|
+
|
|
86
|
+
return ids;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isPrimitive(value: unknown): value is PrimitiveParam {
|
|
90
|
+
const type = typeof value;
|
|
91
|
+
|
|
92
|
+
return type === "string" || type === "number" || type === "boolean";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Compares segment parameters between two states without creating intermediate objects.
|
|
97
|
+
* Returns true if all primitive params for the given segment are equal in both states.
|
|
98
|
+
*/
|
|
99
|
+
function segmentParamsEqual(
|
|
100
|
+
name: string,
|
|
101
|
+
toMetaParams: Record<string, unknown>,
|
|
102
|
+
toState: State,
|
|
103
|
+
fromState: State,
|
|
104
|
+
): boolean {
|
|
105
|
+
const keys = toMetaParams[name];
|
|
106
|
+
|
|
107
|
+
if (!keys || typeof keys !== "object") {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const key of Object.keys(keys)) {
|
|
112
|
+
const toVal = toState.params[key];
|
|
113
|
+
const fromVal = fromState.params[key];
|
|
114
|
+
|
|
115
|
+
if (
|
|
116
|
+
isPrimitive(toVal) &&
|
|
117
|
+
isPrimitive(fromVal) &&
|
|
118
|
+
String(toVal) !== String(fromVal)
|
|
119
|
+
) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Finds the point where two state paths diverge based on segments and parameters.
|
|
129
|
+
* Compares both segment names and their parameters to find the first difference.
|
|
130
|
+
*
|
|
131
|
+
* @param toMetaParams - Cached meta.params from toState (avoids per-segment WeakMap lookup)
|
|
132
|
+
* @param toState - Target state
|
|
133
|
+
* @param fromState - Source state
|
|
134
|
+
* @param toStateIds - Segment IDs for target state
|
|
135
|
+
* @param fromStateIds - Segment IDs for source state
|
|
136
|
+
* @param maxI - Maximum index to check (minimum of both arrays)
|
|
137
|
+
* @returns Index of first difference, or maxI if all checked segments match
|
|
138
|
+
*/
|
|
139
|
+
function pointOfDifference(
|
|
140
|
+
toMetaParams: Record<string, unknown>,
|
|
141
|
+
toState: State,
|
|
142
|
+
fromState: State,
|
|
143
|
+
toStateIds: string[],
|
|
144
|
+
fromStateIds: string[],
|
|
145
|
+
maxI: number,
|
|
146
|
+
): number {
|
|
147
|
+
for (let i = 0; i < maxI; i++) {
|
|
148
|
+
const toSegment = toStateIds[i];
|
|
149
|
+
const fromSegment = fromStateIds[i];
|
|
150
|
+
|
|
151
|
+
// Different segment names - immediate difference
|
|
152
|
+
if (toSegment !== fromSegment) {
|
|
153
|
+
return i;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!segmentParamsEqual(toSegment, toMetaParams, toState, fromState)) {
|
|
157
|
+
return i;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return maxI;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Converts a route name to an array of hierarchical segment identifiers.
|
|
166
|
+
* Each segment ID includes all parent segments in the path.
|
|
167
|
+
*
|
|
168
|
+
* @param name - Route name in dot notation (e.g., 'users.profile.edit')
|
|
169
|
+
* @returns Array of cumulative segment IDs
|
|
170
|
+
* @throws {Error} If route depth exceeds maximum allowed depth
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* // Simple route
|
|
174
|
+
* nameToIDs('users');
|
|
175
|
+
* // Returns: ['users']
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* // Nested route
|
|
179
|
+
* nameToIDs('users.profile.edit');
|
|
180
|
+
* // Returns: ['users', 'users.profile', 'users.profile.edit']
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* // Empty string (root route)
|
|
184
|
+
* nameToIDs('');
|
|
185
|
+
* // Returns: ['']
|
|
186
|
+
*
|
|
187
|
+
* @remarks
|
|
188
|
+
* Input parameter is NOT validated in this function for performance reasons.
|
|
189
|
+
* Validation significantly slows down nameToIDs execution.
|
|
190
|
+
* The input should be validated by the function/method that calls nameToIDs.
|
|
191
|
+
*/
|
|
192
|
+
const nameToIDsCache = new Map<string, string[]>();
|
|
193
|
+
|
|
194
|
+
export function nameToIDs(name: string): string[] {
|
|
195
|
+
const cached = nameToIDsCache.get(name);
|
|
196
|
+
|
|
197
|
+
if (cached) {
|
|
198
|
+
return cached;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const result = computeNameToIDs(name);
|
|
202
|
+
|
|
203
|
+
Object.freeze(result);
|
|
204
|
+
nameToIDsCache.set(name, result);
|
|
205
|
+
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function computeNameToIDs(name: string): string[] {
|
|
210
|
+
if (!name) {
|
|
211
|
+
return [DEFAULT_ROUTE_NAME];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const firstDot = name.indexOf(ROUTE_SEGMENT_SEPARATOR);
|
|
215
|
+
|
|
216
|
+
if (firstDot === -1) {
|
|
217
|
+
return [name];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const secondDot = name.indexOf(ROUTE_SEGMENT_SEPARATOR, firstDot + 1);
|
|
221
|
+
|
|
222
|
+
if (secondDot === -1) {
|
|
223
|
+
return [name.slice(0, firstDot), name];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const thirdDot = name.indexOf(ROUTE_SEGMENT_SEPARATOR, secondDot + 1);
|
|
227
|
+
|
|
228
|
+
if (thirdDot === -1) {
|
|
229
|
+
return [name.slice(0, firstDot), name.slice(0, secondDot), name];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const fourthDot = name.indexOf(ROUTE_SEGMENT_SEPARATOR, thirdDot + 1);
|
|
233
|
+
|
|
234
|
+
if (fourthDot === -1) {
|
|
235
|
+
return [
|
|
236
|
+
name.slice(0, firstDot),
|
|
237
|
+
name.slice(0, secondDot),
|
|
238
|
+
name.slice(0, thirdDot),
|
|
239
|
+
name,
|
|
240
|
+
];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return nameToIDsGeneral(name);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Calculates the transition path between two router states.
|
|
248
|
+
* Determines which route segments need to be deactivated and activated
|
|
249
|
+
* to transition from one state to another.
|
|
250
|
+
*
|
|
251
|
+
* @param toState - Target state to transition to
|
|
252
|
+
* @param fromState - Current state to transition from (optional)
|
|
253
|
+
* @returns Transition path with intersection and segments to activate/deactivate
|
|
254
|
+
*
|
|
255
|
+
* @throws {TypeError} When toState is null or undefined
|
|
256
|
+
* @throws {TypeError} When toState is not an object
|
|
257
|
+
* @throws {TypeError} When toState.name is missing or not a string
|
|
258
|
+
* @throws {TypeError} When toState.params is missing or not an object
|
|
259
|
+
* @throws {TypeError} When toState.path is missing or not a string
|
|
260
|
+
* @throws {TypeError} When toState.name contains invalid route format:
|
|
261
|
+
* - Contains only whitespace (e.g., " ")
|
|
262
|
+
* - Has consecutive dots (e.g., "users..profile")
|
|
263
|
+
* - Has leading/trailing dots (e.g., ".users" or "users.")
|
|
264
|
+
* - Segments don't match pattern [a-zA-Z_][a-zA-Z0-9_-]* (e.g., "users.123")
|
|
265
|
+
* - Contains spaces or special characters (e.g., "users profile")
|
|
266
|
+
* - Exceeds maximum length (8192 characters)
|
|
267
|
+
* @throws {TypeError} When fromState is provided and has any of the validation errors listed above for toState
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* // ✅ Valid calls
|
|
271
|
+
* getTransitionPath({ name: 'users.profile', params: {}, path: '/users/profile' });
|
|
272
|
+
* getTransitionPath(toState, fromState);
|
|
273
|
+
* getTransitionPath({ name: '', params: {}, path: '/' }); // root route
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* // ❌ Invalid calls that throw TypeError
|
|
277
|
+
* getTransitionPath(null); // toState is null
|
|
278
|
+
* getTransitionPath(undefined); // toState is undefined
|
|
279
|
+
* getTransitionPath({}); // missing required fields
|
|
280
|
+
* getTransitionPath({ name: 123, params: {}, path: '/' }); // name not a string
|
|
281
|
+
* getTransitionPath({ name: 'home', path: '/' }); // missing params
|
|
282
|
+
* getTransitionPath({ name: 'users..profile', params: {}, path: '/' }); // consecutive dots
|
|
283
|
+
* getTransitionPath({ name: '.users', params: {}, path: '/' }); // leading dot
|
|
284
|
+
* getTransitionPath({ name: 'users.', params: {}, path: '/' }); // trailing dot
|
|
285
|
+
* getTransitionPath({ name: 'users profile', params: {}, path: '/' }); // contains space
|
|
286
|
+
* getTransitionPath({ name: 'users.123', params: {}, path: '/' }); // segment starts with number
|
|
287
|
+
* getTransitionPath(validToState, { name: 'invalid..route', params: {}, path: '/' }); // fromState invalid
|
|
288
|
+
*
|
|
289
|
+
* @example
|
|
290
|
+
* // Full activation (no fromState)
|
|
291
|
+
* getTransitionPath(makeState('users.profile'));
|
|
292
|
+
* // Returns: {
|
|
293
|
+
* // intersection: '',
|
|
294
|
+
* // toActivate: ['users', 'users.profile'],
|
|
295
|
+
* // toDeactivate: []
|
|
296
|
+
* // }
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* // Partial transition with common ancestor
|
|
300
|
+
* getTransitionPath(
|
|
301
|
+
* makeState('users.profile'),
|
|
302
|
+
* makeState('users.list')
|
|
303
|
+
* );
|
|
304
|
+
* // Returns: {
|
|
305
|
+
* // intersection: 'users',
|
|
306
|
+
* // toActivate: ['users.profile'],
|
|
307
|
+
* // toDeactivate: ['users.list']
|
|
308
|
+
* // }
|
|
309
|
+
*
|
|
310
|
+
* @example
|
|
311
|
+
* // Complete route change
|
|
312
|
+
* getTransitionPath(
|
|
313
|
+
* makeState('admin.dashboard'),
|
|
314
|
+
* makeState('users.profile')
|
|
315
|
+
* );
|
|
316
|
+
* // Returns: {
|
|
317
|
+
* // intersection: '',
|
|
318
|
+
* // toActivate: ['admin', 'admin.dashboard'],
|
|
319
|
+
* // toDeactivate: ['users.profile', 'users']
|
|
320
|
+
* // }
|
|
321
|
+
*/
|
|
322
|
+
// Single-entry cache: shouldUpdateNode calls getTransitionPath N times per
|
|
323
|
+
// navigation with the same state objects (once per subscribed node).
|
|
324
|
+
// Cache by reference eliminates N-1 redundant computations.
|
|
325
|
+
let cached1To: State | undefined;
|
|
326
|
+
let cached1From: State | undefined;
|
|
327
|
+
let cached1Result: TransitionPath | null = null;
|
|
328
|
+
|
|
329
|
+
let cached2To: State | undefined;
|
|
330
|
+
let cached2From: State | undefined;
|
|
331
|
+
let cached2Result: TransitionPath | null = null;
|
|
332
|
+
|
|
333
|
+
function computeTransitionPath(
|
|
334
|
+
toState: State,
|
|
335
|
+
fromState?: State,
|
|
336
|
+
): TransitionPath {
|
|
337
|
+
// ===== FAST PATH 1: Initial navigation (no fromState) =====
|
|
338
|
+
// This is the best performing case in benchmarks (5M ops/sec)
|
|
339
|
+
if (!fromState) {
|
|
340
|
+
return {
|
|
341
|
+
intersection: EMPTY_INTERSECTION,
|
|
342
|
+
toActivate: nameToIDs(toState.name),
|
|
343
|
+
toDeactivate: FROZEN_EMPTY_ARRAY,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ===== FAST PATH 3: Missing meta requires full reload =====
|
|
348
|
+
// Single WeakMap lookup per state, reused in pointOfDifference/segmentParamsEqual
|
|
349
|
+
const toMetaParams = getStateMetaParams(toState);
|
|
350
|
+
const fromMetaParams = getStateMetaParams(fromState);
|
|
351
|
+
|
|
352
|
+
if (!toMetaParams && !fromMetaParams) {
|
|
353
|
+
return {
|
|
354
|
+
intersection: EMPTY_INTERSECTION,
|
|
355
|
+
toActivate: nameToIDs(toState.name),
|
|
356
|
+
toDeactivate: reverseArray(nameToIDs(fromState.name)),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ===== STANDARD PATH: Routes with parameters =====
|
|
361
|
+
const toStateIds = nameToIDs(toState.name);
|
|
362
|
+
const fromStateIds = nameToIDs(fromState.name);
|
|
363
|
+
const maxI = Math.min(fromStateIds.length, toStateIds.length);
|
|
364
|
+
|
|
365
|
+
const i = pointOfDifference(
|
|
366
|
+
(toMetaParams ?? fromMetaParams) as Record<string, unknown>,
|
|
367
|
+
toState,
|
|
368
|
+
fromState,
|
|
369
|
+
toStateIds,
|
|
370
|
+
fromStateIds,
|
|
371
|
+
maxI,
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
// Optimization: Build deactivation list in reverse order directly
|
|
375
|
+
// instead of slice(i).toReversed() which creates 2 arrays
|
|
376
|
+
let toDeactivate: string[];
|
|
377
|
+
|
|
378
|
+
if (i >= fromStateIds.length) {
|
|
379
|
+
toDeactivate = FROZEN_EMPTY_ARRAY;
|
|
380
|
+
} else if (i === 0 && fromStateIds.length === 1) {
|
|
381
|
+
// Single-segment route: reversed = original, reuse cached frozen array
|
|
382
|
+
toDeactivate = fromStateIds;
|
|
383
|
+
} else {
|
|
384
|
+
toDeactivate = [];
|
|
385
|
+
|
|
386
|
+
for (let j = fromStateIds.length - 1; j >= i; j--) {
|
|
387
|
+
toDeactivate.push(fromStateIds[j]);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Build activation list — reuse cached frozen array when using full list
|
|
392
|
+
const toActivate = i === 0 ? toStateIds : toStateIds.slice(i);
|
|
393
|
+
|
|
394
|
+
// Determine intersection point (common ancestor)
|
|
395
|
+
const intersection = i > 0 ? fromStateIds[i - 1] : EMPTY_INTERSECTION;
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
intersection,
|
|
399
|
+
toDeactivate,
|
|
400
|
+
toActivate,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function getTransitionPath(
|
|
405
|
+
toState: State,
|
|
406
|
+
fromState?: State,
|
|
407
|
+
): TransitionPath {
|
|
408
|
+
if (
|
|
409
|
+
cached1Result !== null &&
|
|
410
|
+
toState === cached1To &&
|
|
411
|
+
fromState === cached1From
|
|
412
|
+
) {
|
|
413
|
+
return cached1Result;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/* v8 ignore next 6 -- @preserve: 2nd cache slot hit path exercised by alternating navigation benchmarks, not unit tests */
|
|
417
|
+
if (
|
|
418
|
+
cached2Result !== null &&
|
|
419
|
+
toState === cached2To &&
|
|
420
|
+
fromState === cached2From
|
|
421
|
+
) {
|
|
422
|
+
return cached2Result;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const result = computeTransitionPath(toState, fromState);
|
|
426
|
+
|
|
427
|
+
cached2To = cached1To;
|
|
428
|
+
cached2From = cached1From;
|
|
429
|
+
cached2Result = cached1Result;
|
|
430
|
+
|
|
431
|
+
cached1To = toState;
|
|
432
|
+
cached1From = fromState;
|
|
433
|
+
cached1Result = result;
|
|
434
|
+
|
|
435
|
+
return result;
|
|
436
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// packages/core/src/typeGuards.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RealRouter-specific type guards for logger configuration
|
|
5
|
+
*/
|
|
6
|
+
import type { LoggerConfig, LogLevelConfig } from "@real-router/logger";
|
|
7
|
+
|
|
8
|
+
const VALID_LEVELS_SET = new Set<string>(["all", "warn-error", "error-only"]);
|
|
9
|
+
|
|
10
|
+
function isValidLevel(value: unknown): value is LogLevelConfig {
|
|
11
|
+
return typeof value === "string" && VALID_LEVELS_SET.has(value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function formatValue(value: unknown): string {
|
|
15
|
+
if (typeof value === "string") {
|
|
16
|
+
return `"${value}"`;
|
|
17
|
+
}
|
|
18
|
+
if (typeof value === "object") {
|
|
19
|
+
return JSON.stringify(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
23
|
+
return String(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isLoggerConfig(config: unknown): config is LoggerConfig {
|
|
27
|
+
if (typeof config !== "object" || config === null) {
|
|
28
|
+
throw new TypeError("Logger config must be an object");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const obj = config;
|
|
32
|
+
|
|
33
|
+
// Check for unknown properties
|
|
34
|
+
for (const key of Object.keys(obj)) {
|
|
35
|
+
if (key !== "level" && key !== "callback") {
|
|
36
|
+
throw new TypeError(`Unknown logger config property: "${key}"`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Validate level if present
|
|
41
|
+
if ("level" in obj && obj.level !== undefined && !isValidLevel(obj.level)) {
|
|
42
|
+
throw new TypeError(
|
|
43
|
+
`Invalid logger level: ${formatValue(obj.level)}. Expected: "all" | "warn-error" | "error-only"`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Validate callback if present
|
|
48
|
+
if (
|
|
49
|
+
"callback" in obj &&
|
|
50
|
+
obj.callback !== undefined &&
|
|
51
|
+
typeof obj.callback !== "function"
|
|
52
|
+
) {
|
|
53
|
+
throw new TypeError(
|
|
54
|
+
`Logger callback must be a function, got ${typeof obj.callback}`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return true;
|
|
59
|
+
}
|