defmodule AprsmeWeb.Live.SharedPacketHandler do @moduledoc """ Shared functionality for handling packet updates in LiveView modules. Provides common patterns for packet filtering, enrichment, and processing. """ alias Aprsme.Callsign alias Aprsme.DeviceCache alias Aprsme.EncodingUtils @doc """ Handles incoming packet updates with filtering and processing. ## Options * `:filter_fn` - Function to determine if packet should be processed (required) * `:process_fn` - Function to process matching packets and update socket (required) * `:enrich_packet` - Whether to enrich packet with device info (default: true) """ def handle_packet_update(packet, socket, opts) do filter_fn = Keyword.fetch!(opts, :filter_fn) process_fn = Keyword.fetch!(opts, :process_fn) enrich_packet? = Keyword.get(opts, :enrich_packet, true) dispatch_packet_update(filter_fn.(packet, socket), packet, socket, process_fn, enrich_packet?) end defp dispatch_packet_update(false, _packet, socket, _process_fn, _enrich?), do: {:noreply, socket} defp dispatch_packet_update(true, packet, socket, process_fn, enrich?) do packet |> maybe_enrich(enrich?) |> process_fn.(socket) end defp maybe_enrich(packet, true), do: packet |> sanitize_packet() |> enrich_with_device_info() defp maybe_enrich(packet, false), do: packet @doc """ Checks if packet sender matches the given callsign. """ def packet_matches_callsign?(packet, callsign) do packet_sender = Map.get(packet, "sender") || Map.get(packet, :sender, "") Callsign.matches?(packet_sender, callsign) end @doc """ Checks if packet contains weather data. """ def has_weather_data?(packet) do Enum.any?(EncodingUtils.weather_fields(), fn field -> value = Map.get(packet, field) || Map.get(packet, to_string(field)) not is_nil(value) end) end @doc """ Sanitizes packet strings to handle encoding issues. """ def sanitize_packet(packet) do EncodingUtils.sanitize_packet(packet) end @doc """ Enriches packet with device information. """ def enrich_with_device_info(packet) do device_identifier = get_device_identifier(packet) device = lookup_device_info(device_identifier) packet |> Map.put(:device_model, device && device.model) |> Map.put(:device_vendor, device && device.vendor) |> Map.put(:device_contact, device && device.contact) |> Map.put(:device_class, device && device.class) end @doc """ Batch enrich multiple packets with device info for better performance. """ def enrich_packets_with_device_info(packets) when is_list(packets) do # Get unique device identifiers from all packets device_identifiers = packets |> Enum.map(&get_device_identifier/1) |> Enum.reject(&nil_or_empty?/1) |> Enum.uniq() # Batch lookup devices (DeviceCache is already optimized with caching) device_map = Map.new(device_identifiers, fn identifier -> {identifier, DeviceCache.lookup_device(identifier)} end) # Enrich packets using the cached device map Enum.map(packets, fn packet -> device_identifier = get_device_identifier(packet) device = Map.get(device_map, device_identifier) packet |> Map.put(:device_model, device && device.model) |> Map.put(:device_vendor, device && device.vendor) |> Map.put(:device_contact, device && device.contact) |> Map.put(:device_class, device && device.class) end) end defp get_device_identifier(packet) do Map.get(packet, :device_identifier) || Map.get(packet, "device_identifier") end defp lookup_device_info(nil), do: nil defp lookup_device_info(""), do: nil defp lookup_device_info(identifier), do: DeviceCache.lookup_device(identifier) defp nil_or_empty?(nil), do: true defp nil_or_empty?(""), do: true defp nil_or_empty?(_), do: false @doc """ Creates a filter function that checks both callsign and weather data. """ def callsign_and_weather_filter(callsign) do fn packet, _socket -> packet_matches_callsign?(packet, callsign) and has_weather_data?(packet) end end @doc """ Creates a filter function that only checks callsign. """ def callsign_filter(callsign) do fn packet, _socket -> packet_matches_callsign?(packet, callsign) end end end