Session

session-rs

A lightweight, async WebSocket protocol for Rust.

## Introduction `session-rs` is a small request/response + notification protocol that runs over WebSockets, with typed methods on both ends. - **Typed methods**: requests, responses and errors are (de)serialized for you. - **Both directions**: either peer can send requests and notifications. - **Transport-agnostic**: the protocol runs over any `Sink`/`Stream` of frames. Adapters ship for [tokio-tungstenite](https://docs.rs/tokio-tungstenite) and [axum](https://docs.rs/axum). - **Bounded**: the built-in server limits message and frame sizes (1 MiB by default). ## Installation ```bash cargo add session-rs ``` | Feature | Default | Enables | | --- | --- | --- | | `server` | yes | `SessionServer`, a standalone WebSocket server | | `client` | yes | `Session::connect` for `ws://` URLs | | `rustls` / `native-tls` | no | `wss://` URLs in `Session::connect` | | `axum` | no | `Session::from_axum` for axum WebSocket upgrades | ## Usage Define a method once and share it between both peers: ```rust #[derive(Debug, Serialize, Deserialize)] struct Data; impl Method for Data { const NAME: &'static str = "data"; type Request = String; type Response = String; type Error = String; } ``` ### Server ```rust let server = SessionServer::bind("127.0.0.1:8080").await?; server .session_loop(async |session, addr| { // Runs for every new client. Register handlers here; the session // starts reading once this returns, so no message arrives early. session .on_request::(async |_id, req| Ok(format!("echo: {req}"))) .await; Ok(()) }) .await?; ``` Use `SessionServer::with_config(ServerConfig { .. })` to change the size limits or the handshake timeout. ### Client ```rust let session = Session::connect("ws://127.0.0.1:8080").await?; session.start_receiver(); let reply = session.request::("Hello".to_string()).await?; // Ok("echo: Hello") ``` ### axum With the `axum` feature, a session can share a router with ordinary HTTP routes, such as a health check: ```rust async fn ws(upgrade: WebSocketUpgrade) -> Response { upgrade.max_message_size(1 << 20).on_upgrade(async |socket| { let session = Session::from_axum(socket); session.on_request::(async |_, req| Ok(req)).await; session.start_receiver(); }) } let app = Router::new() .route("/", get(ws)) .route("/health", get(async || "ok")); ``` ### Other transports `Session::from_transport(sink, stream)` accepts any `Sink` and `Stream>`, and `Session::from_tungstenite` wraps an existing tokio-tungstenite stream (e.g. one accepted over TLS). The transport must answer pings itself. ### Semantics - Incoming requests and notifications are handled one at a time, in arrival order. Responses to your own requests are delivered independently, so a handler can `request` from its peer. - A request for an unknown method, or with data that doesn't deserialize, gets an error response instead of no reply. - A handler that panics fails only its own request. - `request` fails with `Error::ConnectionClosed` if the session closes first; `request_timeout` adds a deadline. - `on_close` runs exactly once, whichever side closes. `start_ping(interval, timeout)` closes peers that stop answering pings. ## Protocol Every message is a JSON text frame with a `type` tag. #### Request The `id` is chosen by the sender and increments per peer. ```json { "type": "request", "id": 1, "method": "data", "data": "Hello from client" } ``` #### Response A response **must** carry the id of the request it answers. ```json { "type": "response", "id": 1, "result": "Hello from server" } ``` #### Error response ```json { "type": "errorresponse", "id": 1, "error": "Invalid data" } ``` #### Notification A notification is fire-and-forget and gets no response. ```json { "type": "notification", "method": "data", "data": "Hello from server" } ```