@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.
Files changed (87) hide show
  1. package/dist/cjs/Router-Dh1xgFLI.d.ts.map +1 -0
  2. package/dist/cjs/RouterValidator-TUi8eT8Q.d.ts.map +1 -0
  3. package/dist/cjs/api.d.ts.map +1 -0
  4. package/dist/cjs/index.d.ts.map +1 -0
  5. package/dist/cjs/utils.d.ts.map +1 -0
  6. package/dist/cjs/validation.d.ts.map +1 -0
  7. package/dist/esm/Router-BPkXwb1J.d.mts.map +1 -0
  8. package/dist/esm/RouterValidator-DphcVMEp.d.mts.map +1 -0
  9. package/dist/esm/api.d.mts.map +1 -0
  10. package/dist/esm/index.d.mts.map +1 -0
  11. package/dist/esm/utils.d.mts.map +1 -0
  12. package/dist/esm/validation.d.mts.map +1 -0
  13. package/package.json +9 -12
  14. package/src/Router.ts +684 -0
  15. package/src/RouterError.ts +324 -0
  16. package/src/api/cloneRouter.ts +77 -0
  17. package/src/api/getDependenciesApi.ts +168 -0
  18. package/src/api/getLifecycleApi.ts +65 -0
  19. package/src/api/getPluginApi.ts +167 -0
  20. package/src/api/getRoutesApi.ts +573 -0
  21. package/src/api/helpers.ts +10 -0
  22. package/src/api/index.ts +16 -0
  23. package/src/api/types.ts +12 -0
  24. package/src/constants.ts +87 -0
  25. package/src/createRouter.ts +32 -0
  26. package/src/fsm/index.ts +5 -0
  27. package/src/fsm/routerFSM.ts +120 -0
  28. package/src/getNavigator.ts +30 -0
  29. package/src/guards.ts +46 -0
  30. package/src/helpers.ts +179 -0
  31. package/src/index.ts +50 -0
  32. package/src/internals.ts +173 -0
  33. package/src/namespaces/DependenciesNamespace/dependenciesStore.ts +30 -0
  34. package/src/namespaces/DependenciesNamespace/index.ts +5 -0
  35. package/src/namespaces/EventBusNamespace/EventBusNamespace.ts +311 -0
  36. package/src/namespaces/EventBusNamespace/index.ts +5 -0
  37. package/src/namespaces/EventBusNamespace/types.ts +11 -0
  38. package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +405 -0
  39. package/src/namespaces/NavigationNamespace/constants.ts +55 -0
  40. package/src/namespaces/NavigationNamespace/index.ts +5 -0
  41. package/src/namespaces/NavigationNamespace/transition/completeTransition.ts +100 -0
  42. package/src/namespaces/NavigationNamespace/transition/errorHandling.ts +124 -0
  43. package/src/namespaces/NavigationNamespace/transition/guardPhase.ts +221 -0
  44. package/src/namespaces/NavigationNamespace/types.ts +100 -0
  45. package/src/namespaces/OptionsNamespace/OptionsNamespace.ts +28 -0
  46. package/src/namespaces/OptionsNamespace/constants.ts +19 -0
  47. package/src/namespaces/OptionsNamespace/helpers.ts +50 -0
  48. package/src/namespaces/OptionsNamespace/index.ts +7 -0
  49. package/src/namespaces/OptionsNamespace/validators.ts +13 -0
  50. package/src/namespaces/PluginsNamespace/PluginsNamespace.ts +291 -0
  51. package/src/namespaces/PluginsNamespace/constants.ts +34 -0
  52. package/src/namespaces/PluginsNamespace/index.ts +7 -0
  53. package/src/namespaces/PluginsNamespace/types.ts +22 -0
  54. package/src/namespaces/PluginsNamespace/validators.ts +28 -0
  55. package/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts +377 -0
  56. package/src/namespaces/RouteLifecycleNamespace/index.ts +5 -0
  57. package/src/namespaces/RouteLifecycleNamespace/types.ts +10 -0
  58. package/src/namespaces/RouterLifecycleNamespace/RouterLifecycleNamespace.ts +81 -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 +26 -0
  62. package/src/namespaces/RoutesNamespace/RoutesNamespace.ts +535 -0
  63. package/src/namespaces/RoutesNamespace/constants.ts +6 -0
  64. package/src/namespaces/RoutesNamespace/forwardChain.ts +34 -0
  65. package/src/namespaces/RoutesNamespace/helpers.ts +126 -0
  66. package/src/namespaces/RoutesNamespace/index.ts +11 -0
  67. package/src/namespaces/RoutesNamespace/routeGuards.ts +62 -0
  68. package/src/namespaces/RoutesNamespace/routesStore.ts +346 -0
  69. package/src/namespaces/RoutesNamespace/types.ts +81 -0
  70. package/src/namespaces/StateNamespace/StateNamespace.ts +211 -0
  71. package/src/namespaces/StateNamespace/helpers.ts +24 -0
  72. package/src/namespaces/StateNamespace/index.ts +5 -0
  73. package/src/namespaces/StateNamespace/types.ts +15 -0
  74. package/src/namespaces/index.ts +35 -0
  75. package/src/stateMetaStore.ts +15 -0
  76. package/src/transitionPath.ts +436 -0
  77. package/src/typeGuards.ts +59 -0
  78. package/src/types/RouterValidator.ts +154 -0
  79. package/src/types.ts +69 -0
  80. package/src/utils/getStaticPaths.ts +50 -0
  81. package/src/utils/index.ts +5 -0
  82. package/src/utils/serializeState.ts +22 -0
  83. package/src/validation.ts +12 -0
  84. package/src/wiring/RouterWiringBuilder.ts +261 -0
  85. package/src/wiring/index.ts +7 -0
  86. package/src/wiring/types.ts +47 -0
  87. 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,5 @@
1
+ // packages/core/src/namespaces/StateNamespace/index.ts
2
+
3
+ export { StateNamespace } from "./StateNamespace";
4
+
5
+ export type { StateNamespaceDependencies } from "./types";
@@ -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
+ }