mirror of
https://github.com/fly-apps/live_beats.git
synced 2024-11-25 01:10:59 +00:00
Optimize presence and rate limit pings
This commit is contained in:
parent
de2f473624
commit
8cd6048d4b
5 changed files with 122 additions and 62 deletions
|
@ -183,7 +183,7 @@ Hooks.Ping = {
|
||||||
this.handleEvent("pong", () => {
|
this.handleEvent("pong", () => {
|
||||||
let rtt = Date.now() - this.nowMs
|
let rtt = Date.now() - this.nowMs
|
||||||
this.el.innerText = `ping: ${rtt}ms`
|
this.el.innerText = `ping: ${rtt}ms`
|
||||||
this.timer = setTimeout(() => this.ping(rtt), 1000)
|
this.timer = setTimeout(() => this.ping(rtt), 100)
|
||||||
})
|
})
|
||||||
this.ping(null)
|
this.ping(null)
|
||||||
},
|
},
|
||||||
|
|
|
@ -13,6 +13,7 @@ defmodule LiveBeatsWeb.Presence do
|
||||||
import LiveBeatsWeb.LiveHelpers
|
import LiveBeatsWeb.LiveHelpers
|
||||||
|
|
||||||
alias LiveBeats.{Accounts, MediaLibrary}
|
alias LiveBeats.{Accounts, MediaLibrary}
|
||||||
|
alias LiveBeatsWeb.Presence.BadgeComponent
|
||||||
|
|
||||||
def subscribe(%MediaLibrary.Profile{} = profile) do
|
def subscribe(%MediaLibrary.Profile{} = profile) do
|
||||||
LiveBeats.PresenceClient.subscribe(profile)
|
LiveBeats.PresenceClient.subscribe(profile)
|
||||||
|
@ -29,57 +30,79 @@ defmodule LiveBeatsWeb.Presence do
|
||||||
{key, %{metas: metas, user: users[String.to_integer(key)]}}
|
{key, %{metas: metas, user: users[String.to_integer(key)]}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
defmodule LiveBeatsWeb.Presence.BadgeListComponent do
|
def listening_now(assigns) do
|
||||||
use LiveBeatsWeb, :live_component
|
import Phoenix.LiveView
|
||||||
|
count = Enum.count(assigns.presence_ids)
|
||||||
|
|
||||||
|
assigns =
|
||||||
|
assigns
|
||||||
|
|> assign(:count, count)
|
||||||
|
|> assign_new(:total_count, fn -> count end)
|
||||||
|
|
||||||
def render(assigns) do
|
|
||||||
~H"""
|
~H"""
|
||||||
<div class="px-4 mt-6 sm:px-6 lg:px-8"> <!-- users -->
|
<div class="px-4 mt-6 sm:px-6 lg:px-8"> <!-- users -->
|
||||||
<h2 class="text-gray-500 text-xs font-medium uppercase tracking-wide">Listening now</h2>
|
<h2 class="text-gray-500 text-xs font-medium uppercase tracking-wide">Listening now (<%= @count %>)</h2>
|
||||||
<ul
|
<ul
|
||||||
id="listening-now"
|
id="listening-now"
|
||||||
phx-update="prepend"
|
|
||||||
role="list"
|
role="list"
|
||||||
x-max="1"
|
x-max="1"
|
||||||
class="grid grid-cols-1 gap-4 sm:gap-4 sm:grid-cols-2 xl:grid-cols-5 mt-3"
|
class="grid grid-cols-1 gap-4 sm:gap-4 sm:grid-cols-2 xl:grid-cols-5 mt-3"
|
||||||
>
|
>
|
||||||
<%= for presence <- @presences do %>
|
<%= for {id, _time} <- Enum.sort(@presence_ids, fn {_, t1}, {_, t2} -> t1 < t2 end) do %>
|
||||||
<li id={"presence-#{presence.id}"} class="relative col-span-1 flex shadow-sm rounded-md overflow-hidden">
|
<.live_component
|
||||||
<.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">
|
id={id}
|
||||||
<img class="w-12 h-12 flex-shrink-0 flex items-center justify-center rounded-l-md bg-purple-600" src={presence.avatar_url} alt="">
|
module={BadgeComponent}
|
||||||
<div class="flex-1 flex items-center justify-between text-gray-900 text-sm font-medium hover:text-gray-600 pl-3">
|
presence={@presences[id]}
|
||||||
<div class="flex-1 py-1 text-sm truncate">
|
/>
|
||||||
<%= render_slot(@inner_block, %{user: presence, ping: @pings[presence.id], region: @regions[presence.id]}) %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</.link>
|
|
||||||
</li>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
|
<%= if @total_count > @count do %>
|
||||||
|
<p>+ <%= @total_count - @count %> more</p>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule LiveBeatsWeb.Presence.BadgeComponent do
|
||||||
|
use LiveBeatsWeb, :live_component
|
||||||
|
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<li id={"presence-#{@id}"} class="relative col-span-1 flex shadow-sm rounded-md overflow-hidden">
|
||||||
|
<.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">
|
||||||
|
<img class="w-12 h-12 flex-shrink-0 flex items-center justify-center rounded-l-md bg-purple-600" src={@presence.avatar_url} alt="">
|
||||||
|
<div class="flex-1 flex items-center justify-between text-gray-900 text-sm font-medium hover:text-gray-600 pl-3">
|
||||||
|
<div class="flex-1 py-1 text-sm truncate">
|
||||||
|
<%= @presence.username %>
|
||||||
|
<%= if @ping do %>
|
||||||
|
<p class="text-gray-400 text-xs">ping: <%= @ping %>ms</p>
|
||||||
|
<%= if @region do %><img class="inline w-7 h-7 absolute right-3 top-3" src={"https://fly.io/ui/images/#{@region}.svg"} /><% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</.link>
|
||||||
|
</li>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
def mount(socket) do
|
def mount(socket) do
|
||||||
{:ok, socket, temporary_assigns: [presences: [], pings: %{}, regions: %{}]}
|
{:ok, socket, temporary_assigns: [presence: nil, ping: nil, region: nil]}
|
||||||
end
|
end
|
||||||
|
|
||||||
def update(%{action: {:ping, action}}, socket) do
|
def update(%{action: {:ping, action}}, socket) do
|
||||||
%{user: user, ping: ping, region: region} = action
|
%{user: user, ping: ping, region: region} = action
|
||||||
|
|
||||||
{:ok,
|
{:ok, assign(socket, presence: user, ping: ping, region: region)}
|
||||||
socket
|
|
||||||
|> assign(:presences, [user])
|
|
||||||
|> update(:pings, &Map.put(&1, user.id, ping))
|
|
||||||
|> update(:regions, &Map.put(&1, user.id, region))}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update(%{presence: nil}, socket), do: {:ok, socket}
|
||||||
|
|
||||||
def update(assigns, socket) do
|
def update(assigns, socket) do
|
||||||
{:ok,
|
{:ok,
|
||||||
socket
|
socket
|
||||||
|> assign(assigns)
|
|> assign(id: assigns.id, presence: assigns.presence)
|
||||||
|> assign_new(:pings, fn -> %{} end)
|
|> assign_new(:pings, fn -> %{} end)
|
||||||
|> assign_new(:regions, fn -> %{} end)}
|
|> assign_new(:regions, fn -> %{} end)}
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,12 +13,16 @@ defmodule LiveBeatsWeb.LiveHelpers do
|
||||||
|
|
||||||
def profile_path(current_user_or_profile, action \\ :show)
|
def profile_path(current_user_or_profile, action \\ :show)
|
||||||
|
|
||||||
|
def profile_path(username, action) when is_binary(username) do
|
||||||
|
Routes.profile_path(LiveBeatsWeb.Endpoint, action, username)
|
||||||
|
end
|
||||||
|
|
||||||
def profile_path(%Accounts.User{} = current_user, action) do
|
def profile_path(%Accounts.User{} = current_user, action) do
|
||||||
Routes.profile_path(LiveBeatsWeb.Endpoint, action, current_user.username)
|
profile_path(current_user.username, action)
|
||||||
end
|
end
|
||||||
|
|
||||||
def profile_path(%MediaLibrary.Profile{} = profile, action) do
|
def profile_path(%MediaLibrary.Profile{} = profile, action) do
|
||||||
Routes.profile_path(LiveBeatsWeb.Endpoint, action, profile.username)
|
profile_path(profile.username, action)
|
||||||
end
|
end
|
||||||
|
|
||||||
def connection_status(assigns) do
|
def connection_status(assigns) do
|
||||||
|
@ -134,12 +138,13 @@ defmodule LiveBeatsWeb.LiveHelpers do
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
def link(%{navigate: to} = assigns) do
|
def link(%{navigate: _to} = assigns) do
|
||||||
opts = assigns |> assigns_to_attributes() |> Keyword.put(:to, to)
|
assigns = assign_new(assigns, :class, fn -> nil end)
|
||||||
assigns = assign(assigns, :opts, opts)
|
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
<%= live_redirect @opts do %><%= render_slot(@inner_block) %><% end %>
|
<a href={@navigate} data-phx-link="redirect" data-phx-link-state="push" class={@class}>
|
||||||
|
<%= render_slot(@inner_block) %>
|
||||||
|
</a>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
defmodule LiveBeatsWeb.Nav do
|
defmodule LiveBeatsWeb.Nav do
|
||||||
import Phoenix.LiveView
|
import Phoenix.LiveView
|
||||||
|
|
||||||
alias LiveBeats.MediaLibrary
|
alias LiveBeats.{Accounts, MediaLibrary}
|
||||||
alias LiveBeatsWeb.{ProfileLive, SettingsLive}
|
alias LiveBeatsWeb.{ProfileLive, SettingsLive}
|
||||||
|
|
||||||
def on_mount(:default, _params, _session, socket) do
|
def on_mount(:default, _params, _session, socket) do
|
||||||
|
@ -32,15 +32,26 @@ defmodule LiveBeatsWeb.Nav do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_event("ping", %{"rtt" => rtt}, socket) do
|
defp handle_event("ping", %{"rtt" => rtt}, socket) do
|
||||||
%{current_user: current_user} = socket.assigns
|
{:halt,
|
||||||
|
socket
|
||||||
if rtt && current_user && current_user.active_profile_user_id do
|
|> rate_limited_ping_broadcast(socket.assigns.current_user, rtt)
|
||||||
MediaLibrary.broadcast_ping(current_user, rtt, socket.assigns.region)
|
|> push_event("pong", %{})}
|
||||||
end
|
|
||||||
|
|
||||||
{:halt, push_event(socket, "pong", %{})}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp rate_limited_ping_broadcast(socket, %Accounts.User{} = user, rtt) when is_integer(rtt) do
|
||||||
|
now = System.system_time(:millisecond)
|
||||||
|
last_ping_at = socket.assigns[:last_ping_at]
|
||||||
|
|
||||||
|
if is_nil(last_ping_at) || now - last_ping_at > 1000 do
|
||||||
|
MediaLibrary.broadcast_ping(user, rtt, socket.assigns.region)
|
||||||
|
assign(socket, :last_ping_at, now)
|
||||||
|
else
|
||||||
|
socket
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp rate_limited_ping_broadcast(socket, _user, _rtt), do: socket
|
||||||
|
|
||||||
defp handle_event(_, _, socket), do: {:cont, socket}
|
defp handle_event(_, _, socket), do: {:cont, socket}
|
||||||
|
|
||||||
defp current_user_profile_username(socket) do
|
defp current_user_profile_username(socket) do
|
||||||
|
|
|
@ -5,6 +5,8 @@ defmodule LiveBeatsWeb.ProfileLive do
|
||||||
alias LiveBeatsWeb.{LayoutComponent, Presence}
|
alias LiveBeatsWeb.{LayoutComponent, Presence}
|
||||||
alias LiveBeatsWeb.ProfileLive.{SongRowComponent, UploadFormComponent}
|
alias LiveBeatsWeb.ProfileLive.{SongRowComponent, UploadFormComponent}
|
||||||
|
|
||||||
|
@max_presences 20
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<.title_bar>
|
<.title_bar>
|
||||||
|
@ -39,17 +41,11 @@ defmodule LiveBeatsWeb.ProfileLive do
|
||||||
</:actions>
|
</:actions>
|
||||||
</.title_bar>
|
</.title_bar>
|
||||||
|
|
||||||
<.live_component
|
<Presence.listening_now
|
||||||
let={%{user: user, ping: ping, region: region}}
|
|
||||||
id={:presence_badges} module={Presence.BadgeListComponent}
|
|
||||||
presences={@presences}
|
presences={@presences}
|
||||||
>
|
presence_ids={@presence_ids}
|
||||||
<%= user.username %>
|
total_count={@presences_count}
|
||||||
<%= if ping do %>
|
/>
|
||||||
<p class="text-gray-400 text-xs">ping: <%= ping %>ms</p>
|
|
||||||
<%= if region do %><img class="inline w-7 h-7 absolute right-3 top-3" src={"https://fly.io/ui/images/#{region}.svg"} /><% end %>
|
|
||||||
<% end %>
|
|
||||||
</.live_component>
|
|
||||||
|
|
||||||
<div id="dialogs" phx-update="append">
|
<div id="dialogs" phx-update="append">
|
||||||
<%= for song <- if(@owns_profile?, do: @songs, else: []), id = "delete-modal-#{song.id}" do %>
|
<%= for song <- if(@owns_profile?, do: @songs, else: []), id = "delete-modal-#{song.id}" do %>
|
||||||
|
@ -115,7 +111,7 @@ defmodule LiveBeatsWeb.ProfileLive do
|
||||||
|> list_songs()
|
|> list_songs()
|
||||||
|> assign_presences()
|
|> assign_presences()
|
||||||
|
|
||||||
{:ok, socket, temporary_assigns: [songs: [], presences: []]}
|
{:ok, socket, temporary_assigns: [songs: [], presences: %{}]}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_params(params, _url, socket) do
|
def handle_params(params, _url, socket) do
|
||||||
|
@ -152,15 +148,14 @@ defmodule LiveBeatsWeb.ProfileLive do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info({LiveBeats.PresenceClient, %{user_joined: presence}}, socket) do
|
def handle_info({LiveBeats.PresenceClient, %{user_joined: presence}}, socket) do
|
||||||
%{user: user} = presence
|
{:noreply, assign_presence(socket, presence)}
|
||||||
{:noreply, update(socket, :presences, &[user | &1])}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info({LiveBeats.PresenceClient, %{user_left: presence}}, socket) do
|
def handle_info({LiveBeats.PresenceClient, %{user_left: presence}}, socket) do
|
||||||
%{user: user} = presence
|
%{user: user} = presence
|
||||||
|
|
||||||
if presence.metas == [] do
|
if presence.metas == [] do
|
||||||
{:noreply, push_event(socket, "remove-el", %{id: "presence-#{user.id}"})}
|
{:noreply, remove_presence(socket, user)}
|
||||||
else
|
else
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
@ -191,8 +186,9 @@ defmodule LiveBeatsWeb.ProfileLive do
|
||||||
|
|
||||||
def handle_info({MediaLibrary, {:ping, ping}}, socket) do
|
def handle_info({MediaLibrary, {:ping, ping}}, socket) do
|
||||||
%{user: user, rtt: rtt, region: region} = ping
|
%{user: user, rtt: rtt, region: region} = ping
|
||||||
send_update(Presence.BadgeListComponent,
|
|
||||||
id: :presence_badges,
|
send_update(Presence.BadgeComponent,
|
||||||
|
id: user.id,
|
||||||
action: {:ping, %{user: user, ping: rtt, region: region}}
|
action: {:ping, %{user: user, ping: rtt, region: region}}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -276,18 +272,43 @@ defmodule LiveBeatsWeb.ProfileLive do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp assign_presences(socket) do
|
defp assign_presences(socket) do
|
||||||
if profile = socket.assigns.profile do
|
socket = assign(socket, presences_count: 0, presences: %{}, presence_ids: %{})
|
||||||
presences =
|
|
||||||
profile
|
|
||||||
|> LiveBeats.PresenceClient.list()
|
|
||||||
|> Enum.map(fn {_key, meta} -> meta.user end)
|
|
||||||
|
|
||||||
assign(socket, presences: presences)
|
if profile = connected?(socket) && socket.assigns.profile do
|
||||||
|
profile
|
||||||
|
|> LiveBeats.PresenceClient.list()
|
||||||
|
|> Enum.reduce(socket, fn {_, presence}, acc -> assign_presence(acc, presence) end)
|
||||||
else
|
else
|
||||||
assign(socket, presences: [])
|
socket
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp assign_presence(socket, presence) do
|
||||||
|
%{user: user} = presence
|
||||||
|
%{presence_ids: presence_ids} = socket.assigns
|
||||||
|
|
||||||
|
cond do
|
||||||
|
Map.has_key?(presence_ids, user.id) ->
|
||||||
|
socket
|
||||||
|
|
||||||
|
Enum.count(presence_ids) < @max_presences ->
|
||||||
|
socket
|
||||||
|
|> update(:presences, &Map.put(&1, user.id, user))
|
||||||
|
|> update(:presence_ids, &Map.put(&1, user.id, System.system_time()))
|
||||||
|
|> update(:presences_count, &(&1 + 1))
|
||||||
|
|
||||||
|
true ->
|
||||||
|
update(socket, :presences_count, &(&1 + 1))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp remove_presence(socket, user) do
|
||||||
|
socket
|
||||||
|
|> update(:presences, &Map.delete(&1, user.id))
|
||||||
|
|> update(:presence_ids, &Map.delete(&1, user.id))
|
||||||
|
|> update(:presences_count, &(&1 - 1))
|
||||||
|
end
|
||||||
|
|
||||||
defp url_text(nil), do: ""
|
defp url_text(nil), do: ""
|
||||||
|
|
||||||
defp url_text(url_str) do
|
defp url_text(url_str) do
|
||||||
|
|
Loading…
Reference in a new issue