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}