Last active
June 28, 2019 22:38
-
-
Save theorygeek/dded80c0711144f46537017edf804695 to your computer and use it in GitHub Desktop.
Better GraphQL Promise Handling
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module AsyncGraphResolution | |
extend ActiveSupport::Concern | |
class_methods do | |
def async(method_name) | |
unbound_method = instance_method(method_name) | |
define_method(method_name) do |*args| | |
FiberResolver.new(unbound_method.bind(self), context, args, method_name, unbound_method.owner.name) | |
end | |
end | |
end | |
class FiberResolver | |
def initialize(resolver, query_context, resolver_args, method_name, klass_name) | |
@method_name = method_name | |
@klass_name = klass_name | |
build_fiber(resolver, GraphQL::Batch::Executor.current) | |
@resolution = query_context[:fiber_resolvers] ||= [] | |
@resolution << self | |
# Execute the fiber up until the point when it yields, so that we can try to keep as much | |
# of the execution on the original call stack as possible. | |
@next_value = resolver_args | |
iterate | |
end | |
def resolved_value | |
return @resolved_value if defined?(@resolved_value) | |
# Get all of the fibers that need to execute. As long as any of them are alive, we'll keep resolving them. | |
unresolved_fibers = @resolution.slice!(0..-1) | |
unresolved_fibers.select!(&:unresolved?) | |
while unresolved_fibers.any? | |
unresolved_fibers.each(&:iterate) | |
unresolved_fibers.select!(&:unresolved?) | |
end | |
@resolved_value | |
end | |
def iterate | |
@next_value = @next_value.sync if @next_value.is_a?(Promise) | |
@next_value = @fiber.resume(@next_value) | |
return if unresolved? | |
@resolved_value = @next_value | |
end | |
def unresolved? | |
@fiber.alive? | |
end | |
private | |
# This happens in its own method so that we avoid hanging onto a reference to a bunch | |
# of external variables | |
def build_fiber(resolver, batch_executor) | |
@fiber = Fiber.new do |resolver_args| | |
GraphQL::Batch::Executor.current = batch_executor | |
resolver.call(*resolver_args) do |next_promise| | |
invariant(next_promise.is_a?(Promise)) { "Illegal value yielded from #{@method_name} on #{@klass_name}. Expected a Promise, got #{next_promise.inspect}" } | |
Fiber.yield(next_promise) | |
end | |
end | |
end | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Types | |
class BaseObject < GraphQL::Schema::Object | |
include AsyncGraphResolution | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Types | |
class Order < BaseObject | |
field :products, [Product], "Products on this order", null: false | |
# Declare that the method is `async` below. Then whenever you have a Promise, you can | |
# `yield` it. The promises get grouped together and then resolved all at once. | |
# Requires that you have GraphQL::Batch configured. | |
def products | |
yield AssociationLoader.preload(object, :products) | |
object.products | |
end | |
async :products | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Configure your schema so that it knows how to resolve the async methods | |
class YourSchema < GraphQL::Schema | |
lazy_resolve(AsyncGraphResolution::FiberResolver, :resolved_value) | |
lazy_resolve(Promise, :sync) | |
use GraphQL::Batch | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
One caveat is that when you execute code inside of a
Fiber
, you lose all of yourThread.current
values. That's why I had to manually copy theGraphQL::Batch::Executor.current
value into the fiber.