LiveView Async With Regular Processes

So, LiveView 0.20 brought an async processing API which is a thin wrapper around tasks. It’s a very nice convenience, but the main thing that intrigues me is support within live_components. This is the first time that live components have been able to receive system messages directly. Anyone who’s tried to do async data loading in a live component knows what I’m talking about.

This is great, but it’s a very narrow API. It lets you perform a single async task and get its results. I’m proposing a more generic version of this API. Currently we have

def mount(%{"id" => id}, _, socket) do
  {:ok,
   socket
   |> assign(:org, AsyncResult.loading())
   |> start_async(:my_task, fn -> fetch_org!(id) end)
end

def handle_async(:my_task, {:ok, fetched_org}, socket) do
  %{org: org} = socket.assigns
  {:noreply, assign(socket, :org, AsyncResult.ok(org, fetched_org))}
end

def handle_async(:my_task, {:exit, reason}, socket) do
  %{org: org} = socket.assigns
  {:noreply, assign(socket, :org, AsyncResult.failed(org, {:exit, reason}))}
end

but I’d like to be able to do

defmodule App.LiveOrg do
  use GenServer

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts)
  end

  @impl true
  def init(opts) do
    :ok = Phoenix.PubSub.subscribe(App.PubSub, "org:#{opts.id}")
    org = fetch_org!(id)
    :ok = Phoenix.LiveView.async_update(opts.live_view_ref, org)
    {:ok, %{org_id: opts.id, live_view_ref: opts.live_view_ref}}
  end

  @impl true
  def handle_info(_pub_sub_message, state) do
    org = fetch_org!(state.org_id)
    :ok = Phoenix.LiveView.async_update(opts.live_view_ref, org))
    {:noreply, state}
  end
end

# in my live view or live component

def mount(%{"id" => id}, _, socket) do
  {:ok,
   socket
   |> assign(:org, AsyncResult.loading())
   |> start_async(:my_live_org, App.LiveOrg, %{id: id})
end

def handle_async(:my_live_org, {:update, fetched_org}, socket) do
  %{org: org} = socket.assigns
  {:noreply, assign(socket, :org, AsyncResult.ok(org, fetched_org))}
end

def handle_async(:my_live_org, {:exit, reason}, socket) do
  %{org: org} = socket.assigns
  {:noreply, assign(socket, :org, AsyncResult.failed(org, {:exit, reason}))}
end

Just to clarify what’s happening here, I’m asking for start_async to be able to start and supervise arbitrary processes. When it does this it should pass a processes a :live_view_ref option to the process which the new Phoenix.LiveView.async_update/2 function can take to send an async update back to the live view or live component. This allows the started process to send multiple updates, and for those updates to target the underlying phoenix live process and assign correctly (regardless of if that assign is in a live view or a live component).

Conceptually this is nearly identical to the current task based API, and I think we should keep the task based API around as a convenience on top of this API, but I’m a fan of having the full power of OTP rather than just tasks.

2 Likes