Skip to content

Commit 9acb950

Browse files
authored
fix: publish variants returning ConnectableObservable not properly utilizing lift (#6003)
- Adds tests to show fixed issues - Adds tests to show issues that simply can't be fixed, and support a case against removing operators that return ConnectableObservable, as well as possibly a case against lifting Subject. - Moves logic that patched lift for ConnectableObservable to the constructor so it is used by all multicast operators
1 parent 412d1fd commit 9acb950

File tree

3 files changed

+249
-14
lines changed

3 files changed

+249
-14
lines changed

spec/Observable-spec.ts

Lines changed: 241 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { expect } from 'chai';
22
import * as sinon from 'sinon';
33
import { Observer, TeardownLogic } from '../src/internal/types';
44
import { Observable, config, Subscription, noop, Subscriber, Operator, NEVER, Subject, of, throwError, empty } from 'rxjs';
5-
import { map, multicast, refCount, filter, count, tap, combineLatest, concat, merge, race, zip, catchError, concatMap, switchMap } from 'rxjs/operators';
5+
import { map, multicast, refCount, filter, count, tap, combineLatest, concat, merge, race, zip, catchError, concatMap, switchMap, publish, publishLast, publishBehavior, share} from 'rxjs/operators';
66
import { TestScheduler } from 'rxjs/testing';
77
import { observableMatcher } from './helpers/observableMatcher';
88

@@ -810,6 +810,246 @@ describe('Observable.lift', () => {
810810
);
811811
});
812812

813+
814+
it('should compose through publish and refCount', (done) => {
815+
const result = new MyCustomObservable<number>((observer) => {
816+
observer.next(1);
817+
observer.next(2);
818+
observer.next(3);
819+
observer.complete();
820+
}).pipe(
821+
publish(),
822+
refCount(),
823+
map((x) => 10 * x)
824+
);
825+
826+
expect(result instanceof MyCustomObservable).to.be.true;
827+
828+
const expected = [10, 20, 30];
829+
830+
result.subscribe(
831+
function (x) {
832+
expect(x).to.equal(expected.shift());
833+
},
834+
() => {
835+
done(new Error('should not be called'));
836+
},
837+
() => {
838+
done();
839+
}
840+
);
841+
});
842+
843+
844+
it('should compose through publishLast and refCount', (done) => {
845+
const result = new MyCustomObservable<number>((observer) => {
846+
observer.next(1);
847+
observer.next(2);
848+
observer.next(3);
849+
observer.complete();
850+
}).pipe(
851+
publishLast(),
852+
refCount(),
853+
map((x) => 10 * x)
854+
);
855+
856+
expect(result instanceof MyCustomObservable).to.be.true;
857+
858+
const expected = [30];
859+
860+
result.subscribe(
861+
function (x) {
862+
expect(x).to.equal(expected.shift());
863+
},
864+
() => {
865+
done(new Error('should not be called'));
866+
},
867+
() => {
868+
done();
869+
}
870+
);
871+
});
872+
873+
it('should compose through publishBehavior and refCount', (done) => {
874+
const result = new MyCustomObservable<number>((observer) => {
875+
observer.next(1);
876+
observer.next(2);
877+
observer.next(3);
878+
observer.complete();
879+
}).pipe(
880+
publishBehavior(0),
881+
refCount(),
882+
map((x) => 10 * x)
883+
);
884+
885+
expect(result instanceof MyCustomObservable).to.be.true;
886+
887+
const expected = [0, 10, 20, 30];
888+
889+
result.subscribe(
890+
function (x) {
891+
expect(x).to.equal(expected.shift());
892+
},
893+
() => {
894+
done(new Error('should not be called'));
895+
},
896+
() => {
897+
done();
898+
}
899+
);
900+
});
901+
902+
it('should composes Subjects in the simple case', () => {
903+
const subject = new Subject<number>();
904+
905+
const result = subject.pipe(
906+
map((x) => 10 * x)
907+
) as any as Subject<number>; // Yes, this is correct. (but you're advised not to do this)
908+
909+
expect(result instanceof Subject).to.be.true;
910+
911+
const emitted: any[] = [];
912+
result.subscribe(value => emitted.push(value));
913+
914+
result.next(10);
915+
result.next(20);
916+
result.next(30);
917+
918+
expect(emitted).to.deep.equal([100, 200, 300]);
919+
});
920+
921+
/**
922+
* Seriously, never do this. It's probably bad that we've allowed this. Fortunately, it's not
923+
* a common practice, so maybe we can remove it?
924+
*/
925+
it('should demonstrate the horrors of sharing and lifting the Subject through', () => {
926+
const subject = new Subject<number>();
927+
928+
const shared = subject.pipe(
929+
share()
930+
);
931+
932+
const result1 = shared.pipe(
933+
map(x => x * 10)
934+
) as any as Subject<number>; // Yes, this is correct.
935+
936+
const result2 = shared.pipe(
937+
map(x => x - 10)
938+
) as any as Subject<number>; // Yes, this is correct.
939+
expect(result1 instanceof Subject).to.be.true;
940+
941+
const emitted1: any[] = [];
942+
result1.subscribe(value => emitted1.push(value));
943+
944+
const emitted2: any[] = [];
945+
result2.subscribe(value => emitted2.push(value));
946+
947+
// THIS IS HORRIBLE DON'T DO THIS.
948+
result1.next(10);
949+
result2.next(20); // Yuck
950+
result1.next(30);
951+
952+
expect(emitted1).to.deep.equal([100, 200, 300]);
953+
expect(emitted2).to.deep.equal([0, 10, 20]);
954+
});
955+
956+
/**
957+
* This section outlines one of the reasons that we need to get rid of operators that return
958+
* Connectable observable. Likewise it also reveals a slight design flaw in `lift`. It
959+
* probably should have never tried to compose through the Subject's observer methods.
960+
* If you're a user and you're reading this... NEVER try to use this feature, it's likely
961+
* to go away at some point.
962+
*
963+
* The problem is that you can have the Subject parts, or you can have the ConnectableObservable parts,
964+
* but you can't have both.
965+
*/
966+
describe.skip('The lift through Connectable gaff', () => {
967+
it('should compose through multicast and refCount, even if it is a Subject', () => {
968+
const subject = new Subject<number>();
969+
970+
const result = subject.pipe(
971+
multicast(() => new Subject<number>()),
972+
refCount(),
973+
map((x) => 10 * x)
974+
) as any as Subject<number>; // Yes, this is correct.
975+
976+
expect(result instanceof Subject).to.be.true;
977+
978+
const emitted: any[] = [];
979+
result.subscribe(value => emitted.push(value));
980+
981+
result.next(10);
982+
result.next(20);
983+
result.next(30);
984+
985+
expect(emitted).to.deep.equal([100, 200, 300]);
986+
});
987+
988+
it('should compose through publish and refCount, even if it is a Subject', () => {
989+
const subject = new Subject<number>();
990+
991+
const result = subject.pipe(
992+
publish(),
993+
refCount(),
994+
map((x) => 10 * x)
995+
) as any as Subject<number>; // Yes, this is correct.
996+
997+
expect(result instanceof Subject).to.be.true;
998+
999+
const emitted: any[] = [];
1000+
result.subscribe(value => emitted.push(value));
1001+
1002+
result.next(10);
1003+
result.next(20);
1004+
result.next(30);
1005+
1006+
expect(emitted).to.deep.equal([100, 200, 300]);
1007+
});
1008+
1009+
1010+
it('should compose through publishLast and refCount, even if it is a Subject', () => {
1011+
const subject = new Subject<number>();
1012+
1013+
const result = subject.pipe(
1014+
publishLast(),
1015+
refCount(),
1016+
map((x) => 10 * x)
1017+
) as any as Subject<number>; // Yes, this is correct.
1018+
1019+
expect(result instanceof Subject).to.be.true;
1020+
1021+
const emitted: any[] = [];
1022+
result.subscribe(value => emitted.push(value));
1023+
1024+
result.next(10);
1025+
result.next(20);
1026+
result.next(30);
1027+
1028+
expect(emitted).to.deep.equal([100, 200, 300]);
1029+
});
1030+
1031+
it('should compose through publishBehavior and refCount, even if it is a Subject', () => {
1032+
const subject = new Subject<number>();
1033+
1034+
const result = subject.pipe(
1035+
publishBehavior(0),
1036+
refCount(),
1037+
map((x) => 10 * x)
1038+
) as any as Subject<number>; // Yes, this is correct.
1039+
1040+
expect(result instanceof Subject).to.be.true;
1041+
1042+
const emitted: any[] = [];
1043+
result.subscribe(value => emitted.push(value));
1044+
1045+
result.next(10);
1046+
result.next(20);
1047+
result.next(30);
1048+
1049+
expect(emitted).to.deep.equal([0, 100, 200, 300]);
1050+
});
1051+
});
1052+
8131053
it('should compose through multicast with selector function', (done) => {
8141054
const result = new MyCustomObservable<number>((observer) => {
8151055
observer.next(1);

src/internal/observable/ConnectableObservable.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Subscriber } from '../Subscriber';
44
import { Subscription } from '../Subscription';
55
import { refCount as higherOrderRefCount } from '../operators/refCount';
66
import { OperatorSubscriber } from '../operators/OperatorSubscriber';
7+
import { hasLift } from '../util/lift';
78

89
/**
910
* @class ConnectableObservable<T>
@@ -28,6 +29,12 @@ export class ConnectableObservable<T> extends Observable<T> {
2829
*/
2930
constructor(public source: Observable<T>, protected subjectFactory: () => Subject<T>) {
3031
super();
32+
// If we have lift, monkey patch that here. This is done so custom observable
33+
// types will compose through multicast. Otherwise the resulting observable would
34+
// simply be an instance of `ConnectableObservable`.
35+
if (hasLift(source)) {
36+
this.lift = source.lift;
37+
}
3138
}
3239

3340
protected _subscribe(subscriber: Subscriber<T>) {

src/internal/operators/multicast.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { Subject } from '../Subject';
22
import { Observable } from '../Observable';
33
import { ConnectableObservable } from '../observable/ConnectableObservable';
44
import { OperatorFunction, UnaryFunction, ObservedValueOf, ObservableInput } from '../types';
5-
import { hasLift } from '../util/lift';
65
import { isFunction } from '../util/isFunction';
76
import { connect } from './connect';
87

@@ -81,16 +80,5 @@ export function multicast<T, R>(
8180
});
8281
}
8382

84-
return (source: Observable<T>) => {
85-
const connectable: any = new ConnectableObservable(source, subjectFactory);
86-
// If we have lift, monkey patch that here. This is done so custom observable
87-
// types will compose through multicast. Otherwise the resulting observable would
88-
// simply be an instance of `ConnectableObservable`.
89-
if (hasLift(source)) {
90-
connectable.lift = source.lift;
91-
}
92-
connectable.source = source;
93-
connectable.subjectFactory = subjectFactory;
94-
return connectable;
95-
};
83+
return (source: Observable<T>) => new ConnectableObservable<any>(source, subjectFactory);
9684
}

0 commit comments

Comments
 (0)