graphql.execution.Async Maven / Gradle / Ivy
package graphql.execution;
import graphql.Assert;
import graphql.Internal;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import static graphql.Assert.assertTrue;
@Internal
@SuppressWarnings("FutureReturnValueIgnored")
public class Async {
/**
* A builder of materialized objects or {@link CompletableFuture}s than can present a promise to the list of them
*
* This builder has a strict contract on size whereby if the expectedSize is five, then there MUST be five elements presented to it.
*
* @param for two
*/
public interface CombinedBuilder {
/**
* This adds a {@link CompletableFuture} into the collection of results
*
* @param completableFuture the CF to add
*/
void add(CompletableFuture completableFuture);
/**
* This adds a new value which can be either a materialized value or a {@link CompletableFuture}
*
* @param object the object to add
*/
void addObject(Object object);
/**
* This will return a {@code CompletableFuture>} even if the inputs are all materialized values
*
* @return a CompletableFuture to a List of values
*/
CompletableFuture> await();
/**
* This will return a {@code CompletableFuture>} if ANY of the input values are async
* otherwise it just return a materialised {@code List}
*
* @return either a CompletableFuture or a materialized list
*/
/* CompletableFuture> | List */ Object awaitPolymorphic();
}
/**
* Combines zero or more CFs into one. It is a wrapper around CompletableFuture.allOf
.
*
* @param expectedSize how many we expect
* @param for two
*
* @return a combined builder of CFs
*/
public static CombinedBuilder ofExpectedSize(int expectedSize) {
if (expectedSize == 0) {
return new Empty<>();
} else if (expectedSize == 1) {
return new Single<>();
} else {
return new Many<>(expectedSize);
}
}
private static class Empty implements CombinedBuilder {
private int ix;
@Override
public void add(CompletableFuture completableFuture) {
this.ix++;
}
@Override
public void addObject(Object object) {
this.ix++;
}
@Override
public CompletableFuture> await() {
assertTrue(ix == 0, "expected size was 0 got %d", ix);
return typedEmpty();
}
@Override
public Object awaitPolymorphic() {
Assert.assertTrue(ix == 0, () -> "expected size was " + 0 + " got " + ix);
return Collections.emptyList();
}
// implementation details: infer the type of Completable> from a singleton empty
private static final CompletableFuture> EMPTY = CompletableFuture.completedFuture(Collections.emptyList());
@SuppressWarnings("unchecked")
private static CompletableFuture typedEmpty() {
return (CompletableFuture) EMPTY;
}
}
private static class Single implements CombinedBuilder {
// avoiding array allocation as there is only 1 CF
private Object value;
private int ix;
@Override
public void add(CompletableFuture completableFuture) {
this.value = completableFuture;
this.ix++;
}
@Override
public void addObject(Object object) {
this.value = object;
this.ix++;
}
@Override
public CompletableFuture> await() {
commonSizeAssert();
if (value instanceof CompletableFuture) {
@SuppressWarnings("unchecked")
CompletableFuture cf = (CompletableFuture) value;
return cf.thenApply(Collections::singletonList);
}
//noinspection unchecked
return CompletableFuture.completedFuture(Collections.singletonList((T) value));
}
@Override
public Object awaitPolymorphic() {
commonSizeAssert();
if (value instanceof CompletableFuture) {
@SuppressWarnings("unchecked")
CompletableFuture cf = (CompletableFuture) value;
return cf.thenApply(Collections::singletonList);
}
//noinspection unchecked
return Collections.singletonList((T) value);
}
private void commonSizeAssert() {
Assert.assertTrue(ix == 1, () -> "expected size was " + 1 + " got " + ix);
}
}
private static class Many implements CombinedBuilder {
private final Object[] array;
private int ix;
private int cfCount;
private Many(int size) {
this.array = new Object[size];
this.ix = 0;
cfCount = 0;
}
@Override
public void add(CompletableFuture completableFuture) {
array[ix++] = completableFuture;
cfCount++;
}
@Override
public void addObject(Object object) {
array[ix++] = object;
if (object instanceof CompletableFuture) {
cfCount++;
}
}
@SuppressWarnings("unchecked")
@Override
public CompletableFuture> await() {
commonSizeAssert();
CompletableFuture> overallResult = new CompletableFuture<>();
if (cfCount == 0) {
overallResult.complete(materialisedList(array));
} else {
CompletableFuture[] cfsArr = copyOnlyCFsToArray();
CompletableFuture.allOf(cfsArr)
.whenComplete((ignored, exception) -> {
if (exception != null) {
overallResult.completeExceptionally(exception);
return;
}
List results = new ArrayList<>(array.length);
if (cfsArr.length == array.length) {
// they are all CFs
for (CompletableFuture cf : cfsArr) {
results.add(cf.join());
}
} else {
// it's a mixed bag of CFs and materialized objects
for (Object object : array) {
if (object instanceof CompletableFuture) {
CompletableFuture cf = (CompletableFuture) object;
// join is safe since they are all completed earlier via CompletableFuture.allOf()
results.add(cf.join());
} else {
results.add((T) object);
}
}
}
overallResult.complete(results);
});
}
return overallResult;
}
@SuppressWarnings("unchecked")
@NotNull
private CompletableFuture[] copyOnlyCFsToArray() {
if (cfCount == array.length) {
// if it's all CFs - make a type safe copy via C code
return Arrays.copyOf(array, array.length, CompletableFuture[].class);
} else {
int i = 0;
CompletableFuture[] dest = new CompletableFuture[cfCount];
for (Object o : array) {
if (o instanceof CompletableFuture) {
dest[i] = (CompletableFuture) o;
i++;
}
}
return dest;
}
}
@Override
public Object awaitPolymorphic() {
if (cfCount == 0) {
commonSizeAssert();
return materialisedList(array);
} else {
return await();
}
}
@NotNull
private List materialisedList(Object[] array) {
List results = new ArrayList<>(array.length);
for (Object object : array) {
//noinspection unchecked
results.add((T) object);
}
return results;
}
private void commonSizeAssert() {
Assert.assertTrue(ix == array.length, () -> "expected size was " + array.length + " got " + ix);
}
}
@SuppressWarnings("unchecked")
public static CompletableFuture> each(Collection list, Function cfOrMaterialisedValueFactory) {
Object l = eachPolymorphic(list, cfOrMaterialisedValueFactory);
if (l instanceof CompletableFuture) {
return (CompletableFuture>) l;
} else {
return CompletableFuture.completedFuture((List) l);
}
}
/**
* This will run the value factory for each of the values in the provided list.
*
* If any of the values provided is a {@link CompletableFuture} it will return a {@link CompletableFuture} result object
* that joins on all values otherwise if none of the values are a {@link CompletableFuture} then it will return a materialized list.
*
* @param list the list to work over
* @param cfOrMaterialisedValueFactory the value factory to call for each iterm in the list
* @param for two
*
* @return a {@link CompletableFuture} to the list of resolved values or the list of values in a materialized fashion
*/
public static /* CompletableFuture> | List */ Object eachPolymorphic(Collection list, Function cfOrMaterialisedValueFactory) {
CombinedBuilder