Skip to content

Instantly share code, notes, and snippets.

@brainlid
Last active February 14, 2024 21:22
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save brainlid/e56a1e04aa8babd8bc6b851d5a07eda5 to your computer and use it in GitHub Desktop.
Save brainlid/e56a1e04aa8babd8bc6b851d5a07eda5 to your computer and use it in GitHub Desktop.
Example files for a LiveView blog post that starts an async process using Phoenix Async Assigns to perform work and send message back to the LiveView. https://fly.io/phoenix-files/abusing-liveview-new-async-assigns-feature/
defmodule MyAppyWeb.TaskTestLive.Index do
use MyAppyWeb, :live_view
require Logger
alias Phoenix.LiveView.AsyncResult
@impl true
def mount(_params, _session, socket) do
socket =
socket
|> assign(:async_result, %AsyncResult{})
|> assign(:messages, [])
{:ok, socket}
end
@impl true
# start the async process
def handle_event("start", _params, socket) do
socket =
socket
|> assign(:messages, [])
|> start_test_task()
{:noreply, socket}
end
# cancel the async process
def handle_event("cancel", _params, socket) do
socket =
socket
|> cancel_async(:running_task)
|> assign(:async_result, %AsyncResult{})
|> put_flash(:info, "Cancelled")
{:noreply, socket}
end
# handles async function returning a successful result
def handle_async(:running_task, {:ok, :ok = _success_result}, socket) do
# discard the result of the successful async function. The side-effects are
# what we want.
socket =
socket
|> put_flash(:info, "Completed!")
|> assign(:async_result, AsyncResult.ok(%AsyncResult{}, :ok))
{:noreply, socket}
end
# handles async function returning an error as a result
def handle_async(:running_task, {:ok, {:error, reason}}, socket) do
socket =
socket
|> put_flash(:error, reason)
|> assign(:async_result, AsyncResult.failed(%AsyncResult{}, reason))
{:noreply, socket}
end
# handles async function exploding
def handle_async(:running_task, {:exit, reason}, socket) do
socket =
socket
|> put_flash(:error, "Task failed: #{inspect(reason)}")
|> assign(:async_result, %AsyncResult{})
{:noreply, socket}
end
@impl true
# handle receiving a message sent from the async process
def handle_info({:task_message, message}, socket) do
socket =
socket
|> assign(:messages, [message | socket.assigns.messages])
{:noreply, socket}
end
# start the async process
def start_test_task(socket) do
live_view_pid = self()
socket
|> assign(:async_result, AsyncResult.loading())
|> start_async(:running_task, fn ->
# the code to run async
Enum.each(1..5, fn n ->
Process.sleep(1_000)
IO.puts("SENDING ASYNC TASK MESSAGE #{n}")
# raise "TASK RAISED EXCEPTION"
send(live_view_pid, {:task_message, "Async work chunk #{n}"})
end)
# return a small, controlled value because it isn't being used anyway.
:ok
# return an error result from the function
# {:error, "Function reporting error"}
end)
end
end
<.header>
Task Test Output
</.header>
<div class="my-4 text-center">
<.button :if={!@async_result.loading} phx-click="start">Start</.button>
<.button :if={@async_result.loading} phx-click="cancel">Cancel</.button>
</div>
<ul id="messages" role="list" class="text-sm divide-y divide-gray-100">
<li :for={msg <- @messages} class="relative flex justify-between gap-x-6 py-5 hover:bg-zinc-50 sm:rounded">
<div class="font-semibold text-zinc-900">
<%= msg %>
</div>
</li>
</ul>
defmodule MyAppWeb.Router do
use MyAppWeb, :router
# ...
scope "/", MyAppWeb do
pipe_through :browser
get "/", PageController, :home
live "/task_test", TaskTestLive.Index, :index
end
# ...
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment