mercredi 17 février 2021

How do you share state between processes in Elixir?

My goal is to create a simple chat application with Phoenix that follow best practices. For the sake of complicity, the application is built around chat "rooms", where each room consists of a list of messages. Each room has an associated identifier (a six character long alphanumeric string), so that other users can enter it and join.

Currently, I have modeled this as two modules: a LiveView to handle user input and show a list of messages in a room, and a module to track the state of a room. The code roughly looks like this

defmodule Chat.ChatServer do
  use GenServer

  # Client

  def start_link(room_id) do
    GenServer.start_link(__MODULE__, %{room_id: room_id, messages: []}, name: via_tuple(room_id))
  end

  def whereis(room_id) do
    GenServer.whereis(via_tuple(room_id))
  end

  defp via_tuple(room_id) do
    {:via, Registry, {Registry.Chat, room_id}}
  end

  def add_message(room_id, message) do
    GenServer.cast(via_tuple(room_id), {:add_message, message})
  end

  def get_messages(room_id) do
    GenServer.call(via_tuple(room_id), :get_messages)
  end

  def subscribe(room_id) do
    Phoenix.PubSub.subscribe(Chat.PubSub, "chat:#{room_id}")
  end

  # Server

  def init(messages) do
    {:ok, messages}
  end

  def handle_cast({:add_message, new_message}, %{messages: messages} = state) do
    new_messages = [new_message | messages]
    {:noreply, %{state | messages: new_messages}, {:continue, :notify_subscribers}}
  end

  def handle_call(:get_messages, _from, %{messages: messages} = state) do
    {:reply, messages, state}
  end

  def handle_continue(:notify_subscribers, %{room_id: room_id} = state) do
    Phoenix.PubSub.broadcast(Chat.PubSub, "chat:#{room_id}", {:updated_messages})
    {:noreply, state}
  end
end
defmodule ChatWeb.ChatLive do
  use ChatWeb, :live_view

  alias Chat.{ChatServer}

  def mount(%{"id" => room_id}, _session, socket) do
    if connected?(socket) do
      ChatServer.subscribe(room_id)
    end

    assigns = [
      messages: ChatServer.get_messages(room_id),
      room_id: room_id
    ]

    {:ok, assign(socket, assigns)}
  end

  def handle_event("add_message", _, %{assigns: %{room_id: room_id, messages: messages}} = socket) do
    ChatServer.add_message(room_id, "Example message")
    {:noreply, socket}
  end

  def handle_info({:updated_messages}, %{assigns: %{room_id: room_id}} = socket) do
    messages = ChatServer.get_messages(room_id)
    {:noreply, assign(socket, :messages, messages)}
  end
end

As you can see, I have modeled the chat room as a GenServer. Are there any downsides to doing this instead of keeping all rooms in a single GenServer process?

Is this correct usage of GenServer? Is it a better approach to have one GenServer to wrap all the rooms, instead of one GenServer process per room?

Some issues that I have observed but don't know how to resolve are that as of now, each process keeps the name of itself in its state. The only reason for that is so that it can broadcast only to the processes that are subscribed to that particular room. This is my attempt at the observer pattern. Is there a simple way to do this without keeping the name in the state?

In the latest version of Phoenix, PubSub is not visible in the docs. I assume that is due to it being an internal API that is no longer intended to be used like I've done above. Is that correct?

Finally, to avoid the LiveView refetching the messages from the GenServer before the new message has been appended to the state. Is this misuse of handle_continue/2?

Aucun commentaire:

Enregistrer un commentaire