graphql 2.6.3 → 2.6.10

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/field_extractor.rb +17 -1
  3. data/lib/generators/graphql/templates/schema.erb +2 -1
  4. data/lib/graphql/analysis/query_complexity.rb +2 -2
  5. data/lib/graphql/dashboard/application_controller.rb +5 -1
  6. data/lib/graphql/dashboard.rb +3 -0
  7. data/lib/graphql/dataloader/async_dataloader.rb +333 -68
  8. data/lib/graphql/dataloader/source.rb +32 -19
  9. data/lib/graphql/dataloader.rb +52 -36
  10. data/lib/graphql/execution/field_resolve_step.rb +79 -19
  11. data/lib/graphql/execution/finalize.rb +7 -8
  12. data/lib/graphql/execution/interpreter/runtime.rb +1 -8
  13. data/lib/graphql/execution/interpreter.rb +9 -1
  14. data/lib/graphql/execution/prepare_object_step.rb +8 -4
  15. data/lib/graphql/execution/runner.rb +17 -5
  16. data/lib/graphql/execution/selections_step.rb +10 -6
  17. data/lib/graphql/execution_error.rb +4 -0
  18. data/lib/graphql/float_decoding_error.rb +13 -0
  19. data/lib/graphql/float_encoding_error.rb +28 -0
  20. data/lib/graphql/language/block_string.rb +6 -11
  21. data/lib/graphql/language/cache.rb +84 -14
  22. data/lib/graphql/language/lexer.rb +15 -9
  23. data/lib/graphql/language/nodes.rb +2 -1
  24. data/lib/graphql/language.rb +3 -3
  25. data/lib/graphql/pagination/relation_connection.rb +10 -1
  26. data/lib/graphql/query/variable_validation_error.rb +16 -1
  27. data/lib/graphql/query/variables.rb +1 -1
  28. data/lib/graphql/railtie.rb +2 -1
  29. data/lib/graphql/schema/build_from_definition.rb +11 -3
  30. data/lib/graphql/schema/directive.rb +3 -0
  31. data/lib/graphql/schema/input_object.rb +2 -2
  32. data/lib/graphql/schema/resolver.rb +1 -1
  33. data/lib/graphql/schema/wrapper.rb +4 -0
  34. data/lib/graphql/schema.rb +5 -5
  35. data/lib/graphql/static_validation/rules/fields_will_merge.rb +66 -29
  36. data/lib/graphql/subscriptions/serialize.rb +10 -4
  37. data/lib/graphql/types/float.rb +18 -4
  38. data/lib/graphql/version.rb +1 -1
  39. data/lib/graphql.rb +2 -0
  40. metadata +4 -58
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 16abc9c2a5eda0da251dbe7e14433241bc7d038d2527926832ceb29336fb857a
4
- data.tar.gz: c89bd2a4b340ca30bfa7b823f51e4671972355d54bd56fdaed598c13a415c3b3
3
+ metadata.gz: a6c191887acba06a4030f962822e9106e9d3aa9d5550c8cd25ea76f343927f1b
4
+ data.tar.gz: b984ad9a61a391b4b8a42ade3bb9c63dc79ab2b3ee987845d655dc7038df1255
5
5
  SHA512:
6
- metadata.gz: 6452d2a517c502b4a8934582d060885a5f6462de244a1f331433f1076dc57a879cdd081e147f70925064a99607e0794fcf6eadad7a3f4a9111a5ab979c552554
7
- data.tar.gz: b6a0219606d905fc885ab2192596d1acb1d92bce407defc751bfafb1a7199e9c32275c1d3202f3246251e72b80554698b1f54920f76178f1ddc81b41d628bacd
6
+ metadata.gz: 7deac13ce0bbc579609e28575a309caa46086196f1381cfc5fbd6a5770b2059e123cb047cc835c573ad76be1df264a3b0d52d8692dbf161ad0d0248f8cfed2a3
7
+ data.tar.gz: 1362c0867ad8bbbe086b1999c710f447de6ea16b53b8a05a83dc39cba6c7b7d8d5cccdc388a3dff2f42c1b5135ff59a4c78dd2d53badd34b28718a73aa331b92
@@ -6,7 +6,23 @@ module Graphql
6
6
  module FieldExtractor
7
7
  def fields
8
8
  columns = []
