Skip to content

Commit a7464bb

Browse files
feat(Transition): Normalize all transition errors to a Rejection.
BREAKING CHANGE: All Transition errors are now wrapped in a Rejection object. #### Before: Previously, if a transition hook returned a rejected promise: ```js .onStart({}, () => Promise.reject('reject transition')); ``` In `onError` or `transtion.promise.catch()`, the raw rejection was returned: ```js .onError({}, (trans, err) => err === 'reject transition') ``` #### Now: Now, the error is wrapped in a Rejection object. - The detail (thrown error or rejected value) is still available as `.detail`. ```js .onError({}, (trans, err) => err instanceof Rejection && err.detail === 'reject transition') ``` - The Rejection object indicates the `.type` of transition rejection (ABORTED, ERROR, SUPERSEDED and/or redirection). ```js .onError({}, (trans, err) => { err.type === RejectType.ABORTED === 3 }); ``` #### Motivation: Errors *thrown from* a hook and rejection values *returned from* a hook can now be processed in the same way. #### BC Likelihood How likely is this to affect me? Medium: apps which have onError handlers for rejected values #### BC Severity How severe is this change? Low: Find all error handlers (or .catch/.then chains) that do not understand Rejection. Add `err.detail` processing.
1 parent 05013a6 commit a7464bb

File tree

4 files changed

+86
-62
lines changed

4 files changed

+86
-62
lines changed

src/transition/transitionEventType.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@ export class TransitionEventType {
1616
public reverseSort: boolean = false,
1717
public getResultHandler: GetResultHandler = TransitionHook.HANDLE_RESULT,
1818
public getErrorHandler: GetErrorHandler = TransitionHook.REJECT_ERROR,
19-
public rejectIfSuperseded: boolean = true,
19+
public synchronous: boolean = false,
2020
) { }
2121
}

src/transition/transitionHook.ts

+73-51
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,21 @@
11
/**
22
* @coreapi
33
* @module transition
4-
*/ /** for typedoc */
5-
import {TransitionHookOptions, HookResult} from "./interface";
6-
import {defaults, noop, identity} from "../common/common";
7-
import {fnToString, maxLength} from "../common/strings";
8-
import {isPromise} from "../common/predicates";
9-
import {val, is, parse} from "../common/hof";
10-
import {trace} from "../common/trace";
11-
import {services} from "../common/coreservices";
12-
13-
import {Rejection} from "./rejectFactory";
14-
import {TargetState} from "../state/targetState";
15-
import {Transition} from "./transition";
16-
import {State} from "../state/stateObject";
17-
import {TransitionEventType} from "./transitionEventType";
18-
import {StateService} from "../state/stateService"; // has or is using
19-
import {RegisteredHook} from "./hookRegistry"; // has or is using
4+
*/
5+
/** for typedoc */
6+
import { TransitionHookOptions, HookResult } from './interface';
7+
import { defaults, noop, silentRejection } from '../common/common';
8+
import { fnToString, maxLength } from '../common/strings';
9+
import { isPromise } from '../common/predicates';
10+
import { is, parse } from '../common/hof';
11+
import { trace } from '../common/trace';
12+
import { services } from '../common/coreservices';
13+
import { Rejection } from './rejectFactory';
14+
import { TargetState } from '../state/targetState';
15+
import { Transition } from './transition';
16+
import { State } from '../state/stateObject';
17+
import { TransitionEventType } from './transitionEventType';
18+
import { RegisteredHook } from './hookRegistry'; // has or is using
2019

