Hi. I am learning Elixir recently, decided to make a small simple pet project “Chat Room” to learn some OTP.
The general points:
Need to create a gen_server which will have a state which
- store last 100 messages
- have a list of chat room participants (using join/leave functions)
- Send message which stores in last 100 messages history AND broadcasts message to a chat room members (using PID, of course)
- Add a monitor by chat room on participants PID to handle room leave when client process dies.
I used the via_tuple example I found. At this point, I can create a chatroom and messages in it. Now the main problem for me, due to lack of experience, is how to create a structure to store message history, members and replace this inside the GenServer so that I can do join/leave inside the room.
I would like at least some example of implementation and I will pick up from there.
This is my code for now:
server.ex
Summary
defmodule Chat.Server do
use GenServer
# Client API
def start_link(name) do
GenServer.start_link(__MODULE__, [], name: via_tuple(name))
end
def add_message(room_name, message) do
GenServer.cast(via_tuple(room_name), {:add_message, message})
end
def get_messages(room_name) do
GenServer.call(via_tuple(room_name), :get_messages)
end
defp via_tuple (room_name) do
{:via, Chat.Registry, {:chat_room, room_name}}
end
# SERVER
@impl true
def init(messages) do
{:ok, messages}
end
@impl true
def handle_cast({:add_message, new_message}, messages) do
{:noreply, [new_message | messages]}
end
@impl true
def handle_call(:get_messages, _from, messages) do
{:reply, messages, messages}
end
end
supervisor.ex
Summary
defmodule Chat.Supervisor do
use DynamicSupervisor
def start_link do
DynamicSupervisor.start_link(__MODULE__, [], name: :chat_supervisor)
end
def start_room(name) do
spec = %{id: Chat.Server, start: {Chat.Server, :start_link, [name]}}
DynamicSupervisor.start_child(:chat_supervisor, spec)
end
def init(_) do
DynamicSupervisor.init(
strategy: :one_for_one,
extra_arguments: []
)
end
end
registry.ex
Summary
defmodule Chat.Registry do
use GenServer
# API
def start_link do
# register our registry, with a simple name,
# just so to reference it in the other functions.
GenServer.start_link(__MODULE__, nil, name: :registry)
end
def whereis_name(room_name) do
GenServer.call(:registry, {:whereis_name, room_name})
end
def register_name(room_name, pid) do
GenServer.call(:registry, {:register_name, room_name, pid})
end
def unregister_name(room_name) do
GenServer.cast(:registry, {:unregister_name, room_name})
end
def send(room_name, message) do
# If we try to send a message to a process
# that is not registered, we return a tuple in the format
# {:badarg, {process_name, error_message}}.
# Otherwise, we just forward the message to the pid of this room.
case whereis_name(room_name) do
:undefined ->
{:badarg, {room_name, message}}
pid ->
Kernel.send(pid, message)
pid
end
end
# SERVER
def init(_) do
{:ok, Map.new}
end
def handle_call({:whereis_name, room_name}, _from, state) do
{:reply, Map.get(state, room_name, :undefined), state}
end
def handle_call({:register_name, room_name, pid}, _from, state) do
case Map.get(state, room_name) do
nil ->
# When a new process is registered, we start monitoring it
Process.monitor(pid)
{:reply, :yes, Map.put(state, room_name, pid)}
_ ->
{:reply, :no, state}
end
end
def handle_info({:DOWN, _, :process, pid, _}, state) do
# When a monitored process dies, we will receive a `:DOWN` message
# that we can use to remove the dead pid from our registry
{:noreply, remove_pid(state, pid)}
end
def remove_pid(state, pid_to_remove) do
# And here we just filter out the dead pid
remove = fn {_key, pid} -> pid != pid_to_remove end
Enum.filter(state, remove) |> Enum.into(%{})
end
end