Skip to content

Commit fa60bb9

Browse files
committed
refactor(DeferredExecution) use Scope/Thread/Frame approach
1 parent 88f64ec commit fa60bb9

File tree

1 file changed

+110
-84
lines changed

1 file changed

+110
-84
lines changed

lib/graphql/execution/deferred_execution.rb

Lines changed: 110 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,30 @@ module Execution
33
# A query execution strategy that emits
44
# `{ path: [...], value: ... }` patches as it
55
# resolves the query.
6+
#
7+
# TODO: how to handle if a selection set defers one member,
8+
# but a later member gets InvalidNullError?
69
class DeferredExecution
710
include GraphQL::Language
811

912
def execute(ast_operation, root_type, query_object)
1013
collector = query_object.context[:collector]
11-
exec_context = Execution::Context.new(query_object, self)
12-
initial_defers = []
13-
initial_frame = Frame.new(
14+
15+
scope = ExecScope.new(query_object)
16+
initial_thread = ExecThread.new
17+
initial_frame = ExecFrame.new(
1418
node: ast_operation,
1519
value: query_object.root_value,
16-
type_defn: root_type,
17-
exec_context: exec_context,
18-
path: [],
20+
type: root_type,
21+
path: []
1922
)
20-
initial_result = resolve_or_defer_frame(initial_frame, initial_defers)
23+
24+
initial_result = resolve_or_defer_frame(scope, initial_thread, initial_frame)
2125

2226
if collector
2327
initial_patch = {"data" => initial_result}
2428

25-
initial_errors = initial_frame.errors + query_object.context.errors
29+
initial_errors = initial_thread.errors + query_object.context.errors
2630
error_idx = initial_errors.length
2731

2832
if initial_errors.any?
@@ -31,122 +35,138 @@ def execute(ast_operation, root_type, query_object)
3135

3236
collector.patch([], initial_patch)
3337

34-
defers = initial_defers + initial_frame.defers
38+
defers = initial_thread.defers
3539
while defers.any?
3640
next_defers = []
3741
defers.each do |deferred_frame|
38-
deferred_result = resolve_frame(deferred_frame)
42+
deferred_thread = ExecThread.new
43+
deferred_result = resolve_frame(scope, deferred_thread, deferred_frame)
3944
# No use patching for nil, that's there already
4045
if !deferred_result.nil?
4146
collector.patch(["data"] + deferred_frame.path, deferred_result)
4247
end
43-
deferred_frame.errors.each do |deferred_error|
48+
deferred_thread.errors.each do |deferred_error|
4449
collector.patch(["errors", error_idx], deferred_error.to_h)
4550
error_idx += 1
4651
end
47-
next_defers.push(*deferred_frame.defers)
52+
next_defers.push(*deferred_thread.defers)
4853
end
4954
defers = next_defers
5055
end
5156
else
52-
query_object.context.errors.push(*initial_frame.errors)
57+
query_object.context.errors.push(*initial_thread.errors)
5358
end
5459

5560
initial_result
5661
end
5762

58-
class Frame
59-
attr_reader :node, :value, :type_defn, :exec_context, :path, :defers, :errors
60-
def initialize(node:, value:, type_defn:, exec_context:, path:)
61-
@node = node
62-
@value = value
63-
@type_defn = type_defn
64-
@exec_context = exec_context
65-
@path = path
66-
@defers = []
63+
# Global, window-like object for a query
64+
class ExecScope
65+
attr_reader :query, :schema
66+
67+
def initialize(query)
68+
@query = query
69+
@schema = query.schema
70+
end
71+
72+
def get_type(type)
73+
@schema.types[type]
74+
end
75+
76+
def get_fragment(name)
77+
@query.fragments[name]
78+
end
79+
80+
def get_field(type, name)
81+
@schema.get_field(type, name) || raise("No field named '#{name}' found for #{type}")
82+
end
83+
end
84+
85+
# One serial stream of execution
86+
class ExecThread
87+
attr_reader :errors, :defers
88+
def initialize
6789
@errors = []
90+
@defers = []
6891
end
92+
end
6993

70-
def spawn_child(child_options)
71-
own_options = {
72-
node: @node,
73-
value: @value,
74-
type_defn: @type_defn,
75-
exec_context: @exec_context,
76-
path: @path,
77-
}
78-
init_options = own_options.merge(child_options)
79-
self.class.new(init_options)
94+
# One step of execution
95+
class ExecFrame
96+
attr_reader :node, :path, :type, :value
97+
def initialize(node:, path:, type:, value:)
98+
@node = node
99+
@path = path
100+
@type = type
101+
@value = value
80102
end
81103
end
82104

83105
private
84106

85107
# If this `frame` is marked as defer, add it to `defers`
86108
# Otherwise, resolve it.
87-
def resolve_or_defer_frame(frame, defers)
109+
def resolve_or_defer_frame(scope, thread, frame)
88110
if frame.node.directives.any? { |dir| dir.name == "defer" }
89-
defers << frame
111+
thread.defers << frame
90112
nil
91113
else
92-
resolve_frame(frame)
114+
resolve_frame(scope, thread, frame)
93115
end
94116
end
95117

96118
# Determine this frame's result and write it into `#result`.
97119
# Anything marked as `@defer` will be deferred.
98-
def resolve_frame(frame)
120+
def resolve_frame(scope, thread, frame)
99121
ast_node = frame.node
100122
case ast_node
101123
when Nodes::OperationDefinition
102-
resolve_selections(ast_node, frame)
124+
resolve_selections(scope, thread, frame)
103125
when Nodes::Field
104-
type_defn = frame.type_defn
105-
# Use Context because it provides dynamic fields too (like __typename)
106-
field_defn = frame.exec_context.get_field(type_defn, ast_node.name)
126+
type_defn = frame.type
127+
# Use scope because it provides dynamic fields too (like __typename)
128+
field_defn = scope.get_field(type_defn, ast_node.name)
107129

108-
field_result = resolve_field_frame(field_defn, frame)
130+
field_result = resolve_field_frame(scope, thread, frame, field_defn)
109131
return_type_defn = field_defn.type
110132

111-
if field_result.is_a?(GraphQL::ExecutionError)
112-
field_result.ast_node = ast_node
113-
frame.errors << field_result
114-
nil
115-
else
116133
resolve_value(
117-
type_defn: return_type_defn,
118-
value: field_result,
119-
frame: frame,
134+
scope,
135+
thread,
136+
frame,
137+
field_result,
138+
return_type_defn,
120139
)
121-
end
122-
# when Nodes::FragmentSpread
123140
else
124141
raise("No defined resolution for #{ast_node.class.name} (#{ast_node})")
125142
end
126143
end
127144

128-
def resolve_selections(ast_node, outer_frame)
145+
def resolve_selections(scope, thread, outer_frame)
129146
merged_selections = GraphQL::Execution::SelectionOnType.flatten(
130-
outer_frame.exec_context,
147+
scope,
131148
outer_frame.value,
132-
outer_frame.type_defn,
133-
ast_node
149+
outer_frame.type,
150+
outer_frame.node,
134151
)
135152

136153
resolved_selections = merged_selections.each_with_object({}) do |ast_selection, memo|
137154
selection_key = path_step(ast_selection)
138155

139-
inner_frame = outer_frame.spawn_child(
156+
inner_frame = ExecFrame.new(
140157
node: ast_selection,
141158
path: outer_frame.path + [selection_key],
159+
type: outer_frame.type,
160+
value: outer_frame.value,
142161
)
143162

144-
inner_result = resolve_or_defer_frame(inner_frame, outer_frame.defers)
145-
outer_frame.errors.push(*inner_frame.errors)
146-
outer_frame.defers.push(*inner_frame.defers)
163+
inner_result = resolve_or_defer_frame(scope, thread, inner_frame)
147164
memo[selection_key] = inner_result
148165
end
149166
resolved_selections
167+
rescue GraphQL::InvalidNullError => err
168+
err.parent_error? || thread.errors << err
169+
nil
150170
end
151171

152172
def path_step(ast_node)
@@ -158,11 +178,11 @@ def path_step(ast_node)
158178
end
159179
end
160180

161-
def resolve_field_frame(field_defn, frame)
181+
def resolve_field_frame(scope, thread, frame, field_defn)
162182
ast_node = frame.node
163-
type_defn = frame.type_defn
183+
type_defn = frame.type
164184
value = frame.value
165-
query = frame.exec_context.query
185+
query = scope.query
166186

167187
# Build arguments according to query-string literals, default values, and query variables
168188
arguments = GraphQL::Query::LiteralInput.from_arguments(
@@ -172,10 +192,10 @@ def resolve_field_frame(field_defn, frame)
172192
)
173193

174194
# This is the last call in the middleware chain; it actually calls the user's resolve proc
175-
field_resolve_middleware_proc = -> (_parent_type, parent_object, field_definition, field_args, context, _next) {
176-
context.ast_node = ast_node
177-
value = field_definition.resolve(parent_object, field_args, context)
178-
context.ast_node = nil
195+
field_resolve_middleware_proc = -> (_parent_type, parent_object, field_definition, field_args, query_ctx, _next) {
196+
query_ctx.ast_node = ast_node
197+
value = field_definition.resolve(parent_object, field_args, query_ctx)
198+
query_ctx.ast_node = nil
179199
value
180200
}
181201

@@ -188,13 +208,20 @@ def resolve_field_frame(field_defn, frame)
188208
)
189209

190210
begin
191-
chain.call
211+
resolve_fn_value = chain.call
192212
rescue GraphQL::ExecutionError => err
193-
err
213+
resolve_fn_value = err
194214
end
215+
216+
if resolve_fn_value.is_a?(GraphQL::ExecutionError)
217+
thread.errors << resolve_fn_value
218+
resolve_fn_value.ast_node = ast_node
219+
end
220+
221+
resolve_fn_value
195222
end
196223

197-
def resolve_value(type_defn:, value:, frame:)
224+
def resolve_value(scope, thread, frame, value, type_defn)
198225
if value.nil? || value.is_a?(GraphQL::ExecutionError)
199226
if type_defn.kind.non_null?
200227
raise GraphQL::InvalidNullError.new(frame.node.name, value)
@@ -207,36 +234,35 @@ def resolve_value(type_defn:, value:, frame:)
207234
type_defn.coerce_result(value)
208235
when GraphQL::TypeKinds::NON_NULL
209236
wrapped_type = type_defn.of_type
210-
resolve_value(type_defn: wrapped_type, value: value, frame: frame)
237+
resolve_value(scope, thread, frame, value, wrapped_type)
211238
when GraphQL::TypeKinds::LIST
212239
wrapped_type = type_defn.of_type
213240
resolved_values = value.each_with_index.map do |item, idx|
214-
inner_frame = frame.spawn_child({
215-
path: frame.path + [idx]
241+
inner_frame = ExecFrame.new({
242+
node: frame.node,
243+
path: frame.path + [idx],
244+
type: wrapped_type,
245+
value: item,
216246
})
217-
inner_result = resolve_value(type_defn: wrapped_type, value: item, frame: inner_frame)
218-
frame.errors.push(*inner_frame.errors)
219-
frame.defers.push(*inner_frame.defers)
220-
inner_result
247+
resolve_value(scope, thread, inner_frame, item, wrapped_type)
221248
end
222249
resolved_values
223250
when GraphQL::TypeKinds::INTERFACE, GraphQL::TypeKinds::UNION
224-
resolved_type = type_defn.resolve_type(value, frame.exec_context)
251+
resolved_type = type_defn.resolve_type(value, scope)
225252

226253
if !resolved_type.is_a?(GraphQL::ObjectType)
227254
raise GraphQL::ObjectType::UnresolvedTypeError.new(type_defn, value)
228255
else
229-
resolve_value(value: value, type_defn: resolved_type, frame: frame)
256+
resolve_value(scope, thread, frame, value, resolved_type)
230257
end
231258
when GraphQL::TypeKinds::OBJECT
232-
inner_frame = frame.spawn_child(
259+
inner_frame = ExecFrame.new(
260+
node: frame.node,
261+
path: frame.path,
233262
value: value,
234-
type_defn: type_defn,
263+
type: type_defn,
235264
)
236-
inner_result = resolve_selections(frame.node, inner_frame)
237-
frame.errors.push(*inner_frame.errors)
238-
frame.defers.push(*inner_frame.defers)
239-
inner_result
265+
resolve_selections(scope, thread, inner_frame)
240266
else
241267
raise("No ResolveValue for kind: #{type_defn.kind.name} (#{type_defn})")
242268
end

0 commit comments

Comments
 (0)