Skip to content

Commit 666c6d7

Browse files
fix(transitionHook): Do not process transition hooks after router has been disposed.
1 parent 4bdce47 commit 666c6d7

File tree

4 files changed

+71
-18
lines changed

4 files changed

+71
-18
lines changed

src/router.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ let _routerInstance = 0;
3535
* [[UrlService.listen]] then [[UrlService.sync]].
3636
*/
3737
export class UIRouter {
38-
/** @hidden */
39-
$id: number = _routerInstance++;
38+
/** @hidden */ $id = _routerInstance++;
39+
/** @hidden */ _disposed = false;
40+
/** @hidden */ private _disposables: Disposable[] = [];
4041

4142
/** Provides trace information to the console */
4243
trace: Trace = trace;
@@ -71,8 +72,6 @@ export class UIRouter {
7172
/** Provides services related to the URL */
7273
urlService: UrlService = new UrlService(this);
7374

74-
/** @hidden */
75-
private _disposables: Disposable[] = [];
7675

7776
/** Registers an object to be notified when the router is disposed */
7877
disposable(disposable: Disposable) {
@@ -95,6 +94,7 @@ export class UIRouter {
9594
return undefined;
9695
}
9796

97+
this._disposed = true;
9898
this._disposables.slice().forEach(d => {
9999
try {
100100
typeof d.dispose === 'function' && d.dispose(this);

src/transition/transitionHook.ts

+27-12
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,14 @@ export class TransitionHook {
8383
let hook = this.registeredHook;
8484
if (hook._deregistered) return;
8585

86+
let notCurrent = this.getNotCurrentRejection();
87+
if (notCurrent) return notCurrent;
88+
8689
let options = this.options;
8790
trace.traceHookInvocation(this, this.transition, options);
8891

89-
// A new transition started before this hook (from a previous transition) could be run.
90-
if (this.isSuperseded()) {
91-
return Rejection.superseded(options.current()).toPromise();
92-
}
93-
9492
const invokeCallback = () =>
95-
hook.callback.call(this.options.bind, this.transition, this.stateContext);
93+
hook.callback.call(options.bind, this.transition, this.stateContext);
9694

9795
const normalizeErr = err =>
9896
Rejection.normalize(err).toPromise();
@@ -127,12 +125,8 @@ export class TransitionHook {
127125
* was started while the hook was still running
128126
*/
129127
handleHookResult(result: HookResult): Promise<HookResult> {
130-
// This transition is no longer current.
131-
// Another transition started while this hook was still running.
132-
if (this.isSuperseded()) {
133-
// Abort this transition
134-
return Rejection.superseded(this.options.current()).toPromise();
135-
}
128+
let notCurrent = this.getNotCurrentRejection();
129+
if (notCurrent) return notCurrent;
136130

137131
// Hook returned a promise
138132
if (isPromise(result)) {
@@ -156,6 +150,27 @@ export class TransitionHook {
156150
}
157151
}
158152

153+
154+
/**
155+
* Return a Rejection promise if the transition is no longer current due
156+
* to a stopped router (disposed), or a new transition has started and superseded this one.
157+
*/
158+
private getNotCurrentRejection() {
159+
let router = this.transition.router;
160+
161+
// The router is stopped
162+
if (router._disposed) {
163+
return Rejection.aborted(`UIRouter instance #${router.$id} has been stopped (disposed)`).toPromise();
164+
}
165+
166+
// This transition is no longer current.
167+
// Another transition started while this hook was still running.
168+
if (this.isSuperseded()) {
169+
// Abort this transition
170+
return Rejection.superseded(this.options.current()).toPromise();
171+
}
172+
}
173+
159174
toString() {
160175
let { options, registeredHook } = this;
161176
let event = parse("traceData.hookType")(options) || "internal",

test/_testUtils.ts

+4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { map } from "../src/common/common";
33

44
let stateProps = ["resolve", "resolvePolicy", "data", "template", "templateUrl", "url", "name", "params"];
55

6+
export const delay = (ms) =>
7+
new Promise<any>(resolve => setTimeout(resolve, ms));
8+
export const _delay = (ms) => () => delay(ms);
9+
610
export function tree2Array(tree, inheritName) {
711

812
function processState(parent, state, name) {

test/hooksSpec.ts

+36-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { UIRouter } from "../src/router";
2-
import { tree2Array } from "./_testUtils";
2+
import {delay, tree2Array} from "./_testUtils";
33
import { find } from "../src/common/common";
44
import { StateService } from "../src/state/stateService";
55
import { StateDeclaration } from "../src/state/interface";
@@ -19,7 +19,7 @@ let statetree = {
1919
};
2020

2121
describe("hooks", () => {
22-
let router;
22+
let router: UIRouter;
2323
let $state: StateService;
2424
let $transitions: TransitionService;
2525
let states: StateDeclaration[];
@@ -179,4 +179,38 @@ describe("hooks", () => {
179179
});
180180
});
181181

182+
it("should not run a hook after the router is stopped", (done) => {
183+
init();
184+
let called = false;
185+
router.transitionService.onSuccess({}, () => called = true);
186+
187+
$state.go('A').catch(err => {
188+
expect(called).toBe(false);
189+
expect(err).toBeDefined();
190+
expect(err.detail).toContain('disposed');
191+
done();
192+
});
193+
194+
router.dispose();
195+
});
196+
197+
it("should not process a hook result after the router is stopped", (done) => {
198+
init();
199+
let called = false;
200+
let disposed, isdisposed = new Promise<any>(resolve => disposed = resolve);
201+
202+
router.transitionService.onEnter({}, () => {
203+
called = true;
204+
return isdisposed.then(() => $state.target('B'));
205+
});
206+
207+
$state.go('A').catch(err => {
208+
expect(called).toBe(true);
209+
expect(err).toBeDefined();
210+
expect(err.detail).toContain('disposed');
211+
done();
212+
});
213+
214+
setTimeout(() => { router.dispose(); disposed(); }, 50);
215+
});
182216
});

0 commit comments

Comments
 (0)