Thanks, everyone. I ended up with a solution that is a mix of your suggestions.
Summary:
- I’m adding a guest token to the browser session/cookie (there is a session cookie even when the user is not logged in) when the first tab is opened. When the user logs in, the guest token is cleared from the session.
- In the
on_mount
of every LiveView we check if such token exists, if it does, we subscribe to reload##{guest_token}
.
- When a LiveView receives that message we send a force redirect to the socket.
I’m using the same solution to fix the issue I mentioned here where other tabs would see the “We can’t find the internet. Attempting to reconnect” message when logging out.
Although forcing a page reload is an okay solution, for now, I would like to have more control over sessions in LiveView so I could refresh the page state without a page reload. I think that’s not a solved issue at the moment. I understand why we need a cookie-based session for the Plug requests (and initiating the WebSockets connections) but I’m thinking of also having a localStorage-based session (with all the security you can have) to allow the live sessions to be more dynamic.
Implementation Details:
In the fetch_current_user
method that is run in the :browser
pipeline I add a “guest session token” to the cookie.
defmodule AppWeb.UserAuth do
...
def fetch_current_user(conn, _opts) do
{user_token, conn} = ensure_user_token(conn)
user = user_token && Accounts.get_user_by_session_token(user_token)
assign(conn, :current_user, user)
conn
|> assign(:current_user, user)
|> maybe_add_guest_session_token() <--- Added this
end
defp maybe_add_guest_session_token(conn) do
if conn.assigns.current_user || get_session(conn, :guest_session_token) do
conn
else
put_session(conn, :guest_session_token, Ecto.UUID.generate())
end
end
end
The default live session runs the on_mount of a new module:
live_session :default, on_mount: AppWeb.RealTimeSessions do
The RealTimeSessions module looks like this:
defmodule SwayzeWeb.RealTimeSessions do
@moduledoc false
import Phoenix.LiveView
def on_mount(:default, _params, session, socket) do
if connected?(socket) do
session |> maybe_subscribe_to_guest_session() |> maybe_subscribe_to_logged_in_session()
socket =
socket
|> attach_hook(
:save_request_path,
:handle_params,
&save_request_path/3
)
|> attach_hook(:handle_reload, :handle_info, &reload_page/2)
{:cont, socket}
else
{:cont, socket}
end
end
defp reload_page(_msg, socket) do
if socket.assigns[:trigger_submit] do
{:cont, socket}
else
{:cont, socket |> redirect(to: socket.assigns.current_uri)}
end
end
defp save_request_path(_params, uri, socket) do
{:cont, Phoenix.Component.assign(socket, :current_uri, URI.parse(uri) |> Map.get(:path))}
end
defp maybe_subscribe_to_guest_session(session) do
guest_session_token = Map.get(session, "guest_session_token")
if guest_session_token do
Phoenix.PubSub.subscribe(Swayze.PubSub, "reload##{guest_session_token}")
end
session
end
defp maybe_subscribe_to_logged_in_session(session) do
logged_in_session_token = Map.get(session, "logged_in_session_token")
if logged_in_session_token do
Phoenix.PubSub.subscribe(Swayze.PubSub, "reload##{logged_in_session_token}")
end
session
end
end
The force reload caused some issues with the login LiveView specs where the liveview used in the spec would be killed because it was receiving the reload message and forcing a redirect on the socket. This doesn’t really happen in production or even development because we have a full page reload on login (which is when we broadcast that message), but it happened in the specs. For now, I’m checking if socket.assigns[:trigger_submit]
is assigned and aborting the force reload. I’m pretty sure that’s a bad idea but I didn’t find a better solution just yet.