Hi all,

So long long ago, I wrote a Promise class for our GWT project to make 
easier to write complex async code that ran in the browser but could also 
be run in Junit tests running in the plain JRE, and sometimes on the server 
in cases where we could assume the Promise would complete synchronously. 

Fast forward 10+ years, and the Promise has long been standardized and 
baked into browsers themselves, WITH great support in DevTools for 
following the path of a Promise across resolutions.

I'm refactoring this class to use the browser's native Promise 
implementation. Normally the way I would do this is:
1. Align the Java implementation class to the Browser's Promise API
2. Add a supersource implementation that uses JSNI to invoke the browser's 
API when compiling.

BUT - is this still the "best" way to do this in 2025, with @JsTypes?
AND - there is already a Promise class in the elemental library that we use 
extensively, but it's a "native" class, so we can't use it in the JRE. Can 
I provide a JRE-safe implementation of elemental2.promise.Promise without 
monkey-patching elemental2-promise ?

Has anyone else implemented something similar?

Best,
Alex
 

-- 
You received this message because you are subscribed to the Google Groups "GWT 
Users" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/google-web-toolkit/710fa0f3-d544-4a59-b065-1a6dfeb25d20n%40googlegroups.com.
/*
 * ActivityInfo 4.0
 * Copyright © 2014-2022 BeDataDriven Groep B.V. - All Rights Reserved
 *
 * MIT License  
 */
package org.activityinfo.promise;

import com.google.common.base.Functions;
import com.google.common.collect.Lists;
import com.google.gwt.user.client.rpc.AsyncCallback;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;


/**
 * The Promise interface represents a proxy for a value not necessarily known at its creation time.
 * It allows you to associate handlers to an asynchronous action's eventual success or failure.
 * This let asynchronous methods to return values like synchronous methods: instead of the final value,
 * the asynchronous method returns a promise of having a value at some point in the future.
 *
 * @param <T> the type of the promised value
 */
public final class Promise<T> implements AsyncCallback<T> {

    private static final Logger LOGGER = Logger.getLogger(Promise.class.getName());

    public enum State {

        /**
         * The action relating to the promise succeeded
         */
        FULFILLED,

        /**
         * The action relating to the promise failed
         */
        REJECTED,

        /**
         * Hasn't fulfilled or rejected yet
         */
        PENDING
    }

    private boolean fulfilled;
    private @Nullable T value;
    private @MonotonicNonNull Throwable exception;

    private @MonotonicNonNull List<AsyncCallback<? super T>> callbacks = null;

    public Promise() {
    }

    public State getState() {
        if(exception != null) {
            return State.REJECTED;
        } else if(fulfilled) {
            return State.FULFILLED;
        } else {
            return State.PENDING;
        }
    }

    public boolean isSettled() {
        return fulfilled || exception != null;
    }

    public final void resolve(T value) {
        if (isSettled()) {
            return;
        }
        this.value = value;
        this.fulfilled = true;

        publishFulfillment();
    }

    @SuppressWarnings("nullness:argument") // Complex, if fulfilled = true, then value is @NonNull, but only IF T is also @NonNull
    public void then(AsyncCallback<? super T> callback) {
        if(exception != null) {
            callback.onFailure(exception);
        } else if(fulfilled) {
            callback.onSuccess(value);
        } else {
            // Pending...
            if (callbacks == null) {
                callbacks = Lists.newArrayList();
            }
            callbacks.add(callback);
        }
    }

    public Promise<T> catch_(Function<Throwable, T> function) {
        return this.<T>then(result -> result, function);
    }

    public <R extends @Nullable Object> Promise<R> join(final Function<? super T, Promise<R>> onFulfilled) {
        return join(onFulfilled, null);
    }

    public <R extends @Nullable Object> Promise<R> join(final Function<@Nullable ? super T, Promise<R>> onFulfilled, final @Nullable Function<Throwable, Promise<R>> onRejected) {
        final Promise<R> chained = new Promise<>();
        then(new AsyncCallback<T>() {
            @Override
            public void onFailure(Throwable caught) {
                if(onRejected == null) {
                    chained.onFailure(caught);
                } else {
                    try {
                        onRejected.apply(caught).then(chained);
                    } catch (Throwable recaught) {
                        chained.onFailure(recaught);
                    }
                }
            }

            @Override
            public void onSuccess(T t) {
                try {
                    Promise<R> result = onFulfilled.apply(t);
                    assert result != null : "function " + onFulfilled + " returned null!!";
                    result.then(chained);
                } catch(Throwable caught) {
                    chained.onFailure(caught);
                }
            }
        });
        return chained;
    }


    public Promise<Void> thenDiscardResult() {
        return then(Functions.constant(null));
    }

    public <R> Promise<R> join(Supplier<Promise<R>> supplier) {
        return join(x -> supplier.get());
    }


