WHAT IF? UltronEx and Liveview
Scroll DownIf you had the chance to watch Disney+ WHAT IF? then you would know that in multiverse somewhere there is a version of Ultron that is too powerful for anyone to counter. In elixir-verse its the same with UltronEx.
In one elixir-verse UltronEx runs on plug/cowboy with Javascript glue on the client side
In another elixir-verse it runs on Phoenix + Pubsub + Liveview with zero Javascript to handle connection or updating UI.
and the Liveview UltronEx is far superior in many aspects. For Ultronex, Phoenix gives it a convention based structure over using plug/cowboy to maintain an opinionated strucutre which over time became difficult to be shared and worked on. With Liveview there is no immediate need of Javascript as all actions are managed with help of phoenix/liveview helpers, it helped eliminate all the Javascript that was there for sockets and UI update as all HTML for UI update was pushed directly from server. Liveview + Pubsub also provided an event driven model for processing messages managing pressure in case of a burst.
With Liveview the amount of data sent to client also reduced with the Filter now being part of Liveview and can match events before they are sent. In comparison the plug/cowboy + Javascript version it would only hide them from display if they were filtered out. You could achieve something similar in the JS version but that would require more code and coordination which is exactly what Liveview simplifies with just few line of code and lets you concentrate on the functionality rather than solving the complexity.
Also Liveview works well with Javascript added. The clipboard still runs on localstorage in the same page and uses Vanilla JS and it doesn't conflict or breaks anything.
Previously to have the UI work and manage socket connection this was the code on the client and server.
P.S Do not judge my JS \_(-_-)_/
client side code
(() => {
class myWebsocketHandler {
setupSocket() {
this.socket = new WebSocket(`wss://${window.location.host}/ultronex/ws/slack`)
this.socket.addEventListener("message", (event) => {
if (this.filterMatchContent(event.data) === true){
this.updateContent(event)
}
else{
console.log("Message received not matching filter")
}
})
this.socket.addEventListener("close", () => {
this.setupSocket()
})
}
filterMatchContent(event_data){
var filter = document.getElementById("filter-content").value
if(filter !==""){
return event_data.match(new RegExp(filter)) !== null
}
else{
return true
}
}
contentType(data) {
if (data.match(/id="success-/) !== null) {
return "success-content"
} else if (data.match(/id="danger-/) !== null) {
return "danger-content"
} else if (data.match(/id="warning-/) !== null) {
return "warning-content"
} else if (data.match(/id="pattern-/) !== null) {
return "pattern-content"
} else if (data.match(/id="rate-limit-/) !== null) {
return "rate-limit-content"
} else {
return "stats-content"
}
}
updateContent(event) {
var prependList = ["success-content", "danger-content", "warning-content"]
var event_id = this.contentType(event.data)
if (prependList.includes(event_id)) {
this.prependContent(event_id, event)
} else {
document.getElementById(event_id).innerHTML = event.data
}
}
prependContent(event_id, event) {
if ($("#stream-content-toggle").is(':checked')){
if ($(`#${event_id}-toggle`).is(':checked')) {
const divTag = document.createElement("div")
divTag.innerHTML = event.data
document.getElementById(event_id).prepend(divTag)
} else {
//Do nothing just log
console.log(`Event received for ${event_id}, user stream is turned off`)
}
} else {
//Do nothing just log
console.log(`UltronEx stream is turned off`)
}
}
}
const websocketClass = new myWebsocketHandler()
websocketClass.setupSocket()
})()
server side code
defmodule Ultronex.Server.Websocket.SocketHandler do
@moduledoc """
Documentation for Ultronex.Server.Websocket.SocketHandler
"""
@behaviour :cowboy_websocket
def init(request, _state) do
state = %{registry_key: request.path}
{:cowboy_websocket, request, state}
end
def websocket_init(state) do
Registry.UltronexApp
|> Registry.register(state.registry_key, {})
{:ok, state}
end
def websocket_handle({:text, json}, state) do
payload = Jason.decode!(json)
message = payload["data"]["message"]
websocket_send_msg(message, state)
{:reply, {:text, message}, state}
end
def websocket_info(info, state) do
{:reply, {:text, info}, state}
end
def websocket_send_msg(message, state) do
Registry.UltronexApp
|> Registry.dispatch(state.registry_key, fn entries ->
for {pid, _} <- entries do
if pid != self() do
Process.send(pid, message, [])
end
end
end)
end
end
and now everything is in a single Liveview on server
defmodule UltronexWeb.Stream.LiveView do
use UltronexWeb, :live_view
@queue "stream"
@cache :ultronex_cachex
def mount(_params, _session, socket) do
if connected?(socket), do: UltronexWeb.Endpoint.subscribe(@queue)
{:ok, base_stream(socket), temporary_assigns: [msg: []]}
end
def handle_info(%{event: "success", payload: payload}, socket) do
{:noreply,
assign(socket, term: get_filter_term(socket.id), msg: filter_msg(socket.id, payload))}
end
def handle_info(%{event: "danger", payload: payload}, socket) do
{:noreply,
assign(socket, term: get_filter_term(socket.id), msg: filter_msg(socket.id, payload))}
end
def handle_info(%{event: "warning", payload: payload}, socket) do
{:noreply,
assign(socket, term: get_filter_term(socket.id), msg: filter_msg(socket.id, payload))}
end
def handle_event(
"filter",
%{"filter" => %{"term" => incoming_message_filter}},
socket
) do
if String.trim(incoming_message_filter) == "",
do: Cachex.del(@cache, socket.id),
else: Cachex.put(@cache, socket.id, {:ok, incoming_message_filter})
{:noreply, assign(socket, term: incoming_message_filter, msg: :empty)}
end
defp base_stream(socket) do
assign(socket, msg: :empty, term: "")
end
defp filter_msg(socket_id, msg) do
case Cachex.get(@cache, socket_id) do
{:ok, {:ok, filter}} ->
if String.match?(msg.payload, ~r/#{filter}/), do: msg, else: :empty
{:ok, nil} ->
msg
end
end
defp get_filter_term(socket_id) do
case Cachex.get(@cache, socket_id) do
{:ok, {:ok, filter}} ->
filter
{:ok, nil} ->
""
end
end
end
With this no more context switching, no more googling over how to work with JS in this case coming from an elixir service. No more worrying managing of connections/delivery. It really did make things go faster from a development perspective as everything was written in Elixir and no back and forth between client and server code. Also HEEx is very powerful to write your markup.
All in all it was a good developer experience working with Liveview and it surely stands out as a strong contender for building apps/services that require realtime capabilities.
View Comments