diff --git a/lib/aprsme/spatial_pubsub.ex b/lib/aprsme/spatial_pubsub.ex index 97ead72..98f7b88 100644 --- a/lib/aprsme/spatial_pubsub.ex +++ b/lib/aprsme/spatial_pubsub.ex @@ -18,42 +18,64 @@ defmodule Aprsme.SpatialPubSub do Register a client with their current viewport bounds. """ def register_viewport(client_id, bounds) do - GenServer.call(__MODULE__, {:register_viewport, client_id, bounds}) + with_server({:error, :not_running}, fn -> + GenServer.call(__MODULE__, {:register_viewport, client_id, bounds}) + end) end @doc """ Update a client's viewport bounds. """ def update_viewport(client_id, bounds) do - GenServer.call(__MODULE__, {:update_viewport, client_id, bounds}) + with_server({:error, :not_running}, fn -> + GenServer.call(__MODULE__, {:update_viewport, client_id, bounds}) + end) end @doc """ Unregister a client. """ def unregister_client(client_id) do - GenServer.cast(__MODULE__, {:unregister_client, client_id}) + with_server(:ok, fn -> + GenServer.cast(__MODULE__, {:unregister_client, client_id}) + end) end @doc """ Broadcast a packet to all clients whose viewports contain the packet's location. """ def broadcast_packet(packet) do - GenServer.cast(__MODULE__, {:broadcast_packet, packet}) + with_server(:ok, fn -> + GenServer.cast(__MODULE__, {:broadcast_packet, packet}) + end) end @doc """ Get statistics about spatial filtering. """ def get_stats do - GenServer.call(__MODULE__, :get_stats) + with_server( + %{ + total_broadcasts: 0, + filtered_broadcasts: 0, + total_packets: 0, + clients_count: 0, + grid_cells: 0, + avg_clients_per_cell: 0.0 + }, + fn -> + GenServer.call(__MODULE__, :get_stats) + end + ) end @doc """ Start telemetry reporting for LiveDashboard integration. """ def start_telemetry_reporting do - GenServer.cast(__MODULE__, :start_telemetry_reporting) + with_server(:ok, fn -> + GenServer.cast(__MODULE__, :start_telemetry_reporting) + end) end # Server callbacks @@ -439,4 +461,12 @@ defmodule Aprsme.SpatialPubSub do %{} ) end + + defp with_server(default, fun) do + if Process.whereis(__MODULE__) do + fun.() + else + default + end + end end diff --git a/lib/aprsme/streaming_packets_pubsub.ex b/lib/aprsme/streaming_packets_pubsub.ex index 6a2a19e..42848ce 100644 --- a/lib/aprsme/streaming_packets_pubsub.ex +++ b/lib/aprsme/streaming_packets_pubsub.ex @@ -32,28 +32,36 @@ defmodule Aprsme.StreamingPacketsPubSub do - :ok """ def subscribe_to_bounds(pid, bounds) do - GenServer.call(__MODULE__, {:subscribe, pid, bounds}) + with_server({:error, :not_running}, fn -> + GenServer.call(__MODULE__, {:subscribe, pid, bounds}) + end) end @doc """ Unsubscribe from packet notifications. """ def unsubscribe(pid) do - GenServer.call(__MODULE__, {:unsubscribe, pid}) + with_server(:ok, fn -> + GenServer.call(__MODULE__, {:unsubscribe, pid}) + end) end @doc """ Broadcast a packet to all subscribers whose bounds contain the packet's location. """ def broadcast_packet(packet) do - GenServer.cast(__MODULE__, {:broadcast, packet}) + with_server(:ok, fn -> + GenServer.cast(__MODULE__, {:broadcast, packet}) + end) end @doc """ List all active subscribers and their bounds. """ def list_subscribers do - GenServer.call(__MODULE__, :list_subscribers) + with_server([], fn -> + GenServer.call(__MODULE__, :list_subscribers) + end) end # Server Callbacks @@ -200,4 +208,12 @@ defmodule Aprsme.StreamingPacketsPubSub do defp test_env? do Application.get_env(:aprsme, :env) == :test end + + defp with_server(default, fun) do + if Process.whereis(__MODULE__) do + fun.() + else + default + end + end end diff --git a/test/aprsme/spatial_pubsub_test.exs b/test/aprsme/spatial_pubsub_test.exs index b7a4636..c4810bb 100644 --- a/test/aprsme/spatial_pubsub_test.exs +++ b/test/aprsme/spatial_pubsub_test.exs @@ -97,4 +97,41 @@ defmodule Aprsme.SpatialPubSubTest do SpatialPubSub.unregister_client(client_id) end end + + describe "when the server is not running" do + setup do + pid = Process.whereis(SpatialPubSub) + + if pid do + GenServer.stop(pid) + end + + on_exit(fn -> + if !Process.whereis(SpatialPubSub) do + start_supervised!({SpatialPubSub, []}) + end + end) + + :ok + end + + test "public APIs return safe defaults instead of crashing" do + bounds = %{north: 41.0, south: 40.0, east: -73.0, west: -74.0} + + assert {:error, :not_running} = SpatialPubSub.register_viewport("missing", bounds) + assert {:error, :not_running} = SpatialPubSub.update_viewport("missing", bounds) + assert :ok = SpatialPubSub.unregister_client("missing") + assert :ok = SpatialPubSub.broadcast_packet(%{lat: 40.5, lon: -73.5}) + assert :ok = SpatialPubSub.start_telemetry_reporting() + + assert SpatialPubSub.get_stats() == %{ + total_broadcasts: 0, + filtered_broadcasts: 0, + total_packets: 0, + clients_count: 0, + grid_cells: 0, + avg_clients_per_cell: 0.0 + } + end + end end diff --git a/test/aprsme/streaming_packets_pubsub_test.exs b/test/aprsme/streaming_packets_pubsub_test.exs index 6905cc0..d1a1784 100644 --- a/test/aprsme/streaming_packets_pubsub_test.exs +++ b/test/aprsme/streaming_packets_pubsub_test.exs @@ -140,6 +140,33 @@ defmodule Aprsme.StreamingPacketsPubSubTest do end end + describe "when the server is not running" do + setup do + pid = Process.whereis(StreamingPacketsPubSub) + + if pid do + GenServer.stop(pid) + end + + on_exit(fn -> + if !Process.whereis(StreamingPacketsPubSub) do + start_supervised!({StreamingPacketsPubSub, []}) + end + end) + + :ok + end + + test "returns safe defaults instead of crashing" do + bounds = %{north: 40.0, south: 30.0, east: -70.0, west: -80.0} + + assert {:error, :not_running} = StreamingPacketsPubSub.subscribe_to_bounds(self(), bounds) + assert :ok = StreamingPacketsPubSub.unsubscribe(self()) + assert :ok = StreamingPacketsPubSub.broadcast_packet(%{latitude: 35.0, longitude: -75.0}) + assert [] == StreamingPacketsPubSub.list_subscribers() + end + end + describe "performance" do test "handles high volume of packets efficiently" do bounds = %{north: 90.0, south: -90.0, east: 180.0, west: -180.0}