Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 42 additions & 29 deletions lib/phoenix_live_view/channel.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1153,9 +1153,9 @@ defmodule Phoenix.LiveView.Channel do

diff = Diff.render_private(socket, diff)
new_socket = Utils.clear_temp(socket)
new_state = destroy_discarded_components(%{state | socket: new_socket}, fingerprints)

{:diff, diff,
%{state | socket: new_socket, fingerprints: fingerprints, components: components}}
{:diff, diff, %{new_state | fingerprints: fingerprints, components: components}}
end

defp reply(state, {ref, extra}, status, payload) do
Expand Down Expand Up @@ -1705,38 +1705,51 @@ defmodule Phoenix.LiveView.Channel do
end

defp delete_components(state, cids) do
upload_cids = Enum.into(state.upload_names, MapSet.new(), fn {_name, {_ref, cid}} -> cid end)

Enum.flat_map_reduce(cids, state, fn cid, acc ->
{deleted_cids, new_components} = Diff.delete_component(cid, acc.components)
new_state = destroy_components(acc, deleted_cids)
{deleted_cids, %{new_state | components: new_components}}
end)
end

canceled_confs =
Enum.flat_map(deleted_cids, fn deleted_cid ->
read_socket(acc, deleted_cid, fn c_socket, component ->
:telemetry.execute([:phoenix, :live_component, :destroyed], %{}, %{
socket: c_socket,
component: component,
cid: deleted_cid,
live_view_socket: acc.socket
})

cancel_asyncs(c_socket)
Phoenix.LiveView.PubSub.unsubscribe_cid(deleted_cid)

if deleted_cid in upload_cids do
{_new_c_socket, canceled_confs} = Upload.maybe_cancel_uploads(c_socket)
canceled_confs
else
[]
end
end)
end)
# Diff.render/4 discards all components when the root fingerprint changes,
# for example when render/1 returns a different template. Those components
# never go through delete_components/2, so we destroy them here.
defp destroy_discarded_components(%{fingerprints: {old_print, _}} = state, {new_print, _})
when old_print != nil and old_print != new_print do
{cid_to_component, _id_to_cid, _uuids} = state.components
destroy_components(state, Map.keys(cid_to_component))
end

new_state =
Enum.reduce(canceled_confs, acc, fn conf, acc -> drop_upload_name(acc, conf.name) end)
defp destroy_discarded_components(state, _new_fingerprints), do: state

{deleted_cids, %{new_state | components: new_components}}
end)
# The components must still be in state.components.
defp destroy_components(state, cids) do
upload_cids = MapSet.new(state.upload_names, fn {_name, {_ref, cid}} -> cid end)

canceled_confs =
Enum.flat_map(cids, fn cid ->
read_socket(state, cid, fn c_socket, component ->
:telemetry.execute([:phoenix, :live_component, :destroyed], %{}, %{
socket: c_socket,
component: component,
cid: cid,
live_view_socket: state.socket
})

cancel_asyncs(c_socket)
Phoenix.LiveView.PubSub.unsubscribe_cid(cid)

if cid in upload_cids do
{_new_c_socket, canceled_confs} = Upload.maybe_cancel_uploads(c_socket)
canceled_confs
else
[]
end
end)
end)

Enum.reduce(canceled_confs, state, fn conf, acc -> drop_upload_name(acc, conf.name) end)
end

defp ensure_unique_upload_name!(state, conf) do
Expand Down
17 changes: 17 additions & 0 deletions test/phoenix_live_view/integrations/live_components_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,27 @@ defmodule Phoenix.LiveView.LiveComponentsTest do
end

test "tracks removals when whole root changes", %{conn: conn} do
ref =
:telemetry_test.attach_event_handlers(self(), [[:phoenix, :live_component, :destroyed]])

{:ok, view, _html} = live(conn, "/components")
assert render_click(view, "disable-all", %{}) =~ "Disabled\n"
# Sync to make sure it is still alive
assert render(view) =~ "Disabled\n"

assert_received {[:phoenix, :live_component, :destroyed], ^ref, _,
%{
component: StatefulComponent,
cid: 1,
socket: %{assigns: %{name: "chris"}}
}}

assert_received {[:phoenix, :live_component, :destroyed], ^ref, _,
%{
component: StatefulComponent,
cid: 2,
socket: %{assigns: %{name: "jose"}}
}}
end

test "tracks removals from a nested LiveView", %{conn: conn} do
Expand Down
21 changes: 21 additions & 0 deletions test/phoenix_live_view/integrations/pubsub_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ defmodule Phoenix.LiveViewTest.Support.PubSubLive do
assign(socket, :messages, socket.assigns.messages ++ [message])
end

def render(%{replaced: true} = assigns) do
~H"""
<div id="replaced">replaced</div>
"""
end

