Skip to main content

moqtap_client/draft19/
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, cancellation, or PUBLISH_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.
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 (REQUEST_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    /// Subscribing | Active -> Done, Done -> Done (this subscription's request
90    /// stream was cancelled).
91    ///
92    /// This draft has no UNSUBSCRIBE message. Section 3.3.3: "Once a request
93    /// stream has been opened, the request MAY be cancelled by either endpoint."
94    ///
95    /// `Subscribing` is accepted because the precondition is the stream being
96    /// open, and it is open from the SUBSCRIBE that opened it: a subscription
97    /// can be withdrawn before it is ever answered.
98    ///
99    /// `Idle` is refused, on the other half of the same sentence: nothing has
100    /// been written, so there is no stream to terminate. `Done` stays `Done` —
101    /// nothing finishes a request stream's send half on the ordinary path, so a
102    /// caller that walks away from a request that has already ended still
103    /// resets the stream, and that reset is an ordinary end rather than a
104    /// fault.
105    pub fn on_request_cancelled(&mut self) -> Result<(), SubscriptionError> {
106        match self.state {
107            SubscriptionState::Subscribing | SubscriptionState::Active => {
108                self.state = SubscriptionState::Done;
109                Ok(())
110            }
111            SubscriptionState::Done => Ok(()),
112            SubscriptionState::Idle => Err(SubscriptionError::InvalidTransition {
113                from: self.state,
114                event: "on_request_cancelled".to_string(),
115            }),
116        }
117    }
118
119    /// REQUEST_UPDATE received -- a self-transition, from Subscribing as well as
120    /// from Active.
121    ///
122    /// Section 10.9 orders an update against the request rather than against
123    /// the request's answer: the sender of a SUBSCRIBE "can later send a
124    /// REQUEST_UPDATE on the same bidi stream as the request to modify it",
125    /// where later is later than the SUBSCRIBE. The stream is open from the
126    /// moment the SUBSCRIBE opens it.
127    ///
128    /// Draft-19 contemplates the case outright. Section 10.3.1.7 bounds how
129    /// many REQUEST_UPDATEs may be outstanding on one request stream at a
130    /// time, a limit that means nothing to a sender that waits for each
131    /// answer before sending the next message.
132    ///
133    /// So a peer that sends SUBSCRIBE and REQUEST_UPDATE back to back breaks no
134    /// rule this draft states, and an update arriving before the answer leaves
135    /// the subscription where it found it. `Idle` and `Done` are still refused:
136    /// in neither does the subscription an update names exist.
137    pub fn on_subscribe_update(&mut self) -> Result<(), SubscriptionError> {
138        if matches!(self.state, SubscriptionState::Subscribing | SubscriptionState::Active) {
139            Ok(())
140        } else {
141            Err(SubscriptionError::InvalidTransition {
142                from: self.state,
143                event: "on_subscribe_update".to_string(),
144            })
145        }
146    }
147
148    /// Active -> Done (PUBLISH_DONE received).
149    pub fn on_publish_done(&mut self) -> Result<(), SubscriptionError> {
150        if self.state == SubscriptionState::Active {
151            self.state = SubscriptionState::Done;
152            Ok(())
153        } else {
154            Err(SubscriptionError::InvalidTransition {
155                from: self.state,
156                event: "on_publish_done".to_string(),
157            })
158        }
159    }
160}