lilydocs
HTTP APIWebSocketConsumer
Fundamentals3 min read

Scopes and interfaces

Match Singleton, Scoped, and Transient choices to the actual lifetimes of handshakes, messages, and lifecycle hooks.

A DI lifetime determines when a service is created and which owner cleans it up. Using Arc<T> does not select a lifetime; it only shares a reference.

Service lifetimes#

Lifetime Sharing Typical use
Singleton One instance throughout the container Pools, immutable policies, shared clients
Scoped One instance within the same active scope Message transactions, units of work
Transient A new instance for each resolution Short-lived helper services

Putting an Arc clone of a Scoped service into a connection local does not extend the right to use that resource. dispose may run when the scope closes. Store connection data that must be shared as owned DTOs.

WebSocket scopes#

Boundary Scope behavior
Handshake middleware / identity A temporary handshake scope when these steps exist
Connection middleware Does not provide a persistent DI scope for the connection lifetime
connected Its own lifecycle invocation scope
Each selected message A shared scope for middleware, guards, extractors, and the action
disconnected A new cleanup invocation scope

The handshake scope is cleaned up before HTTP 101. Owned data transferred into the identity result remains; the Scoped verifier instance does not. Connected and disconnected do not share a scope. A message waiting in the queue has not yet created a DI scope or action task.

Interface registration#

A concrete service can also be exposed through a trait object. The following snippet requires no additional dependencies beyond the DI dependencies:

Interface service
use std::sync::Arc;
use lilyrs::websocket::Injectable;
use lilyrs::websocket::ServiceTrait;

trait GreetingSource: Send + Sync {
    fn greeting(&self) -> &'static str;
}

#[derive(Injectable)]
#[service(interface = dyn GreetingSource, lifetime = "Singleton")]
struct TurkishGreeting;
impl GreetingSource for TurkishGreeting {
    fn greeting(&self) -> &'static str { "Merhaba" }
}
impl ServiceTrait for TurkishGreeting {}

#[derive(Injectable)]
#[service(lifetime = "Scoped")]
struct WelcomeService {
    #[inject]
    source: Arc<dyn GreetingSource>,
}
impl ServiceTrait for WelcomeService {}

An action parameter can also use Service<dyn GreetingSource>. Two active implementations of the same interface cause a build error; if a choice is needed, make the DI registration unambiguous in the application's startup model. Do not use a field contract such as #[inject(as = ...)].

Lifetime compatibility#

Resolve a message's Scoped transaction through the active exchange instead of binding it to a Singleton controller or middleware in the constructor. Concrete and interface resolutions follow the same registration and lifetime model. Do not expect the same allocation when resolving a Transient service twice.

This distinction is particularly important in the PostgreSQL transaction, authentication verifier, and message middleware examples.

Sources for this page v0.1.0

This guide is based on the reviewed framework commit.

QUICK START
Documentation
Source code