I asked this on community Slack, but I think it could be useful to get more feedback/ideas here.
Objective: I am wanting to push notifications from server → client so I can display a desktop notifications. I am using LiveView, and I’d like to avoid adding a separate Channel or a second LiveView (firm on this).
I’m looking for ideas on how to handle this. It would be ideal if this was just natively built in, but I can’t find it if it does exist. Maybe this is on the roadmap or something, which is one reason I’m publicly asking and sharing my ideas.
I am not trying to solve notifications during server disconnection. That’s a whole can of worms that frankly I’m okay with not worrying about atm.
First Pass: My initial idea is to leverage a mount hook (I’m not using on_mount
, but something similar I wrote before that was added) and do something like this:
defp register_notification_hooks(socket = %{assigns: %{user: user}}) do
if connected?(socket) do
Phoenix.PubSub.subscribe(Clove.PubSub, Clove.Notifications.user_notification_topic(user))
attach_hook(socket, :desktop_notification_listener, :handle_info, &__MODULE__.notification_callback/2)
else
socket
end
end
def notification_callback({:notifications_new, _notification}, socket) do
# push_event here
{:cont, socket}
end
def notification_callback(_msg, socket) do
{:cont, socket}
end
This would result in an event being pushed to the client. It’s pretty seamless and would work across the entire app due to how my mount code is implemented. It also uses public functions supported by LiveView. But I see a pretty big issue:
Each time that I redirect in the application, the old Channel is torn down and a new one is made. This will run the mount code again and everything ends up correct, but events would be missed in the interim because the process that was subscribed (LiveView.Channel) does not exist for a small period of time. (The transport process exists during the entire transition.)
Second Idea: My next idea is to leverage the Phoenix.Socket transport and directly push events to the client, even if the Channel doesn’t exist. I have prototyped pushing messages, but haven’t wired it all up. The general idea is something like:
defp register_notification_hooks(socket = %{assigns: %{user: user}}) do
if connected?(socket) do
NotificationWatchdog.start_server(transport_pid(socket))
else
socket
end
end
defmodule NotificationWatchdog do
# This would be a GenServer that uses Registry to guarantee a unique link per transport_pid
# Upon init, PubSub.subscribe would be used to register this process to the PubSub
# The transport_pid would be linked to make sure the processes always exist in unison
#
# When the event comes in, the notification would be directly pushed to the transport
# using something like the function below
defp push_event(transport_pid, notification) do
message = %Phoenix.Socket.Message{join_ref: nil, topic: nil, event: "notification", payload: notification}
send(transport_pid, Phoenix.Socket.V2.JSONSerializer.encode!(message))
end
end
I can leverage liveSocket.getSocket().onMessage
in JavaScript to handle the message.
This should work, and would not disconnect during redirection. It uses internal messages supported by Phoenix.Socket
, and the serializer is not publicly exposed from LiveView (so it’s hardcoded). I don’t really like those aspects.
Is there something simpler I’m missing?