Part #4 of our series on building web console for Elixir & Phoenix applications with Phoenix LiveView.
In this series:
- Part 1: Project set up and simple code executor
- Part 2: Making imports and macros work
- Part 3: Managing multiple supervised sessions
- Part 4: Making it real-time
Video version
Watch the video version below or or directly on YouTube for a complete step-by-step walkthrough.
Figure out the PubSub server
In order to make our web console real-time, we need to send messages between sessions and LiveViews. The usual way to make it happen in a Phoenix environment is to use Phoenix.PubSub
. Our first problem, however, appears to be figuring out what is the pub-sub server the library is going to use.
Since Backdoor
is a library, and is going to be mounted in external applications’ routers, it won’t start its own Endpoint
, nor start its own instance of Phoenix.PubSub
. We need to figure it out, and luckily we have a way to do so. Ideally I also want this as a configuration option, so that users of the library can configure it to something else than the given endpoint uses - for the sake of security, performance or other choice of developer.
We can find out the PubSub
instance given endpoint uses when the Backdoor.BackdoorLive
mounts:
# lib/backdoor/live/backdoor_live.ex
...
def mount(_params, _session, socket) do
Phoenix.PubSub.subscribe(find_pubsub_server(socket.endpoint), @topic_name)
...
end
...
defp find_pubsub_server(endpoint) do
case Application.get_env(:backdoor, :pubsub_server) do
nil ->
server = endpoint.config(:pubsub_server) || endpoint.__pubsub_server__()
Application.put_env(:backdoor, :pubsub_server, server)
server
server ->
server
end
end
The configuration option is then stored in the application environment, but it can be overwritten by the end user setting it in their config/config.ex
or similar.
The place where we emit the events is Backdoor.Session.CodeRunner
, and it also needs to find out the PubSub
instance, but it’s easier since we can reliably rely on application environment variable being present at this moment:
# lib/backdoor/session/log.ex
...
@topic_name "backdoor_events"
defp log(session_id, value) do
Phoenix.PubSub.broadcast(pubsub_server(), @topic_name, {:put_log, session_id, value})
Backdoor.Session.Log.put_log(via_tuple(Backdoor.Session.Log, session_id), value)
end
defp pubsub_server() do
Application.get_env(:backdoor, :pubsub_server)
end
This code makes our code runner emit events, whenever a new log entry is being added. We use similar technique in Backdoor.Session
itself, but won’t describe it as it is fairly repetitive, and just link for the reader to have a peek at if interested.
Making LiveView reacting to published events
We already subscribe to broadcasted messages in the mount/2
function of our LiveView
, but we need to handle the incoming messages. This is done by introducing appropriate handle_info/2
callbacks to catch published broadcasts.
For example, to handle log messages we need to alter our execute
command handler, and instead of handling return value in this function - we add a handle_info/2
callback to catch this event asynchronously:
@impl true
def handle_info({:put_log, session_id, log}, %{assigns: %{current_session_id: sid}} = socket)
when session_id == sid do
{:noreply,
socket
|> assign(logs: socket.assigns.logs ++ [log])}
end
def handle_info({:put_log, _session_id, _log}, socket) do
{:noreply, socket}
end
@impl true
def handle_event("execute", %{"command" => %{"text" => command}}, socket) do
Backdoor.Session.execute(socket.assigns.current_session_id, command)
{:noreply, socket |> assign(:input_value, "")}
end
There are a few more events that need to be handled in the LiveView
, but they largely follow the same pattern: whenever an event happens that all LiveViews
need to be reacting to, we publish such events on the PubSub
topic and make the LiveViews
capture it.
Notes
Watch the screencast directly on YouTube
You can find this and future videos on our YouTube channel.
The full code from this episode can be found on GitHub
Post by Hubert Łępicki
Hubert is partner at AmberBit. Rails, Elixir and functional programming are his areas of expertise.