moqtap_client/draft17/publish.rs
1/// Publish lifecycle states (publisher side).
2#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3pub enum PublishState {
4 /// Initial state before any PUBLISH message is sent.
5 Idle,
6 /// PUBLISH has been sent; awaiting OK or ERROR.
7 Publishing,
8 /// PUBLISH_OK received; the track is being published.
9 Active,
10 /// Publish has ended (error, cancellation, or PUBLISH_DONE sent).
11 Done,
12}
13
14/// Errors that can occur during publish state transitions.
15#[derive(Debug, thiserror::Error, PartialEq, Eq)]
16pub enum PublishError {
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: PublishState,
22 /// The name of the event that was rejected.
23 event: String,
24 },
25}
26
27/// Pure state machine for a MoQT publish request (publisher side).
28/// Transitions: Idle -> Publishing -> Active -> Done.
29pub struct PublishStateMachine {
30 state: PublishState,
31}
32
33impl Default for PublishStateMachine {
34 fn default() -> Self {
35 Self::new()
36 }
37}
38
39impl PublishStateMachine {
40 /// Creates a new state machine in the [`PublishState::Idle`] state.
41 pub fn new() -> Self {
42 Self { state: PublishState::Idle }
43 }
44
45 /// Returns the current state of the publish request.
46 pub fn state(&self) -> PublishState {
47 self.state
48 }
49
50 /// Idle -> Publishing (PUBLISH sent).
51 pub fn on_publish_sent(&mut self) -> Result<(), PublishError> {
52 if self.state == PublishState::Idle {
53 self.state = PublishState::Publishing;
54 Ok(())
55 } else {
56 Err(PublishError::InvalidTransition {
57 from: self.state,
58 event: "on_publish_sent".to_string(),
59 })
60 }
61 }
62
63 /// Publishing -> Active (PUBLISH_OK received).
64 pub fn on_publish_ok(&mut self) -> Result<(), PublishError> {
65 if self.state == PublishState::Publishing {
66 self.state = PublishState::Active;
67 Ok(())
68 } else {
69 Err(PublishError::InvalidTransition {
70 from: self.state,
71 event: "on_publish_ok".to_string(),
72 })
73 }
74 }
75
76 /// Publishing -> Done (REQUEST_ERROR received).
77 pub fn on_publish_error(&mut self) -> Result<(), PublishError> {
78 if self.state == PublishState::Publishing {
79 self.state = PublishState::Done;
80 Ok(())
81 } else {
82 Err(PublishError::InvalidTransition {
83 from: self.state,
84 event: "on_publish_error".to_string(),
85 })
86 }
87 }
88
89 /// Active -> Done (PUBLISH_DONE sent by publisher).
90 pub fn on_publish_done_sent(&mut self) -> Result<(), PublishError> {
91 if self.state == PublishState::Active {
92 self.state = PublishState::Done;
93 Ok(())
94 } else {
95 Err(PublishError::InvalidTransition {
96 from: self.state,
97 event: "on_publish_done_sent".to_string(),
98 })
99 }
100 }
101
102 /// Publishing | Active -> Done, Done -> Done (this request's stream was
103 /// cancelled).
104 ///
105 /// PUBLISH_DONE ends a publication the receiver accepted. This is the
106 /// request itself being withdrawn, which this draft puts at the stream —
107 /// Section 3.3.1: "Once a request
108 /// stream has been opened, the request MAY be cancelled by either endpoint."
109 ///
110 /// `Idle` is refused, on the other half of the same sentence: nothing has
111 /// been written, so there is no stream to terminate. `Done` stays `Done` —
112 /// nothing finishes a request stream's send half on the ordinary path, so a
113 /// caller that walks away from a request that has already ended still
114 /// resets the stream, and that reset is an ordinary end rather than a
115 /// fault.
116 pub fn on_request_cancelled(&mut self) -> Result<(), PublishError> {
117 match self.state {
118 PublishState::Publishing | PublishState::Active => {
119 self.state = PublishState::Done;
120 Ok(())
121 }
122 PublishState::Done => Ok(()),
123 PublishState::Idle => Err(PublishError::InvalidTransition {
124 from: self.state,
125 event: "on_request_cancelled".to_string(),
126 }),
127 }
128 }
129}