### Install Dependencies Source: https://github.com/totodore/socketioxide/blob/main/examples/react-rooms-chat/client/README.md Run this command to install all necessary project dependencies. ```shell $ npm install ``` -------------------------------- ### Setting up Socket.IO Namespace and Handler Source: https://github.com/totodore/socketioxide/blob/main/crates/socketioxide/docs/operators/broadcast.md This example demonstrates how to initialize a Socket.IO service and define a namespace with a handler function that utilizes broadcast operators. Ensure you have a Socket.IO server setup to use this. ```rust # use socketioxide::SocketIo; # use socketioxide::extract::*; # use serde_json::Value; async fn handler(io: SocketIo, socket: SocketRef, Data(data): Data::) { // This message will be broadcast to all sockets in this namespace except this one. socket.broadcast().emit("test", &data).await; // This message will be broadcast to all sockets in this namespace, including this one. io.emit("test", &data).await; } let (_, io) = SocketIo::new_svc(); io.ns("/", async |s: SocketRef| s.on("test", handler)); ``` -------------------------------- ### Example: Fetching and Logging Socket IDs Source: https://github.com/totodore/socketioxide/blob/main/crates/socketioxide/docs/operators/fetch_sockets.md This example shows how to fetch all sockets within a specific room and then iterate through them to print their IDs. It requires the `socketioxide` crate and its `extract` module. ```rust # use socketioxide::{SocketIo, extract::*}; # async fn doc_main() { let (_, io) = SocketIo::new_svc(); let sockets = io.within("room1").fetch_sockets().await.unwrap(); for socket in sockets { println!("Socket ID: {:?}", socket.data().id); } # } ``` -------------------------------- ### Run Development Server Source: https://github.com/totodore/socketioxide/blob/main/examples/react-rooms-chat/client/README.md Execute this command to start the client's development server. The client will be accessible at http://localhost:5173. ```shell $ npm run dev ``` -------------------------------- ### Commit Message Examples Source: https://github.com/totodore/socketioxide/blob/main/CONTRIBUTING.md Examples of valid commit messages for documentation and core fixes. ```text docs(changelog): update change log to beta.5 ``` ```text fix(core): need to depend on latest rxjs and zone.js ``` -------------------------------- ### Run Test Suites Source: https://github.com/totodore/socketioxide/blob/main/CONTRIBUTING.md Execute the corresponding test suites for engine.io-protocol or socket.io-protocol after starting the e2e server. ```shell cd engine.io-protocol/test-suite && npm test ``` ```shell cd socket.io-protocol/test-suite && npm test ``` -------------------------------- ### Multi-Node Chat Application Setup with Redis Adapter Source: https://github.com/totodore/socketioxide/blob/main/crates/socketioxide-redis/README.md Sets up a Socket.IO server with the Redis adapter for multi-node support in an Axum application. Includes Redis connection, adapter configuration, and Axum router setup with CORS. ```rust use serde::{Deserialize, Serialize}; use socketioxide::{ adapter::Adapter, extract::{Data, Extension, SocketRef, State}, SocketIo, }; use socketioxide_redis::{drivers::redis::redis_client as redis, RedisAdapter, RedisAdapterCtr}; use tower::ServiceBuilder; use tower_http::{cors::CorsLayer, services::ServeDir}; use tracing::info; use tracing_subscriber::FmtSubscriber; #[derive(Deserialize, Serialize, Debug, Clone)] #[serde(transparent)] struct Username(String); #[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "camelCase", untagged)] enum Res { Login { #[serde(rename = "numUsers")] num_users: usize, }, UserEvent { #[serde(rename = "numUsers")] num_users: usize, username: Username, }, Message { username: Username, message: String, }, Username { username: Username, }, } #[derive(Clone)] struct RemoteUserCnt(redis::aio::MultiplexedConnection); impl RemoteUserCnt { fn new(conn: redis::aio::MultiplexedConnection) -> Self { Self(conn) } async fn add_user(&self) -> Result { let mut conn = self.0.clone(); let num_users: usize = redis::cmd("INCR") .arg("num_users") .query_async(&mut conn) .await?; Ok(num_users) } async fn remove_user(&self) -> Result { let mut conn = self.0.clone(); let num_users: usize = redis::cmd("DECR") .arg("num_users") .query_async(&mut conn) .await?; Ok(num_users) } } #[tokio::main] async fn main() -> Result<(), Box> { let subscriber = FmtSubscriber::new(); tracing::subscriber::set_global_default(subscriber)?; info!("Starting server"); let client = redis::Client::open("redis://127.0.0.1:6379?protocol=resp3")?; let adapter = RedisAdapterCtr::new_with_redis(&client).await?; let conn = client.get_multiplexed_tokio_connection().await?; let (layer, io) = SocketIo::builder() .with_state(RemoteUserCnt::new(conn)) .with_adapter::>(adapter) .build_layer(); io.ns("/", on_connect).await?; let app = axum::Router::new() .fallback_service(ServeDir::new("dist")) .layer( ServiceBuilder::new() .layer(CorsLayer::permissive()) // Enable CORS policy .layer(layer), ); let port = std::env::var("PORT") .map(|s| s.parse().unwrap()) .unwrap_or(3000); let listener = tokio::net::TcpListener::bind(("0.0.0.0", port)) .await .unwrap(); axum::serve(listener, app).await.unwrap(); Ok(()) } async fn on_connect(socket: SocketRef) { socket.on("new message", on_msg); socket.on("add user", on_add_user); socket.on("typing", on_typing); socket.on("stop typing", on_stop_typing); socket.on_disconnect(on_disconnect); } async fn on_msg( s: SocketRef, Data(msg): Data, Extension(username): Extension, ) { let msg = &Res::Message { username, message: msg, }; s.broadcast().emit("new message", msg).await.ok(); } async fn on_add_user( s: SocketRef, Data(username): Data, user_cnt: State, ) { if s.extensions.get::().is_some() { return; } let num_users = user_cnt.add_user().await.unwrap_or(0); s.extensions.insert(Username(username.clone())); s.emit("login", &Res::Login { num_users }).ok(); let res = &Res::UserEvent { num_users, username: Username(username), }; s.broadcast().emit("user joined", res).await.ok(); } async fn on_typing(s: SocketRef, Extension(username): Extension) { s.broadcast() .emit("typing", &Res::Username { username }) .await .ok(); } async fn on_stop_typing(s: SocketRef, Extension(username): Extension) { s.broadcast() .emit("stop typing", &Res::Username { username }) .await .ok(); } async fn on_disconnect( s: SocketRef, user_cnt: State, Extension(username): Extension, ) { let num_users = user_cnt.remove_user().await.unwrap_or(0); let res = &Res::UserEvent { num_users, username, }; s.broadcast().emit("user left", res).await.ok(); } ``` -------------------------------- ### Install Test Suite Dependencies Source: https://github.com/totodore/socketioxide/blob/main/CONTRIBUTING.md Install Node.js dependencies for the test suites of engine.io-protocol and socket.io-protocol. ```shell cd engine.io-protocol/test-suite npm install ``` ```shell cd ../../socket.io-protocol/test-suite npm install ``` -------------------------------- ### Rust Echo Server with Axum Example Source: https://github.com/totodore/socketioxide/blob/main/README.md This snippet demonstrates how to set up an echo server using Socket.IO and the Axum web framework in Rust. It handles basic connections, message echoing, and acknowledgments. ```rust use axum::routing::get; use serde_json::Value; use socketioxide::{ extract::{ AckSender, Bin, Data, SocketRef }, SocketIo, }; use tracing::info; use tracing_subscriber::FmtSubscriber; fn on_connect(socket: SocketRef, Data(data): Data) { info!("Socket.IO connected: {:?} {:?}", socket.ns(), socket.id); socket.emit("auth", data).ok(); socket.on( "message", |socket: SocketRef, Data::(data), Bin(bin)| { info!("Received event: {:?} {:?}", data, bin); socket.bin(bin).emit("message-back", data).ok(); }, ); socket.on( "message-with-ack", |Data::(data), ack: AckSender, Bin(bin)| { info!("Received event: {:?} {:?}", data, bin); ack.bin(bin).send(data).ok(); }, ); } #[tokio::main] async fn main() -> Result<(), Box> { tracing::subscriber::set_global_default(FmtSubscriber::default())?; let (layer, io) = SocketIo::new_layer(); io.ns("/", on_connect); io.ns("/custom", on_connect); let app = axum::Router::new() .route("/", get(|| async { "Hello, World!" })) .layer(layer); info!("Starting server"); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, app).await.unwrap(); Ok(()) } ``` -------------------------------- ### Basic Axum Server with Engine.IO Handler Source: https://github.com/totodore/socketioxide/blob/main/crates/engineioxide/README.md This example demonstrates setting up a basic Axum server with Engine.IO integration. It includes a custom handler for managing socket connections, disconnections, and messages, along with global and socket-specific state management. ```rust use bytes::Bytes; use engineioxide::layer::EngineIoLayer; use engineioxide::handler::EngineIoHandler; use engineioxide::{Socket, DisconnectReason, Str}; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; use axum::routing::get; // Global state, with axum it must be clonable #[derive(Debug, Default, Clone)] struct MyHandler { user_cnt: Arc, } // Socket state #[derive(Debug, Default)] struct SocketState { id: Mutex, } impl EngineIoHandler for MyHandler { type Data = SocketState; fn on_connect(self: Arc, socket: Arc>) { let cnt = self.user_cnt.fetch_add(1, Ordering::Relaxed) + 1; socket.emit(cnt.to_string()).ok(); } fn on_disconnect(&self, socket: Arc>, reason: DisconnectReason ) { let cnt = self.user_cnt.fetch_sub(1, Ordering::Relaxed) - 1; socket.emit(cnt.to_string()).ok(); } fn on_message(self: &Arc, msg: Str, socket: Arc>) { *socket.data.id.lock().unwrap() = msg.into(); // bind a provided user id to a socket } fn on_binary(self: &Arc, data: Bytes, socket: Arc>) { } } // Create a new engineio layer let layer = EngineIoLayer::new(Arc::new(MyHandler::default())); let app = axum::Router::<()>::new() .route("/", get(|| async { "Hello, World!" })) .layer(layer); // Spawn the axum server ``` -------------------------------- ### Run E2E Test Server Source: https://github.com/totodore/socketioxide/blob/main/CONTRIBUTING.md Start the end-to-end test servers for engineioxide or socketioxide. Ensure to enable the necessary features (v3, v4). ```shell cargo run --bin engineioxide-e2e --features v3,v4 ``` ```shell cargo run --bin socketioxide-e2e --features v3,v4 ``` -------------------------------- ### Rustfmt Code Formatting Source: https://github.com/totodore/socketioxide/blob/main/CONTRIBUTING.md Ensure code adheres to the rustfmt style guide by running `cargo fmt`. Pull requests with unformatted code will fail CI. ```shell cargo fmt ``` -------------------------------- ### Rust Chat Application Example Source: https://github.com/totodore/socketioxide/blob/main/README.md This snippet shows how to implement a chat application using Socket.IO in Rust. It handles new messages, user joining/leaving, and typing indicators. ```rust io.ns("/", |s: SocketRef| { s.on("new message", |s: SocketRef, Data::(msg)| { let username = s.extensions.get::().unwrap().clone(); let msg = Res::Message { username, message: msg, }; s.broadcast().emit("new message", msg).ok(); }); s.on( "add user", |s: SocketRef, Data::(username), user_cnt: State| { if s.extensions.get::().is_some() { return; } let num_users = user_cnt.add_user(); s.extensions.insert(Username(username.clone())); s.emit("login", Res::Login { num_users }).ok(); let res = Res::UserEvent { num_users, username: Username(username), }; s.broadcast().emit("user joined", res).ok(); }, ); s.on("typing", |s: SocketRef| { let username = s.extensions.get::().unwrap().clone(); s.broadcast() .emit("typing", Res::Username { username }) .ok(); }); s.on("stop typing", |s: SocketRef| { let username = s.extensions.get::().unwrap().clone(); s.broadcast() .emit("stop typing", Res::Username { username }) .ok(); }); s.on_disconnect(|s: SocketRef, user_cnt: State| { if let Some(username) = s.extensions.get::() { let num_users = user_cnt.remove_user(); let res = Res::UserEvent { num_users, username: username.clone(), }; s.broadcast().emit("user left", res).ok(); } }); }); ``` -------------------------------- ### Implement Background Task with `SocketIo` Handle Source: https://context7.com/totodore/socketioxide/llms.txt The `SocketIo` handle is `Clone + Send + Sync`, allowing it to be moved into any async task. This example demonstrates a background task that periodically emits metrics about connection counts. ```rust use socketioxide::{extract::SocketRef, SocketIo}; async fn metrics_broadcaster(io: SocketIo) { let mut interval = tokio::time::interval(std::time::Duration::from_secs(5)); loop { interval.tick().await; let socket_count = io.of("/").map(|ns| ns.sockets().len()).unwrap_or(0); io.emit("metrics", &serde_json::json!({ "connections": socket_count })) .await .ok(); } } #[tokio::main] async fn main() -> Result<(), Box> { let (layer, io) = SocketIo::new_layer(); io.ns("/", async |s: SocketRef| { println!("New client: {}", s.id); }); tokio::spawn(metrics_broadcaster(io.clone())); let app = axum::Router::new().layer(layer); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?; axum::serve(listener, app).await?; Ok(()) } ``` -------------------------------- ### Inspect Connected Sockets with BroadcastOperators Source: https://context7.com/totodore/socketioxide/llms.txt Use `sockets()` to get local `SocketRef` handles within a namespace. Use `fetch_sockets()` with a remote adapter to get `RemoteSocket` handles covering all cluster nodes. ```rust use socketioxide::extract::SocketRef; use socketioxide::SocketIo; let (_, io) = SocketIo::new_svc(); io.ns("/", async |s: SocketRef| { s.on("who_is_in_lobby", async |s: SocketRef| { // Local sockets in room "lobby" let sockets = s.within("lobby").sockets(); for peer in &sockets { peer.emit("hello", &s.id.to_string()).ok(); } s.emit("count", &(sockets.len() as u32)).ok(); }); }); // Fetch all sockets (local + remote) across the cluster async fn audit(io: SocketIo) { let all = io.broadcast().fetch_sockets().await.unwrap(); println!("Total sockets in cluster: {}", all.len()); } ``` -------------------------------- ### Implement `FromConnectParts` for Custom Query Parameter Extraction Source: https://context7.com/totodore/socketioxide/llms.txt Implement `FromConnectParts` to create custom extractors for connection data. This example extracts a 'token' query parameter from the connection request. ```rust use std::sync::Arc; use std::convert::Infallible; use socketioxide::{ adapter::Adapter, handler::{FromConnectParts, FromMessageParts}, socket::Socket, SocketIo, extract::SocketRef, }; use socketioxide_core::Value; struct QueryParam(Option); impl FromConnectParts for QueryParam { type Error = Infallible; fn from_connect_parts(s: &Arc>, _: &Option) -> Result { let value = s.req_parts().uri.query() .and_then(|q| q.split('&').find(|p| p.starts_with("token=")).map(|p| p[6..].to_string())); Ok(QueryParam(value)) } } let (_, io) = SocketIo::new_svc(); io.ns("/", async |s: SocketRef, QueryParam(token): QueryParam| { match token { Some(t) => println!("Token from query: {}", t), None => { s.disconnect().ok(); } } }); ``` -------------------------------- ### Typing Indicator Event Handling Source: https://github.com/totodore/socketioxide/blob/main/crates/socketioxide-mongodb/README.md Handles the 'typing' event to broadcast when a user starts typing. Requires `username`. ```rust async fn on_typing(s: SocketRef, Extension(username): Extension) { s.broadcast() .emit("typing", &Res::Username { username }) .await .ok(); } ``` -------------------------------- ### Get All Rooms in a Namespace Source: https://github.com/totodore/socketioxide/blob/main/crates/socketioxide/docs/operators/rooms.md Retrieves all rooms within a specific namespace. This operation is asynchronous and may involve network communication if remote adapters are used. ```rust use socketioxide::{SocketIo, extract::SocketRef}; async fn handler(socket: SocketRef, io: SocketIo) { println!("Socket connected to the / namespace with id: {}", socket.id); let rooms = io.rooms().await.unwrap(); println!("All rooms in the / namespace: {:?}", rooms); } let (_, io) = SocketIo::new_svc(); io.ns("/", handler); ``` -------------------------------- ### Emit to Multiple Rooms Including Current Socket Source: https://github.com/totodore/socketioxide/blob/main/crates/socketioxide/docs/operators/within.md Use the `within` operator to specify one or more rooms to broadcast to. This example demonstrates broadcasting to 'room1', 'room2', 'room3', and 'room4', including the socket that triggered the event. ```rust # use socketioxide::{SocketIo, extract::*}; # use serde_json::Value; async fn handler(socket: SocketRef, Data(data): Data::) { let other_rooms = "room4".to_string(); // Emit a message to all sockets in room1, room2, room3, and room4, including the current socket socket .within("room1") .within(["room2", "room3"]) .within(vec![other_rooms]) .emit("test", &data) .await; } let (_, io) = SocketIo::new_svc(); io.ns("/", async |s: SocketRef| s.on("test", handler)); ``` -------------------------------- ### Register Dynamic Namespaces with Path Parameters Source: https://context7.com/totodore/socketioxide/llms.txt Use `dyn_ns` to create namespaces that match path patterns with `{param}` or `{*wildcard}` placeholders. This allows for dynamic namespace creation based on incoming connection paths. ```rust use socketioxide::{extract::SocketRef, SocketIo}; let (_, io) = SocketIo::new_svc(); // Matches /room/lobby, /room/arena, etc. io.dyn_ns("/room/{room_id}", async |s: SocketRef| { println!("Joined namespace: {}", s.ns()); }).unwrap(); // Catch-all wildcard io.dyn_ns("/org/{*path}", async |s: SocketRef| { println!("Dynamic path matched: {}", s.ns()); }).unwrap(); ``` -------------------------------- ### Implement Connection Middlewares for Authentication and Logging Source: https://context7.com/totodore/socketioxide/llms.txt Use the `with` method on a connect handler to chain middlewares that run before connection. Middlewares can perform authentication or logging and must return `Result<(), E>` where `E: Display`. Returning `Err` will refuse the connection. ```rust use socketioxide::{handler::ConnectHandler, extract::{Data, SocketRef}, SocketIo}; #[derive(Debug)] struct AuthError; impl std::fmt::Display for AuthError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "unauthorized") } } impl std::error::Error for AuthError {} async fn auth_middleware(Data(token): Data) -> Result<(), AuthError> { if token == "secret" { Ok(()) } else { Err(AuthError) } } async fn log_middleware(s: SocketRef) -> Result<(), AuthError> { println!("New connection attempt from sid={}", s.id); Ok(()) } async fn on_connect(s: SocketRef) { println!("Authenticated socket {}", s.id); } let (_, io) = SocketIo::new_svc(); // Middlewares are evaluated right-to-left (log_middleware first, then auth_middleware) io.ns("/secure", on_connect.with(auth_middleware).with(log_middleware)); ``` -------------------------------- ### Incorrect Approach: Fetch Sockets Then Act Source: https://github.com/totodore/socketioxide/blob/main/crates/socketioxide/docs/operators/fetch_sockets.md This approach demonstrates fetching all sockets within a room first, then iterating over them to perform actions like emitting or leaving the room. It is less efficient than the direct approach. ```rust # use socketioxide::{SocketIo, extract::*}; # async fn doc_main() { # let (_, io) = SocketIo::new_svc(); let sockets = io.within("room1").fetch_sockets().await.unwrap(); for socket in sockets { socket.emit("test", &"Hello").await.unwrap(); socket.leave("room1").await.unwrap(); } # } ``` -------------------------------- ### Get Local Sockets within Rooms Source: https://github.com/totodore/socketioxide/blob/main/crates/socketioxide/docs/operators/sockets.md Retrieve local sockets that are within specified rooms, optionally excluding others. Useful for accessing socket extensions or managing room memberships. ```rust # use socketioxide::{SocketIo, extract::*}; async fn handler(socket: SocketRef) { // Find extension data in each socket in the room1 and room3 rooms, except for room2 let sockets = socket.within("room1").within("room3").except("room2").sockets(); for socket in sockets { println!("Socket extension: {:?}", socket.extensions.get::()); } } let (_, io) = SocketIo::new_svc(); io.ns("/", async |s: SocketRef| s.on("test", handler)); ``` -------------------------------- ### Configure Socket.IO with Builder Pattern Source: https://context7.com/totodore/socketioxide/llms.txt Use `SocketIo::builder` to customize configuration options like paths, timeouts, buffer sizes, and transports before building a layer or service. ```rust use std::time::Duration; use socketioxide::{SocketIo, ParserConfig, TransportType}; let (layer, io) = SocketIo::builder() .req_path("/socket.io") // default Socket.IO path .ping_interval(Duration::from_secs(25)) .ping_timeout(Duration::from_secs(20)) .ack_timeout(Duration::from_secs(10)) .connect_timeout(Duration::from_secs(45)) .max_buffer_size(256) // packets buffered per connection .max_payload(5_000_000) // 5 MB max HTTP payload .transports([TransportType::Websocket]) // WebSocket only .with_state(MyAppState::new()) // global shared state (requires "state" feature) .build_layer(); ``` -------------------------------- ### Create Socket.IO Layer with Default Configuration Source: https://context7.com/totodore/socketioxide/llms.txt Use `SocketIo::new_layer` to create a Tower layer and a Socket.IO handle. The layer can be mounted into any Tower-compatible router, and the handle is used for subsequent operations. ```rust use axum::routing::get; use socketioxide::{extract::SocketRef, SocketIo}; #[tokio::main] async fn main() -> Result<(), Box> { let (layer, io) = SocketIo::new_layer(); io.ns("/", async |s: SocketRef| { println!("Client connected: {}", s.id); }); let app = axum::Router::new() .route("/", get(async || "OK")) .layer(layer); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?; axum::serve(listener, app).await?; Ok(()) } ``` -------------------------------- ### Emit binary data to a single client using `bytes` crate Source: https://github.com/totodore/socketioxide/blob/main/crates/socketioxide/docs/operators/emit.md Shows how to emit binary data to a single client using the `bytes` crate. It illustrates sending `Bytes` directly and handling arrays as arguments, similar to non-binary data. ```rust use socketioxide::{SocketIo, extract::*}; use serde_json::Value; use std::sync::Arc; use bytes::Bytes; async fn handler(socket: SocketRef, Data(data): Data::<(String, Bytes, Bytes)>) { // Emit a test message to the client socket.emit("test", &data).ok(); // Emit a test message with multiple arguments to the client socket.emit("test", &("world", "hello", Bytes::from_static(&[1, 2, 3, 4]))).ok(); // Emit a test message with an array as the first argument let arr = [1, 2, 3, 4]; socket.emit("test", &[arr]).ok(); } let (_, io) = SocketIo::new_svc(); io.ns("/", async |socket: SocketRef| socket.on("test", handler)); ``` -------------------------------- ### Axum Server with Socket.IO MongoDB Adapter Source: https://github.com/totodore/socketioxide/blob/main/crates/socketioxide-mongodb/README.md Sets up an Axum server with the Socket.IO MongoDB adapter. Configures MongoDB connection, message expiration strategy, and integrates the adapter with Socket.IO and Axum's middleware. ```rust use std::time::Duration; use serde::{Deserialize, Serialize}; use socketioxide::{ adapter::Adapter, extract::{Data, Extension, SocketRef, State}, SocketIo, }; use socketioxide_mongodb::{ drivers::mongodb::mongodb_client::{ self as mongodb, bson::{doc, oid::ObjectId, Document}, options::ReturnDocument, Collection, Database, }, MessageExpirationStrategy, MongoDbAdapter, MongoDbAdapterConfig, MongoDbAdapterCtr, }; use tower::ServiceBuilder; use tower_http::{cors::CorsLayer, services::ServeDir}; use tracing::info; use tracing_subscriber::FmtSubscriber; #[derive(Deserialize, Serialize, Debug)] struct ChatDocument { _id: ObjectId, remote_user_count: usize, } impl ChatDocument { const COLLECTION_NAME: &'static str = "chat"; const ID: ObjectId = make_object_id( [0x66, 0x2A, 0x15, 0x49], [0xE9, 0x2C, 0x3C, 0x8D], [0x5E, 0x8B], ); } #[derive(Deserialize, Serialize, Debug, Clone)] #[serde(transparent)] struct Username(String); #[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "camelCase", untagged)] enum Res { Login { #[serde(rename = "numUsers")] num_users: usize, }, UserEvent { #[serde(rename = "numUsers")] num_users: usize, username: Username, }, Message { username: Username, message: String, }, Username { username: Username, }, } #[derive(Clone)] struct RemoteUserCnt { collec: Collection, } impl RemoteUserCnt { fn new(conn: Database) -> Self { Self { collec: conn.collection(ChatDocument::COLLECTION_NAME), } } async fn add_user(&self) -> Result { let where_doc: Document = doc! { "_id": ChatDocument::ID, }; let doc = self .collec .find_one_and_update(where_doc, doc! { "$inc": { "remote_user_count": 1 } }) .return_document(ReturnDocument::After) .await?; Ok(doc.map_or(0, |doc| doc.remote_user_count)) } async fn remove_user(&self) -> Result { let where_doc: Document = doc! { "_id": ChatDocument::ID, }; let doc = self .collec .find_one_and_update(where_doc, doc! { "$dec": { "remote_user_count": 1 } }) .return_document(ReturnDocument::After) .await?; Ok(doc.map_or(0, |doc| doc.remote_user_count)) } } #[tokio::main] async fn main() -> Result<(), Box> { let subscriber = FmtSubscriber::new(); tracing::subscriber::set_global_default(subscriber)?; info!("Starting server"); const URI: &str = "mongodb://127.0.0.1:27017/?replicaSet=rs0&directConnection=true"; const DB: &str = "chat"; let db = mongodb::Client::with_uri_str(URI).await?.database(DB); // This is the default expiration strategy for messages in the database. let strategy = MessageExpirationStrategy::TtlIndex(Duration::from_secs(60)); let config = MongoDbAdapterConfig::new() .with_expiration_strategy(strategy) .with_collection("socket.io-adapter-ttl"); let adapter = MongoDbAdapterCtr::new_with_mongodb_config(db.clone(), config).await?; let (layer, io) = SocketIo::builder() .with_state(RemoteUserCnt::new(db)) .with_adapter::>(adapter) .build_layer(); io.ns("/", on_connect).await?; let app = axum::Router::new() .fallback_service(ServeDir::new("dist")) .layer( ServiceBuilder::new() .layer(CorsLayer::permissive()) // Enable CORS policy .layer(layer), ); let port = std::env::var("PORT") .map(|s| s.parse().unwrap()) .unwrap_or(3000); let listener = tokio::net::TcpListener::bind(("0.0.0.0", port)) .await .unwrap(); axum::serve(listener, app).await.unwrap(); Ok(()) } async fn on_connect(socket: SocketRef) { socket.on("new message", on_msg); socket.on("add user", on_add_user); socket.on("typing", on_typing); socket.on("stop typing", on_stop_typing); socket.on_disconnect(on_disconnect); } async fn on_msg( s: SocketRef, Data(msg): Data, Extension(username): Extension, ) { let msg = &Res::Message { username, message: msg, }; s.broadcast().emit("new message", msg).await.ok(); } async fn on_add_user( s: SocketRef, Data(username): Data, user_cnt: State, ) { if s.extensions.get::().is_some() { return; } let num_users = user_cnt.add_user().await.unwrap_or(0); s.extensions.insert(Username(username.clone())); s.emit("login", &Res::Login { num_users }).ok(); ``` -------------------------------- ### Clone Protocol Repositories Source: https://github.com/totodore/socketioxide/blob/main/CONTRIBUTING.md Clone the engine.io-protocol and/or socket.io-protocol repositories if you need to modify protocol handling. ```shell git clone https://github.com/socketio/engine.io-protocol ``` ```shell git clone https://github.com/socketio/socket.io-protocol ``` -------------------------------- ### Clone Socket.IO Repository Source: https://github.com/totodore/socketioxide/blob/main/CONTRIBUTING.md Clone the main socketioxide repository to begin development. ```shell git clone https://github.com/totodore/socketioxide ``` -------------------------------- ### Manage Socket.IO Lifecycle with `close` and `delete_ns` Source: https://context7.com/totodore/socketioxide/llms.txt Gracefully close all connections using `io.close().await` or remove a specific namespace and disconnect its clients with `io.delete_ns("/temp")`. ```rust use socketioxide::{extract::SocketRef, SocketIo}; async fn shutdown(io: SocketIo) { // Gracefully close all connections (triggers on_disconnect handlers) io.close().await; } async fn remove_namespace(io: SocketIo) { // Disconnect all sockets from /temp and remove the namespace io.delete_ns("/temp"); } // Get a BroadcastOperators for a specific namespace async fn target_namespace(io: SocketIo) { if let Some(ns) = io.of("/admin") { ns.emit("maintenance", &"going down").await.ok(); let count = ns.sockets().len(); println!("{} admin sockets notified", count); } } ``` -------------------------------- ### Using `to()` operator Source: https://github.com/totodore/socketioxide/blob/main/crates/socketioxide/docs/operators/to.md Demonstrates how to use the `to()` operator to emit messages to sockets in specific rooms, both from a socket context and the global `io` context. ```APIDOC ## Method: to() ### Description Selects all the sockets in the given rooms except for the current socket when called from a socket context. When called from the `io` (global context) level, it includes all sockets in the specified rooms. ### Usage **From a socket context:** ```rust socket.to("room1").to(["room2", "room3"]).emit("test", &data).await; ``` This emits a message to all sockets in `room1`, `room2`, and `room3`, excluding the current socket. **From the global `io` context:** ```rust io.to("room1").to(["room2", "room3"]).emit("test", &data).await; ``` This emits a message to all sockets in `room1`, `room2`, and `room3`, including the current socket if it's in any of those rooms. ### Parameters - **rooms** (String or array of Strings): The room name(s) to target. ### Example ```rust # use socketioxide::{SocketIo, extract::*}; # use serde_json::Value; async fn handler(socket: SocketRef, io: SocketIo, Data(data): Data::) { // Emit a message to all sockets in room1, room2, room3, and room4, except the current socket socket .to("room1") .to(["room2", "room3"]) .emit("test", &data) .await; // Emit a message to all sockets in room1, room2, room3, and room4, including the current socket io .to("room1") .to(["room2", "room3"]) .emit("test", &data) .await; } let (_, io) = SocketIo::new_svc(); io.ns("/", async |s: SocketRef| s.on("test", handler)); ``` ``` -------------------------------- ### Join Sockets to Rooms Source: https://github.com/totodore/socketioxide/blob/main/crates/socketioxide/docs/operators/join.md Adds sockets that are currently in 'room1' and 'room3' to 'room4' and 'room5'. The `await` is necessary due to potential remote communication. ```rust use socketioxide::SocketIo; use socketioxide::extract::*; async fn handler(socket: SocketRef) { // Add all sockets that are in room1 and room3 to room4 and room5 socket.within("room1").within("room3").join(["room4", "room5"]).await.unwrap(); // We should retrieve all the local sockets that are in room4 and room5 let sockets = socket.within("room4").within("room5").sockets(); } let (_, io) = SocketIo::new_svc(); io.ns("/", async |s: SocketRef| s.on("test", handler)); ``` -------------------------------- ### Create Standalone Socket.IO Service Source: https://context7.com/totodore/socketioxide/llms.txt Use `SocketIo::new_svc` to create a standalone Hyper service. This service responds to Socket.IO requests and returns 404 for all other non-Socket.IO requests. ```rust use socketioxide::{extract::SocketRef, SocketIo}; #[tokio::main] async fn main() -> Result<(), Box> { let (svc, io) = SocketIo::new_svc(); io.ns("/", async |s: SocketRef| { println!("Connected: {}", s.id); }); // Use `svc` directly as a hyper service let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?; axum::serve(listener, svc).await?; Ok(()) } ``` -------------------------------- ### Registering Dynamic Namespaces Source: https://context7.com/totodore/socketioxide/llms.txt Allows for the creation of child namespaces based on path patterns with placeholders like `{param}` or `{*wildcard}`. ```APIDOC ## `SocketIo::dyn_ns` — Register a dynamic namespace Accepts path patterns with `{param}` or `{*wildcard}` placeholders, creating a child namespace for every matching path. ```rust use socketioxide::{extract::SocketRef, SocketIo}; let (_, io) = SocketIo::new_svc(); // Matches /room/lobby, /room/arena, etc. io.dyn_ns("/room/{room_id}", async |s: SocketRef| { println!("Joined namespace: {}", s.ns()); }).unwrap(); // Catch-all wildcard io.dyn_ns("/org/{*path}", async |s: SocketRef| { println!("Dynamic path matched: {}", s.ns()); }).unwrap(); ``` ``` -------------------------------- ### SocketIo::builder / SocketIoBuilder Source: https://context7.com/totodore/socketioxide/llms.txt Configures Socket.IO with custom parameters using a builder pattern before creating a layer or service. Allows setting paths, timeouts, buffer sizes, and more. ```APIDOC ## SocketIo::builder / SocketIoBuilder — Custom configuration The builder exposes all tunable parameters before constructing either a layer or service. ### Example ```rust use std::time::Duration; use socketioxide::{SocketIo, ParserConfig, TransportType}; let (layer, io) = SocketIo::builder() .req_path("/socket.io") // default Socket.IO path .ping_interval(Duration::from_secs(25)) .ping_timeout(Duration::from_secs(20)) .ack_timeout(Duration::from_secs(10)) .connect_timeout(Duration::from_secs(45)) .max_buffer_size(256) // packets buffered per connection .max_payload(5_000_000) // 5 MB max HTTP payload .transports([TransportType::Websocket]) // WebSocket only .with_state(MyAppState::new()) // global shared state (requires "state" feature) .build_layer(); ``` ``` -------------------------------- ### Room Management with Socket.IO Source: https://context7.com/totodore/socketioxide/llms.txt Manage client subscriptions to named rooms using `Socket::join` and `Socket::leave`. Use `Socket::rooms` to inspect current room memberships. Rooms are essential for targeted broadcasting. ```rust use socketioxide::{extract::SocketRef, SocketIo}; let (_, io) = SocketIo::new_svc(); io.ns("/", async |s: SocketRef| { // Join multiple rooms at once s.join(["lobby", "general"]); println!("Rooms: {:?}", s.rooms()); s.on("join_room", async |s: SocketRef, Data(room): socketioxide::extract::Data| { s.join(room.clone()); s.emit("joined", &room).ok(); println!("Socket {} rooms: {:?}", s.id, s.rooms()); }); s.on("leave_room", async |s: SocketRef, Data(room): socketioxide::extract::Data| { s.leave(room.clone()); s.emit("left", &room).ok(); }); }); ``` -------------------------------- ### SocketIo::new_svc Source: https://context7.com/totodore/socketioxide/llms.txt Creates a standalone Hyper service and a SocketIo handle. The service responds to Socket.IO requests and returns 404 for all other non-Socket.IO requests. ```APIDOC ## SocketIo::new_svc — Create a standalone service Returns a `(SocketIoService, SocketIo)` pair that acts as a standalone Hyper service, responding with 404 to all non-Socket.IO requests. ### Example ```rust use socketioxide::{extract::SocketRef, SocketIo}; #[tokio::main] async fn main() -> Result<(), Box> { let (svc, io) = SocketIo::new_svc(); io.ns("/", async |s: SocketRef| { println!("Connected: {}", s.id); }); // Use `svc` directly as a hyper service let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?; axum::serve(listener, svc).await?; Ok(()) } ``` ``` -------------------------------- ### Configure MessagePack Parser Source: https://context7.com/totodore/socketioxide/llms.txt Use `ParserConfig::msgpack()` to configure Socket.IO to use MessagePack for serialization. This is faster and more memory-efficient than the default parser, especially for binary data. ```rust use socketioxide::{SocketIo, ParserConfig}; let (layer, io) = SocketIo::builder() .with_parser(ParserConfig::msgpack()) .build_layer(); ``` -------------------------------- ### Correct Approach: Emit and Disconnect Directly Source: https://github.com/totodore/socketioxide/blob/main/crates/socketioxide/docs/operators/fetch_sockets.md Use this approach to directly emit events or disconnect sockets within a room without fetching them first. This is more efficient than fetching sockets and then performing actions. ```rust # use socketioxide::{SocketIo, extract::*}; # async fn doc_main() { # let (_, io) = SocketIo::new_svc(); io.within("room1").emit("foo", "bar").await.unwrap(); io.within("room1").disconnect().await.unwrap(); # } ``` -------------------------------- ### Connect Middlewares Source: https://context7.com/totodore/socketioxide/llms.txt Middlewares that execute before the connect handler. They can be used for authentication or other pre-connection logic. Returning an error will refuse the connection. ```APIDOC ## `ConnectHandler::with` — Connect middlewares Middlewares run before the connect handler. They must return `Result<(), E> where E: Display`. Returning `Err` refuses the connection and sends a `connect_error` event to the client. ```rust use socketioxide::{handler::ConnectHandler, extract::{Data, SocketRef}, SocketIo}; #[derive(Debug)] struct AuthError; impl std::fmt::Display for AuthError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "unauthorized") } } impl std::error::Error for AuthError {} async fn auth_middleware(Data(token): Data) -> Result<(), AuthError> { if token == "secret" { Ok(()) } else { Err(AuthError) } } async fn log_middleware(s: SocketRef) -> Result<(), AuthError> { println!("New connection attempt from sid={}", s.id); Ok(()) } async fn on_connect(s: SocketRef) { println!("Authenticated socket {}", s.id); } let (_, io) = SocketIo::new_svc(); // Middlewares are evaluated right-to-left (log_middleware first, then auth_middleware) io.ns("/secure", on_connect.with(auth_middleware).with(log_middleware)); ``` ``` -------------------------------- ### User Joined Event Handling Source: https://github.com/totodore/socketioxide/blob/main/crates/socketioxide-mongodb/README.md Handles the 'user joined' event by broadcasting user information. Requires `num_users` and `username`. ```rust async fn on_user_joined(s: SocketRef, Extension(username): Extension) { let num_users = user_cnt.get_user_count().await.unwrap_or(0); let res = &Res::UserEvent { num_users, username: Username(username), }; s.broadcast().emit("user joined", res).await.ok(); } ``` -------------------------------- ### Emit to specific rooms including current socket from global context Source: https://github.com/totodore/socketioxide/blob/main/crates/socketioxide/docs/operators/to.md When emitting from the global `io` context, `to()` selects all sockets within the specified rooms, including the current socket if it resides in those rooms. This is useful for broadcasting to a set of rooms without excluding any socket. ```rust io .to("room1") .to(["room2", "room3"]) .emit("test", &data) .await; ``` -------------------------------- ### Socket::join / Socket::leave / Socket::rooms Source: https://context7.com/totodore/socketioxide/llms.txt Sockets can join and leave arbitrary named rooms. Rooms are the fundamental unit of targeted broadcasting. ```APIDOC ## Socket::join / Socket::leave / Socket::rooms — Room management ### Description Sockets can join and leave arbitrary named rooms. Rooms are the fundamental unit of targeted broadcasting. ### Methods - **join(room_name | rooms_array)**: Joins the socket to one or more rooms. - **leave(room_name)**: Leaves a specific room. - **rooms()**: Returns a set of rooms the socket is currently in. ### Parameters - **room_name** (string) - The name of the room. - **rooms_array** (array of strings) - An array of room names. ### Request Example ```rust use socketioxide::{extract::SocketRef, SocketIo}; let (_, io) = SocketIo::new_svc(); io.ns("/", async |s: SocketRef| { // Join multiple rooms at once s.join(["lobby", "general"]); println!("Rooms: {:?}", s.rooms()); s.on("join_room", async |s: SocketRef, Data(room): socketioxide::extract::Data| { s.join(room.clone()); s.emit("joined", &room).ok(); println!("Socket {} rooms: {:?}", s.id, s.rooms()); }); s.on("leave_room", async |s: SocketRef, Data(room): socketioxide::extract::Data| { s.leave(room.clone()); s.emit("left", &room).ok(); }); }); ``` ```