The EventSource API
Most of the web is request-and-response: the browser asks, the server answers, done. Server-sent events change the shape — the browser opens one connection and the server pushes messages down it whenever it likes, for as long as it stays open. It’s the simple, one-directional cousin of WebSockets: a good fit for live prices, notifications, progress bars, a clock.
There are two halves — a route that streams, and a browser API that listens.
The server half (already written)
src/routes/api/events/+handler.js is a normal marko-run handler that
returns a streaming Response. Instead of a string or some JSON, its
body is a ReadableStream, and it pushes one message a second:
controller.enqueue( new TextEncoder().encode(`data: ${JSON.stringify(payload)}\n\n`),);That data: … \n\n is the entire SSE wire format: a data: line, then a
blank line to end the message. Two more details make it work — the
Content-Type: text/event-stream header, which tells the browser to hold
the connection open and treat what arrives as a stream rather than a
document; and cancel(), which the runtime calls when the client
disconnects, so the interval is cleared and nothing keeps running after
everyone’s gone. No library — just web-standard streaming.
The browser half (your job)
EventSource is the built-in client for this. You hand it a URL, and its
onmessage fires once for every data: line the server sends. Open
src/tags/live-feed.marko and fill in the <script>:
<script> if (live) { const source = new EventSource("/api/events"); source.onmessage = (event) => { const data = JSON.parse(event.data); messages = [...messages.slice(-5), data]; }; $signal.onabort = () => source.close(); }</script>Three things are doing real work here:
- A
<script>is an effect — the same one you met with the stopwatch. It runs when the component mounts and re-runs whenever a tag variable it reads changes. It readslive, which matters in a moment. - Assigning to
messagesis an ordinary reactive update, so every arriving event re-renders the list — the clock ticks on its own. $signal.onabortis the cleanup: when the effect re-runs or the component unmounts, the connection closes. That one line is why an open socket never leaks.
Watch the clock start ticking in the preview. Now click Stop: live
flips to false, the effect re-runs, $signal aborts — the old connection
closes and the clock freezes. Click Subscribe and the effect re-runs
again, opening a fresh connection. One <let> and one $signal line gave
you start-and-stop, with nothing left dangling in between.
That’s the whole loop: a handler streaming text/event-stream, and a
component that subscribes, renders each event reactively, and tears the
connection down the moment it’s done with it.
- Installing dependencies
- Starting dev server