Skip to main content

moqtap_client/draft07/
subscription.rs

1/// Subscription lifecycle states.
2#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3pub enum SubscriptionState {
4    /// Initial state before any SUBSCRIBE message is sent.
5    Idle,
6    /// SUBSCRIBE has been sent; awaiting OK or ERROR.
7    Subscribing,
8    /// Subscription is accepted and data may be flowing.
9    Active,
10    /// Subscription has ended (error, unsubscribe, or subscribe done).
11    Done,
12}
13
14/// Errors that can occur during subscription state transitions.
15#[derive(Debug, thiserror::Error, PartialEq, Eq)]
16pub enum SubscriptionError {
17    /// An event was received that is not valid for the current state.
18    #[error("invalid transition from {from:?} on event {event}")]
19    InvalidTransition {
20        /// The state the machine was in when the invalid event arrived.
21        from: SubscriptionState,
22        /// The name of the event that was rejected.
23        event: String,
24    },
25}
26
27/// Pure state machine for a MoQT subscription (draft-07).
28/// Transitions: Idle → Subscribing → Active → Done.
29pub struct SubscriptionStateMachine {
30    state: SubscriptionState,
31}
32
33impl Default for SubscriptionStateMachine {
34    fn default() -> Self {
35        Self::new()
36    }
37}
38
39impl SubscriptionStateMachine {
40    /// Creates a new state machine in the [`SubscriptionState::Idle`] state.
41    pub fn new() -> Self {
42        Self { state: SubscriptionState::Idle }
43    }
44
45    /// Returns the current state of the subscription.
46    pub fn state(&self) -> SubscriptionState {
47        self.state
48    }
49
50    /// Idle → Subscribing (SUBSCRIBE sent).
51    pub fn on_subscribe_sent(&mut self) -> Result<(), SubscriptionError> {
52        if self.state == SubscriptionState::Idle {
53            self.state = SubscriptionState::Subscribing;
54            Ok(())
55        } else {
56            Err(SubscriptionError::InvalidTransition {
57                from: self.state,
58                event: "on_subscribe_sent".to_string(),
59            })
60        }
61    }
62
63    /// Subscribing → Active (SUBSCRIBE_OK received).
64    pub fn on_subscribe_ok(&mut self) -> Result<(), SubscriptionError> {
65        if self.state == SubscriptionState::Subscribing {
66            self.state = SubscriptionState::Active;
67            Ok(())
68        } else {
69            Err(SubscriptionError::InvalidTransition {
70                from: self.state,
71                event: "on_subscribe_ok".to_string(),
72            })
73        }
74    }
75
76    /// Subscribing → Done (SUBSCRIBE_ERROR received).
77    pub fn on_subscribe_error(&mut self) -> Result<(), SubscriptionError> {
78        if self.state == SubscriptionState::Subscribing {
79            self.state = SubscriptionState::Done;
80            Ok(())
81        } else {
82            Err(SubscriptionError::InvalidTransition {
83                from: self.state,
84                event: "on_subscribe_error".to_string(),
85            })
86        }
87    }
88
89    /// Active → Done (UNSUBSCRIBE sent).
90    pub fn on_unsubscribe(&mut self) -> Result<(), SubscriptionError> {
91        if self.state == SubscriptionState::Active {
92            self.state = SubscriptionState::Done;
93            Ok(())
94        } else {
95            Err(SubscriptionError::InvalidTransition {
96                from: self.state,
97                event: "on_unsubscribe".to_string(),
98            })
99        }
100    }
101
102    /// SUBSCRIBE_UPDATE received -- a self-transition, from Subscribing as well as
103    /// from Active.
104    ///
105    /// Section 6.5 orders an update against the subscription rather than
106    /// against the subscription's answer. All it asks of the identifier is
107    /// that it already name something: "This MUST match an existing Subscribe
108    /// ID" — and a Subscribe ID exists from the moment the SUBSCRIBE carrying
109    /// it is sent, not from the moment it is answered.
110    ///
111    /// So a peer that sends SUBSCRIBE and SUBSCRIBE_UPDATE back to back breaks no
112    /// rule this draft states, and an update arriving before the answer leaves
113    /// the subscription where it found it. `Idle` and `Done` are still refused:
114    /// in neither does the subscription an update names exist.
115    pub fn on_subscribe_update(&mut self) -> Result<(), SubscriptionError> {
116        if matches!(self.state, SubscriptionState::Subscribing | SubscriptionState::Active) {
117            Ok(())
118        } else {
119            Err(SubscriptionError::InvalidTransition {
120                from: self.state,
121                event: "on_subscribe_update".to_string(),
122            })
123        }
124    }
125
126    /// Active → Done (SUBSCRIBE_DONE received — publisher finished, and `Done` unchanged).
127    ///
128    /// # Why `Done` is not refused
129    ///
130    /// Because UNSUBSCRIBE is usually what put the subscription there, and this
131    /// message is what a publisher is meant to answer one with. A subscriber
132    /// that withdraws is told the subscription has ended, with a code saying it
133    /// was its own doing — so `on_unsubscribe` followed by `on_subscribe_done` is
134    /// the ordinary end of a subscription rather than a peer misbehaving.
135    ///
136    /// Refusing the second half would make a conforming relay's last message
137    /// read as a protocol error against this endpoint's own bookkeeping — an
138    /// `invalid transition from Done` raised against this endpoint, not the
139    /// relay, and so a wall rather than a finding about the peer.
140    ///
141    /// `Idle` and `Subscribing` are still refused. In neither is there an active
142    /// subscription for this message to end.
143    pub fn on_subscribe_done(&mut self) -> Result<(), SubscriptionError> {
144        match self.state {
145            SubscriptionState::Active => {
146                self.state = SubscriptionState::Done;
147                Ok(())
148            }
149            SubscriptionState::Done => Ok(()),
150            _ => Err(SubscriptionError::InvalidTransition {
151                from: self.state,
152                event: "on_subscribe_done".to_string(),
153            }),
154        }
155    }
156}
157
158/// The same six transitions, named for the end that sees them.
159///
160/// A subscription this endpoint publishes runs through the states in the same
161/// order as one it subscribes to, with every message going the other way: the
162/// SUBSCRIBE arrives instead of leaving, the answer leaves instead of
163/// arriving. Sharing the transitions and not the names is what lets a refusal
164/// say which event was refused, rather than naming the mirror image of it.
165impl SubscriptionStateMachine {
166    /// Idle -> Subscribing (SUBSCRIBE received from a subscribing peer).
167    pub fn on_subscribe_received(&mut self) -> Result<(), SubscriptionError> {
168        self.on_subscribe_sent().map_err(|_| SubscriptionError::InvalidTransition {
169            from: self.state(),
170            event: "on_subscribe_received".to_string(),
171        })
172    }
173
174    /// Subscribing -> Active (SUBSCRIBE_OK sent to the subscribing peer).
175    pub fn on_subscribe_ok_sent(&mut self) -> Result<(), SubscriptionError> {
176        self.on_subscribe_ok().map_err(|_| SubscriptionError::InvalidTransition {
177            from: self.state(),
178            event: "on_subscribe_ok_sent".to_string(),
179        })
180    }
181
182    /// Subscribing -> Done (SUBSCRIBE_ERROR sent to the subscribing peer).
183    pub fn on_subscribe_error_sent(&mut self) -> Result<(), SubscriptionError> {
184        self.on_subscribe_error().map_err(|_| SubscriptionError::InvalidTransition {
185            from: self.state(),
186            event: "on_subscribe_error_sent".to_string(),
187        })
188    }
189
190    /// Active -> Done (UNSUBSCRIBE received from the subscribing peer).
191    pub fn on_unsubscribe_received(&mut self) -> Result<(), SubscriptionError> {
192        self.on_unsubscribe().map_err(|_| SubscriptionError::InvalidTransition {
193            from: self.state(),
194            event: "on_unsubscribe_received".to_string(),
195        })
196    }
197
198    /// Active -> Done (SUBSCRIBE_DONE sent to the subscribing peer), and `Done`
199    /// unchanged — this endpoint answers a peer's UNSUBSCRIBE with this
200    /// message, and the withdrawal it answers has already recorded the end.
201    /// See [`SubscriptionStateMachine::on_subscribe_done`], whose tolerance this inherits.
202    pub fn on_subscribe_done_sent(&mut self) -> Result<(), SubscriptionError> {
203        self.on_subscribe_done().map_err(|_| SubscriptionError::InvalidTransition {
204            from: self.state(),
205            event: "on_subscribe_done_sent".to_string(),
206        })
207    }
208
209    /// Subscribing or Active, unchanged (SUBSCRIBE_UPDATE received from the subscribing peer).
210    pub fn on_subscribe_update_received(&mut self) -> Result<(), SubscriptionError> {
211        self.on_subscribe_update().map_err(|_| SubscriptionError::InvalidTransition {
212            from: self.state(),
213            event: "on_subscribe_update_received".to_string(),
214        })
215    }
216}