Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow sharing assigns between live navigation #3482

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
4 changes: 2 additions & 2 deletions assets/js/phoenix_live_view/live_socket.js
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ export default class LiveSocket {
let removeEls = DOM.all(this.outgoingMainEl, `[${this.binding("remove")}]`)
let newMainEl = DOM.cloneNode(this.outgoingMainEl, "")
this.main.showLoader(this.loaderTimeout)
this.main.destroy()
this.main.destroy(false)

this.main = this.newRootView(newMainEl, flash, liveReferer)
this.main.setRedirect(href)
Expand All @@ -407,7 +407,7 @@ export default class LiveSocket {
onDone()
})
}
})
}, true)
}

transitionRemoves(elements, skipSticky, callback){
Expand Down
58 changes: 51 additions & 7 deletions assets/js/phoenix_live_view/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ export default class View {
return val === "" ? null : val
}

destroy(callback = function (){ }){
destroy(leave = true, callback = function (){ }){
this.destroyAllChildren()
this.destroyed = true
delete this.root.children[this.id]
Expand All @@ -221,10 +221,14 @@ export default class View {
DOM.markPhxChildDestroyed(this.el)

this.log("destroyed", () => ["the child has been removed from the parent"])
this.channel.leave()
.receive("ok", onFinished)
.receive("error", onFinished)
.receive("timeout", onFinished)
if(leave){
this.channel.leave()
.receive("ok", onFinished)
.receive("error", onFinished)
.receive("timeout", onFinished)
} else {
onFinished()
}
}

setContainerClasses(...classes){
Expand Down Expand Up @@ -761,6 +765,8 @@ export default class View {
this.onChannel("redirect", ({to, flash}) => this.onRedirect({to, flash}))
this.onChannel("live_patch", (redir) => this.onLivePatch(redir))
this.onChannel("live_redirect", (redir) => this.onLiveRedirect(redir))
this.onChannel("live_handover", (redir) => this.startHandover(redir))
this.onChannel("phx_handover", (payload) => this.completeHandover(payload))
this.channel.onError(reason => this.onError(reason))
this.channel.onClose(reason => this.onClose(reason))
}
Expand All @@ -786,6 +792,44 @@ export default class View {

onRedirect({to, flash, reloadToken}){ this.liveSocket.redirect(to, flash, reloadToken) }

startHandover(redir){
if(!this.isMain()){
throw new Error("unexpected handover for non main view")
}
let {to, kind, _flash} = redir
let href = this.expandURL(to)
let scroll = window.scrollY
this.liveSocket.withPageLoading({to: href, kind}, done => {
// let liveReferer = this.currentLocation.href
let removeEls = DOM.all(this.el, `[${this.binding("remove")}]`)
let newMainEl = DOM.cloneNode(this.el, "")
this.outGoingEl = this.el
this.el = newMainEl
this.showLoader(this.liveSocket.loaderTimeout)

this.setRedirect(href)
this.liveSocket.transitionRemoves(removeEls, true)
this.handoverCallback = () => {
this.stopCallback = function(){}
this.liveSocket.requestDOMUpdate(() => {
// remove phx-remove els right before we replace the main element
removeEls.forEach(el => el.remove())
DOM.findPhxSticky(document).forEach(el => newMainEl.appendChild(el))
this.outGoingEl.replaceWith(this.el)
Browser.pushState(kind, {type: "redirect", id: this.id, scroll: scroll}, href)
DOM.dispatchEvent(window, "phx:navigate", {detail: {href, patch: false, pop: false}})
this.liveSocket.registerNewLocation(window.location)
done()
})
}
})
}

completeHandover(payload){
this.stopCallback = this.handoverCallback
this.onJoin(payload)
}

isDestroyed(){ return this.destroyed }

joinDead(){ this.isDead = true }
Expand All @@ -795,7 +839,7 @@ export default class View {
return this.joinPush
}

join(callback){
join(callback, handover = false){
this.showLoader(this.liveSocket.loaderTimeout)
this.bindChannel()
if(this.isMain()){
Expand All @@ -806,7 +850,7 @@ export default class View {
callback ? callback(this.joinCount, onDone) : onDone()
}

this.wrapPush(() => this.channel.join(), {
this.wrapPush(() => this.channel.join(this.liveSocket.socket.timeout, handover), {
ok: (resp) => this.liveSocket.requestDOMUpdate(() => this.onJoin(resp)),
error: (error) => this.onJoinError(error),
timeout: () => this.onJoinError({reason: "timeout"})
Expand Down
135 changes: 115 additions & 20 deletions lib/phoenix_live_view/channel.ex
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ defmodule Phoenix.LiveView.Channel do
end)
end

def handle_call({@prefix, :child_mount, _child_pid, assign_new}, _from, state) do
def handle_call({@prefix, :get_assigns, _child_pid, assign_new}, _from, state) do
assigns = Map.take(state.socket.assigns, assign_new)
{:reply, {:ok, assigns}, state}
end
Expand Down Expand Up @@ -848,9 +848,12 @@ defmodule Phoenix.LiveView.Channel do
opts = copy_flash(new_state, flash, opts)

new_state
|> push_pending_events_on_redirect(new_socket)
|> push_live_redirect(opts, ref)
|> stop_shutdown_redirect(:live_redirect, opts)
|> maybe_handover(opts, ref, fn new_state ->
new_state
|> push_pending_events_on_redirect(new_socket)
|> push_live_redirect(opts, ref)
|> stop_shutdown_redirect(:live_redirect, opts)
end)

{:live, :patch, %{to: _to, kind: _kind} = opts} when root_pid == self() ->
{params, action} = patch_params_and_action!(new_socket, opts)
Expand Down Expand Up @@ -1079,6 +1082,34 @@ defmodule Phoenix.LiveView.Channel do
end
end

# handover from previous LV in the same live session
defp mount({:handover, new_verified, route, url, params}, from, phx_socket)
when is_map_key(phx_socket, :handover_pid) and is_pid(phx_socket.handover_pid) and
phx_socket.handover_pid == new_verified.root_pid do
%Phoenix.Socket{private: %{connect_info: connect_info}} = phx_socket
%Session{view: view} = new_verified

new_verified = %{new_verified | root_pid: self()}

case load_live_view(view) do
{:ok, config} ->
verified_mount(
new_verified,
config,
route,
url,
params,
from,
phx_socket,
connect_info
)

{:error, _reason} ->
GenServer.reply(from, {:error, %{reason: "stale"}})
{:stop, :shutdown, :no_state}
end
end

defp mount(%{}, from, phx_socket) do
Logger.error("Mounting #{phx_socket.topic} failed because no session was provided")
GenServer.reply(from, {:error, %{reason: "stale"}})
Expand All @@ -1098,7 +1129,7 @@ defmodule Phoenix.LiveView.Channel do
end

defp verified_mount(
%Session{} = verified,
%Session{} = verified_session,
config,
route,
url,
Expand All @@ -1110,19 +1141,21 @@ defmodule Phoenix.LiveView.Channel do
%Session{
id: id,
view: view,
root_view: root_view,
parent_pid: parent,
root_pid: root_pid,
session: verified_user_session,
assign_new: assign_new,
router: router
} = verified
} = verified_session

%Phoenix.Socket{
endpoint: endpoint,
transport_pid: transport_pid
} = phx_socket

# TODO: change this to directly pattern match on handover_pid above
# when we require Phoenix 1.8
handover_pid = Map.get(phx_socket, :handover_pid)

Process.put(:"$initial_call", {view, :mount, 3})

case params do
Expand All @@ -1134,7 +1167,7 @@ defmodule Phoenix.LiveView.Channel do
connect_params = params["params"]

# Optional verified parts
flash = verify_flash(endpoint, verified, params["flash"], connect_params)
flash = verify_flash(endpoint, verified_session, params["flash"], connect_params)

# connect_info is either a Plug.Conn during tests or a Phoenix.Socket map
socket_session = Map.get(connect_info, :session, %{})
Expand Down Expand Up @@ -1164,7 +1197,13 @@ defmodule Phoenix.LiveView.Channel do
merged_session = Map.merge(socket_session, verified_user_session)
lifecycle = load_lifecycle(config, route)

case mount_private(parent, root_view, assign_new, connect_params, connect_info, lifecycle) do
case mount_private(
verified_session,
connect_params,
connect_info,
lifecycle,
handover_pid
) do
{:ok, mount_priv} ->
socket = Utils.configure_socket(socket, mount_priv, action, flash, host_uri)

Expand All @@ -1174,7 +1213,7 @@ defmodule Phoenix.LiveView.Channel do
|> Utils.maybe_call_live_view_mount!(view, params, merged_session, url)
|> build_state(phx_socket)
|> maybe_call_mount_handle_params(router, url, params)
|> reply_mount(from, verified, route)
|> reply_mount(from, verified_session, route)
|> maybe_subscribe_to_live_reload()
rescue
exception ->
Expand Down Expand Up @@ -1254,41 +1293,59 @@ defmodule Phoenix.LiveView.Channel do
socket
end

defp mount_private(nil, root_view, assign_new, connect_params, connect_info, lifecycle) do
defp mount_private(
%Session{parent_pid: nil, root_view: root_view, assign_new: assign_new} =
verified_session,
connect_params,
connect_info,
lifecycle,
handover_pid
) do
{:ok,
%{
verified_session: verified_session,
connect_params: connect_params,
connect_info: connect_info,
assign_new: {%{}, assign_new},
lifecycle: lifecycle,
root_view: root_view,
live_temp: %{}
live_temp: %{},
handover_pid: handover_pid
}}
end

defp mount_private(parent, root_view, assign_new, connect_params, connect_info, lifecycle) do
case sync_with_parent(parent, assign_new) do
defp mount_private(
%Session{parent_pid: parent_pid, root_view: root_view, assign_new: assign_new} =
verified_session,
connect_params,
connect_info,
lifecycle,
_handover_pid
) do
case get_assigns(parent_pid, assign_new) do
{:ok, parent_assigns} ->
# Child live views always ignore the layout on `:use`.
{:ok,
%{
verified_session: verified_session,
connect_params: connect_params,
connect_info: connect_info,
assign_new: {parent_assigns, assign_new},
live_layout: false,
lifecycle: lifecycle,
root_view: root_view,
live_temp: %{}
live_temp: %{},
handover_pid: nil
}}

{:error, :noproc} ->
{:error, :noproc}
end
end

defp sync_with_parent(parent, assign_new) do
def get_assigns(pid, keys) do
try do
GenServer.call(parent, {@prefix, :child_mount, self(), assign_new})
GenServer.call(pid, {@prefix, :get_assigns, self(), keys})
catch
:exit, {:noproc, _} -> {:error, :noproc}
end
Expand Down Expand Up @@ -1336,8 +1393,10 @@ defmodule Phoenix.LiveView.Channel do
{:noreply, post_verified_mount(new_state)}

{:live_redirect, opts, new_state} ->
GenServer.reply(from, {:error, %{live_redirect: opts}})
{:stop, :shutdown, new_state}
maybe_handover(new_state, opts, nil, fn new_state ->
GenServer.reply(from, {:error, %{live_redirect: opts}})
{:stop, :shutdown, new_state}
end)

{:redirect, opts, new_state} ->
GenServer.reply(from, {:error, %{redirect: opts}})
Expand Down Expand Up @@ -1579,4 +1638,40 @@ defmodule Phoenix.LiveView.Channel do
%{}
end
end

defp handover? do
phoenix_vsn = to_string(Application.spec(:phoenix)[:vsn])
Version.match?(phoenix_vsn, ">= 1.8.0-dev")
end

defp maybe_handover(state, redirect_opts, ref, fallback) do
%{socket: %{parent_pid: parent, private: %{verified_session: session}} = socket} = state
%{to: to} = redirect_opts
# get the full uri to verify the new session
destructure [path, query], :binary.split(to, "?")
to = %{socket.host_uri | path: path, query: query}
params = (query && Plug.Conn.Query.decode(query)) || %{}

if diff = Diff.get_push_events_diff(socket), do: push_diff(state, diff, ref)

# we can only handover on Phoenix >= 1.8.0 and when we are mounted at the router
with true <- handover?(),
nil <- parent,
{:ok, new_verified, route, url} <-
authorize_session(
session,
socket.endpoint,
%{"redirect" => to}
) do
%{topic: topic, join_ref: join_ref} = state
state = push(state, "live_handover", redirect_opts)

msg_payload = {:handover, new_verified, route, url, params}
send(socket.transport_pid, {:handover, msg_payload, self(), topic, join_ref})

{:noreply, state}
else
_ -> fallback.(state)
end
end
end
4 changes: 2 additions & 2 deletions lib/phoenix_live_view/socket.ex
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ defmodule Phoenix.LiveView.Socket do
}

channel "lvu:*", Phoenix.LiveView.UploadChannel
channel "lv:*", Phoenix.LiveView.Channel
channel "lv:*", Phoenix.LiveView.Channel, handover_on_rejoin: true

@impl Phoenix.Socket
def connect(_params, %Phoenix.Socket{} = socket, connect_info) do
Expand All @@ -111,7 +111,7 @@ defmodule Phoenix.LiveView.Socket do
use Phoenix.Socket

channel "lvu:*", Phoenix.LiveView.UploadChannel
channel "lv:*", Phoenix.LiveView.Channel
channel "lv:*", Phoenix.LiveView.Channel, handover_on_rejoin: true

def connect(params, socket, info), do: {:ok, socket}
defdelegate id(socket), to: unquote(__MODULE__)
Expand Down
Loading
Loading