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

Tutorial 17: In-process realtime and presence

Rullst Core provides bounded in-process broadcast channels and presence state. These primitives do not create an HTTP WebSocket endpoint by themselves and do not synchronize independent server processes.

Subscribe before publishing

#![allow(unused)]
fn main() {
use std::sync::Arc;

use rullst::realtime::{BroadcastManager, RealtimeError, RealtimeMessage};

async fn local_exchange() -> Result<RealtimeMessage, RealtimeError> {
    let manager = Arc::new(BroadcastManager::new());
    let mut receiver = manager.get_or_create("room:general").subscribe();

    manager.publish(
        "room:general",
        "message.created",
        r#"{"sender":"alice","content":"hello"}"#,
    )?;

    receiver
        .recv()
        .await
        .map_err(|error| RealtimeError::BroadcastError(error.to_string()))
}
}

Channel uses tokio::sync::broadcast: a slow receiver can lag, and publishing without any receiver returns RealtimeError::BroadcastError.

Bind channels and presence to an authenticated tenant

Construct TenantContext only from trusted authentication/membership state. The wrappers validate logical names and ensure that identical room names use different backend namespaces:

#![allow(unused)]
fn main() {
use std::sync::Arc;

use rullst::realtime::{
    BroadcastManager, PresenceTracker, TenantPresence, TenantRealtime,
};
use rullst::security::TenantContext;

fn publish_for_school() -> Result<(), Box<dyn std::error::Error>> {
    let context = TenantContext::try_new("school-alpha")?;
    let realtime = TenantRealtime::from_context(
        Arc::new(BroadcastManager::new()),
        &context,
    );
    let presence = TenantPresence::from_context(
        Arc::new(PresenceTracker::new()),
        &context,
    );

    let mut receiver = realtime.subscribe("course/42")?;
    presence.user_joined("course/42", "learner-7")?;
    realtime.publish("course/42", "lesson.completed", r#"{"lesson_id":9}"#)?;

    assert_eq!(presence.count_online("course/42")?, 1);
    assert!(receiver.try_recv().is_ok());
    Ok(())
}
}

The tenant wrapper limits names and payloads (64 KiB), but the application still owns authentication, room-level authorization, connection lifecycle and replay. Use rullst::live::live_ws_handler for the separate per-connection LiveComponent flow, or build an Axum WebSocket handler around these primitives. A distributed transport is still roadmap work.