001package arez.promise;
002
003import akasha.promise.Promise;
004import arez.Arez;
005import arez.annotations.Action;
006import arez.annotations.ArezComponent;
007import arez.annotations.Feature;
008import arez.annotations.Observable;
009import java.util.Objects;
010import javax.annotation.Nonnull;
011import jsinterop.base.Js;
012import static org.realityforge.braincheck.Guards.*;
013
014/**
015 * An observable model that wraps a Promise and exposes observable state that track
016 * the state of the promise. The observable exposes the state of the promise as well
017 * as the value that it resolves to or the error it was rejected with as observable
018 * properties.
019 *
020 * <p>A very simple example</p>
021 * <pre>{@code
022 * import akasha.Console;
023 * import akasha.Response;
024 * import akasha.WindowGlobal;
025 * import akasha.promise.Promise;
026 * import arez.Arez;
027 * import arez.promise.ObservablePromise;
028 * import com.google.gwt.core.client.EntryPoint;
029 *
030 * public class Example
031 *   implements EntryPoint
032 * {
033 *   public void onModuleLoad()
034 *   {
035 *     final Promise<Response> fetch = WindowGlobal.fetch( "https://example.com/" );
036 *     final ObservablePromise<Response, Object> observablePromise = ObservablePromise.create( promise );
037 *     Arez.context().observer( () -> Console.log( "Promise Status: " + observablePromise.getState() ) );
038 *   }
039 * }
040 * }</pre>
041 *
042 * @param <T> the type of the value that the promise will resolve to.
043 * @param <E> the type of the error if the promise is rejected.
044 */
045@ArezComponent( requireId = Feature.DISABLE )
046public abstract class ObservablePromise<T, E>
047{
048  /**
049   * The state of the promise.
050   */
051  public enum State
052  {
053    PENDING, FULFILLED, REJECTED
054  }
055
056  /**
057   * The underlying promise.
058   * This is not converted to a local variable to make it easy to debug scenarios from within the
059   * browsers DevTools.
060   */
061  @SuppressWarnings( "FieldCanBeLocal" )
062  private final Promise<T> _promise;
063  /**
064   * The state of the promise. Starts as {@link State#PENDING} and then transitions to either
065   * {@link State#FULFILLED} or {@link State#REJECTED}.
066   */
067  @Nonnull
068  private State _state;
069  /**
070   * The value that the promise resolved to. This is not valid unless the state is {@link State#FULFILLED}.
071   */
072  private T _value;
073  /**
074   * The error that the promise was rejected with. This is not valid unless the state is {@link State#REJECTED}.
075   */
076  private E _error;
077
078  /**
079   * Create the observable model that wraps specified promise.
080   *
081   * @param <T>     the type of the value that the promise will resolve to.
082   * @param <E>     the type of the error if the promise is rejected.
083   * @param promise the promise to wrap.
084   * @return the ObservablePromise
085   */
086  @Nonnull
087  public static <T, E> ObservablePromise<T, E> create( @Nonnull final Promise<T> promise )
088  {
089    return new Arez_ObservablePromise<>( promise );
090  }
091
092  ObservablePromise( @Nonnull final Promise<T> promise )
093  {
094    _state = State.PENDING;
095    _promise = Objects.requireNonNull( promise );
096    _promise.then( this::onFulfilled ).catch_( this::onRejected );
097  }
098
099  /**
100   * Return the promise state.
101   *
102   * @return the promise state.
103   */
104  @Observable
105  @Nonnull
106  public State getState()
107  {
108    return _state;
109  }
110
111  void setState( @Nonnull final State state )
112  {
113    _state = Objects.requireNonNull( state );
114  }
115
116  /**
117   * Return the value that the promise was resolved to.
118   * This should NOT be called if the state is not {@link State#FULFILLED} and will result in an invariant
119   * failure if invariants are enabled.
120   *
121   * @return the value that the promise was resolved to.
122   */
123  @Observable
124  public T getValue()
125  {
126    if ( Arez.shouldCheckApiInvariants() )
127    {
128      apiInvariant( () -> _state == State.FULFILLED,
129                    () -> "Arez-0165: ObservablePromise.getValue() called when the promise is not in " +
130                          "fulfilled state. State: " + _state + ", Promise: " + _promise );
131    }
132    return _value;
133  }
134
135  void setValue( final T value )
136  {
137    if ( Arez.shouldCheckInvariants() )
138    {
139      invariant( () -> _state == State.FULFILLED,
140                 () -> "Arez-0166: ObservablePromise.setValue() called when promise is in incorrect state. " +
141                       "State: " + _state + ", Promise: " + _promise );
142    }
143    _value = value;
144  }
145
146  /**
147   * Return the error that the promise was rejected with.
148   * This should NOT be called if the state is not {@link State#REJECTED} and will result in an invariant
149   * failure if invariants are enabled.
150   *
151   * @return the error that the promise was rejected with.
152   */
153  @Observable
154  public E getError()
155  {
156    if ( Arez.shouldCheckApiInvariants() )
157    {
158      apiInvariant( () -> _state == State.REJECTED,
159                    () -> "ObservablePromise.getError() called when the promise is not in " +
160                          "rejected state. State: " + _state + ", Promise: " + _promise );
161    }
162    return _error;
163  }
164
165  void setError( final E error )
166  {
167    if ( Arez.shouldCheckInvariants() )
168    {
169      invariant( () -> _state == State.REJECTED,
170                 () -> "ObservablePromise.setError() called when promise is in incorrect state. " +
171                       "State: " + _state + ", Promise: " + _promise );
172    }
173    _error = error;
174  }
175
176  @Action
177  @Nonnull
178  Promise<T> onFulfilled( final T value )
179  {
180    setState( State.FULFILLED );
181    setValue( value );
182    return Promise.resolve( value );
183  }
184
185  @Action
186  Promise<Object> onRejected( @Nonnull final Object error )
187  {
188    setState( State.REJECTED );
189    setError( Js.uncheckedCast( error ) );
190    return Promise.reject( error );
191  }
192}