RatMinimal web framework

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