Skip to content

Instantly share code, notes, and snippets.

@manuel-rubio
Created November 15, 2022 11:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save manuel-rubio/4077abd7c308cb900fa98ea9e26f4b9f to your computer and use it in GitHub Desktop.
Save manuel-rubio/4077abd7c308cb900fa98ea9e26f4b9f to your computer and use it in GitHub Desktop.
Chat server using GenServer
defmodule Chat do
@moduledoc """
Handle a process with a name for performing chat with other processes of the same kind.
It is an example about how a chat could be, based on the interchange of information
between the processes in a direct way, or using a list of processes.
"""
use GenServer
@type name() :: atom()
@type t() :: %__MODULE__{
name: name(),
roster: MapSet.t(name())
}
defstruct [
name: nil,
roster: MapSet.new()
]
@doc """
Starting the process. We need to provide a name and it must be an atom:
Examples:
iex> {:ok, pid} = Chat.start_link :user1
iex> {:error, {:already_started, ^pid}} = Chat.start_link :user1
iex> Process.alive?(pid)
true
"""
@spec start_link(name()) :: GenServer.on_start()
def start_link(name) when is_atom(name) do
GenServer.start_link(__MODULE__, [name], name: name)
end
@impl GenServer
@doc false
def init([name]) do
{:ok, %__MODULE__{name: name}}
end
@type opts() :: [
{:to, name()},
{:message, any()}
]
@doc """
Send a message from a process to another different process. It's
first sending the command to the originator process, it must exist, and
it's sending the message to the destination process, it also must exist.
Examples:
iex> {:ok, _pid} = Chat.start_link :alice
iex> {:ok, _pid} = Chat.start_link :bob
iex> Chat.send :alice, to: :bob, message: "hello Bob!"
:ok
<alice> hello Bob!
"""
@spec send(name(), opts()) :: :ok
def send(from, opts) do
GenServer.cast(from, {:send, opts[:to], opts[:message]})
end
@doc """
Add a contact for the roster of the process. Adding contacts let us
perform broadcast to all of these contacts.
Examples:
iex> {:ok, _pid} = Chat.start_link :alice
iex> {:ok, _pid} = Chat.start_link :bob
iex> :ok = Chat.add_contact :alice, :bob
iex> :ok = Chat.send :alice, message: "hello there!"
:ok
<alice> (broadcast) hello there!
"""
@spec add_contact(user :: name(), contact :: name()) :: :ok
def add_contact(user, contact) do
GenServer.cast(user, {:add_contact, contact})
end
@impl GenServer
@doc false
def handle_cast({:send, nil, message}, state_data) do
for name <- state_data.roster, pid = Process.whereis(name), Process.alive?(pid) do
GenServer.cast(pid, {:received, nil, state_data.name, message})
end
{:noreply, state_data}
end
def handle_cast({:send, to, message}, state_data) do
if pid = Process.whereis(to) do
GenServer.cast(pid, {:received, to, state_data.name, message})
end
{:noreply, state_data}
end
def handle_cast({:add_contact, contact}, state_data) do
{:noreply, %__MODULE__{state_data | roster: MapSet.put(state_data.roster, contact)}}
end
def handle_cast({:received, nil, from, message}, state_data) do
IO.puts("<#{from}> (broadcast) #{message}")
{:noreply, state_data}
end
def handle_cast({:received, to, from, message}, state_data) do
IO.puts("<#{from}> (#{to}) #{message}")
{:noreply, state_data}
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment