Skip to main content

iridium_stomp/
subscription.rs

1use crate::connection::ConnError;
2use crate::connection::Connection;
3use crate::frame::Frame;
4use futures::stream::Stream;
5use std::pin::Pin;
6use std::task::{Context, Poll};
7use tokio::sync::mpsc;
8
9/// Options to configure a subscription. `headers` are forwarded to the
10/// broker as-is when sending the SUBSCRIBE frame and persisted locally so
11/// they can be re-sent on reconnect. This allows broker-specific durable
12/// subscription extensions to be used (for example ActiveMQ's durable
13/// subscription headers) while keeping the library generic.
14///
15/// # Durability
16///
17/// Durability is requested through `headers`, using whatever header the broker
18/// defines for it - STOMP itself has no durable-subscription concept. For
19/// example, ActiveMQ uses `activemq.subscriptionName`:
20///
21/// ```ignore
22/// let options = SubscriptionOptions {
23///     headers: vec![(
24///         "activemq.subscriptionName".to_string(),
25///         "my-durable-sub".to_string(),
26///     )],
27/// };
28/// ```
29///
30/// On brokers where a durable queue is declared administratively, such as
31/// RabbitMQ, pass that queue as the `destination` argument; nothing extra is
32/// needed here.
33#[derive(Debug, Clone, Default)]
34pub struct SubscriptionOptions {
35    /// Extra headers to include on the SUBSCRIBE frame.
36    pub headers: Vec<(String, String)>,
37}
38
39/// A lightweight handle returned from `Connection::subscribe` that packages the
40/// subscription id, destination, and the receiving side of the subscription.
41///
42/// The `Subscription` provides convenience helpers for acknowledging or
43/// negative-acknowledging messages; these delegate to the underlying
44/// `Connection` handle.
45pub struct Subscription {
46    id: String,
47    destination: String,
48    receiver: mpsc::Receiver<Frame>,
49    conn: Connection,
50    /// Set once the subscription has been unsubscribed explicitly, or once the
51    /// caller has taken ownership of the receiver via [`into_receiver`]. Guards
52    /// `Drop` against sending a second UNSUBSCRIBE, or any UNSUBSCRIBE when the
53    /// caller has chosen to keep driving the stream itself.
54    ///
55    /// [`into_receiver`]: Subscription::into_receiver
56    unsubscribed: bool,
57}
58
59impl Subscription {
60    pub(crate) fn new(
61        id: String,
62        destination: String,
63        receiver: mpsc::Receiver<Frame>,
64        conn: Connection,
65    ) -> Self {
66        Self {
67            id,
68            destination,
69            receiver,
70            conn,
71            unsubscribed: false,
72        }
73    }
74
75    /// Returns the local subscription id.
76    pub fn id(&self) -> &str {
77        &self.id
78    }
79
80    /// Returns the destination this subscription listens to.
81    pub fn destination(&self) -> &str {
82        &self.destination
83    }
84
85    /// Consume the `Subscription` and return the underlying receiver so the
86    /// caller can drive message handling directly.
87    ///
88    /// The subscription stays active: the caller now owns the stream, so `Drop`
89    /// does not send an UNSUBSCRIBE. Call [`Connection::unsubscribe`] with the
90    /// id if you later want to stop it.
91    ///
92    /// [`Connection::unsubscribe`]: crate::Connection::unsubscribe
93    pub fn into_receiver(mut self) -> mpsc::Receiver<Frame> {
94        // The caller keeps the stream, so suppress the drop-time UNSUBSCRIBE.
95        self.unsubscribed = true;
96        // `Subscription` implements `Drop`, so the receiver cannot be moved out
97        // directly (E0509). Swap in a throwaway and return the real one; the
98        // dummy is dropped harmlessly when `self` drops.
99        let (_dummy_tx, dummy_rx) = mpsc::channel(1);
100        std::mem::replace(&mut self.receiver, dummy_rx)
101    }
102
103    /// Acknowledge a message by its `message-id` header. Delegates to
104    /// `Connection::ack` using the local subscription id.
105    pub async fn ack(&self, message_id: &str) -> Result<(), ConnError> {
106        self.conn.ack(&self.id, message_id).await
107    }
108
109    /// Negative-acknowledge a message by its `message-id` header.
110    pub async fn nack(&self, message_id: &str) -> Result<(), ConnError> {
111        self.conn.nack(&self.id, message_id).await
112    }
113
114    /// Consume the subscription and unsubscribe from the server.
115    ///
116    /// This is a convenience that calls `Connection::unsubscribe` with the
117    /// local subscription id and drops the receiver. It confirms nothing beyond
118    /// queuing the UNSUBSCRIBE; the returned error means either the id was no
119    /// longer registered locally (for example the subscription had already been
120    /// abandoned after repeated broker errors) or the frame could not be queued.
121    /// Dropping the handle instead does the same thing on a best-effort basis
122    /// (see the `Drop` impl).
123    pub async fn unsubscribe(mut self) -> Result<(), ConnError> {
124        // Mark first so the upcoming drop does not send a second UNSUBSCRIBE.
125        self.unsubscribed = true;
126        self.conn.unsubscribe(&self.id).await
127    }
128}
129
130impl Drop for Subscription {
131    /// Best-effort UNSUBSCRIBE when the handle is dropped without an explicit
132    /// [`unsubscribe`](Subscription::unsubscribe).
133    ///
134    /// A dropped handle means the caller is done receiving, so the broker-side
135    /// subscription should stop rather than linger and keep delivering (and be
136    /// replayed on reconnect). `Drop` cannot `.await`, so this is best-effort
137    /// via [`Connection::unsubscribe_best_effort`]. It is skipped when the
138    /// subscription was already unsubscribed or when the receiver was handed off
139    /// through [`into_receiver`](Subscription::into_receiver).
140    fn drop(&mut self) {
141        if self.unsubscribed {
142            return;
143        }
144        self.conn.unsubscribe_best_effort(&self.id);
145    }
146}
147
148impl Stream for Subscription {
149    type Item = Frame;
150
151    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
152        // Safe to get a mutable reference because all fields of `Subscription`
153        // are `Unpin` (String, Receiver, Connection). We then delegate to the
154        // tokio mpsc receiver's `poll_recv` which returns `Poll<Option<T>>`.
155        let this = self.get_mut();
156        Pin::new(&mut this.receiver).poll_recv(cx)
157    }
158}