Skip to content

Commit 280f7ed

Browse files
staltzkwonoj
authored andcommitted
fix(catch): fix catch to dispose old subscriptions
Fix catch operator to not have anymore a shared underlying Subscription, and instead reset the subscription for each new observable replacing the caught error. This fixes a potential memory leak if catch is used as an infinite retry, because subscriptions would be retained since the beginning, and would increasing each time a catch is performed. Resolves issue #763.
1 parent 08af3e8 commit 280f7ed

File tree

2 files changed

+60
-33
lines changed

2 files changed

+60
-33
lines changed

spec/operators/catch-spec.js

+37-20
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ describe('Observable.prototype.catch()', function () {
2525

2626
it('should catch error and replace it with a cold Observable', function () {
2727
var e1 = hot('--a--b--#----| ');
28-
var e1subs = '^ !';
28+
var e1subs = '^ ! ';
2929
var e2 = cold( '1-2-3-4-5-|');
3030
var e2subs = ' ^ !';
3131
var expected = '--a--b--1-2-3-4-5-|';
@@ -37,9 +37,23 @@ describe('Observable.prototype.catch()', function () {
3737
expectSubscriptions(e2.subscriptions).toBe(e2subs);
3838
});
3939

40+
it('should allow unsubscribing explicitly and early', function () {
41+
var e1 = hot('--1-2-3-4-5-6---#');
42+
var unsub = ' ! ';
43+
var e1subs = '^ ! ';
44+
var expected = '--1-2-3- ';
45+
46+
var result = e1.catch(function () {
47+
return Observable.of('X', 'Y', 'Z');
48+
});
49+
50+
expectObservable(result, unsub).toBe(expected);
51+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
52+
});
53+
4054
it('should catch error and replace it with a hot Observable', function () {
4155
var e1 = hot('--a--b--#----| ');
42-
var e1subs = '^ !';
56+
var e1subs = '^ ! ';
4357
var e2 = hot('1-2-3-4-5-6-7-8-9-|');
4458
var e2subs = ' ^ !';
4559
var expected = '--a--b--5-6-7-8-9-|';
@@ -54,8 +68,8 @@ describe('Observable.prototype.catch()', function () {
5468
it('should catch and allow the cold observable to be repeated with the third ' +
5569
'(caught) argument', function () {
5670
var e1 = cold('--a--b--c--------| ');
57-
var subs = ['^ !',
58-
' ^ !',
71+
var subs = ['^ ! ',
72+
' ^ ! ',
5973
' ^ !'];
6074
var expected = '--a--b----a--b----a--b--#';
6175

@@ -81,7 +95,7 @@ describe('Observable.prototype.catch()', function () {
8195
it('should catch and allow the hot observable to proceed with the third ' +
8296
'(caught) argument', function () {
8397
var e1 = hot('--a--b--c----d---|');
84-
var subs = ['^ !',
98+
var subs = ['^ ! ',
8599
' ^ !'];
86100
var expected = '--a--b-------d---|';
87101

@@ -132,41 +146,44 @@ describe('Observable.prototype.catch()', function () {
132146

133147
it('should complete if you return Observable.empty()', function () {
134148
var e1 = hot('--a--b--#');
135-
var subs = '^ !';
149+
var e1subs = '^ !';
150+
var e2 = cold( '|');
151+
var e2subs = ' (^!)';
136152
var expected = '--a--b--|';
137153

138-
var result = e1.catch(function (err) {
139-
return Observable.empty();
140-
});
154+
var result = e1.catch(function () { return e2; });
141155

142156
expectObservable(result).toBe(expected);
143-
expectSubscriptions(e1.subscriptions).toBe(subs);
157+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
158+
expectSubscriptions(e2.subscriptions).toBe(e2subs);
144159
});
145160

146161
it('should raise error if you return Observable.throw()', function () {
147162
var e1 = hot('--a--b--#');
148-
var subs = '^ !';
163+
var e1subs = '^ !';
164+
var e2 = cold( '#');
165+
var e2subs = ' (^!)';
149166
var expected = '--a--b--#';
150167

151-
var result = e1.catch(function (err) {
152-
return Observable.throw('error');
153-
});
168+
var result = e1.catch(function () { return e2; });
154169

155170
expectObservable(result).toBe(expected);
156-
expectSubscriptions(e1.subscriptions).toBe(subs);
171+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
172+
expectSubscriptions(e2.subscriptions).toBe(e2subs);
157173
});
158174

159175
it('should never terminate if you return Observable.never()', function () {
160176
var e1 = hot('--a--b--#');
161-
var subs = '^ ';
177+
var e1subs = '^ !';
178+
var e2 = cold( '-');
179+
var e2subs = ' ^';
162180
var expected = '--a--b---';
163181

164-
var result = e1.catch(function (err) {
165-
return Observable.never();
166-
});
182+
var result = e1.catch(function () { return e2; });
167183

168184
expectObservable(result).toBe(expected);
169-
expectSubscriptions(e1.subscriptions).toBe(subs);
185+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
186+
expectSubscriptions(e2.subscriptions).toBe(e2subs);
170187
});
171188

172189
it('should pass the error as the first argument', function (done) {

src/operators/catch.ts

+23-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {Operator} from '../Operator';
22
import {Subscriber} from '../Subscriber';
33
import {Observable} from '../Observable';
4+
import {Subscription} from '../Subscription';
45
import {tryCatch} from '../util/tryCatch';
56
import {errorObject} from '../util/errorObject';
67

@@ -20,12 +21,9 @@ export function _catch<T>(selector: (err: any, caught: Observable<any>) => Obser
2021
}
2122

2223
class CatchOperator<T, R> implements Operator<T, R> {
23-
selector: (err: any, caught: Observable<any>) => Observable<any>;
2424
caught: Observable<any>;
25-
source: Observable<T>;
2625

27-
constructor(selector: (err: any, caught: Observable<any>) => Observable<any>) {
28-
this.selector = selector;
26+
constructor(private selector: (err: any, caught: Observable<any>) => Observable<any>) {
2927
}
3028

3129
call(subscriber: Subscriber<T>): Subscriber<T> {
@@ -34,23 +32,35 @@ class CatchOperator<T, R> implements Operator<T, R> {
3432
}
3533

3634
class CatchSubscriber<T> extends Subscriber<T> {
37-
selector: (err: any, caught: Observable<any>) => Observable<any>;
38-
caught: Observable<any>;
35+
private lastSubscription: Subscription<T>;
36+
37+
constructor(public destination: Subscriber<T>,
38+
private selector: (err: any, caught: Observable<any>) => Observable<any>,
39+
private caught: Observable<any>) {
40+
super(null);
41+
this.lastSubscription = this;
42+
}
3943

40-
constructor(destination: Subscriber<T>,
41-
selector: (err: any, caught: Observable<any>) => Observable<any>,
42-
caught: Observable<any>) {
43-
super(destination);
44-
this.selector = selector;
45-
this.caught = caught;
44+
_next(value: T) {
45+
this.destination.next(value);
4646
}
4747

4848
_error(err) {
4949
const result = tryCatch(this.selector)(err, this.caught);
5050
if (result === errorObject) {
5151
this.destination.error(errorObject.e);
5252
} else {
53-
this.add(result.subscribe(this.destination));
53+
this.lastSubscription.unsubscribe();
54+
this.lastSubscription = result.subscribe(this.destination);
5455
}
5556
}
57+
58+
_complete() {
59+
this.lastSubscription.unsubscribe();
60+
this.destination.complete();
61+
}
62+
63+
_unsubscribe() {
64+
this.lastSubscription.unsubscribe();
65+
}
5666
}

0 commit comments

Comments
 (0)