pub struct SessionEngine<L, C, G>where
L: Lifecycle,{ /* private fields */ }Expand description
A session, its protocol, and everything in flight on it.
L is the protocol, C the clock, I the id source, G the event log.
All four are consumed by impl Trait at the call site so a server with one
of each monomorphises once.
§Examples
use std::time::Duration;
use protocol_droid::application::session::{
Clock, Declaration, DeclarationError, EngineError, IdSource, Lifecycle, NullEventLog,
Resolution, SessionEngine,
};
use protocol_droid::domain::session::{Instant, SessionId, SessionState};
struct Fixed;
impl Clock for Fixed {
fn now(&self) -> Instant {
Instant::from_duration(Duration::ZERO)
}
}
struct Counting(std::sync::atomic::AtomicU64);
impl IdSource for Counting {
fn next_session_id(&self) -> SessionId {
SessionId::new(self.0.fetch_add(1, std::sync::atomic::Ordering::Relaxed))
}
}
/// A protocol that agrees to whatever it is asked for.
struct Echo;
impl Lifecycle for Echo {
type Profile = ();
type Version = u16;
fn classify(
&self,
state: SessionState,
_profile: Option<()>,
method: &str,
) -> protocol_droid::application::session::Classification {
use protocol_droid::application::session::Classification;
match method {
"handshake/start" => Classification::HandshakeStart,
"handshake/confirm" => Classification::HandshakeConfirm,
_ if state.admits_ordinary_work() => Classification::Ordinary,
_ => Classification::Refused,
}
}
fn resolve(
&self,
declaration: Declaration<'_>,
) -> Result<Resolution<(), u16>, DeclarationError> {
let Declaration::Handshake { version, .. } = declaration else {
return Err(DeclarationError::NoVersion);
};
Ok(Resolution {
profile: (),
version: u16::try_from(version.len()).unwrap_or(u16::MAX),
echoed: true,
})
}
}
let mut engine = SessionEngine::open(
Echo,
Fixed,
&Counting(std::sync::atomic::AtomicU64::new(0)),
NullEventLog,
);
assert_eq!(engine.state(), SessionState::Uninitialised);Implementations§
Source§impl<L, C, G> SessionEngine<L, C, G>
impl<L, C, G> SessionEngine<L, C, G>
Sourcepub fn open<J: IdSource>(lifecycle: L, clock: C, ids: &J, log: G) -> Self
pub fn open<J: IdSource>(lifecycle: L, clock: C, ids: &J, log: G) -> Self
Opens a session and records that it exists.
The identity comes from the injected source rather than from a counter in here, so a deployment that needs ids unique across processes seeds its own source and this constructor does not change.
Sourcepub fn open_stateless<J: IdSource>(
lifecycle: L,
clock: C,
ids: &J,
log: G,
) -> Self
pub fn open_stateless<J: IdSource>( lifecycle: L, clock: C, ids: &J, log: G, ) -> Self
Opens a session that negotiates per message and retains nothing.
The 2026-07-28 shape: there is no handshake to wait for, and the session exists only so a request has something to be negotiated against.
Sourcepub const fn state(&self) -> SessionState
pub const fn state(&self) -> SessionState
Where it is in its lifecycle.
Sourcepub const fn resolution(&self) -> Option<Resolution<L::Profile, L::Version>>
pub const fn resolution(&self) -> Option<Resolution<L::Profile, L::Version>>
The negotiated profile, if anything has been negotiated.
Sourcepub fn in_flight_ids(&self) -> impl Iterator<Item = &RequestId>
pub fn in_flight_ids(&self) -> impl Iterator<Item = &RequestId>
The ids of everything in flight, in no particular order.
Sourcepub fn request(&self, id: &RequestId) -> Option<&InFlight>
pub fn request(&self, id: &RequestId) -> Option<&InFlight>
The entry for id, if it is still running.
Sourcepub fn admit(
&self,
method: &str,
id: Option<&RequestId>,
) -> Result<Classification, Refusal>
pub fn admit( &self, method: &str, id: Option<&RequestId>, ) -> Result<Classification, Refusal>
Decides whether a message naming method may run.
The order of the checks is the contract, and it is not arbitrary:
terminal first, so a closing session is closed to the lifecycle’s
opinion of it; then the protocol’s own classification; then the
handshake gate; then the profile gate. A lifecycle that answers
Ordinary for a method on an uninitialised session is describing its
own vocabulary, not overriding the state machine.
§Errors
A Refusal, carrying the state that produced it.
Sourcepub fn negotiate(
&mut self,
declaration: Declaration<'_>,
) -> Result<Resolution<L::Profile, L::Version>, EngineError>
pub fn negotiate( &mut self, declaration: Declaration<'_>, ) -> Result<Resolution<L::Profile, L::Version>, EngineError>
Turns what a peer declared into a profile, and moves the session on.
The event applied is chosen by the session’s current state rather than by what was declared: a stateless session stays stateless, and a stateful one starts its handshake. That keeps “went stateless” a property of how the session was opened, which is the only place the distinction is actually known.
§Errors
EngineError::Declaration when the declaration is not something the
protocol can answer, and EngineError::Lifecycle when the resulting
transition is not legal from here — a second handshake on a session
that already has one.
Sourcepub fn confirm(&mut self) -> Result<(), EngineError>
pub fn confirm(&mut self) -> Result<(), EngineError>
Records that the peer confirmed the handshake.
Separate from negotiate because the specification
has two messages here and only the first one carries a version: a peer
that sends the second is saying “I have it”, and there is nothing left
to parse.
§Errors
EngineError::Lifecycle when the session is not waiting for a
confirmation.
Sourcepub fn close(&mut self) -> Result<Vec<RequestId>, EngineError>
pub fn close(&mut self) -> Result<Vec<RequestId>, EngineError>
Starts closing, and aborts everything that was still running.
The aborts happen here rather than at finish because
a handler awaiting a signal that will never arrive is a leaked task,
and the whole point of entering Closing is that nothing more will be
admitted.
§Errors
EngineError::Lifecycle from a session that is already closing or
closed.
Sourcepub fn finish(&mut self) -> Result<(), EngineError>
pub fn finish(&mut self) -> Result<(), EngineError>
Records that the session finished. Terminal.
§Errors
EngineError::Lifecycle from a session that is not closing.
Sourcepub fn register(
&mut self,
id: RequestId,
origin: RequestOrigin,
method: &str,
token: Option<ProgressToken>,
budget: Option<Duration>,
) -> Result<CancellationToken, DuplicateRequest>
pub fn register( &mut self, id: RequestId, origin: RequestOrigin, method: &str, token: Option<ProgressToken>, budget: Option<Duration>, ) -> Result<CancellationToken, DuplicateRequest>
Records a request as in flight and hands back its cancellation handle.
The handle is returned rather than left in the table because a handler
that has to await it cannot hold the engine’s borrow across the wait,
and this engine is single-writer. Everything a canceller needs is on
the token; everything an observer needs is on
request.
§Errors
DuplicateRequest when the id is already in flight. The existing
entry is untouched — the first request owns the id.
Sourcepub fn cancellation(&self, id: &RequestId) -> Option<CancellationToken>
pub fn cancellation(&self, id: &RequestId) -> Option<CancellationToken>
The cancellation handle for id, without taking the engine mutably.
The read-only half of register, for a task that is
already running and wants to stop waiting when the peer says so.
Sourcepub fn cancel(
&mut self,
id: &RequestId,
reason: Option<CancelReason>,
) -> CancelOutcome
pub fn cancel( &mut self, id: &RequestId, reason: Option<CancelReason>, ) -> CancelOutcome
Cancels a request.
Never fails. See CancelOutcome, and the specification’s reason for
it: a cancellation notification may arrive after its request has
finished, and reporting that as an error would turn a race the sender
is entitled to lose into a fault.
Sourcepub fn progress(
&mut self,
id: &RequestId,
progress: f64,
message: Option<String>,
) -> bool
pub fn progress( &mut self, id: &RequestId, progress: f64, message: Option<String>, ) -> bool
Records progress for id and says whether it should go on the wire.
false for an unknown id, for one that carried no progress token, and
for a value that does not move that request’s cursor forward.
Sourcepub fn expire(&mut self) -> Vec<RequestId>
pub fn expire(&mut self) -> Vec<RequestId>
Aborts and forgets every request whose budget is spent.
Returns the ids that were expired, in no particular order. This is a tick rather than a per-frame cost: the arithmetic is a subtraction and the table walk is proportional to what is in flight, which is bounded by what the peer asked for.
Sourcepub fn complete(&mut self, id: &RequestId) -> Option<Option<AbortReason>>
pub fn complete(&mut self, id: &RequestId) -> Option<Option<AbortReason>>
Forgets a finished request, and says whether it may be answered.
None for an id that is not in flight, which is the case a transport
has to treat as “already reported” rather than as a fault. Some(reason)
means the request was aborted while it ran and no response is due —
the peer withdrew it, or its budget ran out.
Sourcepub async fn dispatch<H, T>(
&mut self,
id: RequestId,
origin: RequestOrigin,
method: &str,
token: Option<ProgressToken>,
budget: Option<Duration>,
handler: H,
) -> Result<Dispatch<T>, DuplicateRequest>where
H: AsyncFnOnce(CancellationToken) -> T,
pub async fn dispatch<H, T>(
&mut self,
id: RequestId,
origin: RequestOrigin,
method: &str,
token: Option<ProgressToken>,
budget: Option<Duration>,
handler: H,
) -> Result<Dispatch<T>, DuplicateRequest>where
H: AsyncFnOnce(CancellationToken) -> T,
Runs one request from registration to completion.
The three steps are the specification’s ordering and the engine’s
whole reason for existing: register so a cancellation has something to
find, run the handler with a handle it owns, then remove the entry and
report what happened. A handler that returns after its request was
aborted produces Dispatch::Aborted, and the caller writes nothing —
that is the invariant the cancellation test pins.
The handler is an AsyncFnOnce rather than a boxed future so the
closure is monomorphised with the call rather than erased behind a
Pin<Box<dyn Future>>; it is called once, so async_call_once’s
owning-capture future is the right shape.
§Errors
DuplicateRequest when the id is already in flight. Nothing is run
and the existing entry is left alone.