async.ex (3460B)
1 defmodule Absinthe.Middleware.Async do 2 @moduledoc """ 3 This plugin enables asynchronous execution of a field. 4 5 See also `Absinthe.Resolution.Helpers.async/1` 6 7 # Example Usage: 8 9 Using the `Absinthe.Resolution.Helpers.async/1` helper function: 10 ```elixir 11 field :time_consuming, :thing do 12 resolve fn _, _, _ -> 13 async(fn -> 14 {:ok, long_time_consuming_function()} 15 end) 16 end 17 end 18 ``` 19 20 Using the bare plugin API 21 ```elixir 22 field :time_consuming, :thing do 23 resolve fn _, _, _ -> 24 task = Task.async(fn -> 25 {:ok, long_time_consuming_function()} 26 end) 27 {:middleware, #{__MODULE__}, task} 28 end 29 end 30 ``` 31 32 This module also serves as an example for how to build middleware that uses the 33 resolution callbacks. 34 35 See the source code and associated comments for further details. 36 """ 37 38 @behaviour Absinthe.Middleware 39 @behaviour Absinthe.Plugin 40 41 # A function has handed resolution off to this middleware. The first argument 42 # is the current resolution struct. The second argument is the function to 43 # execute asynchronously, and opts we'll want to use when it is time to await 44 # the task. 45 # 46 # This function suspends resolution, and sets the async flag true in the resolution 47 # accumulator. This will be used later to determine whether we need to run resolution 48 # again. 49 # 50 # This function inserts additional middleware into the remaining middleware 51 # stack for this field. On the next resolution pass, we need to `Task.await` the 52 # task so we have actual data. Thus, we prepend this module to the middleware stack. 53 def call(%{state: :unresolved} = res, {fun, opts}) when is_function(fun), 54 do: call(res, {Task.async(fun), opts}) 55 56 def call(%{state: :unresolved} = res, {task, opts}) do 57 task_data = {task, opts} 58 59 %{ 60 res 61 | state: :suspended, 62 acc: Map.put(res.acc, __MODULE__, true), 63 middleware: [{__MODULE__, task_data} | res.middleware] 64 } 65 end 66 67 def call(%{state: :unresolved} = res, %Task{} = task), do: call(res, {task, []}) 68 69 # This is the clause that gets called on the second pass. There's very little 70 # to do here. We just need to await the task started in the previous pass. 71 # 72 # Finally, we apply the result to the resolution using a helper function that ensures 73 # we handle the different tuple results. 74 # 75 # The `put_result` function handles setting the appropriate state. 76 # If the result is an `{:ok, value} | {:error, reason}` tuple it will set 77 # the state to `:resolved`, and if it is another middleware tuple it will 78 # set the state to unresolved. 79 def call(%{state: :suspended} = res, {task, opts}) do 80 result = Task.await(task, opts[:timeout] || 30_000) 81 82 res 83 |> Absinthe.Resolution.put_result(result) 84 end 85 86 # We must set the flag to false because if a previous resolution iteration 87 # set it to true it needs to go back to false now. It will be set 88 # back to true if any field uses this plugin again. 89 def before_resolution(exec) do 90 put_in(exec.acc[__MODULE__], false) 91 end 92 93 # Nothing to do after resolution for this plugin, so we no-op 94 def after_resolution(exec), do: exec 95 96 # If the flag is set we need to do another resolution phase. 97 # otherwise, we do not 98 def pipeline(pipeline, exec) do 99 case exec.acc do 100 %{__MODULE__ => true} -> 101 [Absinthe.Phase.Document.Execution.Resolution | pipeline] 102 103 _ -> 104 pipeline 105 end 106 end 107 end