package learnrxjava.examples; import java.util.concurrent.TimeUnit; import rx.Observable; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; import rx.schedulers.TestScheduler; import rx.subjects.PublishSubject; public class GroupByLogic { public static void main(String[] args) { final TestScheduler testScheduler = Schedulers.test(); final PublishSubject<PlayEvent> testSubject = PublishSubject.<PlayEvent> create(); TestSubscriber<StreamState> ts = new TestSubscriber<StreamState>(); testSubject.groupBy(playEvt -> playEvt.getOriginatorId()) .flatMap(groupedObservable -> { System.out.println("***** new group: " + groupedObservable.getKey()); return groupedObservable /* Timeout after last event, and preventing memory leaks so that inactive streams are closed */ .timeout(3, TimeUnit.HOURS, testScheduler) /* So that consecutive identical playevents are skipped, can also use skipWhile */ .distinctUntilChanged(PlayEvent::getSession) .onErrorResumeNext(t -> { System.out.println(" ***** complete group: " + groupedObservable.getKey()); // complete if we timeout or have an error of any kind (this could emit a special PlayEvent instead return Observable.empty(); }) // since we have finite groups we can `reduce` to a single value, otherwise use `scan` if you want to emit along the way .reduce(new StreamState(), (state, playEvent) -> { System.out.println(" state: " + state + " event: " + playEvent.id + "-" + playEvent.session); // business logic happens here to accumulate state state.addEvent(playEvent); return state; }) .filter(state -> { // if using `scan` above instead of `reduce` you could conditionally remove what you don't want to pass down return true; }); }) .doOnNext(e -> System.out.println(">>> Output State: " + e)) .subscribe(ts); testSubject.onNext(createPlayEvent(1, "a")); testSubject.onNext(createPlayEvent(2, "a")); testScheduler.advanceTimeBy(2, TimeUnit.HOURS); testSubject.onNext(createPlayEvent(1, "b")); testScheduler.advanceTimeBy(2, TimeUnit.HOURS); testSubject.onNext(createPlayEvent(1, "a")); testSubject.onNext(createPlayEvent(2, "b")); System.out.println("onNext after 4 hours: " + ts.getOnNextEvents()); testScheduler.advanceTimeBy(3, TimeUnit.HOURS); System.out.println("onNext after 7 hours: " + ts.getOnNextEvents()); testSubject.onCompleted(); testSubject.onNext(createPlayEvent(2, "b")); System.out.println("onNext after complete: " + ts.getOnNextEvents()); ts.assertTerminalEvent(); ts.assertNoErrors(); } public static PlayEvent createPlayEvent(int id, String v) { return new PlayEvent(id, v); } public static class PlayEvent { private int id; private String session; public PlayEvent(int id, String session) { this.id = id; this.session = session; } public int getOriginatorId() { return id; } public String getSession() { return session; } } public static class StreamState { private int id = -1; public void addEvent(PlayEvent event) { if (id == -1) { this.id = event.id; } // add business logic here } @Override public String toString() { return "StreamState => id: " + id; } } }