Skip to content

Commit

Permalink
Migrate TransitionAbortedError to builder + interface.
Browse files Browse the repository at this point in the history
* Avoid trying to manually extend from a native error, instead use a
normal `Error` and add our name/code to it.
* introduce structurally Abortable concept
  • Loading branch information
rwjblue authored and stefanpenner committed Nov 10, 2020
1 parent 867f632 commit 38f4ca2
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 63 deletions.
32 changes: 10 additions & 22 deletions lib/router/route-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,12 @@ import InternalTransition, {
QUERY_PARAMS_SYMBOL,
} from './transition';
import { isParam, isPromise, merge } from './utils';
import { throwIfAborted } from './transition-aborted-error';

interface IModel {
id?: string | number;
}

interface AbortableTransition<T extends boolean, R extends Route> extends InternalTransition<R> {
isAborted: T;
}

function checkForAbort<U, T extends AbortableTransition<I, R>, R extends Route, I extends boolean>(
transition: T,
value: U
): I extends true ? never : U;
function checkForAbort<U, T extends AbortableTransition<I, R>, R extends Route, I extends boolean>(
transition: T,
value: U
): never | U {
if (transition.isAborted) {
throw new Error('Transition aborted');
}

return value;
}

export interface Route {
inaccessibleByURL?: boolean;
routeName: string;
Expand Down Expand Up @@ -241,11 +223,17 @@ export default class InternalRouteInfo<T extends Route> {

resolve(transition: InternalTransition<T>): Promise<ResolvedRouteInfo<T>> {
return Promise.resolve(this.routePromise)
.then((route: Route) => checkForAbort(transition, route))
.then((route: Route) => {
throwIfAborted(transition);
return route;
})
.then(() => this.runBeforeModelHook(transition))
.then(() => checkForAbort(transition, null))
.then(() => throwIfAborted(transition))
.then(() => this.getModel(transition))
.then((resolvedModel) => checkForAbort(transition, resolvedModel))
.then((resolvedModel) => {
throwIfAborted(transition);
return resolvedModel;
})
.then((resolvedModel) => this.runAfterModelHook(transition, resolvedModel))
.then((resolvedModel) => this.becomeResolved(transition, resolvedModel));
}
Expand Down
12 changes: 4 additions & 8 deletions lib/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import InternalTransition, {
QUERY_PARAMS_SYMBOL,
STATE_SYMBOL,
} from './transition';
import TransitionAbortedError from './transition-aborted-error';
import { throwIfAborted, isTransitionAborted } from './transition-aborted-error';
import { TransitionIntent } from './transition-intent';
import NamedTransitionIntent from './transition-intent/named-transition-intent';
import URLTransitionIntent from './transition-intent/url-transition-intent';
Expand Down Expand Up @@ -371,7 +371,7 @@ export default abstract class Router<T extends Route> {
// Resolve with the final route.
return routeInfos[routeInfos.length - 1].route!;
} catch (e) {
if (!(e instanceof TransitionAbortedError)) {
if (!isTransitionAborted(e)) {
let infos = transition[STATE_SYMBOL]!.routeInfos;
transition.trigger(true, 'error', e, transition, infos[infos.length - 1].route);
transition.abort();
Expand Down Expand Up @@ -523,9 +523,7 @@ export default abstract class Router<T extends Route> {
}
}

if (transition && transition.isAborted) {
throw new TransitionAbortedError();
}
throwIfAborted(transition);

route.context = context;

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

if (transition && transition.isAborted) {
throw new TransitionAbortedError();
}
throwIfAborted(transition);

currentRouteInfos.push(routeInfo);
return route;
Expand Down
57 changes: 35 additions & 22 deletions lib/router/transition-aborted-error.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,42 @@
export interface TransitionAbortedErrorConstructor {
new (message?: string): ITransitionAbortedError;
readonly prototype: ITransitionAbortedError;
export interface TransitionAbortedError extends Error {
name: 'TransitionAborted';
code: 'TRANSITION_ABORTED';
}

export interface ITransitionAbortedError extends Error {
constructor: TransitionAbortedErrorConstructor;
export function isTransitionAborted(maybeError: unknown): maybeError is TransitionAbortedError {
return (
typeof maybeError === 'object' &&
maybeError !== null &&
(maybeError as TransitionAbortedError).code === 'TRANSITION_ABORTED'
);
}

const TransitionAbortedError: TransitionAbortedErrorConstructor = (function () {
TransitionAbortedError.prototype = Object.create(Error.prototype);
TransitionAbortedError.prototype.constructor = TransitionAbortedError;

function TransitionAbortedError(this: ITransitionAbortedError, message?: string) {
let error = Error.call(this, message);
this.name = 'TransitionAborted';
this.message = message || 'TransitionAborted';
interface Abortable<T extends boolean> {
isAborted: T;
[key: string]: unknown;
}

if (Error.captureStackTrace) {
Error.captureStackTrace(this, TransitionAbortedError);
} else {
this.stack = error.stack;
}
}
function isAbortable<T extends boolean>(maybeAbortable: unknown): maybeAbortable is Abortable<T> {
return (
typeof maybeAbortable === 'object' &&
maybeAbortable !== null &&
typeof (maybeAbortable as Abortable<T>).isAborted === 'boolean'
);
}

return TransitionAbortedError as any;
})();
export function buildTransitionAborted() {
let error = new Error('TransitionAborted') as TransitionAbortedError;
error.name = 'TransitionAborted';
error.code = 'TRANSITION_ABORTED';
return error;
}

export default TransitionAbortedError;
export function throwIfAborted<T extends boolean>(
maybe: Abortable<T>
): T extends true ? never : void;
export function throwIfAborted(maybe: unknown): void;
export function throwIfAborted(maybe: unknown | Abortable<boolean>): never | void {
if (isAbortable(maybe) && maybe.isAborted) {
throw buildTransitionAborted();
}
}
5 changes: 2 additions & 3 deletions lib/router/transition-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Dict } from './core';
import InternalRouteInfo, { Route, ResolvedRouteInfo } from './route-info';
import Transition from './transition';
import { forEach, promiseLabel } from './utils';
import { throwIfAborted } from './transition-aborted-error';

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

// Proceed after ensuring that the redirect hook
// didn't abort this transition by transitioning elsewhere.
if (transition.isAborted) {
throw new Error('Transition aborted');
}
throwIfAborted(transition);

return resolveOneRouteInfo(currentState, transition);
}
Expand Down
7 changes: 4 additions & 3 deletions lib/router/transition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Promise } from 'rsvp';
import { Dict, Maybe, Option } from './core';
import InternalRouteInfo, { Route, RouteInfo, RouteInfoWithAttributes } from './route-info';
import Router from './router';
import TransitionAborted, { ITransitionAbortedError } from './transition-aborted-error';
import { TransitionAbortedError, buildTransitionAborted } from './transition-aborted-error';
import { OpaqueIntent } from './transition-intent';
import TransitionState, { TransitionError } from './transition-state';
import { log, promiseLabel } from './utils';
Expand Down Expand Up @@ -439,9 +439,10 @@ export default class Transition<T extends Route> implements Partial<Promise<T>>
Logs and returns an instance of TransitionAborted.
*/
export function logAbort(transition: Transition<any>): ITransitionAbortedError {
export function logAbort(transition: Transition<any>): TransitionAbortedError {
log(transition.router, transition.sequence, 'detected abort.');
return new TransitionAborted();

return buildTransitionAborted();
}

export function isTransition(obj: unknown): obj is typeof Transition {
Expand Down
4 changes: 2 additions & 2 deletions tests/test_helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import Router, { Route, Transition } from 'router';
import { Dict } from 'router/core';
import RouteInfo, { UnresolvedRouteInfoByParam } from 'router/route-info';
import { logAbort, PublicTransition } from 'router/transition';
import TransitionAbortedError from 'router/transition-aborted-error';
import { TransitionError } from 'router/transition-state';
import { UnrecognizedURLError } from 'router/unrecognized-url-error';
import { configure, resolve } from 'rsvp';
import { isTransitionAborted } from 'router/transition-aborted-error';

QUnit.config.testTimeout = 1000;

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

function assertAbort(assert: Assert) {
return function _assertAbort(e: Error) {
assert.ok(e instanceof TransitionAbortedError, 'transition was redirected/aborted');
assert.ok(isTransitionAborted(e), 'transition was redirected/aborted');
};
}

Expand Down
21 changes: 18 additions & 3 deletions tests/transition-aborted-error_test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import TransitionAbortedError from 'router/transition-aborted-error';
import {
throwIfAborted,
isTransitionAborted,
buildTransitionAborted,
} from 'router/transition-aborted-error';
import { module, test } from './test_helpers';

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

try {
throw new TransitionAbortedError('Message');
throw buildTransitionAborted();
} catch (e) {
error = e;
}
Expand All @@ -19,6 +23,17 @@ test('correct inheritance and name', function (assert) {
"TransitionAbortedError has the name 'TransitionAborted'"
);

assert.ok(error instanceof TransitionAbortedError);
assert.ok(isTransitionAborted(error));
assert.ok(error instanceof Error);
});

test('throwIfAborted', function (assert) {
throwIfAborted(undefined);
throwIfAborted(null);
throwIfAborted({});
throwIfAborted({ apple: false });
throwIfAborted({ isAborted: false });
throwIfAborted({ isAborted: false, other: 'key' });
assert.throws(() => throwIfAborted({ isAborted: true }), /TransitionAborted/);
assert.throws(() => throwIfAborted({ isAborted: true, other: 'key' }), /TransitionAborted/);
});

0 comments on commit 38f4ca2

Please sign in to comment.