Skip to content

Instantly share code, notes, and snippets.

@theorygeek
Last active June 28, 2019 22:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save theorygeek/dded80c0711144f46537017edf804695 to your computer and use it in GitHub Desktop.
Save theorygeek/dded80c0711144f46537017edf804695 to your computer and use it in GitHub Desktop.
Better GraphQL Promise Handling
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
module Types
class BaseObject < GraphQL::Schema::Object
include AsyncGraphResolution
end
end
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
# 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
@theorygeek
Copy link
Author

One caveat is that when you execute code inside of a Fiber, you lose all of your Thread.current values. That's why I had to manually copy the GraphQL::Batch::Executor.current value into the fiber.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment