From 216916817b708607898546fe48d4ff93af526e2f Mon Sep 17 00:00:00 2001 From: Berenice Medel Date: Tue, 30 Nov 2021 12:06:35 -0600 Subject: [PATCH 01/19] configure presence for tracking the users listening a playlist --- lib/live_beats/application.ex | 2 ++ lib/live_beats_web/live/player_live.ex | 2 ++ lib/live_beats_web/live/profile_live.ex | 27 +++++++++++++++++++++++-- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/lib/live_beats/application.ex b/lib/live_beats/application.ex index e40f2a0..823bf5f 100644 --- a/lib/live_beats/application.ex +++ b/lib/live_beats/application.ex @@ -17,6 +17,8 @@ defmodule LiveBeats.Application do LiveBeatsWeb.Telemetry, # Start the PubSub system {Phoenix.PubSub, name: LiveBeats.PubSub}, + #start presence + LiveBeatsWeb.Presence, # Start the Endpoint (http/https) LiveBeatsWeb.Endpoint # Start a worker by calling: LiveBeats.Worker.start_link(arg) diff --git a/lib/live_beats_web/live/player_live.ex b/lib/live_beats_web/live/player_live.ex index b74dd08..5f0a129 100644 --- a/lib/live_beats_web/live/player_live.ex +++ b/lib/live_beats_web/live/player_live.ex @@ -258,6 +258,8 @@ defmodule LiveBeatsWeb.PlayerLive do def handle_info({MediaLibrary, _}, socket), do: {:noreply, socket} + def handle_info(%{event: "presence_diff"}, socket), do: {:noreply, socket} + defp play_song(socket, %Song{} = song, elapsed) do socket |> push_play(song, elapsed) diff --git a/lib/live_beats_web/live/profile_live.ex b/lib/live_beats_web/live/profile_live.ex index 61041cc..8968407 100644 --- a/lib/live_beats_web/live/profile_live.ex +++ b/lib/live_beats_web/live/profile_live.ex @@ -87,6 +87,12 @@ defmodule LiveBeatsWeb.ProfileLive do if connected?(socket) do MediaLibrary.subscribe_to_profile(profile) Accounts.subscribe(current_user.id) + LiveBeatsWeb.Presence.track( + self(), + topic(profile.user_id), + current_user.id, + current_user + ) end active_song_id = @@ -169,6 +175,14 @@ defmodule LiveBeatsWeb.ProfileLive do def handle_info({Accounts, _}, socket), do: {:noreply, socket} + def handle_info( + %{event: "presence_diff", payload: %{joins: _joins, leaves: _leaves}}, + %{assigns: %{presences: _users}} = socket + ) do + + {:noreply, assign_presences(socket)} + end + defp stop_song(socket, song_id) do SongRowComponent.send_status(song_id, :stopped) @@ -242,8 +256,15 @@ defmodule LiveBeatsWeb.ProfileLive do end defp assign_presences(socket) do - users = Accounts.lists_users_by_active_profile(socket.assigns.profile.user_id, limit: 10) - assign(socket, presences: users) + presences = socket.assigns.profile.user_id + |> topic() + |> Presence.list() + |> Enum.map(fn {_user_id, user_data} -> + user_data[:metas] + |> List.first() + end) + + assign(socket, presences: presences) end defp url_text(nil), do: "" @@ -252,4 +273,6 @@ defmodule LiveBeatsWeb.ProfileLive do uri = URI.parse(url_str) uri.host <> uri.path end + + defp topic(user_id) when is_integer(user_id), do: "profile:#{user_id}" end From bd927bf8b2e8540c6b26c2704fb58d5151ddaf91 Mon Sep 17 00:00:00 2001 From: Berenice Medel Date: Wed, 8 Dec 2021 23:33:29 -0600 Subject: [PATCH 02/19] add presence client --- lib/live_beats/application.ex | 3 +- .../presence/phoenix_presence_client.ex | 85 +++++++++++++++++++ lib/live_beats/presence/presence_client.ex | 28 ++++++ lib/live_beats_web/live/profile_live.ex | 6 +- 4 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 lib/live_beats/presence/phoenix_presence_client.ex create mode 100644 lib/live_beats/presence/presence_client.ex diff --git a/lib/live_beats/application.ex b/lib/live_beats/application.ex index 823bf5f..e6ce972 100644 --- a/lib/live_beats/application.ex +++ b/lib/live_beats/application.ex @@ -20,9 +20,10 @@ defmodule LiveBeats.Application do #start presence LiveBeatsWeb.Presence, # Start the Endpoint (http/https) - LiveBeatsWeb.Endpoint + LiveBeatsWeb.Endpoint, # Start a worker by calling: LiveBeats.Worker.start_link(arg) # {LiveBeats.Worker, arg} + {Phoenix.Presence.Client, client: LiveBeats.PresenceClient, pubsub: LiveBeats.PubSub, presence: LiveBeats.Presence} ] # See https://hexdocs.pm/elixir/Supervisor.html diff --git a/lib/live_beats/presence/phoenix_presence_client.ex b/lib/live_beats/presence/phoenix_presence_client.ex new file mode 100644 index 0000000..b663840 --- /dev/null +++ b/lib/live_beats/presence/phoenix_presence_client.ex @@ -0,0 +1,85 @@ +defmodule Phoenix.Presence.Client do + use GenServer + + @doc """ + TODO + + ## Options + + * `:pubsub` - The required name of the pubsub server + * `:presence` - The required name of the presence module + * `:client` - The required callback module + """ + def start_link(opts) do + GenServer.start_link(__MODULE__, opts, name: PresenceClient) + end + + def track(topic, key, meta) do + GenServer.call(PresenceClient, {:track, self(), topic, key, meta}) + end + + def untrack(topic, key) do + GenServer.call(PresenceClient, {:untrack, self(), topic, key}) + end + + def init(opts) do + client = Keyword.fetch!(opts, :client) + client_state = client.init(%{}) + + state = %{ + topics: %{}, + client: client, + pubsub: Keyword.fetch!(opts, :pubsub), + presence_mod: Keyword.fetch!(opts, :presence), + client_state: client_state + } + + {:ok, state} + end + + def handle_info(%{topic: topic, event: "presence_diff", payload: diff}, state) do + {:noreply, merge_diff(state, topic, diff)} + end + + def handle_call({:track, pid, topic, key, meta}, _from, state) do + {:reply, :ok, track_pid(state, pid, topic, key, meta)} + end + + def handle_call({:untrack, pid, topic, key}, _from, state) do + {:reply, :ok, untrack_pid(state, pid, topic, key)} + end + + defp track_pid(state, pid, topic, key, meta) do + case Map.fetch(state.topics, topic) do + {:ok, presences} -> + state.presence_mod.track(pid, topic, key, meta) + # update topics state... + # new_state + state + + :error -> + # subscribe to topic we weren't yet tracking + Phoenix.PubSub.subscribe(state.pubsub, topic) + # new_state + state + end + end + + defp untrack_pid(state, pid, topic, key) do + state.presence_mod.untrack(pid, topic, key) + # remove presence from state.topics + # if no more presences for given topic, unsubscribe + # Phoenix.PubSub.unsubscribe(state.pubsub, topic) + # new_state + state + end + + defp merge_diff(state, topic, diff) do + # merge diff into state.topics + # invoke state.client.handle_join|handle_leave + # if no more presences for given topic, unsubscribe + # Phoenix.PubSub.unsubscribe(state.pubsub, topic) + # new_state + state + end +end diff --git a/lib/live_beats/presence/presence_client.ex b/lib/live_beats/presence/presence_client.ex new file mode 100644 index 0000000..0c5cefc --- /dev/null +++ b/lib/live_beats/presence/presence_client.ex @@ -0,0 +1,28 @@ +defmodule LiveBeats.PresenceClient do + @behaviour Phoenix.Presence.Client + + @presence LiveBeats.Presence + + def start_link(opts) do + Phoenix.Presence.Client.start_link(presence: @presence, client: __MODULE__) + end + + def list(topic) do + @presence.list(topic) + end + + def init(_opts) do + # user-land state + {:ok, %{}} + end + + def handle_join(key, presence, state) do + # can local pubsub to LVs about new join + {:ok, state} + end + + def handle_leave(key, presence, state) do + # can local pubsub to LVs about new leave + {:ok, state} + end +end diff --git a/lib/live_beats_web/live/profile_live.ex b/lib/live_beats_web/live/profile_live.ex index 8968407..c206b36 100644 --- a/lib/live_beats_web/live/profile_live.ex +++ b/lib/live_beats_web/live/profile_live.ex @@ -87,11 +87,9 @@ defmodule LiveBeatsWeb.ProfileLive do if connected?(socket) do MediaLibrary.subscribe_to_profile(profile) Accounts.subscribe(current_user.id) - LiveBeatsWeb.Presence.track( - self(), - topic(profile.user_id), + Phoenix.Presence.Client.track(topic(profile.user_id), current_user.id, - current_user + %{} ) end From 4634d32295710ad0f2a3d2d7d2334c1f40e33bb2 Mon Sep 17 00:00:00 2001 From: Berenice Medel Date: Thu, 9 Dec 2021 01:26:28 -0600 Subject: [PATCH 03/19] update phoenix client state -add topics - remove_topics - add_presences - remove_presences --- lib/live_beats/application.ex | 2 +- .../presence/phoenix_presence_client.ex | 96 ++++++++++++++++--- 2 files changed, 82 insertions(+), 16 deletions(-) diff --git a/lib/live_beats/application.ex b/lib/live_beats/application.ex index e6ce972..e46cbfe 100644 --- a/lib/live_beats/application.ex +++ b/lib/live_beats/application.ex @@ -23,7 +23,7 @@ defmodule LiveBeats.Application do LiveBeatsWeb.Endpoint, # Start a worker by calling: LiveBeats.Worker.start_link(arg) # {LiveBeats.Worker, arg} - {Phoenix.Presence.Client, client: LiveBeats.PresenceClient, pubsub: LiveBeats.PubSub, presence: LiveBeats.Presence} + {Phoenix.Presence.Client, client: LiveBeats.PresenceClient, pubsub: LiveBeats.PubSub, presence: LiveBeatsWeb.Presence} ] # See https://hexdocs.pm/elixir/Supervisor.html diff --git a/lib/live_beats/presence/phoenix_presence_client.ex b/lib/live_beats/presence/phoenix_presence_client.ex index b663840..7912b9a 100644 --- a/lib/live_beats/presence/phoenix_presence_client.ex +++ b/lib/live_beats/presence/phoenix_presence_client.ex @@ -51,35 +51,101 @@ defmodule Phoenix.Presence.Client do defp track_pid(state, pid, topic, key, meta) do case Map.fetch(state.topics, topic) do - {:ok, presences} -> + {:ok, _topic_content} -> state.presence_mod.track(pid, topic, key, meta) - # update topics state... - # new_state - state + update_topics_state(:add_new_presence, state, topic, key, meta) :error -> # subscribe to topic we weren't yet tracking Phoenix.PubSub.subscribe(state.pubsub, topic) - # new_state - state + state.presence_mod.track(pid, topic, key, meta) + update_topics_state(:add_new_topic, state, topic, key, meta) end end defp untrack_pid(state, pid, topic, key) do state.presence_mod.untrack(pid, topic, key) # remove presence from state.topics - # if no more presences for given topic, unsubscribe - # Phoenix.PubSub.unsubscribe(state.pubsub, topic) - # new_state - state + if Map.has_key?(state.topics, topic) do + presences_count = + state.topics[topic] + |> Map.keys() + |> Enum.count() + + # if no more presences for given topic, unsubscribe + if presences_count == 0 do + Phoenix.PubSub.unsubscribe(state.pubsub, topic) + update_topics_state(:remove_topic, state, topic, key) + else + update_topics_state(:remove_presence, state, topic, key) + end + else + state + end end - defp merge_diff(state, topic, diff) do + # is a join + defp merge_diff(state, topic, %{leaves: leaves, joins: joins}) when map_size(leaves) == 0 do # merge diff into state.topics - # invoke state.client.handle_join|handle_leave + joined_key = Map.keys(joins) |> hd + joined_meta = joins[joined_key].metas |> hd + + state.client.handle_join(topic, joined_key, joined_meta, state) + + if Map.has_key?(state.topics, topic) do + update_topics_state(:add_new_presence, state, topic, joined_key, joined_meta) + else + update_topics_state(:add_new_topic, state, topic, joined_key, joined_meta) + end + end + + defp merge_diff(state, topic, %{leaves: leaves, joins: joins}) when map_size(joins) == 0 do + presences_count = + state.topics[topic] + |> Map.keys() + |> Enum.count() + + leaved_key = Map.keys(leaves) |> hd + left_meta = leaves[leaved_key].metas |> hd + + state.client.handle_leave(topic, leaved_key, left_meta, state) # if no more presences for given topic, unsubscribe - # Phoenix.PubSub.unsubscribe(state.pubsub, topic) - # new_state - state + if presences_count == 0 do + Phoenix.PubSub.unsubscribe(state.pubsub, topic) + update_topics_state(:remove_topic, state, topic, leaved_key) + else + update_topics_state(:remove_presence, state, topic, leaved_key) + end + end + + defp update_topics_state(:add_new_topic, %{topics: topics} = state, topic, key, meta) do + topic_presences = %{key => meta} + updated_topics = Map.put_new(topics, topic, topic_presences) + Map.put(state, :topics, updated_topics) + end + + defp update_topics_state(:add_new_presence, %{topics: topics} = state, topic, key, meta) do + updated_topic = + topics[topic] + |> Map.put_new(key, meta) + + updated_topics = Map.put(topics, topic, updated_topic) + + Map.put(state, :topics, updated_topics) + end + + defp update_topics_state(:remove_topic, %{topics: topics} = state, topic, _key) do + updated_topics = Map.delete(topics, topic) + Map.put(state, :topics, updated_topics) + end + + defp update_topics_state(:remove_presence, %{topics: topics} = state, topic, key) do + updated_presences = + topics[topic] + |> Map.delete(key) + + updated_topics = Map.put(topics, topic, updated_presences) + + Map.put(state, :topics, updated_topics) end end From 463faafe71bae460c49bc1c179b28156992b08ef Mon Sep 17 00:00:00 2001 From: Berenice Medel Date: Thu, 9 Dec 2021 01:29:00 -0600 Subject: [PATCH 04/19] Implement handle_leave and handle_join in presence_client - The profile Liveview is subscribed to active_users topic - user_joined and user_left events are sent - Users are added and removed from presences assign --- lib/live_beats/accounts.ex | 4 +++ lib/live_beats/presence/presence_client.ex | 31 +++++++++++++++---- lib/live_beats_web/channels/presence.ex | 9 ++++++ lib/live_beats_web/live/profile_live.ex | 35 ++++++++++++++-------- 4 files changed, 61 insertions(+), 18 deletions(-) diff --git a/lib/live_beats/accounts.ex b/lib/live_beats/accounts.ex index 9b3070e..7ec1f1a 100644 --- a/lib/live_beats/accounts.ex +++ b/lib/live_beats/accounts.ex @@ -21,6 +21,10 @@ defmodule LiveBeats.Accounts do Repo.all(from u in User, limit: ^Keyword.fetch!(opts, :limit)) end + def list_users_by_ids(user_ids) when is_list(user_ids) do + Repo.all(from u in User, where: u.id in ^user_ids) + end + def lists_users_by_active_profile(id, opts) do Repo.all( from u in User, where: u.active_profile_user_id == ^id, limit: ^Keyword.fetch!(opts, :limit) diff --git a/lib/live_beats/presence/presence_client.ex b/lib/live_beats/presence/presence_client.ex index 0c5cefc..fed70ad 100644 --- a/lib/live_beats/presence/presence_client.ex +++ b/lib/live_beats/presence/presence_client.ex @@ -1,7 +1,8 @@ defmodule LiveBeats.PresenceClient do @behaviour Phoenix.Presence.Client - @presence LiveBeats.Presence + @presence LiveBeatsWeb.Presence + @pubsub LiveBeats.PubSub def start_link(opts) do Phoenix.Presence.Client.start_link(presence: @presence, client: __MODULE__) @@ -16,13 +17,33 @@ defmodule LiveBeats.PresenceClient do {:ok, %{}} end - def handle_join(key, presence, state) do - # can local pubsub to LVs about new join + def handle_join(topic, key, meta, state) do + active_users_topic = + topic + |> profile_identifier() + |> active_users_topic() + + Phoenix.PubSub.broadcast!(@pubsub, active_users_topic, {__MODULE__, %{user_joined: key}}) + {:ok, state} end - def handle_leave(key, presence, state) do - # can local pubsub to LVs about new leave + def handle_leave(topic, key, presence, state) do + active_users_topic = + topic + |> profile_identifier() + |> active_users_topic() + + Phoenix.PubSub.broadcast!(@pubsub, active_users_topic, {__MODULE__, %{user_left: key}}) {:ok, state} end + + defp active_users_topic(user_id) do + "active_users:#{user_id}" + end + + defp profile_identifier(topic) do + "active_profile:" <> identifier = topic + identifier + end end diff --git a/lib/live_beats_web/channels/presence.ex b/lib/live_beats_web/channels/presence.ex index 9378943..adfd407 100644 --- a/lib/live_beats_web/channels/presence.ex +++ b/lib/live_beats_web/channels/presence.ex @@ -10,6 +10,7 @@ defmodule LiveBeatsWeb.Presence do import Phoenix.LiveView.Helpers import LiveBeatsWeb.LiveHelpers + @pubsub LiveBeats.PubSub def listening_now(assigns) do ~H""" @@ -31,4 +32,12 @@ defmodule LiveBeatsWeb.Presence do """ end + + def subscribe(user_id) do + Phoenix.PubSub.subscribe(@pubsub, topic(user_id)) + end + + defp topic(profile) do + "active_users:#{profile.user_id}" + end end diff --git a/lib/live_beats_web/live/profile_live.ex b/lib/live_beats_web/live/profile_live.ex index c206b36..b9ff901 100644 --- a/lib/live_beats_web/live/profile_live.ex +++ b/lib/live_beats_web/live/profile_live.ex @@ -87,6 +87,7 @@ defmodule LiveBeatsWeb.ProfileLive do if connected?(socket) do MediaLibrary.subscribe_to_profile(profile) Accounts.subscribe(current_user.id) + LiveBeatsWeb.Presence.subscribe(profile) Phoenix.Presence.Client.track(topic(profile.user_id), current_user.id, %{} @@ -146,6 +147,22 @@ defmodule LiveBeatsWeb.ProfileLive do {:noreply, socket} end + def handle_info({LiveBeats.PresenceClient, %{user_joined: user_id}}, socket) do + new_user = Accounts.get_user!(user_id) + updated_presences = + if new_user in socket.assigns.presences do + socket.assigns.presences + else + [new_user | socket.assigns.presences] + end + {:noreply, assign(socket, :presences, updated_presences)} + end + + def handle_info({LiveBeats.PresenceClient, %{user_left: user_id}}, socket) do + updated_presences = socket.assign.presences |> Enum.reject(fn user -> user.id == user_id end) + {:noreply, assign(socket, :presences, updated_presences)} + end + def handle_info({Accounts, %Accounts.Events.ActiveProfileChanged{} = event}, socket) do {:noreply, assign(socket, active_profile_id: event.new_profile_user_id)} end @@ -173,14 +190,6 @@ defmodule LiveBeatsWeb.ProfileLive do def handle_info({Accounts, _}, socket), do: {:noreply, socket} - def handle_info( - %{event: "presence_diff", payload: %{joins: _joins, leaves: _leaves}}, - %{assigns: %{presences: _users}} = socket - ) do - - {:noreply, assign_presences(socket)} - end - defp stop_song(socket, song_id) do SongRowComponent.send_status(song_id, :stopped) @@ -256,11 +265,11 @@ defmodule LiveBeatsWeb.ProfileLive do defp assign_presences(socket) do presences = socket.assigns.profile.user_id |> topic() - |> Presence.list() - |> Enum.map(fn {_user_id, user_data} -> - user_data[:metas] - |> List.first() + |> LiveBeats.PresenceClient.list() + |> Enum.map(fn {user_id, _user_data} -> + user_id end) + |> Accounts.list_users_by_ids() assign(socket, presences: presences) end @@ -272,5 +281,5 @@ defmodule LiveBeatsWeb.ProfileLive do uri.host <> uri.path end - defp topic(user_id) when is_integer(user_id), do: "profile:#{user_id}" + defp topic(user_id) when is_integer(user_id), do: "active_profile:#{user_id}" end From c22e104a244dd04378990bc880a55c64dad9c937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Berenice=20Medel=20S=C3=A1nchez?= Date: Thu, 9 Dec 2021 11:56:02 -0600 Subject: [PATCH 05/19] change key to string when tracking and untrackin presences --- .../presence/phoenix_presence_client.ex | 19 ++++++++++++------- lib/live_beats_web/live/profile_live.ex | 2 +- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/live_beats/presence/phoenix_presence_client.ex b/lib/live_beats/presence/phoenix_presence_client.ex index 7912b9a..c35a19e 100644 --- a/lib/live_beats/presence/phoenix_presence_client.ex +++ b/lib/live_beats/presence/phoenix_presence_client.ex @@ -15,11 +15,11 @@ defmodule Phoenix.Presence.Client do end def track(topic, key, meta) do - GenServer.call(PresenceClient, {:track, self(), topic, key, meta}) + GenServer.call(PresenceClient, {:track, self(), topic, to_string(key), meta}) end def untrack(topic, key) do - GenServer.call(PresenceClient, {:untrack, self(), topic, key}) + GenServer.call(PresenceClient, {:untrack, self(), to_string(topic), key}) end def init(opts) do @@ -41,6 +41,11 @@ defmodule Phoenix.Presence.Client do {:noreply, merge_diff(state, topic, diff)} end + def handle_call(:state, _from, state) do + IO.inspect(state.topics, label: :state_topics) + {:reply, :ok, state} + end + def handle_call({:track, pid, topic, key, meta}, _from, state) do {:reply, :ok, track_pid(state, pid, topic, key, meta)} end @@ -105,16 +110,16 @@ defmodule Phoenix.Presence.Client do |> Map.keys() |> Enum.count() - leaved_key = Map.keys(leaves) |> hd - left_meta = leaves[leaved_key].metas |> hd + left_key = Map.keys(leaves) |> hd + left_meta = leaves[left_key].metas |> hd - state.client.handle_leave(topic, leaved_key, left_meta, state) + state.client.handle_leave(topic, left_key, left_meta, state) # if no more presences for given topic, unsubscribe if presences_count == 0 do Phoenix.PubSub.unsubscribe(state.pubsub, topic) - update_topics_state(:remove_topic, state, topic, leaved_key) + update_topics_state(:remove_topic, state, topic, left_key) else - update_topics_state(:remove_presence, state, topic, leaved_key) + update_topics_state(:remove_presence, state, topic, left_key) end end diff --git a/lib/live_beats_web/live/profile_live.ex b/lib/live_beats_web/live/profile_live.ex index b9ff901..affaa3c 100644 --- a/lib/live_beats_web/live/profile_live.ex +++ b/lib/live_beats_web/live/profile_live.ex @@ -159,7 +159,7 @@ defmodule LiveBeatsWeb.ProfileLive do end def handle_info({LiveBeats.PresenceClient, %{user_left: user_id}}, socket) do - updated_presences = socket.assign.presences |> Enum.reject(fn user -> user.id == user_id end) + updated_presences = socket.assigns.presences |> Enum.reject(fn user -> user.id == user_id end) {:noreply, assign(socket, :presences, updated_presences)} end From 0579f9f2935d9b11518d4a69722e847070b7bdb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Berenice=20Medel=20S=C3=A1nchez?= Date: Fri, 10 Dec 2021 10:20:07 -0600 Subject: [PATCH 06/19] handle leaves and joins in the same function --- .../presence/phoenix_presence_client.ex | 90 ++++++++++--------- 1 file changed, 47 insertions(+), 43 deletions(-) diff --git a/lib/live_beats/presence/phoenix_presence_client.ex b/lib/live_beats/presence/phoenix_presence_client.ex index c35a19e..54edf74 100644 --- a/lib/live_beats/presence/phoenix_presence_client.ex +++ b/lib/live_beats/presence/phoenix_presence_client.ex @@ -55,16 +55,16 @@ defmodule Phoenix.Presence.Client do end defp track_pid(state, pid, topic, key, meta) do + #presences are handled when the presence_diff event received case Map.fetch(state.topics, topic) do {:ok, _topic_content} -> state.presence_mod.track(pid, topic, key, meta) - update_topics_state(:add_new_presence, state, topic, key, meta) - + state :error -> # subscribe to topic we weren't yet tracking Phoenix.PubSub.subscribe(state.pubsub, topic) state.presence_mod.track(pid, topic, key, meta) - update_topics_state(:add_new_topic, state, topic, key, meta) + state end end @@ -72,10 +72,7 @@ defmodule Phoenix.Presence.Client do state.presence_mod.untrack(pid, topic, key) # remove presence from state.topics if Map.has_key?(state.topics, topic) do - presences_count = - state.topics[topic] - |> Map.keys() - |> Enum.count() + presences_count = topic_presences_count(state, topic) # if no more presences for given topic, unsubscribe if presences_count == 0 do @@ -89,61 +86,62 @@ defmodule Phoenix.Presence.Client do end end - # is a join - defp merge_diff(state, topic, %{leaves: leaves, joins: joins}) when map_size(leaves) == 0 do + defp merge_diff(state, topic, %{leaves: leaves, joins: joins}) do + #add new topic if needed + updated_state = + if Map.has_key?(state.topics, topic) do + state + else + update_topics_state(:add_new_topic, state, topic) + end + # merge diff into state.topics - joined_key = Map.keys(joins) |> hd - joined_meta = joins[joined_key].metas |> hd + {updated_state, _topic} = Enum.reduce(joins, {updated_state, topic}, &handle_join/2) + {updated_state, _topic} = Enum.reduce(leaves, {updated_state, topic}, &handle_leave/2) - state.client.handle_join(topic, joined_key, joined_meta, state) - - if Map.has_key?(state.topics, topic) do - update_topics_state(:add_new_presence, state, topic, joined_key, joined_meta) - else - update_topics_state(:add_new_topic, state, topic, joined_key, joined_meta) - end - end - - defp merge_diff(state, topic, %{leaves: leaves, joins: joins}) when map_size(joins) == 0 do - presences_count = - state.topics[topic] - |> Map.keys() - |> Enum.count() - - left_key = Map.keys(leaves) |> hd - left_meta = leaves[left_key].metas |> hd - - state.client.handle_leave(topic, left_key, left_meta, state) - # if no more presences for given topic, unsubscribe - if presences_count == 0 do + # if no more presences for given topic, unsubscribe and remove topic + if topic_presences_count(updated_state, topic) == 0 do Phoenix.PubSub.unsubscribe(state.pubsub, topic) - update_topics_state(:remove_topic, state, topic, left_key) + update_topics_state(:remove_topic, updated_state, topic) else - update_topics_state(:remove_presence, state, topic, left_key) + updated_state end end - defp update_topics_state(:add_new_topic, %{topics: topics} = state, topic, key, meta) do - topic_presences = %{key => meta} - updated_topics = Map.put_new(topics, topic, topic_presences) + defp handle_join({joined_key, meta}, {state, topic}) do + joined_meta = Map.get(meta, :metas, []) + updated_state = update_topics_state(:add_new_presence, state, topic, joined_key, joined_meta) + state.client.handle_join(topic, joined_key, joined_meta, state) + {updated_state, topic} + end + + defp handle_leave({left_key, meta}, {state, topic}) do + left_meta = Map.get(meta, :metas, []) + updated_state = update_topics_state(:remove_presence, state, topic, left_key) + state.client.handle_leave(topic, left_key, meta, state) + {updated_state, topic} + end + + defp update_topics_state(:add_new_topic, %{topics: topics} = state, topic) do + updated_topics = Map.put_new(topics, topic, %{}) + Map.put(state, :topics, updated_topics) + end + + defp update_topics_state(:remove_topic, %{topics: topics} = state, topic) do + updated_topics = Map.delete(topics, topic) Map.put(state, :topics, updated_topics) end defp update_topics_state(:add_new_presence, %{topics: topics} = state, topic, key, meta) do updated_topic = topics[topic] - |> Map.put_new(key, meta) + |> Map.put(key, meta) updated_topics = Map.put(topics, topic, updated_topic) Map.put(state, :topics, updated_topics) end - defp update_topics_state(:remove_topic, %{topics: topics} = state, topic, _key) do - updated_topics = Map.delete(topics, topic) - Map.put(state, :topics, updated_topics) - end - defp update_topics_state(:remove_presence, %{topics: topics} = state, topic, key) do updated_presences = topics[topic] @@ -153,4 +151,10 @@ defmodule Phoenix.Presence.Client do Map.put(state, :topics, updated_topics) end + + defp topic_presences_count(state, topic) do + state.topics[topic] + |> Map.keys() + |> Enum.count() + end end From bdb4319f16cf1fddda864e5b3c7b3ed64bd5eff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Berenice=20Medel=20S=C3=A1nchez?= Date: Fri, 10 Dec 2021 10:38:05 -0600 Subject: [PATCH 07/19] unsubscribe is no more handled in untrack_pid --- .../presence/phoenix_presence_client.ex | 20 +++++-------------- lib/live_beats/presence/presence_client.ex | 4 ++-- lib/live_beats_web/live/profile_live.ex | 3 ++- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/lib/live_beats/presence/phoenix_presence_client.ex b/lib/live_beats/presence/phoenix_presence_client.ex index 54edf74..82c66d1 100644 --- a/lib/live_beats/presence/phoenix_presence_client.ex +++ b/lib/live_beats/presence/phoenix_presence_client.ex @@ -19,7 +19,7 @@ defmodule Phoenix.Presence.Client do end def untrack(topic, key) do - GenServer.call(PresenceClient, {:untrack, self(), to_string(topic), key}) + GenServer.call(PresenceClient, {:untrack, self(), topic, to_string(key)}) end def init(opts) do @@ -55,11 +55,12 @@ defmodule Phoenix.Presence.Client do end defp track_pid(state, pid, topic, key, meta) do - #presences are handled when the presence_diff event received + # presences are handled when the presence_diff event is received case Map.fetch(state.topics, topic) do {:ok, _topic_content} -> state.presence_mod.track(pid, topic, key, meta) state + :error -> # subscribe to topic we weren't yet tracking Phoenix.PubSub.subscribe(state.pubsub, topic) @@ -69,25 +70,15 @@ defmodule Phoenix.Presence.Client do end defp untrack_pid(state, pid, topic, key) do - state.presence_mod.untrack(pid, topic, key) - # remove presence from state.topics if Map.has_key?(state.topics, topic) do - presences_count = topic_presences_count(state, topic) - - # if no more presences for given topic, unsubscribe - if presences_count == 0 do - Phoenix.PubSub.unsubscribe(state.pubsub, topic) - update_topics_state(:remove_topic, state, topic, key) - else - update_topics_state(:remove_presence, state, topic, key) - end + state.presence_mod.untrack(pid, topic, key) else state end end defp merge_diff(state, topic, %{leaves: leaves, joins: joins}) do - #add new topic if needed + # add new topic if needed updated_state = if Map.has_key?(state.topics, topic) do state @@ -116,7 +107,6 @@ defmodule Phoenix.Presence.Client do end defp handle_leave({left_key, meta}, {state, topic}) do - left_meta = Map.get(meta, :metas, []) updated_state = update_topics_state(:remove_presence, state, topic, left_key) state.client.handle_leave(topic, left_key, meta, state) {updated_state, topic} diff --git a/lib/live_beats/presence/presence_client.ex b/lib/live_beats/presence/presence_client.ex index fed70ad..602730a 100644 --- a/lib/live_beats/presence/presence_client.ex +++ b/lib/live_beats/presence/presence_client.ex @@ -17,7 +17,7 @@ defmodule LiveBeats.PresenceClient do {:ok, %{}} end - def handle_join(topic, key, meta, state) do + def handle_join(topic, key, _meta, state) do active_users_topic = topic |> profile_identifier() @@ -28,7 +28,7 @@ defmodule LiveBeats.PresenceClient do {:ok, state} end - def handle_leave(topic, key, presence, state) do + def handle_leave(topic, key, _meta, state) do active_users_topic = topic |> profile_identifier() diff --git a/lib/live_beats_web/live/profile_live.ex b/lib/live_beats_web/live/profile_live.ex index affaa3c..e898324 100644 --- a/lib/live_beats_web/live/profile_live.ex +++ b/lib/live_beats_web/live/profile_live.ex @@ -159,7 +159,8 @@ defmodule LiveBeatsWeb.ProfileLive do end def handle_info({LiveBeats.PresenceClient, %{user_left: user_id}}, socket) do - updated_presences = socket.assigns.presences |> Enum.reject(fn user -> user.id == user_id end) + updated_presences = socket.assigns.presences + |> Enum.reject(fn user -> user.id == String.to_integer(user_id) end) {:noreply, assign(socket, :presences, updated_presences)} end From 6b5e82013090a6a6061ee2dda0e268a977a00779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Berenice=20Medel=20S=C3=A1nchez?= Date: Mon, 13 Dec 2021 12:33:13 -0600 Subject: [PATCH 08/19] initial tests added --- config/config.exs | 2 + config/test.exs | 2 + lib/live_beats/application.ex | 2 +- .../presence/phoenix_presence_client.ex | 3 +- .../presence/presence_client_test.exs | 49 +++++++++++++++++++ test/support/presence/client_mock.ex | 17 +++++++ test/support/presence/presence_mock.ex | 31 ++++++++++++ 7 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 test/live_beats/presence/presence_client_test.exs create mode 100644 test/support/presence/client_mock.ex create mode 100644 test/support/presence/presence_mock.ex diff --git a/config/config.exs b/config/config.exs index b937f88..e1ab7f1 100644 --- a/config/config.exs +++ b/config/config.exs @@ -58,6 +58,8 @@ config :logger, :console, # Use Jason for JSON parsing in Phoenix config :phoenix, :json_library, Jason +config :live_beats, :presence_client, LiveBeats.PresenceClient + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{config_env()}.exs" diff --git a/config/test.exs b/config/test.exs index 043e354..63469dc 100644 --- a/config/test.exs +++ b/config/test.exs @@ -31,3 +31,5 @@ config :logger, level: :warn # Initialize plugs at runtime for faster test compilation config :phoenix, :plug_init_mode, :runtime + +config :live_beats, :presence_client, Phoenix.Presence.Client.Mock diff --git a/lib/live_beats/application.ex b/lib/live_beats/application.ex index e46cbfe..067f171 100644 --- a/lib/live_beats/application.ex +++ b/lib/live_beats/application.ex @@ -23,7 +23,7 @@ defmodule LiveBeats.Application do LiveBeatsWeb.Endpoint, # Start a worker by calling: LiveBeats.Worker.start_link(arg) # {LiveBeats.Worker, arg} - {Phoenix.Presence.Client, client: LiveBeats.PresenceClient, pubsub: LiveBeats.PubSub, presence: LiveBeatsWeb.Presence} + {Phoenix.Presence.Client, client: Application.get_env(:live_beats, :presence_client), pubsub: LiveBeats.PubSub, presence: LiveBeatsWeb.Presence} ] # See https://hexdocs.pm/elixir/Supervisor.html diff --git a/lib/live_beats/presence/phoenix_presence_client.ex b/lib/live_beats/presence/phoenix_presence_client.ex index 82c66d1..9baea08 100644 --- a/lib/live_beats/presence/phoenix_presence_client.ex +++ b/lib/live_beats/presence/phoenix_presence_client.ex @@ -42,8 +42,7 @@ defmodule Phoenix.Presence.Client do end def handle_call(:state, _from, state) do - IO.inspect(state.topics, label: :state_topics) - {:reply, :ok, state} + {:reply, state, state} end def handle_call({:track, pid, topic, key, meta}, _from, state) do diff --git a/test/live_beats/presence/presence_client_test.exs b/test/live_beats/presence/presence_client_test.exs new file mode 100644 index 0000000..2ac01e5 --- /dev/null +++ b/test/live_beats/presence/presence_client_test.exs @@ -0,0 +1,49 @@ +defmodule Phoenix.Presence.ClientTest do + use ExUnit.Case, async: true + + alias Phoenix.Presence.Client.PresenceMock + + test "When a new process is tracked, a topic is created" do + presence_key = 1 + topic_id = 100 + + {:ok, pid} = PresenceMock.start_link(id: presence_key) + + PresenceMock.track(pid, topic(topic_id), presence_key) + assert Process.alive?(pid) + # _ = :sys.get_state(PresenceClient) + :timer.sleep(1000)# not the best + + assert %{topics: %{"mock_topic:100" => %{"1" => [%{phx_ref: _ref}]}}} = + GenServer.call(PresenceClient, :state) + + send(pid, :quit) + :timer.sleep(1000) + refute Process.alive?(pid) + end + + test "topic is removed from the topics state when there is no more presences" do + presence_key = 1 + topic_id = 100 + + {:ok, pid} = PresenceMock.start_link(id: presence_key) + + PresenceMock.track(pid, topic(topic_id), presence_key) + assert Process.alive?(pid) + # _ = :sys.get_state(PresenceClient) + + :timer.sleep(1000)# not the best + + assert %{topics: %{"mock_topic:100" => %{"1" => [%{phx_ref: _ref}]}}} = + GenServer.call(PresenceClient, :state) + + send(pid, :quit) + :timer.sleep(1000) + refute Process.alive?(pid) + assert %{topics: %{}} = GenServer.call(PresenceClient, :state) + end + + defp topic(id) do + "mock_topic:#{id}" + end +end diff --git a/test/support/presence/client_mock.ex b/test/support/presence/client_mock.ex new file mode 100644 index 0000000..6ac611b --- /dev/null +++ b/test/support/presence/client_mock.ex @@ -0,0 +1,17 @@ +defmodule Phoenix.Presence.Client.Mock do + + def init(_opts) do + {:ok, %{}} + end + + def handle_join(_topic, _key, _meta, state) do + IO.inspect(:handle_join) + {:ok, state} + end + + def handle_leave(_topic, _key, _meta, state) do + IO.inspect(:handle_leave) + {:ok, state} + end + +end diff --git a/test/support/presence/presence_mock.ex b/test/support/presence/presence_mock.ex new file mode 100644 index 0000000..91126e5 --- /dev/null +++ b/test/support/presence/presence_mock.ex @@ -0,0 +1,31 @@ +defmodule Phoenix.Presence.Client.PresenceMock do + + use GenServer + alias Phoenix.Presence.Client + + + def start_link(opts \\ []) do + GenServer.start_link(__MODULE__, opts[:id], opts) + end + + @impl true + def init(id) do + {:ok, %{id: id}} + end + + def track(pid, topic, key) do + GenServer.cast(pid, {:track, topic, key}) + end + + @impl true + def handle_info(:quit, state) do + IO.inspect(:quit) + {:stop, :normal, state} + end + + @impl true + def handle_cast({:track, topic, key}, state) do + Client.track(topic, key, %{}) + {:noreply, state} + end +end From defb8ccf922c36d1bd888d3f26dddfde34c41e3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Berenice=20Medel=20S=C3=A1nchez?= Date: Mon, 13 Dec 2021 14:15:29 -0600 Subject: [PATCH 09/19] update/remove metas --- .../presence/phoenix_presence_client.ex | 55 +++++++++++++++---- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/lib/live_beats/presence/phoenix_presence_client.ex b/lib/live_beats/presence/phoenix_presence_client.ex index 9baea08..0eff36b 100644 --- a/lib/live_beats/presence/phoenix_presence_client.ex +++ b/lib/live_beats/presence/phoenix_presence_client.ex @@ -100,13 +100,16 @@ defmodule Phoenix.Presence.Client do defp handle_join({joined_key, meta}, {state, topic}) do joined_meta = Map.get(meta, :metas, []) - updated_state = update_topics_state(:add_new_presence, state, topic, joined_key, joined_meta) + + updated_state = + update_topics_state(:add_new_presence_or_metas, state, topic, joined_key, joined_meta) + state.client.handle_join(topic, joined_key, joined_meta, state) {updated_state, topic} end defp handle_leave({left_key, meta}, {state, topic}) do - updated_state = update_topics_state(:remove_presence, state, topic, left_key) + updated_state = update_topics_state(:remove_presence_or_metas, state, topic, left_key, meta) state.client.handle_leave(topic, left_key, meta, state) {updated_state, topic} end @@ -121,22 +124,54 @@ defmodule Phoenix.Presence.Client do Map.put(state, :topics, updated_topics) end - defp update_topics_state(:add_new_presence, %{topics: topics} = state, topic, key, meta) do + defp update_topics_state( + :add_new_presence_or_metas, + %{topics: topics} = state, + topic, + key, + new_metas + ) do + topic_info = topics[topic] + updated_topic = - topics[topic] - |> Map.put(key, meta) + case Map.fetch(topic_info, key) do + # existing presence, add new metas + {:ok, existing_metas} -> + remaining_metas = new_metas -- existing_metas + updated_metas = existing_metas ++ remaining_metas + Map.put(topic_info, key, updated_metas) + + :error -> + # there are no presences for that key + Map.put(topic_info, key, new_metas) + end updated_topics = Map.put(topics, topic, updated_topic) Map.put(state, :topics, updated_topics) end - defp update_topics_state(:remove_presence, %{topics: topics} = state, topic, key) do - updated_presences = - topics[topic] - |> Map.delete(key) + defp update_topics_state( + :remove_presence_or_metas, + %{topics: topics} = state, + topic, + key, + deleted_metas + ) do + topic_info = topics[topic] - updated_topics = Map.put(topics, topic, updated_presences) + state_metas = Map.get(topic_info, key, []) + remaining_metas = state_metas -- Map.get(deleted_metas, :metas, []) + + updated_topic = + case remaining_metas do + # delete presence + [] -> Map.delete(topic_info, key) + # delete metas + _ -> Map.put(topic_info, key, remaining_metas) + end + + updated_topics = Map.put(topics, topic, updated_topic) Map.put(state, :topics, updated_topics) end From 9b5587d4841ce082c0c9cdfbdc0f124e5a91a84e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Berenice=20Medel=20S=C3=A1nchez?= Date: Tue, 14 Dec 2021 10:23:19 -0600 Subject: [PATCH 10/19] implement fetch callback to list users --- lib/live_beats/accounts.ex | 4 ++-- lib/live_beats/presence/phoenix_presence_client.ex | 4 +--- lib/live_beats_web/channels/presence.ex | 14 ++++++++++++++ lib/live_beats_web/live/profile_live.ex | 5 +---- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/lib/live_beats/accounts.ex b/lib/live_beats/accounts.ex index 7ec1f1a..6ec2ac0 100644 --- a/lib/live_beats/accounts.ex +++ b/lib/live_beats/accounts.ex @@ -21,8 +21,8 @@ defmodule LiveBeats.Accounts do Repo.all(from u in User, limit: ^Keyword.fetch!(opts, :limit)) end - def list_users_by_ids(user_ids) when is_list(user_ids) do - Repo.all(from u in User, where: u.id in ^user_ids) + def get_users_map(user_ids) when is_list(user_ids) do + Repo.all(from u in User, where: u.id in ^user_ids, select: {u.id, u}) end def lists_users_by_active_profile(id, opts) do diff --git a/lib/live_beats/presence/phoenix_presence_client.ex b/lib/live_beats/presence/phoenix_presence_client.ex index 0eff36b..e90bb32 100644 --- a/lib/live_beats/presence/phoenix_presence_client.ex +++ b/lib/live_beats/presence/phoenix_presence_client.ex @@ -177,8 +177,6 @@ defmodule Phoenix.Presence.Client do end defp topic_presences_count(state, topic) do - state.topics[topic] - |> Map.keys() - |> Enum.count() + map_size(state.topics[topic]) end end diff --git a/lib/live_beats_web/channels/presence.ex b/lib/live_beats_web/channels/presence.ex index adfd407..7593efc 100644 --- a/lib/live_beats_web/channels/presence.ex +++ b/lib/live_beats_web/channels/presence.ex @@ -12,6 +12,8 @@ defmodule LiveBeatsWeb.Presence do import LiveBeatsWeb.LiveHelpers @pubsub LiveBeats.PubSub + alias LiveBeats.Accounts + def listening_now(assigns) do ~H""" @@ -33,6 +35,18 @@ defmodule LiveBeatsWeb.Presence do """ end + def fetch(_topic, presences) do + users = + presences + |> Map.keys() + |> Accounts.get_users_map() + |> Enum.into(%{}) + + for {key, %{metas: metas}} <- presences, into: %{} do + {key, %{metas: metas, user: users[String.to_integer(key)]}} + end + end + def subscribe(user_id) do Phoenix.PubSub.subscribe(@pubsub, topic(user_id)) end diff --git a/lib/live_beats_web/live/profile_live.ex b/lib/live_beats_web/live/profile_live.ex index e898324..fae41f3 100644 --- a/lib/live_beats_web/live/profile_live.ex +++ b/lib/live_beats_web/live/profile_live.ex @@ -267,10 +267,7 @@ defmodule LiveBeatsWeb.ProfileLive do presences = socket.assigns.profile.user_id |> topic() |> LiveBeats.PresenceClient.list() - |> Enum.map(fn {user_id, _user_data} -> - user_id - end) - |> Accounts.list_users_by_ids() + |> Enum.map(fn {_key, meta} -> meta.user end) assign(socket, presences: presences) end From 028c619047e5e510f617358fa730b9fb66c73aca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Berenice=20Medel=20S=C3=A1nchez?= Date: Tue, 14 Dec 2021 10:27:55 -0600 Subject: [PATCH 11/19] keep client state in the phoenix presence client --- lib/live_beats/presence/phoenix_presence_client.ex | 11 ++++++++--- lib/live_beats/presence/presence_client.ex | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/live_beats/presence/phoenix_presence_client.ex b/lib/live_beats/presence/phoenix_presence_client.ex index e90bb32..1089e69 100644 --- a/lib/live_beats/presence/phoenix_presence_client.ex +++ b/lib/live_beats/presence/phoenix_presence_client.ex @@ -24,7 +24,7 @@ defmodule Phoenix.Presence.Client do def init(opts) do client = Keyword.fetch!(opts, :client) - client_state = client.init(%{}) + {:ok, client_state} = client.init(%{}) state = %{ topics: %{}, @@ -104,13 +104,18 @@ defmodule Phoenix.Presence.Client do updated_state = update_topics_state(:add_new_presence_or_metas, state, topic, joined_key, joined_meta) - state.client.handle_join(topic, joined_key, joined_meta, state) + {:ok, updated_client_state} = state.client.handle_join(topic, joined_key, joined_meta, state.client_state) + updated_state = Map.put(updated_state, :client_state, updated_client_state) + {updated_state, topic} end defp handle_leave({left_key, meta}, {state, topic}) do updated_state = update_topics_state(:remove_presence_or_metas, state, topic, left_key, meta) - state.client.handle_leave(topic, left_key, meta, state) + + {:ok, updated_client_state} = state.client.handle_leave(topic, left_key, meta, state.client_state) + updated_state = Map.put(updated_state, :client_state, updated_client_state) + {updated_state, topic} end diff --git a/lib/live_beats/presence/presence_client.ex b/lib/live_beats/presence/presence_client.ex index 602730a..4ce8d3f 100644 --- a/lib/live_beats/presence/presence_client.ex +++ b/lib/live_beats/presence/presence_client.ex @@ -23,7 +23,7 @@ defmodule LiveBeats.PresenceClient do |> profile_identifier() |> active_users_topic() - Phoenix.PubSub.broadcast!(@pubsub, active_users_topic, {__MODULE__, %{user_joined: key}}) + Phoenix.PubSub.local_broadcast(@pubsub, active_users_topic, {__MODULE__, %{user_joined: key}}) {:ok, state} end @@ -34,7 +34,7 @@ defmodule LiveBeats.PresenceClient do |> profile_identifier() |> active_users_topic() - Phoenix.PubSub.broadcast!(@pubsub, active_users_topic, {__MODULE__, %{user_left: key}}) + Phoenix.PubSub.local_broadcast(@pubsub, active_users_topic, {__MODULE__, %{user_left: key}}) {:ok, state} end From 0bde8f40d63393c7ecfad1dc9be260847892f33c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Berenice=20Medel=20S=C3=A1nchez?= Date: Tue, 14 Dec 2021 12:30:55 -0600 Subject: [PATCH 12/19] don't start presence client on tests --- config/config.exs | 2 -- config/test.exs | 2 -- lib/live_beats/application.ex | 10 ++++++++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/config/config.exs b/config/config.exs index e1ab7f1..b937f88 100644 --- a/config/config.exs +++ b/config/config.exs @@ -58,8 +58,6 @@ config :logger, :console, # Use Jason for JSON parsing in Phoenix config :phoenix, :json_library, Jason -config :live_beats, :presence_client, LiveBeats.PresenceClient - # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{config_env()}.exs" diff --git a/config/test.exs b/config/test.exs index 63469dc..043e354 100644 --- a/config/test.exs +++ b/config/test.exs @@ -31,5 +31,3 @@ config :logger, level: :warn # Initialize plugs at runtime for faster test compilation config :phoenix, :plug_init_mode, :runtime - -config :live_beats, :presence_client, Phoenix.Presence.Client.Mock diff --git a/lib/live_beats/application.ex b/lib/live_beats/application.ex index 067f171..bdd0725 100644 --- a/lib/live_beats/application.ex +++ b/lib/live_beats/application.ex @@ -23,8 +23,8 @@ defmodule LiveBeats.Application do LiveBeatsWeb.Endpoint, # Start a worker by calling: LiveBeats.Worker.start_link(arg) # {LiveBeats.Worker, arg} - {Phoenix.Presence.Client, client: Application.get_env(:live_beats, :presence_client), pubsub: LiveBeats.PubSub, presence: LiveBeatsWeb.Presence} - ] + + ] ++ start_presence_client(Mix.env) # See https://hexdocs.pm/elixir/Supervisor.html # for other strategies and supported options @@ -39,4 +39,10 @@ defmodule LiveBeats.Application do LiveBeatsWeb.Endpoint.config_change(changed, removed) :ok end + + defp start_presence_client(:test), do: [] + + defp start_presence_client(_) do + [{Phoenix.Presence.Client, client: LiveBeats.PresenceClient, pubsub: LiveBeats.PubSub, presence: LiveBeatsWeb.Presence, name: PresenceClient}] + end end From 84d4eead7ac798581cd6a05d8cbb69921ba55c18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Berenice=20Medel=20S=C3=A1nchez?= Date: Tue, 14 Dec 2021 12:32:51 -0600 Subject: [PATCH 13/19] use genserver pid or name in client API --- .../presence/phoenix_presence_client.ex | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/live_beats/presence/phoenix_presence_client.ex b/lib/live_beats/presence/phoenix_presence_client.ex index 1089e69..e4d945c 100644 --- a/lib/live_beats/presence/phoenix_presence_client.ex +++ b/lib/live_beats/presence/phoenix_presence_client.ex @@ -11,15 +11,20 @@ defmodule Phoenix.Presence.Client do * `:client` - The required callback module """ def start_link(opts) do - GenServer.start_link(__MODULE__, opts, name: PresenceClient) + case Keyword.fetch(opts, :name) do + {:ok, name} -> + GenServer.start_link(__MODULE__, opts, name: name) + + :error -> GenServer.start_link(__MODULE__, opts) + end end - def track(topic, key, meta) do - GenServer.call(PresenceClient, {:track, self(), topic, to_string(key), meta}) + def track(pid \\ PresenceClient, topic, key, meta) do + GenServer.call(pid, {:track, self(), topic, to_string(key), meta}) end - def untrack(topic, key) do - GenServer.call(PresenceClient, {:untrack, self(), topic, to_string(key)}) + def untrack(pid \\ PresenceClient, topic, key) do + GenServer.call(pid, {:untrack, self(), topic, to_string(key)}) end def init(opts) do From 8dfb96c78a8be069698100d66c5b2958a6936d7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Berenice=20Medel=20S=C3=A1nchez?= Date: Tue, 14 Dec 2021 16:14:37 -0600 Subject: [PATCH 14/19] fix tests --- .../presence/presence_client_test.exs | 74 ++++++++++++------- test/support/presence/presence_mock.ex | 8 +- 2 files changed, 53 insertions(+), 29 deletions(-) diff --git a/test/live_beats/presence/presence_client_test.exs b/test/live_beats/presence/presence_client_test.exs index 2ac01e5..bedc562 100644 --- a/test/live_beats/presence/presence_client_test.exs +++ b/test/live_beats/presence/presence_client_test.exs @@ -1,46 +1,70 @@ defmodule Phoenix.Presence.ClientTest do - use ExUnit.Case, async: true + use ExUnit.Case alias Phoenix.Presence.Client.PresenceMock + alias Phoenix.Presence.Client - test "When a new process is tracked, a topic is created" do + @pubsub LiveBeats.PubSub + @client Phoenix.Presence.Client.Mock + @presence LiveBeatsWeb.Presence + + @presence_client_opts [client: @client, pubsub: @pubsub, presence: @presence] + + setup tags do + pid = Ecto.Adapters.SQL.Sandbox.start_owner!(LiveBeats.Repo, shared: not tags[:async]) + on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end) + + :ok + end + + test "When a new process is tracked, a topic key is added to the topics state" do presence_key = 1 - topic_id = 100 + topic = topic(100) - {:ok, pid} = PresenceMock.start_link(id: presence_key) + {:ok, presence_client} = Client.start_link(@presence_client_opts) + {:ok, presence_process} = PresenceMock.start_link(id: presence_key) - PresenceMock.track(pid, topic(topic_id), presence_key) - assert Process.alive?(pid) - # _ = :sys.get_state(PresenceClient) - :timer.sleep(1000)# not the best + Phoenix.PubSub.subscribe(@pubsub, topic) + Process.monitor(presence_process) - assert %{topics: %{"mock_topic:100" => %{"1" => [%{phx_ref: _ref}]}}} = - GenServer.call(PresenceClient, :state) + PresenceMock.track(presence_client, presence_process, topic, presence_key) - send(pid, :quit) - :timer.sleep(1000) - refute Process.alive?(pid) + assert Process.alive?(presence_process) + + assert_receive %{event: "presence_diff"} + + client_state = :sys.get_state(presence_client) + + assert %{topics: %{^topic => %{"1" => [%{phx_ref: _ref}]}}} = client_state end test "topic is removed from the topics state when there is no more presences" do presence_key = 1 - topic_id = 100 + topic = topic(100) - {:ok, pid} = PresenceMock.start_link(id: presence_key) + {:ok, presence_client} = Client.start_link(@presence_client_opts) + {:ok, presence_process} = PresenceMock.start_link(id: presence_key) - PresenceMock.track(pid, topic(topic_id), presence_key) - assert Process.alive?(pid) - # _ = :sys.get_state(PresenceClient) + Phoenix.PubSub.subscribe(@pubsub, topic) + Process.monitor(presence_process) - :timer.sleep(1000)# not the best + PresenceMock.track(presence_client, presence_process, topic, presence_key) - assert %{topics: %{"mock_topic:100" => %{"1" => [%{phx_ref: _ref}]}}} = - GenServer.call(PresenceClient, :state) + assert Process.alive?(presence_process) - send(pid, :quit) - :timer.sleep(1000) - refute Process.alive?(pid) - assert %{topics: %{}} = GenServer.call(PresenceClient, :state) + assert_receive %{event: "presence_diff"} + + client_state = :sys.get_state(presence_client) + + assert %{topics: %{^topic => %{"1" => [%{phx_ref: _ref}]}}} = client_state + + send(presence_process, :quit) + + assert_receive {:DOWN, _ref, :process, ^presence_process, _reason} + + client_state = :sys.get_state(presence_client) + + assert %{topics: %{}} = client_state end defp topic(id) do diff --git a/test/support/presence/presence_mock.ex b/test/support/presence/presence_mock.ex index 91126e5..f882ec9 100644 --- a/test/support/presence/presence_mock.ex +++ b/test/support/presence/presence_mock.ex @@ -13,8 +13,8 @@ defmodule Phoenix.Presence.Client.PresenceMock do {:ok, %{id: id}} end - def track(pid, topic, key) do - GenServer.cast(pid, {:track, topic, key}) + def track(client_pid, pid, topic, key) do + GenServer.cast(pid, {:track, client_pid, topic, key}) end @impl true @@ -24,8 +24,8 @@ defmodule Phoenix.Presence.Client.PresenceMock do end @impl true - def handle_cast({:track, topic, key}, state) do - Client.track(topic, key, %{}) + def handle_cast({:track, client_pid, topic, key}, state) do + Client.track(client_pid, topic, key, %{}) {:noreply, state} end end From 4d0665679c29a3ba509e776312d04822ed68a1fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Berenice=20Medel=20S=C3=A1nchez?= Date: Wed, 15 Dec 2021 12:40:37 -0600 Subject: [PATCH 15/19] add metas tests --- .../presence/presence_client_test.exs | 60 ++++++++++++++++--- test/support/presence/presence_mock.ex | 8 +-- 2 files changed, 55 insertions(+), 13 deletions(-) diff --git a/test/live_beats/presence/presence_client_test.exs b/test/live_beats/presence/presence_client_test.exs index bedc562..9fcb26f 100644 --- a/test/live_beats/presence/presence_client_test.exs +++ b/test/live_beats/presence/presence_client_test.exs @@ -28,13 +28,9 @@ defmodule Phoenix.Presence.ClientTest do Process.monitor(presence_process) PresenceMock.track(presence_client, presence_process, topic, presence_key) - - assert Process.alive?(presence_process) - assert_receive %{event: "presence_diff"} client_state = :sys.get_state(presence_client) - assert %{topics: %{^topic => %{"1" => [%{phx_ref: _ref}]}}} = client_state end @@ -49,24 +45,70 @@ defmodule Phoenix.Presence.ClientTest do Process.monitor(presence_process) PresenceMock.track(presence_client, presence_process, topic, presence_key) - assert Process.alive?(presence_process) - assert_receive %{event: "presence_diff"} client_state = :sys.get_state(presence_client) - assert %{topics: %{^topic => %{"1" => [%{phx_ref: _ref}]}}} = client_state send(presence_process, :quit) - assert_receive {:DOWN, _ref, :process, ^presence_process, _reason} client_state = :sys.get_state(presence_client) - assert %{topics: %{}} = client_state end + test "When there are two presences for the same key, the metas are accumulated" do + presence_key = 1 + topic = topic(100) + + {:ok, presence_client} = Client.start_link(@presence_client_opts) + {:ok, presence_process_1} = PresenceMock.start_link(id: presence_key) + {:ok, presence_process_2} = PresenceMock.start_link(id: presence_key) + + Phoenix.PubSub.subscribe(@pubsub, topic) + + PresenceMock.track(presence_client, presence_process_1, topic, presence_key, %{m1: :m1}) + assert_receive %{event: "presence_diff"} + + PresenceMock.track(presence_client, presence_process_2, topic, presence_key, %{m2: :m2}) + assert_receive %{event: "presence_diff"} + + + client_state = :sys.get_state(presence_client) + + assert %{topics: %{^topic => %{"1" => [%{m1: :m1}, %{m2: :m2}]}}} = client_state + end + + test "When there are two presences for the same key and one leaves, just the meta is deleted" do + presence_key = 1 + topic = topic(100) + + {:ok, presence_client} = Client.start_link(@presence_client_opts) + {:ok, presence_process_1} = PresenceMock.start_link(id: presence_key) + {:ok, presence_process_2} = PresenceMock.start_link(id: presence_key) + + Phoenix.PubSub.subscribe(@pubsub, topic) + Process.monitor(presence_process_1) + + PresenceMock.track(presence_client, presence_process_1, topic, presence_key, %{m1: :m1}) + assert_receive %{event: "presence_diff"} + + PresenceMock.track(presence_client, presence_process_2, topic, presence_key, %{m2: :m2}) + assert_receive %{event: "presence_diff"} + + + client_state = :sys.get_state(presence_client) + assert %{topics: %{^topic => %{"1" => [%{m1: :m1}, %{m2: :m2}]}}} = client_state + + send(presence_process_1, :quit) + assert_receive {:DOWN, _ref, :process, ^presence_process_1, _reason} + assert_receive %{event: "presence_diff"} + + client_state = :sys.get_state(presence_client) + assert %{topics: %{^topic => %{"1" => [%{m2: :m2}]}}} = client_state + end + defp topic(id) do "mock_topic:#{id}" end diff --git a/test/support/presence/presence_mock.ex b/test/support/presence/presence_mock.ex index f882ec9..e693d91 100644 --- a/test/support/presence/presence_mock.ex +++ b/test/support/presence/presence_mock.ex @@ -13,8 +13,8 @@ defmodule Phoenix.Presence.Client.PresenceMock do {:ok, %{id: id}} end - def track(client_pid, pid, topic, key) do - GenServer.cast(pid, {:track, client_pid, topic, key}) + def track(client_pid, pid, topic, key, meta \\ %{}) do + GenServer.cast(pid, {:track, client_pid, topic, key, meta}) end @impl true @@ -24,8 +24,8 @@ defmodule Phoenix.Presence.Client.PresenceMock do end @impl true - def handle_cast({:track, client_pid, topic, key}, state) do - Client.track(client_pid, topic, key, %{}) + def handle_cast({:track, client_pid, topic, key, meta}, state) do + Client.track(client_pid, topic, key, meta) {:noreply, state} end end From dcaf0240429ef0b16fa7bc4bace0734250265bec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Berenice=20Medel=20S=C3=A1nchez?= Date: Wed, 15 Dec 2021 12:44:10 -0600 Subject: [PATCH 16/19] change tests description --- test/live_beats/presence/presence_client_test.exs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/live_beats/presence/presence_client_test.exs b/test/live_beats/presence/presence_client_test.exs index 9fcb26f..f332063 100644 --- a/test/live_beats/presence/presence_client_test.exs +++ b/test/live_beats/presence/presence_client_test.exs @@ -17,7 +17,7 @@ defmodule Phoenix.Presence.ClientTest do :ok end - test "When a new process is tracked, a topic key is added to the topics state" do + test "A topic key is added to the topics state when a new process is tracked" do presence_key = 1 topic = topic(100) @@ -58,7 +58,7 @@ defmodule Phoenix.Presence.ClientTest do assert %{topics: %{}} = client_state end - test "When there are two presences for the same key, the metas are accumulated" do + test "metas are accumulated when there are two presences for the same key" do presence_key = 1 topic = topic(100) @@ -80,7 +80,7 @@ defmodule Phoenix.Presence.ClientTest do assert %{topics: %{^topic => %{"1" => [%{m1: :m1}, %{m2: :m2}]}}} = client_state end - test "When there are two presences for the same key and one leaves, just the meta is deleted" do + test "Just one meta is deleted when there are two presences for the same key and one leaves" do presence_key = 1 topic = topic(100) From b8bbba8ecb1a233a7e1a5a70a2839f5e2f0f782d Mon Sep 17 00:00:00 2001 From: Chris McCord Date: Thu, 16 Dec 2021 12:19:02 -0500 Subject: [PATCH 17/19] Fix tests --- lib/live_beats/application.ex | 19 ++++++----- mix.exs | 2 +- mix.lock | 4 +-- .../presence/presence_client_test.exs | 32 +++++++++++-------- .../live_beats_web/live/profile_live_test.exs | 20 ++++++------ test/support/conn_case.ex | 14 ++++++++ test/support/presence/presence_mock.ex | 1 - 7 files changed, 54 insertions(+), 38 deletions(-) diff --git a/lib/live_beats/application.ex b/lib/live_beats/application.ex index bdd0725..e6ff89f 100644 --- a/lib/live_beats/application.ex +++ b/lib/live_beats/application.ex @@ -17,14 +17,19 @@ defmodule LiveBeats.Application do LiveBeatsWeb.Telemetry, # Start the PubSub system {Phoenix.PubSub, name: LiveBeats.PubSub}, - #start presence + # start presence LiveBeatsWeb.Presence, + {Phoenix.Presence.Client, + client: LiveBeats.PresenceClient, + pubsub: LiveBeats.PubSub, + presence: LiveBeatsWeb.Presence, + name: PresenceClient}, # Start the Endpoint (http/https) - LiveBeatsWeb.Endpoint, + LiveBeatsWeb.Endpoint + # Start a worker by calling: LiveBeats.Worker.start_link(arg) # {LiveBeats.Worker, arg} - - ] ++ start_presence_client(Mix.env) + ] # See https://hexdocs.pm/elixir/Supervisor.html # for other strategies and supported options @@ -39,10 +44,4 @@ defmodule LiveBeats.Application do LiveBeatsWeb.Endpoint.config_change(changed, removed) :ok end - - defp start_presence_client(:test), do: [] - - defp start_presence_client(_) do - [{Phoenix.Presence.Client, client: LiveBeats.PresenceClient, pubsub: LiveBeats.PubSub, presence: LiveBeatsWeb.Presence, name: PresenceClient}] - end end diff --git a/mix.exs b/mix.exs index 47d8fc8..9fb21cb 100644 --- a/mix.exs +++ b/mix.exs @@ -33,7 +33,7 @@ defmodule LiveBeats.MixProject do # Type `mix help deps` for examples and options. defp deps do [ - {:phoenix, "~> 1.6.0"}, + {:phoenix, github: "phoenixframework/phoenix", override: true}, {:phoenix_ecto, "~> 4.4"}, {:ecto_sql, "~> 3.6"}, {:postgrex, ">= 0.0.0"}, diff --git a/mix.lock b/mix.lock index e8670e3..3f22b01 100644 --- a/mix.lock +++ b/mix.lock @@ -20,12 +20,12 @@ "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, "mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"}, "mint": {:hex, :mint, "1.3.0", "396b3301102f7b775e103da5a20494b25753aed818d6d6f0ad222a3a018c3600", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "a9aac960562e43ca69a77e5176576abfa78b8398cec5543dd4fb4ab0131d5c1e"}, - "phoenix": {:hex, :phoenix, "1.6.2", "6cbd5c8ed7a797f25a919a37fafbc2fb1634c9cdb12a4448d7a5d0b26926f005", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7bbee475acae0c3abc229b7f189e210ea788e63bd168e585f60c299a4b2f9133"}, + "phoenix": {:git, "https://github.com/phoenixframework/phoenix.git", "8d2b33ac9691bd624ede602088d213f89600d233", []}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"}, "phoenix_html": {:hex, :phoenix_html, "3.1.0", "0b499df05aad27160d697a9362f0e89fa0e24d3c7a9065c2bd9d38b4d1416c09", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0c0a98a2cefa63433657983a2a594c7dee5927e4391e0f1bfd3a151d1def33fc"}, "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.5.0", "3282d8646e1bfc1ef1218f508d9fcefd48cf47f9081b7667bd9b281b688a49cf", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.6", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.16.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "609740be43de94ae0abd2c4300ff0356a6e8a9487bf340e69967643a59fa7ec8"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"}, - "phoenix_live_view": {:git, "https://github.com/phoenixframework/phoenix_live_view.git", "5409845a27938924c0d9a6267b498438a9103295", []}, + "phoenix_live_view": {:git, "https://github.com/phoenixframework/phoenix_live_view.git", "d250ad2efd9159c0866def5f5d666bdeeb22ac90", []}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, "phoenix_view": {:hex, :phoenix_view, "1.0.0", "fea71ecaaed71178b26dd65c401607de5ec22e2e9ef141389c721b3f3d4d8011", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "82be3e2516f5633220246e2e58181282c71640dab7afc04f70ad94253025db0c"}, "plug": {:hex, :plug, "1.12.1", "645678c800601d8d9f27ad1aebba1fdb9ce5b2623ddb961a074da0b96c35187d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d57e799a777bc20494b784966dc5fbda91eb4a09f571f76545b72a634ce0d30b"}, diff --git a/test/live_beats/presence/presence_client_test.exs b/test/live_beats/presence/presence_client_test.exs index f332063..5443320 100644 --- a/test/live_beats/presence/presence_client_test.exs +++ b/test/live_beats/presence/presence_client_test.exs @@ -1,3 +1,8 @@ +defmodule Phoenix.Presence.ClientTest.Presence do + use Phoenix.Presence, otp_app: :live_beats, + pubsub_server: LiveBeats.PubSub +end + defmodule Phoenix.Presence.ClientTest do use ExUnit.Case @@ -6,11 +11,12 @@ defmodule Phoenix.Presence.ClientTest do @pubsub LiveBeats.PubSub @client Phoenix.Presence.Client.Mock - @presence LiveBeatsWeb.Presence + @presence Phoenix.Presence.ClientTest.Presence @presence_client_opts [client: @client, pubsub: @pubsub, presence: @presence] setup tags do + start_supervised!({@presence, []}) pid = Ecto.Adapters.SQL.Sandbox.start_owner!(LiveBeats.Repo, shared: not tags[:async]) on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end) @@ -21,8 +27,8 @@ defmodule Phoenix.Presence.ClientTest do presence_key = 1 topic = topic(100) - {:ok, presence_client} = Client.start_link(@presence_client_opts) - {:ok, presence_process} = PresenceMock.start_link(id: presence_key) + {:ok, presence_client} = start_supervised({Client, @presence_client_opts}) + {:ok, presence_process} = start_supervised({PresenceMock, id: presence_key}) Phoenix.PubSub.subscribe(@pubsub, topic) Process.monitor(presence_process) @@ -38,8 +44,8 @@ defmodule Phoenix.Presence.ClientTest do presence_key = 1 topic = topic(100) - {:ok, presence_client} = Client.start_link(@presence_client_opts) - {:ok, presence_process} = PresenceMock.start_link(id: presence_key) + {:ok, presence_client} = start_supervised({Client, @presence_client_opts}) + {:ok, presence_process} = start_supervised({PresenceMock, id: presence_key}) Phoenix.PubSub.subscribe(@pubsub, topic) Process.monitor(presence_process) @@ -62,9 +68,9 @@ defmodule Phoenix.Presence.ClientTest do presence_key = 1 topic = topic(100) - {:ok, presence_client} = Client.start_link(@presence_client_opts) - {:ok, presence_process_1} = PresenceMock.start_link(id: presence_key) - {:ok, presence_process_2} = PresenceMock.start_link(id: presence_key) + {:ok, presence_client} = start_supervised({Client, @presence_client_opts}) + {:ok, presence_process_1} = start_supervised({PresenceMock, id: presence_key}, id: :mock_1) + {:ok, presence_process_2} = start_supervised({PresenceMock, id: presence_key}, id: :mock_2) Phoenix.PubSub.subscribe(@pubsub, topic) @@ -74,7 +80,6 @@ defmodule Phoenix.Presence.ClientTest do PresenceMock.track(presence_client, presence_process_2, topic, presence_key, %{m2: :m2}) assert_receive %{event: "presence_diff"} - client_state = :sys.get_state(presence_client) assert %{topics: %{^topic => %{"1" => [%{m1: :m1}, %{m2: :m2}]}}} = client_state @@ -84,9 +89,9 @@ defmodule Phoenix.Presence.ClientTest do presence_key = 1 topic = topic(100) - {:ok, presence_client} = Client.start_link(@presence_client_opts) - {:ok, presence_process_1} = PresenceMock.start_link(id: presence_key) - {:ok, presence_process_2} = PresenceMock.start_link(id: presence_key) + {:ok, presence_client} = start_supervised({Client, @presence_client_opts}) + {:ok, presence_process_1} = start_supervised({PresenceMock, id: presence_key}, id: :mock_1) + {:ok, presence_process_2} = start_supervised({PresenceMock, id: presence_key}, id: :mock_2) Phoenix.PubSub.subscribe(@pubsub, topic) Process.monitor(presence_process_1) @@ -97,7 +102,6 @@ defmodule Phoenix.Presence.ClientTest do PresenceMock.track(presence_client, presence_process_2, topic, presence_key, %{m2: :m2}) assert_receive %{event: "presence_diff"} - client_state = :sys.get_state(presence_client) assert %{topics: %{^topic => %{"1" => [%{m1: :m1}, %{m2: :m2}]}}} = client_state @@ -106,7 +110,7 @@ defmodule Phoenix.Presence.ClientTest do assert_receive %{event: "presence_diff"} client_state = :sys.get_state(presence_client) - assert %{topics: %{^topic => %{"1" => [%{m2: :m2}]}}} = client_state + assert %{topics: %{^topic => %{"1" => [%{m2: :m2}]}}} = client_state end defp topic(id) do diff --git a/test/live_beats_web/live/profile_live_test.exs b/test/live_beats_web/live/profile_live_test.exs index 6833ab6..69aeb50 100644 --- a/test/live_beats_web/live/profile_live_test.exs +++ b/test/live_beats_web/live/profile_live_test.exs @@ -23,8 +23,8 @@ defmodule LiveBeatsWeb.ProfileLiveTest do # uploads assert lv - |> element("#upload-btn") - |> render_click() + |> element("#upload-btn") + |> render_click() assert render(lv) =~ "Add Songs" @@ -43,16 +43,16 @@ defmodule LiveBeatsWeb.ProfileLiveTest do [%{"ref" => ref}] = mp3.entries refute lv - |> form("#song-form") - |> render_change(%{ - "_target" => ["songs", ref, "artist"], - "songs" => %{ - ref => %{"artist" => "Anon", "attribution" => "", "title" => "silence1s"} - } - }) =~ "can't be blank" + |> form("#song-form") + |> render_change(%{ + "_target" => ["songs", ref, "artist"], + "songs" => %{ + ref => %{"artist" => "Anon", "attribution" => "", "title" => "silence1s"} + } + }) =~ "can't be blank" assert {:ok, new_lv, html} = - lv |> form("#song-form") |> render_submit() |> follow_redirect(conn) + lv |> form("#song-form") |> render_submit() |> follow_redirect(conn) assert_redirected(lv, "/#{current_user.username}") assert html =~ "1 song(s) uploaded" diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 0c82483..bc1415c 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -35,9 +35,23 @@ defmodule LiveBeatsWeb.ConnCase do end end + defp wait_for_children(children_lookup) when is_function(children_lookup) do + Process.sleep(100) + + for pid <- children_lookup.() do + ref = Process.monitor(pid) + assert_receive {:DOWN, ^ref, _, _, _}, 1000 + end + end + setup tags do pid = Ecto.Adapters.SQL.Sandbox.start_owner!(LiveBeats.Repo, shared: not tags[:async]) on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end) + + on_exit(fn -> + wait_for_children(fn -> LiveBeatsWeb.Presence.fetchers_pids() end) + end) + {:ok, conn: Phoenix.ConnTest.build_conn()} end diff --git a/test/support/presence/presence_mock.ex b/test/support/presence/presence_mock.ex index e693d91..9df5b52 100644 --- a/test/support/presence/presence_mock.ex +++ b/test/support/presence/presence_mock.ex @@ -1,5 +1,4 @@ defmodule Phoenix.Presence.Client.PresenceMock do - use GenServer alias Phoenix.Presence.Client From a65c789748fedbc1293104b4ca99b2c7a93b97ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Berenice=20Medel=20S=C3=A1nchez?= Date: Thu, 16 Dec 2021 12:30:21 -0600 Subject: [PATCH 18/19] add presence client behaviour callbacks --- lib/live_beats/presence/phoenix_presence_client.ex | 4 ++++ lib/live_beats/presence/presence_client.ex | 7 +++---- test/support/presence/client_mock.ex | 2 -- test/support/presence/presence_mock.ex | 1 - 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/live_beats/presence/phoenix_presence_client.ex b/lib/live_beats/presence/phoenix_presence_client.ex index e4d945c..c8187e8 100644 --- a/lib/live_beats/presence/phoenix_presence_client.ex +++ b/lib/live_beats/presence/phoenix_presence_client.ex @@ -1,6 +1,10 @@ defmodule Phoenix.Presence.Client do use GenServer + @callback init(state :: term) :: {:ok, new_state :: term} + @callback handle_join(topic :: String.t(), key :: String.t(), meta :: [map()], state :: term) :: {:ok, term} + @callback handle_leave(topic :: String.t(), key :: String.t(), meta :: [map()], state :: term) :: {:ok, term} + @doc """ TODO diff --git a/lib/live_beats/presence/presence_client.ex b/lib/live_beats/presence/presence_client.ex index 4ce8d3f..17280b2 100644 --- a/lib/live_beats/presence/presence_client.ex +++ b/lib/live_beats/presence/presence_client.ex @@ -4,19 +4,17 @@ defmodule LiveBeats.PresenceClient do @presence LiveBeatsWeb.Presence @pubsub LiveBeats.PubSub - def start_link(opts) do - Phoenix.Presence.Client.start_link(presence: @presence, client: __MODULE__) - end - def list(topic) do @presence.list(topic) end + @impl Phoenix.Presence.Client def init(_opts) do # user-land state {:ok, %{}} end + @impl Phoenix.Presence.Client def handle_join(topic, key, _meta, state) do active_users_topic = topic @@ -28,6 +26,7 @@ defmodule LiveBeats.PresenceClient do {:ok, state} end + @impl Phoenix.Presence.Client def handle_leave(topic, key, _meta, state) do active_users_topic = topic diff --git a/test/support/presence/client_mock.ex b/test/support/presence/client_mock.ex index 6ac611b..fadacb7 100644 --- a/test/support/presence/client_mock.ex +++ b/test/support/presence/client_mock.ex @@ -5,12 +5,10 @@ defmodule Phoenix.Presence.Client.Mock do end def handle_join(_topic, _key, _meta, state) do - IO.inspect(:handle_join) {:ok, state} end def handle_leave(_topic, _key, _meta, state) do - IO.inspect(:handle_leave) {:ok, state} end diff --git a/test/support/presence/presence_mock.ex b/test/support/presence/presence_mock.ex index 9df5b52..f05dadd 100644 --- a/test/support/presence/presence_mock.ex +++ b/test/support/presence/presence_mock.ex @@ -18,7 +18,6 @@ defmodule Phoenix.Presence.Client.PresenceMock do @impl true def handle_info(:quit, state) do - IO.inspect(:quit) {:stop, :normal, state} end From 9998e06caadcc4a5390f5a3a51748af6606add3a Mon Sep 17 00:00:00 2001 From: Chris McCord Date: Tue, 11 Jan 2022 14:57:06 -0500 Subject: [PATCH 19/19] Optimize presence. Avoid fetching each user by passing in full pre-fetched presences from Presence.fetch/2 callback. Use temporary assigns in ProfileLive to avoid duping presences in memeory. Handle removes by a small hook event --- assets/js/app.js | 1 + .../presence/phoenix_presence_client.ex | 17 +++++++--- lib/live_beats/presence/presence_client.ex | 17 +++++++--- lib/live_beats_web/channels/presence.ex | 17 +++++++--- lib/live_beats_web/live/profile_live.ex | 34 ++++++++----------- .../live_beats_web/live/profile_live_test.exs | 11 ++---- 6 files changed, 56 insertions(+), 41 deletions(-) diff --git a/assets/js/app.js b/assets/js/app.js index 7abeb7f..08093cd 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -195,6 +195,7 @@ window.addEventListener("phx:page-loading-stop", info => topbar.hide()) window.addEventListener("phx:page-loading-stop", routeUpdated) window.addEventListener("js:exec", e => e.target[e.detail.call](...e.detail.args)) +window.addEventListener("phx:remove-el", e => document.getElementById(e.detail.id).remove()) // connect if there are any LiveViews on the page liveSocket.getSocket().onOpen(() => execJS("#connection-status", "js-hide")) diff --git a/lib/live_beats/presence/phoenix_presence_client.ex b/lib/live_beats/presence/phoenix_presence_client.ex index c8187e8..afda887 100644 --- a/lib/live_beats/presence/phoenix_presence_client.ex +++ b/lib/live_beats/presence/phoenix_presence_client.ex @@ -2,8 +2,10 @@ defmodule Phoenix.Presence.Client do use GenServer @callback init(state :: term) :: {:ok, new_state :: term} - @callback handle_join(topic :: String.t(), key :: String.t(), meta :: [map()], state :: term) :: {:ok, term} - @callback handle_leave(topic :: String.t(), key :: String.t(), meta :: [map()], state :: term) :: {:ok, term} + @callback handle_join(topic :: String.t(), key :: String.t(), meta :: [map()], state :: term) :: + {:ok, term} + @callback handle_leave(topic :: String.t(), key :: String.t(), meta :: [map()], state :: term) :: + {:ok, term} @doc """ TODO @@ -19,7 +21,8 @@ defmodule Phoenix.Presence.Client do {:ok, name} -> GenServer.start_link(__MODULE__, opts, name: name) - :error -> GenServer.start_link(__MODULE__, opts) + :error -> + GenServer.start_link(__MODULE__, opts) end end @@ -113,7 +116,9 @@ defmodule Phoenix.Presence.Client do updated_state = update_topics_state(:add_new_presence_or_metas, state, topic, joined_key, joined_meta) - {:ok, updated_client_state} = state.client.handle_join(topic, joined_key, joined_meta, state.client_state) + {:ok, updated_client_state} = + state.client.handle_join(topic, joined_key, meta, state.client_state) + updated_state = Map.put(updated_state, :client_state, updated_client_state) {updated_state, topic} @@ -122,7 +127,9 @@ defmodule Phoenix.Presence.Client do defp handle_leave({left_key, meta}, {state, topic}) do updated_state = update_topics_state(:remove_presence_or_metas, state, topic, left_key, meta) - {:ok, updated_client_state} = state.client.handle_leave(topic, left_key, meta, state.client_state) + {:ok, updated_client_state} = + state.client.handle_leave(topic, left_key, meta, state.client_state) + updated_state = Map.put(updated_state, :client_state, updated_client_state) {updated_state, topic} diff --git a/lib/live_beats/presence/presence_client.ex b/lib/live_beats/presence/presence_client.ex index 17280b2..0ddec5e 100644 --- a/lib/live_beats/presence/presence_client.ex +++ b/lib/live_beats/presence/presence_client.ex @@ -15,25 +15,34 @@ defmodule LiveBeats.PresenceClient do end @impl Phoenix.Presence.Client - def handle_join(topic, key, _meta, state) do + def handle_join(topic, _key, presence, state) do active_users_topic = topic |> profile_identifier() |> active_users_topic() - Phoenix.PubSub.local_broadcast(@pubsub, active_users_topic, {__MODULE__, %{user_joined: key}}) + Phoenix.PubSub.local_broadcast( + @pubsub, + active_users_topic, + {__MODULE__, %{user_joined: presence}} + ) {:ok, state} end @impl Phoenix.Presence.Client - def handle_leave(topic, key, _meta, state) do + def handle_leave(topic, _key, presence, state) do active_users_topic = topic |> profile_identifier() |> active_users_topic() - Phoenix.PubSub.local_broadcast(@pubsub, active_users_topic, {__MODULE__, %{user_left: key}}) + Phoenix.PubSub.local_broadcast( + @pubsub, + active_users_topic, + {__MODULE__, %{user_left: presence}} + ) + {:ok, state} end diff --git a/lib/live_beats_web/channels/presence.ex b/lib/live_beats_web/channels/presence.ex index 7593efc..d203309 100644 --- a/lib/live_beats_web/channels/presence.ex +++ b/lib/live_beats_web/channels/presence.ex @@ -5,8 +5,9 @@ defmodule LiveBeatsWeb.Presence do See the [`Phoenix.Presence`](http://hexdocs.pm/phoenix/Phoenix.Presence.html) docs for more details. """ - use Phoenix.Presence, otp_app: :live_beats, - pubsub_server: LiveBeats.PubSub + use Phoenix.Presence, + otp_app: :live_beats, + pubsub_server: LiveBeats.PubSub import Phoenix.LiveView.Helpers import LiveBeatsWeb.LiveHelpers @@ -18,10 +19,16 @@ defmodule LiveBeatsWeb.Presence do ~H"""
-

