Created
November 15, 2022 11:03
-
-
Save manuel-rubio/4077abd7c308cb900fa98ea9e26f4b9f to your computer and use it in GitHub Desktop.
Chat server using GenServer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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