quic_reverse/
state.rs

1// Copyright 2024-2026 Farlight Networks, LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Session state machine.
16//!
17//! Defines the lifecycle states of a quic-reverse session and valid transitions.
18
19/// Session lifecycle state.
20///
21/// The session progresses through these states:
22/// ```text
23/// Init ──► Negotiating ──► Ready ──► Closing ──► Closed
24///              │             │          │
25///              │             ▼          │
26///              │       Disconnected ────┴──► Closed
27///              │             │
28///              └─────────────┴──────────────► Closed (on error)
29/// ```
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
31pub enum State {
32    /// Initial state before negotiation begins.
33    #[default]
34    Init,
35    /// Handshake in progress (Hello/HelloAck exchange).
36    Negotiating,
37    /// Session is ready for stream operations.
38    Ready,
39    /// Connection lost but session not yet closed.
40    ///
41    /// The application may attempt to reconnect from this state.
42    Disconnected,
43    /// Graceful shutdown in progress.
44    Closing,
45    /// Session has terminated.
46    Closed,
47}
48
49impl State {
50    /// Converts from u8 representation used in atomic storage.
51    #[must_use]
52    pub(crate) const fn from_u8(v: u8) -> Self {
53        match v {
54            0 => Self::Init,
55            1 => Self::Negotiating,
56            2 => Self::Ready,
57            3 => Self::Disconnected,
58            4 => Self::Closing,
59            _ => Self::Closed,
60        }
61    }
62
63    /// Returns true if the session can accept new stream operations.
64    #[must_use]
65    pub const fn is_ready(&self) -> bool {
66        matches!(self, Self::Ready)
67    }
68
69    /// Returns true if the session has terminated.
70    #[must_use]
71    pub const fn is_closed(&self) -> bool {
72        matches!(self, Self::Closed)
73    }
74
75    /// Returns true if the connection was lost.
76    #[must_use]
77    pub const fn is_disconnected(&self) -> bool {
78        matches!(self, Self::Disconnected)
79    }
80
81    /// Returns true if a transition to the target state is valid.
82    #[must_use]
83    #[allow(clippy::match_same_arms)] // Keep separate for documentation clarity
84    pub const fn can_transition_to(&self, target: Self) -> bool {
85        use State::{Closed, Closing, Disconnected, Init, Negotiating, Ready};
86
87        match (*self, target) {
88            // Normal progression
89            (Init, Negotiating) => true,
90            (Negotiating, Ready) => true,
91            (Ready, Closing) => true,
92            (Closing, Closed) => true,
93
94            // Disconnection from Ready state
95            (Ready, Disconnected) => true,
96
97            // Disconnected can transition to Closed
98            (Disconnected, Closed) => true,
99
100            // Any state can transition to Closed (error/abort/normal)
101            // Note: (Closing, Closed) is already handled above
102            (Init | Negotiating | Ready | Closed, Closed) => true,
103
104            // Everything else is invalid
105            _ => false,
106        }
107    }
108}
109
110impl std::fmt::Display for State {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        match self {
113            Self::Init => write!(f, "init"),
114            Self::Negotiating => write!(f, "negotiating"),
115            Self::Ready => write!(f, "ready"),
116            Self::Disconnected => write!(f, "disconnected"),
117            Self::Closing => write!(f, "closing"),
118            Self::Closed => write!(f, "closed"),
119        }
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn valid_forward_transitions() {
129        assert!(State::Init.can_transition_to(State::Negotiating));
130        assert!(State::Negotiating.can_transition_to(State::Ready));
131        assert!(State::Ready.can_transition_to(State::Closing));
132        assert!(State::Closing.can_transition_to(State::Closed));
133    }
134
135    #[test]
136    fn error_transitions_to_closed() {
137        assert!(State::Init.can_transition_to(State::Closed));
138        assert!(State::Negotiating.can_transition_to(State::Closed));
139        assert!(State::Ready.can_transition_to(State::Closed));
140        assert!(State::Closing.can_transition_to(State::Closed));
141    }
142
143    #[test]
144    fn invalid_transitions() {
145        // Can't go backwards
146        assert!(!State::Ready.can_transition_to(State::Negotiating));
147        assert!(!State::Ready.can_transition_to(State::Init));
148        assert!(!State::Closed.can_transition_to(State::Ready));
149
150        // Can't skip states
151        assert!(!State::Init.can_transition_to(State::Ready));
152        assert!(!State::Negotiating.can_transition_to(State::Closing));
153    }
154
155    #[test]
156    fn state_display() {
157        assert_eq!(State::Init.to_string(), "init");
158        assert_eq!(State::Ready.to_string(), "ready");
159        assert_eq!(State::Closed.to_string(), "closed");
160    }
161
162    #[test]
163    fn is_ready() {
164        assert!(!State::Init.is_ready());
165        assert!(!State::Negotiating.is_ready());
166        assert!(State::Ready.is_ready());
167        assert!(!State::Closing.is_ready());
168        assert!(!State::Closed.is_ready());
169    }
170
171    #[test]
172    fn is_closed() {
173        assert!(!State::Init.is_closed());
174        assert!(!State::Ready.is_closed());
175        assert!(State::Closed.is_closed());
176    }
177
178    #[test]
179    fn disconnected_transitions() {
180        assert!(State::Ready.can_transition_to(State::Disconnected));
181        assert!(State::Disconnected.can_transition_to(State::Closed));
182        assert!(!State::Init.can_transition_to(State::Disconnected));
183        assert!(!State::Disconnected.can_transition_to(State::Ready));
184    }
185
186    #[test]
187    fn is_disconnected() {
188        assert!(!State::Ready.is_disconnected());
189        assert!(State::Disconnected.is_disconnected());
190        assert!(!State::Closed.is_disconnected());
191    }
192}