graphiti 1.13.3 → 1.13.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f75003b9d958879d44777f39f94e8221224b3826d80a8fe6e420e510ea4dd163
4
- data.tar.gz: 0d0dda07deaefcc5fdfc7c91f44b3b7677b82ed950770ddb08cf24d49ea5e10e
3
+ metadata.gz: 7f23c59b6750dc9a8dcaefaba6d6250a9deac2676c33b01930b0c715767b85a1
4
+ data.tar.gz: 2252b0e870042723a30af9844d6006d8587f14228d82bc3eafd36c4b72b2fc8c
5
5
  SHA512:
6
- metadata.gz: 57117fb28dc6f0434c91b57e8b7960caeb5bdb33d2d2768009138ec66f10d7d057c472c696e07debeb4af149e5de4ddfcffb30741b473810d4b589a7ac4adc8e
7
- data.tar.gz: 394c4e38acfc68b78c966ecf3cb7dbd73a5038a5163c8c72045e185eb02a9ed4f919c20ee2598b015cfc27f04b749d1fc7f5b306856befd13d006cb81d0a73af
6
+ metadata.gz: '0579733a14742e30623cb686c0ba0c925f34458501ee6a7b322da2a6aa0fe7a85a4ccc422bf969858ed5eae306e65f3c2fb4e554995db51b7437910bd46237b6'
7
+ data.tar.gz: aa3fcd0b1e671594e23bb006d126e55366633038b48c53332940f1ba5cfbf4782995cd4556867ad707bb9b13440d045208cc51b66d24b2bc070019d9eb9e108b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  graphiti changelog
2
2
 
