Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Live updates

zapline::live pushes changes to every open page as they happen: publish to a topic, and every browser subscribed to it gets the message. With htmx you write no JavaScript. It's always available (no feature flag).

#![allow(unused)]
fn main() {
use zapline::live::Live;
use zapline::prelude::*;

#[get("/live/chat")]
async fn listen(live: Live) -> Response {
    live.sse("chat").into_response()
}

#[post("/chat")]
async fn say(live: Live, views: Views, Form(msg): Form<Message>) -> Result<StatusCode> {
    let html = views.render("chat/message.html", context! { msg })?;
    live.publish("chat", "message", html.0);
    Ok(StatusCode::NO_CONTENT)
}

let app = App::new().live(Live::new()).mount((listen, say));
}
<script src="https://unpkg.com/htmx-ext-sse@2.2.2/sse.js" defer></script>

<div hx-ext="sse" sse-connect="/live/chat" sse-swap="message" hx-swap="beforeend"></div>

sse-swap names the event to swap in. Here every message is appended to the <div>.

Projects created with zapline new --template fullstack already work this way: open the todo page in two tabs, change a todo in one, and the other updates. The handlers publish the re-rendered list block after each change.

The API

Call
live.publish(topic, event, data)Send to everyone on topic. Returns how many got it.
live.publish_json(topic, event, &value)The same, with value as JSON.
live.sse(topic)An SSE response streaming the topic, with a keep-alive every 15 s.
live.subscribe(topic)A Subscription: sub.next().await, or use it as a Stream.
Live::new().capacity(n)Messages kept for a subscriber that falls behind (default 256).

Topics are just strings. Use room:42 or user:7 for one room or one user, and check access in the handler that calls live.sse (for example by taking a CurrentUser argument).

A topic exists while someone listens. A slow subscriber skips the messages it missed rather than slowing everyone else down. The dev console lists topics and their subscriber counts.

WebSockets

Forward a subscription to a socket:

#[get("/ws/chat")]
async fn ws(live: Live, upgrade: WebSocketUpgrade) -> Response {
    upgrade.on_upgrade(move |mut socket| async move {
        let mut sub = live.subscribe("chat");
        while let Some(update) = sub.next().await {
            if socket.send(Message::text(update.data)).await.is_err() {
                break;
            }
        }
    })
}

Several instances

Live::new() reaches the subscribers of one process. Behind a load balancer with several instances, use Live::new().redis(redis) (feature redis) so every update reaches visitors on every instance: see Running several instances.

Testing

Subscribe in the test and assert on what a handler published:

let client = app(Database::sqlite_memory()).test();
let live: Live = client.resources().await.require()?;
let mut updates = live.subscribe("todos");
client.post("/todos").form(&[("title", "x")]).send().await;
assert!(updates.next().await.unwrap().data.contains("x"));