9
- columns += (klass&.columns&.map { |c| generate_column_string(c) } || [])
9
+ if (model_columns = klass&.columns)
10
+ filter = if defined?(ActiveSupport::ParameterFilter)
11
+ fp = if defined?(Rails) && Rails.application && (app_config = Rails.application.config.filter_parameters).present? && !app_config.empty?
12
+ app_config
13
+ elsif ActiveSupport.respond_to?(:filter_parameters)
14
+ ActiveSupport.filter_parameters
15
+ else
16
+ []
17
+ end
18
+ ActiveSupport::ParameterFilter.new(fp, mask: nil)
19
+ else
20
+ nil
21
+ end
22
+ columns += model_columns
23
+ .select { |c| filter ? filter.filter_param(c.name, c.name) : true }
24
+ .map { |c| generate_column_string(c) }
25
+ end
10
26
  columns + custom_fields
11
27
  end
12
28
 
@@ -26,7 +26,8 @@ class <%= schema_name %> < GraphQL::Schema
26
26
  raise(GraphQL::RequiredImplementationMissingError)
27
27
  end
28
28
 
29
- # Limit the size of incoming queries:
29
+ # Limit the depth and size of incoming queries:
30
+ max_depth(15)
30
31
  max_query_string_tokens(5000)
31
32
 
32
33
  # Stop validating when it encounters this many errors:
@@ -29,14 +29,14 @@ module GraphQL
29
29
  future_complexity
30
30
  end
31
31
  when nil
32
- subject.logger.warn <<~GRAPHQL
32
+ subject.logger.warn <<~MESSAGE
33
33
  GraphQL-Ruby's complexity cost system is getting some "breaking fixes" in a future version. See the migration notes at https://graphql-ruby.org/api-doc/#{GraphQL::VERSION}/GraphQL/Schema.html#complexity_cost_calculation_mode_for-class_method
34
34
 
