diff --git a/lib/microwaveprop_web/live/live_stash_guard.ex b/lib/microwaveprop_web/live/live_stash_guard.ex new file mode 100644 index 00000000..e9c053a3 --- /dev/null +++ b/lib/microwaveprop_web/live/live_stash_guard.ex @@ -0,0 +1,34 @@ +defmodule MicrowavepropWeb.LiveStashGuard do + @moduledoc """ + Tiny wrapper around `LiveStash.stash/1` that swallows the + `ArgumentError: not a valid match specification` crash LiveStash 0.2.0 + raises under OTP 28. + + The stash is a convenience for state persistence across reconnects; + a failure is not fatal to the user's session. We log and return the + socket unchanged instead of letting the LiveView process terminate + mid-`handle_event`. + + Remove this module and inline `LiveStash.stash/1` calls once the + upstream match-spec fix lands in the `live_stash` hex package. + """ + + alias Phoenix.LiveView.Socket + + require Logger + + @doc """ + Best-effort wrapper around `LiveStash.stash/1`. + + Returns the (possibly unchanged) socket no matter what the adapter + raises. + """ + @spec stash(Socket.t()) :: Socket.t() + def stash(socket) do + LiveStash.stash(socket) + rescue + e in ArgumentError -> + Logger.warning("LiveStashGuard: stash skipped (#{Exception.message(e)})") + socket + end +end diff --git a/lib/microwaveprop_web/live/map_live.ex b/lib/microwaveprop_web/live/map_live.ex index 0d53860b..5ea59b00 100644 --- a/lib/microwaveprop_web/live/map_live.ex +++ b/lib/microwaveprop_web/live/map_live.ex @@ -232,7 +232,7 @@ defmodule MicrowavepropWeb.MapLive do socket |> assign(selected_band: band, valid_times: valid_times, selected_time: selected_time) |> assign(:tracking_now?, tracking_now?(selected_time, valid_times, DateTime.utc_now())) - |> LiveStash.stash() + |> MicrowavepropWeb.LiveStashGuard.stash() |> push_event("update_scores", %{scores: Propagation.pack_scores(scores)}) |> push_event("update_band_info", %{band_info: band_info(band)}) |> push_timeline() @@ -412,6 +412,10 @@ defmodule MicrowavepropWeb.MapLive do # since the JS hook expects `hours` in valid_time order. forecast_times = Enum.reject(socket.assigns.valid_times, &(selected && DateTime.compare(&1, selected) == :eq)) + # `on_timeout: :kill_task` + explicit filter on :ok keeps a slow NFS + # read from cascading the whole stream's :timeout exit up into the + # LiveView process. A timed-out forecast hour just drops from the + # preload payload; the client re-requests it on demand. hours = forecast_times |> Task.async_stream( @@ -423,9 +427,13 @@ defmodule MicrowavepropWeb.MapLive do end, max_concurrency: 4, ordered: true, - timeout: 10_000 + timeout: 10_000, + on_timeout: :kill_task ) - |> Enum.map(fn {:ok, h} -> h end) + |> Enum.flat_map(fn + {:ok, h} -> [h] + {:exit, _} -> [] + end) {:noreply, socket diff --git a/lib/microwaveprop_web/live/submit_live.ex b/lib/microwaveprop_web/live/submit_live.ex index 98edb3f2..66dfa1c1 100644 --- a/lib/microwaveprop_web/live/submit_live.ex +++ b/lib/microwaveprop_web/live/submit_live.ex @@ -57,7 +57,7 @@ defmodule MicrowavepropWeb.SubmitLive do @impl true def handle_event("switch_tab", %{"tab" => tab}, socket) do socket = assign(socket, active_tab: String.to_existing_atom(tab)) - {:noreply, LiveStash.stash(socket)} + {:noreply, MicrowavepropWeb.LiveStashGuard.stash(socket)} end def handle_event("validate", %{"contact" => contact_params}, socket) do