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}