Connected and disconnected
Use controller hooks with the right scopes and cancellation phases, and tie disconnected eligibility to a successful connected result.
Controller lifecycle hooks group per-connection domain behavior. Use them for presence records or application-owned connection tracking. For reusable general connection policy, connection middleware is appropriate.
Write the hooks#
Add the example as src/lifecycle.rs and include it from the crate root:
use std::sync::Arc;
use lilyrs::websocket::{CleanupCancellation, DisconnectReason, ExecutionCancellation,
Extensions, WebSocketContext, WebSocketController, WebSocketControllerInitError,
WebSocketControllerTrait, WebSocketLifecycleError, async_trait, websocket_controller};
#[derive(WebSocketController)]
#[namespace("presence")]
struct PresenceController;
#[async_trait]
impl WebSocketControllerTrait for PresenceController {
async fn new(_: Arc<Extensions>) -> Result<Self, WebSocketControllerInitError> { Ok(Self) }
}
#[websocket_controller]
impl PresenceController {
#[connected]
#[timeout(seconds = 5)]
async fn connected(&self, _: WebSocketContext, stopping: ExecutionCancellation)
-> Result<(), WebSocketLifecycleError>
{
if stopping.is_cancelled() {
return Err(WebSocketLifecycleError::rejected("CONNECT_STOPPED"));
}
Ok(()) // This success makes the disconnected callback eligible to run.
}
#[disconnected]
#[timeout(seconds = 5)]
async fn disconnected(&self, _: WebSocketContext, reason: DisconnectReason,
cleanup: CleanupCancellation) -> Result<(), WebSocketLifecycleError>
{
let _ = (reason, cleanup.is_cancelled());
// Clean up the presence record with an idempotent, bounded service.
Ok(())
}
}Download the complete example.
Each hook uses its own DI invocation scope. A Scoped service resolved during connected is not reused during disconnected. Keep required owned connection data within the appropriate context boundary; do not carry a service Arc outside its scope.
When does disconnected run?#
The disconnected obligation arises when connected is actually invoked and returns Ok(()). Defining only disconnected does not guarantee it runs on every disconnect. If there is no connected work, a successful no-op connected like the example is sufficient.
This success is recorded without waiting for connected-scope disposal. A later disposal failure does not erase the callback's successful return. Cleanup code must handle such partial states idempotently.
Execution and cleanup#
connected is in the execution phase and can receive ExecutionCancellation. disconnected is in the cleanup phase and receives CleanupCancellation. Types for the opposite phase are rejected at compile time. Lifecycle hooks cannot receive payload consumers and return Result<(), WebSocketLifecycleError>.
The controller timeout is the message default; a lifecycle hook's duration comes from its own attribute or connection_lifecycle_timeout_secs. A cleanup signal does not grant authority to cancel other work. The old Cancellation name is an execution alias and is not used in new examples.
Sending to a closing connection#
The caller's socket is terminal while disconnected runs. Do not expect sends to the caller to succeed. A bounded notification to another healthy connection can be attempted, but resource cleanup must not depend on its delivery.
Presence cleanup in the example is a comment; you supply the real store and cleanup implementation. Test callbacks that can finish only partially, timeouts, and the shutdown result.
Sources for this page v0.1.0
This guide is based on the reviewed framework commit.