35
35
  To opt into the future behavior, configure your schema (#{subject.schema.name ? subject.schema.name : subject.schema.ancestors}) with:
36
36
 
37
37
  complexity_cost_calculation_mode(:future) # or `:legacy`, `:compare`
38
38
 
39
- GRAPHQL
39
+ MESSAGE
40
40
  max_possible_complexity(mode: :legacy)
41
41
  else
42
42
  raise ArgumentError, "Expected `:future`, `:legacy`, `:compare`, or `nil` from `#{query.schema}.complexity_cost_calculation_mode_for` but got: #{query.schema.complexity_cost_calculation_mode.inspect}"
@@ -22,7 +22,11 @@ module Graphql
22
22
 
23
23
  def schema_class
24
24
  @schema_class ||= begin
25
- schema_param = request.query_parameters["schema"] || params[:schema]
25
+ configured_schemas = Array(request.path_parameters[:schema] || request.path_parameters["schema"])
26
+ schema_param = request.query_parameters["schema"]
27
+ schema_param = configured_schemas.find { |schema| schema.to_s == schema_param } if schema_param
28
+ schema_param ||= configured_schemas.first
29
+
26
30
  case schema_param
27
31
  when Class
28
32
  schema_param
@@ -10,6 +10,9 @@ module Graphql
10
10
  # @example Mounting the Dashboard in your app
11
11
  # mount GraphQL::Dashboard, at: "graphql_dashboard", schema: "MySchema"
12
12
  #
13
+ # Pass an array to allow selecting from multiple schemas with the `schema` query parameter.
14
+ # mount GraphQL::Dashboard, at: "graphql_dashboard", schema: ["MySchema", "OtherSchema"]
15
+ #
13
16
  # @example Authenticating the Dashboard with HTTP Basic Auth
14
17
  # # config/initializers/graphql_dashboard.rb
15
18
  # GraphQL::Dashboard.middleware.use(Rack::Auth::Basic) do |username, password|
@@ -1,110 +1,375 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GraphQL
3
4
  class Dataloader
4
5
  class AsyncDataloader < Dataloader
6
+ def self.use(...)
7
+ install_graphql_methods
8
+ super
9
+ end
10
+
11
+ def self.install_graphql_methods
12
+ if !Async::Task.method_defined?(:cancel)
13
+ Async::Task.alias_method(:cancel, :stop)
14
+ end
15
+ if !Async::Task.method_defined?(:graphql_async_dataloader_run)
16
+ Async::Task.attr_accessor(:graphql_async_dataloader_run)
17
+ Async::Task.attr_accessor(:graphql_async_dataloader_condition)
18
+ end
19
+ end
20
+
21
+ def initialize(...)
22
+ super
23
+ create_pending_run
24
+ end
25
+
26
+ # @api private
27
+ attr_reader :pending_sources
28
+
29
+ def create_pending_run
30
+ jobs_fiber_limit, total_fiber_limit = calculate_fiber_limit
31
+ @pending_run = Run.new(self, total_fiber_limit, jobs_fiber_limit)
32
+ end
33
+
5
34
  def yield(source = Fiber[:__graphql_current_dataloader_source])
6
- trace = Fiber[:__graphql_current_multiplex]&.current_trace
35
+ task = Async::Task.current
36
+ run = task.graphql_async_dataloader_run
37
+ trace = run.trace
7
38
  trace&.dataloader_fiber_yield(source)
8
- if (condition = Fiber[:graphql_dataloader_next_tick])
9
- condition.wait
10
- else
11
- Fiber.yield
39
+ if !run.push_task_message(:paused_task, task)
40
+ task.stop
41
+ end
42
+ condition = task.graphql_async_dataloader_condition
43
+ condition.wait
44
+ if !run.push_task_message(:resumed_task, task)
45
+ task.stop
12
46
  end
13
47
  trace&.dataloader_fiber_resume(source)
14
48
  nil
15
49
  end
16
50
 
17
- def run(trace_query_lazy: nil)
18
- trace = Fiber[:__graphql_current_multiplex]&.current_trace
19
- jobs_fiber_limit, total_fiber_limit = calculate_fiber_limit
20
- job_fibers = []
21
- next_job_fibers = []
22
- source_tasks = []
23
- next_source_tasks = []
24
- first_pass = true
25
- sources_condition = Async::Condition.new
26
- manager = spawn_fiber do
27
- trace&.begin_dataloader(self)
28
- while first_pass || !job_fibers.empty?
29
- first_pass = false
30
- fiber_vars = get_fiber_variables
31
-
32
- run_pending_steps(job_fibers, next_job_fibers, source_tasks, jobs_fiber_limit, trace)
33
-
34
- Sync do |root_task|
35
- set_fiber_variables(fiber_vars)
36
- while !source_tasks.empty? || @source_cache.each_value.any? { |group_sources| group_sources.each_value.any?(&:pending?) }
37
- while (task = (source_tasks.shift || (((job_fibers.size + next_job_fibers.size + source_tasks.size + next_source_tasks.size) < total_fiber_limit) && spawn_source_task(root_task, sources_condition, trace))))
38
- if task.alive?
39
- root_task.yield # give the source task a chance to run
40
- next_source_tasks << task
41
- end
51
+ class Run
52
+ def initialize(dataloader, total_fiber_limit, jobs_fiber_limit)
53
+ @dataloader = dataloader
54
+ @root_task = nil
55
+ @trace = nil
56
+ @jobs = []
57
+
58
+ @total_fiber_limit = total_fiber_limit
59
+ @jobs_fiber_limit = jobs_fiber_limit
60
+ @lazies_at_depth = Hash.new { |h, k| h[k] = [] }
61
+
62
+ @running_tasks = nil
63
+ @tasks_channel = nil
64
+ @tasks_channel_task = nil
65
+ @activity = nil
66
+ @task_error = nil
67
+ @expected_resumes = 0
68
+ @mode = nil
69
+
70
+ @snoozed_jobs_condition = Async::Condition.new
71
+ @snoozed_sources_condition = Async::Condition.new
72
+ end
73
+
74
+ attr_accessor :trace, :root_task
75
+
76
+ attr_reader :dataloader, :jobs, :lazies_at_depth, :jobs_fiber_limit, :snoozed_jobs_condition, :snoozed_sources_condition
77
+
78
+ def jobs_bandwidth?
79
+ running_count < @jobs_fiber_limit
80
+ end
81
+
82
+ def sources_bandwidth?
83
+ running_count < current_sources_fiber_limit
84
+ end
85
+
86
+ def close_queues
87
+ @tasks_channel.close
88
+ @tasks_channel_task.cancel
89
+ end
90
+
91
+ # Push to the tasks_channel, tolerating a closed channel: on the error path, `run_queue`
92
+ # closes the channel while sibling tasks can still run one more slice before
93
+ # `root_task.cancel` reaches them. Record `:task_error` payloads so they aren't lost, and
94
+ # return false so the caller can stop the task instead of raising `ClosedError` into user code.
95
+ def push_task_message(msg, data)
96
+ @tasks_channel.push([msg, data])
97
+ true
98
+ rescue Async::Queue::ClosedError
99
+ if msg == :task_error
100
+ @task_error ||= data
101
+ end
102
+ false
103
+ end
104
+
105
+ def wait_for_activity
106
+ @activity.wait
107
+ end
108
+
109
+ def quiesced?
110
+ @running_tasks.empty? && @tasks_channel.empty? && @expected_resumes == 0
111
+ end
112
+
113
+ def has_pending_work?
114
+ @mode == :jobs ? @jobs.any? : @dataloader.pending_sources.any?(&:pending?) # rubocop:disable Development/NoneWithoutBlockCop
115
+ end
116
+
117
+ def has_bandwidth?
118
+ @mode == :jobs ? jobs_bandwidth? : sources_bandwidth?
119
+ end
120
+
121
+ # Signalled tasks don't appear in any accounting until their first slice
122
+ # pushes `:resumed_task`, so they have to be counted at signal time:
123
+ def expect_resumes(count)
124
+ @expected_resumes = count
125
+ end
126
+
127
+ def check_error!
128
+ if (err = @task_error)
129
+ @task_error = nil
130
+ raise err
131
+ end
132
+ end
133
+
134
+ def new_queues(mode)
135
+ @mode = mode
136
+ @tasks_channel = Async::Queue.new(parent: @root_task)
137
+ @activity = Async::Condition.new
138
+ @task_error = nil
139
+ @expected_resumes = 0
140
+ @running_tasks = []
141
+ @tasks_channel_task = @root_task.async do |_t|
142
+ while ((msg, data) = @tasks_channel.wait)
143
+ case msg
144
+ when :started_task
145
+ @running_tasks.push(data)
146
+ data.run
147
+ when :resumed_task
148
+ if @expected_resumes > 0
149
+ @expected_resumes -= 1
42
150
  end
43
- sources_condition.signal
44
- source_tasks.concat(next_source_tasks)
45
- next_source_tasks.clear
151
+ @running_tasks.push(data)
152
+ when :finished_task, :paused_task
153
+ @running_tasks.delete(data)
154
+ when :task_error
155
+ @task_error ||= data
156
+ else
157
+ raise ArgumentError, "Unknown tasks_channel action: #{msg.inspect}"
46
158
  end
159
+ @activity.signal
47
160
  end
161
+ end
162
+ end
48
163
 
49
- if !@lazies_at_depth.empty?
50
- with_trace_query_lazy(trace_query_lazy) do
51
- run_next_pending_lazies(job_fibers, trace)
52
- run_pending_steps(job_fibers, next_job_fibers, source_tasks, jobs_fiber_limit, trace)
53
- end
164
+ def running?
165
+ @snoozed_jobs_condition.waiting? || @snoozed_sources_condition.waiting?
166
+ end
167
+
168
+ def current_sources_fiber_limit
169
+ within_limit = @total_fiber_limit - running_count
170
+ if within_limit < 1
171
+ 1
172
+ else
173
+ within_limit
174
+ end
175
+ end
176
+
177
+ private
178
+
179
+ def running_count
180
+ @snoozed_jobs_condition.instance_variable_get(:@ready).num_waiting +
181
+ @snoozed_sources_condition.instance_variable_get(:@ready).num_waiting +
182
+ (@running_tasks&.size || 0)
183
+ end
184
+ end
185
+
186
+ def append_job(callable = nil, &block)
187
+ active_run.jobs.push(callable || block)
188
+ nil
189
+ end
190
+
191
+ def lazy_at_depth(depth, lazy)
192
+ active_run.lazies_at_depth[depth] << lazy
193
+ end
194
+
195
+ def active_run
196
+ @pending_run || current_task_run || raise(GraphQL::Error, "No available Run to append to, GraphQL-Ruby bug")
197
+ end
198
+
199
+ # The current task's run, but only if it belongs to this dataloader. A different
200
+ # dataloader may be running inside one of our tasks (or vice versa), e.g. a query
201
+ # executed from a resolver or a subscription trigger; its run must not be reused.
202
+ def current_task_run
203
+ run = Async::Task.current?&.graphql_async_dataloader_run
204
+ run if run&.dataloader.equal?(self)
205
+ end
206
+
207
+ def run_isolated
208
+ previous_run = current_task_run
209
+ prev_pending_keys = {}
210
+ # Clear pending loads but keep already-cached records
211
+ # in case they are useful to the given block.
212
+ @source_cache.each do |source_class, batched_sources|
213
+ batched_sources.each do |batch_args, batched_source_instance|
214
+ if batched_source_instance.pending?
215
+ prev_pending_keys[batched_source_instance] = batched_source_instance.pending.dup
216
+ batched_source_instance.pending.clear
54
217
  end
55
218
  end
56
- trace&.end_dataloader(self)
57
219
  end
58
220
 
59
- manager.resume
60
- if manager.alive?
61
- raise "Invariant: Manager didn't terminate successfully: #{manager}"
221
+ res = nil
222
+ create_pending_run
223
+ @pending_run.jobs << -> { res = yield }
224
+ run
225
+ res
226
+ ensure
227
+ if previous_run
228
+ Async::Task.current.graphql_async_dataloader_run = previous_run
229
+ # clear the one created in #run:
230
+ @pending_run = nil
62
231
  end
232
+ prev_pending_keys.each do |source_instance, pending|
233
+ pending.each do |key, value|
234
+ next if source_instance.results.key?(key)
235
+
236
+ queue_pending_source(source_instance) if source_instance.pending.empty?
237
+ source_instance.pending[key] = value
238
+ end
239
+ end
240
+ end
241
+
242
+ def run(trace_query_lazy: nil)
243
+ trace = Fiber[:__graphql_current_multiplex]&.current_trace
244
+ run = @pending_run || current_task_run || raise(GraphQL::Error, "No available Run, GraphQL-Ruby internal bug")
245
+ @pending_run = nil
246
+ run.trace = trace
247
+ first_pass = true
248
+ trace&.begin_dataloader(self)
249
+ fiber_vars = get_fiber_variables
250
+ raised_error = nil
251
+ jobs = run.jobs
252
+ Sync do |_maybe_new_task|
253
+ # Make sure there's a new task instance to hold `.graphql_...` state:
254
+ task = Async::Task.new do |root_task|
255
+ run.root_task = root_task
256
+ root_task.graphql_async_dataloader_run = run
257
+ set_fiber_variables(fiber_vars)
258
+
259
+ while first_pass || run.running? || !jobs.empty?
260
+ first_pass = false
261
+ run_queue(run, run.snoozed_jobs_condition, :jobs)
262
+ run_queue(run, run.snoozed_sources_condition, :sources)
63
263
 
264
+ if !run.lazies_at_depth.empty?
265
+ with_trace_query_lazy(trace_query_lazy) do
266
+ if enqueue_next_pending_lazies(run.lazies_at_depth)
267
+ run_queue(run, run.snoozed_jobs_condition, :jobs)
268
+ end
269
+ end
270
+ end
271
+ end
272
+ rescue StandardError => err
273
+ raised_error = err
274
+ root_task.cancel
275
+ end
276
+
277
+ task.run
278
+ task.wait
279
+ end
280
+ create_pending_run
281
+ if raised_error
282
+ raise raised_error
283
+ end
284
+ trace&.end_dataloader(self)
64
285
  rescue UncaughtThrowError => e
65
286
  throw e.tag, e.value
66
287
  end
67
288
 
68
289
  private
69
290
 
70
- def run_pending_steps(job_fibers, next_job_fibers, source_tasks, jobs_fiber_limit, trace)
71
- while (f = (job_fibers.shift || (((job_fibers.size + next_job_fibers.size + source_tasks.size) < jobs_fiber_limit) && spawn_job_fiber(trace))))
72
- if f.alive?
73
- finished = run_fiber(f)
74
- if !finished
75
- next_job_fibers << f
291
+ def run_queue(run, condition, mode)
292
+ opened_queues = false
293
+
294
+ if condition.waiting?
295
+ opened_queues = true
296
+ run.new_queues(mode)
297
+ run.expect_resumes(condition.instance_variable_get(:@ready).num_waiting)
298
+ condition.signal
299
+ end
300
+
301
+ loop do
302
+ pending_work = (mode == :jobs) ? (!run.jobs.empty? && run.jobs_bandwidth? ? run.jobs : nil) : (drain_pending_sources)
303
+ if pending_work
304
+ if opened_queues == false
305
+ opened_queues = true
306
+ run.new_queues(mode)
307
+ end
308
+ num_tasks = mode == :sources ? run.current_sources_fiber_limit : 1
309
+ if num_tasks > pending_work.size
310
+ num_tasks = pending_work.size
76
311
  end
312
+ spawn_tasks(run, mode, condition, pending_work, num_tasks)
77
313
  end
78
- end
79
- job_fibers.concat(next_job_fibers)
80
- next_job_fibers.clear
81
- end
82
314
 
83
- def spawn_source_task(parent_task, condition, trace)
84
- pending_sources = nil
85
- @source_cache.each_value do |source_by_batch_params|
86
- source_by_batch_params.each_value do |source|
87
- if source.pending?
88
- pending_sources ||= []
89
- pending_sources << source
315
+ if !opened_queues
316
+ break
317
+ end
318
+
319
+ run.check_error!
320
+
321
+ if run.quiesced?
322
+ if !run.has_pending_work? || !run.has_bandwidth?
323
+ break
90
324
  end
325
+ # Quiesced, but more work appeared - loop around to drain it.
326
+ else
327
+ run.wait_for_activity
91
328
  end
92
329
  end
330
+ ensure
331
+ if opened_queues
332
+ run.close_queues
333
+ end
334
+ end
93
335
 
94
- if pending_sources
95
- fiber_vars = get_fiber_variables
96
- parent_task.async do
97
- trace&.dataloader_spawn_source_fiber(pending_sources)
336
+ # Use a separate method for this so that the outer loop's reassignment of `pending_work`
337
+ # doesn't affect already-running tasks which (would) close over that variable
338
+ def spawn_tasks(run, mode, condition, pending_work, num_tasks)
339
+ fiber_vars = get_fiber_variables
340
+ trace = run.trace
341
+ num_tasks.times do
342
+ new_task = Async::Task.new(run.root_task) do |task|
343
+ task.graphql_async_dataloader_run = run
344
+ task.graphql_async_dataloader_condition = condition
98
345
  set_fiber_variables(fiber_vars)
99
- Fiber[:graphql_dataloader_next_tick] = condition
100
- pending_sources.each do |s|
101
- trace&.begin_dataloader_source(s)
102
- s.run_pending_keys
103
- trace&.end_dataloader_source(s)
346
+ case mode
347
+ when :jobs
348
+ trace&.dataloader_spawn_execution_fiber(pending_work)
349
+ while job = pending_work.shift
350
+ job.call
351
+ end
352
+ when :sources
353
+ trace&.dataloader_spawn_source_fiber(pending_work)
354
+ while (source = pending_work.shift)
355
+ Fiber[:__graphql_current_dataloader_source] = source
356
+ trace&.begin_dataloader_source(source)
357
+ source.run_pending_keys
358
+ trace&.end_dataloader_source(source)
359
+ end
360
+ else
361
+ raise ArgumentError, "Unknown mode: #{mode.inspect}"
104
362
  end
363
+ nil
364
+ rescue StandardError => err
365
+ run.push_task_message(:task_error, err)
366
+ else
367
+ run.push_task_message(:finished_task, task)
368
+ ensure
105
369
  cleanup_fiber
106
370
  trace&.dataloader_fiber_exit
107
371
  end
372
+ run.push_task_message(:started_task, new_task)
108
373
  end
109
374
  end
110
375
  end
@@ -20,9 +20,7 @@ module GraphQL
20
20
  # @return [Dataloader::Request] a pending request for a value from `key`. Call `.load` on that object to wait for the result.
21
21
  def request(value)
22
22
  res_key = result_key_for(value)
23
- if !@results.key?(res_key)
24
- @pending[res_key] ||= normalize_fetch_key(value)
25
- end
23
+ add_pending_key(res_key, value)
26
24
  Dataloader::Request.new(self, value)
27
25
  end
28
26
 
@@ -51,9 +49,7 @@ module GraphQL
51
49
  def request_all(values)
52
50
  values.each do |v|
53
51
  res_key = result_key_for(v)
54
- if !@results.key?(res_key)
55
- @pending[res_key] ||= normalize_fetch_key(v)
56
- end
52
+ add_pending_key(res_key, v)
57
53
  end
58
54
  Dataloader::RequestAll.new(self, values)
59
55
  end
@@ -65,7 +61,7 @@ module GraphQL
65
61
  if @results.key?(result_key)
66
62
  result_for(result_key)
67
63
  else
68
- @pending[result_key] ||= normalize_fetch_key(value)
64
+ add_pending_key(result_key, value)
69
65
  sync([result_key])
70
66
  result_for(result_key)
71
67
  end
@@ -79,8 +75,7 @@ module GraphQL
79
75
  values.each { |v|
80
76
  k = result_key_for(v)
81
77
  result_keys << k
82
- if !@results.key?(k)
83
- @pending[k] ||= normalize_fetch_key(v)
78
+ if add_pending_key(k, v)
84
79
  pending_keys << k
85
80
  end
86
81
  }
@@ -89,7 +84,8 @@ module GraphQL
89
84
  sync(pending_keys)
90
85
  end
91
86
 
92
- result_keys.map { |k| result_for(k) }
87
+ result_keys.map! { |k| result_for(k) }
88
+ result_keys
93
89
  end
94
90
 
95
91
  # Subclasses must implement this method to return a value for each of `keys`
@@ -105,6 +101,7 @@ module GraphQL
105
101
  # Then run the batch and update the cache.
106
102
  # @return [void]
107
103
  def sync(pending_result_keys)
104
+ @dataloader.queue_pending_source(self) if pending?
108
105
  @dataloader.yield(self)
109
106
  iterations = 0
110
107
  while pending_result_keys.any? { |key| !@results.key?(key) }
@@ -138,22 +135,25 @@ module GraphQL
138
135
  # @api private
139
136
  # @return [void]
140
137
  def run_pending_keys
141
- if !@fetching.empty?
142
- @fetching.each_key { |k| @pending.delete(k) }
143
- end
138
+ @fetching.each_key { |k| @pending.delete(k) }
144
139
  return if @pending.empty?
145
140
  fetch_h = @pending
146
- @pending = {}
147
141
  @fetching.merge!(fetch_h)
142
+ @pending = {}
148
143
  results = fetch(fetch_h.values)
149
- fetch_h.each_with_index do |(key, _value), idx|
144
+ idx = 0
145
+
146
+ fetch_h.each_key do |key|
150
147
  @results[key] = results[idx]
148
+ @fetching.delete(key)
149
+ idx += 1
151
150
  end
152
151
  nil
153
152
  rescue StandardError => error
154
- fetch_h.each_key { |key| @results[key] = error }
155
- ensure
156
- fetch_h && fetch_h.each_key { |k| @fetching.delete(k) }
153
+ fetch_h.each_key { |key|
154
+ @results[key] = error
155
+ @fetching.delete(key)
156
+ }
157
157
  end
158
158
 
159
159
  # These arguments are given to `dataloader.with(source_class, ...)`. The object
@@ -171,7 +171,11 @@ module GraphQL
171
171
  # @param batch_kwargs [Hash]
172
172
  # @return [Object]
173
173
  def self.batch_key_for(*batch_args, **batch_kwargs)
174
- [*batch_args, **batch_kwargs]
174
+ if batch_kwargs.any? # rubocop:disable Development/NoneWithoutBlockCop
175
+ [*batch_args, **batch_kwargs]
176
+ else
177
+ batch_args
178
+ end
175
179
  end
176
180
 
177
181
  # Clear any already-loaded objects for this source
@@ -185,6 +189,15 @@ module GraphQL
185
189
 
186
190
  private
187
191
 
192
+ def add_pending_key(result_key, value)
193
+ return false if @results.key?(result_key)
194
+
195
+ was_empty = @pending.empty?
196
+ @pending[result_key] ||= normalize_fetch_key(value)
197
+ @dataloader.queue_pending_source(self) if was_empty
198
+ true
199
+ end
200
+
188
201
  # Reads and returns the result for the key from the internal cache, or raises an error if the result was an error
189
202
  # @param key [Object] key passed to {#load} or {#load_all}
190
203
  # @return [Object] The result from {#fetch} for `key`.