toni
WebSocket

Rooms

Group WebSocket clients into named rooms and broadcast to specific groups.

Rooms let you group clients by a logical name and broadcast to a specific group. Common use cases: chat channels, game lobbies, collaboration sessions, topic subscriptions.

Joining and leaving rooms

Manage room membership from within the gateway using the connection manager:

use toni::{injectable, async_trait};
use toni::websocket::{GatewayTrait, WsClient, WsMessage, DisconnectReason, ConnectionManager};

#[injectable(pub struct RoomGateway {
    #[inject]
    connection_manager: ConnectionManager,
})]
impl RoomGateway {
    pub fn new(connection_manager: ConnectionManager) -> Self {
        Self { connection_manager }
    }
}

#[async_trait]
impl GatewayTrait for RoomGateway {
    fn path(&self) -> &str { "/rooms" }

    async fn on_connect(&self, client: &WsClient) {
        client.send(WsMessage::text("Connected. Send 'join:<room>' to join a room.")).await;
    }

    async fn on_message(&self, client: &WsClient, message: WsMessage) {
        let text = match message.as_text() {
            Some(t) => t,
            None => return,
        };

        if let Some(room) = text.strip_prefix("join:") {
            self.connection_manager.join_room(client.id(), room).await;
            client.send(WsMessage::text(format!("Joined room: {room}"))).await;

            // Notify others in the room
            self.connection_manager
                .broadcast_to_room(room, WsMessage::text(
                    format!("{} joined", client.id())
                ))
                .await;
        } else if let Some(room) = text.strip_prefix("leave:") {
            self.connection_manager.leave_room(client.id(), room).await;
            client.send(WsMessage::text(format!("Left room: {room}"))).await;
        } else if let Some(rest) = text.strip_prefix("msg:") {
            // rest format: "<room>:<message>"
            if let Some((room, msg)) = rest.split_once(':') {
                self.connection_manager
                    .broadcast_to_room(room, WsMessage::text(
                        format!("[{room}] {}: {msg}", client.id())
                    ))
                    .await;
            }
        }
    }

    async fn on_disconnect(&self, client: &WsClient, _reason: DisconnectReason) {
        // Automatically removed from all rooms on disconnect
        self.connection_manager.remove_client(client.id()).await;
    }
}

ConnectionManager API

manager.join_room(client_id, room_name)              // add client to room
manager.leave_room(client_id, room_name)             // remove from specific room
manager.remove_client(client_id)                     // remove from all rooms + disconnect
manager.broadcast_to_room(room, WsMessage)           // send to all in room
manager.get_room_members(room_name)                  // list client IDs in room
manager.get_client_rooms(client_id)                  // list rooms a client is in
manager.room_count()                                 // total number of active rooms

Broadcasting to rooms from services

#[injectable(pub struct ChatService {
    #[inject]
    connection_manager: ConnectionManager,
})]
impl ChatService {
    pub fn new(connection_manager: ConnectionManager) -> Self { Self { connection_manager } }

    pub async fn send_to_channel(&self, channel: &str, sender: &str, message: &str) {
        let formatted = format!("[{channel}] {sender}: {message}");
        self.connection_manager
            .broadcast_to_room(channel, WsMessage::text(formatted))
            .await;
    }
}

Module setup

use toni::websocket::BroadcastModule;

#[module(
    imports: [BroadcastModule],
    providers: [RoomGateway, ChatService],
)]
pub struct ChatModule;

On this page