package io.nextop.rx; import com.google.common.cache.*; import immutablecollections.ImMap; import immutablecollections.ImSet; import io.nextop.Id; import rx.Observable; import rx.Observer; import rx.Subscriber; import rx.Subscription; import rx.functions.Action0; import rx.functions.Action1; import rx.functions.Func1; import rx.functions.Func2; import rx.subscriptions.BooleanSubscription; import javax.annotation.Nullable; import java.util.Iterator; /** Base for model or view model managers. Provides a stream of objects cleanup, * based on a stream of updates applied to a persistent state. */ // TODO provide controls for caching the persistent state public abstract class RxManager<M extends RxManaged> { private final Cache<Id, ManagedState> cachedStates; private ImMap<Id, ManagedState> subscribedStates; public RxManager() { cachedStates = CacheBuilder.<Id, ManagedState>newBuilder() .concurrencyLevel(1) .removalListener(new RemovalListener<Id, ManagedState>() { @Override public void onRemoval(RemovalNotification<Id, ManagedState> notification) { ManagedState state = notification.getValue(); state.cached = false; cleanup(state); } }).weigher(new Weigher<Id, ManagedState>() { @Override public int weigh(Id key, ManagedState value) { return /* FIXME */ 1; } }).maximumWeight(/* FIXME */ 1024 ).build(); subscribedStates = ImMap.empty(); } public Observable<M> peek(Id id) { @Nullable M m = peekValue(id); if (null != m) { return Observable.just(m); } else { return Observable.empty(); } } public @Nullable M peekValue(Id id) { @Nullable ManagedState state = state(id, false); if (null != state && state.syncd) { return state.m; } else { return null; } } public Observable<M> get(Id id) { return getCompleteState(id).map(new Func1<ManagedState, M>() { @Override public M call(ManagedState state) { return state.m; } }); } /** removes all subscriptions and clears the cache. * The manager is still usable after this operation. */ public void clear() { unsubscribe(); cachedStates.invalidateAll(); assert 0 == cachedStates.size(); } public void unsubscribe() { for (Iterator<ImMap.Entry<Id, ManagedState>> itr = subscribedStates.iterator(); itr.hasNext(); ) { itr.next().getValue().unsubscribe(); } assert subscribedStates.isEmpty(); } private void addSubscribedState(ManagedState state) { subscribedStates = subscribedStates.put(state.id, state); startUpdates(state.m, state); } private void removeSubscribedState(ManagedState state) { stopUpdates(state.id); subscribedStates = subscribedStates.remove(state.id); cleanup(state); } @Nullable private ManagedState state(Id id, boolean create) { // always isSend the cache first because it counts references - e.g. lru @Nullable ManagedState state = cachedStates.getIfPresent(id); if (null != state) { return state; } state = subscribedStates.get(id); if (null == state && create) { M m = create(id); if (!id.equals(m.id)) { throw new IllegalStateException("#create must return a managed object with the same id as input."); } state = new ManagedState(m); verifyState(state); } if (null != state) { // put it in the cache state.cached = true; cachedStates.put(id, state); } return state; } private void cleanup(ManagedState state) { if (!state.isCached() && !state.isSubscribed()) { state.close(); } } // returned objects here are not guaranteed to have startUpdates/stopUpdates called before ejecting // creating these objects should have no side effects protected abstract M create(Id id); // important: this is not an update block // updates must be done inside of update(...) to publish correctly protected void startUpdates(M m, RxState state) { // the default action is to publish the state in memory // in cases where the state has to be read from state.sync(); } protected void stopUpdates(Id id) { // Do nothing } // FIXME close callback for state // FIXME close callback on RxManaged // protected void publish(Id id) { // @Nullable MM state = states.get(id); // if (null != state) { // publish(state); // } // } private void publish(ManagedState state) { int publishCount = ++state.publishCount; for (Subscriber<? super ManagedState> subscriber : state.subscribers) { subscriber.onNext(state); // a nested publish cut off this publish // don't publish an older notification if (publishCount != state.publishCount) { break; } } } // FIXME rename "syncd" to "sync'd" // protected void setSyncd(Id id) { // updateState(id, new Action1<ManagedState>() { // @Override // public void call(ManagedState mm) { // mm.syncd = true; // } // }); // } protected void updateComplete(final Id id, final Func2<M, RxState, M> updater) { updateCompleteState(id, new UpdateAdapter(updater)); } // updates the view model // publishes an update protected void update(final Id id, final Func2<M, RxState, M> updater) { updateState(id, new UpdateAdapter(updater)); } final class UpdateAdapter implements Action1<ManagedState> { final Func2<M, RxState, M> updater; UpdateAdapter(Func2<M, RxState, M> updater) { this.updater = updater; } @Override public void call(ManagedState state) { @Nullable M newM = updater.call(state.m, state); // null returns mean "do not update reference" if (null != newM) { state.m = newM; } } } private void updateCompleteState(final Id id, final Action1<ManagedState> updater) { getCompleteState(id).take(1).subscribe(new UpdateStateAdapter(updater)); } // updates the view model // publishes an update private void updateState(final Id id, final Action1<ManagedState> updater) { getState(id).take(1).subscribe(new UpdateStateAdapter(updater)); } final class UpdateStateAdapter implements Observer<ManagedState> { final Action1<ManagedState> updater; @Nullable ManagedState state = null; UpdateStateAdapter(Action1<ManagedState> updater) { this.updater = updater; } @Override public void onNext(ManagedState state) { // apply at most once if (null != this.state) { throw new IllegalStateException(); } this.state = state; updater.call(state); verifyState(state); } @Override public void onCompleted() { if (null != state) { publish(state); } } @Override public void onError(Throwable e) { // TODO log } } private Observable<ManagedState> getCompleteState(final Id id) { return getState(id).filter(new Func1<ManagedState, Boolean>() { @Override public Boolean call(ManagedState state) { return state.syncd; } }); } private Observable<ManagedState> getState(final Id id) { return Observable.create(new Observable.OnSubscribe<ManagedState>() { @Override public void call(final Subscriber<? super ManagedState> subscriber) { final ManagedState state = state(id, true); subscriber.add(BooleanSubscription.create(new Action0() { @Override public void call() { state.removeSubscriber(subscriber); } })); int publishCount = state.publishCount; state.addSubscriber(subscriber); // check to avoid double-publishing if (publishCount == state.publishCount) { subscriber.onNext(state); } } }); } private void verifyState(ManagedState state) { if (null == state.m) { throw new IllegalStateException(); } if (!state.id.equals(state.m.id)) { throw new IllegalStateException(); } } private final class ManagedState implements RxState { public final Id id; public M m; // mark this true when the version in memory has caught up with the upstream // when syncd, the state will be exposed via get/peek public boolean syncd = false; ImSet<Subscriber<? super ManagedState>> subscribers = ImSet.empty(); int refCount = 0; int publishCount = 0; boolean cached = false; final RxLifecycleBinder.Lifted binder = new RxLifecycleBinder.Lifted(); public ManagedState(M m) { this.m = m; id = m.id; // always connected binder.connect(null); } boolean isCached() { return cached; } boolean isSubscribed() { return 0 < refCount; } void addSubscriber(Subscriber<? super ManagedState> subscriber) { subscribers = subscribers.adding(subscriber); if (1 == ++refCount) { addSubscribedState(this); // startUpdates(state.m, state); } } void removeSubscriber(Subscriber<? super ManagedState> subscriber) { subscribers = subscribers.removing(subscriber); --refCount; if (0 == refCount) { binder.reset(); removeSubscribedState(this); // stopUpdates(id); } } void unsubscribe() { for (Subscriber<? super ManagedState> subscriber : subscribers) { subscriber.unsubscribe(); } } void close() { m.close(); } /////// RxState /////// @Override public <T> Observable<T> bind(Observable<T> source) { return binder.bind(source); } @Override public void bind(Subscription s) { binder.bind(s); } @Override public void sync() { if (!syncd) { syncd = true; publish(this); } } } public static interface RxState { <T> Observable<T> bind(Observable<T> source); void bind(Subscription s); /** call this when the state in memory has caught up with some version of the truth */ void sync(); } }