@real-router/core 0.36.1 → 0.37.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 (37) hide show
  1. package/README.md +144 -168
  2. package/dist/cjs/api.js +1 -1
  3. package/dist/cjs/api.js.map +1 -1
  4. package/dist/cjs/index.js +1 -1
  5. package/dist/cjs/index.js.map +1 -1
  6. package/dist/cjs/metafile-cjs.json +1 -1
  7. package/dist/esm/api.mjs +1 -1
  8. package/dist/esm/api.mjs.map +1 -1
  9. package/dist/esm/chunk-CG7TKDP3.mjs +1 -0
  10. package/dist/esm/chunk-CG7TKDP3.mjs.map +1 -0
  11. package/dist/esm/index.mjs +1 -1
  12. package/dist/esm/metafile-esm.json +1 -1
  13. package/package.json +7 -7
  14. package/src/Router.ts +33 -12
  15. package/src/api/getPluginApi.ts +19 -4
  16. package/src/api/getRoutesApi.ts +0 -20
  17. package/src/constants.ts +2 -0
  18. package/src/fsm/routerFSM.ts +2 -21
  19. package/src/internals.ts +21 -2
  20. package/src/namespaces/EventBusNamespace/EventBusNamespace.ts +40 -37
  21. package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +221 -153
  22. package/src/namespaces/NavigationNamespace/constants.ts +55 -0
  23. package/src/namespaces/NavigationNamespace/transition/completeTransition.ts +100 -0
  24. package/src/namespaces/NavigationNamespace/transition/errorHandling.ts +34 -0
  25. package/src/namespaces/NavigationNamespace/transition/guardPhase.ts +214 -0
  26. package/src/namespaces/NavigationNamespace/types.ts +14 -30
  27. package/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts +6 -1
  28. package/src/namespaces/RoutesNamespace/RoutesNamespace.ts +36 -35
  29. package/src/namespaces/RoutesNamespace/forwardToValidation.ts +2 -5
  30. package/src/namespaces/RoutesNamespace/types.ts +1 -2
  31. package/src/namespaces/StateNamespace/StateNamespace.ts +13 -17
  32. package/src/transitionPath.ts +68 -39
  33. package/src/wiring/RouterWiringBuilder.ts +16 -15
  34. package/dist/esm/chunk-HZ7RFKT5.mjs +0 -1
  35. package/dist/esm/chunk-HZ7RFKT5.mjs.map +0 -1
  36. package/src/namespaces/NavigationNamespace/transition/executeLifecycleGuards.ts +0 -52
  37. package/src/namespaces/NavigationNamespace/transition/index.ts +0 -93
