Part #5 of our series on building web console for Elixir & Phoenix applications with Phoenix LiveView.

In this series:

  1. Part 1: Project set up and simple code executor
  2. Part 2: Making imports and macros work
  3. Part 3: Managing multiple supervised sessions
  4. Part 4: Making it real-time
  5. Part 5: Capturing the outpu

Video version

Watch the video version below or or directly on YouTube for a complete step-by-step walkthrough.

# 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.