Skip to content

Commit 867f632

Browse files
author
Robert Jackson
authored
Merge pull request #306 from tildeio/refactor-transition-state-resolution
2 parents 0b6236e + c2e85f1 commit 867f632

File tree

5 files changed

+160
-195
lines changed

5 files changed

+160
-195
lines changed

lib/router/route-info.ts

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,25 @@ interface IModel {
1414
id?: string | number;
1515
}
1616

17+
interface AbortableTransition<T extends boolean, R extends Route> extends InternalTransition<R> {
18+
isAborted: T;
19+
}
20+
21+
function checkForAbort<U, T extends AbortableTransition<I, R>, R extends Route, I extends boolean>(
22+
transition: T,
23+
value: U
24+
): I extends true ? never : U;
25+
function checkForAbort<U, T extends AbortableTransition<I, R>, R extends Route, I extends boolean>(
26+
transition: T,
27+
value: U
28+
): never | U {
29+
if (transition.isAborted) {
30+
throw new Error('Transition aborted');
31+
}
32+
33+
return value;
34+
}
35+
1736
export interface Route {
1837
inaccessibleByURL?: boolean;
1938
routeName: string;
@@ -40,8 +59,6 @@ export interface Route {
4059
buildRouteInfoMetadata?(): unknown;
4160
}
4261

43-
export type Continuation = () => PromiseLike<boolean> | boolean;
44-
4562
export interface RouteInfo {
4663
readonly name: string;
4764
readonly parent: RouteInfo | RouteInfoWithAttributes | null;
@@ -222,16 +239,13 @@ export default class InternalRouteInfo<T extends Route> {
222239
return this.params || {};
223240
}
224241

225-
resolve(
226-
shouldContinue: Continuation,
227-
transition: InternalTransition<T>
228-
): Promise<ResolvedRouteInfo<T>> {
242+
resolve(transition: InternalTransition<T>): Promise<ResolvedRouteInfo<T>> {
229243
return Promise.resolve(this.routePromise)
230-
.then((route: Route) => this.checkForAbort(shouldContinue, route))
244+
.then((route: Route) => checkForAbort(transition, route))
231245
.then(() => this.runBeforeModelHook(transition))
232-
.then(() => this.checkForAbort(shouldContinue, null))
246+
.then(() => checkForAbort(transition, null))
233247
.then(() => this.getModel(transition))
234-
.then((resolvedModel) => this.checkForAbort(shouldContinue, resolvedModel))
248+
.then((resolvedModel) => checkForAbort(transition, resolvedModel))
235249
.then((resolvedModel) => this.runAfterModelHook(transition, resolvedModel))
236250
.then((resolvedModel) => this.becomeResolved(transition, resolvedModel));
237251
}
@@ -375,14 +389,6 @@ export default class InternalRouteInfo<T extends Route> {
375389
});
376390
}
377391

378-
private checkForAbort<T>(shouldContinue: Continuation, value: T) {
379-
return Promise.resolve(shouldContinue()).then(function () {
380-
// We don't care about shouldContinue's resolve value;
381-
// pass along the original value passed to this fn.
382-
return value;
383-
}, null);
384-
}
385-
386392
private stashResolvedModel(transition: InternalTransition<T>, resolvedModel: Dict<unknown>) {
387393
transition.resolvedModels = transition.resolvedModels || {};
388394
transition.resolvedModels[this.name] = resolvedModel;
@@ -429,10 +435,7 @@ export class ResolvedRouteInfo<T extends Route> extends InternalRouteInfo<T> {
429435
this.context = context;
430436
}
431437

432-
resolve(
433-
_shouldContinue: Continuation,
434-
transition: InternalTransition<T>
435-
): Promise<InternalRouteInfo<T>> {
438+
resolve(transition: InternalTransition<T>): Promise<InternalRouteInfo<T>> {
436439
// A ResolvedRouteInfo just resolved with itself.
437440
if (transition && transition.resolvedModels) {
438441
transition.resolvedModels[this.name] = this.context as Dict<unknown>;

lib/router/transition-state.ts

Lines changed: 79 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,84 @@
11
import { Promise } from 'rsvp';
22
import { Dict } from './core';
3-
import InternalRouteInfo, { Continuation, Route } from './route-info';
3+
import InternalRouteInfo, { Route, ResolvedRouteInfo } from './route-info';
44
import Transition from './transition';
55
import { forEach, promiseLabel } from './utils';
66

77
interface IParams {
88
[key: string]: unknown;
99
}
1010

11+
function handleError<T extends Route>(
12+
currentState: TransitionState<T>,
13+
transition: Transition<T>,
14+
error: Error
15+
): never {
16+
// This is the only possible
17+
// reject value of TransitionState#resolve
18+
let routeInfos = currentState.routeInfos;
19+
let errorHandlerIndex =
20+
transition.resolveIndex >= routeInfos.length ? routeInfos.length - 1 : transition.resolveIndex;
21+
22+
let wasAborted = transition.isAborted;
23+
24+
throw new TransitionError(
25+
error,
26+
currentState.routeInfos[errorHandlerIndex].route!,
27+
wasAborted,
28+
currentState
29+
);
30+
}
31+
32+
function resolveOneRouteInfo<T extends Route>(
33+
currentState: TransitionState<T>,
34+
transition: Transition<T>
35+
): void | Promise<void> {
36+
if (transition.resolveIndex === currentState.routeInfos.length) {
37+
// This is is the only possible
38+
// fulfill value of TransitionState#resolve
39+
return;
40+
}
41+
42+
let routeInfo = currentState.routeInfos[transition.resolveIndex];
43+
44+
return routeInfo
45+
.resolve(transition)
46+
.then(proceed.bind(null, currentState, transition), null, currentState.promiseLabel('Proceed'));
47+
}
48+
49+
function proceed<T extends Route>(
50+
currentState: TransitionState<T>,
51+
transition: Transition<T>,
52+
resolvedRouteInfo: ResolvedRouteInfo<T>
53+
): void | Promise<void> {
54+
let wasAlreadyResolved = currentState.routeInfos[transition.resolveIndex].isResolved;
55+
56+
// Swap the previously unresolved routeInfo with
57+
// the resolved routeInfo
58+
currentState.routeInfos[transition.resolveIndex++] = resolvedRouteInfo;
59+
60+
if (!wasAlreadyResolved) {
61+
// Call the redirect hook. The reason we call it here
62+
// vs. afterModel is so that redirects into child
63+
// routes don't re-run the model hooks for this
64+
// already-resolved route.
65+
let { route } = resolvedRouteInfo;
66+
if (route !== undefined) {
67+
if (route.redirect) {
68+
route.redirect(resolvedRouteInfo.context as Dict<unknown>, transition);
69+
}
70+
}
71+
}
72+
73+
// Proceed after ensuring that the redirect hook
74+
// didn't abort this transition by transitioning elsewhere.
75+
if (transition.isAborted) {
76+
throw new Error('Transition aborted');
77+
}
78+
79+
return resolveOneRouteInfo(currentState, transition);
80+
}
81+
1182
export default class TransitionState<T extends Route> {
1283
routeInfos: InternalRouteInfo<T>[] = [];
1384
queryParams: Dict<unknown> = {};
@@ -25,7 +96,7 @@ export default class TransitionState<T extends Route> {
2596
return promiseLabel("'" + targetName + "': " + label);
2697
}
2798

28-
resolve(shouldContinue: Continuation, transition: Transition<T>): Promise<TransitionState<T>> {
99+
resolve(transition: Transition<T>): Promise<TransitionState<T>> {
29100
// First, calculate params for this state. This is useful
30101
// information to provide to the various route hooks.
31102
let params = this.params;
@@ -36,87 +107,15 @@ export default class TransitionState<T extends Route> {
36107

37108
transition.resolveIndex = 0;
38109

39-
let currentState = this;
40-
let wasAborted = false;
41-
42110
// The prelude RSVP.resolve() async moves us into the promise land.
43111
return Promise.resolve(null, this.promiseLabel('Start transition'))
44-
.then(resolveOneRouteInfo, null, this.promiseLabel('Resolve route'))
45-
.catch(handleError, this.promiseLabel('Handle error'));
46-
47-
function innerShouldContinue() {
48-
return Promise.resolve(
49-
shouldContinue(),
50-
currentState.promiseLabel('Check if should continue')
51-
).catch(function (reason) {
52-
// We distinguish between errors that occurred
53-
// during resolution (e.g. before"Model/model/afterModel),
54-
// and aborts due to a rejecting promise from shouldContinue().
55-
wasAborted = true;
56-
return Promise.reject(reason);
57-
}, currentState.promiseLabel('Handle abort'));
58-
}
59-
60-
function handleError(error: Error) {
61-
// This is the only possible
62-
// reject value of TransitionState#resolve
63-
let routeInfos = currentState.routeInfos;
64-
let errorHandlerIndex =
65-
transition.resolveIndex >= routeInfos.length
66-
? routeInfos.length - 1
67-
: transition.resolveIndex;
68-
return Promise.reject(
69-
new TransitionError(
70-
error,
71-
currentState.routeInfos[errorHandlerIndex].route!,
72-
wasAborted,
73-
currentState
74-
)
75-
);
76-
}
77-
78-
function proceed(resolvedRouteInfo: InternalRouteInfo<T>): Promise<InternalRouteInfo<T>> {
79-
let wasAlreadyResolved = currentState.routeInfos[transition.resolveIndex].isResolved;
80-
81-
// Swap the previously unresolved routeInfo with
82-
// the resolved routeInfo
83-
currentState.routeInfos[transition.resolveIndex++] = resolvedRouteInfo;
84-
85-
if (!wasAlreadyResolved) {
86-
// Call the redirect hook. The reason we call it here
87-
// vs. afterModel is so that redirects into child
88-
// routes don't re-run the model hooks for this
89-
// already-resolved route.
90-
let { route } = resolvedRouteInfo;
91-
if (route !== undefined) {
92-
if (route.redirect) {
93-
route.redirect(resolvedRouteInfo.context as Dict<unknown>, transition);
94-
}
95-
}
96-
}
97-
98-
// Proceed after ensuring that the redirect hook
99-
// didn't abort this transition by transitioning elsewhere.
100-
return innerShouldContinue().then(
101-
resolveOneRouteInfo,
112+
.then(
113+
resolveOneRouteInfo.bind(null, this, transition),
102114
null,
103-
currentState.promiseLabel('Resolve route')
104-
);
105-
}
106-
107-
function resolveOneRouteInfo(): TransitionState<T> | Promise<any> {
108-
if (transition.resolveIndex === currentState.routeInfos.length) {
109-
// This is is the only possible
110-
// fulfill value of TransitionState#resolve
111-
return currentState;
112-
}
113-
114-
let routeInfo = currentState.routeInfos[transition.resolveIndex];
115-
116-
return routeInfo
117-
.resolve(innerShouldContinue, transition)
118-
.then(proceed, null, currentState.promiseLabel('Proceed'));
119-
}
115+
this.promiseLabel('Resolve route')
116+
)
117+
.catch(handleError.bind(null, this, transition), this.promiseLabel('Handle error'))
118+
.then(() => this);
120119
}
121120
}
122121

lib/router/transition.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -169,17 +169,11 @@ export default class Transition<T extends Route> implements Partial<Promise<T>>
169169
}
170170

171171
this.sequence = router.currentSequence++;
172-
this.promise = state
173-
.resolve(() => {
174-
if (this.isAborted) {
175-
return Promise.reject(false, promiseLabel('Transition aborted - reject'));
176-
}
177-
178-
return Promise.resolve(true);
179-
}, this)
180-
.catch((result: TransitionError) => {
181-
return Promise.reject(this.router.transitionDidError(result, this));
182-
}, promiseLabel('Handle Abort'));
172+
this.promise = state.resolve(this).catch((result: TransitionError) => {
173+
let error = this.router.transitionDidError(result, this);
174+
175+
throw error;
176+
}, promiseLabel('Handle Abort'));
183177
} else {
184178
this.promise = Promise.resolve(this[STATE_SYMBOL]!);
185179
this[PARAMS_SYMBOL] = {};

0 commit comments

Comments
 (0)