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 +4 -4
- data/CHANGELOG.md +8 -0
- data/lib/graphiti/debugger.rb +18 -6
- data/lib/graphiti/scope.rb +73 -45
- data/lib/graphiti/sideload/polymorphic_belongs_to.rb +25 -23
- data/lib/graphiti/sideload.rb +38 -37
- data/lib/graphiti/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7f23c59b6750dc9a8dcaefaba6d6250a9deac2676c33b01930b0c715767b85a1
|
|
4
|
+
data.tar.gz: 2252b0e870042723a30af9844d6006d8587f14228d82bc3eafd36c4b72b2fc8c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
data/lib/graphiti/debugger.rb
CHANGED
|
@@ -4,11 +4,22 @@
|
|
|
4
4
|
module Graphiti
|
|
5
5
|
class Debugger
|
|
6
6
|
class << self
|
|
7
|
-
attr_accessor :enabled, :
|
|
7
|
+
attr_accessor :enabled, :debug_models, :preserve, :pry
|
|
8
8
|
end
|
|
9
|
-
|
|
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
|
-
|
|
193
|
+
statements = chunks
|
|
194
|
+
statements.each do |chunk|
|
|
183
195
|
if (parent = chunk[:parent])
|
|
184
|
-
relevant =
|
|
196
|
+
relevant = statements.find { |c| c[:resource] == parent }
|
|
185
197
|
relevant[:children].unshift(chunk) if relevant
|
|
186
198
|
end
|
|
187
199
|
end
|
|
188
|
-
|
|
189
|
-
|
|
200
|
+
statements.reject! { |c| !!c[:parent] }
|
|
201
|
+
statements
|
|
190
202
|
end
|
|
191
203
|
|
|
192
204
|
def chunk_to_hash(chunk)
|
data/lib/graphiti/scope.rb
CHANGED
|
@@ -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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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 =
|
|
166
|
-
|
|
167
|
-
|
|
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,
|
|
194
|
+
sideload.future_resolve(parent_results, future_query, parent_resource)
|
|
172
195
|
end
|
|
173
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
128
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
data/lib/graphiti/sideload.rb
CHANGED
|
@@ -240,9 +240,11 @@ module Graphiti
|
|
|
240
240
|
end
|
|
241
241
|
|
|
242
242
|
def load(parents, query, graph_parent)
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
324
|
-
raise Errors::SingularSideload.new(self, parents.length)
|
|
325
|
-
end
|
|
306
|
+
assert_singular!(parents)
|
|
326
307
|
|
|
327
308
|
if self.class.scope_proc
|
|
328
|
-
|
|
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)
|
data/lib/graphiti/version.rb
CHANGED
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.
|
|
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-
|
|
11
|
+
date: 2026-08-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: jsonapi-serializable
|