aprs.me/lib/aprsme_web/live/map_live/subscriptions.ex
Graham McIntire cc88fd4a10
Some checks failed
Build and Push / Build and Push Docker Image (push) Failing after 2s
fix: dialyzer errors, pubsub failure handling, cache TTL, stream trim perf, and clearer warnings
2026-07-26 16:19:42 -05:00

117 lines
3.9 KiB
Elixir

defmodule AprsmeWeb.MapLive.Subscriptions do
@moduledoc """
Manages Phoenix.PubSub subscriptions and connection lifecycle for the map LiveView.
Extracted from Index to reduce module size.
"""
import Phoenix.Component, only: [assign: 3]
import Phoenix.LiveView, only: [connected?: 1, push_event: 3, put_flash: 3]
alias Phoenix.LiveView.Socket
@doc """
Set up spatial and PubSub subscriptions for a newly mounted socket.
Only creates subscriptions when the socket is connected (LiveSocket).
"""
@spec setup(Socket.t(), map()) :: Socket.t()
def setup(socket, initial_bounds) do
do_setup(socket, initial_bounds, connected?(socket))
end
@doc """
Clean up all subscriptions and timers when the LiveView terminates.
"""
@spec teardown(Socket.t()) :: :ok
def teardown(socket) do
_teardown_connection_monitor(socket)
_teardown_spatial(socket)
_teardown_all_timers(socket)
_teardown_batch_tasks(socket)
:ok
end
@doc """
Update the spatial viewport bounds for an existing subscription.
"""
@spec update_viewport(Socket.t(), map()) :: :ok | nil
def update_viewport(socket, map_bounds) do
if socket.assigns[:spatial_client_id] do
Aprsme.SpatialPubSub.update_viewport(socket.assigns.spatial_client_id, map_bounds)
end
end
# --- Private helpers ---
defp do_setup(socket, _initial_bounds, false), do: socket
defp do_setup(socket, initial_bounds, true) do
# Check if we should accept new connections
if Application.get_env(:aprsme, :cluster_enabled, false) and
not Aprsme.ConnectionMonitor.accepting_connections?() do
socket
|> put_flash(:info, "This server is currently at capacity. Please try again in a moment.")
|> push_event("redirect_to_least_loaded", %{})
|> assign(:connection_draining, true)
else
# Generate a unique client ID for this LiveView instance
client_id = "liveview_#{:erlang.phash2(self())}"
# Register with connection monitor
_ =
if Application.get_env(:aprsme, :cluster_enabled, false) do
_ = Aprsme.ConnectionMonitor.register_connection()
# Subscribe to drain events for this node
Phoenix.PubSub.subscribe(Aprsme.PubSub, "connection:drain:#{Node.self()}")
end
# Register the actual initial viewport.
{:ok, spatial_topic} = Aprsme.SpatialPubSub.register_viewport(client_id, initial_bounds)
# Subscribe to the spatial topic for this client
:ok = Phoenix.PubSub.subscribe(Aprsme.PubSub, spatial_topic)
# Still subscribe to bad packets (they don't have location)
:ok = Phoenix.PubSub.subscribe(Aprsme.PubSub, "bad_packets")
# Subscribe to deployment events
:ok = Phoenix.PubSub.subscribe(Aprsme.PubSub, "deployment_events")
_ = Process.send_after(self(), :cleanup_old_packets, 60_000)
socket
|> assign(:spatial_client_id, client_id)
|> assign(:connection_draining, false)
end
end
defp _teardown_connection_monitor(socket) do
_ =
if Application.get_env(:aprsme, :cluster_enabled, false) and
not socket.assigns[:connection_draining] do
Aprsme.ConnectionMonitor.unregister_connection()
end
end
defp _teardown_spatial(socket) do
_ =
if socket.assigns[:spatial_client_id] do
Aprsme.SpatialPubSub.unregister_client(socket.assigns.spatial_client_id)
end
end
defp _teardown_all_timers(socket) do
cancel_if_exists(socket.assigns[:buffer_timer])
cancel_if_exists(socket.assigns[:bounds_update_timer])
cancel_if_exists(socket.assigns[:hover_end_timer])
end
defp cancel_if_exists(nil), do: :ok
defp cancel_if_exists(timer_ref), do: Process.cancel_timer(timer_ref)
defp _teardown_batch_tasks(socket) do
_ =
if socket.assigns[:pending_batch_tasks] do
Enum.each(socket.assigns.pending_batch_tasks, &Process.cancel_timer/1)
end
end
end