Skip to content

Commit 8953628

Browse files
committed
Experimental support for @defer
1 parent 44619c1 commit 8953628

9 files changed

Lines changed: 310 additions & 6 deletions

File tree

‎src/main/java/graphql/Directives.java‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package graphql;
22

33

4+
import graphql.introspection.Introspection;
45
import graphql.schema.GraphQLDirective;
56
import graphql.schema.GraphQLNonNull;
67

@@ -47,5 +48,11 @@ public class Directives {
4748
.validLocations(FIELD_DEFINITION)
4849
.build();
4950

51+
@ExperimentalApi
52+
public static final GraphQLDirective DeferDirective = GraphQLDirective.newDirective()
53+
.name("defer")
54+
.description("This experimental directive allows results to be deferred during execution")
55+
.validLocations(Introspection.DirectiveLocation.FIELD)
56+
.build();
5057

5158
}

‎src/main/java/graphql/ExecutionResult.java‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package graphql;
22

33

4+
import graphql.execution.defer.DeferredResults;
5+
46
import java.util.List;
57
import java.util.Map;
68

@@ -27,7 +29,6 @@ public interface ExecutionResult {
2729
*/
2830
Map<Object, Object> getExtensions();
2931

30-
3132
/**
3233
* The graphql specification says that result of a call should be a map that follows certain rules on what items
3334
* should be present. Certain JSON serializers may or may interpret {@link ExecutionResult} to spec, so this method
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package graphql;
2+
3+
import java.lang.annotation.Documented;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
import static java.lang.annotation.ElementType.CONSTRUCTOR;
9+
import static java.lang.annotation.ElementType.FIELD;
10+
import static java.lang.annotation.ElementType.METHOD;
11+
import static java.lang.annotation.ElementType.TYPE;
12+
13+
/**
14+
* This represents code that the graphql-java project considers experimental API and has less of an imperative to be stable within
15+
* releases.
16+
*
17+
* In general unnecessary changes will be avoided and this code is aiming to be public one day but you should be aware that things
18+
* might change.
19+
*/
20+
@Retention(RetentionPolicy.RUNTIME)
21+
@Target(value = {CONSTRUCTOR, METHOD, TYPE, FIELD})
22+
@Documented
23+
public @interface ExperimentalApi {
24+
}

‎src/main/java/graphql/execution/AbstractAsyncExecutionStrategy.java‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,16 @@ protected BiConsumer<List<ExecutionResult>, Throwable> handleResults(ExecutionCo
3232
String fieldName = fieldNames.get(ix++);
3333
resolvedValuesByField.put(fieldName, executionResult.getData());
3434
}
35-
overallResult.complete(new ExecutionResultImpl(resolvedValuesByField, executionContext.getErrors()));
35+
36+
Map<Object, Object> extensions = null;
37+
DeferSupport deferSupport = executionContext.getDeferSupport();
38+
if (deferSupport.isDeferDetected()) {
39+
extensions = new LinkedHashMap<>();
40+
extensions.put("deferredResults", deferSupport.getPublisher());
41+
}
42+
overallResult.complete(new ExecutionResultImpl(resolvedValuesByField, executionContext.getErrors(), extensions));
3643
};
3744
}
45+
46+
3847
}

‎src/main/java/graphql/execution/AsyncExecutionStrategy.java‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ public CompletableFuture<ExecutionResult> execute(ExecutionContext executionCont
5050
ExecutionStrategyParameters newParameters = parameters
5151
.transform(builder -> builder.field(currentField).path(fieldPath));
5252

53+
DeferSupport deferSupport = executionContext.getDeferSupport();
54+
if (deferSupport.checkForDeferDirective(currentField)) {
55+
deferSupport.enqueue(() -> resolveField(executionContext, newParameters));
56+
continue;
57+
}
5358
CompletableFuture<ExecutionResult> future = resolveField(executionContext, newParameters);
5459
futures.add(future);
5560
}
@@ -63,4 +68,5 @@ public CompletableFuture<ExecutionResult> execute(ExecutionContext executionCont
6368
return overallResult;
6469
}
6570

