lib/live_state/channel.ex

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(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()}
              | {:no_reply, new_state :: term}

  @doc """
  The key on assigns to hold application state. Defaults to `state`.
  """
  @callback state_key() :: atom()

  @doc """
  Receives pubsub message and current state. Returns new state
  """
  @callback handle_message(message :: term(), state :: term()) :: {:ok, term()} | {:error, any()}

  defmacro __using__(web_module: web_module) do
    quote do
      use unquote(web_module), :channel

      @behaviour unquote(__MODULE__)

      def join(_channel, _payload, socket) do
        send(self(), :after_join)
        {:ok, socket}
      end

      def handle_info(:after_join, socket) do
        {:ok, state} = init(socket)
        push(socket, "state:change", state)
        {:noreply, socket |> assign(state_key(), state)}
      end

      def handle_info(message, %{assigns: assigns} = socket) do
        {:ok, new_state} = handle_message(message, Map.get(assigns, state_key()))
        update_state(socket, new_state)
      end

      def handle_in("lvs_evt:" <> event_name, payload, %{assigns: assigns} = socket) do
        case handle_event(event_name, payload, Map.get(assigns, state_key())) do
          {:noreply, new_state} ->
            update_state(socket, new_state)

          {:reply, event_or_events, new_state} ->
            push_events(socket, event_or_events)
            update_state(socket, new_state)
        end
      end

      def state_key, do: :state

      def handle_message(_message, state), do: {:ok, state}

      defp update_state(socket, new_state) do
        push(socket, "state:change", new_state)
        {:noreply, socket |> assign(state_key(), 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

      defoverridable state_key: 0, handle_message: 2, handle_in: 3, handle_info: 2, join: 3
    end
  end
end