Current section
Files
Jump to
Current section
Files
lib/phoenix/sync/shape_request_registry.ex
defmodule Phoenix.Sync.ShapeRequestRegistry do
@moduledoc false
use GenServer
alias Phoenix.Sync.PredefinedShape
# Client API
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def register_shape(%PredefinedShape{} = shape) do
GenServer.call(__MODULE__, {:register_shape, shape})
end
def unregister_shape(key) do
GenServer.call(__MODULE__, {:unregister_shape, key})
end
def interrupt_matching(shape, shape_opts \\ [])
def interrupt_matching(match_spec, _opts) when is_list(match_spec) do
match_spec = Keyword.replace_lazy(match_spec, :params, &normalize_params_form/1)
do_interrupt_matching(fn shape ->
Enum.all?(match_spec, fn {k, v} -> Map.get(shape, k) == v end)
end)
end
def interrupt_matching(matcher, _opts) when is_function(matcher, 1) do
GenServer.call(__MODULE__, {:interrupt_matching, matcher})
end
def interrupt_matching(queryable, shape_opts)
when is_atom(queryable) or is_struct(queryable, Ecto.Query) do
match_shape =
queryable
|> PredefinedShape.new!(shape_opts)
|> match_params()
do_interrupt_matching(fn shape -> match_shape == shape end)
rescue
e -> {:error, e}
end
def interrupt_matching(table, opts) when is_binary(table) do
interrupt_matching(Keyword.put(opts, :table, table))
end
defp do_interrupt_matching(fun) do
GenServer.call(__MODULE__, {:interrupt_matching, fun})
end
defp normalize_params_form(params) when is_list(params) do
params
|> Enum.with_index(fn elem, index -> {to_string(index + 1), to_string(elem)} end)
|> Map.new()
end
defp normalize_params_form(params) when is_map(params) do
Map.new(params, fn {k, v} -> {to_string(k), to_string(v)} end)
end
def registered_requests do
GenServer.call(__MODULE__, :registered_requests)
end
# Server implementation
@impl GenServer
def init(_opts) do
{:ok, %{subscriptions: %{}, monitors: %{}}}
end
@impl GenServer
def handle_call({:register_shape, shape}, {request_pid, _ref}, state) do
%{
monitors: monitors,
subscriptions: subscriptions
} = state
monitor_ref = Process.monitor(request_pid)
monitors = Map.put(monitors, monitor_ref, monitor_ref)
shape_info = {shape, request_pid}
subscriptions = Map.put(subscriptions, monitor_ref, shape_info)
{:reply, {:ok, monitor_ref}, %{state | monitors: monitors, subscriptions: subscriptions}}
end
def handle_call({:unregister_shape, key}, _from, %{subscriptions: subscriptions} = state) do
{:reply, :ok, %{state | subscriptions: Map.delete(subscriptions, key)}}
end
def handle_call({:interrupt_matching, matcher}, _from, state) do
interrupted_count =
state.subscriptions
|> Enum.filter(fn {_id, {shape, _request_pid}} ->
shape
|> match_params()
|> matcher.()
end)
|> Enum.reduce(0, fn {key, {_shape, request_pid}}, acc ->
send(request_pid, {:interrupt_shape, key, :server_interrupt})
acc + 1
end)
{:reply, {:ok, interrupted_count}, state}
end
def handle_call(:registered_requests, _from, %{subscriptions: subscriptions} = state) do
{:reply, Map.to_list(subscriptions), state}
end
@impl GenServer
def handle_info({:DOWN, monitor_ref, :process, _pid, _reason}, state) do
case Map.pop(state.monitors, monitor_ref) do
{nil, monitors} ->
{:noreply, %{state | monitors: monitors}}
{key, monitors} ->
subscriptions = Map.delete(state.subscriptions, key)
{:noreply, %{state | subscriptions: subscriptions, monitors: monitors}}
end
end
defp match_params(%PredefinedShape{} = shape) do
shape
|> PredefinedShape.to_shape_params()
|> Map.new()
|> Map.drop([:replica])
end
end