Skip to content

Commit f2ec21b

Browse files
Merge pull request ReactiveX#425 from benjchristensen/pull-407-refCount
Manual Merge of Pull Request ReactiveX#407
2 parents b530357 + 34a6e25 commit f2ec21b

File tree

6 files changed

+279
-1
lines changed

6 files changed

+279
-1
lines changed

rxjava-core/src/main/java/rx/Observable.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
import rx.plugins.RxJavaErrorHandler;
8686
import rx.plugins.RxJavaObservableExecutionHook;
8787
import rx.plugins.RxJavaPlugins;
88+
import rx.subjects.AsyncSubject;
8889
import rx.subjects.PublishSubject;
8990
import rx.subjects.ReplaySubject;
9091
import rx.subjects.Subject;
@@ -3657,6 +3658,14 @@ public ConnectableObservable<T> publish() {
36573658
return OperationMulticast.multicast(this, PublishSubject.<T> create());
36583659
}
36593660

3661+
/**
3662+
* Returns a {@link ConnectableObservable} that shares a single subscription that contains the last notification only.
3663+
* @return a {@link ConnectableObservable}
3664+
*/
3665+
public ConnectableObservable<T> publishLast() {
3666+
return OperationMulticast.multicast(this, AsyncSubject.<T> create());
3667+
}
3668+
36603669
/**
36613670
* Synonymous with <code>reduce()</code>.
36623671
* <p>
@@ -4414,7 +4423,7 @@ public Boolean call(T t) {
44144423
* For why this is being used see https://github.com/Netflix/RxJava/issues/216 for discussion on "Guideline 6.4: Protect calls to user code from within an operator"
44154424
*
44164425
* NOTE: If strong reasons for not depending on package names comes up then the implementation of this method can change to looking for a marker interface.
4417-
*
4426+
*
44184427
* @param o
44194428
* @return {@code true} if the given function is an internal implementation, and {@code false} otherwise.
44204429
*/

rxjava-core/src/main/java/rx/observables/ConnectableObservable.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import rx.Observable;
1919
import rx.Observer;
2020
import rx.Subscription;
21+
import rx.operators.OperationRefCount;
22+
import rx.util.functions.Func1;
2123

2224
/**
2325
* A ConnectableObservable resembles an ordinary {@link Observable}, except that it does not begin
@@ -46,4 +48,12 @@ protected ConnectableObservable(OnSubscribeFunc<T> onSubscribe) {
4648
*/
4749
public abstract Subscription connect();
4850

51+
/**
52+
* Returns an observable sequence that stays connected to the source as long
53+
* as there is at least one subscription to the observable sequence.
54+
* @return a {@link Observable}
55+
*/
56+
public Observable<T> refCount() {
57+
return Observable.create(OperationRefCount.refCount(this));
58+
}
4959
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/**
2+
* Copyright 2013 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package rx.operators;
17+
18+
import rx.Observable;
19+
import rx.Observer;
20+
import rx.Subscription;
21+
import rx.observables.ConnectableObservable;
22+
import rx.subscriptions.Subscriptions;
23+
import rx.util.functions.Action0;
24+
25+
/**
26+
* Returns an observable sequence that stays connected to the source as long
27+
* as there is at least one subscription to the observable sequence.
28+
*/
29+
public final class OperationRefCount<T> {
30+
public static <T> Observable.OnSubscribeFunc<T> refCount(ConnectableObservable<T> connectableObservable) {
31+
return new RefCount<T>(connectableObservable);
32+
}
33+
34+
private static class RefCount<T> implements Observable.OnSubscribeFunc<T> {
35+
private final ConnectableObservable<T> innerConnectableObservable;
36+
private final Object gate = new Object();
37+
private int count = 0;
38+
private Subscription connection = null;
39+
40+
public RefCount(ConnectableObservable<T> innerConnectableObservable) {
41+
this.innerConnectableObservable = innerConnectableObservable;
42+
}
43+
44+
@Override
45+
public Subscription onSubscribe(Observer<? super T> observer) {
46+
final Subscription subscription = innerConnectableObservable.subscribe(observer);
47+
synchronized (gate) {
48+
if (count++ == 0) {
49+
connection = innerConnectableObservable.connect();
50+
}
51+
}
52+
return Subscriptions.create(new Action0() {
53+
@Override
54+
public void call() {
55+
synchronized (gate) {
56+
if (--count == 0) {
57+
connection.unsubscribe();
58+
connection = null;
59+
}
60+
}
61+
subscription.unsubscribe();
62+
}
63+
});
64+
}
65+
}
66+
}

