Skip to main content

Module realtime

Module realtime 

Source
Expand description

Realtime updates: WebSocket channels on a Durable Object, HTML broadcasts for htmx (feature realtime).

Works like Rails’ Action Cable and Turbo Streams: browsers subscribe to a named channel over a WebSocket, and handlers or jobs broadcast HTML fragments (or JSON) to every subscriber.

use axum::{Router, extract::{Path, State}, response::Response, routing::{get, post}};
use ocre::{Ctx, Error, Result, realtime::{self, WebSocketUpgrade}};

// src/realtime.rs: who may listen to which channel (`ocre g scaffold ... --realtime` writes it).
async fn connect(State(ctx): State<Ctx>, Path(channel): Path<String>, upgrade: WebSocketUpgrade) -> Result<Response> {
    match channel.as_str() {
        "posts" => {}
        _ => return Err(Error::NotFound),
    }
    upgrade.connect(&ctx, &channel).await
}

// Any handler or job: the new row goes to the top of every open index page.
async fn create(State(ctx): State<Ctx>) -> Result<&'static str> {
    let row_html = "<tr id=\"post_1\"><td>Hello</td></tr>";
    realtime::broadcast(&ctx, "posts", &realtime::prepend("posts", row_html)).await?;
    Ok("created")
}

fn routes() -> Router<Ctx> {
    Router::new().route("/realtime/{channel}", get(connect)).route("/posts", post(create))
}

In the page, htmx’s WebSocket extension connects and swaps each message into the element with the same id (hx-swap-oob), without custom JavaScript. Load the extension in the layout’s <head>, after htmx: a page that loaded it itself would not connect when reached through an hx-boost link.

<!-- templates/layout.html, in <head> -->
<script src="https://unpkg.com/htmx-ext-ws@2.0.4/dist/ws.js" crossorigin="anonymous"></script>
<!-- the page -->
<div hx-ext="ws" ws-connect="/realtime/posts">
  <table><tbody id="posts">...<tr id="post_1">...</tr></tbody></table>
</div>

Messages: an element with an id replaces the page element with that id; append, prepend, update and remove build the other swaps. One message may hold several of them.

How it runs: one Durable Object of class OcreChannel (binding CHANNELS) per channel name holds the channel’s WebSockets with the WebSocket Hibernation API, so it is evicted from memory, and costs no duration, between broadcasts while browsers stay connected. It stores nothing.

Free plan (see the README’s Realtime section): each connection (and reconnection) and each broadcast is one Durable Object request (100,000 a day); messages sent to browsers are free; a connection is also one Worker request, while a broadcast is a subrequest of the request that sends it. Needs Ocre’s realtime feature and, in cloudflare.config.ts, the binding and the SQLite-backed OcreChannel export (see OcreChannel). API-only apps can use the same pieces and broadcast JSON.

Rails’ Action Cable pieces map as follows: the connect handler is the connection (identified_by is WebSocketUpgrade::identified_by, its checks and Result are the callbacks and rescue_from), the channel name and the route’s query string are the channel params, client actions (perform) are ordinary routes that broadcast, and WebSocketUpgrade::rebroadcast relays what clients send to the other subscribers. dev_routes lists recent broadcasts for tests.

Structs§

OcreChannel
The Durable Object class behind each realtime channel, exported by Ocre under the name OcreChannel.
WebSocketUpgrade
Axum extractor for a WebSocket handshake (Upgrade: websocket), finished with connect.

Constants§

CHANNELS_BINDING
Name of the Durable Object binding holding the channels, declared in cloudflare.config.ts.
CHANNEL_CLASS
Name of the Durable Object class Ocre exports for channels (OcreChannel).
LOG_PREFIX
Prefix of every line Ocre logs about realtime, e.g. in ocre dev output.
MAX_CHANNEL_LEN
Longest channel name, in bytes.
MAX_IDENTITY_LEN
Longest identity given to WebSocketUpgrade::identified_by, in bytes.
MAX_REBROADCAST_BYTES
Longest client message WebSocketUpgrade::rebroadcast relays, in bytes; longer ones are dropped.

Functions§

append
Builds a message that inserts html at the end of the element with id target (htmx beforeend).
broadcast
Sends message (an HTML fragment or JSON text) to every browser connected to channel.
dev_routes
Development endpoint listing recent broadcasts, served by ocre dev only, for tests (Rails’ assert_broadcasts).
prepend
Builds a message that inserts html at the start of the element with id target (htmx afterbegin).
remove
Builds a message that removes the element with id id from the page (htmx delete).
update
Builds a message that replaces the contents of the element with id target (htmx innerHTML).