defmodule LiveDebuggerWeb.Hooks.LinkedView do @moduledoc """ This hook should be used for views that are monitoring a debugged LiveView process. It assumes that the view is mounted with a `pid` param that identifies the LiveView process. It starts an async job to fetch the LiveView process via `lv_process` assign that is `AsyncResult` struct. - If the `lv_process` assign is not found, it will try to find a successor LiveView process and navigate to it. - If no successor is found, it will navigate to the error page. It also handles situation when process dies by updating `lv_process.alive?` field. The only thing you need to do after adding this hook is to handle loading state in the template. ## Example ```html <.async_result :let={lv_process} assign={@lv_process}> <:loading>
<.spinner size="xl" />
``` """ require Logger import Phoenix.LiveView import LiveDebuggerWeb.Helpers import Phoenix.Component alias LiveDebugger.Feature alias LiveDebugger.Structs.LvProcess alias Phoenix.LiveView.AsyncResult alias LiveDebugger.Services.LiveViewDiscoveryService alias LiveDebugger.Utils.Parsers alias LiveDebuggerWeb.Helpers.RoutesHelper alias LiveDebugger.Utils.PubSub, as: PubSubUtils defmacro __using__(_opts) do quote do on_mount({LiveDebuggerWeb.Hooks.LinkedView, :add_hook}) end end def on_mount(:add_hook, %{"pid" => string_pid}, _session, socket) do socket |> attach_hook(:linked_view, :handle_async, &handle_async/3) |> attach_hook(:linked_view, :handle_info, &handle_info/2) |> start_async_assign_lv_process(string_pid) end # When fetching LvProcess fails, we try to find a successor LvProcess def handle_async(:fetch_lv_process, {:ok, nil}, socket) do socket |> find_successor_lv_process() |> halt() end # When fetching LvProcess succeeds, we subscribe to its process state def handle_async(:fetch_lv_process, {:ok, fetched_lv_process}, socket) do PubSubUtils.process_status_topic() |> PubSubUtils.subscribe!() if Feature.enabled?(:dead_view_mode) do LiveDebugger.GenServers.EtsTableServer.watch(fetched_lv_process.pid) end socket |> assign(:lv_process, AsyncResult.ok(fetched_lv_process)) |> halt() end # When fetching LvProcess fails, we assign the error to the socket def handle_async(:fetch_lv_process, {:exit, reason}, socket) do Logger.error( "LiveDebugger encountered unexpected error while fetching information for process: #{inspect(reason)}" ) socket |> push_navigate(to: RoutesHelper.error("unexpected_error")) |> halt() end def handle_async(:find_successor, {:ok, %{pid: successor_pid}}, socket) do socket |> push_navigate(to: RoutesHelper.channel_dashboard(successor_pid)) |> halt() end def handle_async(:find_successor, _, socket) do socket |> push_navigate(to: RoutesHelper.error("unexpected_error")) |> halt() end def handle_async(_, _, socket), do: {:cont, socket} def handle_info(:find_successor, socket) do socket |> async_find_successor_lv_process() |> halt() end def handle_info( {:process_status, {:died, pid}}, %{assigns: %{lv_process: %{result: %LvProcess{pid: pid}}}} = socket ) when is_pid(pid) do socket |> make_lv_process_dead() |> halt() end def handle_info( {:process_status, {:dead, pid}}, %{assigns: %{lv_process: %{result: %LvProcess{pid: pid}}}} = socket ) do if Feature.enabled?(:dead_view_mode) do socket else find_successor_lv_process(socket) end |> halt() end def handle_info(_, socket), do: {:cont, socket} defp async_find_successor_lv_process( %{assigns: %{lv_process: %{result: %LvProcess{} = lv_process}}} = socket ) do socket |> assign(:lv_process, AsyncResult.loading()) |> start_async(:find_successor, fn -> fetch_with_retries(fn -> LiveViewDiscoveryService.successor_lv_process(lv_process) end) end) end defp async_find_successor_lv_process(socket) do push_navigate(socket, to: RoutesHelper.error("not_found")) end defp find_successor_lv_process(socket) do with %{lv_process: %{result: %LvProcess{} = lv_process}} <- socket.assigns, %{pid: successor_pid} <- fetch_with_retries(fn -> LiveViewDiscoveryService.successor_lv_process(lv_process) end) do push_navigate(socket, to: RoutesHelper.channel_dashboard(successor_pid)) else _ -> push_navigate(socket, to: RoutesHelper.error("not_found")) end end defp start_async_assign_lv_process(socket, string_pid) do case Parsers.string_to_pid(string_pid) do {:ok, pid} -> socket |> assign(:lv_process, AsyncResult.loading()) |> start_async(:fetch_lv_process, fetch_lv_process_with_retries(pid)) |> cont() :error -> socket |> push_navigate(to: RoutesHelper.error("invalid_pid")) |> halt() end end defp fetch_lv_process_with_retries(pid) do fn -> fetch_with_retries(fn -> LvProcess.new(pid) end) end end defp fetch_with_retries(function) do with nil <- fetch_after(function, 200), nil <- fetch_after(function, 800) do fetch_after(function, 1000) end end defp fetch_after(function, milliseconds) do Process.sleep(milliseconds) function.() end defp make_lv_process_dead( %{assigns: %{lv_process: %AsyncResult{ok?: true, result: result}}} = socket ) do lv_process = AsyncResult.ok(%LvProcess{result | alive?: false}) assign(socket, :lv_process, lv_process) end end