Skip to content

Commit 7522c26

Browse files
fix(Rejection): Dont log an ignored trans as console.error
refactor(Rejection): rename TransitionRejection to Rejection. Remove RejectFactory in favor of static methods on Rejection. We were creating a promise in RejectFactory and then not using the promise. Chrome (and ng2-polyfills) was logging this to the console as an uncaught error. Closes #2676
1 parent 62b2ebc commit 7522c26

File tree

7 files changed

+92
-92
lines changed

7 files changed

+92
-92
lines changed

src/common/strings.ts

+11-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/** @module common_strings */ /** */
22

33
import {isString, isArray, isDefined, isNull, isPromise, isInjectable, isObject} from "./predicates";
4-
import {TransitionRejection} from "../transition/rejectFactory";
4+
import {Rejection} from "../transition/rejectFactory";
55
import {IInjectable, identity} from "./common";
66
import {pattern, is, not, val, invoke} from "./hof";
77
import {Transition} from "../transition/transition";
@@ -47,7 +47,6 @@ function _fromJson(json) {
4747

4848

4949
function promiseToString(p) {
50-
if (is(TransitionRejection)(p.reason)) return p.reason.toString();
5150
return `Promise(${JSON.stringify(p)})`;
5251
}
5352

@@ -62,15 +61,18 @@ export function fnToString(fn: IInjectable) {
6261
return _fn && _fn.toString() || "undefined";
6362
}
6463

64+
const isTransitionRejectionPromise = Rejection.isTransitionRejectionPromise;
6565

6666
let stringifyPattern = pattern([
67-
[not(isDefined), val("undefined")],
68-
[isNull, val("null")],
69-
[isPromise, promiseToString],
70-
[is(Transition), invoke("toString")],
71-
[is(Resolvable), invoke("toString")],
72-
[isInjectable, functionToString],
73-
[val(true), identity]
67+
[not(isDefined), val("undefined")],
68+
[isNull, val("null")],
69+
[isPromise, promiseToString],
70+
[isTransitionRejectionPromise, x => x._transitionRejection.toString()],
71+
[is(Rejection), invoke("toString")],
72+
[is(Transition), invoke("toString")],
73+
[is(Resolvable), invoke("toString")],
74+
[isInjectable, functionToString],
75+
[val(true), identity]
7476
]);
7577

7678
export function stringify(o) {

src/state/hooks/transitionManager.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {Param} from "../../params/param";
44

55
import {TreeChanges} from "../../transition/interface";
66
import {Transition} from "../../transition/transition";
7-
import {TransitionRejection, RejectType} from "../../transition/rejectFactory";
7+
import {Rejection, RejectType} from "../../transition/rejectFactory";
88

99
import {StateDeclaration} from "../interface";
1010
import {StateService} from "../stateService";
@@ -76,7 +76,7 @@ export class TransitionManager {
7676
transRejected(error): (StateDeclaration|Promise<any>) {
7777
let {transition, $state, $q} = this;
7878
// Handle redirect and abort
79-
if (error instanceof TransitionRejection) {
79+
if (error instanceof Rejection) {
8080
if (error.type === RejectType.IGNORED) {
8181
// Update $stateParmas/$state.params/$location.url if transition ignored, but dynamic params have changed.
8282
let dynamic = $state.$current.parameters().filter(prop('dynamic'));

src/state/stateService.ts

+4-7
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {UrlRouter} from "../url/urlRouter";
1515

1616
import {TransitionOptions} from "../transition/interface";
1717
import {TransitionService, defaultTransOpts} from "../transition/transitionService";
18-
import {RejectFactory} from "../transition/rejectFactory";
18+
import {Rejection} from "../transition/rejectFactory";
1919
import {Transition} from "../transition/transition";
2020

2121
import {StateOrName, StateDeclaration} from "./interface";
@@ -40,8 +40,6 @@ export class StateService {
4040
get current() { return this.globals.current; }
4141
get $current() { return this.globals.$current; }
4242

43-
private rejectFactory = new RejectFactory();
44-
4543
constructor(private $view: ViewService,
4644
private $urlRouter: UrlRouter,
4745
private $transitions: TransitionService,
@@ -66,7 +64,6 @@ export class StateService {
6664
let latest = latestThing();
6765
let $from$ = PathFactory.makeTargetState(fromPath);
6866
let callbackQueue = new Queue<Function>([].concat(this.stateProvider.invalidCallbacks));
69-
let rejectFactory = this.rejectFactory;
7067
let {$q, $injector} = services;
7168

7269
const invokeCallback = (callback: Function) => $q.when($injector.invoke(callback, null, { $to$, $from$ }));
@@ -79,15 +76,15 @@ export class StateService {
7976
// Recreate the TargetState, in case the state is now defined.
8077
target = this.target(target.identifier(), target.params(), target.options());
8178

82-
if (!target.valid()) return rejectFactory.invalid(target.error());
83-
if (latestThing() !== latest) return rejectFactory.superseded();
79+
if (!target.valid()) return Rejection.invalid(target.error()).toPromise();
80+
if (latestThing() !== latest) return Rejection.superseded().toPromise();
8481

8582
return this.transitionTo(target.identifier(), target.params(), target.options());
8683
};
8784

8885
function invokeNextCallback() {
8986
let nextCallback = callbackQueue.dequeue();
90-
if (nextCallback === undefined) return rejectFactory.invalid($to$.error());
87+
if (nextCallback === undefined) return Rejection.invalid($to$.error()).toPromise();
9188
return invokeCallback(nextCallback).then(checkForRedirect).then(result => result || invokeNextCallback());
9289
}
9390

src/transition/rejectFactory.ts

+30-25
Original file line numberDiff line numberDiff line change
@@ -8,59 +8,64 @@ export enum RejectType {
88
SUPERSEDED = 2, ABORTED = 3, INVALID = 4, IGNORED = 5
99
}
1010

11-
export class TransitionRejection {
11+
export class Rejection {
1212
type: number;
1313
message: string;
1414
detail: string;
1515
redirected: boolean;
1616

17-
constructor(type, message, detail) {
18-
extend(this, {
19-
type: type,
20-
message: message,
21-
detail: detail
22-
});
17+
constructor(type, message?, detail?) {
18+
this.type = type;
19+
this.message = message;
20+
this.detail = detail;
2321
}
2422

2523
toString() {
2624
const detailString = d => d && d.toString !== Object.prototype.toString ? d.toString() : stringify(d);
2725
let type = this.type, message = this.message, detail = detailString(this.detail);
2826
return `TransitionRejection(type: ${type}, message: ${message}, detail: ${detail})`;
2927
}
30-
}
3128

29+
toPromise() {
30+
return extend(services.$q.reject(this), { _transitionRejection: this });
31+
}
32+
33+
/** Returns true if the obj is a rejected promise created from the `asPromise` factory */
34+
static isTransitionRejectionPromise(obj) {
35+
return obj && (typeof obj.then === 'function') && obj._transitionRejection instanceof Rejection;
36+
}
3237

33-
export class RejectFactory {
34-
constructor() {}
35-
superseded(detail?: any, options?: any) {
38+
/** Returns a TransitionRejection due to transition superseded */
39+
static superseded(detail?: any, options?: any) {
3640
let message = "The transition has been superseded by a different transition (see detail).";
37-
let reason = new TransitionRejection(RejectType.SUPERSEDED, message, detail);
41+
let rejection = new Rejection(RejectType.SUPERSEDED, message, detail);
3842
if (options && options.redirected) {
39-
reason.redirected = true;
43+
rejection.redirected = true;
4044
}
41-
return extend(services.$q.reject(reason), {reason: reason});
45+
return rejection;
4246
}
4347

44-
redirected(detail?: any) {
45-
return this.superseded(detail, {redirected: true});
48+
/** Returns a TransitionRejection due to redirected transition */
49+
static redirected(detail?: any) {
50+
return Rejection.superseded(detail, {redirected: true});
4651
}
4752

48-
invalid(detail?: any) {
53+
/** Returns a TransitionRejection due to invalid transition */
54+
static invalid(detail?: any) {
4955
let message = "This transition is invalid (see detail)";
50-
let reason = new TransitionRejection(RejectType.INVALID, message, detail);
51-
return extend(services.$q.reject(reason), {reason: reason});
56+
return new Rejection(RejectType.INVALID, message, detail);
5257
}
5358

54-
ignored(detail?: any) {
59+
/** Returns a TransitionRejection due to ignored transition */
60+
static ignored(detail?: any) {
5561
let message = "The transition was ignored.";
56-
let reason = new TransitionRejection(RejectType.IGNORED, message, detail);
57-
return extend(services.$q.reject(reason), {reason: reason});
62+
return new Rejection(RejectType.IGNORED, message, detail);
5863
}
5964

60-
aborted(detail?: any) {
65+
/** Returns a TransitionRejection due to aborted transition */
66+
static aborted(detail?: any) {
6167
// TODO think about how to encapsulate an Error() object
6268
let message = "The transition has been aborted.";
63-
let reason = new TransitionRejection(RejectType.ABORTED, message, detail);
64-
return extend(services.$q.reject(reason), {reason: reason});
69+
return new Rejection(RejectType.ABORTED, message, detail);
6570
}
6671
}

src/transition/transition.ts

+10-9
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,27 @@
22
import {trace} from "../common/trace";
33
import {services} from "../common/coreservices";
44
import {
5-
map, find, extend, filter, mergeR, unnest, tail,
6-
omit, toJson, abstractKey, arrayTuples, allTrueR, unnestR, identity, anyTrueR
5+
map, find, extend, filter, mergeR, tail,
6+
omit, toJson, abstractKey, arrayTuples, unnestR, identity, anyTrueR
77
} from "../common/common";
88
import { isObject } from "../common/predicates";
99
import { not, prop, propEq, val } from "../common/hof";
1010

1111
import {StateDeclaration, StateOrName} from "../state/interface";
1212
import {TransitionOptions, TransitionHookOptions, TreeChanges, IHookRegistry, IHookRegistration, IHookGetter} from "./interface";
1313

14-
import {TransitionHook, HookRegistry, matchState, HookBuilder, RejectFactory} from "./module";
14+
import {TransitionHook, HookRegistry, matchState, HookBuilder} from "./module";
1515
import {Node} from "../path/node";
1616
import {PathFactory} from "../path/pathFactory";
1717
import {State, TargetState} from "../state/module";
1818
import {Param} from "../params/module";
1919
import {Resolvable} from "../resolve/module";
2020
import {TransitionService} from "./transitionService";
2121
import {ViewConfig} from "../view/interface";
22+
import {Rejection} from "./rejectFactory";
2223

2324

24-
let transitionCount = 0, REJECT = new RejectFactory();
25+
let transitionCount = 0;
2526
const stateSelf: (_state: State) => StateDeclaration = prop("self");
2627

2728
/**
@@ -375,8 +376,9 @@ export class Transition implements IHookRegistry {
375376

376377
let syncResult = runSynchronousHooks(hookBuilder.getOnBeforeHooks());
377378

378-
if (TransitionHook.isRejection(syncResult)) {
379-
let rejectReason = (<any> syncResult).reason;
379+
if (Rejection.isTransitionRejectionPromise(syncResult)) {
380+
syncResult.catch(() => 0); // issue #2676
381+
let rejectReason = (<any> syncResult)._transitionRejection;
380382
this._deferred.reject(rejectReason);
381383
return this.promise;
382384
}
@@ -389,8 +391,7 @@ export class Transition implements IHookRegistry {
389391

390392
if (this.ignored()) {
391393
trace.traceTransitionIgnored(this);
392-
let ignored = REJECT.ignored();
393-
this._deferred.reject(ignored.reason);
394+
this._deferred.reject(Rejection.ignored());
394395
return this.promise;
395396
}
396397

@@ -410,7 +411,7 @@ export class Transition implements IHookRegistry {
410411

411412
trace.traceTransitionStart(this);
412413

413-
let chain = hookBuilder.asyncHooks().reduce((_chain, step) => _chain.then(step.invokeStep), syncResult);
414+
let chain = hookBuilder.asyncHooks().reduce((_chain, step) => _chain.then(step.invokeHook.bind(step)), syncResult);
414415
chain.then(resolve, reject);
415416

416417
return this.promise;

src/transition/transitionHook.ts

+32-35
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,10 @@ import {not, pattern, val, eq, is, parse } from "../common/hof";
77
import {trace} from "../common/trace";
88
import {services} from "../common/coreservices";
99

10-
import {TransitionRejection, RejectFactory} from "./rejectFactory";
10+
import {Rejection} from "./rejectFactory";
1111
import {TargetState} from "../state/module";
1212
import {ResolveContext} from "../resolve/module";
1313

14-
let REJECT = new RejectFactory();
15-
1614
let defaultOptions: TransitionHookOptions = {
1715
async: true,
1816
rejectIfSuperseded: true,
@@ -32,43 +30,45 @@ export class TransitionHook {
3230

3331
private isSuperseded = () => this.options.current() !== this.options.transition;
3432

35-
/**
36-
* Handles transition abort and transition redirect. Also adds any returned resolvables
37-
* to the pathContext for the current pathElement. If the transition is rejected, then a rejected
38-
* promise is returned here, otherwise undefined is returned.
39-
*/
40-
mapHookResult: Function = pattern([
41-
// Transition is no longer current
42-
[this.isSuperseded, () => REJECT.superseded(this.options.current())],
43-
// If the hook returns false, abort the current Transition
44-
[eq(false), () => REJECT.aborted("Hook aborted transition")],
45-
// If the hook returns a Transition, halt the current Transition and redirect to that Transition.
46-
[is(TargetState), (target) => REJECT.redirected(target)],
47-
// A promise was returned, wait for the promise and then chain another hookHandler
48-
[isPromise, (promise) => promise.then(this.handleHookResult.bind(this))]
49-
]);
50-
51-
invokeStep = (moreLocals) => { // bind to this
33+
invokeHook(moreLocals) {
5234
let { options, fn, resolveContext } = this;
5335
let locals = extend({}, this.locals, moreLocals);
5436
trace.traceHookInvocation(this, options);
5537
if (options.rejectIfSuperseded && this.isSuperseded()) {
56-
return REJECT.superseded(options.current());
38+
return Rejection.superseded(options.current()).toPromise();
5739
}
5840

5941
// TODO: Need better integration of returned promises in synchronous code.
6042
if (!options.async) {
6143
let hookResult = resolveContext.invokeNow(fn, locals, options);
6244
return this.handleHookResult(hookResult);
6345
}
64-
return resolveContext.invokeLater(fn, locals, options).then(this.handleHookResult.bind(this));
46+
return resolveContext.invokeLater(fn, locals, options).then(val => this.handleHookResult(val));
6547
};
6648

67-
handleHookResult(hookResult) {
49+
/**
50+
* This method handles the return value of a Transition Hook.
51+
*
52+
* A hook can return false, a redirect (TargetState), or a promise (which may resolve to false or a redirect)
53+
*/
54+
handleHookResult(hookResult): Promise<any> {
6855
if (!isDefined(hookResult)) return undefined;
69-
trace.traceHookResult(hookResult, undefined, this.options);
7056

71-
let transitionResult = this.mapHookResult(hookResult);
57+
/**
58+
* Handles transition superseded, transition aborted and transition redirect.
59+
*/
60+
const mapHookResult = pattern([
61+
// Transition is no longer current
62+
[this.isSuperseded, () => Rejection.superseded(this.options.current()).toPromise()],
63+
// If the hook returns false, abort the current Transition
64+
[eq(false), () => Rejection.aborted("Hook aborted transition").toPromise()],
65+
// If the hook returns a Transition, halt the current Transition and redirect to that Transition.
66+
[is(TargetState), (target) => Rejection.redirected(target).toPromise()],
67+
// A promise was returned, wait for the promise and then chain another hookHandler
68+
[isPromise, (promise) => promise.then(this.handleHookResult.bind(this))]
69+
]);
70+
71+
let transitionResult = mapHookResult(hookResult);
7272
if (transitionResult) trace.traceHookResult(hookResult, transitionResult, this.options);
7373

7474
return transitionResult;
@@ -92,24 +92,21 @@ export class TransitionHook {
9292
let results = [];
9393
for (let i = 0; i < hooks.length; i++) {
9494
try {
95-
results.push(hooks[i].invokeStep(locals));
95+
results.push(hooks[i].invokeHook(locals));
9696
} catch (exception) {
97-
if (!swallowExceptions) return REJECT.aborted(exception);
98-
console.log("Swallowed exception during synchronous hook handler: " + exception); // TODO: What to do here?
97+
if (!swallowExceptions) {
98+
return Rejection.aborted(exception).toPromise();
99+
}
100+
101+
console.error("Swallowed exception during synchronous hook handler: " + exception); // TODO: What to do here?
99102
}
100103
}
101104

102-
let rejections = results.filter(TransitionHook.isRejection);
105+
let rejections = results.filter(Rejection.isTransitionRejectionPromise);
103106
if (rejections.length) return rejections[0];
104107

105108
return results
106-
.filter(not(TransitionHook.isRejection))
107109
.filter(<Predicate<any>> isPromise)
108110
.reduce((chain, promise) => chain.then(val(promise)), services.$q.when());
109111
}
110-
111-
112-
static isRejection(hookResult) {
113-
return hookResult && hookResult.reason instanceof TransitionRejection && hookResult;
114-
}
115112
}

0 commit comments

Comments
 (0)