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