Skip to main content

liminal_sdk/connection/
lifecycle.rs

1use alloc::boxed::Box;
2use alloc::format;
3use alloc::string::String;
4use alloc::vec::Vec;
5use core::fmt;
6use core::pin::Pin;
7use core::task::{Context, Poll};
8
9use futures_core::Stream;
10
11use crate::SdkError;
12
13/// Application-visible lifecycle state for a remote SDK connection.
14#[derive(Clone, Debug, PartialEq, Eq)]
15pub enum ConnectionState {
16    /// The SDK is establishing a connection.
17    Connecting,
18    /// The SDK has an active connection.
19    Connected,
20    /// A caller-authorized real reconnect attempt is in progress.
21    Reconnecting,
22    /// The SDK is disconnected and will not become connected without reconnecting.
23    Disconnected {
24        /// Reason the SDK entered the disconnected state.
25        reason: DisconnectReason,
26    },
27}
28
29/// Reason a connection entered the disconnected state.
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum DisconnectReason {
32    /// The connection was closed intentionally.
33    Normal,
34    /// The connection closed because of an error.
35    Error,
36    /// The connection closed because a timeout elapsed.
37    Timeout,
38}
39
40/// Event emitted after a connection lifecycle transition succeeds.
41#[derive(Clone, Debug, PartialEq, Eq)]
42pub struct ConnectionEvent {
43    /// State before the transition.
44    pub previous: ConnectionState,
45    /// State after the transition.
46    pub current: ConnectionState,
47}
48
49impl ConnectionEvent {
50    /// Creates a connection transition event.
51    #[must_use]
52    pub const fn new(previous: ConnectionState, current: ConnectionState) -> Self {
53        Self { previous, current }
54    }
55}
56
57/// Stream wrapper for observing connection lifecycle events.
58///
59/// Concrete SDK clients can wrap their runtime-specific event stream in this
60/// type while exposing a stable SDK item type of [`ConnectionEvent`].
61pub struct ConnectionEvents<S> {
62    inner: S,
63}
64
65impl<S> ConnectionEvents<S> {
66    /// Wraps a stream that yields connection transition events.
67    #[must_use]
68    pub const fn new(inner: S) -> Self {
69        Self { inner }
70    }
71
72    /// Returns a shared reference to the wrapped stream.
73    #[must_use]
74    pub const fn inner(&self) -> &S {
75        &self.inner
76    }
77
78    /// Unwraps the runtime-specific event stream.
79    #[must_use]
80    pub fn into_inner(self) -> S {
81        self.inner
82    }
83}
84
85impl<S: Clone> Clone for ConnectionEvents<S> {
86    fn clone(&self) -> Self {
87        Self {
88            inner: self.inner.clone(),
89        }
90    }
91}
92
93impl<S> fmt::Debug for ConnectionEvents<S> {
94    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
95        formatter.debug_struct("ConnectionEvents").finish()
96    }
97}
98
99impl<S> Stream for ConnectionEvents<S>
100where
101    S: Stream<Item = ConnectionEvent> + Unpin,
102{
103    type Item = ConnectionEvent;
104
105    fn poll_next(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
106        let stream = &mut self.as_mut().get_mut().inner;
107        Pin::new(stream).poll_next(context)
108    }
109}
110
111type ConnectionObserver = Box<dyn FnMut(&ConnectionEvent) + Send>;
112
113/// Owns the SDK connection lifecycle state and emits validated transitions.
114pub struct ConnectionLifecycle {
115    state: ConnectionState,
116    observers: Vec<ConnectionObserver>,
117}
118
119impl ConnectionLifecycle {
120    /// Creates a lifecycle in the [`ConnectionState::Connecting`] state.
121    #[must_use]
122    pub const fn new() -> Self {
123        Self {
124            state: ConnectionState::Connecting,
125            observers: Vec::new(),
126        }
127    }
128
129    /// Returns the current connection state.
130    #[must_use]
131    pub const fn state(&self) -> &ConnectionState {
132        &self.state
133    }
134
135    /// Registers an observer that is called after each successful transition.
136    pub fn observe(&mut self, observer: impl FnMut(&ConnectionEvent) + Send + 'static) {
137        self.observers.push(Box::new(observer));
138    }
139
140    /// Transitions from [`ConnectionState::Disconnected`] to connecting.
141    ///
142    /// # Errors
143    ///
144    /// Returns [`SdkError`] when the lifecycle is not disconnected.
145    pub fn connect(&mut self) -> Result<(), SdkError> {
146        match self.state {
147            ConnectionState::Disconnected { .. } => {
148                self.transition(ConnectionState::Connecting);
149                Ok(())
150            }
151            _ => Err(invalid_transition(&self.state, "Connecting")),
152        }
153    }
154
155    /// Transitions from connecting or reconnecting to connected.
156    ///
157    /// # Errors
158    ///
159    /// Returns [`SdkError`] when the lifecycle is disconnected or already connected.
160    pub fn connected(&mut self) -> Result<(), SdkError> {
161        match self.state {
162            ConnectionState::Connecting | ConnectionState::Reconnecting => {
163                self.transition(ConnectionState::Connected);
164                Ok(())
165            }
166            _ => Err(invalid_transition(&self.state, "Connected")),
167        }
168    }
169
170    /// Records that an externally authorized real reconnect attempt started.
171    ///
172    /// This method has no delay, timer, retry counter, or authority producer. The
173    /// remote participant entrypoint obtains one-use attempt authority from
174    /// `liminal-protocol` before calling the transport.
175    ///
176    /// # Errors
177    ///
178    /// Returns [`SdkError`] when reconnecting from the current state is invalid.
179    pub fn reconnect_started(&mut self) -> Result<(), SdkError> {
180        match self.state {
181            ConnectionState::Connecting | ConnectionState::Connected => {
182                self.transition(ConnectionState::Reconnecting);
183                Ok(())
184            }
185            ConnectionState::Reconnecting => Err(invalid_transition(&self.state, "Reconnecting")),
186            ConnectionState::Disconnected { .. } => {
187                Err(invalid_transition(&self.state, "Reconnecting"))
188            }
189        }
190    }
191
192    /// Transitions to disconnected with the supplied reason.
193    ///
194    /// # Errors
195    ///
196    /// Returns [`SdkError`] when the lifecycle is already disconnected.
197    pub fn disconnect(&mut self, reason: DisconnectReason) -> Result<(), SdkError> {
198        match self.state {
199            ConnectionState::Connecting
200            | ConnectionState::Connected
201            | ConnectionState::Reconnecting => {
202                self.transition(ConnectionState::Disconnected { reason });
203                Ok(())
204            }
205            ConnectionState::Disconnected { .. } => {
206                Err(invalid_transition(&self.state, "Disconnected"))
207            }
208        }
209    }
210
211    fn transition(&mut self, next: ConnectionState) {
212        let previous = core::mem::replace(&mut self.state, next);
213        let event = ConnectionEvent::new(previous, self.state.clone());
214
215        for observer in &mut self.observers {
216            observer(&event);
217        }
218    }
219}
220
221impl Default for ConnectionLifecycle {
222    fn default() -> Self {
223        Self::new()
224    }
225}
226
227impl fmt::Debug for ConnectionLifecycle {
228    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
229        formatter
230            .debug_struct("ConnectionLifecycle")
231            .field("state", &self.state)
232            .field("observers", &self.observers.len())
233            .finish()
234    }
235}
236
237fn invalid_transition(previous: &ConnectionState, requested: &str) -> SdkError {
238    connection_error(format!(
239        "invalid connection transition from {previous:?} to {requested}"
240    ))
241}
242
243const fn connection_error(description: String) -> SdkError {
244    SdkError::Connection { description }
245}
246
247#[cfg(test)]
248mod tests;