Context and local data
Use connection capabilities, message metadata, and typed local data within the correct lifetimes.
Context provides connection access; local data carries small typed values belonging to a particular operation. Do not confuse the lifetime of a DI service with that of a local DTO.
Connection and message contexts#
WebSocketContext groups namespace, connection identity, client targets, rooms, and identity access. Inside an action, it carries a view tied to the message's identity snapshot. WebSocketMessageContext exposes the event, message ID, timestamp, deadline, cancellation, and envelope metadata.
handshake_headers() is the original Upgrade information, while MessageHeaders is envelope metadata. message_age() measures a message's age; connection_age()/uptime() measures the connection's age. Do not use these as the same latency metric.
Three local boundaries#
| Data | Where it is written | Lifetime |
|---|---|---|
| Handshake local | WsHandshakeExchange::insert_local |
Handshake/identity steps |
| Connection local | Identity result's insert_connection_local method |
Immutable throughout the connection |
| Message local | WsMessageExchange::insert_message_local |
One message pipeline |
Handshake locals do not automatically become connection locals. Explicitly transfer only required owned data in the identity result. Handshake and connection locals have a limit of 32 types. Use small DTOs meeting Clone/Send/Sync requirements; do not store scope-owned resources.
Pass data from middleware to an action#
The MessageTimer example writes StartedAt(tokio::time::Instant) into a message local. The action can read it optionally:
#[message("timed")]
async fn timed(
&self,
started: Option<lilyrs::websocket::MessageLocal<StartedAt>>,
) -> Result<lilyrs::websocket::NoReply, lilyrs::websocket::WebSocketActionError> {
if let Some(lilyrs::websocket::MessageLocal(StartedAt(start))) = started {
let _elapsed = start.elapsed();
}
Ok(lilyrs::websocket::NoReply)
}Add this snippet to a controller macro impl and import the example's StartedAt type. Without middleware registration, the optional extractor returns None; for a required local, include the corresponding middleware in the route's plan.
What happens when identity changes?#
The Principal snapshot of a message in progress is fixed. Refresh does not cause its guard and action to see different identities. The next message receives the new identity. Connection locals are not changed by refresh; identity lifecycle explains this boundary.
Putting a Scoped service Arc in a local does not delay scope cleanup. Resolve the resource again from the active exchange or choose the correct DI lifetime if it must live longer. Cloning context does not grant unlimited background sends or execution authority after shutdown either; cancellation and shutdown boundaries remain.
Sources for this page v0.1.0
This guide is based on the reviewed framework commit.