F6.09 · part 2 of 3
Broadcast engine events
This is where the static dashboard from F6.09.1 comes alive, and it reuses F6.07 wholesale. The
domain already broadcasts a tagged event on a topic the moment a write succeeds —
{:course_created, course}, {:enrolled, enrollment} — so
the dashboard does not need any new emission code; it only needs to listen. It subscribes to the
"events" topic on its connected mount (the
connected?/1 guard again), and from then on every event arrives in
handle_info/2 as a process message. The key idea is the fold: each
handle_info/2 clause takes one event and applies it to the read model — bump the
matching count with update/3, and stream_insert/3 a row at the
top of the feed. This is the same shape as reducing a list, except the “list” arrives over time and
each step pushes a diff to the browser. The dashboard never recomputes from scratch and never re-queries the
database on each event; it maintains its derived numbers incrementally from the stream of facts. That is exactly
the event-sourcing intuition from F5, expressed as a live LiveView: the engine is the source of events, and the
dashboard is a projection that folds them as they come.
Four parts: subscribe versus handle versus the projection, the path an event folds along,
subscribing at mount, and the handle_info/2 clauses that do the folding.
Subscribe, handle, fold
Three ideas drive the live update. Subscribe joins the engine’s topic.
handle_info/2 receives each broadcast. The projection
is the result — counts and a feed folded from the events. Select one.
The live update · select one
piece: subscribe
is: join the engine’s topic at mount
The dashboard adds no broadcasting of its own. The domain was already emitting these events for F6.07; the dashboard is one more subscriber, which is why making a page live costs so little here.
An event folds in
Trace one enrollment. The engine records it; the context broadcasts {:enrolled, _}
on "events"; PubSub delivers it to the dashboard’s mailbox;
handle_info/2 bumps enrollments_count and inserts a feed row;
the diff goes to the browser. The number ticks up and a line appears, with no reload and no re-query.
event → handle_info → bump count + insert row → diff
Folding rather than recomputing is what lets the dashboard stay cheap under load. A thousand events are a thousand small updates, each touching one count and inserting one row — never a thousand full re-reads of the database.
Subscribing at mount
mount/3 from F6.09.1 gains one line: subscribe to "events"
behind connected?/1, then seed as before. The emission side is unchanged from F6.07
— the context broadcasts after each write — so the dashboard only consumes. The comment below shows
the events the domain already publishes.
# the dashboard joins the engine's event topic on the connected mount def mount(_params, _session, socket) do if connected?(socket), do: Portal.subscribe("events") {:ok, seed(socket)} # seed/1 sets counts + an empty feed (F6.09.1) end # the domain already emits these after a write (F6.07) — the dashboard only listens: # Portal.broadcast("events", {:course_created, course}) # Portal.broadcast("events", {:enrolled, enrollment})
Subscribing on the connected mount, not the first paint, means the disconnected HTTP render does no work it would throw away — the same discipline from F6.06.1, applied to a read-only screen.
Folding each event
One handle_info/2 clause per event kind. Each pipes the socket through
update/3 to bump the relevant count and stream_insert/3 to
prepend a feed row built by a small row/2 helper. The clauses are exhaustive for the
events you care about; anything else can fall through to a catch-all that ignores it.
# each event folds into the read model: bump a count, prepend to the feed def handle_info({:course_created, course}, socket) do {:noreply, socket |> update(:courses_count, fn n -> n + 1 end) |> stream_insert(:events, row("course created", course.title), at: 0)} end def handle_info({:enrolled, enrollment}, socket) do {:noreply, socket |> update(:enrollments_count, fn n -> n + 1 end) |> stream_insert(:events, row("enrolled", enrollment.id), at: 0)} end
subscribe once
On the connected mount; the domain already broadcasts the events.
fold each event
handle_info/2 bumps a count and inserts a row — incrementally.
Next: many clients, live — one fold per dashboard, every viewer in sync, with a Presence count and clustering for production.