Current section

Files

Jump to
datocms_graphql_client lib datocms graphql_client query_monitor.ex
Raw

lib/datocms/graphql_client/query_monitor.ex

defmodule DatoCMS.GraphQLClient.QueryMonitor do
@moduledoc false
use GenServer
alias DatoCMS.GraphQLClient.Backends.StandardClient, as: Client
@datocms_live_update_query_registry :datocms_live_update_query_registry
def registry_name, do: @datocms_live_update_query_registry
def subscribe!(query, params \\ %{}, callback) do
signature = signature(query, params)
{:ok, body} =
case Registry.lookup(@datocms_live_update_query_registry, signature) do
[{_monitor, body}] ->
{:ok, body}
[] ->
new(query, params, signature, callback)
end
{:ok, body}
end
def start_link(opts) do
# Do initial call for channel URL
{:ok, url} = Client.query(opts[:query], opts[:params])
state =
opts
|> Keyword.drop([:query, :params])
|> Keyword.merge(url: url, received: false)
GenServer.start_link(__MODULE__, state)
end
def init(opts) do
{:ok, opts}
end
def handle_call({:start}, _from, state) do
# Call EventsourceEx with our own pid as stream_to so we get handle_info/2 calls
{:ok, pid} = EventsourceEx.new(state[:url], stream_to: self(), headers: [])
{:reply, {:ok}, Keyword.put(state, :eventsource_pid, pid)}
end
def handle_info(%EventsourceEx.Message{event: "update", data: data}, state) do
parsed = Jason.decode!(data, keys: :atoms)
body = parsed.response.data
if state[:received] do
Registry.update_value(@datocms_live_update_query_registry, state[:signature], fn _val -> body end)
# Allow our client to act on the change
state[:callback].()
{:noreply, state}
else
Registry.register(@datocms_live_update_query_registry, state[:signature], body)
# The Task in new/4 is waiting on us.
# Send the body to the original subscribe!/3
# call can return
send(state[:initial_pid], {:ok, :body, body})
{:noreply, Keyword.put(state, :received, true)}
end
end
def handle_info(%EventsourceEx.Message{event: "ping"}, state) do
{:noreply, state}
end
defp signature(query, params) do
:crypto.hash(:sha, [query, inspect(params)])
end
defp new(query, params, signature, callback) do
# Note: there is a race condition between here and register/3 later.
# The consequence is simply that we may end up with
# a duplicate query and duplicate eventsource calls
Task.async(fn ->
{:ok, pid} = start_link([
query: query,
params: params,
initial_pid: self(),
signature: signature,
callback: callback
])
{:ok} = GenServer.call(pid, {:start})
receive do
{:ok, :body, body} ->
{:ok, body}
after
5_000 ->
{:error, :timeout}
end
end)
|> Task.await(:infinity)
end
end