001package arez.promise;
002
003import arez.Arez;
004import arez.annotations.Action;
005import arez.annotations.ArezComponent;
006import arez.annotations.Feature;
007import arez.annotations.Observable;
008import java.util.Objects;
009import javax.annotation.Nonnull;
010import jsinterop.annotations.JsFunction;
011import jsinterop.annotations.JsMethod;
012import jsinterop.annotations.JsPackage;
013import jsinterop.annotations.JsType;
014import jsinterop.base.Js;
015import static org.realityforge.braincheck.Guards.*;
016
017/**
018 * An observable model that wraps a Promise and exposes observable state that track
019 * the state of the promise. The observable exposes the state of the promise as well
020 * as the value that it resolves to or the error it was rejected with as observable
021 * properties.
022 *
023 * <p>A very simple example</p>
024 * <pre>{@code
025 * import arez.Arez;
026 * import arez.promise.ObservablePromise;
027 * import arez.promise.ObservablePromise.Promise;
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> promise = fetch( "https://example.com/" );
036 *     final ObservablePromise<Response, Object> observablePromise = ObservablePromise.create( promise );
037 *     Arez.context().observer( () -> System.out.println( "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   * Minimal facade for a JavaScript promise.
050   *
051   * @param <T> the type of value produced by the promise.
052   */
053  @JsType( isNative = true, name = "Promise", namespace = JsPackage.GLOBAL )
054  public static class Promise<T>
055  {
056    protected Promise()
057    {
058    }
059
060    @JsMethod
061    static native <V> Promise<V> resolve( V value );
062
063    @JsMethod
064    static native <V> Promise<V> reject( Object error );
065
066    @JsMethod
067    native <V> Promise<V> then( OnFulfilledCallback<? super T, V> callback );
068
069    @JsMethod( name = "catch" )
070    native <V> Promise<V> catch_( OnRejectedCallback<V> callback );
071  }
072
073  @JsFunction
074  private interface OnFulfilledCallback<T, V>
075  {
076    Promise<V> onFulfilled( T value );
077  }
078
079  @JsFunction
080  private interface OnRejectedCallback<V>
081  {
082    Promise<V> onRejected( Object error );
083  }
084
085  /**
086   * The state of the promise.
087   */
088  public enum State
089  {
090    PENDING, FULFILLED, REJECTED
091  }
092
093  /**
094   * The underlying promise.
095   * This is not converted to a local variable to make it easy to debug scenarios from within the
096   * browsers DevTools.
097   */
098  @SuppressWarnings( "FieldCanBeLocal" )
099  private final Promise<T> _promise;
100  /**
101   * The state of the promise. Starts as {@link State#PENDING} and then transitions to either
102   * {@link State#FULFILLED} or {@link State#REJECTED}.
103   */
104  @Nonnull
105  private State _state;
106  /**
107   * The value that the promise resolved to. This is not valid unless the state is {@link State#FULFILLED}.
108   */
109  private T _value;
110  /**
111   * The error that the promise was rejected with. This is not valid unless the state is {@link State#REJECTED}.
112   */
113  private E _error;
114
115  /**
116   * Create the observable model that wraps specified promise.
117   *
118   * @param <T>     the type of the value that the promise will resolve to.
119   * @param <E>     the type of the error if the promise is rejected.
120   * @param promise the promise to wrap.
121   * @return the ObservablePromise
122   */
123  @Nonnull
124  public static <T, E> ObservablePromise<T, E> create( @Nonnull final Promise<T> promise )
125  {
126    return new Arez_ObservablePromise<>( promise );
127  }
128
129  ObservablePromise( @Nonnull final Promise<T> promise )
130  {
131    _state = State.PENDING;
132    _promise = Objects.requireNonNull( promise );
133    _promise.then( this::onFulfilled ).catch_( this::onRejected );
134  }
135
136  /**
137   * Return the promise state.
138   *
139   * @return the promise state.
140   */
141  @Observable
142  @Nonnull
143  public State getState()
144  {
145    return _state;
146  }
147
148  void setState( @Nonnull final State state )
149  {
150    _state = Objects.requireNonNull( state );
151  }
152
153  /**
154   * Return the value that the promise was resolved to.
155   * This should NOT be called if the state is not {@link State#FULFILLED} and will result in an invariant
156   * failure if invariants are enabled.
157   *
158   * @return the value that the promise was resolved to.
159   */
160  @Observable
161  public T getValue()
162  {
163    if ( Arez.shouldCheckApiInvariants() )
164    {
165      apiInvariant( () -> _state == State.FULFILLED,
166                    () -> "Arez-0165: ObservablePromise.getValue() called when the promise is not in " +
167                          "fulfilled state. State: " + _state + ", Promise: " + _promise );
168    }
169    return _value;
170  }
171
172  void setValue( final T value )
173  {
174    if ( Arez.shouldCheckInvariants() )
175    {
176      invariant( () -> _state == State.FULFILLED,
177                 () -> "Arez-0166: ObservablePromise.setValue() called when promise is in incorrect state. " +
178                       "State: " + _state + ", Promise: " + _promise );
179    }
180    _value = value;
181  }
182
183  /**
184   * Return the error that the promise was rejected with.
185   * This should NOT be called if the state is not {@link State#REJECTED} and will result in an invariant
186   * failure if invariants are enabled.
187   *
188   * @return the error that the promise was rejected with.
189   */
190  @Observable
191  public E getError()
192  {
193    if ( Arez.shouldCheckApiInvariants() )
194    {
195      apiInvariant( () -> _state == State.REJECTED,
196                    () -> "ObservablePromise.getError() called when the promise is not in " +
197                          "rejected state. State: " + _state + ", Promise: " + _promise );
198    }
199    return _error;
200  }
201
202  void setError( final E error )
203  {
204    if ( Arez.shouldCheckInvariants() )
205    {
206      invariant( () -> _state == State.REJECTED,
207                 () -> "ObservablePromise.setError() called when promise is in incorrect state. " +
208                       "State: " + _state + ", Promise: " + _promise );
209    }
210    _error = error;
211  }
212
213  @Action
214  @Nonnull
215  Promise<T> onFulfilled( final T value )
216  {
217    setState( State.FULFILLED );
218    setValue( value );
219    return Promise.resolve( value );
220  }
221
222  @Action
223  Promise<Object> onRejected( @Nonnull final Object error )
224  {
225    setState( State.REJECTED );
226    setError( Js.uncheckedCast( error ) );
227    return Promise.reject( error );
228  }
229}