All Downloads are FREE. Search and download functionalities are using the official Maven repository.

rx.subjects.ReplaySubject Maven / Gradle / Ivy

There is a newer version: 1.3.8
Show newest version
/**
 * Copyright 2014 Netflix, Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package rx.subjects;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import rx.Observer;
import rx.Scheduler;
import rx.annotations.Beta;
import rx.exceptions.Exceptions;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.internal.operators.NotificationLite;
import rx.internal.util.UtilityFunctions;
import rx.schedulers.Timestamped;
import rx.subjects.ReplaySubject.NodeList.Node;
import rx.subjects.SubjectSubscriptionManager.SubjectObserver;

/**
 * Subject that buffers all items it observes and replays them to any {@link Observer} that subscribes.
 * 

* *

* Example usage: *

*

 {@code

  ReplaySubject subject = ReplaySubject.create();
  subject.onNext("one");
  subject.onNext("two");
  subject.onNext("three");
  subject.onCompleted();

  // both of the following will get the onNext/onCompleted calls from above
  subject.subscribe(observer1);
  subject.subscribe(observer2);

  } 
 * 
 * @param 
 *          the type of items observed and emitted by the Subject
 */
public final class ReplaySubject extends Subject {
    /**
     * Creates an unbounded replay subject.
     * 

* The internal buffer is backed by an {@link ArrayList} and starts with an initial capacity of 16. Once the * number of items reaches this capacity, it will grow as necessary (usually by 50%). However, as the * number of items grows, this causes frequent array reallocation and copying, and may hurt performance * and latency. This can be avoided with the {@link #create(int)} overload which takes an initial capacity * parameter and can be tuned to reduce the array reallocation frequency as needed. * * @param * the type of items observed and emitted by the Subject * @return the created subject */ public static ReplaySubject create() { return create(16); } /** * Creates an unbounded replay subject with the specified initial buffer capacity. *

* Use this method to avoid excessive array reallocation while the internal buffer grows to accomodate new * items. For example, if you know that the buffer will hold 32k items, you can ask the * {@code ReplaySubject} to preallocate its internal array with a capacity to hold that many items. Once * the items start to arrive, the internal array won't need to grow, creating less garbage and no overhead * due to frequent array-copying. * * @param * the type of items observed and emitted by the Subject * @param capacity * the initial buffer capacity * @return the created subject */ public static ReplaySubject create(int capacity) { final UnboundedReplayState state = new UnboundedReplayState(capacity); SubjectSubscriptionManager ssm = new SubjectSubscriptionManager(); ssm.onStart = new Action1>() { @Override public void call(SubjectObserver o) { // replay history for this observer using the subscribing thread int lastIndex = state.replayObserverFromIndex(0, o); // now that it is caught up add to observers o.index(lastIndex); } }; ssm.onAdded = new Action1>() { @Override public void call(SubjectObserver o) { synchronized (o) { if (!o.first || o.emitting) { return; } o.first = false; o.emitting = true; } boolean skipFinal = false; try { //noinspection UnnecessaryLocalVariable - Avoid re-read from outside this scope final UnboundedReplayState localState = state; for (;;) { int idx = o.index(); int sidx = localState.get(); if (idx != sidx) { Integer j = localState.replayObserverFromIndex(idx, o); o.index(j); } synchronized (o) { if (sidx == localState.get()) { o.emitting = false; skipFinal = true; break; } } } } finally { if (!skipFinal) { synchronized (o) { o.emitting = false; } } } } }; ssm.onTerminated = new Action1>() { @Override public void call(SubjectObserver o) { Integer idx = o.index(); if (idx == null) { idx = 0; } // we will finish replaying if there is anything left state.replayObserverFromIndex(idx, o); } }; return new ReplaySubject(ssm, ssm, state); } /** * Creates an unbounded replay subject with the bounded-implementation for testing purposes. *

* This variant behaves like the regular unbounded {@code ReplaySubject} created via {@link #create()} but * uses the structures of the bounded-implementation. This is by no means intended for the replacement of * the original, array-backed and unbounded {@code ReplaySubject} due to the additional overhead of the * linked-list based internal buffer. The sole purpose is to allow testing and reasoning about the behavior * of the bounded implementations without the interference of the eviction policies. * * @param * the type of items observed and emitted by the Subject * @return the created subject */ /* public */ static ReplaySubject createUnbounded() { final BoundedState state = new BoundedState( new EmptyEvictionPolicy(), UtilityFunctions.identity(), UtilityFunctions.identity() ); return createWithState(state, new DefaultOnAdd(state)); } /** * Creates a size-bounded replay subject. *

* In this setting, the {@code ReplaySubject} holds at most {@code size} items in its internal buffer and * discards the oldest item. *

* When observers subscribe to a terminated {@code ReplaySubject}, they are guaranteed to see at most * {@code size} {@code onNext} events followed by a termination event. *

* If an observer subscribes while the {@code ReplaySubject} is active, it will observe all items in the * buffer at that point in time and each item observed afterwards, even if the buffer evicts items due to * the size constraint in the mean time. In other words, once an Observer subscribes, it will receive items * without gaps in the sequence. * * @param * the type of items observed and emitted by the Subject * @param size * the maximum number of buffered items * @return the created subject */ public static ReplaySubject createWithSize(int size) { final BoundedState state = new BoundedState( new SizeEvictionPolicy(size), UtilityFunctions.identity(), UtilityFunctions.identity() ); return createWithState(state, new DefaultOnAdd(state)); } /** * Creates a time-bounded replay subject. *

* In this setting, the {@code ReplaySubject} internally tags each observed item with a timestamp value * supplied by the {@link Scheduler} and keeps only those whose age is less than the supplied time value * converted to milliseconds. For example, an item arrives at T=0 and the max age is set to 5; at T>=5 * this first item is then evicted by any subsequent item or termination event, leaving the buffer empty. *

* Once the subject is terminated, observers subscribing to it will receive items that remained in the * buffer after the terminal event, regardless of their age. *

* If an observer subscribes while the {@code ReplaySubject} is active, it will observe only those items * from within the buffer that have an age less than the specified time, and each item observed thereafter, * even if the buffer evicts items due to the time constraint in the mean time. In other words, once an * observer subscribes, it observes items without gaps in the sequence except for any outdated items at the * beginning of the sequence. *

* Note that terminal notifications ({@code onError} and {@code onCompleted}) trigger eviction as well. For * example, with a max age of 5, the first item is observed at T=0, then an {@code onCompleted} notification * arrives at T=10. If an observer subscribes at T=11, it will find an empty {@code ReplaySubject} with just * an {@code onCompleted} notification. * * @param * the type of items observed and emitted by the Subject * @param time * the maximum age of the contained items * @param unit * the time unit of {@code time} * @param scheduler * the {@link Scheduler} that provides the current time * @return the created subject */ public static ReplaySubject createWithTime(long time, TimeUnit unit, final Scheduler scheduler) { final BoundedState state = new BoundedState( new TimeEvictionPolicy(unit.toMillis(time), scheduler), new AddTimestamped(scheduler), new RemoveTimestamped() ); return createWithState(state, new TimedOnAdd(state, scheduler)); } /** * Creates a time- and size-bounded replay subject. *

* In this setting, the {@code ReplaySubject} internally tags each received item with a timestamp value * supplied by the {@link Scheduler} and holds at most {@code size} items in its internal buffer. It evicts * items from the start of the buffer if their age becomes less-than or equal to the supplied age in * milliseconds or the buffer reaches its {@code size} limit. *

* When observers subscribe to a terminated {@code ReplaySubject}, they observe the items that remained in * the buffer after the terminal notification, regardless of their age, but at most {@code size} items. *

* If an observer subscribes while the {@code ReplaySubject} is active, it will observe only those items * from within the buffer that have age less than the specified time and each subsequent item, even if the * buffer evicts items due to the time constraint in the mean time. In other words, once an observer * subscribes, it observes items without gaps in the sequence except for the outdated items at the beginning * of the sequence. *

* Note that terminal notifications ({@code onError} and {@code onCompleted}) trigger eviction as well. For * example, with a max age of 5, the first item is observed at T=0, then an {@code onCompleted} notification * arrives at T=10. If an observer subscribes at T=11, it will find an empty {@code ReplaySubject} with just * an {@code onCompleted} notification. * * @param * the type of items observed and emitted by the Subject * @param time * the maximum age of the contained items * @param unit * the time unit of {@code time} * @param size * the maximum number of buffered items * @param scheduler * the {@link Scheduler} that provides the current time * @return the created subject */ public static ReplaySubject createWithTimeAndSize(long time, TimeUnit unit, int size, final Scheduler scheduler) { final BoundedState state = new BoundedState( new PairEvictionPolicy( new SizeEvictionPolicy(size), new TimeEvictionPolicy(unit.toMillis(time), scheduler) ), new AddTimestamped(scheduler), new RemoveTimestamped() ); return createWithState(state, new TimedOnAdd(state, scheduler)); } /** * Creates a bounded replay subject with the given state shared between the subject and the * {@link OnSubscribe} functions. * * @param * the type of items observed and emitted by the Subject * @param state * the shared state * @return the created subject */ static final ReplaySubject createWithState(final BoundedState state, Action1> onStart) { SubjectSubscriptionManager ssm = new SubjectSubscriptionManager(); ssm.onStart = onStart; ssm.onAdded = new Action1>() { @Override public void call(SubjectObserver o) { synchronized (o) { if (!o.first || o.emitting) { return; } o.first = false; o.emitting = true; } boolean skipFinal = false; try { for (;;) { NodeList.Node idx = o.index(); NodeList.Node sidx = state.tail(); if (idx != sidx) { NodeList.Node j = state.replayObserverFromIndex(idx, o); o.index(j); } synchronized (o) { if (sidx == state.tail()) { o.emitting = false; skipFinal = true; break; } } } } finally { if (!skipFinal) { synchronized (o) { o.emitting = false; } } } } }; ssm.onTerminated = new Action1>() { @Override public void call(SubjectObserver t1) { NodeList.Node l = t1.index(); if (l == null) { l = state.head(); } state.replayObserverFromIndex(l, t1); } }; return new ReplaySubject(ssm, ssm, state); } /** The state storing the history and the references. */ final ReplayState state; /** The manager of subscribers. */ final SubjectSubscriptionManager ssm; ReplaySubject(OnSubscribe onSubscribe, SubjectSubscriptionManager ssm, ReplayState state) { super(onSubscribe); this.ssm = ssm; this.state = state; } @Override public void onNext(T t) { if (ssm.active) { state.next(t); for (SubjectSubscriptionManager.SubjectObserver o : ssm.observers()) { if (caughtUp(o)) { o.onNext(t); } } } } @Override public void onError(final Throwable e) { if (ssm.active) { state.error(e); List errors = null; for (SubjectObserver o : ssm.terminate(NotificationLite.instance().error(e))) { try { if (caughtUp(o)) { o.onError(e); } } catch (Throwable e2) { if (errors == null) { errors = new ArrayList(); } errors.add(e2); } } Exceptions.throwIfAny(errors); } } @Override public void onCompleted() { if (ssm.active) { state.complete(); for (SubjectObserver o : ssm.terminate(NotificationLite.instance().completed())) { if (caughtUp(o)) { o.onCompleted(); } } } } /** * @return Returns the number of subscribers. */ /* Support test. */int subscriberCount() { return ssm.get().observers.length; } @Override public boolean hasObservers() { return ssm.observers().length > 0; } private boolean caughtUp(SubjectObserver o) { if (!o.caughtUp) { if (state.replayObserver(o)) { o.caughtUp = true; o.index(null); // once caught up, no need for the index anymore } return false; } else { // it was caught up so proceed the "raw route" return true; } } // ********************* // State implementations // ********************* /** * The unbounded replay state. * @param the input and output type */ static final class UnboundedReplayState extends AtomicInteger implements ReplayState { private final NotificationLite nl = NotificationLite.instance(); /** The buffer. */ private final ArrayList list; /** The termination flag. */ private volatile boolean terminated; public UnboundedReplayState(int initialCapacity) { list = new ArrayList(initialCapacity); } @Override public void next(T n) { if (!terminated) { list.add(nl.next(n)); getAndIncrement(); // release index } } public void accept(Observer o, int idx) { nl.accept(o, list.get(idx)); } @Override public void complete() { if (!terminated) { terminated = true; list.add(nl.completed()); getAndIncrement(); // release index } } @Override public void error(Throwable e) { if (!terminated) { terminated = true; list.add(nl.error(e)); getAndIncrement(); // release index } } @Override public boolean terminated() { return terminated; } @Override public boolean replayObserver(SubjectObserver observer) { synchronized (observer) { observer.first = false; if (observer.emitting) { return false; } } Integer lastEmittedLink = observer.index(); if (lastEmittedLink != null) { int l = replayObserverFromIndex(lastEmittedLink, observer); observer.index(l); return true; } else { throw new IllegalStateException("failed to find lastEmittedLink for: " + observer); } } @Override public Integer replayObserverFromIndex(Integer idx, SubjectObserver observer) { int i = idx; while (i < get()) { accept(observer, i); i++; } return i; } @Override public Integer replayObserverFromIndexTest(Integer idx, SubjectObserver observer, long now) { return replayObserverFromIndex(idx, observer); } @Override public int size() { int idx = get(); // aquire if (idx > 0) { Object o = list.get(idx - 1); if (nl.isCompleted(o) || nl.isError(o)) { return idx - 1; // do not report a terminal event as part of size } } return idx; } @Override public boolean isEmpty() { return size() == 0; } @Override @SuppressWarnings("unchecked") public T[] toArray(T[] a) { int s = size(); if (s > 0) { if (s > a.length) { a = (T[])Array.newInstance(a.getClass().getComponentType(), s); } for (int i = 0; i < s; i++) { a[i] = (T)list.get(i); } if (a.length > s) { a[s] = null; } } else if (a.length > 0) { a[0] = null; } return a; } @Override public T latest() { int idx = get(); if (idx > 0) { Object o = list.get(idx - 1); if (nl.isCompleted(o) || nl.isError(o)) { if (idx > 1) { return nl.getValue(list.get(idx - 2)); } return null; } return nl.getValue(o); } return null; } } /** * The bounded replay state. * @param the input and output type */ static final class BoundedState implements ReplayState> { final NodeList list; final EvictionPolicy evictionPolicy; final Func1 enterTransform; final Func1 leaveTransform; final NotificationLite nl = NotificationLite.instance(); volatile boolean terminated; volatile NodeList.Node tail; public BoundedState(EvictionPolicy evictionPolicy, Func1 enterTransform, Func1 leaveTransform) { this.list = new NodeList(); this.tail = list.tail; this.evictionPolicy = evictionPolicy; this.enterTransform = enterTransform; this.leaveTransform = leaveTransform; } @Override public void next(T value) { if (!terminated) { list.addLast(enterTransform.call(nl.next(value))); evictionPolicy.evict(list); tail = list.tail; } } @Override public void complete() { if (!terminated) { terminated = true; list.addLast(enterTransform.call(nl.completed())); evictionPolicy.evictFinal(list); tail = list.tail; } } @Override public void error(Throwable e) { if (!terminated) { terminated = true; list.addLast(enterTransform.call(nl.error(e))); // don't evict the terminal value evictionPolicy.evictFinal(list); tail = list.tail; } } public void accept(Observer o, NodeList.Node node) { nl.accept(o, leaveTransform.call(node.value)); } /** * Accept only non-stale nodes. * @param o the target observer * @param node the node to accept or reject * @param now the current time */ public void acceptTest(Observer o, NodeList.Node node, long now) { Object v = node.value; if (!evictionPolicy.test(v, now)) { nl.accept(o, leaveTransform.call(v)); } } public Node head() { return list.head; } public Node tail() { return tail; } @Override public boolean replayObserver(SubjectObserver observer) { synchronized (observer) { observer.first = false; if (observer.emitting) { return false; } } NodeList.Node lastEmittedLink = observer.index(); NodeList.Node l = replayObserverFromIndex(lastEmittedLink, observer); observer.index(l); return true; } @Override public NodeList.Node replayObserverFromIndex( NodeList.Node l, SubjectObserver observer) { while (l != tail()) { accept(observer, l.next); l = l.next; } return l; } @Override public NodeList.Node replayObserverFromIndexTest( NodeList.Node l, SubjectObserver observer, long now) { while (l != tail()) { acceptTest(observer, l.next, now); l = l.next; } return l; } @Override public boolean terminated() { return terminated; } @Override public int size() { int size = 0; NodeList.Node l = head(); NodeList.Node next = l.next; while (next != null) { size++; l = next; next = next.next; } if (l.value != null) { Object value = leaveTransform.call(l.value); if (value != null && (nl.isError(value) || nl.isCompleted(value))) { return size - 1; } } return size; } @Override public boolean isEmpty() { NodeList.Node l = head(); NodeList.Node next = l.next; if (next == null) { return true; } Object value = leaveTransform.call(next.value); return nl.isError(value) || nl.isCompleted(value); } @Override @SuppressWarnings("unchecked") public T[] toArray(T[] a) { List list = new ArrayList(); NodeList.Node l = head(); NodeList.Node next = l.next; while (next != null) { Object o = leaveTransform.call(next.value); if (next.next == null && (nl.isError(o) || nl.isCompleted(o))) { break; } else { list.add((T)o); } l = next; next = next.next; } return list.toArray(a); } @Override public T latest() { Node h = head().next; if (h == null) { return null; } Node p = null; while (h != tail()) { p = h; h = h.next; } Object value = leaveTransform.call(h.value); if (nl.isError(value) || nl.isCompleted(value)) { if (p != null) { value = leaveTransform.call(p.value); return nl.getValue(value); } return null; } return nl.getValue(value); } } // ************** // API interfaces // ************** /** * General API for replay state management. * @param the input and output type * @param the index type */ interface ReplayState { /** @return true if the subject has reached a terminal state. */ boolean terminated(); /** * Replay contents to the given observer. * @param observer the receiver of events * @return true if the subject has caught up */ boolean replayObserver(SubjectObserver observer); /** * Replay the buffered values from an index position and return a new index * @param idx the current index position * @param observer the receiver of events * @return the new index position */ I replayObserverFromIndex( I idx, SubjectObserver observer); /** * Replay the buffered values from an index position while testing for stale entries and return a new index * @param idx the current index position * @param observer the receiver of events * @return the new index position */ I replayObserverFromIndexTest( I idx, SubjectObserver observer, long now); /** * Add an OnNext value to the buffer * @param value the value to add */ void next(T value); /** * Add an OnError exception and terminate the subject * @param e the exception to add */ void error(Throwable e); /** * Add an OnCompleted exception and terminate the subject */ void complete(); /** * @return the number of non-terminal values in the replay buffer. */ int size(); /** * @return true if the replay buffer is empty of non-terminal values */ boolean isEmpty(); /** * Copy the current values (minus any terminal value) from the buffer into the array * or create a new array if there isn't enough room. * @param a the array to fill in * @return the array or a new array containing the current values */ T[] toArray(T[] a); /** * Returns the latest value that has been buffered or null if no such value * present. * @return the latest value buffered or null if none */ T latest(); } /** Interface to manage eviction checking. */ interface EvictionPolicy { /** * Subscribe-time checking for stale entries. * @param value the value to test * @param now the current time * @return true if the value may be evicted */ boolean test(Object value, long now); /** * Evict values from the list. * @param list the node list */ void evict(NodeList list); /** * Evict values from the list except the very last which is considered * a terminal event * @param list the node list */ void evictFinal(NodeList list); } // ************************ // Callback implementations // ************************ /** * Remove elements from the beginning of the list if the size exceeds some threshold. */ static final class SizeEvictionPolicy implements EvictionPolicy { final int maxSize; public SizeEvictionPolicy(int maxSize) { this.maxSize = maxSize; } @Override public void evict(NodeList t1) { while (t1.size() > maxSize) { t1.removeFirst(); } } @Override public boolean test(Object value, long now) { return false; // size gets never stale } @Override public void evictFinal(NodeList t1) { while (t1.size() > maxSize + 1) { t1.removeFirst(); } } } /** * Remove elements from the beginning of the list if the Timestamped value is older than * a threshold. */ static final class TimeEvictionPolicy implements EvictionPolicy { final long maxAgeMillis; final Scheduler scheduler; public TimeEvictionPolicy(long maxAgeMillis, Scheduler scheduler) { this.maxAgeMillis = maxAgeMillis; this.scheduler = scheduler; } @Override public void evict(NodeList t1) { long now = scheduler.now(); while (!t1.isEmpty()) { NodeList.Node n = t1.head.next; if (test(n.value, now)) { t1.removeFirst(); } else { break; } } } @Override public void evictFinal(NodeList t1) { long now = scheduler.now(); while (t1.size > 1) { NodeList.Node n = t1.head.next; if (test(n.value, now)) { t1.removeFirst(); } else { break; } } } @Override public boolean test(Object value, long now) { Timestamped ts = (Timestamped)value; return ts.getTimestampMillis() <= now - maxAgeMillis; } } /** * Pairs up two eviction policy callbacks. */ static final class PairEvictionPolicy implements EvictionPolicy { final EvictionPolicy first; final EvictionPolicy second; public PairEvictionPolicy(EvictionPolicy first, EvictionPolicy second) { this.first = first; this.second = second; } @Override public void evict(NodeList t1) { first.evict(t1); second.evict(t1); } @Override public void evictFinal(NodeList t1) { first.evictFinal(t1); second.evictFinal(t1); } @Override public boolean test(Object value, long now) { return first.test(value, now) || second.test(value, now); } } /** Maps the values to Timestamped. */ static final class AddTimestamped implements Func1 { final Scheduler scheduler; public AddTimestamped(Scheduler scheduler) { this.scheduler = scheduler; } @Override public Object call(Object t1) { return new Timestamped(scheduler.now(), t1); } } /** Maps timestamped values back to raw objects. */ static final class RemoveTimestamped implements Func1 { @Override @SuppressWarnings("unchecked") public Object call(Object t1) { return ((Timestamped)t1).getValue(); } } /** * Default action of simply replaying the buffer on subscribe. * @param the input and output value type */ static final class DefaultOnAdd implements Action1> { final BoundedState state; public DefaultOnAdd(BoundedState state) { this.state = state; } @Override public void call(SubjectObserver t1) { NodeList.Node l = state.replayObserverFromIndex(state.head(), t1); t1.index(l); } } /** * Action of replaying non-stale entries of the buffer on subscribe * @param the input and output value */ static final class TimedOnAdd implements Action1> { final BoundedState state; final Scheduler scheduler; public TimedOnAdd(BoundedState state, Scheduler scheduler) { this.state = state; this.scheduler = scheduler; } @Override public void call(SubjectObserver t1) { NodeList.Node l; if (!state.terminated) { // ignore stale entries if still active l = state.replayObserverFromIndexTest(state.head(), t1, scheduler.now()); } else { // accept all if terminated l = state.replayObserverFromIndex(state.head(), t1); } t1.index(l); } } /** * A singly-linked list with volatile next node pointer. * @param the value type */ static final class NodeList { /** * The node containing the value and references to neighbours. * @param the value type */ static final class Node { /** The managed value. */ final T value; /** The hard reference to the next node. */ volatile Node next; Node(T value) { this.value = value; } } /** The head of the list. */ final Node head = new Node(null); /** The tail of the list. */ Node tail = head; /** The number of elements in the list. */ int size; public void addLast(T value) { Node t = tail; Node t2 = new Node(value); t.next = t2; tail = t2; size++; } public T removeFirst() { if (head.next == null) { throw new IllegalStateException("Empty!"); } Node t = head.next; head.next = t.next; if (head.next == null) { tail = head; } size--; return t.value; } public boolean isEmpty() { return size == 0; } public int size() { return size; } public void clear() { tail = head; size = 0; } } /** Empty eviction policy. */ static final class EmptyEvictionPolicy implements EvictionPolicy { @Override public boolean test(Object value, long now) { return true; } @Override public void evict(NodeList list) { } @Override public void evictFinal(NodeList list) { } } /** * Check if the Subject has terminated with an exception. * @return true if the subject has received a throwable through {@code onError}. */ @Beta public boolean hasThrowable() { NotificationLite nl = ssm.nl; Object o = ssm.getLatest(); return nl.isError(o); } /** * Check if the Subject has terminated normally. * @return true if the subject completed normally via {@code onCompleted} */ @Beta public boolean hasCompleted() { NotificationLite nl = ssm.nl; Object o = ssm.getLatest(); return o != null && !nl.isError(o); } /** * Returns the Throwable that terminated the Subject. * @return the Throwable that terminated the Subject or {@code null} if the * subject hasn't terminated yet or it terminated normally. */ @Beta public Throwable getThrowable() { NotificationLite nl = ssm.nl; Object o = ssm.getLatest(); if (nl.isError(o)) { return nl.getError(o); } return null; } /** * Returns the current number of items (non-terminal events) available for replay. * @return the number of items available */ @Beta public int size() { return state.size(); } /** * @return true if the Subject holds at least one non-terminal event available for replay */ @Beta public boolean hasAnyValue() { return !state.isEmpty(); } @Beta public boolean hasValue() { return hasAnyValue(); } /** * Returns a snapshot of the currently buffered non-terminal events into * the provided {@code a} array or creates a new array if it has not enough capacity. * @param a the array to fill in * @return the array {@code a} if it had enough capacity or a new array containing the available values */ @Beta public T[] getValues(T[] a) { return state.toArray(a); } /** An empty array to trigger getValues() to return a new array. */ private static final Object[] EMPTY_ARRAY = new Object[0]; /** * Returns a snapshot of the currently buffered non-terminal events. *

The operation is threadsafe. * * @return a snapshot of the currently buffered non-terminal events. * @since (If this graduates from being an Experimental class method, replace this parenthetical with the release number) */ @SuppressWarnings("unchecked") @Beta public Object[] getValues() { T[] r = getValues((T[])EMPTY_ARRAY); if (r == EMPTY_ARRAY) { return new Object[0]; // don't leak the default empty array. } return r; } @Beta public T getValue() { return state.latest(); } }