graphql 1.11.7 → 1.12.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of graphql might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/lib/generators/graphql/install_generator.rb +7 -5
- data/lib/generators/graphql/relay.rb +55 -0
- data/lib/generators/graphql/relay_generator.rb +20 -0
- data/lib/generators/graphql/templates/base_connection.erb +8 -0
- data/lib/generators/graphql/templates/base_edge.erb +8 -0
- data/lib/generators/graphql/templates/node_type.erb +9 -0
- data/lib/generators/graphql/templates/object.erb +1 -1
- data/lib/generators/graphql/templates/query_type.erb +1 -3
- data/lib/generators/graphql/templates/schema.erb +8 -35
- data/lib/graphql.rb +38 -4
- data/lib/graphql/analysis/analyze_query.rb +7 -0
- data/lib/graphql/analysis/ast.rb +11 -2
- data/lib/graphql/analysis/ast/visitor.rb +9 -1
- data/lib/graphql/backtrace.rb +28 -19
- data/lib/graphql/backtrace/inspect_result.rb +0 -1
- data/lib/graphql/backtrace/legacy_tracer.rb +56 -0
- data/lib/graphql/backtrace/table.rb +22 -3
- data/lib/graphql/backtrace/traced_error.rb +0 -1
- data/lib/graphql/backtrace/tracer.rb +37 -10
- data/lib/graphql/backwards_compatibility.rb +2 -1
- data/lib/graphql/base_type.rb +1 -1
- data/lib/graphql/compatibility/execution_specification.rb +1 -0
- data/lib/graphql/compatibility/lazy_execution_specification.rb +2 -0
- data/lib/graphql/compatibility/query_parser_specification.rb +2 -0
- data/lib/graphql/compatibility/schema_parser_specification.rb +2 -0
- data/lib/graphql/dataloader.rb +208 -0
- data/lib/graphql/dataloader/null_dataloader.rb +21 -0
- data/lib/graphql/dataloader/request.rb +19 -0
- data/lib/graphql/dataloader/request_all.rb +19 -0
- data/lib/graphql/dataloader/source.rb +107 -0
- data/lib/graphql/define/assign_global_id_field.rb +1 -1
- data/lib/graphql/define/instance_definable.rb +32 -2
- data/lib/graphql/define/type_definer.rb +5 -5
- data/lib/graphql/deprecated_dsl.rb +7 -2
- data/lib/graphql/deprecation.rb +13 -0
- data/lib/graphql/enum_type.rb +2 -0
- data/lib/graphql/execution/errors.rb +4 -0
- data/lib/graphql/execution/execute.rb +7 -0
- data/lib/graphql/execution/interpreter.rb +11 -7
- data/lib/graphql/execution/interpreter/arguments.rb +51 -14
- data/lib/graphql/execution/interpreter/arguments_cache.rb +37 -14
- data/lib/graphql/execution/interpreter/handles_raw_value.rb +0 -7
- data/lib/graphql/execution/interpreter/resolve.rb +33 -25
- data/lib/graphql/execution/interpreter/runtime.rb +173 -123
- data/lib/graphql/execution/multiplex.rb +36 -23
- data/lib/graphql/function.rb +4 -0
- data/lib/graphql/input_object_type.rb +2 -0
- data/lib/graphql/interface_type.rb +3 -1
- data/lib/graphql/internal_representation/document.rb +2 -2
- data/lib/graphql/internal_representation/rewrite.rb +1 -1
- data/lib/graphql/language/document_from_schema_definition.rb +50 -23
- data/lib/graphql/object_type.rb +2 -2
- data/lib/graphql/pagination/connection.rb +5 -1
- data/lib/graphql/pagination/connections.rb +6 -16
- data/lib/graphql/parse_error.rb +0 -1
- data/lib/graphql/query.rb +10 -2
- data/lib/graphql/query/arguments.rb +1 -1
- data/lib/graphql/query/arguments_cache.rb +0 -1
- data/lib/graphql/query/context.rb +4 -2
- data/lib/graphql/query/executor.rb +0 -1
- data/lib/graphql/query/null_context.rb +3 -2
- data/lib/graphql/query/serial_execution.rb +1 -0
- data/lib/graphql/query/variable_validation_error.rb +1 -1
- data/lib/graphql/relay/base_connection.rb +7 -0
- data/lib/graphql/relay/connection_instrumentation.rb +4 -4
- data/lib/graphql/relay/connection_type.rb +1 -1
- data/lib/graphql/relay/mutation.rb +1 -0
- data/lib/graphql/relay/node.rb +3 -0
- data/lib/graphql/relay/type_extensions.rb +2 -0
- data/lib/graphql/scalar_type.rb +2 -0
- data/lib/graphql/schema.rb +64 -26
- data/lib/graphql/schema/argument.rb +86 -7
- data/lib/graphql/schema/build_from_definition.rb +139 -51
- data/lib/graphql/schema/directive.rb +76 -0
- data/lib/graphql/schema/directive/flagged.rb +57 -0
- data/lib/graphql/schema/enum.rb +3 -0
- data/lib/graphql/schema/enum_value.rb +12 -6
- data/lib/graphql/schema/field.rb +40 -16
- data/lib/graphql/schema/field/connection_extension.rb +3 -2
- data/lib/graphql/schema/find_inherited_value.rb +3 -1
- data/lib/graphql/schema/input_object.rb +39 -24
- data/lib/graphql/schema/interface.rb +1 -0
- data/lib/graphql/schema/member.rb +4 -0
- data/lib/graphql/schema/member/base_dsl_methods.rb +1 -0
- data/lib/graphql/schema/member/build_type.rb +3 -3
- data/lib/graphql/schema/member/has_arguments.rb +54 -49
- data/lib/graphql/schema/member/has_deprecation_reason.rb +25 -0
- data/lib/graphql/schema/member/has_directives.rb +98 -0
- data/lib/graphql/schema/member/has_fields.rb +1 -4
- data/lib/graphql/schema/member/has_validators.rb +31 -0
- data/lib/graphql/schema/member/instrumentation.rb +0 -1
- data/lib/graphql/schema/member/type_system_helpers.rb +1 -1
- data/lib/graphql/schema/middleware_chain.rb +1 -1
- data/lib/graphql/schema/object.rb +11 -0
- data/lib/graphql/schema/printer.rb +5 -4
- data/lib/graphql/schema/resolver.rb +7 -0
- data/lib/graphql/schema/resolver/has_payload_type.rb +2 -0
- data/lib/graphql/schema/subscription.rb +19 -1
- data/lib/graphql/schema/timeout_middleware.rb +3 -1
- data/lib/graphql/schema/validation.rb +4 -2
- data/lib/graphql/schema/validator.rb +163 -0
- data/lib/graphql/schema/validator/exclusion_validator.rb +31 -0
- data/lib/graphql/schema/validator/format_validator.rb +49 -0
- data/lib/graphql/schema/validator/inclusion_validator.rb +33 -0
- data/lib/graphql/schema/validator/length_validator.rb +57 -0
- data/lib/graphql/schema/validator/numericality_validator.rb +71 -0
- data/lib/graphql/schema/validator/required_validator.rb +68 -0
- data/lib/graphql/static_validation/validator.rb +4 -0
- data/lib/graphql/subscriptions.rb +17 -20
- data/lib/graphql/subscriptions/event.rb +0 -1
- data/lib/graphql/subscriptions/instrumentation.rb +0 -1
- data/lib/graphql/subscriptions/serialize.rb +0 -1
- data/lib/graphql/subscriptions/subscription_root.rb +1 -1
- data/lib/graphql/tracing.rb +2 -2
- data/lib/graphql/tracing/appoptics_tracing.rb +3 -1
- data/lib/graphql/tracing/platform_tracing.rb +3 -1
- data/lib/graphql/tracing/skylight_tracing.rb +1 -1
- data/lib/graphql/types/relay.rb +11 -3
- data/lib/graphql/types/relay/base_connection.rb +2 -92
- data/lib/graphql/types/relay/base_edge.rb +2 -35
- data/lib/graphql/types/relay/connection_behaviors.rb +123 -0
- data/lib/graphql/types/relay/default_relay.rb +27 -0
- data/lib/graphql/types/relay/edge_behaviors.rb +42 -0
- data/lib/graphql/types/relay/has_node_field.rb +41 -0
- data/lib/graphql/types/relay/has_nodes_field.rb +41 -0
- data/lib/graphql/types/relay/node.rb +2 -4
- data/lib/graphql/types/relay/node_behaviors.rb +15 -0
- data/lib/graphql/types/relay/node_field.rb +1 -19
- data/lib/graphql/types/relay/nodes_field.rb +1 -19
- data/lib/graphql/types/relay/page_info.rb +2 -14
- data/lib/graphql/types/relay/page_info_behaviors.rb +25 -0
- data/lib/graphql/union_type.rb +2 -0
- data/lib/graphql/upgrader/member.rb +1 -0
- data/lib/graphql/upgrader/schema.rb +1 -0
- data/lib/graphql/version.rb +1 -1
- metadata +50 -93
- data/lib/graphql/types/relay/base_field.rb +0 -22
- data/lib/graphql/types/relay/base_interface.rb +0 -29
- data/lib/graphql/types/relay/base_object.rb +0 -26
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GraphQL
|
3
|
+
class Backtrace
|
4
|
+
module LegacyTracer
|
5
|
+
module_function
|
6
|
+
|
7
|
+
# Implement the {GraphQL::Tracing} API.
|
8
|
+
def trace(key, metadata)
|
9
|
+
case key
|
10
|
+
when "lex", "parse"
|
11
|
+
# No context here, don't have a query yet
|
12
|
+
nil
|
13
|
+
when "execute_multiplex", "analyze_multiplex"
|
14
|
+
# No query context yet
|
15
|
+
nil
|
16
|
+
when "validate", "analyze_query", "execute_query", "execute_query_lazy"
|
17
|
+
query = metadata[:query] || metadata[:queries].first
|
18
|
+
push_data = query
|
19
|
+
multiplex = query.multiplex
|
20
|
+
when "execute_field", "execute_field_lazy"
|
21
|
+
# The interpreter passes `query:`, legacy passes `context:`
|
22
|
+
context = metadata[:context] || ((q = metadata[:query]) && q.context)
|
23
|
+
push_data = context
|
24
|
+
multiplex = context.query.multiplex
|
25
|
+
else
|
26
|
+
# Custom key, no backtrace data for this
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
|
30
|
+
if push_data
|
31
|
+
multiplex.context[:last_graphql_backtrace_context] = push_data
|
32
|
+
end
|
33
|
+
|
34
|
+
if key == "execute_multiplex"
|
35
|
+
begin
|
36
|
+
yield
|
37
|
+
rescue StandardError => err
|
38
|
+
# This is an unhandled error from execution,
|
39
|
+
# Re-raise it with a GraphQL trace.
|
40
|
+
potential_context = metadata[:multiplex].context[:last_graphql_backtrace_context]
|
41
|
+
|
42
|
+
if potential_context.is_a?(GraphQL::Query::Context) || potential_context.is_a?(GraphQL::Query::Context::FieldResolutionContext)
|
43
|
+
raise TracedError.new(err, potential_context)
|
44
|
+
else
|
45
|
+
raise
|
46
|
+
end
|
47
|
+
ensure
|
48
|
+
metadata[:multiplex].context.delete(:last_graphql_backtrace_context)
|
49
|
+
end
|
50
|
+
else
|
51
|
+
yield
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -1,5 +1,4 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
# test_via: ../backtrace.rb
|
3
2
|
module GraphQL
|
4
3
|
class Backtrace
|
5
4
|
# A class for turning a context into a human-readable table or array
|
@@ -79,6 +78,25 @@ module GraphQL
|
|
79
78
|
# @return [Array] 5 items for a backtrace table (not `key`)
|
80
79
|
def build_rows(context_entry, rows:, top: false)
|
81
80
|
case context_entry
|
81
|
+
when Backtrace::Frame
|
82
|
+
field_alias = context_entry.ast_node.respond_to?(:alias) && context_entry.ast_node.alias
|
83
|
+
value = if top && @override_value
|
84
|
+
@override_value
|
85
|
+
else
|
86
|
+
@context.query.context.namespace(:interpreter)[:runtime].value_at(context_entry.path)
|
87
|
+
end
|
88
|
+
rows << [
|
89
|
+
"#{context_entry.ast_node ? context_entry.ast_node.position.join(":") : ""}",
|
90
|
+
"#{context_entry.field.path}#{field_alias ? " as #{field_alias}" : ""}",
|
91
|
+
"#{context_entry.object.object.inspect}",
|
92
|
+
context_entry.arguments.to_h.inspect,
|
93
|
+
Backtrace::InspectResult.inspect_result(value),
|
94
|
+
]
|
95
|
+
if (parent = context_entry.parent_frame)
|
96
|
+
build_rows(parent, rows: rows)
|
97
|
+
else
|
98
|
+
rows
|
99
|
+
end
|
82
100
|
when GraphQL::Query::Context::FieldResolutionContext
|
83
101
|
ctx = context_entry
|
84
102
|
field_name = "#{ctx.irep_node.owner_type.name}.#{ctx.field.name}"
|
@@ -112,15 +130,16 @@ module GraphQL
|
|
112
130
|
if object.is_a?(GraphQL::Schema::Object)
|
113
131
|
object = object.object
|
114
132
|
end
|
133
|
+
value = context_entry.namespace(:interpreter)[:runtime].value_at([])
|
115
134
|
rows << [
|
116
135
|
"#{position}",
|
117
136
|
"#{op_type}#{op_name ? " #{op_name}" : ""}",
|
118
137
|
"#{object.inspect}",
|
119
138
|
query.variables.to_h.inspect,
|
120
|
-
Backtrace::InspectResult.inspect_result(
|
139
|
+
Backtrace::InspectResult.inspect_result(value),
|
121
140
|
]
|
122
141
|
else
|
123
|
-
raise "Unexpected get_rows subject #{context_entry.inspect}"
|
142
|
+
raise "Unexpected get_rows subject #{context_entry.class} (#{context_entry.inspect})"
|
124
143
|
end
|
125
144
|
end
|
126
145
|
end
|
@@ -1,46 +1,73 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module GraphQL
|
3
3
|
class Backtrace
|
4
|
+
# TODO this is not fiber-friendly
|
4
5
|
module Tracer
|
5
6
|
module_function
|
6
7
|
|
7
8
|
# Implement the {GraphQL::Tracing} API.
|
8
9
|
def trace(key, metadata)
|
9
|
-
|
10
|
+
case key
|
10
11
|
when "lex", "parse"
|
11
12
|
# No context here, don't have a query yet
|
12
13
|
nil
|
13
14
|
when "execute_multiplex", "analyze_multiplex"
|
14
|
-
|
15
|
+
# No query context yet
|
16
|
+
nil
|
15
17
|
when "validate", "analyze_query", "execute_query", "execute_query_lazy"
|
16
|
-
metadata[:query] || metadata[:queries]
|
18
|
+
query = metadata[:query] || metadata[:queries].first
|
19
|
+
push_key = []
|
20
|
+
push_data = query
|
21
|
+
multiplex = query.multiplex
|
17
22
|
when "execute_field", "execute_field_lazy"
|
18
|
-
|
19
|
-
|
23
|
+
query = metadata[:query] || raise(ArgumentError, "Add `legacy: true` to use GraphQL::Backtrace without the interpreter runtime.")
|
24
|
+
multiplex = query.multiplex
|
25
|
+
push_key = metadata[:path].reject { |i| i.is_a?(Integer) }
|
26
|
+
parent_frame = multiplex.context[:graphql_backtrace_contexts][push_key[0..-2]]
|
27
|
+
|
28
|
+
if parent_frame.is_a?(GraphQL::Query)
|
29
|
+
parent_frame = parent_frame.context
|
30
|
+
end
|
31
|
+
|
32
|
+
push_data = Frame.new(
|
33
|
+
query: query,
|
34
|
+
path: push_key,
|
35
|
+
ast_node: metadata[:ast_node],
|
36
|
+
field: metadata[:field],
|
37
|
+
object: metadata[:object],
|
38
|
+
arguments: metadata[:arguments],
|
39
|
+
parent_frame: parent_frame,
|
40
|
+
)
|
20
41
|
else
|
21
42
|
# Custom key, no backtrace data for this
|
22
43
|
nil
|
23
44
|
end
|
24
45
|
|
25
|
-
if push_data
|
26
|
-
|
46
|
+
if push_data && multiplex
|
47
|
+
multiplex.context[:graphql_backtrace_contexts][push_key] = push_data
|
48
|
+
multiplex.context[:last_graphql_backtrace_context] = push_data
|
27
49
|
end
|
28
50
|
|
29
51
|
if key == "execute_multiplex"
|
52
|
+
multiplex_context = metadata[:multiplex].context
|
53
|
+
multiplex_context[:graphql_backtrace_contexts] = {}
|
30
54
|
begin
|
31
55
|
yield
|
32
56
|
rescue StandardError => err
|
33
57
|
# This is an unhandled error from execution,
|
34
58
|
# Re-raise it with a GraphQL trace.
|
35
|
-
potential_context =
|
59
|
+
potential_context = multiplex_context[:last_graphql_backtrace_context]
|
36
60
|
|
37
|
-
if potential_context.is_a?(GraphQL::Query::Context) ||
|
61
|
+
if potential_context.is_a?(GraphQL::Query::Context) ||
|
62
|
+
potential_context.is_a?(GraphQL::Query::Context::FieldResolutionContext) ||
|
63
|
+
potential_context.is_a?(Backtrace::Frame)
|
38
64
|
raise TracedError.new(err, potential_context)
|
39
65
|
else
|
40
66
|
raise
|
41
67
|
end
|
42
68
|
ensure
|
43
|
-
|
69
|
+
multiplex_context.delete(:graphql_backtrace_contexts)
|
70
|
+
multiplex_context.delete(:last_graphql_backtrace_context)
|
44
71
|
end
|
45
72
|
else
|
46
73
|
yield
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module GraphQL
|
3
3
|
# Helpers for migrating in a backwards-compatible way
|
4
|
+
# Remove this in GraphQL-Ruby 2.0, when all users of it will be gone.
|
4
5
|
# @api private
|
5
6
|
module BackwardsCompatibility
|
6
7
|
module_function
|
@@ -21,7 +22,7 @@ module GraphQL
|
|
21
22
|
backtrace = caller(0, 20)
|
22
23
|
# Find the first line in the trace that isn't library internals:
|
23
24
|
user_line = backtrace.find {|l| l !~ /lib\/graphql/ }
|
24
|
-
warn(message + "\n" + user_line + "\n")
|
25
|
+
GraphQL::Deprecation.warn(message + "\n" + user_line + "\n")
|
25
26
|
wrapper = last ? LastArgumentsWrapper : FirstArgumentsWrapper
|
26
27
|
wrapper.new(callable, from)
|
27
28
|
else
|
data/lib/graphql/base_type.rb
CHANGED
@@ -224,7 +224,7 @@ module GraphQL
|
|
224
224
|
private
|
225
225
|
|
226
226
|
def warn_deprecated_coerce(alt_method_name)
|
227
|
-
warn("Coercing without a context is deprecated; use `#{alt_method_name}` if you don't want context-awareness")
|
227
|
+
GraphQL::Deprecation.warn("Coercing without a context is deprecated; use `#{alt_method_name}` if you don't want context-awareness")
|
228
228
|
end
|
229
229
|
end
|
230
230
|
end
|
@@ -32,6 +32,7 @@ module GraphQL
|
|
32
32
|
# @param execution_strategy [<#new, #execute>] An execution strategy class
|
33
33
|
# @return [Class<Minitest::Test>] A test suite for this execution strategy
|
34
34
|
def self.build_suite(execution_strategy)
|
35
|
+
GraphQL::Deprecation.warn "#{self} will be removed from GraphQL-Ruby 2.0. There is no replacement, please open an issue on GitHub if you need support."
|
35
36
|
Class.new(Minitest::Test) do
|
36
37
|
class << self
|
37
38
|
attr_accessor :counter_schema, :specification_schema
|
@@ -7,6 +7,8 @@ module GraphQL
|
|
7
7
|
# @param execution_strategy [<#new, #execute>] An execution strategy class
|
8
8
|
# @return [Class<Minitest::Test>] A test suite for this execution strategy
|
9
9
|
def self.build_suite(execution_strategy)
|
10
|
+
GraphQL::Deprecation.warn "#{self} will be removed from GraphQL-Ruby 2.0. There is no replacement, please open an issue on GitHub if you need support."
|
11
|
+
|
10
12
|
Class.new(Minitest::Test) do
|
11
13
|
class << self
|
12
14
|
attr_accessor :lazy_schema
|
@@ -11,6 +11,8 @@ module GraphQL
|
|
11
11
|
# @yieldreturn [GraphQL::Language::Nodes::Document]
|
12
12
|
# @return [Class<Minitest::Test>] A test suite for this parse function
|
13
13
|
def self.build_suite(&block)
|
14
|
+
GraphQL::Deprecation.warn "#{self} will be removed from GraphQL-Ruby 2.0. There is no replacement, please open an issue on GitHub if you need support."
|
15
|
+
|
14
16
|
Class.new(Minitest::Test) do
|
15
17
|
include QueryAssertions
|
16
18
|
include ParseErrorSpecification
|
@@ -8,6 +8,8 @@ module GraphQL
|
|
8
8
|
# @yieldreturn [GraphQL::Language::Nodes::Document]
|
9
9
|
# @return [Class<Minitest::Test>] A test suite for this parse function
|
10
10
|
def self.build_suite(&block)
|
11
|
+
GraphQL::Deprecation.warn "#{self} will be removed from GraphQL-Ruby 2.0. There is no replacement, please open an issue on GitHub if you need support."
|
12
|
+
|
11
13
|
Class.new(Minitest::Test) do
|
12
14
|
@@parse_fn = block
|
13
15
|
|
@@ -0,0 +1,208 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "graphql/dataloader/null_dataloader"
|
4
|
+
require "graphql/dataloader/request"
|
5
|
+
require "graphql/dataloader/request_all"
|
6
|
+
require "graphql/dataloader/source"
|
7
|
+
|
8
|
+
module GraphQL
|
9
|
+
# This plugin supports Fiber-based concurrency, along with {GraphQL::Dataloader::Source}.
|
10
|
+
#
|
11
|
+
# @example Installing Dataloader
|
12
|
+
#
|
13
|
+
# class MySchema < GraphQL::Schema
|
14
|
+
# use GraphQL::Dataloader
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# @example Waiting for batch-loaded data in a GraphQL field
|
18
|
+
#
|
19
|
+
# field :team, Types::Team, null: true
|
20
|
+
#
|
21
|
+
# def team
|
22
|
+
# dataloader.with(Sources::Record, Team).load(object.team_id)
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
class Dataloader
|
26
|
+
def self.use(schema)
|
27
|
+
schema.dataloader_class = self
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize
|
31
|
+
@source_cache = Hash.new { |h, source_class| h[source_class] = Hash.new { |h2, batch_parameters|
|
32
|
+
source = source_class.new(*batch_parameters)
|
33
|
+
source.setup(self)
|
34
|
+
h2[batch_parameters] = source
|
35
|
+
}
|
36
|
+
}
|
37
|
+
@pending_jobs = []
|
38
|
+
end
|
39
|
+
|
40
|
+
# Get a Source instance from this dataloader, for calling `.load(...)` or `.request(...)` on.
|
41
|
+
#
|
42
|
+
# @param source_class [Class<GraphQL::Dataloader::Source]
|
43
|
+
# @param batch_parameters [Array<Object>]
|
44
|
+
# @return [GraphQL::Dataloader::Source] An instance of {source_class}, initialized with `self, *batch_parameters`,
|
45
|
+
# and cached for the lifetime of this {Multiplex}.
|
46
|
+
def with(source_class, *batch_parameters)
|
47
|
+
@source_cache[source_class][batch_parameters]
|
48
|
+
end
|
49
|
+
|
50
|
+
# Tell the dataloader that this fiber is waiting for data.
|
51
|
+
#
|
52
|
+
# Dataloader will resume the fiber after the requested data has been loaded (by another Fiber).
|
53
|
+
#
|
54
|
+
# @return [void]
|
55
|
+
def yield
|
56
|
+
Fiber.yield
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
|
60
|
+
# @api private Nothing to see here
|
61
|
+
def append_job(&job)
|
62
|
+
# Given a block, queue it up to be worked through when `#run` is called.
|
63
|
+
# (If the dataloader is already running, than a Fiber will pick this up later.)
|
64
|
+
@pending_jobs.push(job)
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
|
68
|
+
# @api private Move along, move along
|
69
|
+
def run
|
70
|
+
# At a high level, the algorithm is:
|
71
|
+
#
|
72
|
+
# A) Inside Fibers, run jobs from the queue one-by-one
|
73
|
+
# - When one of the jobs yields to the dataloader (`Fiber.yield`), then that fiber will pause
|
74
|
+
# - In that case, if there are still pending jobs, a new Fiber will be created to run jobs
|
75
|
+
# - Continue until all jobs have been _started_ by a Fiber. (Any number of those Fibers may be waiting to be resumed, after their data is loaded)
|
76
|
+
# B) Once all known jobs have been run until they are complete or paused for data, run all pending data sources.
|
77
|
+
# - Similarly, create a Fiber to consume pending sources and tell them to load their data.
|
78
|
+
# - If one of those Fibers pauses, then create a new Fiber to continue working through remaining pending sources.
|
79
|
+
# - When a source causes another source to become pending, run the newly-pending source _first_, since it's a dependency of the previous one.
|
80
|
+
# C) After all pending sources have been completely loaded (there are no more pending sources), resume any Fibers that were waiting for data.
|
81
|
+
# - Those Fibers assume that source caches will have been populated with the data they were waiting for.
|
82
|
+
# - Those Fibers may request data from a source again, in which case they will yeilded and be added to a new pending fiber list.
|
83
|
+
# D) Once all pending fibers have been resumed once, return to `A` above.
|
84
|
+
#
|
85
|
+
# For whatever reason, the best implementation I could find was to order the steps `[D, A, B, C]`, with a special case for skipping `D`
|
86
|
+
# on the first pass. I just couldn't find a better way to write the loops in a way that was DRY and easy to read.
|
87
|
+
#
|
88
|
+
pending_fibers = []
|
89
|
+
next_fibers = []
|
90
|
+
first_pass = true
|
91
|
+
|
92
|
+
while first_pass || (f = pending_fibers.shift)
|
93
|
+
if first_pass
|
94
|
+
first_pass = false
|
95
|
+
else
|
96
|
+
# These fibers were previously waiting for sources to load data,
|
97
|
+
# resume them. (They might wait again, in which case, re-enqueue them.)
|
98
|
+
f.resume
|
99
|
+
if f.alive?
|
100
|
+
next_fibers << f
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
while @pending_jobs.any?
|
105
|
+
# Create a Fiber to consume jobs until one of the jobs yields
|
106
|
+
# or jobs run out
|
107
|
+
f = Fiber.new {
|
108
|
+
while (job = @pending_jobs.shift)
|
109
|
+
job.call
|
110
|
+
end
|
111
|
+
}
|
112
|
+
result = f.resume
|
113
|
+
if result.is_a?(StandardError)
|
114
|
+
raise result
|
115
|
+
end
|
116
|
+
# In this case, the job yielded. Queue it up to run again after
|
117
|
+
# we load whatever it's waiting for.
|
118
|
+
if f.alive?
|
119
|
+
next_fibers << f
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
if pending_fibers.empty?
|
124
|
+
# Now, run all Sources which have become pending _before_ resuming GraphQL execution.
|
125
|
+
# Sources might queue up other Sources, which is fine -- those will also run before resuming execution.
|
126
|
+
#
|
127
|
+
# This is where an evented approach would be even better -- can we tell which
|
128
|
+
# fibers are ready to continue, and continue execution there?
|
129
|
+
#
|
130
|
+
source_fiber_stack = if (first_source_fiber = create_source_fiber)
|
131
|
+
[first_source_fiber]
|
132
|
+
else
|
133
|
+
nil
|
134
|
+
end
|
135
|
+
|
136
|
+
if source_fiber_stack
|
137
|
+
# Use a stack with `.pop` here so that when a source causes another source to become pending,
|
138
|
+
# that newly-pending source will run _before_ the one that depends on it.
|
139
|
+
# (See below where the old fiber is pushed to the stack, then the new fiber is pushed on the stack.)
|
140
|
+
while (outer_source_fiber = source_fiber_stack.pop)
|
141
|
+
result = outer_source_fiber.resume
|
142
|
+
if result.is_a?(StandardError)
|
143
|
+
raise result
|
144
|
+
end
|
145
|
+
|
146
|
+
if outer_source_fiber.alive?
|
147
|
+
source_fiber_stack << outer_source_fiber
|
148
|
+
end
|
149
|
+
# If this source caused more sources to become pending, run those before running this one again:
|
150
|
+
next_source_fiber = create_source_fiber
|
151
|
+
if next_source_fiber
|
152
|
+
source_fiber_stack << next_source_fiber
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
# Move newly-enqueued Fibers on to the list to be resumed.
|
157
|
+
# Clear out the list of next-round Fibers, so that
|
158
|
+
# any Fibers that pause can be put on it.
|
159
|
+
pending_fibers.concat(next_fibers)
|
160
|
+
next_fibers.clear
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
if @pending_jobs.any?
|
165
|
+
raise "Invariant: #{@pending_jobs.size} pending jobs"
|
166
|
+
elsif pending_fibers.any?
|
167
|
+
raise "Invariant: #{pending_fibers.size} pending fibers"
|
168
|
+
elsif next_fibers.any?
|
169
|
+
raise "Invariant: #{next_fibers.size} next fibers"
|
170
|
+
end
|
171
|
+
nil
|
172
|
+
end
|
173
|
+
|
174
|
+
private
|
175
|
+
|
176
|
+
# If there are pending sources, return a fiber for running them.
|
177
|
+
# Otherwise, return `nil`.
|
178
|
+
#
|
179
|
+
# @return [Fiber, nil]
|
180
|
+
def create_source_fiber
|
181
|
+
pending_sources = nil
|
182
|
+
@source_cache.each_value do |source_by_batch_params|
|
183
|
+
source_by_batch_params.each_value do |source|
|
184
|
+
if source.pending?
|
185
|
+
pending_sources ||= []
|
186
|
+
pending_sources << source
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
if pending_sources
|
192
|
+
# By passing the whole array into this Fiber, it's possible that we set ourselves up for a bunch of no-ops.
|
193
|
+
# For example, if you have sources `[a, b, c]`, and `a` is loaded, then `b` yields to wait for `d`, then
|
194
|
+
# the next fiber would be dispatched with `[c, d]`. It would fulfill `c`, then `d`, then eventually
|
195
|
+
# the previous fiber would start up again. `c` would no longer be pending, but it would still receive `.run_pending_keys`.
|
196
|
+
# That method is short-circuited since it isn't pending any more, but it's still a waste.
|
197
|
+
#
|
198
|
+
# This design could probably be improved by maintaining a `@pending_sources` queue which is shared by the fibers,
|
199
|
+
# similar to `@pending_jobs`. That way, when a fiber is resumed, it would never pick up work that was finished by a different fiber.
|
200
|
+
source_fiber = Fiber.new do
|
201
|
+
pending_sources.each(&:run_pending_keys)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
source_fiber
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|