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}