@@ -23,6 +23,9 @@ export class EventBusNamespace {
23
23
  readonly #emitter: EventEmitter<RouterEventMap>;
24
24
 
25
25
  #currentToState: State | undefined;
26
+ #pendingToState: State | undefined;
27
+ #pendingFromState: State | undefined;
28
+ #pendingError: unknown;
26
29
 
27
30
  constructor(options: EventBusOptions) {
28
31
  this.#fsm = options.routerFSM;
@@ -90,7 +93,9 @@ export class EventBusNamespace {
90
93
 
91
94
  sendNavigate(toState: State, fromState?: State): void {
92
95
  this.#currentToState = toState;
93
- this.#fsm.send(routerEvents.NAVIGATE, { toState, fromState });
96
+ // Bypass FSM dispatch — forceState + direct emit (no action lookup, no rest params)
97
+ this.#fsm.forceState(routerStates.TRANSITIONING);
98
+ this.emitTransitionStart(toState, fromState);
94
99
  }
95
100
 
96
101
  sendComplete(
@@ -98,15 +103,11 @@ export class EventBusNamespace {
98
103
  fromState?: State,
99
104
  opts: NavigationOptions = {},
100
105
  ): void {
101
- const prev = this.#currentToState;
102
-
103
- this.#fsm.send(routerEvents.COMPLETE, {
104
- state,
105
- fromState,
106
- opts,
107
- });
106
+ // Bypass FSM dispatch — forceState + direct emit
107
+ this.#fsm.forceState(routerStates.READY);
108
+ this.emitTransitionSuccess(state, fromState, opts);
108
109
 
109
- if (this.#currentToState === prev) {
110
+ if (this.#currentToState === state) {
110
111
  this.#currentToState = undefined;
111
112
  }
112
113
  }
@@ -114,7 +115,10 @@ export class EventBusNamespace {
114
115
  sendFail(toState?: State, fromState?: State, error?: unknown): void {
115
116
  const prev = this.#currentToState;
116
117
 
117
- this.#fsm.send(routerEvents.FAIL, { toState, fromState, error });
118
+ this.#pendingToState = toState;
119
+ this.#pendingFromState = fromState;
120
+ this.#pendingError = error;
121
+ this.#fsm.send(routerEvents.FAIL);
118
122
 
119
123
  if (this.#currentToState === prev) {
120
124
  this.#currentToState = undefined;
@@ -132,7 +136,9 @@ export class EventBusNamespace {
132
136
  sendCancel(toState: State, fromState?: State): void {
133
137
  const prev = this.#currentToState;
134
138
 
135
- this.#fsm.send(routerEvents.CANCEL, { toState, fromState });
139
+ this.#pendingToState = toState;
140
+ this.#pendingFromState = fromState;
141
+ this.#fsm.send(routerEvents.CANCEL);
136
142
 
137
143
  if (this.#currentToState === prev) {
138
144
  this.#currentToState = undefined;
@@ -212,6 +218,14 @@ export class EventBusNamespace {
212
218
  this.sendCancel(this.#currentToState!, fromState); // eslint-disable-line @typescript-eslint/no-non-null-assertion -- guaranteed set before TRANSITIONING
213
219
  }
214
220
 
221
+ #emitPendingError(): void {
222
+ this.emitTransitionError(
223
+ this.#pendingToState,
224
+ this.#pendingFromState,
225
+ this.#pendingError as RouterError | undefined,
226
+ );
227
+ }
228
+
215
229
  #setupFSMActions(): void {
216
230
  const fsm = this.#fsm;
217
231
 
@@ -223,40 +237,29 @@ export class EventBusNamespace {
223
237
  this.emitRouterStop();
224
238
  });
225
239
 
226
- fsm.on(routerStates.READY, routerEvents.NAVIGATE, (params) => {
227
- this.emitTransitionStart(params.toState, params.fromState);
228
- });
240
+ // NAVIGATE and COMPLETE actions bypassed — sendNavigate/sendComplete
241
+ // use fsm.forceState() + direct emit for zero-allocation hot path.
242
+ fsm.on(routerStates.TRANSITIONING, routerEvents.CANCEL, () => {
243
+ const toState = this.#pendingToState;
229
244
 
230
- fsm.on(routerStates.TRANSITIONING, routerEvents.COMPLETE, (params) => {
231
- this.emitTransitionSuccess(params.state, params.fromState, params.opts);
232
- });
245
+ /* v8 ignore next -- @preserve: #pendingToState guaranteed set by sendCancel before send() */
246
+ if (toState === undefined) {
247
+ return;
248
+ }
233
249
 
234
- fsm.on(routerStates.TRANSITIONING, routerEvents.CANCEL, (params) => {
235
- this.emitTransitionCancel(params.toState, params.fromState);
250
+ this.emitTransitionCancel(toState, this.#pendingFromState);
236
251
  });
237
252
 
238
- fsm.on(routerStates.STARTING, routerEvents.FAIL, (params) => {
239
- this.emitTransitionError(
240
- params.toState,
241
- params.fromState,
242
- params.error as RouterError | undefined,
243
- );
253
+ fsm.on(routerStates.STARTING, routerEvents.FAIL, () => {
254
+ this.#emitPendingError();
244
255
  });
245
256
 
246
- fsm.on(routerStates.READY, routerEvents.FAIL, (params) => {
247
- this.emitTransitionError(
248
- params.toState,
249
- params.fromState,
250
- params.error as RouterError | undefined,
251
- );
257
+ fsm.on(routerStates.READY, routerEvents.FAIL, () => {
258
+ this.#emitPendingError();
252
259
  });
253
260
 
254
- fsm.on(routerStates.TRANSITIONING, routerEvents.FAIL, (params) => {
255
- this.emitTransitionError(
256
- params.toState,
257
- params.fromState,
258
- params.error as RouterError | undefined,
259
- );
261
+ fsm.on(routerStates.TRANSITIONING, routerEvents.FAIL, () => {
262
+ this.#emitPendingError();
260
263
  });
261
264
  }
262
265
  }
@@ -1,8 +1,15 @@
1
- // packages/core/src/namespaces/NavigationNamespace/NavigationNamespace.ts
2
-
3
1
  import { logger } from "@real-router/logger";
4
2
 
5
- import { transition } from "./transition";
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";
6
13
  import {
7
14
  validateNavigateArgs,
8
15
  validateNavigateToDefaultArgs,
@@ -10,9 +17,9 @@ import {
10
17
  } from "./validators";
11
18
  import { errorCodes, constants } from "../../constants";
12
19
  import { RouterError } from "../../RouterError";
13
- import { nameToIDs } from "../../transitionPath";
20
+ import { getTransitionPath, nameToIDs } from "../../transitionPath";
14
21
 
15
- import type { NavigationDependencies, TransitionOutput } from "./types";
22
+ import type { NavigationContext, NavigationDependencies } from "./types";
16
23
  import type {
17
24
  NavigationOptions,
18
25
  Params,
@@ -36,65 +43,35 @@ function forceReplaceFromUnknown(
36
43
  : opts;
37
44
  }
38
45
 
39
- function stripSignal({
40
- signal: _,
41
- ...rest
42
- }: NavigationOptions): NavigationOptions {
43
- return rest;
44
- }
45
-
46
- function routeTransitionError(
47
- deps: NavigationDependencies,
48
- error: unknown,
49
- toState: State,
50
- fromState: State | undefined,
51
- ): void {
52
- const routerError = error as RouterError;
53
-
54
- if (
55
- routerError.code === errorCodes.TRANSITION_CANCELLED ||
56
- routerError.code === errorCodes.ROUTE_NOT_FOUND
57
- ) {
58
- return;
59
- }
60
-
61
- deps.sendTransitionFail(toState, fromState, routerError);
62
- }
63
-
64
- function buildSuccessState(
65
- finalState: State,
66
- transitionOutput: TransitionOutput["meta"],
46
+ function isSameNavigation(
67
47
  fromState: State | undefined,
68
48
  opts: NavigationOptions,
69
- ): State {
70
- const transitionMeta: TransitionMeta = {
71
- phase: transitionOutput.phase,
72
- ...(fromState?.name !== undefined && { from: fromState.name }),
73
- reason: "success",
74
- segments: transitionOutput.segments,
75
- ...(opts.reload !== undefined && { reload: opts.reload }),
76
- ...(opts.redirected !== undefined && { redirected: opts.redirected }),
77
- };
78
-
79
- Object.freeze(transitionMeta.segments.deactivated);
80
- Object.freeze(transitionMeta.segments.activated);
81
- Object.freeze(transitionMeta.segments);
82
- Object.freeze(transitionMeta);
83
-
84
- return {
85
- ...finalState,
86
- transition: transitionMeta,
87
- };
49
+ toState: State,
50
+ areStatesEqual: (a: State, b: State, ignoreQuery: boolean) => boolean,
51
+ ): boolean {
52
+ return (
53
+ !!fromState &&
54
+ !opts.reload &&
55
+ !opts.force &&
56
+ areStatesEqual(fromState, toState, false)
57
+ );
88
58
  }
89
59
 
90
60
  /**
91
61
  * Independent namespace for managing navigation.
92
62
  *
93
63
  * Handles navigate(), navigateToDefault(), navigateToNotFound(), and transition state.
64
+ *
65
+ * Performance: navigate() uses optimistic sync execution — guards run synchronously
66
+ * until one returns a Promise, then switches to async. This eliminates Promise/AbortController
67
+ * overhead for the common case (no guards or sync guards).
94
68
  */
95
69
  export class NavigationNamespace {
70
+ lastSyncResolved = false;
71
+ lastSyncRejected = false;
96
72
  #deps!: NavigationDependencies;
97
73
  #currentController: AbortController | null = null;
74
+ #navigationId = 0;
98
75
 
99
76
  // =========================================================================
100
77
  // Static validation methods (called by facade before instance methods)
@@ -128,154 +105,176 @@ export class NavigationNamespace {
128
105
  // Instance methods
129
106
  // =========================================================================
130
107
 
131
- /**
132
- * Navigates to a route by name.
133
- * Arguments should be pre-parsed and validated by facade.
134
- */
135
- async navigate(
108
+ navigate(
136
109
  name: string,
137
110
  params: Params,
138
111
  opts: NavigationOptions,
139
112
  ): Promise<State> {
140
- if (!this.#deps.canNavigate()) {
141
- throw new RouterError(errorCodes.ROUTER_NOT_STARTED);
142
- }
143
-
113
+ this.lastSyncResolved = false;
144
114
  const deps = this.#deps;
145
115
 
146
- const result = deps.buildStateWithSegments(name, params);
147
-
148
- if (!result) {
149
- const err = new RouterError(errorCodes.ROUTE_NOT_FOUND);
116
+ // Fast-path sync rejections: cached error + cached Promise.reject
117
+ // No allocations, no throw/catch overhead, facade skips .catch() suppression
118
+ if (!deps.canNavigate()) {
119
+ this.lastSyncRejected = true;
150
120
 
151
- deps.emitTransitionError(undefined, deps.getState(), err);
152
-
153
- throw err;
121
+ return CACHED_NOT_STARTED_REJECTION;
154
122
  }
155
123
 
156
- const { state: route } = result;
157
-
158
- const toState = deps.makeState(
159
- route.name,
160
- route.params,
161
- deps.buildPath(route.name, route.params),
162
- {
163
- params: route.meta,
164
- },
165
- );
166
-
167
- const fromState = deps.getState();
124
+ let toState: State | undefined;
125
+ let fromState: State | undefined;
126
+ let transitionStarted = false;
127
+ let controller: AbortController | null = null;
168
128
 
169
- opts = forceReplaceFromUnknown(opts, fromState);
170
-
171
- if (
172
- fromState &&
173
- !opts.reload &&
174
- !opts.force &&
175
- deps.areStatesEqual(fromState, toState, false)
176
- ) {
177
- const err = new RouterError(errorCodes.SAME_STATES);
129
+ try {
130
+ toState = deps.buildNavigateState(name, params);
178
131
 
179
- deps.emitTransitionError(toState, fromState, err);
132
+ if (!toState) {
133
+ deps.emitTransitionError(
134
+ undefined,
135
+ deps.getState(),
136
+ CACHED_ROUTE_NOT_FOUND_ERROR,
137
+ );
138
+ this.lastSyncRejected = true;
180
139
 
181
- throw err;
182
- }
140
+ return CACHED_ROUTE_NOT_FOUND_REJECTION;
141
+ }
183
142
 
184
- this.#abortPreviousNavigation();
143
+ fromState = deps.getState();
144
+ opts = forceReplaceFromUnknown(opts, fromState);
185
145
 
186
- const controller = new AbortController();
146
+ if (isSameNavigation(fromState, opts, toState, deps.areStatesEqual)) {
147
+ deps.emitTransitionError(toState, fromState, CACHED_SAME_STATES_ERROR);
148
+ this.lastSyncRejected = true;
187
149
 
188
- this.#currentController = controller;
150
+ return CACHED_SAME_STATES_REJECTION;
151
+ }
189
152
 
190
- if (opts.signal) {
191
- if (opts.signal.aborted) {
192
- this.#currentController = null;
153
+ this.#abortPreviousNavigation();
193
154
 
155
+ if (opts.signal?.aborted) {
194
156
  throw new RouterError(errorCodes.TRANSITION_CANCELLED, {
195
157
  reason: opts.signal.reason,
196
158
  });
197
159
  }
198
160
 
199
- opts.signal.addEventListener(
200
- "abort",
201
- () => {
202
- controller.abort(opts.signal?.reason);
203
- },
204
- { once: true, signal: controller.signal },
205
- );
206
- }
161
+ const myId = ++this.#navigationId;
207
162
 
208
- deps.startTransition(toState, fromState);
163
+ deps.startTransition(toState, fromState);
164
+ transitionStarted = true;
209
165
 
210
- try {
211
- const { state: finalState, meta: transitionOutput } = await transition(
212
- deps,
166
+ // Reentrant navigate from TRANSITION_START listener superseded this navigation
167
+ if (this.#navigationId !== myId) {
168
+ throw new RouterError(errorCodes.TRANSITION_CANCELLED);
169
+ }
170
+
171
+ const [canDeactivateFunctions, canActivateFunctions] =
172
+ deps.getLifecycleFunctions();
173
+ const isUnknownRoute = toState.name === constants.UNKNOWN_ROUTE;
174
+
175
+ const { toDeactivate, toActivate, intersection } = getTransitionPath(
213
176
  toState,
214
177
  fromState,
215
- opts,
216
- controller.signal,
178
+ opts.reload,
217
179
  );
218
180
 
219
- if (
220
- finalState.name === constants.UNKNOWN_ROUTE ||
221
- deps.hasRoute(finalState.name)
222
- ) {
223
- const stateWithTransition = buildSuccessState(
224
- finalState,
225
- transitionOutput,
181
+ const shouldDeactivate =
182
+ fromState && !opts.forceDeactivate && toDeactivate.length > 0;
183
+ const shouldActivate = !isUnknownRoute && toActivate.length > 0;
184
+ const hasGuards =
185
+ canDeactivateFunctions.size > 0 || canActivateFunctions.size > 0;
186
+
187
+ if (hasGuards) {
188
+ controller = new AbortController();
189
+ this.#currentController = controller;
190
+
191
+ const signal = controller.signal;
192
+ const isCurrentNav = () =>
193
+ this.#navigationId === myId && deps.isActive();
194
+
195
+ const guardCompletion = executeGuardPipeline(
196
+ canDeactivateFunctions,
197
+ canActivateFunctions,
198
+ toDeactivate,
199
+ toActivate,
200
+ !!shouldDeactivate,
201
+ shouldActivate,
202
+ toState,
226
203
  fromState,
227
- opts,
204
+ signal,
205
+ isCurrentNav,
228
206
  );
229
207
 
230
- deps.setState(stateWithTransition);
231
-
232
- const transitionOpts =
233
- opts.signal === undefined ? opts : stripSignal(opts);
234
-
235
- deps.sendTransitionDone(stateWithTransition, fromState, transitionOpts);
236
-
237
- return stateWithTransition;
238
- } else {
239
- const err = new RouterError(errorCodes.ROUTE_NOT_FOUND, {
240
- routeName: finalState.name,
241
- });
208
+ if (guardCompletion !== undefined) {
209
+ return this.#finishAsyncNavigation(
210
+ guardCompletion,
211
+ {
212
+ toState,
213
+ fromState,
214
+ opts,
215
+ toDeactivate,
216
+ toActivate,
217
+ intersection,
218
+ canDeactivateFunctions,
219
+ },
220
+ controller,
221
+ myId,
222
+ );
223
+ }
224
+
225
+ if (!isCurrentNav()) {
226
+ throw new RouterError(errorCodes.TRANSITION_CANCELLED);
227
+ }
228
+
229
+ this.#cleanupController(controller);
230
+ }
242
231
 
243
- deps.sendTransitionFail(finalState, fromState, err);
232
+ this.lastSyncResolved = true;
244
233
 
245
- throw err;
246
- }
234
+ return Promise.resolve(
235
+ completeTransition(deps, {
236
+ toState,
237
+ fromState,
238
+ opts,
239
+ toDeactivate,
240
+ toActivate,
241
+ intersection,
242
+ canDeactivateFunctions,
243
+ }),
244
+ );
247
245
  } catch (error) {
248
- routeTransitionError(deps, error, toState, fromState);
246
+ this.#handleNavigateError(
247
+ error,
248
+ controller,
249
+ transitionStarted,
250
+ toState,
251
+ fromState,
252
+ );
249
253
 
250
- throw error;
251
- } finally {
252
- controller.abort();
253
- if (this.#currentController === controller) {
254
- this.#currentController = null;
255
- }
254
+ return Promise.reject(error as Error);
256
255
  }
257
256
  }
258
257
 
259
- /**
260
- * Navigates to the default route if configured.
261
- * Arguments should be pre-parsed and validated by facade.
262
- */
263
- async navigateToDefault(opts: NavigationOptions): Promise<State> {
258
+ navigateToDefault(opts: NavigationOptions): Promise<State> {
264
259
  const deps = this.#deps;
265
260
  const options = deps.getOptions();
266
261
 
267
262
  if (!options.defaultRoute) {
268
- throw new RouterError(errorCodes.ROUTE_NOT_FOUND, {
269
- routeName: "defaultRoute not configured",
270
- });
263
+ return Promise.reject(
264
+ new RouterError(errorCodes.ROUTE_NOT_FOUND, {
265
+ routeName: "defaultRoute not configured",
266
+ }),
267
+ );
271
268
  }
272
269
 
273
270
  const { route, params } = deps.resolveDefault();
274
271
 
275
272
  if (!route) {
276
- throw new RouterError(errorCodes.ROUTE_NOT_FOUND, {
277
- routeName: "defaultRoute resolved to empty",
278
- });
273
+ return Promise.reject(
274
+ new RouterError(errorCodes.ROUTE_NOT_FOUND, {
275
+ routeName: "defaultRoute resolved to empty",
276
+ }),
277
+ );
279
278
  }
280
279
 
281
280
  return this.navigate(route, params, opts);
@@ -330,6 +329,75 @@ export class NavigationNamespace {
330
329
  this.#currentController = null;
331
330
  }
332
331
 
332
+ async #finishAsyncNavigation(
333
+ guardCompletion: Promise<void>,
334
+ nav: NavigationContext,
335
+ controller: AbortController,
336
+ myId: number,
337
+ ): Promise<State> {
338
+ const deps = this.#deps;
339
+ const isActive = () =>
340
+ this.#navigationId === myId &&
341
+ !controller.signal.aborted &&
342
+ deps.isActive();
343
+
344
+ try {
345
+ if (nav.opts.signal) {
346
+ if (nav.opts.signal.aborted) {
347
+ throw new RouterError(errorCodes.TRANSITION_CANCELLED, {
348
+ reason: nav.opts.signal.reason,
349
+ });
350
+ }
351
+
352
+ nav.opts.signal.addEventListener(
353
+ "abort",
354
+ () => {
355
+ controller.abort(nav.opts.signal?.reason);
356
+ },
357
+ { once: true, signal: controller.signal },
358
+ );
359
+ }
360
+
361
+ await guardCompletion;
362
+
363
+ if (!isActive()) {
364
+ throw new RouterError(errorCodes.TRANSITION_CANCELLED);
365
+ }
366
+
367
+ return completeTransition(deps, nav);
368
+ } catch (error) {
369
+ routeTransitionError(deps, error, nav.toState, nav.fromState);
370
+
371
+ throw error;
372
+ } finally {
373
+ this.#cleanupController(controller);
374
+ }
375
+ }
376
+
377
+ #handleNavigateError(
378
+ error: unknown,
379
+ controller: AbortController | null,
380
+ transitionStarted: boolean,
381
+ toState: State | undefined,
382
+ fromState: State | undefined,
383
+ ): void {
384
+ if (controller) {
385
+ this.#cleanupController(controller);
386
+ }
387
+
388
+ if (transitionStarted && toState) {
389
+ routeTransitionError(this.#deps, error, toState, fromState);
390
+ }
391
+ }
392
+
393
+ #cleanupController(controller: AbortController): void {
394
+ controller.abort();
395
+
396
+ if (this.#currentController === controller) {
397
+ this.#currentController = null;
398
+ }
399
+ }
400
+
333
401
  #abortPreviousNavigation(): void {
334
402
  if (this.#deps.isTransitioning()) {
335
403
  logger.warn(
@@ -0,0 +1,55 @@
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