defmodule LiveState.Channel do
@moduledoc """
To build a LiveState application, you'll first want to add a channel that implements this
behaviour.
"""
import Phoenix.Socket
alias LiveState.Event
@doc """
Returns the initial application state. Called just after connection
"""
@callback init(channel :: binary(), payload :: term(), socket :: Socket.t()) ::
{:ok, state :: term()}
@doc """
Receives an event an payload from the client and current state. Returns the new state along with (optionally)
a single or list of `LiveState.Event` to dispatch to client
"""
@callback handle_event(event_name :: binary(), payload :: term(), state :: term()) ::
{:reply, reply :: %LiveState.Event{} | list(%LiveState.Event{}), new_state :: any()}
| {:noreply, new_state :: term}
@doc """
The key on assigns to hold application state. Defaults to `:state`.
"""
@callback state_key() :: atom()
@doc """
The key on assigns to hold application state version. Defaults to `:version`.
"""
@callback state_key() :: atom()
@doc """
Receives pubsub message and current state. Returns new state
"""
@callback handle_message(message :: term(), state :: term()) ::
{:reply, reply :: %LiveState.Event{} | list(%LiveState.Event{}), new_state :: any()}
| {:noreply, new_state :: term}
defmacro __using__(opts) do
quote do
use unquote(Keyword.get(opts, :web_module)), :channel
@behaviour unquote(__MODULE__)
@json_patch unquote(Keyword.get(opts, :json_patch))
def join(channel, payload, socket) do
send(self(), {:after_join, channel, payload})
{:ok, socket}
end
def handle_info({:after_join, channel, payload}, socket) do
{:ok, state} = init(channel, payload, socket)
push_state_change(socket, state, 0)
{:noreply, socket |> assign(state_key(), state) |> assign(state_version_key(), 0)}
end
def handle_info(message, %{assigns: assigns} = socket) do
handle_message(message, Map.get(assigns, state_key())) |> maybe_handle_reply(socket)
end
def handle_in("lvs_evt:" <> event_name, payload, %{assigns: assigns} = socket) do
handle_event(event_name, payload, Map.get(assigns, state_key()))
|> maybe_handle_reply(socket)
end
def state_key, do: :state
def state_version_key, do: :version
def handle_message(_message, state), do: {:noreply, state}
def handle_event(_message, _payload, state), do: {:noreply, state}
defp update_state(%{assigns: assigns} = socket, new_state) do
current_state = Map.get(assigns, state_key())
new_state_version = Map.get(assigns, state_version_key()) + 1
if @json_patch do
push_json_patch(socket, current_state, new_state, new_state_version)
else
push_state_change(socket, new_state, new_state_version)
end
{:noreply, socket |> assign(state_key(), new_state) |> assign(state_version_key(), new_state_version)}
end
defp maybe_handle_reply({:noreply, new_state}, socket), do: update_state(socket, new_state)
defp maybe_handle_reply({:reply, event_or_events, new_state}, socket) do
push_events(socket, event_or_events)
update_state(socket, new_state)
end
defp push_events(socket, events) when is_list(events) do
events |> Enum.map(&push_event(socket, &1))
end
defp push_events(socket, event), do: push_event(socket, event)
defp push_event(socket, %Event{name: name, detail: detail}) do
push(socket, name, detail)
end
defp push_state_change(socket, state, version) do
payload = %{} |> Map.put(state_key(), state) |> Map.put(state_version_key(), version)
push(socket, "state:change", payload)
end
defp push_json_patch(socket, current_state, new_state, version) do
push(socket, "state:patch", %{
patch: JSONDiff.diff(current_state, new_state),
version: version
})
end
defoverridable state_key: 0,
handle_message: 2,
handle_in: 3,
handle_info: 2,
handle_event: 3,
join: 3
end
end
end