@real-router/core 0.45.0 → 0.45.1
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/package.json +6 -7
- package/src/Router.ts +0 -684
- package/src/RouterError.ts +0 -324
- package/src/api/cloneRouter.ts +0 -77
- package/src/api/getDependenciesApi.ts +0 -168
- package/src/api/getLifecycleApi.ts +0 -65
- package/src/api/getPluginApi.ts +0 -167
- package/src/api/getRoutesApi.ts +0 -573
- package/src/api/helpers.ts +0 -10
- package/src/api/index.ts +0 -16
- package/src/api/types.ts +0 -12
- package/src/constants.ts +0 -87
- package/src/createRouter.ts +0 -32
- package/src/fsm/index.ts +0 -5
- package/src/fsm/routerFSM.ts +0 -120
- package/src/getNavigator.ts +0 -30
- package/src/guards.ts +0 -46
- package/src/helpers.ts +0 -179
- package/src/index.ts +0 -50
- package/src/internals.ts +0 -173
- package/src/namespaces/DependenciesNamespace/dependenciesStore.ts +0 -30
- package/src/namespaces/DependenciesNamespace/index.ts +0 -5
- package/src/namespaces/EventBusNamespace/EventBusNamespace.ts +0 -311
- package/src/namespaces/EventBusNamespace/index.ts +0 -5
- package/src/namespaces/EventBusNamespace/types.ts +0 -11
- package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +0 -405
- package/src/namespaces/NavigationNamespace/constants.ts +0 -55
- package/src/namespaces/NavigationNamespace/index.ts +0 -5
- package/src/namespaces/NavigationNamespace/transition/completeTransition.ts +0 -100
- package/src/namespaces/NavigationNamespace/transition/errorHandling.ts +0 -124
- package/src/namespaces/NavigationNamespace/transition/guardPhase.ts +0 -221
- package/src/namespaces/NavigationNamespace/types.ts +0 -100
- package/src/namespaces/OptionsNamespace/OptionsNamespace.ts +0 -28
- package/src/namespaces/OptionsNamespace/constants.ts +0 -19
- package/src/namespaces/OptionsNamespace/helpers.ts +0 -50
- package/src/namespaces/OptionsNamespace/index.ts +0 -7
- package/src/namespaces/OptionsNamespace/validators.ts +0 -13
- package/src/namespaces/PluginsNamespace/PluginsNamespace.ts +0 -291
- package/src/namespaces/PluginsNamespace/constants.ts +0 -34
- package/src/namespaces/PluginsNamespace/index.ts +0 -7
- package/src/namespaces/PluginsNamespace/types.ts +0 -22
- package/src/namespaces/PluginsNamespace/validators.ts +0 -28
- package/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts +0 -377
- package/src/namespaces/RouteLifecycleNamespace/index.ts +0 -5
- package/src/namespaces/RouteLifecycleNamespace/types.ts +0 -10
- package/src/namespaces/RouterLifecycleNamespace/RouterLifecycleNamespace.ts +0 -81
- package/src/namespaces/RouterLifecycleNamespace/constants.ts +0 -25
- package/src/namespaces/RouterLifecycleNamespace/index.ts +0 -5
- package/src/namespaces/RouterLifecycleNamespace/types.ts +0 -26
- package/src/namespaces/RoutesNamespace/RoutesNamespace.ts +0 -535
- package/src/namespaces/RoutesNamespace/constants.ts +0 -6
- package/src/namespaces/RoutesNamespace/forwardChain.ts +0 -34
- package/src/namespaces/RoutesNamespace/helpers.ts +0 -126
- package/src/namespaces/RoutesNamespace/index.ts +0 -11
- package/src/namespaces/RoutesNamespace/routeGuards.ts +0 -62
- package/src/namespaces/RoutesNamespace/routesStore.ts +0 -346
- package/src/namespaces/RoutesNamespace/types.ts +0 -81
- package/src/namespaces/StateNamespace/StateNamespace.ts +0 -211
- package/src/namespaces/StateNamespace/helpers.ts +0 -24
- package/src/namespaces/StateNamespace/index.ts +0 -5
- package/src/namespaces/StateNamespace/types.ts +0 -15
- package/src/namespaces/index.ts +0 -35
- package/src/stateMetaStore.ts +0 -15
- package/src/transitionPath.ts +0 -436
- package/src/typeGuards.ts +0 -59
- package/src/types/RouterValidator.ts +0 -154
- package/src/types.ts +0 -69
- package/src/utils/getStaticPaths.ts +0 -50
- package/src/utils/index.ts +0 -5
- package/src/utils/serializeState.ts +0 -22
- package/src/validation.ts +0 -12
- package/src/wiring/RouterWiringBuilder.ts +0 -261
- package/src/wiring/index.ts +0 -7
- package/src/wiring/types.ts +0 -47
- package/src/wiring/wireRouter.ts +0 -26
|
@@ -1,405 +0,0 @@
|
|
|
1
|
-
import { logger } from "@real-router/logger";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
CACHED_NOT_STARTED_REJECTION,
|
|
5
|
-
CACHED_ROUTE_NOT_FOUND_ERROR,
|
|
6
|
-
CACHED_ROUTE_NOT_FOUND_REJECTION,
|
|
7
|
-
CACHED_SAME_STATES_ERROR,
|
|
8
|
-
CACHED_SAME_STATES_REJECTION,
|
|
9
|
-
} from "./constants";
|
|
10
|
-
import { completeTransition } from "./transition/completeTransition";
|
|
11
|
-
import { routeTransitionError } from "./transition/errorHandling";
|
|
12
|
-
import { executeGuardPipeline } from "./transition/guardPhase";
|
|
13
|
-
import { errorCodes, constants } from "../../constants";
|
|
14
|
-
import { RouterError } from "../../RouterError";
|
|
15
|
-
import { getTransitionPath, nameToIDs } from "../../transitionPath";
|
|
16
|
-
|
|
17
|
-
import type { NavigationContext, NavigationDependencies } from "./types";
|
|
18
|
-
import type {
|
|
19
|
-
NavigationOptions,
|
|
20
|
-
Params,
|
|
21
|
-
State,
|
|
22
|
-
TransitionMeta,
|
|
23
|
-
} from "@real-router/types";
|
|
24
|
-
|
|
25
|
-
const FROZEN_ACTIVATED: string[] = [constants.UNKNOWN_ROUTE];
|
|
26
|
-
|
|
27
|
-
Object.freeze(FROZEN_ACTIVATED);
|
|
28
|
-
const FROZEN_REPLACE_OPTS: NavigationOptions = { replace: true };
|
|
29
|
-
|
|
30
|
-
Object.freeze(FROZEN_REPLACE_OPTS);
|
|
31
|
-
|
|
32
|
-
function forceReplaceFromUnknown(
|
|
33
|
-
opts: NavigationOptions,
|
|
34
|
-
fromState: State | undefined,
|
|
35
|
-
): NavigationOptions {
|
|
36
|
-
return fromState?.name === constants.UNKNOWN_ROUTE && !opts.replace
|
|
37
|
-
? { ...opts, replace: true }
|
|
38
|
-
: opts;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function isSameNavigation(
|
|
42
|
-
fromState: State | undefined,
|
|
43
|
-
opts: NavigationOptions,
|
|
44
|
-
toState: State,
|
|
45
|
-
): boolean {
|
|
46
|
-
return (
|
|
47
|
-
!!fromState &&
|
|
48
|
-
!opts.reload &&
|
|
49
|
-
!opts.force &&
|
|
50
|
-
fromState.path === toState.path
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Independent namespace for managing navigation.
|
|
56
|
-
*
|
|
57
|
-
* Handles navigate(), navigateToDefault(), navigateToNotFound(), and transition state.
|
|
58
|
-
*
|
|
59
|
-
* Performance: navigate() uses optimistic sync execution — guards run synchronously
|
|
60
|
-
* until one returns a Promise, then switches to async. This eliminates Promise/AbortController
|
|
61
|
-
* overhead for the common case (no guards or sync guards).
|
|
62
|
-
*/
|
|
63
|
-
export class NavigationNamespace {
|
|
64
|
-
lastSyncResolved = false;
|
|
65
|
-
lastSyncRejected = false;
|
|
66
|
-
#deps!: NavigationDependencies;
|
|
67
|
-
#currentController: AbortController | null = null;
|
|
68
|
-
#navigationId = 0;
|
|
69
|
-
|
|
70
|
-
// =========================================================================
|
|
71
|
-
// Dependency injection
|
|
72
|
-
// =========================================================================
|
|
73
|
-
|
|
74
|
-
setDependencies(deps: NavigationDependencies): void {
|
|
75
|
-
this.#deps = deps;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// =========================================================================
|
|
79
|
-
// Instance methods
|
|
80
|
-
// =========================================================================
|
|
81
|
-
|
|
82
|
-
navigate(
|
|
83
|
-
name: string,
|
|
84
|
-
params: Params,
|
|
85
|
-
opts: NavigationOptions,
|
|
86
|
-
): Promise<State> {
|
|
87
|
-
this.lastSyncResolved = false;
|
|
88
|
-
const deps = this.#deps;
|
|
89
|
-
|
|
90
|
-
// Fast-path sync rejections: cached error + cached Promise.reject
|
|
91
|
-
// No allocations, no throw/catch overhead, facade skips .catch() suppression
|
|
92
|
-
if (!deps.canNavigate()) {
|
|
93
|
-
this.lastSyncRejected = true;
|
|
94
|
-
|
|
95
|
-
return CACHED_NOT_STARTED_REJECTION;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
let toState: State | undefined;
|
|
99
|
-
let fromState: State | undefined;
|
|
100
|
-
let transitionStarted = false;
|
|
101
|
-
let controller: AbortController | null = null;
|
|
102
|
-
|
|
103
|
-
try {
|
|
104
|
-
toState = deps.buildNavigateState(name, params);
|
|
105
|
-
|
|
106
|
-
if (!toState) {
|
|
107
|
-
deps.emitTransitionError(
|
|
108
|
-
undefined,
|
|
109
|
-
deps.getState(),
|
|
110
|
-
CACHED_ROUTE_NOT_FOUND_ERROR,
|
|
111
|
-
);
|
|
112
|
-
this.lastSyncRejected = true;
|
|
113
|
-
|
|
114
|
-
return CACHED_ROUTE_NOT_FOUND_REJECTION;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
fromState = deps.getState();
|
|
118
|
-
opts = forceReplaceFromUnknown(opts, fromState);
|
|
119
|
-
|
|
120
|
-
if (isSameNavigation(fromState, opts, toState)) {
|
|
121
|
-
deps.emitTransitionError(toState, fromState, CACHED_SAME_STATES_ERROR);
|
|
122
|
-
this.lastSyncRejected = true;
|
|
123
|
-
|
|
124
|
-
return CACHED_SAME_STATES_REJECTION;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
this.#abortPreviousNavigation(opts.signal);
|
|
128
|
-
|
|
129
|
-
const myId = ++this.#navigationId;
|
|
130
|
-
|
|
131
|
-
deps.startTransition(toState, fromState);
|
|
132
|
-
transitionStarted = true;
|
|
133
|
-
|
|
134
|
-
// Reentrant navigate from TRANSITION_START listener superseded this navigation
|
|
135
|
-
if (this.#navigationId !== myId) {
|
|
136
|
-
throw new RouterError(errorCodes.TRANSITION_CANCELLED);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const [canDeactivateFunctions, canActivateFunctions] =
|
|
140
|
-
deps.getLifecycleFunctions();
|
|
141
|
-
const isUnknownRoute = toState.name === constants.UNKNOWN_ROUTE;
|
|
142
|
-
|
|
143
|
-
const { toDeactivate, toActivate, intersection } = getTransitionPath(
|
|
144
|
-
toState,
|
|
145
|
-
fromState,
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
const shouldDeactivate =
|
|
149
|
-
fromState && !opts.forceDeactivate && toDeactivate.length > 0;
|
|
150
|
-
const shouldActivate = !isUnknownRoute && toActivate.length > 0;
|
|
151
|
-
const hasGuards =
|
|
152
|
-
canDeactivateFunctions.size > 0 || canActivateFunctions.size > 0;
|
|
153
|
-
|
|
154
|
-
const confirmedToState = toState;
|
|
155
|
-
const emitLeaveApproveCallback = () => {
|
|
156
|
-
deps.sendLeaveApprove(confirmedToState, fromState);
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
const isCurrentNav = () => this.#navigationId === myId && deps.isActive();
|
|
160
|
-
|
|
161
|
-
if (!hasGuards) {
|
|
162
|
-
// No guards — emitLeaveApprove directly before completeTransition
|
|
163
|
-
emitLeaveApproveCallback();
|
|
164
|
-
|
|
165
|
-
// Reentrant check: subscribeLeave() listener may have called router.navigate()
|
|
166
|
-
// Same pattern as reentrant check after emitTransitionStart (line 141-143)
|
|
167
|
-
/* v8 ignore next 3 -- @preserve: reentrant navigate from TRANSITION_LEAVE_APPROVE listener; tested but V8 cannot track the branch through the synchronous callback chain */
|
|
168
|
-
if (this.#navigationId !== myId) {
|
|
169
|
-
throw new RouterError(errorCodes.TRANSITION_CANCELLED);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (hasGuards) {
|
|
174
|
-
controller = new AbortController();
|
|
175
|
-
this.#currentController = controller;
|
|
176
|
-
|
|
177
|
-
const signal = controller.signal;
|
|
178
|
-
|
|
179
|
-
const guardCompletion = executeGuardPipeline(
|
|
180
|
-
canDeactivateFunctions,
|
|
181
|
-
canActivateFunctions,
|
|
182
|
-
toDeactivate,
|
|
183
|
-
toActivate,
|
|
184
|
-
!!shouldDeactivate,
|
|
185
|
-
shouldActivate,
|
|
186
|
-
toState,
|
|
187
|
-
fromState,
|
|
188
|
-
signal,
|
|
189
|
-
isCurrentNav,
|
|
190
|
-
emitLeaveApproveCallback,
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
if (guardCompletion !== undefined) {
|
|
194
|
-
return this.#finishAsyncNavigation(
|
|
195
|
-
guardCompletion,
|
|
196
|
-
{
|
|
197
|
-
toState,
|
|
198
|
-
fromState,
|
|
199
|
-
opts,
|
|
200
|
-
toDeactivate,
|
|
201
|
-
toActivate,
|
|
202
|
-
intersection,
|
|
203
|
-
canDeactivateFunctions,
|
|
204
|
-
},
|
|
205
|
-
controller,
|
|
206
|
-
myId,
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (!isCurrentNav()) {
|
|
211
|
-
throw new RouterError(errorCodes.TRANSITION_CANCELLED);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
this.#cleanupController(controller);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
this.lastSyncResolved = true;
|
|
218
|
-
|
|
219
|
-
return Promise.resolve(
|
|
220
|
-
completeTransition(deps, {
|
|
221
|
-
toState,
|
|
222
|
-
fromState,
|
|
223
|
-
opts,
|
|
224
|
-
toDeactivate,
|
|
225
|
-
toActivate,
|
|
226
|
-
intersection,
|
|
227
|
-
canDeactivateFunctions,
|
|
228
|
-
}),
|
|
229
|
-
);
|
|
230
|
-
} catch (error) {
|
|
231
|
-
this.#handleNavigateError(
|
|
232
|
-
error,
|
|
233
|
-
controller,
|
|
234
|
-
transitionStarted,
|
|
235
|
-
toState,
|
|
236
|
-
fromState,
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
return Promise.reject(error as Error);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
navigateToDefault(opts: NavigationOptions): Promise<State> {
|
|
244
|
-
const deps = this.#deps;
|
|
245
|
-
const options = deps.getOptions();
|
|
246
|
-
|
|
247
|
-
if (!options.defaultRoute) {
|
|
248
|
-
return Promise.reject(
|
|
249
|
-
new RouterError(errorCodes.ROUTE_NOT_FOUND, {
|
|
250
|
-
routeName: "defaultRoute not configured",
|
|
251
|
-
}),
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const { route, params } = deps.resolveDefault();
|
|
256
|
-
|
|
257
|
-
if (!route) {
|
|
258
|
-
return Promise.reject(
|
|
259
|
-
new RouterError(errorCodes.ROUTE_NOT_FOUND, {
|
|
260
|
-
routeName: "defaultRoute resolved to empty",
|
|
261
|
-
}),
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
return this.navigate(route, params, opts);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
navigateToNotFound(path: string): State {
|
|
269
|
-
this.#abortPreviousNavigation();
|
|
270
|
-
|
|
271
|
-
const fromState = this.#deps.getState();
|
|
272
|
-
const deactivated: string[] = fromState
|
|
273
|
-
? nameToIDs(fromState.name).toReversed()
|
|
274
|
-
: [];
|
|
275
|
-
|
|
276
|
-
Object.freeze(deactivated);
|
|
277
|
-
|
|
278
|
-
const segments: TransitionMeta["segments"] = {
|
|
279
|
-
deactivated,
|
|
280
|
-
activated: FROZEN_ACTIVATED,
|
|
281
|
-
intersection: "",
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
Object.freeze(segments);
|
|
285
|
-
|
|
286
|
-
const transitionMeta: TransitionMeta = {
|
|
287
|
-
phase: "activating",
|
|
288
|
-
...(fromState && { from: fromState.name }),
|
|
289
|
-
reason: "success",
|
|
290
|
-
segments,
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
Object.freeze(transitionMeta);
|
|
294
|
-
|
|
295
|
-
const state: State = {
|
|
296
|
-
name: constants.UNKNOWN_ROUTE,
|
|
297
|
-
params: {} as Params,
|
|
298
|
-
path,
|
|
299
|
-
transition: transitionMeta,
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
Object.freeze(state);
|
|
303
|
-
|
|
304
|
-
this.#deps.setState(state);
|
|
305
|
-
this.#deps.emitTransitionSuccess(state, fromState, FROZEN_REPLACE_OPTS);
|
|
306
|
-
|
|
307
|
-
return state;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
abortCurrentNavigation(): void {
|
|
311
|
-
this.#currentController?.abort(
|
|
312
|
-
new RouterError(errorCodes.TRANSITION_CANCELLED),
|
|
313
|
-
);
|
|
314
|
-
this.#currentController = null;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
async #finishAsyncNavigation(
|
|
318
|
-
guardCompletion: Promise<void>,
|
|
319
|
-
nav: NavigationContext,
|
|
320
|
-
controller: AbortController,
|
|
321
|
-
myId: number,
|
|
322
|
-
): Promise<State> {
|
|
323
|
-
const deps = this.#deps;
|
|
324
|
-
const isActive = () =>
|
|
325
|
-
this.#navigationId === myId &&
|
|
326
|
-
!controller.signal.aborted &&
|
|
327
|
-
deps.isActive();
|
|
328
|
-
|
|
329
|
-
try {
|
|
330
|
-
if (nav.opts.signal) {
|
|
331
|
-
if (nav.opts.signal.aborted) {
|
|
332
|
-
throw new RouterError(errorCodes.TRANSITION_CANCELLED, {
|
|
333
|
-
reason: nav.opts.signal.reason,
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
nav.opts.signal.addEventListener(
|
|
338
|
-
"abort",
|
|
339
|
-
() => {
|
|
340
|
-
controller.abort(nav.opts.signal?.reason);
|
|
341
|
-
},
|
|
342
|
-
{ once: true, signal: controller.signal },
|
|
343
|
-
);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
await guardCompletion;
|
|
347
|
-
|
|
348
|
-
if (!isActive()) {
|
|
349
|
-
throw new RouterError(errorCodes.TRANSITION_CANCELLED);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
return completeTransition(deps, nav);
|
|
353
|
-
} catch (error) {
|
|
354
|
-
routeTransitionError(deps, error, nav.toState, nav.fromState);
|
|
355
|
-
|
|
356
|
-
throw error;
|
|
357
|
-
} finally {
|
|
358
|
-
this.#cleanupController(controller);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
#handleNavigateError(
|
|
363
|
-
error: unknown,
|
|
364
|
-
controller: AbortController | null,
|
|
365
|
-
transitionStarted: boolean,
|
|
366
|
-
toState: State | undefined,
|
|
367
|
-
fromState: State | undefined,
|
|
368
|
-
): void {
|
|
369
|
-
if (controller) {
|
|
370
|
-
this.#cleanupController(controller);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (transitionStarted && toState) {
|
|
374
|
-
routeTransitionError(this.#deps, error, toState, fromState);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
#cleanupController(controller: AbortController): void {
|
|
379
|
-
controller.abort();
|
|
380
|
-
|
|
381
|
-
if (this.#currentController === controller) {
|
|
382
|
-
this.#currentController = null;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
#abortPreviousNavigation(externalSignal?: AbortSignal): void {
|
|
387
|
-
if (this.#deps.isTransitioning()) {
|
|
388
|
-
logger.warn(
|
|
389
|
-
"router.navigate",
|
|
390
|
-
"Concurrent navigation detected on shared router instance. " +
|
|
391
|
-
"For SSR, use cloneRouter() to create isolated instance per request.",
|
|
392
|
-
);
|
|
393
|
-
this.#currentController?.abort(
|
|
394
|
-
new RouterError(errorCodes.TRANSITION_CANCELLED),
|
|
395
|
-
);
|
|
396
|
-
this.#deps.cancelNavigation();
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
if (externalSignal?.aborted) {
|
|
400
|
-
throw new RouterError(errorCodes.TRANSITION_CANCELLED, {
|
|
401
|
-
reason: externalSignal.reason,
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
// packages/core/src/namespaces/NavigationNamespace/constants.ts
|
|
2
|
-
|
|
3
|
-
import { errorCodes } from "../../constants";
|
|
4
|
-
import { RouterError } from "../../RouterError";
|
|
5
|
-
|
|
6
|
-
import type { State } from "@real-router/types";
|
|
7
|
-
|
|
8
|
-
// =============================================================================
|
|
9
|
-
// Cached Errors & Rejected Promises (Performance Optimization)
|
|
10
|
-
// =============================================================================
|
|
11
|
-
// Pre-create error instances and rejected promises for sync error paths
|
|
12
|
-
// in navigate(). Eliminates per-call allocations:
|
|
13
|
-
// - new RouterError() — object + stack trace capture (~500ns-2μs)
|
|
14
|
-
// - Promise.reject() — promise allocation
|
|
15
|
-
// - .catch(handler) — derived promise from suppression
|
|
16
|
-
//
|
|
17
|
-
// Trade-off: All error instances share the same stack trace (points here).
|
|
18
|
-
// This is acceptable because:
|
|
19
|
-
// 1. These errors indicate expected conditions, not internal bugs
|
|
20
|
-
// 2. Error code and message are sufficient for debugging
|
|
21
|
-
// 3. The facade skips .catch() suppression for cached promises (zero alloc)
|
|
22
|
-
// =============================================================================
|
|
23
|
-
|
|
24
|
-
export const CACHED_NOT_STARTED_ERROR = new RouterError(
|
|
25
|
-
errorCodes.ROUTER_NOT_STARTED,
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
export const CACHED_ROUTE_NOT_FOUND_ERROR = new RouterError(
|
|
29
|
-
errorCodes.ROUTE_NOT_FOUND,
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
export const CACHED_SAME_STATES_ERROR = new RouterError(errorCodes.SAME_STATES);
|
|
33
|
-
|
|
34
|
-
// Pre-suppressed rejected promises — .catch() at module load prevents
|
|
35
|
-
// unhandled rejection warnings. The facade skips additional .catch() calls
|
|
36
|
-
// via the lastSyncRejected flag (zero derived-promise allocation).
|
|
37
|
-
export const CACHED_NOT_STARTED_REJECTION: Promise<State> = Promise.reject(
|
|
38
|
-
CACHED_NOT_STARTED_ERROR,
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
export const CACHED_ROUTE_NOT_FOUND_REJECTION: Promise<State> = Promise.reject(
|
|
42
|
-
CACHED_ROUTE_NOT_FOUND_ERROR,
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
export const CACHED_SAME_STATES_REJECTION: Promise<State> = Promise.reject(
|
|
46
|
-
CACHED_SAME_STATES_ERROR,
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
// Suppress once at module load — prevents unhandled rejection events.
|
|
50
|
-
// Subsequent .catch() / await by user code still works correctly:
|
|
51
|
-
// a rejected promise stays rejected forever, each .catch() creates
|
|
52
|
-
// its own derived promise and fires its handler.
|
|
53
|
-
CACHED_NOT_STARTED_REJECTION.catch(() => {}); // NOSONAR -- intentional suppression, not a promise chain
|
|
54
|
-
CACHED_ROUTE_NOT_FOUND_REJECTION.catch(() => {}); // NOSONAR
|
|
55
|
-
CACHED_SAME_STATES_REJECTION.catch(() => {}); // NOSONAR
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { errorCodes, constants } from "../../../constants";
|
|
2
|
-
import { freezeStateInPlace } from "../../../helpers";
|
|
3
|
-
import { RouterError } from "../../../RouterError";
|
|
4
|
-
|
|
5
|
-
import type { NavigationDependencies, NavigationContext } from "../types";
|
|
6
|
-
import type {
|
|
7
|
-
NavigationOptions,
|
|
8
|
-
State,
|
|
9
|
-
TransitionMeta,
|
|
10
|
-
} from "@real-router/types";
|
|
11
|
-
|
|
12
|
-
type MutableTransitionMeta = {
|
|
13
|
-
-readonly [K in keyof TransitionMeta]: TransitionMeta[K];
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
function buildTransitionMeta(
|
|
17
|
-
fromState: State | undefined,
|
|
18
|
-
opts: NavigationOptions,
|
|
19
|
-
toDeactivate: string[],
|
|
20
|
-
toActivate: string[],
|
|
21
|
-
intersection: string,
|
|
22
|
-
): TransitionMeta {
|
|
23
|
-
const meta: MutableTransitionMeta = {
|
|
24
|
-
phase: "activating",
|
|
25
|
-
reason: "success",
|
|
26
|
-
segments: {
|
|
27
|
-
deactivated: toDeactivate,
|
|
28
|
-
activated: toActivate,
|
|
29
|
-
intersection,
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
if (fromState?.name !== undefined) {
|
|
34
|
-
meta.from = fromState.name;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (opts.reload !== undefined) {
|
|
38
|
-
meta.reload = opts.reload;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (opts.redirected !== undefined) {
|
|
42
|
-
meta.redirected = opts.redirected;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return meta;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function stripSignal({
|
|
49
|
-
signal: _,
|
|
50
|
-
...rest
|
|
51
|
-
}: NavigationOptions): NavigationOptions {
|
|
52
|
-
return rest;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function completeTransition(
|
|
56
|
-
deps: NavigationDependencies,
|
|
57
|
-
nav: NavigationContext,
|
|
58
|
-
): State {
|
|
59
|
-
const { toState, fromState, opts, toDeactivate, toActivate, intersection } =
|
|
60
|
-
nav;
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
toState.name !== constants.UNKNOWN_ROUTE &&
|
|
64
|
-
!deps.hasRoute(toState.name)
|
|
65
|
-
) {
|
|
66
|
-
const err = new RouterError(errorCodes.ROUTE_NOT_FOUND, {
|
|
67
|
-
routeName: toState.name,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
deps.sendTransitionFail(toState, fromState, err);
|
|
71
|
-
|
|
72
|
-
throw err;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (fromState) {
|
|
76
|
-
for (const name of toDeactivate) {
|
|
77
|
-
if (!toActivate.includes(name) && nav.canDeactivateFunctions.has(name)) {
|
|
78
|
-
deps.clearCanDeactivate(name);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
(toState as { transition: TransitionMeta }).transition = buildTransitionMeta(
|
|
84
|
-
fromState,
|
|
85
|
-
opts,
|
|
86
|
-
toDeactivate,
|
|
87
|
-
toActivate,
|
|
88
|
-
intersection,
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
const finalState = freezeStateInPlace(toState);
|
|
92
|
-
|
|
93
|
-
deps.setState(finalState);
|
|
94
|
-
|
|
95
|
-
const transitionOpts = opts.signal === undefined ? opts : stripSignal(opts);
|
|
96
|
-
|
|
97
|
-
deps.sendTransitionDone(finalState, fromState, transitionOpts);
|
|
98
|
-
|
|
99
|
-
return finalState;
|
|
100
|
-
}
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
// packages/core/src/namespaces/NavigationNamespace/transition/errorHandling.ts
|
|
2
|
-
|
|
3
|
-
import { errorCodes } from "../../../constants";
|
|
4
|
-
import { RouterError } from "../../../RouterError";
|
|
5
|
-
|
|
6
|
-
import type { NavigationDependencies } from "../types";
|
|
7
|
-
import type { State } from "@real-router/types";
|
|
8
|
-
|
|
9
|
-
export function routeTransitionError(
|
|
10
|
-
deps: NavigationDependencies,
|
|
11
|
-
error: unknown,
|
|
12
|
-
toState: State,
|
|
13
|
-
fromState: State | undefined,
|
|
14
|
-
): void {
|
|
15
|
-
const routerError = error as RouterError;
|
|
16
|
-
|
|
17
|
-
if (
|
|
18
|
-
routerError.code === errorCodes.TRANSITION_CANCELLED ||
|
|
19
|
-
routerError.code === errorCodes.ROUTE_NOT_FOUND
|
|
20
|
-
) {
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
deps.sendTransitionFail(toState, fromState, routerError);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function handleGuardError(
|
|
28
|
-
error: unknown,
|
|
29
|
-
errorCode: string,
|
|
30
|
-
segment: string,
|
|
31
|
-
): never {
|
|
32
|
-
if (error instanceof DOMException && error.name === "AbortError") {
|
|
33
|
-
throw new RouterError(errorCodes.TRANSITION_CANCELLED);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
rethrowAsRouterError(error, errorCode, segment);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Error metadata structure for transition errors.
|
|
41
|
-
* Contains information extracted from caught exceptions.
|
|
42
|
-
*/
|
|
43
|
-
export interface SyncErrorMetadata {
|
|
44
|
-
[key: string]: unknown;
|
|
45
|
-
message?: string;
|
|
46
|
-
stack?: string | undefined;
|
|
47
|
-
cause?: unknown;
|
|
48
|
-
segment?: string;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Re-throws a caught error as a RouterError with the given error code.
|
|
53
|
-
* If the error is already a RouterError, sets the code directly.
|
|
54
|
-
* Otherwise wraps it with wrapSyncError metadata.
|
|
55
|
-
*/
|
|
56
|
-
export function rethrowAsRouterError(
|
|
57
|
-
error: unknown,
|
|
58
|
-
errorCode: string,
|
|
59
|
-
segment: string,
|
|
60
|
-
): never {
|
|
61
|
-
if (error instanceof RouterError) {
|
|
62
|
-
error.setCode(errorCode);
|
|
63
|
-
|
|
64
|
-
throw error;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
throw new RouterError(errorCode, wrapSyncError(error, segment));
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const reservedRouterErrorProps = new Set([
|
|
71
|
-
"code",
|
|
72
|
-
"segment",
|
|
73
|
-
"path",
|
|
74
|
-
"redirect",
|
|
75
|
-
]);
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Wraps a synchronously thrown value into structured error metadata.
|
|
79
|
-
*
|
|
80
|
-
* This helper extracts useful debugging information from various thrown values:
|
|
81
|
-
* - Error instances: extracts message, stack, and cause (ES2022+)
|
|
82
|
-
* - Plain objects: spreads properties into metadata
|
|
83
|
-
* - Primitives (string, number, etc.): returns minimal metadata
|
|
84
|
-
*
|
|
85
|
-
* @param thrown - The value caught in a try-catch block
|
|
86
|
-
* @param segment - Route segment name (for lifecycle hooks)
|
|
87
|
-
* @returns Structured error metadata for RouterError
|
|
88
|
-
*/
|
|
89
|
-
export function wrapSyncError(
|
|
90
|
-
thrown: unknown,
|
|
91
|
-
segment: string,
|
|
92
|
-
): SyncErrorMetadata {
|
|
93
|
-
const base: SyncErrorMetadata = { segment };
|
|
94
|
-
|
|
95
|
-
// Handle Error instances - extract all useful properties
|
|
96
|
-
if (thrown instanceof Error) {
|
|
97
|
-
return {
|
|
98
|
-
...base,
|
|
99
|
-
message: thrown.message,
|
|
100
|
-
stack: thrown.stack,
|
|
101
|
-
// Error.cause requires ES2022+ - safely access if present
|
|
102
|
-
...("cause" in thrown &&
|
|
103
|
-
thrown.cause !== undefined && { cause: thrown.cause }),
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Handle plain objects - spread properties into metadata, filtering reserved props
|
|
108
|
-
if (thrown && typeof thrown === "object") {
|
|
109
|
-
const filtered: Record<string, unknown> = {};
|
|
110
|
-
|
|
111
|
-
for (const [key, value] of Object.entries(thrown)) {
|
|
112
|
-
// Issue #39: Skip reserved properties to avoid RouterError constructor TypeError
|
|
113
|
-
if (!reservedRouterErrorProps.has(key)) {
|
|
114
|
-
filtered[key] = value;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return { ...base, ...filtered };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Primitives (string, number, boolean, null, undefined, symbol, bigint)
|
|
122
|
-
// Return base metadata only - the primitive value isn't useful as metadata
|
|
123
|
-
return base;
|
|
124
|
-
}
|