    public Promise<T> thenDo(final Consumer<T> consumer) {
        return then(result -> {
            consumer.accept(result);
            return result;
        });
    }
    public <R> Promise<R> then(final Function<? super T, R> onFulfilled) {
        return then(onFulfilled, null);
    }

    public <R> Promise<R> then(final Function<? super T, R> onFulfilled, final @Nullable Function<Throwable, R> onRejected) {
        assert onFulfilled != null : "function is null";

        final Promise<R> chained = new Promise<>();
        then(new AsyncCallback<T>() {

            @Override
            public void onFailure(Throwable caught) {
                if(onRejected != null) {
                    try {
                        chained.resolve(onRejected.apply(caught));
                    } catch (Throwable recaught) {
                        chained.reject(recaught);
                    }
                } else {
                    chained.reject(caught);
                }
            }

            @Override
            public void onSuccess(T t) {
                try {
                    chained.resolve(onFulfilled.apply(t));
                } catch (Throwable caught) {
                    chained.reject(caught);
                }
            }
        });
        return chained;
    }


    public <R> Promise<R> then(final Supplier<R> function) {
        assert function != null : "function is null";
        return then(x -> function.get());
    }

    @SuppressWarnings("nullness:return")
    private T getOrThrowIfNotYetLoaded() {
        if(!fulfilled) {
            throw new IllegalStateException();
        }
        return value;
    }

    public Throwable getException() {
        if(exception == null) {
            throw new IllegalStateException();
        }
        return exception;
    }

    @Override
    public void onFailure(Throwable caught) {
        reject(caught);
    }

    @Override
    public void onSuccess(T result) {
        resolve(result);
    }

    public final void reject(Throwable caught) {

        LOGGER.log(Level.WARNING, "Promise rejected", caught);

        if (isSettled()) {
            return;
        }
        this.exception = caught;

        publishRejection();
    }

    @RequiresNonNull("exception")
    private void publishRejection() {
        if (callbacks != null) {
            for (AsyncCallback<? super T> callback : callbacks) {
                callback.onFailure(exception);
            }
        }
    }

    @SuppressWarnings("nullness:argument")
    private void publishFulfillment() {
        if (callbacks != null) {
            for (AsyncCallback<? super T> callback : callbacks) {
                callback.onSuccess(value);
            }
        }
    }

    public static <T> Promise<T> resolved(T value) {
        Promise<T> promise = new Promise<>();
        promise.resolve(value);
        return promise;
    }

    public static Promise<Void> done() {
        return Promise.resolved(null);
    }

    public static <X> Promise<X> rejected(Throwable exception) {
        Promise<X> promise = new Promise<>();
        promise.reject(exception);
        return promise;
    }

    /**
     * Applies an asynchronous function to each of the elements in {@code items},
     */
    public static <T> Promise<Void> forEach(Iterable<T> items, final Function<? super T, Promise<Void>> function) {
        Promise<Void> promise = Promise.resolved(null);
        for(final T item : items) {
            promise = promise.join(new Function<Void, Promise<Void>>() {
                @Override
                public Promise<Void> apply(@Nullable Void input) {
                    return function.apply(item);
                }
            });
        }
        return promise;
    }

    public static Promise<Void> waitAll(Promise<?>... promises) {
        return waitAll(Arrays.asList(promises));
    }

    public static Promise<Void> waitAll(final List<? extends Promise<?>> promises) {

        if(promises.isEmpty()) {
            return Promise.done();
        }

        final Promise<Void> result = new Promise<>();
        final int[] remaining = new int[] { promises.size() };
        AsyncCallback<Void> callback = new AsyncCallback<Void>() {
            @Override
            public void onFailure(Throwable caught) {
                result.onFailure(caught);
            }

            @Override
            public void onSuccess(Void o) {
                remaining[0]--;
                if(remaining[0] == 0) {
                    result.onSuccess(null);
                }
            }
        };
        for(int i=0;i!=promises.size();++i) {
            promises.get(i).thenDiscardResult().then(callback);
        }
        return result;
    }

    public static <T> Promise<List<T>> flatten(final List<Promise<T>> promises) {
        return waitAll(promises).then(new Function<Void, List<T>>() {
            @Override
            public List<T> apply(@Nullable Void aVoid) {
                List<T> items = new ArrayList<>();
                for (Promise<T> promise : promises) {
                    items.add(promise.getOrThrowIfNotYetLoaded());
                }
                return items;
            }
        });
    }

    @Override
    public String toString() {
        if(fulfilled) {
            return "<fulfilled: " + value + ">";
        } else if(exception != null) {
            return "<rejected: " + exception.getClass().getSimpleName() + ">";
        } else {
            return "<pending>";
        }
    }
}

Reply via email to