Loading...
Loading...
Ossido has first-class WebSocket support for building realtime features - chat, live feeds,
presence, notifications. It lives entirely at the Rust/axum server layer; the browser connects
back with its native WebSocket.
Why not during SSR? Ossido's server-side rendering runtime deliberately forbids live I/O (there is no
fetchinside it). WebSockets are a server concern, so they are handled by the axum server - never inside the React render.
src/ws.rs, served at /__ossido/ws.SocketManager stores live connections by an app key and lets you push
messages to them later - from the socket handler or from any #[api] / #[action].connect() client on the frontend.Events are JSON. Each one is a struct with a direction:
#[ossido::server_ws_event("name")] - server -> client (the server sends it, the client
receives it).#[ossido::client_ws_event("name")] - client -> server (the client sends it, the server
receives it).Both apply everything #[ossido::Type] does (serde + TypeScript generation), so the same struct
is usable in Rust and gets a generated TypeScript type automatically.
On the wire every frame is an envelope:
Direction is enforced by the type system: you can only send ServerWsEvents and only
parse ClientWsEvents, so a client-only event can never be sent to a client, and vice-versa.
A bare identifier also works as the name:
#[server_ws_event(user_joined)].
Your project has exactly one WebSocket handler, and it must live in src/ws.rs (a sibling of
src/app.rs). Marking a #[ossido::ws] handler anywhere under src/routes/** is a build error.
The handler receives the raw upgraded WebSocket plus the SocketManager. You decide if and
when to hand the socket to the manager with store(key, socket) - typically after
authenticating - and then loop over inbound events. The key can be anything that is
Eq + Hash + Clone + Send + Sync + 'static (see Keys) - here, the u64 user id.
The handler's parameters, after Request, may appear in any order and are supplied by the
framework or destructured from your ApplicationState (exactly like #[api]):
WebSocket - the framework: the raw upgraded socket.TypedSocket - the framework: a typed wrapper (raw escape hatch).SocketManager - the framework: the global connection store.Logger (logger) - the framework: a request-scoped logger.ApplicationState (src/app.rs).SocketManagerSocketManager is a cheap, cloneable handle over the framework's keyed connection store. It is
global, so you can reach it from anywhere - not just the socket handler. To message a
connection later you look it up by key and send on the returned group:
A key is anything that implements Eq + Hash + Clone + Send + Sync + 'static - a u64 user
id, a String room name, a Uuid, a custom #[derive(Clone, PartialEq, Eq, Hash)] enum, etc.
(the blanket-implemented Key trait). Keys of different types never collide, so you can key some
connections by user id and others by room name in the same app.
store takes ownership of the socket, splits it, spawns its writer task, and indexes it under
key. A key may hold many connections (one user with several tabs/devices).
get(&key) returns a SocketGroup - the 0, 1, or many connections currently stored under that
key. Sending on the group fans out to all of them, so the common "notify this user everywhere
they're connected" case is one line:
store(key, socket) - store a socket under key; returns a Connection.get(&key) - the connections under key; returns a SocketGroup.broadcast(&ev) - send a server event to every live connection.count() - number of live connections.SocketGroup (get's result) offers send(&ev), close(code), len(), is_empty(), and
iter() over individual SocketHandles. A SocketHandle is a cheap, cloneable sender for one
connection - id(), send(&ev), close(code) - safe to hold and use from anywhere.
Connection handleReturned by store; it is the read side of the socket plus the means to reply and re-key:
recv().await - next inbound client event (None on close).send(&ev).await - send a server event back to this client.handle() - a cloneable SocketHandle for sending from elsewhere.add_key(key) - also store this connection under an additional key.id() - this connection's ConnId.close(code).await - close this connection.Dropping a Connection (the handler returns, or the client disconnects) unregisters it from the
ConnId map and every key index automatically.
Browsers cannot set custom headers (e.g. Authorization) on a WebSocket handshake, so auth
relies on what a same-origin handshake carries - cookies - plus optional token schemes.
req (cookies/headers/query) is available before you store the
socket. Authenticate there and simply return (or conn.close(1008)) to reject. Store the
socket keyed by the authenticated identity so you can address the user later.Sec-WebSocket-Protocol subprotocol (connect() forwards protocols);req.Import connect from @ossido-labs/ossido/ws. It resolves ws(s):// from the current origin,
connects to /__ossido/ws, and gives you a small typed handle. Send/receive are typed against the
events you declared - the client only sees ClientWsEvents to send and ServerWsEvents to
receive.
connect() accepts { protocols } for subprotocol-based auth and exposes the underlying
WebSocket as socket.raw if you need it.
TypeScript types for your events are generated automatically into .ossido/types.ts when you run
ossido dev / ossido build, so on and send are type-checked with no extra setup.
recv().await costs a
few KB of task state and zero worker threads; the tokio runtime multiplexes many thousands
of them.tokio::task::spawn_blocking.#[ossido::ws] - the single handler (src/ws.rs).#[ossido::server_ws_event("name")] - server -> client event.#[ossido::client_ws_event("name")] - client -> server event.ossido::ws::SocketManager - keyed connection store (global via sockets()).ossido::ws::Connection - per-connection handle (returned by store).ossido::ws::SocketGroup - connections under a key (returned by get).ossido::ws::SocketHandle - cloneable sender for one connection.ossido::ws::Key - blanket trait for anything usable as a key.ossido::ws::sockets() - the global SocketManager.@ossido-labs/ossido/ws -> connect - the frontend client.GET /__ossido/ws.