Skip to main content

SessionEngine

Struct SessionEngine 

Source
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>
where L: Lifecycle, C: Clock, G: EventLog,

Source

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.

Source

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.

Source

pub const fn id(&self) -> SessionId

Which session this is.

Source

pub const fn state(&self) -> SessionState

Where it is in its lifecycle.

Source

pub const fn resolution(&self) -> Option<Resolution<L::Profile, L::Version>>

The negotiated profile, if anything has been negotiated.

Source

pub const fn lifecycle(&self) -> &L

The protocol this session speaks.

Source

pub fn in_flight(&self) -> usize

How many requests are in flight.

Source

pub fn in_flight_ids(&self) -> impl Iterator<Item = &RequestId>

The ids of everything in flight, in no particular order.

Source

pub fn request(&self, id: &RequestId) -> Option<&InFlight>

The entry for id, if it is still running.

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn finish(&mut self) -> Result<(), EngineError>

Records that the session finished. Terminal.

§Errors

EngineError::Lifecycle from a session that is not closing.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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>

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.

Trait Implementations§

Source§

impl<L, C, G> Debug for SessionEngine<L, C, G>
where L: Lifecycle, C: Debug, G: Debug,

Source§

fn fmt(&self, formatter: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

§

impl<L, C, G> Freeze for SessionEngine<L, C, G>
where L: Freeze, C: Freeze, G: Freeze, Option<Resolution<<L as Lifecycle>::Profile, <L as Lifecycle>::Version>>: Freeze,

§

impl<L, C, G> RefUnwindSafe for SessionEngine<L, C, G>

§

impl<L, C, G> Send for SessionEngine<L, C, G>
where L: Send, C: Send, G: Send, Option<Resolution<<L as Lifecycle>::Profile, <L as Lifecycle>::Version>>: Send,

§

impl<L, C, G> Sync for SessionEngine<L, C, G>
where L: Sync, C: Sync, G: Sync, Option<Resolution<<L as Lifecycle>::Profile, <L as Lifecycle>::Version>>: Sync,

§

impl<L, C, G> Unpin for SessionEngine<L, C, G>
where L: Unpin, C: Unpin, G: Unpin, Option<Resolution<<L as Lifecycle>::Profile, <L as Lifecycle>::Version>>: Unpin,

§

impl<L, C, G> UnsafeUnpin for SessionEngine<L, C, G>

§

impl<L, C, G> UnwindSafe for SessionEngine<L, C, G>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<A, B, T> HttpServerConnExec<A, B> for T
where B: Body,

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more