// Copyright 2016 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.base; import android.os.Handler; import androidx.annotation.IntDef; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.LinkedList; import java.util.List; import java.util.function.Function; /** * A Promise class to be used as a placeholder for a result that will be provided asynchronously. * It must only be accessed from a single thread. * @param The type the Promise will be fulfilled with. */ public class Promise { // TODO(peconn): Implement rejection handlers that can recover from rejection. @IntDef({PromiseState.UNFULFILLED, PromiseState.FULFILLED, PromiseState.REJECTED}) @Retention(RetentionPolicy.SOURCE) private @interface PromiseState { int UNFULFILLED = 0; int FULFILLED = 1; int REJECTED = 2; } @PromiseState private int mState = PromiseState.UNFULFILLED; private T mResult; private final List> mFulfillCallbacks = new LinkedList<>(); private Exception mRejectReason; private final List> mRejectCallbacks = new LinkedList<>(); private final Thread mThread = Thread.currentThread(); private final Handler mHandler = new Handler(); private boolean mThrowingRejectionHandler; /** * A function class for use when chaining Promises with {@link Promise#then(AsyncFunction)}. * @param The type of the function input. * @param The type of the function output. */ public interface AsyncFunction extends Function> {} /** * An exception class for when a rejected Promise is not handled and cannot pass the rejection * to a subsequent Promise. */ public static class UnhandledRejectionException extends RuntimeException { public UnhandledRejectionException(String message, Throwable cause) { super(message, cause); } } /** * Convenience method that calls {@link #then(Callback, Callback)} providing a rejection * {@link Callback} that throws a {@link UnhandledRejectionException}. Only use this on * Promises that do not have rejection handlers or dependant Promises. */ public void then(Callback onFulfill) { checkThread(); // Allow multiple single argument then(Callback)'s, but don't bother adding duplicate // throwing rejection handlers. if (mThrowingRejectionHandler) { thenInner(onFulfill); return; } assert mRejectCallbacks.size() == 0 : "Do not call the single argument Promise.then(Callback) on a Promise that already" + " has a rejection handler."; Callback onReject = reason -> { throw new UnhandledRejectionException( "Promise was rejected without a rejection handler.", reason); }; then(onFulfill, onReject); mThrowingRejectionHandler = true; } /** * Queues {@link Callback}s to be run when the Promise is either fulfilled or rejected. If the * Promise is already fulfilled or rejected, the appropriate callback will be run on the next * iteration of the message loop. * * @param onFulfill The Callback to be called on fulfillment. * @param onReject The Callback to be called on rejection. The argument to onReject will * may be null if the Promise was rejected manually. */ public void then(Callback onFulfill, Callback onReject) { checkThread(); thenInner(onFulfill); exceptInner(onReject); } /** * Adds a rejection handler to the Promise. This handler will be called if this Promise or any * Promises this Promise depends on is rejected or fails. The {@link Callback} will be given * the exception that caused the rejection, or null if the rejection was manual (caused by a * call to {@link #reject()}. */ public void except(Callback onReject) { checkThread(); exceptInner(onReject); } private void thenInner(Callback onFulfill) { if (mState == PromiseState.FULFILLED) { postCallbackToLooper(onFulfill, mResult); } else if (mState == PromiseState.UNFULFILLED) { mFulfillCallbacks.add(onFulfill); } } private void exceptInner(Callback onReject) { assert !mThrowingRejectionHandler : "Do not add an exception handler to a Promise you have " + "called the single argument Promise.then(Callback) on."; if (mState == PromiseState.REJECTED) { postCallbackToLooper(onReject, mRejectReason); } else if (mState == PromiseState.UNFULFILLED) { mRejectCallbacks.add(onReject); } } /** * Queues a {@link Function} to be run when the Promise is fulfilled. When this Promise is * fulfilled, the function will be run and its result will be place in the returned Promise. */ public Promise then(final Function function) { checkThread(); // Create a new Promise to store the result of the function. final Promise promise = new Promise<>(); // Once this Promise is fulfilled: // - Apply the given function to the result. // - Fulfill the new Promise. thenInner( result -> { try { promise.fulfill(function.apply(result)); } catch (Exception e) { // If function application fails, reject the next Promise. promise.reject(e); } }); // If this Promise is rejected, reject the next Promise. exceptInner(promise::reject); return promise; } /** * Queues a {@link Promise.AsyncFunction} to be run when the Promise is fulfilled. When this * Promise is fulfilled, the AsyncFunction will be run. When the result of the AsyncFunction is * available, it will be placed in the returned Promise. */ public Promise then(final AsyncFunction function) { checkThread(); // Create a new Promise to be returned. final Promise promise = new Promise<>(); // Once this Promise is fulfilled: // - Apply the given function to the result (giving us an inner Promise). // - On fulfillment of this inner Promise, fulfill our return Promise. thenInner( result -> { try { // When the inner Promise is fulfilled, fulfill the return Promise. // Alternatively, if the inner Promise is rejected, reject the return // Promise. function.apply(result).then(promise::fulfill, promise::reject); } catch (Exception e) { // If creating the inner Promise failed, reject the next Promise. promise.reject(e); } }); // If this Promise is rejected, reject the next Promise. exceptInner(promise::reject); return promise; } /** * Fulfills the Promise with the result and passes it to any {@link Callback}s previously queued * on the next iteration of the message loop. */ public void fulfill(final T result) { checkThread(); assert mState == PromiseState.UNFULFILLED; mState = PromiseState.FULFILLED; mResult = result; for (final Callback callback : mFulfillCallbacks) { postCallbackToLooper(callback, result); } mFulfillCallbacks.clear(); } /** * Rejects the Promise, rejecting all those Promises that rely on it. * * This may throw an exception if a dependent Promise fails to handle the rejection, so it is * important to make it explicit when a Promise may be rejected, so that users of that Promise * know to provide rejection handling. */ public void reject(final Exception reason) { checkThread(); assert mState == PromiseState.UNFULFILLED; mState = PromiseState.REJECTED; mRejectReason = reason; for (final Callback callback : mRejectCallbacks) { postCallbackToLooper(callback, reason); } mRejectCallbacks.clear(); } /** Rejects a Promise, see {@link #reject(Exception)}. */ public void reject() { reject(null); } /** Returns whether the promise is fulfilled. */ public boolean isFulfilled() { checkThread(); return mState == PromiseState.FULFILLED; } /** Returns whether the promise is rejected. */ public boolean isRejected() { checkThread(); return mState == PromiseState.REJECTED; } /** * Must be called after the promise has been fulfilled. * * @return The promised result. */ public T getResult() { assert isFulfilled(); return mResult; } /** Convenience method to return a Promise fulfilled with the given result. */ public static Promise fulfilled(T result) { Promise promise = new Promise<>(); promise.fulfill(result); return promise; } /** Convenience method to return a rejected Promise. */ public static Promise rejected() { Promise promise = new Promise<>(); promise.reject(); return promise; } private void checkThread() { assert mThread == Thread.currentThread() : "Promise must only be used on a single Thread."; } // We use a different template parameter here so this can be used for both T and Throwables. private void postCallbackToLooper(final Callback callback, final S result) { // Post the callbacks to the Thread looper so we don't get a long chain of callbacks // holding up the thread. mHandler.post(callback.bind(result)); } }