Skip to content

Commit

Permalink
Checkpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
chrismccord committed Aug 11, 2023
1 parent ab39c68 commit eda3907
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 94 deletions.
146 changes: 64 additions & 82 deletions lib/phoenix_live_view/async_result.ex
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ defmodule Phoenix.LiveView.AsyncResult do
<.async_result :let={org} assign={@async.org}>
<:loading>Loading organization...</:loading>
<:empty>You don't have an organization yet</:error>
<:error>there was an error loading the organization</:error>
<:failed>there was an error loading the organization</:failed>
<%= org.name %>
<.async_result>
```
Expand Down Expand Up @@ -108,13 +108,6 @@ defmodule Phoenix.LiveView.AsyncResult do
%AsyncResult{result | state: {:exit, reason}}
end

@doc """
TODO
"""
def throw(%AsyncResult{} = result, value) do
%AsyncResult{result | state: {:throw, value}}
end

@doc """
TODO
"""
Expand Down Expand Up @@ -164,7 +157,7 @@ defmodule Phoenix.LiveView.AsyncResult do
%AsyncResult{state: :canceled} ->
~H|<%= render_slot(@canceled) %>|

%AsyncResult{state: {kind, _value}} when kind in [:error, :exit, :throw] ->
%AsyncResult{state: {kind, _reason}} when kind in [:error, :exit] ->
~H|<%= render_slot(@error, @assign.state) %>|
end
end
Expand All @@ -182,61 +175,68 @@ defmodule Phoenix.LiveView.AsyncResult do
is_function(func, 0) do
keys = List.wrap(key_or_keys)

# verifies result inside task
wrapped_func = fn ->
case func.() do
{:ok, %{} = assigns} ->
if Map.keys(assigns) -- keys == [] do
{:ok, assigns}
else
raise ArgumentError, """
expected assign_async to return map of assigns for all keys
in #{inspect(keys)}, but got: #{inspect(assigns)}
"""
end

{:error, reason} ->
{:error, reason}

other ->
raise ArgumentError, """
expected assign_async to return {:ok, map} of
assigns for #{inspect(keys)} or {:error, reason}, got: #{inspect(other)}
"""
end
end

keys
|> Enum.reduce(socket, fn key, acc ->
Phoenix.Component.assign(acc, key, AsyncResult.new(key, keys))
end)
|> run_async_task(keys, func, fn new_socket, _component_mod, result ->
|> run_async_task(keys, wrapped_func, fn new_socket, _component_mod, result ->
assign_result(new_socket, keys, result)
end)
end

defp assign_result(socket, keys, result) do
case result do
{:ok, %{} = values} ->
if Map.keys(values) -- keys == [] do
Enum.reduce(values, socket, fn {key, val}, acc ->
current_async = get_current_async!(acc, key)
Phoenix.Component.assign(acc, key, AsyncResult.ok(current_async, val))
end)
else
raise ArgumentError, """
expected assign_async to return map of assigns for all keys
in #{inspect(keys)}, but got: #{inspect(values)}
"""
end

{:error, reason} ->
Enum.reduce(keys, socket, fn key, acc ->
{:ok, {:ok, %{} = assigns}} ->
Enum.reduce(assigns, socket, fn {key, val}, acc ->
current_async = get_current_async!(acc, key)
Phoenix.Component.assign(acc, key, AsyncResult.error(current_async, reason))
Phoenix.Component.assign(acc, key, AsyncResult.ok(current_async, val))
end)

{:exit, reason} ->
{:ok, {:error, reason}} ->
Enum.reduce(keys, socket, fn key, acc ->
current_async = get_current_async!(acc, key)
Phoenix.Component.assign(acc, key, AsyncResult.exit(current_async, reason))
Phoenix.Component.assign(acc, key, AsyncResult.error(current_async, reason))
end)

{:throw, value} ->
{:catch, kind, reason, stack} ->
normalized_exit = to_exit(kind, reason, stack)

Enum.reduce(keys, socket, fn key, acc ->
current_async = get_current_async!(acc, key)
Phoenix.Component.assign(acc, key, AsyncResult.throw(current_async, value))
Phoenix.Component.assign(acc, key, AsyncResult.exit(current_async, normalized_exit))
end)

other ->
raise ArgumentError, """
expected assign_async to return {:ok, map} of
assigns for #{inspect(keys)} or {:error, reason}, got: #{inspect(other)}
"""
end
end

defp get_current_async!(socket, key) do
# handle case where assign is temporary and needs to be rebuilt
case socket.assigns do
%{^key => %AsynResult{} = current_async} -> current_async
%{^key => _other} -> AsyncResult.new(key, keys)
%{^key => %AsyncResult{} = current_async} -> current_async
%{^key => _other} -> AsyncResult.new(key, key)
%{} -> raise ArgumentError, "missing async assign #{inspect(key)}"
end
end
Expand All @@ -257,20 +257,13 @@ defmodule Phoenix.LiveView.AsyncResult do
run_async_task(socket, [name], func, fn new_socket, component_mod, result ->
callback_mod = component_mod || new_socket.view

case result do
{tag, value} when tag in [:ok, :error, :exit, :throw] ->
:ok

other ->
raise ArgumentError, """
expected start_async for #{inspect(name)} in #{inspect(callback_mod)}
to return {:ok, result} | {:error, reason}, got:
#{inspect(other)}
"""
end
normalized_result =
case result do
{:ok, result} -> {:ok, result}
{:catch, kind, reason, stack} -> {:exit, to_exit(kind, reason, stack)}
end

case callback_mod.handle_async(name, result, new_socket) do
case callback_mod.handle_async(name, normalized_result, new_socket) do
{:noreply, %Socket{} = new_socket} ->
new_socket

Expand All @@ -292,7 +285,7 @@ defmodule Phoenix.LiveView.AsyncResult do
cid = cid(socket)
ref = make_ref()
{:ok, pid} = Task.start_link(fn -> do_async(lv_pid, cid, ref, keys, func, result_func) end)
update_private_async(socket, keys, {ref, pid})
update_private_async(socket, &Map.put(&1, keys, {ref, pid}))
else
socket
end
Expand All @@ -303,26 +296,31 @@ defmodule Phoenix.LiveView.AsyncResult do
result = func.()

Phoenix.LiveView.Channel.write_socket(lv_pid, cid, fn socket, component_mod ->
handle_current_async(socket, keys, ref, component_mod, result, result_func)
handle_current_async(socket, keys, ref, component_mod, {:ok, result}, result_func)
end)
catch
kind, reason ->
Process.unlink(lv_pid)
caught_result = {:catch, kind, reason, __STACKTRACE__}

Phoenix.LiveView.Channel.write_socket(lv_pid, cid, fn socket, component_mod ->
handle_current_async(socket, keys, ref, component_mod, {kind, reason}, result_func)
handle_current_async(socket, keys, ref, component_mod, caught_result, result_func)
end)

:erlang.raise(kind, reason, __STACKTRACE__)
end
end

defp to_exit(:throw, reason, stack), do: {{:nocatch, reason}, stack}
defp to_exit(:error, reason, stack), do: {reason, stack}
defp to_exit(:exit, reason, _stack), do: reason

# handle race of async being canceled and then reassigned
defp handle_current_async(socket, keys, ref, component_mod, result, result_func)
when is_function(result_func, 3) do
get_private_async(socket, keys) do
{^ref, pid} ->
new_socket = delete_private_async(socket, keys)
case get_private_async(socket, keys) do
{^ref, _pid} ->
new_socket = update_private_async(socket, &Map.delete(&1, keys))
result_func.(new_socket, component_mod, result)

{_ref, _pid} ->
Expand Down Expand Up @@ -357,46 +355,30 @@ defmodule Phoenix.LiveView.AsyncResult do

def cancel_async(%Socket{} = socket, keys) when is_list(keys) do
case get_private_async(socket, keys) do
{ref, pid} when is_pid(pid) ->
{_ref, pid} when is_pid(pid) ->
Process.unlink(pid)
Process.exit(pid, :kill)
delete_private_async(socket, keys)
update_private_async(socket, &Map.delete(&1, keys))

nil ->
raise ArgumentError, "uknown async assign #{inspect(keys)}"
end
end

defp update_private_async(socket, keys, {ref, pid}) do
socket
|> ensure_private_async()
|> Phoenix.Component.update(:async, fn async_map ->
Map.put(async_map, keys, {ref, pid})
end)
end

defp delete_private_async(socket, keys) do
socket
|> ensure_private_async()
|> Phoenix.Component.update(:async, fn async_map -> Map.delete(async_map, keys) end)
end

defp ensure_private_async(socket) do
case socket.private do
%{phoenix_async: _} -> socket
%{} -> Phoenix.LiveView.put_private(socket, :phoenix_async, %{})
end
defp update_private_async(socket, func) do
existing = socket.private[:phoenix_async] || %{}
Phoenix.LiveView.put_private(socket, :phoenix_async, func.(existing))
end

defp get_private_async(%Socket{} = socket, keys) do
socket.private[:phoenix_async][keys]
end

defp cancel_existing(socket, keys) when is_list(keys) do
if get_private_async(acc, keys) do
cancel_async(acc, keys)
defp cancel_existing(%Socket{} = socket, keys) when is_list(keys) do
if get_private_async(socket, keys) do
cancel_async(socket, keys)
else
acc
socket
end
end

Expand Down
4 changes: 2 additions & 2 deletions lib/phoenix_live_view/channel.ex
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ defmodule Phoenix.LiveView.Channel do
component_pids =
state
|> component_privates()
|> get_async_pids()
|> Enum.flat_map(fn {_cid, private} -> get_async_pids(private) end)

{:reply, {:ok, lv_pids ++ component_pids}, state}
end
Expand Down Expand Up @@ -1432,7 +1432,7 @@ defmodule Phoenix.LiveView.Channel do

defp get_async_pids(private) do
case private do
%{phoenix_async: ref_pids} -> Map.values(ref_pids)
%{phoenix_async: ref_pids} -> Enum.flat_map(ref_pids, fn {_key, {_ref, pid}} -> [pid] end)
%{} -> []
end
end
Expand Down
16 changes: 8 additions & 8 deletions test/phoenix_live_view/integrations/assign_async_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ defmodule Phoenix.LiveView.AssignAsyncTest do
{:ok, lv, _html} = live(conn, "/async?test=bad_return")

assert render_async(lv) =~
"error: {:error, %ArgumentError{message: &quot;expected assign_async to return {:ok, map} of\\nassigns for [:data] or {:error, reason}, got: 123\\n&quot;}}"
"exit: {%ArgumentError{message: &quot;expected assign_async to return {:ok, map} of\\nassigns for [:data] or {:error, reason}, got: 123\\n&quot;}"

assert render(lv)
end
Expand All @@ -25,7 +25,7 @@ defmodule Phoenix.LiveView.AssignAsyncTest do
{:ok, lv, _html} = live(conn, "/async?test=bad_ok")

assert render_async(lv) =~
"expected assign_async to return map of\\nassigns for all keys in [:data]"
"expected assign_async to return map of assigns for all keys\\nin [:data]"

assert render(lv)
end
Expand All @@ -38,14 +38,14 @@ defmodule Phoenix.LiveView.AssignAsyncTest do
test "raise during execution", %{conn: conn} do
{:ok, lv, _html} = live(conn, "/async?test=raise")

assert render_async(lv) =~ "error: {:error, %RuntimeError{message: &quot;boom&quot;}}"
assert render_async(lv) =~ "exit: {%RuntimeError{message: &quot;boom&quot;}"
assert render(lv)
end

test "exit during execution", %{conn: conn} do
{:ok, lv, _html} = live(conn, "/async?test=exit")

assert render_async(lv) =~ "error: {:exit, :boom}"
assert render_async(lv) =~ "exit: :boom"
assert render(lv)
end

Expand Down Expand Up @@ -90,7 +90,7 @@ defmodule Phoenix.LiveView.AssignAsyncTest do
{:ok, lv, _html} = live(conn, "/async?test=lc_bad_return")

assert render_async(lv) =~
"error: {:error, %ArgumentError{message: &quot;expected assign_async to return {:ok, map} of\\nassigns for [:lc_data] or {:error, reason}, got: 123\\n&quot;}}"
"exit: {%ArgumentError{message: &quot;expected assign_async to return {:ok, map} of\\nassigns for [:lc_data] or {:error, reason}, got: 123\\n&quot;}"

assert render(lv)
end
Expand All @@ -99,7 +99,7 @@ defmodule Phoenix.LiveView.AssignAsyncTest do
{:ok, lv, _html} = live(conn, "/async?test=lc_bad_ok")

assert render_async(lv) =~
"expected assign_async to return map of\\nassigns for all keys in [:lc_data]"
"expected assign_async to return map of assigns for all keys\\nin [:lc_data]"

assert render(lv)
end
Expand All @@ -112,14 +112,14 @@ defmodule Phoenix.LiveView.AssignAsyncTest do
test "raise during execution", %{conn: conn} do
{:ok, lv, _html} = live(conn, "/async?test=lc_raise")

assert render_async(lv) =~ "error: {:error, %RuntimeError{message: &quot;boom&quot;}}"
assert render_async(lv) =~ "exit: {%RuntimeError{message: &quot;boom&quot;}"
assert render(lv)
end

test "exit during execution", %{conn: conn} do
{:ok, lv, _html} = live(conn, "/async?test=lc_exit")

assert render_async(lv) =~ "error: {:exit, :boom}"
assert render_async(lv) =~ "exit: :boom"
assert render(lv)
end

Expand Down
2 changes: 1 addition & 1 deletion test/phoenix_live_view_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ defmodule Phoenix.LiveViewUnitTest do
describe "put_private" do
test "assigns private keys" do
assert @socket.private[:hello] == nil
assert put_private(@socket, :hello, "world").private[:hello] == "word"
assert put_private(@socket, :hello, "world").private[:hello] == "world"
end

test "disallows reserved keys" do
Expand Down
3 changes: 2 additions & 1 deletion test/support/live_views/general.ex
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ defmodule Phoenix.LiveViewTest.AsyncLive do
<div :if={@data.state == :canceled}>data canceled</div>
<div :if={@data.state == :ok && @data.result == nil}>no data found</div>
<div :if={@data.state == :ok && @data.result}>data: <%= inspect(@data.result) %></div>
<%= with {kind, reason} when kind in [:error, :exit, :throw] <- @data.state do %>
<%= with {kind, reason} when kind in [:error, :exit] <- @data.state do %>
<div><%= kind %>: <%= inspect(reason) %></div>
<% end %>
<%= if @enum do %>
Expand Down Expand Up @@ -428,6 +428,7 @@ end

defmodule Phoenix.LiveViewTest.AsyncLive.LC do
use Phoenix.LiveComponent
alias Phoenix.LiveView.AsyncResult
import Phoenix.LiveView.AsyncResult

def render(assigns) do
Expand Down

0 comments on commit eda3907

Please sign in to comment.