Skip to content

Commit 5a2266a

Browse files
authored
feat(shareReplay): adds shareReplay variant of publishReplay (#2443)
`shareReplay` returns an observable that is the source multicasted over a `ReplaySubject`. That replay subject is recycled on error from the `source`, but not on completion of the source. This makes `shareReplay` ideal for handling things like caching AJAX results, as it's retryable. It's repeat behavior, however, differs from `share` in that it will not repeat the `source` observable, rather it will repeat the `source` observable's values. related #2013, #453, #2043
1 parent 4ffbbe5 commit 5a2266a

File tree

6 files changed

+223
-10
lines changed

6 files changed

+223
-10
lines changed

spec/helpers/marble-testing.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import {Observable} from '../../dist/cjs/Observable';
33
import {SubscriptionLog} from '../../dist/cjs/testing/SubscriptionLog';
44
import {ColdObservable} from '../../dist/cjs/testing/ColdObservable';
55
import {HotObservable} from '../../dist/cjs/testing/HotObservable';
6-
import {observableToBeFn, subscriptionLogsToBeFn} from '../../dist/cjs/testing/TestScheduler';
6+
import {TestScheduler, observableToBeFn, subscriptionLogsToBeFn} from '../../dist/cjs/testing/TestScheduler';
77

88
declare const global: any;
99

10+
export const rxTestScheduler: TestScheduler = global.rxTestScheduler;
11+
1012
export function hot(marbles: string, values?: any, error?: any): HotObservable<any> {
1113
if (!global.rxTestScheduler) {
1214
throw 'tried to use hot() in async test';

spec/operators/shareReplay-spec.ts

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import {expect} from 'chai';
2+
import * as Rx from '../../dist/cjs/Rx';
3+
import marbleTestingSignature = require('../helpers/marble-testing'); // tslint:disable-line:no-require-imports
4+
5+
declare const { asDiagram };
6+
declare const hot: typeof marbleTestingSignature.hot;
7+
declare const cold: typeof marbleTestingSignature.cold;
8+
declare const expectObservable: typeof marbleTestingSignature.expectObservable;
9+
declare const expectSubscriptions: typeof marbleTestingSignature.expectSubscriptions;
10+
11+
const Observable = Rx.Observable;
12+
13+
/** @test {shareReplay} */
14+
describe('Observable.prototype.shareReplay', () => {
15+
it('should mirror a simple source Observable', () => {
16+
const source = cold('--1-2---3-4--5-|');
17+
const sourceSubs = '^ !';
18+
const published = source.shareReplay();
19+
const expected = '--1-2---3-4--5-|';
20+
21+
expectObservable(published).toBe(expected);
22+
expectSubscriptions(source.subscriptions).toBe(sourceSubs);
23+
});
24+
25+
it('should do nothing if result is not subscribed', () => {
26+
let subscribed = false;
27+
const source = new Observable(() => {
28+
subscribed = true;
29+
});
30+
source.shareReplay();
31+
expect(subscribed).to.be.false;
32+
});
33+
34+
it('should multicast the same values to multiple observers, bufferSize=1', () => {
35+
const source = cold('-1-2-3----4-|'); const shared = source.shareReplay(1);
36+
const sourceSubs = '^ !';
37+
const subscriber1 = hot('a| ').mergeMapTo(shared);
38+
const expected1 = '-1-2-3----4-|';
39+
const subscriber2 = hot(' b| ').mergeMapTo(shared);
40+
const expected2 = ' 23----4-|';
41+
const subscriber3 = hot(' c| ').mergeMapTo(shared);
42+
const expected3 = ' 3-4-|';
43+
44+
expectObservable(subscriber1).toBe(expected1);
45+
expectObservable(subscriber2).toBe(expected2);
46+
expectObservable(subscriber3).toBe(expected3);
47+
expectSubscriptions(source.subscriptions).toBe(sourceSubs);
48+
});
49+
50+
it('should multicast the same values to multiple observers, bufferSize=2', () => {
51+
const source = cold('-1-2-----3------4-|'); const shared = source.shareReplay(2);
52+
const sourceSubs = '^ !';
53+
const subscriber1 = hot('a| ').mergeMapTo(shared);
54+
const expected1 = '-1-2-----3------4-|';
55+
const subscriber2 = hot(' b| ').mergeMapTo(shared);
56+
const expected2 = ' (12)-3------4-|';
57+
const subscriber3 = hot(' c| ').mergeMapTo(shared);
58+
const expected3 = ' (23)-4-|';
59+
60+
expectObservable(subscriber1).toBe(expected1);
61+
expectObservable(subscriber2).toBe(expected2);
62+
expectObservable(subscriber3).toBe(expected3);
63+
expectSubscriptions(source.subscriptions).toBe(sourceSubs);
64+
});
65+
66+
it('should multicast an error from the source to multiple observers', () => {
67+
const source = cold('-1-2-3----4-#'); const shared = source.shareReplay(1);
68+
const sourceSubs = '^ !';
69+
const subscriber1 = hot('a| ').mergeMapTo(shared);
70+
const expected1 = '-1-2-3----4-#';
71+
const subscriber2 = hot(' b| ').mergeMapTo(shared);
72+
const expected2 = ' 23----4-#';
73+
const subscriber3 = hot(' c| ').mergeMapTo(shared);
74+
const expected3 = ' 3-4-#';
75+
76+
expectObservable(subscriber1).toBe(expected1);
77+
expectObservable(subscriber2).toBe(expected2);
78+
expectObservable(subscriber3).toBe(expected3);
79+
expectSubscriptions(source.subscriptions).toBe(sourceSubs);
80+
});
81+
82+
it('should multicast an empty source', () => {
83+
const source = cold('|');
84+
const sourceSubs = '(^!)';
85+
const shared = source.shareReplay(1);
86+
const expected = '|';
87+
88+
expectObservable(shared).toBe(expected);
89+
expectSubscriptions(source.subscriptions).toBe(sourceSubs);
90+
});
91+
92+
it('should multicast a never source', () => {
93+
const source = cold('-');
94+
const sourceSubs = '^';
95+
96+
const shared = source.shareReplay(1);
97+
const expected = '-';
98+
99+
expectObservable(shared).toBe(expected);
100+
expectSubscriptions(source.subscriptions).toBe(sourceSubs);
101+
});
102+
103+
it('should multicast a throw source', () => {
104+
const source = cold('#');
105+
const sourceSubs = '(^!)';
106+
const shared = source.shareReplay(1);
107+
const expected = '#';
108+
109+
expectObservable(shared).toBe(expected);
110+
expectSubscriptions(source.subscriptions).toBe(sourceSubs);
111+
});
112+
113+
it('should replay results to subsequent subscriptions if source completes, bufferSize=2', () => {
114+
const source = cold('-1-2-----3-| ');
115+
const shared = source.shareReplay(2);
116+
const sourceSubs = '^ ! ';
117+
const subscriber1 = hot('a| ').mergeMapTo(shared);
118+
const expected1 = '-1-2-----3-| ';
119+
const subscriber2 = hot(' b| ').mergeMapTo(shared);
120+
const expected2 = ' (12)-3-| ';
121+
const subscriber3 = hot(' (c|) ').mergeMapTo(shared);
122+
const expected3 = ' (23|)';
123+
124+
expectObservable(subscriber1).toBe(expected1);
125+
expectObservable(subscriber2).toBe(expected2);
126+
expectObservable(subscriber3).toBe(expected3);
127+
expectSubscriptions(source.subscriptions).toBe(sourceSubs);
128+
});
129+
130+
it('should completely restart for subsequent subscriptions if source errors, bufferSize=2', () => {
131+
const source = cold('-1-2-----3-# ');
132+
const shared = source.shareReplay(2);
133+
const sourceSubs1 = '^ ! ';
134+
const subscriber1 = hot('a| ').mergeMapTo(shared);
135+
const expected1 = '-1-2-----3-# ';
136+
const subscriber2 = hot(' b| ').mergeMapTo(shared);
137+
const expected2 = ' (12)-3-# ';
138+
const subscriber3 = hot(' (c|) ').mergeMapTo(shared);
139+
const expected3 = ' -1-2-----3-#';
140+
const sourceSubs2 = ' ^ !';
141+
142+
expectObservable(subscriber1).toBe(expected1);
143+
expectObservable(subscriber2).toBe(expected2);
144+
expectObservable(subscriber3).toBe(expected3);
145+
expectSubscriptions(source.subscriptions).toBe([sourceSubs1, sourceSubs2]);
146+
});
147+
148+
it('should be retryable, bufferSize=2', () => {
149+
const subs = [];
150+
const source = cold('-1-2-----3-# ');
151+
const shared = source.shareReplay(2).retry(1);
152+
subs.push( '^ ! ');
153+
subs.push( ' ^ ! ');
154+
subs.push( ' ^ !');
155+
const subscriber1 = hot('a| ').mergeMapTo(shared);
156+
const expected1 = '-1-2-----3--1-2-----3-# ';
157+
const subscriber2 = hot(' b| ').mergeMapTo(shared);
158+
const expected2 = ' (12)-3--1-2-----3-# ';
159+
const subscriber3 = hot(' (c|) ').mergeMapTo(shared);
160+
const expected3 = ' (12)-3--1-2-----3-#';
161+
162+
expectObservable(subscriber1).toBe(expected1);
163+
expectObservable(subscriber2).toBe(expected2);
164+
expectObservable(subscriber3).toBe(expected3);
165+
expectSubscriptions(source.subscriptions).toBe(subs);
166+
});
167+
});

src/Rx.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ import './add/operator/sampleTime';
110110
import './add/operator/scan';
111111
import './add/operator/sequenceEqual';
112112
import './add/operator/share';
113+
import './add/operator/shareReplay';
113114
import './add/operator/single';
114115
import './add/operator/skip';
115116
import './add/operator/skipLast';

src/add/operator/shareReplay.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
import { Observable } from '../../Observable';
3+
import { shareReplay } from '../../operator/shareReplay';
4+
5+
Observable.prototype.shareReplay = shareReplay;
6+
7+
declare module '../../Observable' {
8+
interface Observable<T> {
9+
shareReplay: typeof shareReplay;
10+
}
11+
}

src/observable/ConnectableObservable.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export class ConnectableObservable<T> extends Observable<T> {
1212
protected _subject: Subject<T>;
1313
protected _refCount: number = 0;
1414
protected _connection: Subscription;
15+
_isComplete = false;
1516

1617
constructor(protected source: Observable<T>,
1718
protected subjectFactory: () => Subject<T>) {
@@ -33,6 +34,7 @@ export class ConnectableObservable<T> extends Observable<T> {
3334
connect(): Subscription {
3435
let connection = this._connection;
3536
if (!connection) {
37+
this._isComplete = false;
3638
connection = this._connection = new Subscription();
3739
connection.add(this.source
3840
.subscribe(new ConnectableSubscriber(this.getSubject(), this)));
@@ -51,15 +53,18 @@ export class ConnectableObservable<T> extends Observable<T> {
5153
}
5254
}
5355

56+
const connectableProto = <any>ConnectableObservable.prototype;
57+
5458
export const connectableObservableDescriptor: PropertyDescriptorMap = {
5559
operator: { value: null },
5660
_refCount: { value: 0, writable: true },
5761
_subject: { value: null, writable: true },
5862
_connection: { value: null, writable: true },
59-
_subscribe: { value: (<any> ConnectableObservable.prototype)._subscribe },
60-
getSubject: { value: (<any> ConnectableObservable.prototype).getSubject },
61-
connect: { value: (<any> ConnectableObservable.prototype).connect },
62-
refCount: { value: (<any> ConnectableObservable.prototype).refCount }
63+
_subscribe: { value: connectableProto._subscribe },
64+
_isComplete: { value: connectableProto._isComplete, writable: true },
65+
getSubject: { value: connectableProto.getSubject },
66+
connect: { value: connectableProto.connect },
67+
refCount: { value: connectableProto.refCount }
6368
};
6469

6570
class ConnectableSubscriber<T> extends SubjectSubscriber<T> {
@@ -72,17 +77,18 @@ class ConnectableSubscriber<T> extends SubjectSubscriber<T> {
7277
super._error(err);
7378
}
7479
protected _complete(): void {
80+
this.connectable._isComplete = true;
7581
this._unsubscribe();
7682
super._complete();
7783
}
7884
protected _unsubscribe() {
79-
const { connectable } = this;
85+
const connectable = <any>this.connectable;
8086
if (connectable) {
8187
this.connectable = null;
82-
const connection = (<any> connectable)._connection;
83-
(<any> connectable)._refCount = 0;
84-
(<any> connectable)._subject = null;
85-
(<any> connectable)._connection = null;
88+
const connection = connectable._connection;
89+
connectable._refCount = 0;
90+
connectable._subject = null;
91+
connectable._connection = null;
8692
if (connection) {
8793
connection.unsubscribe();
8894
}

src/operator/shareReplay.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Observable } from '../Observable';
2+
import { multicast } from './multicast';
3+
import { ReplaySubject } from '../ReplaySubject';
4+
import { ConnectableObservable } from '../observable/ConnectableObservable';
5+
import { IScheduler } from '../Scheduler';
6+
7+
/**
8+
* @method shareReplay
9+
* @owner Observable
10+
*/
11+
export function shareReplay<T>(
12+
this: Observable<T>,
13+
bufferSize?: number,
14+
windowTime?: number,
15+
scheduler?: IScheduler
16+
): Observable<T> {
17+
let subject: ReplaySubject<T>;
18+
const connectable = multicast.call(this, function shareReplaySubjectFactory(this: ConnectableObservable<T>) {
19+
if (this._isComplete) {
20+
return subject;
21+
} else {
22+
return (subject = new ReplaySubject<T>(bufferSize, windowTime, scheduler));
23+
}
24+
});
25+
return connectable.refCount();
26+
};

0 commit comments

Comments
 (0)