Connect an external websocket to your LiveView

Someone on #elixir asked about how to do this and I my quick googling didn’t find any blog posts about it, so here’s a tiny example.

a deranged ai generated image of a potato, has nothing to do with anything

The code is available on github

Turns out it’s pretty simple to get a proof of concept working. I used the websockex library as that’s what I found first, but Riverside could be a good alternative.

Setup a websocket server

First, we need an external websocket to connect to. Lets create a mix project that spews out data to connected clients using Plug and Bandit.

Feel free to skip this part if you’ve already got a websocket to connect to.

$ mix new spew --sup

Add our dependencies to mix.exs and run mix deps.get

# mix.exs
 defp deps do
    [
      {:bandit, "~> 0.6.0"},
      {:websock_adapter, "~> 0.5"}
    ]
  end

Then we mostly just followed the Plug readme, and setup our little test server.

# lib/spew/application.ex
defmodule Spew.Application do
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      {Bandit, scheme: :http, plug: Spew.Router, options: [port: 4001]}
    ]
    opts = [strategy: :one_for_one, name: Spew.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Alright, lets add a websocket route in lib/spew/router.ex so we can actually connect to the socket remotely.

# lib/spew/router.ex
defmodule Spew.Router do
  use Plug.Router

  plug(Plug.Logger)
  plug(:match)
  plug(:dispatch)

  get "/websocket" do
    conn
    |> WebSockAdapter.upgrade(Spew.WebsocketServer, [], timeout: 60_000)
    |> halt()
  end

  match _ do
    send_resp(conn, 404, "not found")
  end
end

Use the mighty power of OTP to spew out some semi-random data every second to connected clients. Or do something sensible, it’s up to you…

# lib/spew/websocket_server.ex
defmodule Spew.WebsocketServer do
  def init(options) do
    schedule_emit()
    {:ok, options}
  end

  defp schedule_emit() do
    Process.send_after(self(), :emit_data, 1000)
  end

  def handle_info(:emit_data, state) do
    data = %{
      timestamp: DateTime.utc_now() |> DateTime.to_string(),
      value: :rand.uniform(100)
    }

    schedule_emit()
    {:reply, :ok, {:text, :json.encode(data)}, state}
  end

  def terminate(_reason, state) do
    {:ok, state}
  end
end

The LiveView part

Now, lets add our websockex dependency to mix.exs in our LiveView project.

  defp deps do
    [
      ...
      {:websockex, "~> 0.4.3"}
    ]
  end

Then we need setup websockex mostly as described in the readme, but with a few tweaks.

First we add our websocket client to our applications supervision tree.

Now, doing it like this does result in the application not being able to start unless there actually is an external websocket to connect to when it boots. We should probably use a dynamic supervisor instead. But for now, lets just do it this way.

# lib/my_app/application.ex
defmodule MyApp.Application do
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      ...
      {MyApp.WebSocketClient, []}
    ]
end

Then we setup the actual WebSocketClient module.

Notice the handle_frame where we throw any received messages that matches {:text, msg} to PubSub on the “ws_updates” topic.

defmodule MyApp.WebSocketClient do
  use WebSockex

  def start_link(_) do
    WebSockex.start_link("ws://localhost:4001/websocket", __MODULE__, %{})
  end

  def handle_frame({:text, msg}, state) do
    Phoenix.PubSub.broadcast(MyApp.PubSub, "ws_updates", {:new_message, msg})
    {:ok, state}
  end

  def handle_disconnect(%{reason: {:local, _reason}}, state) do
    {:ok, state}
  end

  def handle_disconnect(disconnect_map, state) do
    super(disconnect_map, state)
  end
end

Then we just have to throw together a LiveView that subscribes to the PubSub topic and renders the messages received from the websocket.

When a pubsub message is received handle_info is called with the new message. Then we just add it to the list of messages and render it.

defmodule MyAppWeb.TestLiveView do
  use MyAppWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    if connected?(socket) do
      Phoenix.PubSub.subscribe(MyApp.PubSub, "ws_updates")
    end

    {:ok, assign(socket, messages: [])}
  end

  @impl true
  def handle_info({:new_message, msg}, socket) do
    {:noreply, update(socket, :messages, fn messages -> [msg | messages] end)}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <div>
      <h1>Live WebSocket Data</h1>
      <ul>
        <%= for message <- @messages do %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
    """
  end
end

Then we add the new view our router and presto, we’re ready to go live.

# lib/my_app_web/router.ex
  scope "/", MyAppWeb do
    pipe_through :browser
    ...
    live "/testview", TestLiveView
  end

Running the app

First we need to start up the spew application.

$ cd spew
$ mix deps.get
$ mix run --no-halt

And then, after we start our LiveView project we’ll be able to see the data coming in.

How amazing is that? Pretty amazing alright.

livview data streaming in