We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
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.
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.