2120
let defaultOptions: TransitionHookOptions = {
2221
current: noop,
@@ -33,35 +32,52 @@ export type ErrorHandler = (error) => Promise<any>;
3332

3433
/** @hidden */
3534
export class TransitionHook {
35+
type: TransitionEventType;
3636
constructor(private transition: Transition,
3737
private stateContext: State,
3838
private registeredHook: RegisteredHook,
3939
private options: TransitionHookOptions) {
4040
this.options = defaults(options, defaultOptions);
41+
this.type = registeredHook.eventType;
4142
}
4243

43-
stateService = () => this.transition.router.stateService;
44+
/**
45+
* These GetResultHandler(s) are used by [[invokeHook]] below
46+
* Each HookType chooses a GetResultHandler (See: [[TransitionService._defineCoreEvents]])
47+
*/
48+
static HANDLE_RESULT: GetResultHandler = (hook: TransitionHook) => (result: HookResult) =>
49+
hook.handleHookResult(result);
4450

45-
static HANDLE_RESULT: GetResultHandler = (hook: TransitionHook) =>
46-
(result: HookResult) =>
47-
hook.handleHookResult(result);
51+
/**
52+
* If the result is a promise rejection, log it.
53+
* Otherwise, ignore the result.
54+
*/
55+
static LOG_REJECTED_RESULT: GetResultHandler = (hook: TransitionHook) => (result: HookResult) => {
56+
isPromise(result) && result.catch(err =>
57+
hook.logError(Rejection.normalize(err)));
58+
return undefined;
59+
};
4860

49-
static IGNORE_RESULT: GetResultHandler = (hook: TransitionHook) =>
50-
(result: HookResult) => undefined;
61+
/**
62+
* These GetErrorHandler(s) are used by [[invokeHook]] below
63+
* Each HookType chooses a GetErrorHandler (See: [[TransitionService._defineCoreEvents]])
64+
*/
65+
static LOG_ERROR: GetErrorHandler = (hook: TransitionHook) => (error) =>
66+
hook.logError(error);
5167

52-
static LOG_ERROR: GetErrorHandler = (hook: TransitionHook) =>
53-
(error) =>
54-
(hook.stateService().defaultErrorHandler()(error), undefined);
68+
static REJECT_ERROR: GetErrorHandler = (hook: TransitionHook) => (error) =>
69+
silentRejection(error);
5570

56-
static REJECT_ERROR: GetErrorHandler = (hook: TransitionHook) =>
57-
(error) =>
58-
Rejection.errored(error).toPromise();
71+
static THROW_ERROR: GetErrorHandler = (hook: TransitionHook) => (error) => {
72+
throw error;
73+
};
5974

60-
static THROW_ERROR: GetErrorHandler = (hook: TransitionHook) =>
61-
undefined;
75+
private isSuperseded = () =>
76+
!this.type.synchronous && this.options.current() !== this.options.transition;
6277

63-
private rejectIfSuperseded = () =>
64-
this.registeredHook.eventType.rejectIfSuperseded && this.options.current() !== this.options.transition;
78+
logError(err): any {
79+
this.transition.router.stateService.defaultErrorHandler()(err);
80+
}
6581

6682
invokeHook(): Promise<HookResult> {
6783
let hook = this.registeredHook;
@@ -71,30 +87,36 @@ export class TransitionHook {
7187
trace.traceHookInvocation(this, this.transition, options);
7288

7389
// A new transition started before this hook (from a previous transition) could be run.
74-
if (this.rejectIfSuperseded()) {
90+
if (this.isSuperseded()) {
7591
return Rejection.superseded(options.current()).toPromise();
7692
}
7793

78-
let cb = hook.callback;
79-
let bind = this.options.bind;
80-
let trans = this.transition;
81-
let state = this.stateContext;
94+
const invokeCallback = () =>
95+
hook.callback.call(this.options.bind, this.transition, this.stateContext);
8296

83-
let errorHandler = hook.eventType.getErrorHandler(this);
84-
let resultHandler = hook.eventType.getResultHandler(this);
85-
resultHandler = resultHandler || identity;
97+
const normalizeErr = err =>
98+
Rejection.normalize(err).toPromise();
8699

87-
if (!errorHandler) {
88-
return resultHandler(cb.call(bind, trans, state));
89-
}
100+
const handleError = err =>
101+
hook.eventType.getErrorHandler(this)(err);
102+
103+
const handleResult = result =>
104+
hook.eventType.getResultHandler(this)(result);
90105

91-
try {
92-
return resultHandler(cb.call(bind, trans, state));
93-
} catch (error) {
94-
return errorHandler(error);
106+
if (this.type.synchronous) {
107+
try {
108+
return handleResult(invokeCallback());
109+
} catch (err) {
110+
return handleError(Rejection.normalize(err));
111+
}
95112
}
113+
114+
return services.$q.when()
115+
.then(invokeCallback)
116+
.catch(normalizeErr)
117+
.then(handleResult, handleError);
96118
}
97-
119+
98120
/**
99121
* This method handles the return value of a Transition Hook.
100122
*
@@ -107,15 +129,15 @@ export class TransitionHook {
107129
handleHookResult(result: HookResult): Promise<HookResult> {
108130
// This transition is no longer current.
109131
// Another transition started while this hook was still running.
110-
if (this.rejectIfSuperseded()) {
132+
if (this.isSuperseded()) {
111133
// Abort this transition
112134
return Rejection.superseded(this.options.current()).toPromise();
113135
}
114136

115137
// Hook returned a promise
116138
if (isPromise(result)) {
117-
// Wait for the promise, then reprocess the settled promise value
118-
return result.then(this.handleHookResult.bind(this));
139+
// Wait for the promise, then reprocess with the resulting value
140+
return result.then(val => this.handleHookResult(val));
119141
}
120142

121143
trace.traceHookResult(result, this.transition, this.options);

src/transition/transitionService.ts

+8-6
Original file line numberDiff line numberDiff line change
@@ -235,19 +235,21 @@ export class TransitionService implements IHookRegistry, Disposable {
235235
const Phase = TransitionHookPhase;
236236
const TH = TransitionHook;
237237
const paths = this._criteriaPaths;
238+
const NORMAL_SORT = false, REVERSE_SORT = true;
239+
const ASYNCHRONOUS = false, SYNCHRONOUS = true;
238240

239-
this._defineEvent("onCreate", Phase.CREATE, 0, paths.to, false, TH.IGNORE_RESULT, TH.THROW_ERROR, false);
241+
this._defineEvent("onCreate", Phase.CREATE, 0, paths.to, NORMAL_SORT, TH.LOG_REJECTED_RESULT, TH.THROW_ERROR, SYNCHRONOUS);
240242

241243
this._defineEvent("onBefore", Phase.BEFORE, 0, paths.to);
242244

243245
this._defineEvent("onStart", Phase.RUN, 0, paths.to);
244-
this._defineEvent("onExit", Phase.RUN, 100, paths.exiting, true);
246+
this._defineEvent("onExit", Phase.RUN, 100, paths.exiting, REVERSE_SORT);
245247
this._defineEvent("onRetain", Phase.RUN, 200, paths.retained);
246248
this._defineEvent("onEnter", Phase.RUN, 300, paths.entering);
247249
this._defineEvent("onFinish", Phase.RUN, 400, paths.to);
248250

249-
this._defineEvent("onSuccess", Phase.SUCCESS, 0, paths.to, false, TH.IGNORE_RESULT, TH.LOG_ERROR, false);
250-
this._defineEvent("onError", Phase.ERROR, 0, paths.to, false, TH.IGNORE_RESULT, TH.LOG_ERROR, false);
251+
this._defineEvent("onSuccess", Phase.SUCCESS, 0, paths.to, NORMAL_SORT, TH.LOG_REJECTED_RESULT, TH.LOG_ERROR, SYNCHRONOUS);
252+
this._defineEvent("onError", Phase.ERROR, 0, paths.to, NORMAL_SORT, TH.LOG_REJECTED_RESULT, TH.LOG_ERROR, SYNCHRONOUS);
251253
}
252254

253255
/** @hidden */
@@ -269,9 +271,9 @@ export class TransitionService implements IHookRegistry, Disposable {
269271
reverseSort: boolean = false,
270272
getResultHandler: GetResultHandler = TransitionHook.HANDLE_RESULT,
271273
getErrorHandler: GetErrorHandler = TransitionHook.REJECT_ERROR,
272-
rejectIfSuperseded: boolean = true)
274+
synchronous: boolean = false)
273275
{
274-
let eventType = new TransitionEventType(name, hookPhase, hookOrder, criteriaMatchPath, reverseSort, getResultHandler, getErrorHandler, rejectIfSuperseded);
276+
let eventType = new TransitionEventType(name, hookPhase, hookOrder, criteriaMatchPath, reverseSort, getResultHandler, getErrorHandler, synchronous);
275277

276278
this._eventTypes.push(eventType);
277279
makeEvent(this, this, eventType);

test/lazyLoadSpec.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ describe('future state', function () {
389389
expect($state.get('A')).toBe(futureStateDef);
390390

391391
$state.go('A').catch(() => {
392-
expect(errors).toEqual(['nope']);
392+
expect(errors.map(x => x.detail)).toEqual(['nope']);
393393
expect($state.get('A')).toBe(futureStateDef);
394394
done();
395395
});
@@ -399,18 +399,18 @@ describe('future state', function () {
399399
expect($state.get('A')).toBe(futureStateDef);
400400

401401
$state.go('A').catch(() => {
402-
expect(errors).toEqual(['nope']);
402+
expect(errors.map(x => x.detail)).toEqual(['nope']);
403403
expect($state.get('A')).toBe(futureStateDef);
404404
expect(count).toBe(1);
405405

406406
$state.go('A').catch(() => {
407-
expect(errors).toEqual(['nope', 'nope']);
407+
expect(errors.map(x => x.detail)).toEqual(['nope', 'nope']);
408408
expect($state.get('A')).toBe(futureStateDef);
409409
expect(count).toBe(2);
410410

411411
// this time it should lazy load
412412
$state.go('A').then(() => {
413-
expect(errors).toEqual(['nope', 'nope']);
413+
expect(errors.map(x => x.detail)).toEqual(['nope', 'nope']);
414414
expect($state.get('A')).toBeTruthy();
415415
expect($state.get('A')).not.toBe(futureStateDef);
416416
expect(count).toBe(3);

0 commit comments

Comments
 (0)