Who's Listening

-
    +

    Here now

    +
      <%= for presence <- @presences do %> -
    • +
    • <.link navigate={profile_path(presence)} class="flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate">
      diff --git a/lib/live_beats_web/live/profile_live.ex b/lib/live_beats_web/live/profile_live.ex index fae41f3..ea4ed5b 100644 --- a/lib/live_beats_web/live/profile_live.ex +++ b/lib/live_beats_web/live/profile_live.ex @@ -88,7 +88,9 @@ defmodule LiveBeatsWeb.ProfileLive do MediaLibrary.subscribe_to_profile(profile) Accounts.subscribe(current_user.id) LiveBeatsWeb.Presence.subscribe(profile) - Phoenix.Presence.Client.track(topic(profile.user_id), + + Phoenix.Presence.Client.track( + topic(profile.user_id), current_user.id, %{} ) @@ -111,7 +113,7 @@ defmodule LiveBeatsWeb.ProfileLive do |> list_songs() |> assign_presences() - {:ok, socket, temporary_assigns: [songs: []]} + {:ok, socket, temporary_assigns: [songs: [], presences: []]} end def handle_params(params, _url, socket) do @@ -147,21 +149,14 @@ defmodule LiveBeatsWeb.ProfileLive do {:noreply, socket} end - def handle_info({LiveBeats.PresenceClient, %{user_joined: user_id}}, socket) do - new_user = Accounts.get_user!(user_id) - updated_presences = - if new_user in socket.assigns.presences do - socket.assigns.presences - else - [new_user | socket.assigns.presences] - end - {:noreply, assign(socket, :presences, updated_presences)} + def handle_info({LiveBeats.PresenceClient, %{user_joined: presence}}, socket) do + %{user: user} = presence + {:noreply, update(socket, :presences, &[user | &1])} end - def handle_info({LiveBeats.PresenceClient, %{user_left: user_id}}, socket) do - updated_presences = socket.assigns.presences - |> Enum.reject(fn user -> user.id == String.to_integer(user_id) end) - {:noreply, assign(socket, :presences, updated_presences)} + def handle_info({LiveBeats.PresenceClient, %{user_left: presence}}, socket) do + %{user: user} = presence + {:noreply, push_event(socket, "remove-el", %{id: "presence-#{user.id}"})} end def handle_info({Accounts, %Accounts.Events.ActiveProfileChanged{} = event}, socket) do @@ -264,10 +259,11 @@ defmodule LiveBeatsWeb.ProfileLive do end defp assign_presences(socket) do - presences = socket.assigns.profile.user_id - |> topic() - |> LiveBeats.PresenceClient.list() - |> Enum.map(fn {_key, meta} -> meta.user end) + presences = + socket.assigns.profile.user_id + |> topic() + |> LiveBeats.PresenceClient.list() + |> Enum.map(fn {_key, meta} -> meta.user end) assign(socket, presences: presences) end diff --git a/test/live_beats_web/live/profile_live_test.exs b/test/live_beats_web/live/profile_live_test.exs index 69aeb50..94ab382 100644 --- a/test/live_beats_web/live/profile_live_test.exs +++ b/test/live_beats_web/live/profile_live_test.exs @@ -51,18 +51,13 @@ defmodule LiveBeatsWeb.ProfileLiveTest do } }) =~ "can't be blank" - assert {:ok, new_lv, html} = - lv |> form("#song-form") |> render_submit() |> follow_redirect(conn) - - assert_redirected(lv, "/#{current_user.username}") - assert html =~ "1 song(s) uploaded" - - assert html =~ "silence1s" + assert lv |> form("#song-form") |> render_submit() =~ "silence1s" + assert_patch(lv, "/#{current_user.username}") # deleting songs song = MediaLibrary.get_first_song(profile) - assert new_lv |> element("#delete-modal-#{song.id}-confirm") |> render_click() + assert lv |> element("#delete-modal-#{song.id}-confirm") |> render_click() {:ok, refreshed_lv, _} = live(conn, LiveHelpers.profile_path(current_user)) refute render(refreshed_lv) =~ "silence1s"