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