Skip to content

Commit f889135

Browse files
author
Robert Jackson
authored
Merge pull request #309 from tildeio/absorb-transition-aborted
2 parents 867f632 + 38f4ca2 commit f889135

File tree

7 files changed

+75
-63
lines changed

7 files changed

+75
-63
lines changed

lib/router/route-info.ts

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,12 @@ import InternalTransition, {
99
QUERY_PARAMS_SYMBOL,
1010
} from './transition';
1111
import { isParam, isPromise, merge } from './utils';
12+
import { throwIfAborted } from './transition-aborted-error';
1213

1314
interface IModel {
1415
id?: string | number;
1516
}
1617

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-
3618
export interface Route {
3719
inaccessibleByURL?: boolean;
3820
routeName: string;
@@ -241,11 +223,17 @@ export default class InternalRouteInfo<T extends Route> {
241223

242224
resolve(transition: InternalTransition<T>): Promise<ResolvedRouteInfo<T>> {
243225
return Promise.resolve(this.routePromise)
244-
.then((route: Route) => checkForAbort(transition, route))
226+
.then((route: Route) => {
227+
throwIfAborted(transition);
228+
return route;
229+
})
245230
.then(() => this.runBeforeModelHook(transition))
246-
.then(() => checkForAbort(transition, null))
231+
.then(() => throwIfAborted(transition))
247232
.then(() => this.getModel(transition))
248-
.then((resolvedModel) => checkForAbort(transition, resolvedModel))
233+
.then((resolvedModel) => {
234+
throwIfAborted(transition);
235+
return resolvedModel;
236+
})
249237
.then((resolvedModel) => this.runAfterModelHook(transition, resolvedModel))
250238
.then((resolvedModel) => this.becomeResolved(transition, resolvedModel));
251239
}

lib/router/router.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import InternalTransition, {
1414
QUERY_PARAMS_SYMBOL,
1515
STATE_SYMBOL,
1616
} from './transition';
17-
import TransitionAbortedError from './transition-aborted-error';
17+
import { throwIfAborted, isTransitionAborted } from './transition-aborted-error';
1818
import { TransitionIntent } from './transition-intent';
1919
import NamedTransitionIntent from './transition-intent/named-transition-intent';
2020
import URLTransitionIntent from './transition-intent/url-transition-intent';
@@ -371,7 +371,7 @@ export default abstract class Router<T extends Route> {
371371
// Resolve with the final route.
372372
return routeInfos[routeInfos.length - 1].route!;
373373
} catch (e) {
374-
if (!(e instanceof TransitionAbortedError)) {
374+
if (!isTransitionAborted(e)) {
375375
let infos = transition[STATE_SYMBOL]!.routeInfos;
376376
transition.trigger(true, 'error', e, transition, infos[infos.length - 1].route);
377377
transition.abort();
@@ -523,9 +523,7 @@ export default abstract class Router<T extends Route> {
523523
}
524524
}
525525

526-
if (transition && transition.isAborted) {
527-
throw new TransitionAbortedError();
528-
}
526+
throwIfAborted(transition);
529527

530528
route.context = context;
531529

@@ -537,9 +535,7 @@ export default abstract class Router<T extends Route> {
537535
route.setup(context!, transition!);
538536
}
539537

540-
if (transition && transition.isAborted) {
541-
throw new TransitionAbortedError();
542-
}
538+
throwIfAborted(transition);
543539

544540
currentRouteInfos.push(routeInfo);
545541
return route;
Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,42 @@
1-
export interface TransitionAbortedErrorConstructor {
2-
new (message?: string): ITransitionAbortedError;
3-
readonly prototype: ITransitionAbortedError;
1+
export interface TransitionAbortedError extends Error {
2+
name: 'TransitionAborted';
3+
code: 'TRANSITION_ABORTED';
44
}
55

6-
export interface ITransitionAbortedError extends Error {
7-
constructor: TransitionAbortedErrorConstructor;
6+
export function isTransitionAborted(maybeError: unknown): maybeError is TransitionAbortedError {
7+
return (
8+
typeof maybeError === 'object' &&
9+
maybeError !== null &&
10+
(maybeError as TransitionAbortedError).code === 'TRANSITION_ABORTED'
11+
);
812
}
913

10-
const TransitionAbortedError: TransitionAbortedErrorConstructor = (function () {
11-
TransitionAbortedError.prototype = Object.create(Error.prototype);
12-
TransitionAbortedError.prototype.constructor = TransitionAbortedError;
13-
14-
function TransitionAbortedError(this: ITransitionAbortedError, message?: string) {
15-
let error = Error.call(this, message);
16-
this.name = 'TransitionAborted';
17-
this.message = message || 'TransitionAborted';
14+
interface Abortable<T extends boolean> {
15+
isAborted: T;
16+
[key: string]: unknown;
17+
}
1818

19-
if (Error.captureStackTrace) {
20-
Error.captureStackTrace(this, TransitionAbortedError);
21-
} else {
22-
this.stack = error.stack;
23-
}
24-
}
19+
function isAbortable<T extends boolean>(maybeAbortable: unknown): maybeAbortable is Abortable<T> {
20+
return (
21+
typeof maybeAbortable === 'object' &&
22+
maybeAbortable !== null &&
23+
typeof (maybeAbortable as Abortable<T>).isAborted === 'boolean'
24+
);
25+
}
2526

26-
return TransitionAbortedError as any;
27-
})();
27+
export function buildTransitionAborted() {
28+
let error = new Error('TransitionAborted') as TransitionAbortedError;
29+
error.name = 'TransitionAborted';
30+
error.code = 'TRANSITION_ABORTED';
31+
return error;
32+
}
2833

29-
export default TransitionAbortedError;
34+
export function throwIfAborted<T extends boolean>(
35+
maybe: Abortable<T>
36+
): T extends true ? never : void;
37+
export function throwIfAborted(maybe: unknown): void;
38+
export function throwIfAborted(maybe: unknown | Abortable<boolean>): never | void {
39+
if (isAbortable(maybe) && maybe.isAborted) {
40+
throw buildTransitionAborted();
41+
}
42+
}

lib/router/transition-state.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Dict } from './core';
33
import InternalRouteInfo, { Route, ResolvedRouteInfo } from './route-info';
44
import Transition from './transition';
55
import { forEach, promiseLabel } from './utils';
6+
import { throwIfAborted } from './transition-aborted-error';
67

78
interface IParams {
89
[key: string]: unknown;
@@ -72,9 +73,7 @@ function proceed<T extends Route>(
7273

7374
// Proceed after ensuring that the redirect hook
7475
// didn't abort this transition by transitioning elsewhere.
75-
if (transition.isAborted) {
76-
throw new Error('Transition aborted');
77-
}
76+
throwIfAborted(transition);
7877

7978
return resolveOneRouteInfo(currentState, transition);
8079
}

lib/router/transition.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Promise } from 'rsvp';
22
import { Dict, Maybe, Option } from './core';
33
import InternalRouteInfo, { Route, RouteInfo, RouteInfoWithAttributes } from './route-info';
44
import Router from './router';
5-
import TransitionAborted, { ITransitionAbortedError } from './transition-aborted-error';
5+
import { TransitionAbortedError, buildTransitionAborted } from './transition-aborted-error';
66
import { OpaqueIntent } from './transition-intent';
77
import TransitionState, { TransitionError } from './transition-state';
88
import { log, promiseLabel } from './utils';
@@ -439,9 +439,10 @@ export default class Transition<T extends Route> implements Partial<Promise<T>>
439439
440440
Logs and returns an instance of TransitionAborted.
441441
*/
442-
export function logAbort(transition: Transition<any>): ITransitionAbortedError {
442+
export function logAbort(transition: Transition<any>): TransitionAbortedError {
443443
log(transition.router, transition.sequence, 'detected abort.');
444-
return new TransitionAborted();
444+
445+
return buildTransitionAborted();
445446
}
446447

447448
export function isTransition(obj: unknown): obj is typeof Transition {

tests/test_helpers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import Router, { Route, Transition } from 'router';
33
import { Dict } from 'router/core';
44
import RouteInfo, { UnresolvedRouteInfoByParam } from 'router/route-info';
55
import { logAbort, PublicTransition } from 'router/transition';
6-
import TransitionAbortedError from 'router/transition-aborted-error';
76
import { TransitionError } from 'router/transition-state';
87
import { UnrecognizedURLError } from 'router/unrecognized-url-error';
98
import { configure, resolve } from 'rsvp';
9+
import { isTransitionAborted } from 'router/transition-aborted-error';
1010

1111
QUnit.config.testTimeout = 1000;
1212

@@ -45,7 +45,7 @@ function module(name: string, options?: any) {
4545

4646
function assertAbort(assert: Assert) {
4747
return function _assertAbort(e: Error) {
48-
assert.ok(e instanceof TransitionAbortedError, 'transition was redirected/aborted');
48+
assert.ok(isTransitionAborted(e), 'transition was redirected/aborted');
4949
};
5050
}
5151

tests/transition-aborted-error_test.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import TransitionAbortedError from 'router/transition-aborted-error';
1+
import {
2+
throwIfAborted,
3+
isTransitionAborted,
4+
buildTransitionAborted,
5+
} from 'router/transition-aborted-error';
26
import { module, test } from './test_helpers';
37

48
module('transition-aborted-error');
@@ -7,7 +11,7 @@ test('correct inheritance and name', function (assert) {
711
let error;
812

913
try {
10-
throw new TransitionAbortedError('Message');
14+
throw buildTransitionAborted();
1115
} catch (e) {
1216
error = e;
1317
}
@@ -19,6 +23,17 @@ test('correct inheritance and name', function (assert) {
1923
"TransitionAbortedError has the name 'TransitionAborted'"
2024
);
2125

22-
assert.ok(error instanceof TransitionAbortedError);
26+
assert.ok(isTransitionAborted(error));
2327
assert.ok(error instanceof Error);
2428
});
29+
30+
test('throwIfAborted', function (assert) {
31+
throwIfAborted(undefined);
32+
throwIfAborted(null);
33+
throwIfAborted({});
34+
throwIfAborted({ apple: false });
35+
throwIfAborted({ isAborted: false });
36+
throwIfAborted({ isAborted: false, other: 'key' });
37+
assert.throws(() => throwIfAborted({ isAborted: true }), /TransitionAborted/);
38+
assert.throws(() => throwIfAborted({ isAborted: true, other: 'key' }), /TransitionAborted/);
39+
});

0 commit comments

Comments
 (0)