Channels
A channel is a named publish/subscribe topic. The server publishes with channel.broadcast(name, payload). Browsers subscribe via the SSE endpoint at /__rat__/channels?name=<name>. There is one broker per server, and topics are created lazily on first publish or subscribe.
Broadcast from a handler
Push to every subscriber
channel.broadcast is a global, available anywhere Rat code runs server-side. The payload serializes to JSON, so pass plain objects, arrays, strings, or numbers.
> page
name: ''
msg: ''
<input value[name] on_input[name << event.target.value]>
<input value[msg] on_input[msg << event.target.value]>
<button on_click[channel.broadcast('chat', {from: name, text: msg})]>
send Subscribe from the browser
Native EventSource
Browsers consume the channel as a standard SSE stream. Open an EventSource against the topic URL and decode each message as JSON. No client library is needed; the browser already speaks SSE.
# in a lang/js/chat.js (inverse tail)
export function listen(onMessage) { const es = new EventSource('/__rat__/channels?name=chat'); es.onmessage = (ev) => onMessage(JSON.parse(ev.data)); }
# then on a page:
> page
<script on_load[chat.listen(handle_message)]> Channel from a Tail worker
Python can broadcast too
Anything that runs in-process with the server can broadcast. A Python Tail worker finishes a long-running job and hands its result back to Rat; the Rat handler relays it to subscribers. One topic, many producers.
# main.rat
> finish_job[job_id]
[result] << ml.run(job_id)
channel.broadcast('job_done', {id: job_id, result: result}) Delivery guarantees
A heartbeat comment every 30 seconds keeps streams alive, and the server re-arms its write deadline on every frame, so a healthy subscriber stays connected indefinitely. When a connection drops, the browser's EventSource reconnects on its own (the server hints a 2-second retry) and resends the id of the last frame it received.
Every broadcast is stamped with a per-channel sequence id and recorded in a small in-memory replay ring that holds the last 64 messages per channel. On reconnect the server replays everything after the subscriber's last-seen id, then resumes live delivery. So a broadcast fired during the retry window arrives late instead of being lost. You write no code for this; it is how the endpoint behaves.
The ring is a reconnect cushion, not a history store. It lives in memory, so a server restart clears it. Ids carry a per-process epoch token, so a stale id from before a restart starts a fresh connection rather than replaying from a counter that no longer means anything. If subscribers need a real backlog on join, keep it in a database and replay it from a channel.on_connect hook with sub.send(payload).
See also Services · Tail (Python) · Location transparency