aprs.me/lib/aprsme/weather_cache.ex
Graham McIntire 6eda1c0290
fix: resolve 19 bugs across Elixir, JS/TS, and config
Critical:
- Remove :protected from WeatherCache ETS (conflicted with :public)
- Register Aprsme.ReplayRegistry in supervision tree
- Route :continue_replay dead message to :start_replay handler
- Add 5000ms timeout to unbounded :rpc.call calls
- Fix falsy lat/lng check rejecting valid 0 coordinates

Medium:
- Fix live_reload pattern (temp_web -> aprsme_web)
- Remove duplicate ecto_repos config
- Make DB SSL configurable via DB_SSL env var
- Track sizeCheckTimeout on self for cleanup in destroyed()
- Wrap pushEvent calls in try/catch
- Remove unused size variable in marker cluster icon
- Capture touch coords at touchstart instead of stale TouchEvent
- Demote console.log to console.debug in production code
- Add catch-all to normalize_bounds preventing GenServer crash

Style:
- Add missing @impl true annotations in is.ex
- Improve migration failure error message
- Use textContent instead of innerHTML for error messages
- Use proper type for longPressTimer instead of any
2026-06-21 11:47:07 -05:00

89 lines
2.1 KiB
Elixir

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
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