@real-router/core 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +1 -3
  2. package/dist/cjs/index.d.ts +1 -1
  3. package/dist/cjs/index.js +1 -1
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/cjs/metafile-cjs.json +1 -1
  6. package/dist/esm/index.d.mts +1 -1
  7. package/dist/esm/index.mjs +1 -1
  8. package/dist/esm/index.mjs.map +1 -1
  9. package/dist/esm/metafile-esm.json +1 -1
  10. package/package.json +7 -5
  11. package/src/Router.ts +1174 -0
  12. package/src/RouterError.ts +324 -0
  13. package/src/constants.ts +112 -0
  14. package/src/createRouter.ts +32 -0
  15. package/src/fsm/index.ts +5 -0
  16. package/src/fsm/routerFSM.ts +129 -0
  17. package/src/getNavigator.ts +15 -0
  18. package/src/helpers.ts +194 -0
  19. package/src/index.ts +46 -0
  20. package/src/namespaces/CloneNamespace/CloneNamespace.ts +120 -0
  21. package/src/namespaces/CloneNamespace/index.ts +3 -0
  22. package/src/namespaces/CloneNamespace/types.ts +46 -0
  23. package/src/namespaces/DependenciesNamespace/DependenciesNamespace.ts +250 -0
  24. package/src/namespaces/DependenciesNamespace/index.ts +3 -0
  25. package/src/namespaces/DependenciesNamespace/validators.ts +105 -0
  26. package/src/namespaces/EventBusNamespace/EventBusNamespace.ts +272 -0
  27. package/src/namespaces/EventBusNamespace/index.ts +5 -0
  28. package/src/namespaces/EventBusNamespace/types.ts +11 -0
  29. package/src/namespaces/MiddlewareNamespace/MiddlewareNamespace.ts +206 -0
  30. package/src/namespaces/MiddlewareNamespace/index.ts +5 -0
  31. package/src/namespaces/MiddlewareNamespace/types.ts +28 -0
  32. package/src/namespaces/MiddlewareNamespace/validators.ts +96 -0
  33. package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +308 -0
  34. package/src/namespaces/NavigationNamespace/index.ts +5 -0
  35. package/src/namespaces/NavigationNamespace/transition/executeLifecycleHooks.ts +84 -0
  36. package/src/namespaces/NavigationNamespace/transition/executeMiddleware.ts +56 -0
  37. package/src/namespaces/NavigationNamespace/transition/index.ts +107 -0
  38. package/src/namespaces/NavigationNamespace/transition/makeError.ts +37 -0
  39. package/src/namespaces/NavigationNamespace/transition/mergeStates.ts +54 -0
  40. package/src/namespaces/NavigationNamespace/transition/processLifecycleResult.ts +81 -0
  41. package/src/namespaces/NavigationNamespace/transition/wrapSyncError.ts +82 -0
  42. package/src/namespaces/NavigationNamespace/types.ts +129 -0
  43. package/src/namespaces/NavigationNamespace/validators.ts +87 -0
  44. package/src/namespaces/OptionsNamespace/OptionsNamespace.ts +50 -0
  45. package/src/namespaces/OptionsNamespace/constants.ts +41 -0
  46. package/src/namespaces/OptionsNamespace/helpers.ts +51 -0
  47. package/src/namespaces/OptionsNamespace/index.ts +11 -0
  48. package/src/namespaces/OptionsNamespace/validators.ts +252 -0
  49. package/src/namespaces/PluginsNamespace/PluginsNamespace.ts +325 -0
  50. package/src/namespaces/PluginsNamespace/constants.ts +35 -0
  51. package/src/namespaces/PluginsNamespace/index.ts +7 -0
  52. package/src/namespaces/PluginsNamespace/types.ts +32 -0
  53. package/src/namespaces/PluginsNamespace/validators.ts +79 -0
  54. package/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts +389 -0
  55. package/src/namespaces/RouteLifecycleNamespace/index.ts +5 -0
  56. package/src/namespaces/RouteLifecycleNamespace/types.ts +17 -0
  57. package/src/namespaces/RouteLifecycleNamespace/validators.ts +65 -0
  58. package/src/namespaces/RouterLifecycleNamespace/RouterLifecycleNamespace.ts +140 -0
  59. package/src/namespaces/RouterLifecycleNamespace/constants.ts +25 -0
  60. package/src/namespaces/RouterLifecycleNamespace/index.ts +5 -0
  61. package/src/namespaces/RouterLifecycleNamespace/types.ts +23 -0
  62. package/src/namespaces/RoutesNamespace/RoutesNamespace.ts +1482 -0
  63. package/src/namespaces/RoutesNamespace/constants.ts +14 -0
  64. package/src/namespaces/RoutesNamespace/helpers.ts +532 -0
  65. package/src/namespaces/RoutesNamespace/index.ts +9 -0
  66. package/src/namespaces/RoutesNamespace/stateBuilder.ts +70 -0
  67. package/src/namespaces/RoutesNamespace/types.ts +82 -0
  68. package/src/namespaces/RoutesNamespace/validators.ts +331 -0
  69. package/src/namespaces/StateNamespace/StateNamespace.ts +317 -0
  70. package/src/namespaces/StateNamespace/helpers.ts +43 -0
  71. package/src/namespaces/StateNamespace/index.ts +5 -0
  72. package/src/namespaces/StateNamespace/types.ts +15 -0
  73. package/src/namespaces/index.ts +42 -0
  74. package/src/transitionPath.ts +441 -0
  75. package/src/typeGuards.ts +74 -0
  76. package/src/types.ts +194 -0
  77. package/src/wiring/RouterWiringBuilder.ts +235 -0
  78. package/src/wiring/index.ts +7 -0
  79. package/src/wiring/types.ts +53 -0
  80. package/src/wiring/wireRouter.ts +29 -0