def render(assigns) do
~H"""
<div id="root">root: {inspect(@messages)}</div>
Expand Down Expand Up @@ -80,6 +86,10 @@ defmodule Phoenix.LiveViewTest.Support.PubSubLive do
{:noreply, assign(socket, :children, socket.assigns.children -- [id])}
end

def handle_event("replace-root", _params, socket) do
{:noreply, assign(socket, :replaced, true)}
end

def handle_info(message, socket) do
{:noreply, assign(socket, :direct_messages, socket.assigns.direct_messages ++ [message])}
end
Expand Down Expand Up @@ -379,6 +389,17 @@ defmodule Phoenix.LiveViewTest.PubSubTest do

eventually(fn -> assert [] = subscriptions() end)
end

test "unsubscribes when the root template is replaced", %{conn: conn} do
{:ok, lv, _html} = live_isolated(conn, PubSubLive)
render_click(lv, "unsubscribe", %{})
render_click(lv, "add-child", %{"id" => "child-1"})

assert [_] = subscriptions()

assert render_click(lv, "replace-root", %{}) =~ "replaced"
assert [] = subscriptions()
end
end

describe "endpoint pubsub server" do
Expand Down
14 changes: 14 additions & 0 deletions test/phoenix_live_view/integrations/start_async_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,20 @@ defmodule Phoenix.LiveView.StartAsyncTest do
refute render(lv) =~ "lc:"
end

test "replacing the root template cancels the component's async tasks", %{conn: conn} do
Process.register(self(), :start_async_test_process)
{:ok, lv, _html} = live(conn, "/start_async?test=lc_cancel_on_destroy")

async_ref = wait_for_async_ready_and_monitor(:start_async_destroy)

assert render(lv) =~ "lc: :loading"

send(lv.pid, :replace_root)

assert_receive {:DOWN, ^async_ref, :process, _pid, {:shutdown, :cancel}}, 1000
assert render(lv) =~ "replaced"
end

test "complex key task", %{conn: conn} do
{:ok, lv, _html} = live(conn, "/start_async?test=lc_complex_key")

Expand Down
26 changes: 26 additions & 0 deletions test/phoenix_live_view/upload/channel_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1500,6 +1500,32 @@ defmodule Phoenix.LiveView.UploadChannelTest do
assert render_upload(avatar, "myfile1.jpeg", 100) =~ "component:myfile1.jpeg:100%"
end

@tag allow: [max_entries: 1, chunk_size: 20, accept: :any]
test "cancel_upload in progress when the root template is replaced", %{lv: lv} do
avatar = file_input(lv, "#upload0", :avatar, build_entries(1))
assert render_upload(avatar, "myfile1.jpeg", 1) =~ "component:myfile1.jpeg:1%"
assert %{"myfile1.jpeg" => channel_pid} = UploadClient.channel_pids(avatar)

unlink(channel_pid, lv, avatar)
Process.monitor(channel_pid)

GenServer.call(lv.pid, {:replace_root, true})
assert render(lv) =~ "replaced"

assert_receive {:DOWN, _ref, :process, ^channel_pid, {:shutdown, :closed}}, 1000

# retry with a new component using the same upload name
GenServer.call(lv.pid, {:replace_root, false})

UploadLive.run(lv, fn component_socket ->
new_socket = Phoenix.LiveView.allow_upload(component_socket, :avatar, accept: :any)
{:reply, :ok, new_socket}
end)

avatar = file_input(lv, "#upload0", :avatar, build_entries(1))
assert render_upload(avatar, "myfile1.jpeg", 100) =~ "component:myfile1.jpeg:100%"
end

@tag allow: [max_entries: 1, chunk_size: 20, accept: :any]
test "cancel_upload not yet in progress when component is removed", %{lv: lv} do
file_name = "myfile1.jpeg"
Expand Down
10 changes: 10 additions & 0 deletions test/support/live_views/start_async.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ defmodule Phoenix.LiveViewTest.Support.StartAsyncLive do
{:cont, assign(socket, lc: false)}
end

def render(%{replaced: true} = assigns) do
~H"""
<div>replaced</div>
"""
end

def render(assigns) do
~H"""
<.live_component
Expand Down Expand Up @@ -204,6 +210,10 @@ defmodule Phoenix.LiveViewTest.Support.StartAsyncLive do
{:noreply, assign(socket, lc: false)}
end

def handle_info(:replace_root, socket) do
{:noreply, assign(socket, replaced: true)}
end

def handle_info(:cancel, socket) do
{:noreply, cancel_async(socket, :result_task)}
end
Expand Down
10 changes: 10 additions & 0 deletions test/support/live_views/upload_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,12 @@ end
defmodule Phoenix.LiveViewTest.Support.UploadLiveWithComponent do
use Phoenix.LiveView

def render(%{replaced: true} = assigns) do
~H"""
<div>replaced</div>
"""
end

def render(assigns) do
~H"""
<div>
Expand All @@ -201,6 +207,10 @@ defmodule Phoenix.LiveViewTest.Support.UploadLiveWithComponent do
{:reply, :ok, assign(socket, :uploads_count, count)}
end

def handle_call({:replace_root, replaced?}, _from, socket) do
{:reply, :ok, assign(socket, :replaced, replaced?)}
end

def handle_call({:run, func}, from, socket) do
send_update(Phoenix.LiveViewTest.Support.UploadComponent, id: "upload0", run: {func, from})
{:noreply, socket}
Expand Down
Loading