Skip to content

Commit e623ec6

Browse files
cartantbenlesh
authored andcommitted
fix(Subscriber): Can no longer subscribe to itself in a circular manner (#4106)
* test(mergeMap): add failing test * fix(subscriber): don't unsubscribe self When unsubscribing a subscriber's parent, make sure that the subscriber itself is not unsubscribed. Closes#4095 * refactor(mergeMap): simplify * chore(typings): use union type for destination * chore(test): remove only * chore(test): use pipe * test(Subscriber): fake add too * test(internals): update for subscription changes * refactor(zip): add to destination * refactor(delay): add to destination * refactor(delayWhen): add to destination * refactor(exhaustMap): add to destination * refactor(expand): add to destination * refactor(mergeScan): add to destination * refactor(observeOn): add to destination * refactor(onErrorResumeNext): add to destination * refactor(sequenceEqual): add to destination * refactor(switchMap): add to destination * chore(Subscriber): remove _addParentTeardownLogic * chore(test): simplify mergeMap test Remove the mapTo and the concat to reduce the number of subscribers to make the test easier to reason with.
1 parent 02780dd commit e623ec6

18 files changed

Lines changed: 118 additions & 61 deletions

‎spec/Subscriber-spec.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ describe('Subscriber', () => {
2323
it('should accept subscribers as a destination if they meet the proper criteria',()=>{
2424
constfakeSubscriber={
2525
[rxSubscriber](this: any){returnthis;},
26-
_addParentTeardownLogic(){/* noop */}
26+
add(){/* noop */},
27+
syncErrorThrowable: false
2728
};
2829

2930
constsubscriber=newSubscriber(fakeSubscriberasany);

‎spec/operators/mergeMap-spec.ts‎

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import{expect}from'chai';
22
import{mergeMap,map}from'rxjs/operators';
3-
import{asapScheduler,defer,Observable,from,of}from'rxjs';
3+
import{asapScheduler,defer,Observable,from,of,timer}from'rxjs';
44
import{hot,cold,expectObservable,expectSubscriptions}from'../helpers/marble-testing';
55

66
declareconsttype: Function;
@@ -717,7 +717,7 @@ describe('mergeMap', () => {
717717
// Added as a failing test when investigating:
718718
// https://github.com/ReactiveX/rxjs/issues/4071
719719

720-
constresults: any[]=[];
720+
constresults: (number|string)[]=[];
721721

722722
of(1).pipe(
723723
mergeMap(()=>defer(()=>
@@ -744,7 +744,7 @@ describe('mergeMap', () => {
744744
// Added as a failing test when investigating:
745745
// https://github.com/ReactiveX/rxjs/issues/4071
746746

747-
constresults: any[]=[];
747+
constresults: (number|string)[]=[];
748748

749749
of(1).pipe(
750750
mergeMap(()=>
@@ -764,6 +764,30 @@ describe('mergeMap', () => {
764764
},0);
765765
});
766766

767+
it('should support wrapped sources',(done: MochaDone)=>{
768+
769+
// Added as a failing test when investigating:
770+
// https://github.com/ReactiveX/rxjs/issues/4095
771+
772+
constresults: (number|string)[]=[];
773+
774+
constwrapped=newObservable<number>(subscriber=>{
775+
constsubscription=timer(0,asapScheduler).subscribe(subscriber);
776+
return()=>subscription.unsubscribe();
777+
});
778+
wrapped.pipe(
779+
mergeMap(()=>timer(0,asapScheduler))
780+
).subscribe({
781+
next(value){results.push(value);},
782+
complete(){results.push('done');}
783+
});
784+
785+
setTimeout(()=>{
786+
expect(results).to.deep.equal([0,'done']);
787+
done();
788+
},0);
789+
});
790+
767791
type('should support type signatures',()=>{
768792
leto: Observable<number>;
769793

‎spec/operators/observeOn-spec.ts‎

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -103,23 +103,21 @@ describe('observeOn operator', () => {
103103
.pipe(observeOn(asapScheduler))
104104
.subscribe(
105105
x=>{
106-
constobserveOnSubscriber=subscription._subscriptions[0];
107-
expect(observeOnSubscriber._subscriptions.length).to.equal(2);// one for the consumer, and one for the notification
108-
expect(observeOnSubscriber._subscriptions[1].state.notification.kind)
109-
.to.equal('N');
110-
expect(observeOnSubscriber._subscriptions[1].state.notification.value)
111-
.to.equal(x);
106+
// see #4106 - inner subscriptions are now added to destinations
107+
// so the subscription will contain an ObserveOnSubscriber and a subscription for the scheduled action
108+
expect(subscription._subscriptions.length).to.equal(2);
109+
constactionSubscription=subscription._subscriptions[1];
110+
expect(actionSubscription.state.notification.kind).to.equal('N');
111+
expect(actionSubscription.state.notification.value).to.equal(x);
112112
results.push(x);
113113
},
114114
err=>done(err),
115115
()=>{
116116
// now that the last nexted value is done, there should only be a complete notification scheduled
117117
// the consumer will have been unsubscribed via Subscriber#_parentSubscription
118-
constobserveOnSubscriber=subscription._subscriptions[0];
119-
expect(observeOnSubscriber._subscriptions.length).to.equal(1);// one for the complete notification
120-
// only this completion notification should remain.
121-
expect(observeOnSubscriber._subscriptions[0].state.notification.kind)
122-
.to.equal('C');
118+
expect(subscription._subscriptions.length).to.equal(1);
119+
constactionSubscription=subscription._subscriptions[0];
120+
expect(actionSubscription.state.notification.kind).to.equal('C');
123121
// After completion, the entire _subscriptions list is nulled out anyhow, so we can't test much further than this.
124122
expect(results).to.deep.equal([1,2,3]);
125123
done();

‎spec/operators/switch-spec.ts‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import{expect}from'chai';
22
import{hot,cold,expectObservable,expectSubscriptions}from'../helpers/marble-testing';
33
import{Observable,of,NEVER,queueScheduler,Subject}from'rxjs';
4-
import{switchAll}from'rxjs/operators';
4+
import{map,switchAll}from'rxjs/operators';
55

66
declarefunctionasDiagram(arg: string): Function;
77
declareconsttype: Function;
@@ -222,9 +222,9 @@ describe('switchAll', () => {
222222
it('should not leak when child completes before each switch (prevent memory leaks #2355)',()=>{
223223
letiStream: Subject<number>;
224224
constoStreamControl=newSubject<number>();
225-
constoStream=oStreamControl.map(()=>{
226-
return(iStream=newSubject<number>());
227-
});
225+
constoStream=oStreamControl.pipe(
226+
map(()=>(iStream=newSubject<number>()))
227+
);
228228
constswitcher=oStream.pipe(switchAll());
229229
constresult: number[]=[];
230230
letsub=switcher.subscribe((x)=>result.push(x));
@@ -242,19 +242,24 @@ describe('switchAll', () => {
242242

243243
it('should not leak if we switch before child completes (prevent memory leaks #2355)',()=>{
244244
constoStreamControl=newSubject<number>();
245-
constoStream=oStreamControl.map(()=>{
246-
return(newSubject<number>());
247-
});
245+
constoStream=oStreamControl.pipe(
246+
map(()=>newSubject<number>())
247+
);
248248
constswitcher=oStream.pipe(switchAll());
249249
constresult: number[]=[];
250250
letsub=switcher.subscribe((x)=>result.push(x));
251251

252252
[0,1,2,3,4].forEach((n)=>{
253253
oStreamControl.next(n);// creates inner
254254
});
255-
// Expect two children of switch(): The oStream and the first inner
255+
// Expect one child of switch(): The oStream
256256
expect(
257257
(subasany)._subscriptions[0]._subscriptions.length
258+
).to.equal(1);
259+
// Expect two children of subscribe(): The destination and the first inner
260+
// See #4106 - inner subscriptions are now added to destinations
261+
expect(
262+
(subasany)._subscriptions.length
258263
).to.equal(2);
259264
sub.unsubscribe();
260265
});

‎src/internal/Observable.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ export class Observable<T> implements Subscribable<T> {
199199
if(operator){
200200
operator.call(sink,this.source);
201201
}else{
202-
sink._addParentTeardownLogic(
202+
sink.add(
203203
this.source||(config.useDeprecatedSynchronousErrorHandling&&!sink.syncErrorThrowable) ?
204204
this._subscribe(sink) :
205205
this._trySubscribe(sink)

‎src/internal/Subscriber.ts‎

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export class Subscriber<T> extends Subscription implements Observer<T> {
4545
/** @internal */syncErrorThrowable: boolean=false;
4646

4747
protectedisStopped: boolean=false;
48-
protecteddestination: PartialObserver<any>;// this `any` is the escape hatch to erase extra type param (e.g. R)
48+
protecteddestination: PartialObserver<any>|Subscriber<any>;// this `any` is the escape hatch to erase extra type param (e.g. R)
4949

5050
private_parentSubscription: Subscription|null=null;
5151

@@ -78,7 +78,7 @@ export class Subscriber<T> extends Subscription implements Observer<T> {
7878
consttrustedSubscriber=destinationOrNext[rxSubscriberSymbol]()asSubscriber<any>;
7979
this.syncErrorThrowable=trustedSubscriber.syncErrorThrowable;
8080
this.destination=trustedSubscriber;
81-
trustedSubscriber._addParentTeardownLogic(this);
81+
trustedSubscriber.add(this);
8282
}else{
8383
this.syncErrorThrowable=true;
8484
this.destination=newSafeSubscriber<T>(this,<PartialObserver<any>>destinationOrNext);
@@ -116,7 +116,6 @@ export class Subscriber<T> extends Subscription implements Observer<T> {
116116
if(!this.isStopped){
117117
this.isStopped=true;
118118
this._error(err);
119-
this._unsubscribeParentSubscription();
120119
}
121120
}
122121

@@ -130,7 +129,6 @@ export class Subscriber<T> extends Subscription implements Observer<T> {
130129
if(!this.isStopped){
131130
this.isStopped=true;
132131
this._complete();
133-
this._unsubscribeParentSubscription();
134132
}
135133
}
136134

@@ -156,20 +154,6 @@ export class Subscriber<T> extends Subscription implements Observer<T> {
156154
this.unsubscribe();
157155
}
158156

159-
/** @deprecated This is an internal implementation detail, do not use. */
160-
_addParentTeardownLogic(parentTeardownLogic: TeardownLogic){
161-
if(parentTeardownLogic!==this){
162-
this._parentSubscription=this.add(parentTeardownLogic);
163-
}
164-
}
165-
166-
/** @deprecated This is an internal implementation detail, do not use. */
167-
_unsubscribeParentSubscription(){
168-
if(this._parentSubscription!==null){
169-
this._parentSubscription.unsubscribe();
170-
}
171-
}
172-
173157
/** @deprecated This is an internal implementation detail, do not use. */
174158
_unsubscribeAndRecycle(): Subscriber<T>{
175159
const{ _parent, _parents }=this;
@@ -326,5 +310,5 @@ export class SafeSubscriber<T> extends Subscriber<T> {
326310
}
327311

328312
exportfunctionisTrustedSubscriber(obj: any){
329-
returnobjinstanceofSubscriber||('_addParentTeardownLogic'inobj&&obj[rxSubscriberSymbol]);
313+
returnobjinstanceofSubscriber||('syncErrorThrowable'inobj&&obj[rxSubscriberSymbol]);
330314
}

‎src/internal/Subscription.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ export class Subscription implements SubscriptionLike {
4545
constructor(unsubscribe?: ()=>void){
4646
if(unsubscribe){
4747
(<any>this)._unsubscribe=unsubscribe;
48-
4948
}
5049
}
5150

‎src/internal/observable/zip.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { isArray } from '../util/isArray';
44
import{Operator}from'../Operator';
55
import{ObservableInput,PartialObserver}from'../types';
66
import{Subscriber}from'../Subscriber';
7+
import{Subscription}from'../Subscription';
78
import{OuterSubscriber}from'../OuterSubscriber';
89
import{InnerSubscriber}from'../InnerSubscriber';
910
import{subscribeToResult}from'../util/subscribeToResult';
@@ -126,6 +127,8 @@ export class ZipSubscriber<T, R> extends Subscriber<T> {
126127
constiterators=this.iterators;
127128
constlen=iterators.length;
128129

130+
this.unsubscribe();
131+
129132
if(len===0){
130133
this.destination.complete();
131134
return;
@@ -135,7 +138,8 @@ export class ZipSubscriber<T, R> extends Subscriber<T> {
135138
for(leti=0;i<len;i++){
136139
letiterator: ZipBufferIterator<any,any>=<any>iterators[i];
137140
if(iterator.stillUnsubscribed){
138-
this.add(iterator.subscribe(iterator,i));
141+
constdestination=this.destinationasSubscription;
142+
destination.add(iterator.subscribe(iterator,i));
139143
}else{
140144
this.active--;// not an observable
141145
}

‎src/internal/operators/delay.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { async } from '../scheduler/async';
22
import{isDate}from'../util/isDate';
33
import{Operator}from'../Operator';
44
import{Subscriber}from'../Subscriber';
5+
import{Subscription}from'../Subscription';
56
import{Notification}from'../Notification';
67
import{Observable}from'../Observable';
78
import{MonoTypeOperatorFunction,PartialObserver,SchedulerAction,SchedulerLike,TeardownLogic}from'../types';
@@ -110,7 +111,8 @@ class DelaySubscriber<T> extends Subscriber<T> {
110111

111112
private_schedule(scheduler: SchedulerLike): void{
112113
this.active=true;
113-
this.add(scheduler.schedule<DelayState<T>>(DelaySubscriber.dispatch,this.delay,{
114+
constdestination=this.destinationasSubscription;
115+
destination.add(scheduler.schedule<DelayState<T>>(DelaySubscriber.dispatch,this.delay,{
114116
source: this,destination: this.destination,scheduler: scheduler
115117
}));
116118
}
@@ -137,10 +139,12 @@ class DelaySubscriber<T> extends Subscriber<T> {
137139
this.errored=true;
138140
this.queue=[];
139141
this.destination.error(err);
142+
this.unsubscribe();
140143
}
141144

142145
protected_complete(){
143146
this.scheduleNotification(Notification.createComplete());
147+
this.unsubscribe();
144148
}
145149
}
146150

‎src/internal/operators/delayWhen.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ class DelayWhenSubscriber<T, R> extends OuterSubscriber<T, R> {
132132
protected_complete(): void{
133133
this.completed=true;
134134
this.tryComplete();
135+
this.unsubscribe();
135136
}
136137

137138
privateremoveSubscription(subscription: InnerSubscriber<T,R>): T{
@@ -149,7 +150,8 @@ class DelayWhenSubscriber<T, R> extends OuterSubscriber<T, R> {
149150
constnotifierSubscription=subscribeToResult(this,delayNotifier,value);
150151

151152
if(notifierSubscription&&!notifierSubscription.closed){
152-
this.add(notifierSubscription);
153+
constdestination=this.destinationasSubscription;
154+
destination.add(notifierSubscription);
153155
this.delayNotifierSubscriptions.push(notifierSubscription);
154156
}
155157
}
@@ -199,6 +201,7 @@ class SubscriptionDelaySubscriber<T> extends Subscriber<T> {
199201
}
200202

201203
protected_complete(){
204+
this.unsubscribe();
202205
this.subscribeToSource();
203206
}
204207

0 commit comments

Comments
 (0)