@@ -0,0 +1,96 @@
1
+ // packages/core/src/namespaces/MiddlewareNamespace/validators.ts
2
+
3
+ /**
4
+ * Static validation functions for MiddlewareNamespace.
5
+ * Called by Router facade before instance methods.
6
+ */
7
+
8
+ import { getTypeDescription } from "type-guards";
9
+
10
+ import { DEFAULT_LIMITS } from "../../constants";
11
+
12
+ import type { MiddlewareFactory } from "../../types";
13
+ import type { DefaultDependencies, Middleware } from "@real-router/types";
14
+
15
+ /**
16
+ * Gets a displayable name for a factory function.
17
+ */
18
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
19
+ function getFactoryName(factory: Function): string {
20
+ return factory.name || "anonymous";
21
+ }
22
+
23
+ /**
24
+ * Validates useMiddleware arguments - each must be a function.
25
+ */
26
+ export function validateUseMiddlewareArgs<D extends DefaultDependencies>(
27
+ middlewares: unknown[],
28
+ ): asserts middlewares is MiddlewareFactory<D>[] {
29
+ for (const [i, middleware] of middlewares.entries()) {
30
+ if (typeof middleware !== "function") {
31
+ throw new TypeError(
32
+ `[router.useMiddleware] Expected middleware factory function at index ${i}, ` +
33
+ `got ${getTypeDescription(middleware)}`,
34
+ );
35
+ }
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Validates that a middleware factory returned a valid middleware function.
41
+ */
42
+ export function validateMiddleware<D extends DefaultDependencies>(
43
+ middleware: unknown,
44
+ factory: MiddlewareFactory<D>,
45
+ ): asserts middleware is Middleware {
46
+ if (typeof middleware !== "function") {
47
+ throw new TypeError(
48
+ `[router.useMiddleware] Middleware factory must return a function, ` +
49
+ `got ${getTypeDescription(middleware)}. ` +
50
+ `Factory: ${getFactoryName(factory)}`,
51
+ );
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Validates that no duplicate factories are being registered.
57
+ */
58
+ export function validateNoDuplicates<D extends DefaultDependencies>(
59
+ newFactories: MiddlewareFactory<D>[],
60
+ existingFactories: MiddlewareFactory<D>[],
61
+ ): void {
62
+ const existingSet = new Set(existingFactories);
63
+
64
+ for (const factory of newFactories) {
65
+ if (existingSet.has(factory)) {
66
+ throw new Error(
67
+ `[router.useMiddleware] Middleware factory already registered. ` +
68
+ `To re-register, first unsubscribe the existing middleware. ` +
69
+ `Factory: ${getFactoryName(factory)}`,
70
+ );
71
+ }
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Validates that adding middleware won't exceed the hard limit.
77
+ */
78
+ export function validateMiddlewareLimit(
79
+ currentCount: number,
80
+ newCount: number,
81
+ maxMiddleware: number = DEFAULT_LIMITS.maxMiddleware,
82
+ ): void {
83
+ if (maxMiddleware === 0) {
84
+ return;
85
+ }
86
+
87
+ const totalSize = currentCount + newCount;
88
+
89
+ if (totalSize > maxMiddleware) {
90
+ throw new Error(
91
+ `[router.useMiddleware] Middleware limit exceeded (${maxMiddleware}). ` +
92
+ `Current: ${currentCount}, Attempting to add: ${newCount}. ` +
93
+ `This indicates an architectural problem. Consider consolidating middleware.`,
94
+ );
95
+ }
96
+ }
@@ -0,0 +1,308 @@
1
+ // packages/core/src/namespaces/NavigationNamespace/NavigationNamespace.ts
2
+
3
+ import { logger } from "@real-router/logger";
4
+
5
+ import { transition } from "./transition";
6
+ import {
7
+ validateNavigateArgs,
8
+ validateNavigateToDefaultArgs,
9
+ validateNavigateToStateArgs,
10
+ validateNavigationOptions,
11
+ } from "./validators";
12
+ import { errorCodes, constants } from "../../constants";
13
+ import { RouterError } from "../../RouterError";
14
+ import { resolveOption } from "../OptionsNamespace";
15
+
16
+ import type {
17
+ NavigationDependencies,
18
+ TransitionDependencies,
19
+ TransitionOutput,
20
+ } from "./types";
21
+ import type {
22
+ NavigationOptions,
23
+ Params,
24
+ State,
25
+ TransitionMeta,
26
+ } from "@real-router/types";
27
+
28
+ /**
29
+ * Independent namespace for managing navigation.
30
+ *
31
+ * Handles navigate(), navigateToDefault(), navigateToState(), and transition state.
32
+ */
33
+ export class NavigationNamespace {
34
+ // ═══════════════════════════════════════════════════════════════════════════
35
+ // Functional reference for cyclic dependency
36
+ // ═══════════════════════════════════════════════════════════════════════════
37
+
38
+ // Dependencies injected via setDependencies (replaces full router reference)
39
+ #canNavigate!: () => boolean;
40
+ #deps!: NavigationDependencies;
41
+ #transitionDeps!: TransitionDependencies;
42
+
43
+ // =========================================================================
44
+ // Static validation methods (called by facade before instance methods)
45
+ // Proxy to functions in validators.ts for separation of concerns
46
+ // =========================================================================
47
+
48
+ static validateNavigateArgs(name: unknown): asserts name is string {
49
+ validateNavigateArgs(name);
50
+ }
51
+
52
+ static validateNavigateToStateArgs(
53
+ toState: unknown,
54
+ fromState: unknown,
55
+ opts: unknown,
56
+ ): void {
57
+ validateNavigateToStateArgs(toState, fromState, opts);
58
+ }
59
+
60
+ static validateNavigateToDefaultArgs(opts: unknown): void {
61
+ validateNavigateToDefaultArgs(opts);
62
+ }
63
+
64
+ static validateNavigationOptions(
65
+ opts: unknown,
66
+ methodName: string,
67
+ ): asserts opts is NavigationOptions {
68
+ validateNavigationOptions(opts, methodName);
69
+ }
70
+
71
+ // =========================================================================
72
+ // Dependency injection
73
+ // =========================================================================
74
+
75
+ /**
76
+ * Sets the canNavigate check (cyclic dependency on EventBusNamespace).
77
+ * Must be called before using navigate().
78
+ */
79
+ setCanNavigate(fn: () => boolean): void {
80
+ this.#canNavigate = fn;
81
+ }
82
+
83
+ /**
84
+ * Sets dependencies for navigation operations.
85
+ * Must be called before using navigation methods.
86
+ */
87
+ setDependencies(deps: NavigationDependencies): void {
88
+ this.#deps = deps;
89
+ }
90
+
91
+ /**
92
+ * Sets dependencies for transition operations.
93
+ * Must be called before using navigation methods.
94
+ */
95
+ setTransitionDependencies(deps: TransitionDependencies): void {
96
+ this.#transitionDeps = deps;
97
+ }
98
+
99
+ // =========================================================================
100
+ // Instance methods
101
+ // =========================================================================
102
+
103
+ /**
104
+ * Navigates to a route by name.
105
+ * Arguments should be pre-parsed and validated by facade.
106
+ */
107
+ async navigate(
108
+ name: string,
109
+ params: Params,
110
+ opts: NavigationOptions,
111
+ ): Promise<State> {
112
+ if (!this.#canNavigate()) {
113
+ throw new RouterError(errorCodes.ROUTER_NOT_STARTED);
114
+ }
115
+
116
+ const deps = this.#deps;
117
+
118
+ const result = deps.buildStateWithSegments(name, params);
119
+
120
+ if (!result) {
121
+ const err = new RouterError(errorCodes.ROUTE_NOT_FOUND);
122
+
123
+ deps.emitTransitionError(undefined, deps.getState(), err);
124
+
125
+ throw err;
126
+ }
127
+
128
+ const { state: route } = result;
129
+
130
+ const toState = deps.makeState(
131
+ route.name,
132
+ route.params,
133
+ deps.buildPath(route.name, route.params),
134
+ {
135
+ params: route.meta,
136
+ options: opts,
137
+ },
138
+ );
139
+
140
+ const fromState = deps.getState();
141
+
142
+ if (
143
+ !opts.reload &&
144
+ !opts.force &&
145
+ deps.areStatesEqual(fromState, toState, false)
146
+ ) {
147
+ const err = new RouterError(errorCodes.SAME_STATES);
148
+
149
+ deps.emitTransitionError(toState, fromState, err);
150
+
151
+ throw err;
152
+ }
153
+
154
+ return this.navigateToState(toState, fromState, opts);
155
+ }
156
+
157
+ /**
158
+ * Internal navigation function that accepts pre-built state.
159
+ * Used by RouterLifecycleNamespace for start() transitions.
160
+ */
161
+ async navigateToState(
162
+ toState: State,
163
+ fromState: State | undefined,
164
+ opts: NavigationOptions,
165
+ ): Promise<State> {
166
+ const deps = this.#deps;
167
+ const transitionDeps = this.#transitionDeps;
168
+
169
+ if (transitionDeps.isTransitioning()) {
170
+ logger.warn(
171
+ "router.navigate",
172
+ "Concurrent navigation detected on shared router instance. " +
173
+ "For SSR, use router.clone() to create isolated instance per request.",
174
+ );
175
+ deps.cancelNavigation();
176
+ }
177
+
178
+ deps.startTransition(toState, fromState);
179
+
180
+ try {
181
+ const { state: finalState, meta: transitionOutput } = await transition(
182
+ transitionDeps,
183
+ toState,
184
+ fromState,
185
+ opts,
186
+ );
187
+
188
+ if (
189
+ finalState.name === constants.UNKNOWN_ROUTE ||
190
+ deps.hasRoute(finalState.name)
191
+ ) {
192
+ const stateWithTransition = NavigationNamespace.#buildSuccessState(
193
+ finalState,
194
+ transitionOutput,
195
+ fromState,
196
+ );
197
+
198
+ deps.setState(stateWithTransition);
199
+ deps.sendTransitionDone(stateWithTransition, fromState, opts);
200
+
201
+ return stateWithTransition;
202
+ } else {
203
+ const err = new RouterError(errorCodes.ROUTE_NOT_FOUND, {
204
+ routeName: finalState.name,
205
+ });
206
+
207
+ deps.sendTransitionError(finalState, fromState, err);
208
+
209
+ throw err;
210
+ }
211
+ } catch (error) {
212
+ this.#routeTransitionError(error, toState, fromState);
213
+
214
+ throw error;
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Navigates to the default route if configured.
220
+ * Arguments should be pre-parsed and validated by facade.
221
+ */
222
+ async navigateToDefault(opts: NavigationOptions): Promise<State> {
223
+ const deps = this.#deps;
224
+ const options = deps.getOptions();
225
+
226
+ if (!options.defaultRoute) {
227
+ throw new RouterError(errorCodes.ROUTE_NOT_FOUND, {
228
+ routeName: "defaultRoute not configured",
229
+ });
230
+ }
231
+
232
+ const resolvedRoute = resolveOption(
233
+ options.defaultRoute,
234
+ deps.getDependency,
235
+ );
236
+
237
+ if (!resolvedRoute) {
238
+ throw new RouterError(errorCodes.ROUTE_NOT_FOUND, {
239
+ routeName: "defaultRoute resolved to empty",
240
+ });
241
+ }
242
+
243
+ const resolvedParams = resolveOption(
244
+ options.defaultParams,
245
+ deps.getDependency,
246
+ );
247
+
248
+ return this.navigate(resolvedRoute, resolvedParams, opts);
249
+ }
250
+
251
+ // =========================================================================
252
+ // Private methods
253
+ // =========================================================================
254
+
255
+ /**
256
+ * Builds the final state with frozen TransitionMeta attached.
257
+ */
258
+ static #buildSuccessState(
259
+ finalState: State,
260
+ transitionOutput: TransitionOutput["meta"],
261
+ fromState: State | undefined,
262
+ ): State {
263
+ const transitionMeta: TransitionMeta = {
264
+ phase: transitionOutput.phase,
265
+ ...(fromState?.name !== undefined && { from: fromState.name }),
266
+ reason: "success",
267
+ segments: transitionOutput.segments,
268
+ };
269
+
270
+ Object.freeze(transitionMeta.segments.deactivated);
271
+ Object.freeze(transitionMeta.segments.activated);
272
+ Object.freeze(transitionMeta.segments);
273
+ Object.freeze(transitionMeta);
274
+
275
+ return {
276
+ ...finalState,
277
+ transition: transitionMeta,
278
+ };
279
+ }
280
+
281
+ /**
282
+ * Routes a caught transition error to the correct FSM event.
283
+ */
284
+ #routeTransitionError(
285
+ error: unknown,
286
+ toState: State,
287
+ fromState: State | undefined,
288
+ ): void {
289
+ const routerError = error as RouterError;
290
+
291
+ // Already routed: cancel/stop sent CANCEL, sendTransitionError called in try block
292
+ if (
293
+ routerError.code === errorCodes.TRANSITION_CANCELLED ||
294
+ routerError.code === errorCodes.ROUTE_NOT_FOUND
295
+ ) {
296
+ return;
297
+ }
298
+
299
+ if (
300
+ routerError.code === errorCodes.CANNOT_ACTIVATE ||
301
+ routerError.code === errorCodes.CANNOT_DEACTIVATE
302
+ ) {
303
+ this.#deps.sendTransitionBlocked(toState, fromState, routerError);
304
+ } else {
305
+ this.#deps.sendTransitionError(toState, fromState, routerError);
306
+ }
307
+ }
308
+ }
@@ -0,0 +1,5 @@
1
+ // packages/core/src/namespaces/NavigationNamespace/index.ts
2
+
3
+ export { NavigationNamespace } from "./NavigationNamespace";
4
+
5
+ export type { NavigationDependencies, TransitionDependencies } from "./types";
@@ -0,0 +1,84 @@
1
+ // packages/real-router/modules/transition/executeLifecycleHooks.ts
2
+
3
+ import { logger } from "@real-router/logger";
4
+ import { isState } from "type-guards";
5
+
6
+ import { rethrowAsRouterError } from "./makeError";
7
+ import { mergeStates } from "./mergeStates";
8
+ import { processLifecycleResult } from "./processLifecycleResult";
9
+ import { errorCodes } from "../../../constants";
10
+ import { RouterError } from "../../../RouterError";
11
+
12
+ import type { State, ActivationFn } from "@real-router/types";
13
+
14
+ // Helper: execution of the Lifecycle Hooks group
15
+ export const executeLifecycleHooks = async (
16
+ hooks: Map<string, ActivationFn>,
17
+ toState: State,
18
+ fromState: State | undefined,
19
+ segments: string[],
20
+ errorCode: string,
21
+ isCancelled: () => boolean,
22
+ ): Promise<State> => {
23
+ let currentState = toState;
24
+ const segmentsToProcess = segments.filter((name) => hooks.has(name));
25
+
26
+ if (segmentsToProcess.length === 0) {
27
+ return currentState;
28
+ }
29
+
30
+ for (const segment of segmentsToProcess) {
31
+ if (isCancelled()) {
32
+ throw new RouterError(errorCodes.TRANSITION_CANCELLED);
33
+ }
34
+
35
+ // Safe cast: segmentsToProcess only contains names that exist in hooks (filtered above)
36
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guaranteed by filter
37
+ const hookFn = hooks.get(segment)!;
38
+
39
+ try {
40
+ const result = hookFn(currentState, fromState);
41
+ const newState = await processLifecycleResult(
42
+ result,
43
+ currentState,
44
+ segment,
45
+ );
46
+
47
+ // Optimization: Early return for undefined newState (most common case ~90%+)
48
+ // This avoids isState() call and subsequent checks
49
+ if (newState !== currentState && isState(newState)) {
50
+ // Guards cannot redirect to a different route
51
+ if (newState.name !== currentState.name) {
52
+ throw new RouterError(errorCode, {
53
+ message:
54
+ "Guards cannot redirect to different route. Use middleware.",
55
+ attemptedRedirect: {
56
+ name: newState.name,
57
+ params: newState.params,
58
+ path: newState.path,
59
+ },
60
+ });
61
+ }
62
+
63
+ // Same route - safe to merge (param modifications, meta changes)
64
+ const hasChanged =
65
+ newState.params !== currentState.params ||
66
+ newState.path !== currentState.path;
67
+
68
+ if (hasChanged) {
69
+ logger.error(
70
+ "core:transition",
71
+ "Warning: State mutated during transition",
72
+ { from: currentState, to: newState },
73
+ );
74
+ }
75
+
76
+ currentState = mergeStates(newState, currentState);
77
+ }
78
+ } catch (error: unknown) {
79
+ rethrowAsRouterError(error, errorCode, segment);
80
+ }
81
+ }
82
+
83
+ return currentState;
84
+ };
@@ -0,0 +1,56 @@
1
+ // packages/real-router/modules/transition/executeMiddleware.ts
2
+
3
+ import { logger } from "@real-router/logger";
4
+ import { isState } from "type-guards";
5
+
6
+ import { rethrowAsRouterError } from "./makeError";
7
+ import { mergeStates } from "./mergeStates";
8
+ import { processLifecycleResult } from "./processLifecycleResult";
9
+ import { errorCodes } from "../../../constants";
10
+ import { RouterError } from "../../../RouterError";
11
+
12
+ import type { State, ActivationFn } from "@real-router/types";
13
+
14
+ // Helper: processing middleware
15
+ export const executeMiddleware = async (
16
+ middlewareFunctions: ActivationFn[],
17
+ toState: State,
18
+ fromState: State | undefined,
19
+ isCancelled: () => boolean,
20
+ ): Promise<State> => {
21
+ let currentState = toState;
22
+
23
+ for (const middlewareFn of middlewareFunctions) {
24
+ if (isCancelled()) {
25
+ throw new RouterError(errorCodes.TRANSITION_CANCELLED);
26
+ }
27
+
28
+ try {
29
+ const result = middlewareFn(currentState, fromState);
30
+ const newState = await processLifecycleResult(result, currentState);
31
+
32
+ // Optimization: Early return for undefined newState (most common case ~90%+)
33
+ // This avoids isState() call and subsequent checks
34
+ if (newState !== currentState && isState(newState)) {
35
+ const hasChanged =
36
+ newState.name !== currentState.name ||
37
+ newState.params !== currentState.params ||
38
+ newState.path !== currentState.path;
39
+
40
+ if (hasChanged) {
41
+ logger.error(
42
+ "core:middleware",
43
+ "Warning: State mutated during middleware execution",
44
+ { from: currentState, to: newState },
45
+ );
46
+ }
47
+
48
+ currentState = mergeStates(newState, currentState);
49
+ }
50
+ } catch (error: unknown) {
51
+ rethrowAsRouterError(error, errorCodes.TRANSITION_ERR);
52
+ }
53
+ }
54
+
55
+ return currentState;
56
+ };
@@ -0,0 +1,107 @@
1
+ // packages/real-router/modules/transition/index.ts
2
+
3
+ import { executeLifecycleHooks } from "./executeLifecycleHooks";
4
+ import { executeMiddleware } from "./executeMiddleware";
5
+ import { constants, errorCodes } from "../../../constants";
6
+ import { RouterError } from "../../../RouterError";
7
+ import { getTransitionPath, nameToIDs } from "../../../transitionPath";
8
+
9
+ import type { TransitionDependencies, TransitionOutput } from "../types";
10
+ import type { NavigationOptions, State } from "@real-router/types";
11
+
12
+ export async function transition(
13
+ deps: TransitionDependencies,
14
+ toState: State,
15
+ fromState: State | undefined,
16
+ opts: NavigationOptions,
17
+ ): Promise<TransitionOutput> {
18
+ // We're caching the necessary data
19
+ const [canDeactivateFunctions, canActivateFunctions] =
20
+ deps.getLifecycleFunctions();
21
+ const middlewareFunctions = deps.getMiddlewareFunctions();
22
+ const isUnknownRoute = toState.name === constants.UNKNOWN_ROUTE;
23
+
24
+ // State management functions
25
+ // Issue #36: Check both explicit cancellation AND router shutdown
26
+ const isCancelled = () => !deps.isActive();
27
+
28
+ const { toDeactivate, toActivate, intersection } = getTransitionPath(
29
+ toState,
30
+ fromState,
31
+ );
32
+
33
+ // determine the necessary steps
34
+ const shouldDeactivate =
35
+ fromState && !opts.forceDeactivate && toDeactivate.length > 0;
36
+ const shouldActivate = !isUnknownRoute && toActivate.length > 0;
37
+ const shouldRunMiddleware = middlewareFunctions.length > 0;
38
+
39
+ let currentState = toState;
40
+
41
+ if (shouldDeactivate) {
42
+ currentState = await executeLifecycleHooks(
43
+ canDeactivateFunctions,
44
+ toState,
45
+ fromState,
46
+ toDeactivate,
47
+ errorCodes.CANNOT_DEACTIVATE,
48
+ isCancelled,
49
+ );
50
+ }
51
+
52
+ if (isCancelled()) {
53
+ throw new RouterError(errorCodes.TRANSITION_CANCELLED);
54
+ }
55
+
56
+ if (shouldActivate) {
57
+ currentState = await executeLifecycleHooks(
58
+ canActivateFunctions,
59
+ currentState,
60
+ fromState,
61
+ toActivate,
62
+ errorCodes.CANNOT_ACTIVATE,
63
+ isCancelled,
64
+ );
65
+ }
66
+
67
+ if (isCancelled()) {
68
+ throw new RouterError(errorCodes.TRANSITION_CANCELLED);
69
+ }
70
+
71
+ if (shouldRunMiddleware) {
72
+ currentState = await executeMiddleware(
73
+ middlewareFunctions,
74
+ currentState,
75
+ fromState,
76
+ isCancelled,
77
+ );
78
+ }
79
+
80
+ if (isCancelled()) {
81
+ throw new RouterError(errorCodes.TRANSITION_CANCELLED);
82
+ }
83
+
84
+ // Automatic cleaning of inactive segments
85
+ if (fromState) {
86
+ const activeSegments = nameToIDs(toState.name);
87
+ const previousActiveSegments = nameToIDs(fromState.name);
88
+
89
+ for (const name of previousActiveSegments) {
90
+ if (!activeSegments.includes(name) && canDeactivateFunctions.has(name)) {
91
+ deps.clearCanDeactivate(name);
92
+ }
93
+ }
94
+ }
95
+
96
+ return {
97
+ state: currentState,
98
+ meta: {
99
+ phase: "middleware",
100
+ segments: {
101
+ deactivated: toDeactivate,
102
+ activated: toActivate,
103
+ intersection,
104
+ },
105
+ },
106
+ };
107
+ }
@@ -0,0 +1,37 @@
1
+ // packages/real-router/modules/transition/makeError.ts
2
+
3
+ import { wrapSyncError } from "./wrapSyncError";
4
+ import { RouterError } from "../../../RouterError";
5
+
6
+ // Helper: Creating an error with code
7
+ export const makeError = (
8
+ code: string,
9
+ err?: RouterError,
10
+ ): RouterError | undefined => {
11
+ if (!err) {
12
+ return undefined;
13
+ }
14
+
15
+ err.setCode(code);
16
+
17
+ return err;
18
+ };
19
+
20
+ /**
21
+ * Re-throws a caught error as a RouterError with the given error code.
22
+ * If the error is already a RouterError, sets the code directly.
23
+ * Otherwise wraps it with wrapSyncError metadata.
24
+ */
25
+ export function rethrowAsRouterError(
26
+ error: unknown,
27
+ errorCode: string,
28
+ segment?: string,
29
+ ): never {
30
+ if (error instanceof RouterError) {
31
+ error.setCode(errorCode);
32
+
33
+ throw error;
34
+ }
35
+
36
+ throw new RouterError(errorCode, wrapSyncError(error, segment));
37
+ }