Elixir Project: Presence
Presence is a record of who is currently in each topic. A topic is a named room, and each user in that room appears exactly once in its list.
Joining lists users in join order
Two users join one topic. The list shows them in the order they joined.
defmodule Presence do
use GenServer
def start_link(opts), do: GenServer.start_link(__MODULE__, %{}, opts)
def join(pid, topic, user), do: GenServer.call(pid, {:join, topic, user})
def list(pid, topic), do: GenServer.call(pid, {:list, topic})
def count(pid, topic), do: GenServer.call(pid, {:count, topic})
def init(state), do: {:ok, state}
def handle_call({:join, topic, user}, _from, state) do
users = Map.get(state, topic, [])
next = if user in users, do: users, else: users ++ [user]
{:reply, :ok, Map.put(state, topic, next)}
end
def handle_call({:list, topic}, _from, state), do: {:reply, Map.get(state, topic, []), state}
def handle_call({:count, topic}, _from, state), do: {:reply, length(Map.get(state, topic, [])), state}
end
{:ok, tracker} = Presence.start_link([])
IO.inspect(Presence.join(tracker, "lobby", "ana"))
IO.inspect(Presence.join(tracker, "lobby", "bo"))
IO.inspect(Presence.list(tracker, "lobby"))
IO.inspect(Presence.count(tracker, "lobby"))
GenServer.stop(tracker)Leaving removes only that user
One user leaves while the other stays. Leaving a user who is absent changes nothing.
defmodule Presence do
use GenServer
def start_link(opts), do: GenServer.start_link(__MODULE__, %{}, opts)
def join(pid, topic, user), do: GenServer.call(pid, {:join, topic, user})
def leave(pid, topic, user), do: GenServer.call(pid, {:leave, topic, user})
def list(pid, topic), do: GenServer.call(pid, {:list, topic})
def count(pid, topic), do: GenServer.call(pid, {:count, topic})
def init(state), do: {:ok, state}
def handle_call({:join, topic, user}, _from, state) do
users = Map.get(state, topic, [])
next = if user in users, do: users, else: users ++ [user]
{:reply, :ok, Map.put(state, topic, next)}
end
def handle_call({:leave, topic, user}, _from, state) do
users = Map.get(state, topic, [])
{:reply, :ok, Map.put(state, topic, List.delete(users, user))}
end
def handle_call({:list, topic}, _from, state), do: {:reply, Map.get(state, topic, []), state}
def handle_call({:count, topic}, _from, state), do: {:reply, length(Map.get(state, topic, [])), state}
end
{:ok, tracker} = Presence.start_link([])
Presence.join(tracker, "lobby", "ana")
Presence.join(tracker, "lobby", "bo")
Presence.leave(tracker, "lobby", "ana")
IO.inspect(Presence.list(tracker, "lobby"))
IO.inspect(Presence.count(tracker, "lobby"))
Presence.leave(tracker, "lobby", "cy")
IO.inspect(Presence.list(tracker, "lobby"))
GenServer.stop(tracker)Topics keep separate user lists
The same user can be in two topics at once. A duplicate join still lists the user once.
defmodule Presence do
use GenServer
def start_link(opts), do: GenServer.start_link(__MODULE__, %{}, opts)
def join(pid, topic, user), do: GenServer.call(pid, {:join, topic, user})
def list(pid, topic), do: GenServer.call(pid, {:list, topic})
def count(pid, topic), do: GenServer.call(pid, {:count, topic})
def init(state), do: {:ok, state}
def handle_call({:join, topic, user}, _from, state) do
users = Map.get(state, topic, [])
next = if user in users, do: users, else: users ++ [user]
{:reply, :ok, Map.put(state, topic, next)}
end
def handle_call({:list, topic}, _from, state), do: {:reply, Map.get(state, topic, []), state}
def handle_call({:count, topic}, _from, state), do: {:reply, length(Map.get(state, topic, [])), state}
end
{:ok, tracker} = Presence.start_link([])
Presence.join(tracker, "lobby", "ana")
Presence.join(tracker, "lobby", "ana")
Presence.join(tracker, "room2", "ana")
IO.inspect(Presence.list(tracker, "lobby"))
IO.inspect(Presence.list(tracker, "room2"))
IO.inspect(Presence.count(tracker, "lobby"))
IO.inspect(Presence.count(tracker, "room2"))
GenServer.stop(tracker)Example
defmodule Presence do
use GenServer
def start_link(opts), do: GenServer.start_link(__MODULE__, %{}, opts)
def join(pid, topic, user), do: GenServer.call(pid, {:join, topic, user})
def leave(pid, topic, user), do: GenServer.call(pid, {:leave, topic, user})
def list(pid, topic), do: GenServer.call(pid, {:list, topic})
def count(pid, topic), do: GenServer.call(pid, {:count, topic})
def init(state), do: {:ok, state}
def handle_call({:join, topic, user}, _from, state) do
users = Map.get(state, topic, [])
next = if user in users, do: users, else: users ++ [user]
{:reply, :ok, Map.put(state, topic, next)}
end
def handle_call({:leave, topic, user}, _from, state) do
users = Map.get(state, topic, [])
{:reply, :ok, Map.put(state, topic, List.delete(users, user))}
end
def handle_call({:list, topic}, _from, state), do: {:reply, Map.get(state, topic, []), state}
def handle_call({:count, topic}, _from, state), do: {:reply, length(Map.get(state, topic, [])), state}
end
{:ok, tracker} = Presence.start_link([])
counter = :counters.new(1, [])
Presence.join(tracker, "lobby", "ana")
:counters.add(counter, 1, 1)
Presence.join(tracker, "lobby", "bo")
:counters.add(counter, 1, 1)
Presence.join(tracker, "room2", "cy")
:counters.add(counter, 1, 1)
Presence.leave(tracker, "lobby", "bo")
IO.inspect(:counters.get(counter, 1))
IO.inspect(Presence.list(tracker, "lobby"))
IO.inspect(Presence.list(tracker, "room2"))
IO.inspect(Presence.count(tracker, "lobby"))
GenServer.stop(tracker)Key points
- Topic: a named room that groups its current users.
- Join: adding a user to a topic list.
- Leave: removing a user from a topic list.
- List: the users currently in one topic, in join order.
- Count: how many users are currently in one topic.
In this example, three joins are counted, one user leaves the lobby, and the final lists show who remains in each topic.
Note
joining the same topic twice does not list the user twice.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
Ana joins the lobby twice and Bo joins once. How many users does the lobby count show?