Skip to content

Commit 375d4a5

Browse files
committed
fix(Subscription): add will return Subscription that removes itself when unsubscribed
There was a problem where subscriptions added to parent subscriptions would not remove themselves when unsubscribed, This meant that when Actions (which are subscriptions) were unsubscribed, they would still hang out in the _subscriptions list and live in memory when we didn't want them to related #2244
1 parent 6922b16 commit 375d4a5

File tree

3 files changed

+45
-20
lines changed

3 files changed

+45
-20
lines changed

spec/Subscription-spec.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,15 +112,19 @@ describe('Subscription', () => {
112112
expect(isCalled).to.equal(true);
113113
});
114114

115-
it('Should returns the passed one if passed a unsubscribed AnonymousSubscription', () => {
116-
const sub = new Subscription();
115+
it('Should wrap the AnonymousSubscription and return a subscription that unsubscribes and removes it when unsubbed', () => {
116+
const sub: any = new Subscription();
117+
let called = false;
117118
const arg = {
118-
isUnsubscribed: true,
119-
unsubscribe: () => undefined,
119+
unsubscribe: () => called = true,
120120
};
121121
const ret = sub.add(arg);
122122

123-
expect(ret).to.equal(arg);
123+
expect(called).to.equal(false);
124+
expect(sub._subscriptions.length).to.equal(1);
125+
ret.unsubscribe();
126+
expect(called).to.equal(true);
127+
expect(sub._subscriptions.length).to.equal(0);
124128
});
125129

126130
it('Should returns the passed one if passed a AnonymousSubscription having not function `unsubscribe` member', () => {

src/Subscription.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export class Subscription implements ISubscription {
4040
*/
4141
public closed: boolean = false;
4242

43+
private _subscriptions: ISubscription[];
44+
4345
/**
4446
* @param {function(): void} [unsubscribe] A function describing how to
4547
* perform the disposal of resources when the `unsubscribe` method is called.
@@ -74,7 +76,10 @@ export class Subscription implements ISubscription {
7476
let trial = tryCatch(_unsubscribe).call(this);
7577
if (trial === errorObject) {
7678
hasErrors = true;
77-
(errors = errors || []).push(errorObject.e);
79+
errors = errors || (
80+
errorObject.e instanceof UnsubscriptionError ?
81+
flattenUnsubscriptionErrors(errorObject.e.errors) : [errorObject.e]
82+
);
7883
}
7984
}
8085

@@ -92,7 +97,7 @@ export class Subscription implements ISubscription {
9297
errors = errors || [];
9398
let err = errorObject.e;
9499
if (err instanceof UnsubscriptionError) {
95-
errors = errors.concat(err.errors);
100+
errors = errors.concat(flattenUnsubscriptionErrors(err.errors));
96101
} else {
97102
errors.push(err);
98103
}
@@ -140,18 +145,20 @@ export class Subscription implements ISubscription {
140145
sub = new Subscription(<(() => void) > teardown);
141146
case 'object':
142147
if (sub.closed || typeof sub.unsubscribe !== 'function') {
143-
break;
148+
return sub;
144149
} else if (this.closed) {
145150
sub.unsubscribe();
146-
} else {
147-
((<any> this)._subscriptions || ((<any> this)._subscriptions = [])).push(sub);
151+
return sub;
148152
}
149153
break;
150154
default:
151155
throw new Error('unrecognized teardown ' + teardown + ' added to Subscription.');
152156
}
153157

154-
return sub;
158+
const childSub = new ChildSubscription(sub, this);
159+
this._subscriptions = this._subscriptions || [];
160+
this._subscriptions.push(childSub);
161+
return childSub;
155162
}
156163

157164
/**
@@ -179,3 +186,19 @@ export class Subscription implements ISubscription {
179186
}
180187
}
181188
}
189+
190+
export class ChildSubscription extends Subscription {
191+
constructor(private _innerSub: ISubscription, private _parent: Subscription) {
192+
super();
193+
}
194+
195+
_unsubscribe() {
196+
const { _innerSub, _parent } = this;
197+
_parent.remove(this);
198+
_innerSub.unsubscribe();
199+
}
200+
}
201+
202+
function flattenUnsubscriptionErrors(errors: any[]) {
203+
return errors.reduce((errs, err) => errs.concat((err instanceof UnsubscriptionError) ? err.errors : err), []);
204+
}

src/scheduler/VirtualTimeScheduler.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,13 @@ export class VirtualAction<T> extends AsyncAction<T> {
5454
}
5555

5656
public schedule(state?: T, delay: number = 0): Subscription {
57-
return !this.id ?
58-
super.schedule(state, delay) : (
59-
// If an action is rescheduled, we save allocations by mutating its state,
60-
// pushing it to the end of the scheduler queue, and recycling the action.
61-
// But since the VirtualTimeScheduler is used for testing, VirtualActions
62-
// must be immutable so they can be inspected later.
63-
<VirtualAction<T>> this.add(
64-
new VirtualAction<T>(this.scheduler, this.work))
65-
).schedule(state, delay);
57+
if (!this.id) {
58+
return super.schedule(state, delay);
59+
}
60+
61+
const action = new VirtualAction(this.scheduler, this.work);
62+
this.add(action);
63+
return action.schedule(state, delay);
6664
}
6765

6866
protected requestAsyncId(scheduler: VirtualTimeScheduler, id?: any, delay: number = 0): any {

0 commit comments

Comments
 (0)