Class: GraphQL::Dataloader::Source

Inherits:
Object
  • Object
show all
Defined in:
lib/graphql/dataloader/source.rb

Constant Summary collapse

MAX_ITERATIONS =
1000

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#dataloaderObject (readonly)

Returns the value of attribute dataloader.



18
19
20
# File 'lib/graphql/dataloader/source.rb', line 18

def dataloader
  @dataloader
end

#pendingObject (readonly)

Returns the value of attribute pending.



184
185
186
# File 'lib/graphql/dataloader/source.rb', line 184

def pending
  @pending
end

#resultsObject (readonly)

Returns the value of attribute results.



184
185
186
# File 'lib/graphql/dataloader/source.rb', line 184

def results
  @results
end

Class Method Details

.batch_key_for(*batch_args, **batch_kwargs) ⇒ Object

These arguments are given to dataloader.with(source_class, ...). The object returned from this method is used to de-duplicate batch loads under the hood by using it as a Hash key.

By default, the arguments are all put in an Array. To customize how this source’s batches are merged, override this method to return something else.

For example, if you pass ActiveRecord::Relations to .with(...), you could override this method to call .to_sql on them, thus merging .load(...) calls when they apply to equivalent relations.

Parameters:

  • batch_args (Array<Object>)
  • batch_kwargs (Hash)

Returns:

  • (Object)


173
174
175
# File 'lib/graphql/dataloader/source.rb', line 173

def self.batch_key_for(*batch_args, **batch_kwargs)
  [*batch_args, **batch_kwargs]
end

Instance Method Details

#clear_cachevoid

This method returns an undefined value.

Clear any already-loaded objects for this source



179
180
181
182
# File 'lib/graphql/dataloader/source.rb', line 179

def clear_cache
  @results.clear
  nil
end

#fetch(keys) ⇒ Array<Object>

Subclasses must implement this method to return a value for each of keys

Parameters:

Returns:

  • (Array<Object>)

    A loaded value for each of keys. The array must match one-for-one to the list of keys.



98
99
100
101
# File 'lib/graphql/dataloader/source.rb', line 98

def fetch(keys)
  # somehow retrieve these from the backend
  raise "Implement `#{self.class}#fetch(#{keys.inspect}) to return a record for each of the keys"
end

#load(value) ⇒ Object

Returns The result from #fetch for key. If key hasn’t been loaded yet, the Fiber will yield until it’s loaded.

Parameters:

  • value (Object)

    A loading value which will be passed to #fetch if it isn’t already in the internal cache.

Returns:

  • (Object)

    The result from #fetch for key. If key hasn’t been loaded yet, the Fiber will yield until it’s loaded.



63
64
65
66
67
68
69
70
71
72
# File 'lib/graphql/dataloader/source.rb', line 63

def load(value)
  result_key = result_key_for(value)
  if @results.key?(result_key)
    result_for(result_key)
  else
    @pending[result_key] ||= normalize_fetch_key(value)
    sync([result_key])
    result_for(result_key)
  end
end

#load_all(values) ⇒ Object

Returns The result from #fetch for keys. If keys haven’t been loaded yet, the Fiber will yield until they’re loaded.

Parameters:

  • values (Array<Object>)

    Loading keys which will be passed to #fetch (or read from the internal cache).

Returns:

  • (Object)

    The result from #fetch for keys. If keys haven’t been loaded yet, the Fiber will yield until they’re loaded.



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/graphql/dataloader/source.rb', line 76

def load_all(values)
  result_keys = []
  pending_keys = []
  values.each { |v|
    k = result_key_for(v)
    result_keys << k
    if !@results.key?(k)
      @pending[k] ||= normalize_fetch_key(v)
      pending_keys << k
    end
  }

  if !pending_keys.empty?
    sync(pending_keys)
  end

  result_keys.map { |k| result_for(k) }
end

#merge(new_results) ⇒ void

This method returns an undefined value.

Add these key-value pairs to this source’s cache (future loads will use these merged values).

Parameters:

  • new_results (Hash<Object => Object>)

    key-value pairs to cache in this source



129
130
131
132
133
134
135
# File 'lib/graphql/dataloader/source.rb', line 129

def merge(new_results)
  new_results.each do |new_k, new_v|
    key = result_key_for(new_k)
    @results[key] = new_v
  end
  nil
