Menu
Public repository / f1f33c69567a554b6ce011822661d3289c1e9c77
AkurAI-Framework / site/content/docs/realtime.md
Latest change 5458532ad0ba451030d0554d03be8ff383ccddb8 - feat(ws,http): pure-std WebSocket (RFC 6455) + Server-Sent Events by Ólafur Búi Ólafsson
# Realtime: WebSocket & SSE
The framework ships two ways to push data to a connected client, both written
from scratch on `std` with zero runtime dependencies: **WebSocket** (RFC 6455,
bidirectional) and **Server-Sent Events** (the WHATWG `text/event-stream` spec,
server→client). Both follow the standards to the letter — a browser's native
`WebSocket` and `EventSource` talk to them directly, no client library needed.
## How it works under the hood
A normal handler returns a `Response`. A realtime handler instead returns a
`Reply::Upgrade`: the server writes the response head, then hands the raw socket
to a closure that owns it for the rest of the connection. That closure runs on a
**dedicated thread**, so a long-lived stream never ties up a request-pool worker.
TLS still terminates at the edge (nginx/Caddy); the server speaks plain HTTP.
## Server-Sent Events
A one-way stream of text events. Ideal for live counters, notifications, logs,
progress, and live-reload — anything the server pushes and the client only reads.
The browser reconnects automatically.
```rust
use akurai_http::{sse, Reply};
fn events() -> Reply {
sse(|mut sink| {
let mut n = 0;
loop {
n += 1;
sink.event("tick", &n.to_string())?; // event: tick\ndata: N\n\n
std::thread::sleep(std::time::Duration::from_secs(1));
}
})
}
```
The `SseSink` handles the wire format: `data`, named `event`, `id`, `retry`, and
`comment` (for keep-alive heartbeats). Multi-line data is split into multiple
`data:` lines per the spec, and every send is flushed so events arrive
immediately. When the client disconnects the next write fails and the closure
returns — the thread ends cleanly.
On the client, it is one line:
```js
const es = new EventSource("/api/events");
es.addEventListener("tick", (e) => console.log("tick", e.data));
```
The built-in demo endpoint is `GET /api/events`.
## WebSocket
Full-duplex messages over a single connection. Use it for chat, collaborative
editing, live dashboards, and anything where the client also sends.
```rust
use akurai_ws::{upgrade, Message};
// in a handler returning `akurai_http::Reply`:
upgrade(req, |mut conn| {
while let Some(msg) = conn.recv()? {
match msg {
Message::Text(t) => conn.send_text(&t)?, // echo
Message::Binary(b) => conn.send_binary(&b)?,
Message::Close(_) => break,
_ => {}
}
}
Ok(())
})
```
`upgrade` performs the RFC 6455 handshake — it validates the `Upgrade`,
`Connection`, `Sec-WebSocket-Key`, and `Sec-WebSocket-Version` headers and
answers with the SHA-1 + base64 `Sec-WebSocket-Accept` token, or returns `400`
if the request is not a valid WebSocket upgrade.
`WsConn` does the rest of the protocol for you:
- **Framing & masking** — client frames must be masked (and are rejected if not);
server frames are sent unmasked, per spec.
- **Fragmentation** — multi-frame messages are reassembled before you see a
`Text` or `Binary`.
- **Ping/pong** — pings are answered with pongs automatically.
- **Close handshake** — a peer close is echoed and surfaced as `Message::Close`.
- **Validation** — text is UTF-8 checked (close `1007` on violation); oversized
frames close `1009`; protocol errors close `1002`.
On the client:
```js
const ws = new WebSocket("ws://localhost:8090/api/ws");
ws.onmessage = (e) => console.log("echo:", e.data);
ws.onopen = () => ws.send("hello");
```
The built-in demo endpoint is `GET /api/ws` (an echo server).
## Choosing between them
- **SSE** — server→client only, text, auto-reconnect, trivial client. Reach for
it first; most "live" features are one-directional.
- **WebSocket** — both directions, text or binary, lower overhead per message.
Use it when the client must send too.
## Limits
Each realtime connection holds one OS thread for its lifetime — the right model
at the small/medium scale the framework targets, not for tens of thousands of
idle connections. WebSocket negotiates no extensions (no `permessage-deflate`),
and a single frame or reassembled message is capped at 16 MiB to bound memory.