rxjava-core/src/test/java/rx/ObservableTests.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,48 @@ public void call(String v) {
489489
}
490490
}
491491

492+
@Test
493+
public void testPublishLast() throws InterruptedException {
494+
final AtomicInteger count = new AtomicInteger();
495+
ConnectableObservable<String> connectable = Observable.create(new OnSubscribeFunc<String>() {
496+
@Override
497+
public Subscription onSubscribe(final Observer<? super String> observer) {
498+
count.incrementAndGet();
499+
final BooleanSubscription subscription = new BooleanSubscription();
500+
new Thread(new Runnable() {
501+
@Override
502+
public void run() {
503+
observer.onNext("first");
504+
observer.onNext("last");
505+
observer.onCompleted();
506+
}
507+
}).start();
508+
return subscription;
509+
}
510+
}).publishLast();
511+
512+
// subscribe once
513+
final CountDownLatch latch = new CountDownLatch(1);
514+
connectable.subscribe(new Action1<String>() {
515+
@Override
516+
public void call(String value) {
517+
assertEquals("last", value);
518+
latch.countDown();
519+
}
520+
});
521+
522+
// subscribe twice
523+
connectable.subscribe(new Action1<String>() {
524+
@Override
525+
public void call(String _) {}
526+
});
527+
528+
Subscription subscription = connectable.connect();
529+
assertTrue(latch.await(1000, TimeUnit.MILLISECONDS));
530+
assertEquals(1, count.get());
531+
subscription.unsubscribe();
532+
}
533+
492534
@Test
493535
public void testReplay() throws InterruptedException {
494536
final AtomicInteger counter = new AtomicInteger();
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package rx;
2+
3+
import static org.junit.Assert.*;
4+
5+
import java.util.ArrayList;
6+
import java.util.List;
7+
import java.util.concurrent.TimeUnit;
8+
9+
import org.junit.Test;
10+
11+
import rx.concurrency.TestScheduler;
12+
import rx.util.functions.Action1;
13+
14+
public class RefCountTest {
15+
16+
@Test
17+
public void testRefCount() {
18+
TestScheduler s = new TestScheduler();
19+
Observable<Long> interval = Observable.interval(100, TimeUnit.MILLISECONDS, s).publish().refCount();
20+
21+
// subscribe list1
22+
final List<Long> list1 = new ArrayList<Long>();
23+
Subscription s1 = interval.subscribe(new Action1<Long>() {
24+
25+
@Override
26+
public void call(Long t1) {
27+
list1.add(t1);
28+
}
29+
30+
});
31+
s.advanceTimeBy(200, TimeUnit.MILLISECONDS);
32+
33+
assertEquals(2, list1.size());
34+
assertEquals(0L, list1.get(0).longValue());
35+
assertEquals(1L, list1.get(1).longValue());
36+
37+
// subscribe list2
38+
final List<Long> list2 = new ArrayList<Long>();
39+
Subscription s2 = interval.subscribe(new Action1<Long>() {
40+
41+
@Override
42+
public void call(Long t1) {
43+
list2.add(t1);
44+
}
45+
46+
});
47+
s.advanceTimeBy(300, TimeUnit.MILLISECONDS);
48+
49+
// list 1 should have 5 items
50+
assertEquals(5, list1.size());
51+
assertEquals(2L, list1.get(2).longValue());
52+
assertEquals(3L, list1.get(3).longValue());
53+
assertEquals(4L, list1.get(4).longValue());
54+
55+
// list 2 should only have 3 items
56+
assertEquals(3, list2.size());
57+
assertEquals(2L, list2.get(0).longValue());
58+
assertEquals(3L, list2.get(1).longValue());
59+
assertEquals(4L, list2.get(2).longValue());
60+
61+
// unsubscribe list1
62+
s1.unsubscribe();
63+
64+
// advance further
65+
s.advanceTimeBy(300, TimeUnit.MILLISECONDS);
66+
67+
// list 1 should still have 5 items
68+
assertEquals(5, list1.size());
69+
70+
// list 2 should have 6 items
71+
assertEquals(6, list2.size());
72+
assertEquals(5L, list2.get(3).longValue());
73+
assertEquals(6L, list2.get(4).longValue());
74+
assertEquals(7L, list2.get(5).longValue());
75+
76+
// unsubscribe list2
77+
s2.unsubscribe();
78+
79+
// advance further
80+
s.advanceTimeBy(1000, TimeUnit.MILLISECONDS);
81+
82+
// the following is not working as it seems the PublishSubject does not allow re-subscribing. TODO fix that in subsequent pull request
83+
84+
85+
// // subscribing a new one should start over because the source should have been unsubscribed
86+
// // subscribe list1
87+
// final List<Long> list3 = new ArrayList<Long>();
88+
// Subscription s3 = interval.subscribe(new Action1<Long>() {
89+
//
90+
// @Override
91+
// public void call(Long t1) {
92+
// list3.add(t1);
93+
// }
94+
//
95+
// });
96+
// s.advanceTimeBy(200, TimeUnit.MILLISECONDS);
97+
//
98+
// assertEquals(2, list3.size());
99+
// assertEquals(0L, list3.get(0).longValue());
100+
// assertEquals(1L, list3.get(1).longValue());
101+
102+
}
103+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package rx;
2+
3+
import org.junit.Before;
4+
import org.junit.Test;
5+
import org.mockito.MockitoAnnotations;
6+
import rx.subscriptions.Subscriptions;
7+
import rx.util.functions.Action0;
8+
9+
import java.util.concurrent.atomic.AtomicInteger;
10+
11+
import static org.junit.Assert.assertEquals;
12+
import static org.mockito.Mockito.mock;
13+
14+
public class RefCountTests {
15+
16+
@Before
17+
public void setUp() {
18+
MockitoAnnotations.initMocks(this);
19+
}
20+
21+
@Test
22+
public void onlyFirstShouldSubscribeAndLastUnsubscribe() {
23+
final AtomicInteger subscriptionCount = new AtomicInteger();
24+
final AtomicInteger unsubscriptionCount = new AtomicInteger();
25+
Observable<Integer> observable = Observable.create(new Observable.OnSubscribeFunc<Integer>() {
26+
@Override
27+
public Subscription onSubscribe(Observer<? super Integer> observer) {
28+
subscriptionCount.incrementAndGet();
29+
return Subscriptions.create(new Action0() {
30+
@Override
31+
public void call() {
32+
unsubscriptionCount.incrementAndGet();
33+
}
34+
});
35+
}
36+
});
37+
Observable<Integer> refCounted = observable.publish().refCount();
38+
Observer<Integer> observer = mock(Observer.class);
39+
Subscription first = refCounted.subscribe(observer);
40+
assertEquals(1, subscriptionCount.get());
41+
Subscription second = refCounted.subscribe(observer);
42+
assertEquals(1, subscriptionCount.get());
43+
first.unsubscribe();
44+
assertEquals(0, unsubscriptionCount.get());
45+
second.unsubscribe();
46+
assertEquals(1, unsubscriptionCount.get());
47+
}
48+
}

0 commit comments

Comments
 (0)