defmodule Formular.Client.PubSub do @scope :formular_pubsub @type formula_name :: String.t() @type code :: String.t() @type event :: code_change_event() | compiled_event() | compile_failed_event() @type code_change_event :: {:code_change, formula_name(), old_code :: code(), new_code :: code()} @type compiled_event :: {:compiled, formula_name(), code(), compile_event_metadata()} @type compile_failed_event :: {:compile_failed, formula_name(), err :: any(), code(), compile_event_metadata()} @type compile_event_metadata :: [{:mod, module()} | {:context, module()}] use GenServer require Logger def start_link(args) do GenServer.start_link(__MODULE__, args, name: __MODULE__) end @doc """ Subscribe the change events for a given formula. Notice that a subscriber can subscribe a formula change for multiple times. In such cases, it also needs to unsubscribe for the same amount of times to stop receiving messages. """ @spec subscribe(formula_name(), pid()) :: :ok def subscribe(formula_name, pid \\ self()), do: :pg.join(@scope, formula_name, pid) @doc """ Stop receiving events from dispatcher. """ @spec unsubscribe(formula_name(), pid()) :: :ok def unsubscribe(formula_name, pid \\ self()), do: :pg.leave(@scope, formula_name, pid) @doc """ Dispatch the event to all subscribers. """ @spec dispatch(event :: event()) :: :ok def dispatch({:code_change, formula_name, _old_code, _new_code} = event), do: do_dispatch(formula_name, event) def dispatch({:compiled, formula_name, _code, _opts} = event), do: do_dispatch(formula_name, event) def dispatch({:compile_failed, formula_name, _err, _code, _opts} = event), do: do_dispatch(formula_name, event) defp do_dispatch(formula_name, event), do: :pg.get_local_members(@scope, formula_name) |> Enum.each(&send(&1, event)) @doc """ Shortcut to `dispatch({:code_change, formula_name, old_code, new_code})` """ @spec dispatch_code_change(formula_name(), old_code :: code(), new_code :: code()) :: :ok def dispatch_code_change(formula_name, old_code, new_code) do dispatch({:code_change, formula_name, old_code, new_code}) end @impl true def init(_args) do case :pg.start(@scope) do {:ok, _pid} -> {:ok, nil} {:error, {:already_started, _pid}} -> {:ok, nil} {:error, _reason} = err -> err end end end