end

#normalize_fetch_key(value) ⇒ Object

Implement this method if varying values given to #load (etc) should be consolidated or normalized before being handed off to your #fetch implementation.

This is different than #result_key_for because that method handles unification inside Dataloader’s cache, but this method changes the value passed into #fetch.

Parameters:

Returns:

  • (Object)

    The value given to #fetch



46
47
48
# File 'lib/graphql/dataloader/source.rb', line 46

def normalize_fetch_key(value)
  value
end

#pending?Boolean

Returns True if this source has any pending requests for data.

Returns:

  • (Boolean)

    True if this source has any pending requests for data.



121
122
123
# File 'lib/graphql/dataloader/source.rb', line 121

def pending?
  !@pending.empty?
end

#request(value) ⇒ Dataloader::Request

Returns a pending request for a value from key. Call .load on that object to wait for the result.

Returns:

  • (Dataloader::Request)

    a pending request for a value from key. Call .load on that object to wait for the result.



21
22
23
24
25
26
27
# File 'lib/graphql/dataloader/source.rb', line 21

def request(value)
  res_key = result_key_for(value)
  if !@results.key?(res_key)
    @pending[res_key] ||= normalize_fetch_key(value)
  end
  Dataloader::Request.new(self, value)
end

#request_all(values) ⇒ Dataloader::Request

Returns a pending request for a values from keys. Call .load on that object to wait for the results.

Returns:

  • (Dataloader::Request)

    a pending request for a values from keys. Call .load on that object to wait for the results.



51
52
53
54
55
56
57
58
59
# File 'lib/graphql/dataloader/source.rb', line 51

def request_all(values)
  values.each do |v|
    res_key = result_key_for(v)
    if !@results.key?(res_key)
      @pending[res_key] ||= normalize_fetch_key(v)
    end
  end
  Dataloader::RequestAll.new(self, values)
end

#result_key_for(value) ⇒ Object

Implement this method to return a stable identifier if different key objects should load the same data value.

Parameters:

  • value (Object)

    A value passed to .request or .load, for which a value will be loaded

Returns:

  • (Object)

    The key for tracking this pending data



34
35
36
# File 'lib/graphql/dataloader/source.rb', line 34

def result_key_for(value)
  value
end

#run_pending_keysvoid

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Called by GraphQL::Dataloader to resolve and pending requests to this source.



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/graphql/dataloader/source.rb', line 140

def run_pending_keys
  if !@fetching.empty?
    @fetching.each_key { |k| @pending.delete(k) }
  end
  return if @pending.empty?
  fetch_h = @pending
  @pending = {}
  @fetching.merge!(fetch_h)
  results = fetch(fetch_h.values)
  fetch_h.each_with_index do |(key, _value), idx|
    @results[key] = results[idx]
  end
  nil
rescue StandardError => error
  fetch_h.each_key { |key| @results[key] = error }
ensure
  fetch_h && fetch_h.each_key { |k| @fetching.delete(k) }
end

#setup(dataloader) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Called by GraphQL::Dataloader to prepare the GraphQL::Dataloader::Source’s internal state



8
9
10
11
12
13
14
15
16
# File 'lib/graphql/dataloader/source.rb', line 8

def setup(dataloader)
  # These keys have been requested but haven't been fetched yet
  @pending = {}
  # These keys have been passed to `fetch` but haven't been finished yet
  @fetching = {}
  # { key => result }
  @results = {}
  @dataloader = dataloader
end

#sync(pending_result_keys) ⇒ void

This method returns an undefined value.

Wait for a batch, if there’s anything to batch. Then run the batch and update the cache.



107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/graphql/dataloader/source.rb', line 107

def sync(pending_result_keys)
  @dataloader.yield(self)
  iterations = 0
  while pending_result_keys.any? { |key| !@results.key?(key) }
    iterations += 1
    if iterations > MAX_ITERATIONS
      raise "#{self.class}#sync tried #{MAX_ITERATIONS} times to load pending keys (#{pending_result_keys}), but they still weren't loaded. There is likely a circular dependency#{@dataloader.fiber_limit ? " or `fiber_limit: #{@dataloader.fiber_limit}` is set too low" : ""}."
    end
    @dataloader.yield(self)
  end
  nil
end