3
+ ## [1.13.4](https://github.com/graphiti-api/graphiti/compare/v1.13.3...v1.13.4) (2026-08-20)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * resolve inline when already on a sideload pool thread ([2757dde](https://github.com/graphiti-api/graphiti/commit/2757ddee5728224540c7a3bfa33583fe71c64703))
9
+ * scope debugger chunks to the request rather than the class ([be1d833](https://github.com/graphiti-api/graphiti/commit/be1d83346e4303eaa700c0cbda6b4c60ddf587e7))
10
+
3
11
  ## [1.13.3](https://github.com/graphiti-api/graphiti/compare/v1.13.2...v1.13.3) (2026-08-09)
4
12
 
5
13
 
@@ -4,11 +4,22 @@
4
4
  module Graphiti
5
5
  class Debugger
6
6
  class << self
7
- attr_accessor :enabled, :chunks, :debug_models, :preserve, :pry
7
+ attr_accessor :enabled, :debug_models, :preserve, :pry
8
8
  end
9
- self.chunks = []
9
+
10
+ CHUNKS = :__graphiti_debugger_chunks
11
+ private_constant :CHUNKS
10
12
 
11
13
  class << self
14
+ # Pool threads inherit this through the thread-local copy Scope#future_with_context makes, so their chunks reach the right request.
15
+ def chunks
16
+ Thread.current[CHUNKS] ||= Concurrent::Array.new
17
+ end
18
+
19
+ def chunks=(value)
20
+ Thread.current[CHUNKS] = value
21
+ end
22
+
12
23
  def on_data(name, start, stop, id, payload)
13
24
  return [] unless enabled
14
25
 
@@ -179,14 +190,15 @@ module Graphiti
179
190
  end
180
191
 
181
192
  def graph_statements
182
- @chunks.each do |chunk|
193
+ statements = chunks
194
+ statements.each do |chunk|
183
195
  if (parent = chunk[:parent])
184
- relevant = chunks.find { |c| c[:resource] == parent }
196
+ relevant = statements.find { |c| c[:resource] == parent }
185
197
  relevant[:children].unshift(chunk) if relevant
186
198
  end
187
199
  end
188
- @chunks.reject! { |c| !!c[:parent] }
189
- @chunks
200
+ statements.reject! { |c| !!c[:parent] }
201
+ statements
190
202
  end
191
203
 
192
204
  def chunk_to_hash(chunk)
@@ -31,6 +31,28 @@ module Graphiti
31
31
  end
32
32
  end
33
33
 
34
+ POOL_THREAD = :__graphiti_pool_thread
35
+ private_constant :POOL_THREAD
36
+
37
+ # A pool thread that waits on the pool deadlocks, since the task it waits for cannot start until the waiting thread frees its slot.
38
+ def self.resolve_synchronously?
39
+ !Graphiti.config.concurrency || on_pool_thread?
40
+ end
41
+
42
+ # TODO: move to Fiber[] once the floor is Ruby 3.2
43
+ def self.on_pool_thread?
44
+ Thread.current[POOL_THREAD] == true
45
+ end
46
+
47
+ # Restores rather than clears because :caller_runs may have run the task on a request thread.
48
+ def self.marking_pool_thread
49
+ previous = Thread.current[POOL_THREAD]
50
+ Thread.current[POOL_THREAD] = true
51
+ yield
52
+ ensure
53
+ Thread.current[POOL_THREAD] = previous
54
+ end
55
+
34
56
  def initialize(object, resource, query, opts = {})
35
57
  @object = object
36
58
  @resource = resource
@@ -48,29 +70,25 @@ module Graphiti
48
70
  # Thread/Fiber storage snapshots, and Rails executor wrappers on every
49
71
  # request purely to drive a thread pool that is intentionally synchronous.
50
72
  # See https://github.com/graphiti-api/graphiti/issues/505
51
- return sync_resolve(&blk) unless Graphiti.config.concurrency
52
-
53
- future_resolve.value!
73
+ if self.class.resolve_synchronously?
74
+ sync_resolve(&blk)
75
+ else
76
+ future_resolve(&blk).value!
77
+ end
54
78
  end
55
79
 
56
80
  def resolve_sideloads(results)
57
- return sync_resolve_sideloads(results) unless Graphiti.config.concurrency
58
-
59
- future_resolve_sideloads(results).value!
81
+ if self.class.resolve_synchronously?
82
+ sync_resolve_sideloads(results)
83
+ else
84
+ future_resolve_sideloads(results).value!
85
+ end
60
86
  end
61
87
 
62
- def future_resolve
88
+ def future_resolve(&blk)
63
89
  return Concurrent::Promises.fulfilled_future([], self.class.global_thread_pool_executor) if @query.zero_results?
64
90
 
65
- resolved = broadcast_data { |payload|
66
- @object = @resource.before_resolve(@object, @query)
67
- payload[:results] = @resource.resolve(@object)
68
- payload[:results]
69
- }
70
- resolved.compact!
71
- assign_serializer(resolved)
72
- yield resolved if block_given?
73
- @opts[:after_resolve]&.call(resolved)
91
+ resolved = resolve_primary_data(&blk)
74
92
  sideloaded = @query.parents.any?
75
93
  close_adapter = Graphiti.config.concurrency && sideloaded
76
94
  if close_adapter
@@ -122,12 +140,29 @@ module Graphiti
122
140
 
123
141
  private
124
142
 
125
- # Synchronous counterpart to #future_resolve, used when concurrency is off.
126
- # Resolves the resource and its sideloads inline without any promise
127
- # machinery. See #resolve.
128
- def sync_resolve
143
+ def sync_resolve(&blk)
129
144
  return [] if @query.zero_results?
130
145
 
146
+ resolved = resolve_primary_data(&blk)
147
+ sync_resolve_sideloads(resolved)
148
+ resolved
149
+ end
150
+
151
+ def sync_resolve_sideloads(results)
152
+ return if results == []
153
+
154
+ each_applicable_sideload do |sideload, sideload_query|
155
+ Graphiti.config.before_sideload&.call(Graphiti.context)
156
+ sideload.resolve(results, sideload_query, @resource)
157
+ end
158
+
159
+ # resolve_sideloads is public, and without this it would return @query.sideloads itself,
160
+ # where a delete would silently drop that sideload from the cache key.
161
+ nil
162
+ end
163
+
164
+ # Runs inline on the calling thread in both the sync and future paths.
165
+ def resolve_primary_data
131
166
  resolved = broadcast_data { |payload|
132
167
  @object = @resource.before_resolve(@object, @query)
133
168
  payload[:results] = @resource.resolve(@object)
@@ -137,40 +172,28 @@ module Graphiti
137
172
  assign_serializer(resolved)
138
173
  yield resolved if block_given?
139
174
  @opts[:after_resolve]&.call(resolved)
140
- sync_resolve_sideloads(resolved) unless @query.sideloads.empty?
141
175
  resolved
142
176
  end
143
177
 
144
- # Synchronous counterpart to #future_resolve_sideloads, used when
145
- # concurrency is off. Resolves each sideload inline. See #resolve_sideloads.
146
- def sync_resolve_sideloads(results)
147
- return if results == []
148
-
149
- @query.sideloads.each_pair do |name, q|
178
+ def each_applicable_sideload
179
+ @query.sideloads.each_pair do |name, sideload_query|
150
180
  sideload = @resource.class.sideload(name)
151
181
  next if sideload.nil? || sideload.shared_remote?
152
182
 
153
- Graphiti.config.before_sideload&.call(Graphiti.context)
154
- sideload.resolve(results, q, @resource)
183
+ yield sideload, sideload_query
155
184
  end
156
-
157
- # Match pre-1.8 semantics: the non-concurrent resolve_sideloads returned
158
- # nil (not the sideloads Hash). Callers don't rely on the return value.
159
- nil
160
185
  end
161
186
 
162
187
  def future_resolve_sideloads(results)
163
188
  return Concurrent::Promises.fulfilled_future(nil, self.class.global_thread_pool_executor) if results == []
164
189
 
165
- sideload_promises = @query.sideloads.filter_map do |name, q|
166
- sideload = @resource.class.sideload(name)
167
- next if sideload.nil? || sideload.shared_remote?
168
-
169
- p = future_with_context(results, q, @resource) do |parent_results, sideload_query, parent_resource|
190
+ sideload_promises = []
191
+ each_applicable_sideload do |sideload, sideload_query|
192
+ promise = future_with_context(results, sideload_query, @resource) do |parent_results, future_query, parent_resource|
170
193
  Graphiti.config.before_sideload&.call(Graphiti.context)
171
- sideload.future_resolve(parent_results, sideload_query, parent_resource)
194
+ sideload.future_resolve(parent_results, future_query, parent_resource)
172
195
  end
173
- p.flat
196
+ sideload_promises << promise.flat
174
197
  end
175
198
 
176
199
  Concurrent::Promises.zip_futures_on(self.class.global_thread_pool_executor, *sideload_promises)
@@ -181,7 +204,10 @@ module Graphiti
181
204
  end
182
205
 
183
206
  def future_with_context(*args)
207
+ # TODO: we only need Fiber.storage after Ruby 3.2 is the floor
184
208
  thread_storage = Thread.current.keys.each_with_object({}) do |key, memo|
209
+ next if key == POOL_THREAD
210
+
185
211
  memo[key] = Thread.current[key]
186
212
  end
187
213
  fiber_storage =
@@ -194,11 +220,13 @@ module Graphiti
194
220
  Concurrent::Promises.future_on(
195
221
  self.class.global_thread_pool_executor, Thread.current.object_id, thread_storage, fiber_storage, *args
196
222
  ) do |thread_id, thread_storage, fiber_storage, *args|
197
- wrap_in_rails_executor do
198
- with_thread_locals(thread_storage) do
199
- with_fiber_locals(fiber_storage) do
200
- Graphiti.broadcast(:global_thread_pool_task_run, self.class.global_thread_pool_stats) do
201
- yield(*args)
223
+ self.class.marking_pool_thread do
224
+ wrap_in_rails_executor do
225
+ with_thread_locals(thread_storage) do
226
+ with_fiber_locals(fiber_storage) do
227
+ Graphiti.broadcast(:global_thread_pool_task_run, self.class.global_thread_pool_stats) do
228
+ yield(*args)
229
+ end
202
230
  end
203
231
  end
204
232
  end
@@ -108,40 +108,42 @@ class Graphiti::Sideload::PolymorphicBelongsTo < Graphiti::Sideload::BelongsTo
108
108
  end
109
109
 
110
110
  def resolve(parents, query, graph_parent)
111
- return future_resolve(parents, query, graph_parent).value! if Graphiti.config.concurrency
111
+ if ::Graphiti::Scope.resolve_synchronously?
112
+ sync_resolve(parents, query, graph_parent)
113
+ else
114
+ future_resolve(parents, query, graph_parent).value!
115
+ end
116
+ end
112
117
 
113
- parents.group_by(&grouper.field_name).each_pair do |group_name, group|
114
- next if group_name.nil? || grouper.ignore?(group_name)
118
+ def future_resolve(parents, query, graph_parent)
119
+ promises = []
120
+ each_resolvable_group(parents, query) do |child, group, child_query|
121
+ promises << child.future_resolve(group, child_query, graph_parent)
122
+ end
123
+ Concurrent::Promises.zip(*promises)
124
+ end
115
125
 
116
- match = ->(c) { c.group_name == group_name.to_sym }
117
- if (sideload = children.values.find(&match))
118
- duped = remove_invalid_sideloads(sideload.resource, query)
119
- sideload.resolve(group, duped, graph_parent)
120
- else
121
- err = ::Graphiti::Errors::PolymorphicSideloadChildNotFound
122
- raise err.new(self, group_name)
123
- end
126
+ private
127
+
128
+ def sync_resolve(parents, query, graph_parent)
129
+ each_resolvable_group(parents, query) do |child, group, child_query|
130
+ child.resolve(group, child_query, graph_parent)
124
131
  end
125
132
  end
126
133
 
127
- def future_resolve(parents, query, graph_parent)
128
- promises = parents.group_by(&grouper.field_name).filter_map do |(group_name, group)|
134
+ def each_resolvable_group(parents, query)
135
+ parents.group_by(&grouper.field_name).each_pair do |group_name, group|
129
136
  next if group_name.nil? || grouper.ignore?(group_name)
130
137
 
131
- match = ->(c) { c.group_name == group_name.to_sym }
132
- if (sideload = children.values.find(&match))
133
- duped = remove_invalid_sideloads(sideload.resource, query)
134
- sideload.future_resolve(group, duped, graph_parent)
135
- else
136
- err = ::Graphiti::Errors::PolymorphicSideloadChildNotFound
137
- raise err.new(self, group_name)
138
+ child = children.values.find { |candidate| candidate.group_name == group_name.to_sym }
139
+ unless child
140
+ raise ::Graphiti::Errors::PolymorphicSideloadChildNotFound.new(self, group_name)
138
141
  end
142
+
143
+ yield child, group, remove_invalid_sideloads(child.resource, query)
139
144
  end
140
- Concurrent::Promises.zip(*promises)
141
145
  end
142
146
 
143
- private
144
-
145
147
  # We may be requesting a relationship that some subclasses support,
146
148
  # but not others. Remove anything we don't support.
147
149
  # TODO: spec to ensure this dupe logic doesn't mutate the original
@@ -240,9 +240,11 @@ module Graphiti
240
240
  end
241
241
 
242
242
  def load(parents, query, graph_parent)
243
- return build_resource_proxy(parents, query, graph_parent).to_a unless Graphiti.config.concurrency
244
-
245
- future_load(parents, query, graph_parent).value!
243
+ if Scope.resolve_synchronously?
244
+ build_resource_proxy(parents, query, graph_parent).to_a
245
+ else
246
+ future_load(parents, query, graph_parent).value!
247
+ end
246
248
  end
247
249
 
248
250
  # Override in subclass
@@ -292,48 +294,19 @@ module Graphiti
292
294
  children.replace(associated) if track_associated
293
295
  end
294
296
 
295
- # Synchronous counterpart to #future_resolve, used when concurrency is off.
296
- # Mirrors #future_resolve but resolves inline via the synchronous
297
- # Scope#resolve / #load paths (no promises). See Scope#sync_resolve_sideloads.
298
297
  def resolve(parents, query, graph_parent)
299
- return future_resolve(parents, query, graph_parent).value! if Graphiti.config.concurrency
300
-
301
- if single? && parents.length > 1
302
- raise Errors::SingularSideload.new(self, parents.length)
303
- end
304
-
305
- if self.class.scope_proc
306
- sideload_scope = fire_scope(parents)
307
- sideload_scope = Scope.new sideload_scope,
308
- resource,
309
- query,
310
- parent: graph_parent,
311
- sideload: self,
312
- sideload_parent_length: parents.length,
313
- default_paginate: false
314
- sideload_scope.resolve do |sideload_results|
315
- fire_assign(parents, sideload_results)
316
- end
298
+ if Scope.resolve_synchronously?
299
+ sync_resolve(parents, query, graph_parent)
317
300
  else
318
- load(parents, query, graph_parent)
301
+ future_resolve(parents, query, graph_parent).value!
319
302
  end
320
303
  end
321
304
 
322
305
  def future_resolve(parents, query, graph_parent)
323
- if single? && parents.length > 1
324
- raise Errors::SingularSideload.new(self, parents.length)
325
- end
306
+ assert_singular!(parents)
326
307
 
327
308
  if self.class.scope_proc
328
- sideload_scope = fire_scope(parents)
329
- sideload_scope = Scope.new sideload_scope,
330
- resource,
331
- query,
332
- parent: graph_parent,
333
- sideload: self,
334
- sideload_parent_length: parents.length,
335
- default_paginate: false
336
- sideload_scope.future_resolve do |sideload_results|
309
+ build_sideload_scope(parents, query, graph_parent).future_resolve do |sideload_results|
337
310
  fire_assign(parents, sideload_results)
338
311
  end
339
312
  else
@@ -401,6 +374,34 @@ module Graphiti
401
374
 
402
375
  private
403
376
 
377
+ def sync_resolve(parents, query, graph_parent)
378
+ assert_singular!(parents)
379
+
380
+ if self.class.scope_proc
381
+ build_sideload_scope(parents, query, graph_parent).resolve do |sideload_results|
382
+ fire_assign(parents, sideload_results)
383
+ end
384
+ else
385
+ load(parents, query, graph_parent)
386
+ end
387
+ end
388
+
389
+ def assert_singular!(parents)
390
+ if single? && parents.length > 1
391
+ raise Errors::SingularSideload.new(self, parents.length)
392
+ end
393
+ end
394
+
395
+ def build_sideload_scope(parents, query, graph_parent)
396
+ Scope.new fire_scope(parents),
397
+ resource,
398
+ query,
399
+ parent: graph_parent,
400
+ sideload: self,
401
+ sideload_parent_length: parents.length,
402
+ default_paginate: false
403
+ end
404
+
404
405
  def future_load(parents, query, graph_parent)
405
406
  proxy = build_resource_proxy(parents, query, graph_parent)
406
407
  proxy.respond_to?(:future_resolve_data) ? proxy.future_resolve_data : Concurrent::Promises.fulfilled_future(proxy)
@@ -1,3 +1,3 @@
1
1
  module Graphiti
2
- VERSION = "1.13.3"
2
+ VERSION = "1.13.4"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphiti
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.13.3
4
+ version: 1.13.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lee Richmond
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-09 00:00:00.000000000 Z
11
+ date: 2026-08-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jsonapi-serializable