Live Tea — Making a CQRS/DDD/Liveview/Elm-architecture chat app

  • Show how phoenix liveview (LV) and CQRS can work together to make fast (really fast), and robust applications.
  • Inspire people to see the possibilities of “server-side-rendering”[¹] with websockets

What I have made

How it works

  1. A browser goes to livechat.stadler.no/some_id and gets html and JS
  2. A WS is set up between client and the backend (handled by LV)
  3. Liveview spawns an actor with the viewed html as its state (handled by LV)
  4. The actor subscribes to to “chat:some_id”
  5. A SendMessage Command is sent via WS, dispatched to an aggregate.
  6. MessageSent event is written to EventStore
  7. An actor subscribing go ES gets the MessageSent event and applies it to the chat state
  8. The updated chat is broadcasted
  9. The html is updated and the diff is sent over WS
  10. On the client morphdom applies this html diff and the user sees the new message. (handled by LV)

DDD (Domain driven design)

CQRS

defmodule MessageSent do 
@derive Jason.Encoder
defstruct [:sender, :chat_id, :content]
end
defmodule SendMessage do 
defstruct [:sender, :chat_id, :content]
end
defmodule Chat do 
defstruct [chat_id: nil, messages: []]
def execute(%Chat{}, %SendMessage{...}) do
%MessageSent{sender: sender, chat_id: chat_id, content: content} end
def apply(%Chat{} = chat, %MessageSent{} = event) do
%Chat{chat | messages: [event | chat.messages] |> Enum.reverse()}
end
end
  • It has RAM-copies of state so lookups are fast
  • It’s built into Elixir/Erlang, and runs on the BEAM
defmodule ChatReadModel do  defmodule Model do
use Memento.Table, attributes: [:chat_id, :sender,:content],
type: :bag
end
use Commanded.Event.Handler,
application: LiveTea.App,
name: __MODULE__
def handle(%MessageSent{} = event, _metadata) do
Memento.transaction! fn ->
Memento.Query.write(%Model{..})
end
Phoenix.PubSub.broadcast(LiveTea.PubSub,
"chat:"<> event.chat_id,
get(event.chat_id))
end
def get(chat_id) do
Memento.transaction! fn ->
Memento.Query.select(Model, {:==, :chat_id, chat_id})
end
end
end

Phoenix liveview & Elixir

  • Erlang, a language made by Ericsson in the 90s to handle telecommunications switches.
  • BEAM, the runtime Erlang runs on.
  • Elixir, a language created in 2011, by Jose Valim, that runs on the BEAM. The syntax is very similar to Ruby making it attractive to a greater (as in larger) community.
  • Phoenix, the most popular web framework for Elixir
  • Phoenix Liveview, an addition to Phoenix released in v.1.5 that allows for server-initiated re-render of web pages.
def start(_type, _args) do
# Start the Telemetry supervisor
LiveTeaWeb.Telemetry,
# Start the PubSub system
{Phoenix.PubSub, name: LiveTea.PubSub},
# Start the Endpoint (http/https)
LiveTeaWeb.Endpoint,
LiveTea.App,
ChatReadModel
end
...
scope "/", LiveTeaWeb do
pipe_through :browser

# Pokemon route (gotta catch em all)
live "/*path", PageLive
end
...
@impl true
## Mounts the liveview and assigns the initial socket state
def mount(_params, session, socket) do
case session["name"] do
nil -> {:ok, socket}
x -> {:ok, assign(socket, name: x)}
end
end# This function is called on the initial render,
# and everytime the url is updated.
# Also when it's just mutating the window. parameter.
# This is my router.
def handle_params(%{"path" => path}, _uri, socket) do
case path do
# Set page to home page
[] -> {:noreply, assign(socket, page: :home)}

# Subscribe to the chat, read the current chat state, and set the page to
# the chat_id
[chat_id] -> Phoenix.PubSub.subscribe(LiveTea.PubSub, "chat:"<>chat_id)
messages = ChatReadModel.get(chat_id)
{:noreply, assign(socket, page: :chat, chat_id: chat_id, messages: messages)}
end

# Render html
def render(assigns) do
~L"""
<%= case @page do %>
<%=:chat -> %> <%= chat_page(assigns)%>
<%=_-> %> <%= home_page(assigns)%>
<% end %>

"""
end
#From the form submit
def handle_event("send_message", %{"message" => msg}, socket) do
:ok = LiveTea.App.dispatch(%SendMessage{chat_id: socket.assigns[:chat_id], sender: socket.assigns[:name] , content: msg})
{:noreply, socket}
end
#From the pubsub subscribed to in handle params
def handle_info(messages, socket) do
new_sock =assign(socket, messages: messages)
{:noreply, push_event(new_sock, "new_message", %{})}
end
def chat_page(assigns) do
~L"""
...
<form phx-submit="send_message">
<textarea id="textarea" placeholder="message; Shift-Enter for a newline"></textarea>
<button type="submit" phx-disable-with="Sending...">
Send
</button>
</form>
"""
end

--

--

--

A norwegian SW-dev with a MSc in mathematics/robotics. I’m working at Dignio , a remote care startup. My greatest achievement is taking 23 consecutive pull ups.

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Creating and maintaining patterns in PatternLab

Photo of the facade of the Palacio de Cristal, of four glass big windows, showing the inside of the building.

Recovering Files From a Flash Drive That Requires Formatting

Controlling video playback using face detection

What’s the Big Deal? Specialization

AD Homelab Upgraded

Let’s add QR feature for our Django application!

Analytical insights on heterogenous data sources in AWS Ecosystem

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Aksel Stadler Kjetså

Aksel Stadler Kjetså

A norwegian SW-dev with a MSc in mathematics/robotics. I’m working at Dignio , a remote care startup. My greatest achievement is taking 23 consecutive pull ups.

More from Medium

OPEN API SPEC Elixir/Phoenix

Open API Spec for a home page request

Debug your website in production

A Quick Guide on Refactoring

Model View Controller Frameworks Explained