71+
6672
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package graphql.execution;
2+
3+
import graphql.Directives;
4+
import graphql.ExecutionResult;
5+
import graphql.Internal;
6+
import graphql.language.Field;
7+
import org.reactivestreams.Publisher;
8+
import org.reactivestreams.Subscriber;
9+
import org.reactivestreams.Subscription;
10+
11+
import java.util.Deque;
12+
import java.util.List;
13+
import java.util.concurrent.CompletableFuture;
14+
import java.util.concurrent.ConcurrentLinkedDeque;
15+
import java.util.concurrent.atomic.AtomicBoolean;
16+
import java.util.function.Supplier;
17+
18+
/**
19+
* This provides support for @defer directives on fields that mean that results will be sent AFTER
20+
* the main result is sent via a Publisher stream.
21+
*/
22+
@Internal
23+
public class DeferSupport implements Publisher<ExecutionResult> {
24+
25+
private final Deque<Supplier<CompletableFuture<ExecutionResult>>> deferredCalls = new ConcurrentLinkedDeque<>();
26+
private final AtomicBoolean deferDetected = new AtomicBoolean(false);
27+
28+
public boolean checkForDeferDirective(List<Field> currentField) {
29+
for (Field field : currentField) {
30+
if (field.getDirective(Directives.DeferDirective.getName()) != null) {
31+
return true;
32+
}
33+
}
34+
return false;
35+
}
36+
37+
@Override
38+
public void subscribe(Subscriber<? super ExecutionResult> subscriber) {
39+
subscriber.onSubscribe(emptySubscription());
40+
boolean ok = drainDeferredCalls(subscriber);
41+
if (ok) {
42+
subscriber.onComplete();
43+
}
44+
}
45+
46+
private boolean drainDeferredCalls(Subscriber<? super ExecutionResult> subscriber) {
47+
while (!deferredCalls.isEmpty()) {
48+
Supplier<CompletableFuture<ExecutionResult>> deferredExecution = deferredCalls.pop();
49+
try {
50+
CompletableFuture<ExecutionResult> future = deferredExecution.get();
51+
//
52+
// this becomes a blocking call on the deferred resolveField value. This and the use of the queue
53+
// means we will resolve them in "encountered" order. We could do it async and hence in any order
54+
//
55+
ExecutionResult executionResult = future.get();
56+
subscriber.onNext(executionResult);
57+
} catch (Exception e) {
58+
subscriber.onError(e);
59+
return false;
60+
}
61+
}
62+
return true;
63+
}
64+
65+
66+
public void enqueue(Supplier<CompletableFuture<ExecutionResult>> deferredCall) {
67+
deferDetected.set(true);
68+
deferredCalls.offer(deferredCall);
69+
}
70+
71+
public boolean isDeferDetected() {
72+
return deferDetected.get();
73+
}
74+
75+
public Publisher<ExecutionResult> getPublisher() {
76+
return this;
77+
}
78+
79+
private Subscription emptySubscription() {
80+
return new Subscription() {
81+
@Override
82+
public void request(long n) {
83+
}
84+
85+
@Override
86+
public void cancel() {
87+
}
88+
};
89+
}
90+
}

‎src/main/java/graphql/execution/ExecutionContext.java‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import graphql.language.OperationDefinition;
1111
import graphql.schema.GraphQLSchema;
1212

13-
import java.util.ArrayList;
1413
import java.util.Collections;
1514
import java.util.List;
1615
import java.util.Map;
@@ -34,6 +33,7 @@ public class ExecutionContext {
3433
private final Object context;
3534
private final Instrumentation instrumentation;
3635
private final List<GraphQLError> errors = new CopyOnWriteArrayList<>();
36+
private final DeferSupport deferSupport = new DeferSupport();
3737

3838
public ExecutionContext(Instrumentation instrumentation, ExecutionId executionId, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState, ExecutionStrategy queryStrategy, ExecutionStrategy mutationStrategy, ExecutionStrategy subscriptionStrategy, Map<String, FragmentDefinition> fragmentsByName, Document document, OperationDefinition operationDefinition, Map<String, Object> variables, Object context, Object root) {
3939
this(instrumentation, executionId, graphQLSchema, instrumentationState, queryStrategy, mutationStrategy, subscriptionStrategy, fragmentsByName, document, operationDefinition, variables, context, root, Collections.emptyList());
@@ -157,6 +157,9 @@ public ExecutionStrategy getSubscriptionStrategy() {
157157
return subscriptionStrategy;
158158
}
159159

160+
public DeferSupport getDeferSupport() {
161+
return deferSupport;
162+
}
160163

161164
/**
162165
* This helps you transform the current ExecutionContext object into another one by starting a builder with all

‎src/main/java/graphql/schema/GraphQLSchema.java‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.util.Arrays;
1313
import java.util.Collection;
1414
import java.util.Collections;
15+
import java.util.HashSet;
1516
import java.util.LinkedHashSet;
1617
import java.util.List;
1718
import java.util.Map;
@@ -202,8 +203,8 @@ public static class Builder {
202203
private GraphQLObjectType mutationType;
203204
private GraphQLObjectType subscriptionType;
204205
private GraphqlFieldVisibility fieldVisibility = DEFAULT_FIELD_VISIBILITY;
205-
private Set<GraphQLType> additionalTypes = Collections.emptySet();
206-
private Set<GraphQLDirective> additionalDirectives = Collections.emptySet();
206+
private Set<GraphQLType> additionalTypes = new HashSet<>();
207+
private Set<GraphQLDirective> additionalDirectives = new HashSet<>();
207208

208209
public Builder query(GraphQLObjectType.Builder builder) {
209210
return query(builder.build());
@@ -243,7 +244,12 @@ public Builder additionalTypes(Set<GraphQLType> additionalTypes) {
243244
}
244245

245246
public Builder additionalDirectives(Set<GraphQLDirective> additionalDirectives) {
246-
this.additionalDirectives = additionalDirectives;
247+
this.additionalDirectives.addAll(additionalDirectives);
248+
return this;
249+
}
250+
251+
public Builder additionalDirective(GraphQLDirective additionalDirective) {
252+
this.additionalDirectives.add(additionalDirective);
247253
return this;
248254
}
249255

0 commit comments

Comments
 (0)