liebus

liebus

Help add "message history" & "participants" to simple Chat Room with GenServer

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

Where Next?

Popular in Questions Top

vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New

Other popular topics Top

baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" => #BSON.ObjectId<58eb1a7a9ad169198c3dXXXX>, "email" => ...
New
sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New

We're in Beta

About us Mission Statement