defmodule Aprsme.WeatherCache do @moduledoc """ Persistent ETS-based cache for weather callsign lookups. Eliminates N+1 queries for `has_weather_packets?` checks during map marker rendering. Entries expire after `@ttl_ms` to stay reasonably current without per-callsign DB queries. """ @cache_name :weather_cache @ttl_ms to_timeout(minute: 5) @doc """ Create the ETS table if it doesn't exist yet. The table is typically created in application.ex alongside other ETS tables. """ def setup do _table = if :ets.whereis(@cache_name) == :undefined do :ets.new(@cache_name, [ :named_table, :public, :set, read_concurrency: true, write_concurrency: true ]) end :ok rescue ArgumentError -> :ok end @doc """ Batch-populate the cache for the given list of callsigns. """ def cache_weather_callsigns(callsigns) when is_list(callsigns) do weather_set = Aprsme.Packets.weather_callsigns(callsigns) expires_at = System.monotonic_time(:millisecond) + @ttl_ms Enum.each(callsigns, fn cs -> key = String.upcase(String.trim(cs)) has_weather = MapSet.member?(weather_set, key) :ets.insert(@cache_name, {key, has_weather, expires_at}) end) :ok rescue _ -> :ok end @doc """ Check if a callsign has weather packets. Returns `:unknown` if not cached, `true`/`false` if cached. """ def weather_callsign?(callsign) when is_binary(callsign) do key = String.upcase(String.trim(callsign)) case :ets.lookup(@cache_name, key) do [{^key, value, expires_at}] -> if System.monotonic_time(:millisecond) < expires_at do value else :ets.delete(@cache_name, key) :unknown end _ -> :unknown end end @doc """ Evict expired entries. """ def evict_expired do now = System.monotonic_time(:millisecond) @cache_name |> :ets.tab2list() |> Enum.each(fn {key, _value, expires_at} when expires_at < now -> :ets.delete(@cache_name, key) _ -> :ok end) :ok end end