@real-router/core 0.56.0 → 0.57.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.
- package/dist/cjs/Router-BSGzVINO.js +6 -0
- package/dist/cjs/Router-BSGzVINO.js.map +1 -0
- package/dist/cjs/api.d.ts +1 -1
- package/dist/cjs/api.js +1 -1
- package/dist/cjs/{cloneRouter-DRieJvam.js → cloneRouter-7z-60z_f.js} +2 -2
- package/dist/cjs/{cloneRouter-DRieJvam.js.map → cloneRouter-7z-60z_f.js.map} +1 -1
- package/dist/cjs/{index-C-i6vx5Y.d.ts → index-BWUmnecT.d.ts} +1 -2
- package/dist/cjs/index-BWUmnecT.d.ts.map +1 -0
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/utils.js +1 -1
- package/dist/cjs/validation.d.ts +1 -1
- package/dist/esm/Router-B7txWo9N.mjs +6 -0
- package/dist/esm/Router-B7txWo9N.mjs.map +1 -0
- package/dist/esm/api.d.mts +1 -1
- package/dist/esm/api.mjs +1 -1
- package/dist/esm/{cloneRouter-DHrH6D_z.mjs → cloneRouter-BNCQ7tIa.mjs} +2 -2
- package/dist/esm/{cloneRouter-DHrH6D_z.mjs.map → cloneRouter-BNCQ7tIa.mjs.map} +1 -1
- package/dist/esm/{index-C-i6vx5Y.d.mts → index-BWUmnecT.d.mts} +1 -2
- package/dist/esm/index-BWUmnecT.d.mts.map +1 -0
- package/dist/esm/index.d.mts +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/utils.mjs +1 -1
- package/dist/esm/validation.d.mts +1 -1
- package/package.json +2 -3
- package/dist/cjs/Router-IEGavTKk.js +0 -6
- package/dist/cjs/Router-IEGavTKk.js.map +0 -1
- package/dist/cjs/index-C-i6vx5Y.d.ts.map +0 -1
- package/dist/esm/Router-B3aeavRb.mjs +0 -6
- package/dist/esm/Router-B3aeavRb.mjs.map +0 -1
- package/dist/esm/index-C-i6vx5Y.d.mts.map +0 -1
- package/src/Router.ts +0 -737
- package/src/RouterError.ts +0 -324
- package/src/api/cloneRouter.ts +0 -159
- package/src/api/getDependenciesApi.ts +0 -160
- package/src/api/getLifecycleApi.ts +0 -65
- package/src/api/getPluginApi.ts +0 -228
- package/src/api/getRoutesApi.ts +0 -831
- 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 -101
- package/src/createRouter.ts +0 -32
- package/src/fsm/index.ts +0 -5
- package/src/fsm/routerFSM.ts +0 -130
- package/src/getNavigator.ts +0 -30
- package/src/guards.ts +0 -46
- package/src/helpers.ts +0 -197
- package/src/index.ts +0 -66
- package/src/internals.ts +0 -228
- package/src/namespaces/DependenciesNamespace/dependenciesStore.ts +0 -30
- package/src/namespaces/DependenciesNamespace/index.ts +0 -5
- package/src/namespaces/EventBusNamespace/EventBusNamespace.ts +0 -522
- package/src/namespaces/EventBusNamespace/index.ts +0 -5
- package/src/namespaces/EventBusNamespace/types.ts +0 -11
- package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +0 -552
- package/src/namespaces/NavigationNamespace/constants.ts +0 -55
- package/src/namespaces/NavigationNamespace/index.ts +0 -5
- package/src/namespaces/NavigationNamespace/transition/completeTransition.ts +0 -108
- package/src/namespaces/NavigationNamespace/transition/errorHandling.ts +0 -124
- package/src/namespaces/NavigationNamespace/transition/guardPhase.ts +0 -283
- package/src/namespaces/NavigationNamespace/types.ts +0 -110
- 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 -558
- 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 -30
- package/src/namespaces/RoutesNamespace/RoutesNamespace.ts +0 -582
- package/src/namespaces/RoutesNamespace/constants.ts +0 -6
- package/src/namespaces/RoutesNamespace/forwardChain.ts +0 -34
- package/src/namespaces/RoutesNamespace/helpers.ts +0 -204
- package/src/namespaces/RoutesNamespace/index.ts +0 -11
- package/src/namespaces/RoutesNamespace/routeGuards.ts +0 -62
- package/src/namespaces/RoutesNamespace/routesStore.ts +0 -566
- package/src/namespaces/RoutesNamespace/types.ts +0 -81
- package/src/namespaces/StateNamespace/StateNamespace.ts +0 -224
- 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 -440
- package/src/typeGuards.ts +0 -59
- package/src/types/RouterValidator.ts +0 -156
- package/src/types.ts +0 -77
- package/src/utils/createRequestScope.ts +0 -174
- package/src/utils/getStaticPaths.ts +0 -50
- package/src/utils/hydrateRouter.ts +0 -89
- package/src/utils/index.ts +0 -27
- package/src/utils/serializeRouterState.ts +0 -120
- package/src/utils/serializeState.ts +0 -63
- package/src/validation.ts +0 -12
- package/src/wiring/RouterWiringBuilder.ts +0 -275
- package/src/wiring/index.ts +0 -7
- package/src/wiring/types.ts +0 -47
- package/src/wiring/wireRouter.ts +0 -26
|
@@ -1,522 +0,0 @@
|
|
|
1
|
-
// packages/core/src/namespaces/EventBusNamespace/EventBusNamespace.ts
|
|
2
|
-
|
|
3
|
-
import { events } from "../../constants";
|
|
4
|
-
import { routerEvents, routerStates } from "../../fsm";
|
|
5
|
-
|
|
6
|
-
import type { EventBusOptions } from "./types";
|
|
7
|
-
import type { RouterEvent, RouterPayloads, RouterState } from "../../fsm";
|
|
8
|
-
import type { RouterError } from "../../RouterError";
|
|
9
|
-
import type { EventMethodMap, RouterEventMap } from "../../types";
|
|
10
|
-
import type { FSM } from "@real-router/fsm";
|
|
11
|
-
import type {
|
|
12
|
-
EventName,
|
|
13
|
-
LeaveFn,
|
|
14
|
-
LeaveState,
|
|
15
|
-
NavigationOptions,
|
|
16
|
-
Plugin,
|
|
17
|
-
State,
|
|
18
|
-
SubscribeFn,
|
|
19
|
-
TreeChangedEvent,
|
|
20
|
-
Unsubscribe,
|
|
21
|
-
} from "@real-router/types";
|
|
22
|
-
import type { EventEmitter } from "event-emitter";
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Internal-only event key for route-tree mutations. Lives on the same
|
|
26
|
-
* `EventEmitter` as the 7 transition events but never enters the public
|
|
27
|
-
* `EventName` union — reachable only through
|
|
28
|
-
* `getRoutesApi(router).subscribeChanges()`.
|
|
29
|
-
*/
|
|
30
|
-
const TREE_CHANGED = "TREE_CHANGED";
|
|
31
|
-
|
|
32
|
-
function ensureError(value: unknown): Error {
|
|
33
|
-
/* v8 ignore next -- @preserve: defensive guard — listeners should always throw Error objects */
|
|
34
|
-
return value instanceof Error ? value : new Error(String(value));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function settleLeavePromises(
|
|
38
|
-
promises: Promise<void>[],
|
|
39
|
-
firstSyncError: unknown,
|
|
40
|
-
signal: AbortSignal,
|
|
41
|
-
): Promise<void> {
|
|
42
|
-
return new Promise<void>((resolve, reject) => {
|
|
43
|
-
const onAbort = (): void => {
|
|
44
|
-
reject(ensureError(signal.reason));
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
if (signal.aborted) {
|
|
48
|
-
onAbort();
|
|
49
|
-
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
54
|
-
|
|
55
|
-
void Promise.allSettled(promises).then((results) => {
|
|
56
|
-
signal.removeEventListener("abort", onAbort);
|
|
57
|
-
|
|
58
|
-
if (signal.aborted) {
|
|
59
|
-
// Race lost to abort — the abort handler already rejected; do nothing
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (firstSyncError !== undefined) {
|
|
64
|
-
reject(ensureError(firstSyncError));
|
|
65
|
-
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const rejected = results.find(
|
|
70
|
-
(result): result is PromiseRejectedResult =>
|
|
71
|
-
result.status === "rejected",
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
if (rejected !== undefined) {
|
|
75
|
-
reject(ensureError(rejected.reason));
|
|
76
|
-
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
resolve();
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export class EventBusNamespace {
|
|
86
|
-
readonly #fsm: FSM<RouterState, RouterEvent, null, RouterPayloads>;
|
|
87
|
-
readonly #emitter: EventEmitter<RouterEventMap>;
|
|
88
|
-
readonly #leaveListeners: LeaveFn[] = [];
|
|
89
|
-
|
|
90
|
-
#currentToState: State | undefined;
|
|
91
|
-
#pendingToState: State | undefined;
|
|
92
|
-
#pendingFromState: State | undefined;
|
|
93
|
-
#pendingError: unknown;
|
|
94
|
-
|
|
95
|
-
constructor(options: EventBusOptions) {
|
|
96
|
-
this.#fsm = options.routerFSM;
|
|
97
|
-
this.#emitter = options.emitter;
|
|
98
|
-
this.#currentToState = undefined;
|
|
99
|
-
this.#setupFSMActions();
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
static validateSubscribeListener(listener: unknown): void {
|
|
103
|
-
if (typeof listener !== "function") {
|
|
104
|
-
throw new TypeError(
|
|
105
|
-
"[router.subscribe] Expected a function. " +
|
|
106
|
-
"For Observable pattern use @real-router/rx package",
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
static validateSubscribeLeaveListener(listener: unknown): void {
|
|
112
|
-
if (typeof listener !== "function") {
|
|
113
|
-
throw new TypeError("[router.subscribeLeave] Expected a function");
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
emitRouterStart(): void {
|
|
118
|
-
this.#emitter.emit(events.ROUTER_START);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
emitRouterStop(): void {
|
|
122
|
-
this.#emitter.emit(events.ROUTER_STOP);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
emitTransitionStart(toState: State, fromState?: State): void {
|
|
126
|
-
this.#emitter.emit(events.TRANSITION_START, toState, fromState);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
emitTransitionSuccess(
|
|
130
|
-
toState: State,
|
|
131
|
-
fromState?: State,
|
|
132
|
-
opts?: NavigationOptions,
|
|
133
|
-
): void {
|
|
134
|
-
this.#emitter.emit(events.TRANSITION_SUCCESS, toState, fromState, opts);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
emitTransitionError(
|
|
138
|
-
toState?: State,
|
|
139
|
-
fromState?: State,
|
|
140
|
-
error?: RouterError,
|
|
141
|
-
): void {
|
|
142
|
-
this.#emitter.emit(events.TRANSITION_ERROR, toState, fromState, error);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
emitTransitionCancel(toState: State, fromState?: State): void {
|
|
146
|
-
this.#emitter.emit(events.TRANSITION_CANCEL, toState, fromState);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
emitTransitionLeaveApprove(toState: State, fromState?: State): void {
|
|
150
|
-
this.#emitter.emit(events.TRANSITION_LEAVE_APPROVE, toState, fromState);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Emits the internal `TREE_CHANGED` event after a structural route-tree
|
|
155
|
-
* mutation. Reuses the shared `EventEmitter` — so depth tracking
|
|
156
|
-
* (`maxEventDepth`) and per-listener error isolation (`onListenerError`)
|
|
157
|
-
* apply automatically.
|
|
158
|
-
*/
|
|
159
|
-
emitTreeChanged(event: TreeChangedEvent): void {
|
|
160
|
-
this.#emitter.emit(TREE_CHANGED, event);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Subscribes to `TREE_CHANGED`. **Lenient** duplicate semantics (mirrors
|
|
165
|
-
* {@link subscribe}): each call wraps the handler in a fresh closure, so N
|
|
166
|
-
* registrations of the same reference produce N independent subscriptions.
|
|
167
|
-
*/
|
|
168
|
-
subscribeTreeChanged(
|
|
169
|
-
handler: (event: TreeChangedEvent) => void,
|
|
170
|
-
): Unsubscribe {
|
|
171
|
-
return this.#emitter.on(TREE_CHANGED, (event: TreeChangedEvent) => {
|
|
172
|
-
handler(event);
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/** Number of active `TREE_CHANGED` listeners (drives conditional emit). */
|
|
177
|
-
treeChangedListenerCount(): number {
|
|
178
|
-
return this.#emitter.listenerCount(TREE_CHANGED);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
sendStart(): void {
|
|
182
|
-
this.#fsm.send(routerEvents.START);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
sendStop(): void {
|
|
186
|
-
this.#fsm.send(routerEvents.STOP);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
sendDispose(): void {
|
|
190
|
-
this.#fsm.send(routerEvents.DISPOSE);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
sendStarted(): void {
|
|
194
|
-
this.#fsm.send(routerEvents.STARTED);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
sendNavigate(toState: State, fromState?: State): void {
|
|
198
|
-
this.#currentToState = toState;
|
|
199
|
-
// Bypass FSM dispatch — forceState + direct emit (no action lookup, no rest params)
|
|
200
|
-
this.#fsm.forceState(routerStates.TRANSITION_STARTED);
|
|
201
|
-
this.emitTransitionStart(toState, fromState);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
sendComplete(
|
|
205
|
-
state: State,
|
|
206
|
-
fromState?: State,
|
|
207
|
-
opts: NavigationOptions = {},
|
|
208
|
-
): void {
|
|
209
|
-
// Bypass FSM dispatch — forceState + direct emit
|
|
210
|
-
this.#fsm.forceState(routerStates.READY);
|
|
211
|
-
this.emitTransitionSuccess(state, fromState, opts);
|
|
212
|
-
|
|
213
|
-
if (this.#currentToState === state) {
|
|
214
|
-
this.#currentToState = undefined;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
sendLeaveApprove(toState: State, fromState?: State): void {
|
|
219
|
-
// Bypass FSM dispatch — forceState + direct emit (no action lookup, no rest params)
|
|
220
|
-
this.#fsm.forceState(routerStates.LEAVE_APPROVED);
|
|
221
|
-
this.emitTransitionLeaveApprove(toState, fromState);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
sendFail(toState?: State, fromState?: State, error?: unknown): void {
|
|
225
|
-
const prev = this.#currentToState;
|
|
226
|
-
|
|
227
|
-
this.#pendingToState = toState;
|
|
228
|
-
this.#pendingFromState = fromState;
|
|
229
|
-
this.#pendingError = error;
|
|
230
|
-
this.#fsm.send(routerEvents.FAIL);
|
|
231
|
-
|
|
232
|
-
if (this.#currentToState === prev) {
|
|
233
|
-
this.#currentToState = undefined;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
sendFailSafe(toState?: State, fromState?: State, error?: unknown): void {
|
|
238
|
-
if (this.isReady()) {
|
|
239
|
-
this.sendFail(toState, fromState, error);
|
|
240
|
-
} else {
|
|
241
|
-
this.emitTransitionError(toState, fromState, error as RouterError);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
sendCancel(toState: State, fromState?: State): void {
|
|
246
|
-
const prev = this.#currentToState;
|
|
247
|
-
|
|
248
|
-
this.#pendingToState = toState;
|
|
249
|
-
this.#pendingFromState = fromState;
|
|
250
|
-
this.#fsm.send(routerEvents.CANCEL);
|
|
251
|
-
|
|
252
|
-
if (this.#currentToState === prev) {
|
|
253
|
-
this.#currentToState = undefined;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
canBeginTransition(): boolean {
|
|
258
|
-
return this.#fsm.canSend(routerEvents.NAVIGATE);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
canStart(): boolean {
|
|
262
|
-
return this.#fsm.canSend(routerEvents.START);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
canCancel(): boolean {
|
|
266
|
-
return this.#fsm.canSend(routerEvents.CANCEL);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
isActive(): boolean {
|
|
270
|
-
const fsmState = this.#fsm.getState();
|
|
271
|
-
|
|
272
|
-
return fsmState !== routerStates.IDLE && fsmState !== routerStates.DISPOSED;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
isDisposed(): boolean {
|
|
276
|
-
return this.#fsm.getState() === routerStates.DISPOSED;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
isTransitioning(): boolean {
|
|
280
|
-
const state = this.#fsm.getState();
|
|
281
|
-
|
|
282
|
-
return (
|
|
283
|
-
state === routerStates.TRANSITION_STARTED ||
|
|
284
|
-
state === routerStates.LEAVE_APPROVED
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
isLeaveApproved(): boolean {
|
|
289
|
-
return this.#fsm.getState() === routerStates.LEAVE_APPROVED;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
isReady(): boolean {
|
|
293
|
-
return this.#fsm.getState() === routerStates.READY;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
isStarting(): boolean {
|
|
297
|
-
return this.#fsm.getState() === routerStates.STARTING;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
getCurrentToState(): State | undefined {
|
|
301
|
-
return this.#currentToState;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Plugin-author API for subscribing to internal router events.
|
|
306
|
-
*
|
|
307
|
-
* @remarks
|
|
308
|
-
*
|
|
309
|
-
* **Duplicate-registration semantics — strict (throws).** Passing the same
|
|
310
|
-
* callback reference twice for the same event throws
|
|
311
|
-
* `Error("Duplicate listener for ...")` from the underlying `EventEmitter`.
|
|
312
|
-
* This is loud-on-misuse by design: plugin code is expected to register
|
|
313
|
-
* each callback once. The contract differs from {@link subscribe} /
|
|
314
|
-
* {@link subscribeLeave}, which are end-user surfaces and silently accept
|
|
315
|
-
* duplicates.
|
|
316
|
-
*/
|
|
317
|
-
addEventListener<E extends EventName>(
|
|
318
|
-
eventName: E,
|
|
319
|
-
cb: Plugin[EventMethodMap[E]],
|
|
320
|
-
): Unsubscribe {
|
|
321
|
-
return this.#emitter.on(
|
|
322
|
-
eventName,
|
|
323
|
-
cb as (...args: RouterEventMap[typeof eventName]) => void,
|
|
324
|
-
);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* End-user / UI-binding API for subscribing to successful transitions.
|
|
329
|
-
*
|
|
330
|
-
* @remarks
|
|
331
|
-
*
|
|
332
|
-
* **Duplicate-registration semantics — independent.** Each call wraps
|
|
333
|
-
* `listener` in a fresh closure and registers it as a distinct internal
|
|
334
|
-
* slot. `router.subscribe(fn)` twice produces **two** active subscriptions;
|
|
335
|
-
* `fn` fires twice per `TRANSITION_SUCCESS`. The returned `Unsubscribe` is
|
|
336
|
-
* paired with its specific call — invoking it removes exactly that
|
|
337
|
-
* registration.
|
|
338
|
-
*
|
|
339
|
-
* This contract differs from {@link addEventListener} (plugin API, throws
|
|
340
|
-
* on duplicate). End-user code that wants idempotent registration must
|
|
341
|
-
* gate itself, e.g. `if (!unsub) unsub = router.subscribe(fn);`.
|
|
342
|
-
*/
|
|
343
|
-
subscribe(listener: SubscribeFn): Unsubscribe {
|
|
344
|
-
return this.#emitter.on(
|
|
345
|
-
events.TRANSITION_SUCCESS,
|
|
346
|
-
(toState: State, fromState?: State) => {
|
|
347
|
-
listener({ route: toState, previousRoute: fromState });
|
|
348
|
-
},
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* End-user / UI-binding API for subscribing to confirmed route departures
|
|
354
|
-
* (`LEAVE_APPROVED` phase). Async listeners block the activation phase.
|
|
355
|
-
*
|
|
356
|
-
* @remarks
|
|
357
|
-
*
|
|
358
|
-
* **Duplicate-registration semantics — independent (with internal quirk).**
|
|
359
|
-
* Each call pushes `listener` onto the internal array; `router.subscribeLeave(fn)`
|
|
360
|
-
* twice produces two array entries. `fn` fires **once** per leave when
|
|
361
|
-
* iteration snapshots the array (a snapshot is taken on entry to
|
|
362
|
-
* `awaitLeaveListeners`), but the function reference is invoked once per
|
|
363
|
-
* array slot — so in practice the wrapper fires twice through the same
|
|
364
|
-
* closure (no observable difference for stateless `fn`).
|
|
365
|
-
*
|
|
366
|
-
* The returned `Unsubscribe` removes the **first** array entry matching the
|
|
367
|
-
* function reference (`indexOf` semantic), not the most recently added one.
|
|
368
|
-
* Net effect of N subscribes + M unsubscribes is correct (N - M entries
|
|
369
|
-
* remain), but the specific physical entry that survives is reverse of the
|
|
370
|
-
* unsubscribe-call order. Irrelevant in practice — the function reference
|
|
371
|
-
* is the same; observable behaviour is identical regardless of which
|
|
372
|
-
* physical entry is removed.
|
|
373
|
-
*
|
|
374
|
-
* Contract differs from {@link addEventListener} (throws on duplicate).
|
|
375
|
-
* For idempotent registration, gate at the call site.
|
|
376
|
-
*/
|
|
377
|
-
subscribeLeave(listener: LeaveFn): Unsubscribe {
|
|
378
|
-
this.#leaveListeners.push(listener);
|
|
379
|
-
|
|
380
|
-
return () => {
|
|
381
|
-
const idx = this.#leaveListeners.indexOf(listener);
|
|
382
|
-
|
|
383
|
-
if (idx !== -1) {
|
|
384
|
-
this.#leaveListeners.splice(idx, 1);
|
|
385
|
-
}
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
hasLeaveListeners(): boolean {
|
|
390
|
-
return this.#leaveListeners.length > 0;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
awaitLeaveListeners(
|
|
394
|
-
toState: State,
|
|
395
|
-
fromState: State | undefined,
|
|
396
|
-
signal: AbortSignal,
|
|
397
|
-
): Promise<void> | undefined {
|
|
398
|
-
if (fromState === undefined) {
|
|
399
|
-
return undefined;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Freeze the payload wrapper so listeners cannot mutate it (`payload.route`
|
|
403
|
-
// is already deep-frozen via the State immutability invariant; this closes
|
|
404
|
-
// the wrapper-mutation gap surfaced by audit `probe-05-payload-frozen`).
|
|
405
|
-
const leaveState: LeaveState = Object.freeze({
|
|
406
|
-
route: fromState,
|
|
407
|
-
nextRoute: toState,
|
|
408
|
-
signal,
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
let promises: Promise<void>[] | undefined;
|
|
412
|
-
let firstSyncError: unknown;
|
|
413
|
-
|
|
414
|
-
// Snapshot before iteration — a listener that reentrantly calls
|
|
415
|
-
// `subscribeLeave(newFn)` or its own `unsubscribe()` must not affect the
|
|
416
|
-
// current emit cycle. Symmetric with the EventEmitter snapshot invariant
|
|
417
|
-
// (PR #666 / #659). Use `Array.from` rather than `[...array]` to keep the
|
|
418
|
-
// intent explicit (some lint rules treat spread-of-own-array as
|
|
419
|
-
// redundant and silently revert it).
|
|
420
|
-
const snapshot = [...this.#leaveListeners];
|
|
421
|
-
|
|
422
|
-
for (const listener of snapshot) {
|
|
423
|
-
try {
|
|
424
|
-
const result = listener(leaveState);
|
|
425
|
-
|
|
426
|
-
if (result !== undefined && typeof result.then === "function") {
|
|
427
|
-
promises ??= [];
|
|
428
|
-
promises.push(result);
|
|
429
|
-
}
|
|
430
|
-
} catch (error: unknown) {
|
|
431
|
-
if (firstSyncError === undefined) {
|
|
432
|
-
firstSyncError = error;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
if (promises === undefined) {
|
|
438
|
-
if (firstSyncError !== undefined) {
|
|
439
|
-
throw ensureError(firstSyncError);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
return undefined;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
return settleLeavePromises(promises, firstSyncError, signal);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
clearAll(): void {
|
|
449
|
-
this.#emitter.clearAll();
|
|
450
|
-
this.#leaveListeners.length = 0;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
setLimits(limits: {
|
|
454
|
-
maxListeners: number;
|
|
455
|
-
warnListeners: number;
|
|
456
|
-
maxEventDepth: number;
|
|
457
|
-
}): void {
|
|
458
|
-
this.#emitter.setLimits(limits);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
sendCancelIfPossible(fromState: State | undefined): void {
|
|
462
|
-
const toState = this.#currentToState;
|
|
463
|
-
|
|
464
|
-
if (!this.canCancel() || toState === undefined) {
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
this.sendCancel(toState, fromState);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
#emitPendingError(): void {
|
|
472
|
-
this.emitTransitionError(
|
|
473
|
-
this.#pendingToState,
|
|
474
|
-
this.#pendingFromState,
|
|
475
|
-
this.#pendingError as RouterError | undefined,
|
|
476
|
-
);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
#setupFSMActions(): void {
|
|
480
|
-
const fsm = this.#fsm;
|
|
481
|
-
|
|
482
|
-
fsm.on(routerStates.STARTING, routerEvents.STARTED, () => {
|
|
483
|
-
this.emitRouterStart();
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
fsm.on(routerStates.READY, routerEvents.STOP, () => {
|
|
487
|
-
this.emitRouterStop();
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
// NAVIGATE and COMPLETE actions bypassed — sendNavigate/sendComplete
|
|
491
|
-
// use fsm.forceState() + direct emit for zero-allocation hot path.
|
|
492
|
-
const handleCancel = () => {
|
|
493
|
-
const toState = this.#pendingToState;
|
|
494
|
-
|
|
495
|
-
/* v8 ignore next -- @preserve: #pendingToState guaranteed set by sendCancel before send() */
|
|
496
|
-
if (toState === undefined) {
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
this.emitTransitionCancel(toState, this.#pendingFromState);
|
|
501
|
-
};
|
|
502
|
-
|
|
503
|
-
fsm.on(routerStates.TRANSITION_STARTED, routerEvents.CANCEL, handleCancel);
|
|
504
|
-
fsm.on(routerStates.LEAVE_APPROVED, routerEvents.CANCEL, handleCancel);
|
|
505
|
-
|
|
506
|
-
fsm.on(routerStates.LEAVE_APPROVED, routerEvents.FAIL, () => {
|
|
507
|
-
this.#emitPendingError();
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
fsm.on(routerStates.STARTING, routerEvents.FAIL, () => {
|
|
511
|
-
this.#emitPendingError();
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
fsm.on(routerStates.READY, routerEvents.FAIL, () => {
|
|
515
|
-
this.#emitPendingError();
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
fsm.on(routerStates.TRANSITION_STARTED, routerEvents.FAIL, () => {
|
|
519
|
-
this.#emitPendingError();
|
|
520
|
-
});
|
|
521
|
-
}
|
|
522
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
// packages/core/src/namespaces/EventBusNamespace/types.ts
|
|
2
|
-
|
|
3
|
-
import type { RouterEvent, RouterPayloads, RouterState } from "../../fsm";
|
|
4
|
-
import type { RouterEventMap } from "../../types";
|
|
5
|
-
import type { FSM } from "@real-router/fsm";
|
|
6
|
-
import type { EventEmitter } from "event-emitter";
|
|
7
|
-
|
|
8
|
-
export interface EventBusOptions {
|
|
9
|
-
routerFSM: FSM<RouterState, RouterEvent, null, RouterPayloads>;
|
|
10
|
-
emitter: EventEmitter<RouterEventMap>;
|
|
11
|
-
}
|