Skip to content

Instantly share code, notes, and snippets.

@ivanovv
Created October 14, 2019 01:34
Show Gist options
  • Select an option

  • Save ivanovv/d90660fdb038aed72ba2ce762d942c9a to your computer and use it in GitHub Desktop.

Select an option

Save ivanovv/d90660fdb038aed72ba2ce762d942c9a to your computer and use it in GitHub Desktop.
Server side Phoenix Presence monitor
defmodule PresenceMonitor do
use GenServer
alias App.ViewerPresence
## Client API
def monitor(server_name, topic, subscription_id) do
GenServer.call(server_name, {:monitor, topic, subscription_id})
end
## Server API
def start_link(name) do
GenServer.start_link(__MODULE__, [], name: name)
end
def init(_) do
{:ok, %{topics: %{}, tab_counter: %{}, links: %{}}}
end
def handle_call({:monitor, nil, _subscription_id}, _from, state) do
{:reply, :ok, state}
end
def handle_call({:monitor, topic, subscription_id}, _from, state) do
subscription_id = Integer.to_string(subscription_id)
if Map.has_key?(state.topics, topic) do
{:reply, :ok, link_subscription_to_topic(state, topic, subscription_id)}
else
AppWeb.Endpoint.subscribe(topic)
state =
state
|> put_topic(topic)
|> link_subscription_to_topic(topic, subscription_id)
|> traverse_joins(%{subscription_id => %{}})
AppWeb.Endpoint.broadcast("broadcasts", "stats", state.topics)
{:reply, :ok, state}
end
end
def handle_call({:get_stats}, _from, state) do
{:reply, state.topics, state}
end
def handle_info(%{event: "presence_diff", payload: payload}, state) do
state = state |> traverse_joins(payload.joins) |> traverse_leaves(payload.leaves)
AppWeb.Endpoint.broadcast("broadcasts", "stats", state.topics)
{:noreply, state}
end
defp drop_topic(state, topic) do
:ok = AppWeb.Endpoint.unsubscribe(topic)
Logger.warn("Removing #{topic}, with #{state.topics[topic]} keys")
%{state | topics: Map.delete(state.topics, topic)}
end
defp put_topic(state, topic) do
%{state | topics: Map.put(state.topics, topic, 0)}
end
defp traverse_joins(state, joins) do
Enum.reduce(joins, state, fn {subscription_id, _metas}, acc ->
case Map.fetch(acc.tab_counter, subscription_id) do
:error ->
ViewerPresence.log_subscribe(subscription_id)
set_number_of_tabs(acc, subscription_id, 1)
{:ok, number_of_tabs} ->
set_number_of_tabs(acc, subscription_id, number_of_tabs + 1)
end
end)
end
defp traverse_leaves(state, leaves) do
Enum.reduce(leaves, state, fn {subscription_id, _metas}, acc ->
case Map.fetch(acc.tab_counter, subscription_id) do
{:ok, number_of_tabs} ->
if number_of_tabs - 1 <= 0 do
ViewerPresence.log_unsubscribe(subscription_id)
remove_subscription_from_state(acc, subscription_id)
else
set_number_of_tabs(acc, subscription_id, number_of_tabs - 1)
end
_ ->
acc
end
end)
end
defp set_number_of_tabs(state, subscription_id, number) do
%{state | tab_counter: Map.put(state.tab_counter, subscription_id, number)}
end
defp link_subscription_to_topic(state, topic, subscription_id) do
if Map.has_key?(state.links, subscription_id) do
state
else
state = %{state | links: Map.put(state.links, subscription_id, topic)}
%{state | topics: Map.update(state.topics, topic, 1, &(&1 + 1))}
end
end
defp remove_subscription_from_state(state, subscription_id) do
topic = state.links[subscription_id]
state = %{state | tab_counter: Map.delete(state.tab_counter, subscription_id)}
state = %{state | links: Map.delete(state.links, subscription_id)}
state = %{state | topics: Map.update(state.topics, topic, 0, &(&1 - 1))}
if should_unsubscribe(state, topic) do
drop_topic(state, topic)
else
state
end
end
defp should_unsubscribe(state, topic) do
state.topics[topic] <= 0
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment