Skip to content

Commit 6d6a882

Browse files
felixfbeckercartant
authored andcommitted
fix: Ensure unsubscriptions/teardowns on internal subscribers are idempotent (#5465)
* null out _unsubscribe after unsubscription Fixes #5464 * test: add idempotent unsubscribe tests * fix: null the subscription ctor teardown * refactor: use delete to remove the member * refactor: delete _unsubscribe only if it exists * refactor: replace delete with hasOwnProperty etc * refactor: use _ctorUnsubscribe flag Co-authored-by: Nicholas Jamieson <[email protected]>
1 parent 7437993 commit 6d6a882

File tree

3 files changed

+41
-3
lines changed

3 files changed

+41
-3
lines changed

spec/Subscriber-spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,17 @@ describe('Subscriber', () => {
9797

9898
expect(argument).to.have.lengthOf(0);
9999
});
100+
101+
it('should have idempotent unsubscription', () => {
102+
let count = 0;
103+
const subscriber = new Subscriber();
104+
subscriber.add(() => ++count);
105+
expect(count).to.equal(0);
106+
107+
subscriber.unsubscribe();
108+
expect(count).to.equal(1);
109+
110+
subscriber.unsubscribe();
111+
expect(count).to.equal(1);
112+
});
100113
});

spec/Subscription-spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,5 +159,17 @@ describe('Subscription', () => {
159159
done();
160160
});
161161
});
162+
163+
it('Should have idempotent unsubscription', () => {
164+
let count = 0;
165+
const subscription = new Subscription(() => ++count);
166+
expect(count).to.equal(0);
167+
168+
subscription.unsubscribe();
169+
expect(count).to.equal(1);
170+
171+
subscription.unsubscribe();
172+
expect(count).to.equal(1);
173+
});
162174
});
163175
});

src/internal/Subscription.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ export class Subscription implements SubscriptionLike {
4040
*/
4141
constructor(unsubscribe?: () => void) {
4242
if (unsubscribe) {
43-
(<any> this)._unsubscribe = unsubscribe;
43+
(this as any)._ctorUnsubscribe = true;
44+
(this as any)._unsubscribe = unsubscribe;
4445
}
4546
}
4647

@@ -57,7 +58,7 @@ export class Subscription implements SubscriptionLike {
5758
return;
5859
}
5960

60-
let { _parentOrParents, _unsubscribe, _subscriptions } = (<any> this);
61+
let { _parentOrParents, _ctorUnsubscribe, _unsubscribe, _subscriptions } = (this as any);
6162

6263
this.closed = true;
6364
this._parentOrParents = null;
@@ -75,6 +76,18 @@ export class Subscription implements SubscriptionLike {
7576
}
7677

7778
if (isFunction(_unsubscribe)) {
79+
// It's only possible to null _unsubscribe - to release the reference to
80+
// any teardown function passed in the constructor - if the property was
81+
// actually assigned in the constructor, as there are some classes that
82+
// are derived from Subscriber (which derives from Subscription) that
83+
// implement an _unsubscribe method as a mechanism for obtaining
84+
// unsubscription notifications and some of those subscribers are
85+
// recycled. Also, in some of those subscribers, _unsubscribe switches
86+
// from a prototype method to an instance property - see notifyNext in
87+
// RetryWhenSubscriber.
88+
if (_ctorUnsubscribe) {
89+
(this as any)._unsubscribe = undefined;
90+
}
7891
try {
7992
_unsubscribe.call(this);
8093
} catch (e) {
@@ -131,7 +144,7 @@ export class Subscription implements SubscriptionLike {
131144
add(teardown: TeardownLogic): Subscription {
132145
let subscription = (<Subscription>teardown);
133146

134-
if (!(<any>teardown)) {
147+
if (!teardown) {
135148
return Subscription.EMPTY;
136149
}
137150

0 commit comments

Comments
 (0)