Server-sent events for a live simulation

Simulation frames go out over server-sent events, which are one-way, plain HTTP and reconnecting for free, because nothing a watcher does needs a socket and the little that does is an ordinary POST.

Transport
SSE, one direction
Rooms
broadcast no faster than every 250 ms
Village
a compact frame 4× a second
On the wire
the last 48 ticker lines, not the whole log

Why not websockets

The traffic is almost entirely one-way. The server has news; the watcher has almost nothing to say. A watcher's whole input surface is picking a room, toggling detail, clicking a person, and voting in the village, all of which are fine as ordinary requests.

In exchange, SSE gives you reconnection semantics for free, no upgrade handshake, no sticky-session thinking, and a stream you can read with curl while debugging.

Frames are cheap on purpose

The log the engine keeps is capped at four thousand events so that a saved party is a complete transcript, but only the last forty-eight lines ever go over the wire. Broadcasts coalesce to no more than one every 250 ms even when several people act in the same instant.

The village sends a compact positional frame four times a second and the browser interpolates. The roster, which is every person as a sortable table, is a separate endpoint polled only while it is open, because most watchers never open it.

Make the clock survive a lumpy connection

lib/clock.ts extrapolates the party clock between frames from the last server value and wall time. Without it a slow connection reads as people teleporting; with it, it reads as people walking and a countdown that keeps counting.

That is the general trick for a streamed simulation: send the truth rarely, and let the client be confidently wrong about the microseconds.

Keep proxies off it

Every /api/* response carries X-Robots-Tag: noindex, and the stream must not be buffered or cached by anything in front of it. A CDN that helpfully buffers an event stream turns a live room into a two-minute-old room, which is worse than an error because it looks like it is working.

Read next

More on the build