iridium_stomp/connection.rs
1use futures::{SinkExt, StreamExt, future};
2use std::collections::{HashMap, VecDeque};
3use std::sync::Arc;
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::time::Duration;
6use thiserror::Error;
7use tokio::net::TcpStream;
8use tokio::sync::{Mutex, broadcast, mpsc, oneshot};
9use tokio_util::codec::Framed;
10
11use crate::codec::{StompCodec, StompItem};
12use crate::frame::Frame;
13use crate::parser::DEFAULT_MAX_FRAME_SIZE;
14
15/// Configuration for STOMP heartbeat intervals.
16///
17/// Provides a type-safe way to configure heartbeat values instead of using
18/// raw strings. The `Display` implementation formats the value as required
19/// by the STOMP protocol ("send_ms,receive_ms").
20///
21/// # Example
22///
23/// ```
24/// use iridium_stomp::Heartbeat;
25///
26/// // Create a custom heartbeat configuration
27/// let hb = Heartbeat::new(5000, 10000);
28/// assert_eq!(hb.to_string(), "5000,10000");
29///
30/// // Use predefined configurations
31/// assert_eq!(Heartbeat::disabled().to_string(), "0,0");
32/// assert_eq!(Heartbeat::default().to_string(), "10000,10000");
33/// ```
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct Heartbeat {
36 /// Minimum interval (in milliseconds) between heartbeats the client can send.
37 /// A value of 0 means the client cannot send heartbeats.
38 pub send_ms: u32,
39
40 /// Minimum interval (in milliseconds) between heartbeats the client wants to receive.
41 /// A value of 0 means the client does not want to receive heartbeats.
42 pub receive_ms: u32,
43}
44
45impl Heartbeat {
46 /// Create a new heartbeat configuration with the specified intervals.
47 ///
48 /// # Arguments
49 ///
50 /// * `send_ms` - Minimum interval in milliseconds between heartbeats the client can send.
51 /// * `receive_ms` - Minimum interval in milliseconds between heartbeats the client wants to receive.
52 ///
53 /// # Example
54 ///
55 /// ```
56 /// use iridium_stomp::Heartbeat;
57 ///
58 /// let hb = Heartbeat::new(5000, 10000);
59 /// assert_eq!(hb.send_ms, 5000);
60 /// assert_eq!(hb.receive_ms, 10000);
61 /// ```
62 pub fn new(send_ms: u32, receive_ms: u32) -> Self {
63 Self {
64 send_ms,
65 receive_ms,
66 }
67 }
68
69 /// Create a heartbeat configuration that disables heartbeats entirely.
70 ///
71 /// This is equivalent to `Heartbeat::new(0, 0)`.
72 ///
73 /// # Example
74 ///
75 /// ```
76 /// use iridium_stomp::Heartbeat;
77 ///
78 /// let hb = Heartbeat::disabled();
79 /// assert_eq!(hb.send_ms, 0);
80 /// assert_eq!(hb.receive_ms, 0);
81 /// assert_eq!(hb.to_string(), "0,0");
82 /// ```
83 pub fn disabled() -> Self {
84 Self::new(0, 0)
85 }
86
87 /// Create a heartbeat configuration from a Duration for symmetric heartbeats.
88 ///
89 /// Both send and receive intervals will be set to the same value.
90 ///
91 /// The maximum supported Duration is approximately 49.7 days (u32::MAX milliseconds,
92 /// or 4,294,967,295 ms). If a larger Duration is provided, it will be clamped to
93 /// u32::MAX milliseconds to prevent overflow.
94 ///
95 /// # Example
96 ///
97 /// ```
98 /// use iridium_stomp::Heartbeat;
99 /// use std::time::Duration;
100 ///
101 /// let hb = Heartbeat::from_duration(Duration::from_secs(15));
102 /// assert_eq!(hb.send_ms, 15000);
103 /// assert_eq!(hb.receive_ms, 15000);
104 /// ```
105 pub fn from_duration(interval: Duration) -> Self {
106 let ms = interval.as_millis().min(u32::MAX as u128) as u32;
107 Self::new(ms, ms)
108 }
109}
110
111impl Default for Heartbeat {
112 /// Returns the default heartbeat configuration: 10 seconds for both send and receive.
113 fn default() -> Self {
114 Self::new(10000, 10000)
115 }
116}
117
118impl std::fmt::Display for Heartbeat {
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120 write!(f, "{},{}", self.send_ms, self.receive_ms)
121 }
122}
123
124/// Internal subscription entry stored for each destination.
125#[derive(Clone)]
126pub(crate) struct SubscriptionEntry {
127 pub(crate) id: String,
128 pub(crate) sender: mpsc::Sender<Frame>,
129 pub(crate) ack: String,
130 pub(crate) headers: Vec<(String, String)>,
131}
132
133/// Alias for the subscription dispatch map: destination -> list of
134/// `SubscriptionEntry`.
135pub(crate) type Subscriptions = HashMap<String, Vec<SubscriptionEntry>>;
136
137/// Alias for the pending map: subscription_id -> queue of (message-id, Frame).
138pub(crate) type PendingMap = HashMap<String, VecDeque<(String, Frame)>>;
139
140/// Internal type for resubscribe snapshot entries: (destination, id, ack, headers)
141pub(crate) type ResubEntry = (String, String, String, Vec<(String, String)>);
142
143/// Alias for pending receipt map: receipt-id -> oneshot sender to notify when resolved.
144pub(crate) type PendingReceipts = HashMap<String, oneshot::Sender<Result<(), ServerError>>>;
145
146/// Errors returned by `Connection` operations.
147#[derive(Error, Debug)]
148pub enum ConnError {
149 /// I/O-level error
150 #[error("io error: {0}")]
151 Io(#[from] std::io::Error),
152 /// Protocol-level error
153 #[error("protocol error: {0}")]
154 Protocol(String),
155 /// Receipt timeout error
156 #[error("receipt timeout: no RECEIPT received for '{0}' within timeout")]
157 ReceiptTimeout(String),
158 /// Server rejected the connection (e.g., authentication failure).
159 ///
160 /// This error is returned when the server sends an ERROR frame in response
161 /// to the CONNECT frame. Common causes include invalid credentials,
162 /// unauthorized access, or broker configuration issues.
163 #[error("server rejected connection: {0}")]
164 ServerRejected(ServerError),
165 /// A frame that requested a receipt was rejected by the broker via
166 /// an ERROR frame with a matching receipt-id. The connection may
167 /// still be usable depending on broker behavior.
168 #[error("frame rejected: {0}")]
169 FrameRejected(ServerError),
170}
171
172/// Represents an ERROR frame received from the STOMP server.
173///
174/// STOMP servers send ERROR frames to indicate protocol violations, authentication
175/// failures, or other server-side errors. After sending an ERROR frame, the server
176/// typically closes the connection.
177///
178/// # Example
179///
180/// ```ignore
181/// use iridium_stomp::ReceivedFrame;
182///
183/// while let Some(received) = conn.next_frame().await {
184/// match received {
185/// ReceivedFrame::Frame(frame) => {
186/// // Normal message processing
187/// }
188/// ReceivedFrame::Error(err) => {
189/// eprintln!("Server error: {}", err.message);
190/// if let Some(body) = &err.body {
191/// eprintln!("Details: {}", body);
192/// }
193/// break;
194/// }
195/// }
196/// }
197/// ```
198#[derive(Debug, Clone, PartialEq, Eq)]
199pub struct ServerError {
200 /// The error message from the `message` header.
201 pub message: String,
202
203 /// The error body, if present. Contains additional error details.
204 pub body: Option<String>,
205
206 /// The receipt-id if this error is in response to a specific frame.
207 pub receipt_id: Option<String>,
208
209 /// The original ERROR frame for access to additional headers.
210 pub frame: Frame,
211}
212
213impl ServerError {
214 /// Create a `ServerError` from an ERROR frame.
215 ///
216 /// This is primarily used internally but is public for testing and
217 /// advanced use cases where you need to construct a `ServerError` manually.
218 pub fn from_frame(frame: Frame) -> Self {
219 let message = frame
220 .get_header("message")
221 .unwrap_or("unknown error")
222 .to_string();
223
224 let body = if frame.body.is_empty() {
225 None
226 } else {
227 String::from_utf8(frame.body.clone()).ok()
228 };
229
230 let receipt_id = frame.get_header("receipt-id").map(|s| s.to_string());
231
232 Self {
233 message,
234 body,
235 receipt_id,
236 frame,
237 }
238 }
239}
240
241impl std::fmt::Display for ServerError {
242 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243 write!(f, "STOMP server error: {}", self.message)?;
244 if let Some(body) = &self.body {
245 write!(f, " - {}", body)?;
246 }
247 Ok(())
248 }
249}
250
251impl std::error::Error for ServerError {}
252
253/// The result of receiving a frame from the server.
254///
255/// STOMP servers can send either normal frames (MESSAGE, RECEIPT, etc.) or
256/// ERROR frames indicating a problem. This enum allows callers to handle
257/// both cases with pattern matching.
258///
259/// # Example
260///
261/// ```ignore
262/// use iridium_stomp::ReceivedFrame;
263///
264/// match conn.next_frame().await {
265/// Some(ReceivedFrame::Frame(frame)) => {
266/// println!("Got frame: {}", frame.command);
267/// }
268/// Some(ReceivedFrame::Error(err)) => {
269/// eprintln!("Server error: {}", err);
270/// }
271/// None => {
272/// println!("Connection closed");
273/// }
274/// }
275/// ```
276#[derive(Debug, Clone, PartialEq, Eq)]
277pub enum ReceivedFrame {
278 /// A normal STOMP frame (MESSAGE, RECEIPT, etc.)
279 Frame(Frame),
280 /// An ERROR frame from the server
281 Error(ServerError),
282}
283
284impl ReceivedFrame {
285 /// Returns `true` if this is an error frame.
286 pub fn is_error(&self) -> bool {
287 matches!(self, ReceivedFrame::Error(_))
288 }
289
290 /// Returns `true` if this is a normal frame.
291 pub fn is_frame(&self) -> bool {
292 matches!(self, ReceivedFrame::Frame(_))
293 }
294
295 /// Returns the frame if this is a normal frame, or `None` if it's an error.
296 pub fn into_frame(self) -> Option<Frame> {
297 match self {
298 ReceivedFrame::Frame(f) => Some(f),
299 ReceivedFrame::Error(_) => None,
300 }
301 }
302
303 /// Returns the error if this is an error frame, or `None` if it's a normal frame.
304 pub fn into_error(self) -> Option<ServerError> {
305 match self {
306 ReceivedFrame::Frame(_) => None,
307 ReceivedFrame::Error(e) => Some(e),
308 }
309 }
310}
311
312/// Subscription acknowledgement modes as defined by STOMP 1.2.
313#[derive(Debug, Clone, Copy, PartialEq, Eq)]
314pub enum AckMode {
315 /// Server considers the message delivered as soon as it is sent (no
316 /// explicit acknowledgement required from the client).
317 Auto,
318 /// Client must send an ACK frame; the ACK is cumulative — it
319 /// acknowledges all messages up to and including the specified one.
320 Client,
321 /// Client must send an ACK frame for each individual message.
322 ClientIndividual,
323}
324
325impl AckMode {
326 fn as_str(&self) -> &'static str {
327 match self {
328 AckMode::Auto => "auto",
329 AckMode::Client => "client",
330 AckMode::ClientIndividual => "client-individual",
331 }
332 }
333}
334
335/// Options for customizing the STOMP CONNECT frame.
336///
337/// Use this struct with `Connection::connect_with_options()` to set custom
338/// headers, specify supported STOMP versions, or configure broker-specific
339/// options like `client-id` for durable subscriptions.
340///
341/// # Validation
342///
343/// This struct performs minimal validation. Values are passed to the broker
344/// as-is, and invalid configurations will be rejected by the broker at
345/// connection time. Empty strings are technically accepted but may cause
346/// broker-specific errors.
347///
348/// # Custom Headers
349///
350/// Custom headers added via `header()` cannot override critical STOMP headers
351/// (`accept-version`, `host`, `login`, `passcode`, `heart-beat`, `client-id`).
352/// Such headers are silently ignored. Use the dedicated builder methods to
353/// set these values.
354///
355/// # Example
356///
357/// ```ignore
358/// use iridium_stomp::{Connection, ConnectOptions};
359///
360/// let options = ConnectOptions::default()
361/// .client_id("my-durable-client")
362/// .host("my-vhost")
363/// .header("custom-header", "value");
364///
365/// let conn = Connection::connect_with_options(
366/// "localhost:61613",
367/// "guest",
368/// "guest",
369/// Connection::DEFAULT_HEARTBEAT,
370/// options,
371/// ).await?;
372/// ```
373#[derive(Clone, Default)]
374pub struct ConnectOptions {
375 /// STOMP version(s) to accept (e.g., "1.2" or "1.0,1.1,1.2").
376 /// Defaults to "1.2" if not set.
377 pub accept_version: Option<String>,
378
379 /// Client ID for durable subscriptions (required by ActiveMQ, etc.).
380 pub client_id: Option<String>,
381
382 /// Virtual host header value. Defaults to "/" if not set.
383 pub host: Option<String>,
384
385 /// Additional custom headers to include in the CONNECT frame.
386 /// Note: Headers that would override critical STOMP headers are ignored.
387 pub headers: Vec<(String, String)>,
388
389 /// Optional channel to receive heartbeat notifications.
390 /// When set, the connection will send a `()` on this channel each time
391 /// a heartbeat is received from the server.
392 pub heartbeat_tx: Option<mpsc::Sender<()>>,
393
394 /// How long `Connection::close` waits for the broker's RECEIPT after it
395 /// sends DISCONNECT. Defaults to
396 /// [`Connection::DEFAULT_DISCONNECT_TIMEOUT`] if not set.
397 pub disconnect_timeout: Option<Duration>,
398
399 /// Upper bound on the initial connect-and-handshake before
400 /// [`Connection::connect_with_options`] gives up. When `None` (the
401 /// default), an unreachable broker is retried indefinitely with backoff.
402 /// When set, the whole operation is bounded and, on expiry, the last
403 /// error encountered is returned.
404 pub connect_timeout: Option<Duration>,
405
406 /// Largest inbound frame to accept, in bytes. When `None`, the default is
407 /// [`crate::parser::DEFAULT_MAX_FRAME_SIZE`] (16 MiB). A frame larger than
408 /// this — whether via an oversized `content-length` or a body that never
409 /// terminates — is rejected as a `ConnError::Io`, so a malicious or buggy
410 /// broker cannot exhaust client memory or panic the decoder.
411 pub max_frame_size: Option<usize>,
412}
413
414impl std::fmt::Debug for ConnectOptions {
415 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
416 f.debug_struct("ConnectOptions")
417 .field("accept_version", &self.accept_version)
418 .field("client_id", &self.client_id)
419 .field("host", &self.host)
420 .field("headers", &self.headers)
421 .field(
422 "heartbeat_tx",
423 &self.heartbeat_tx.as_ref().map(|_| "Some(...)"),
424 )
425 .field("disconnect_timeout", &self.disconnect_timeout)
426 .field("connect_timeout", &self.connect_timeout)
427 .field("max_frame_size", &self.max_frame_size)
428 .finish()
429 }
430}
431
432impl ConnectOptions {
433 /// Create a new `ConnectOptions` with default values.
434 pub fn new() -> Self {
435 Self::default()
436 }
437
438 /// Set the STOMP version(s) to accept (builder style).
439 ///
440 /// Examples: "1.2", "1.1,1.2", "1.0,1.1,1.2"
441 pub fn accept_version(mut self, version: impl Into<String>) -> Self {
442 self.accept_version = Some(version.into());
443 self
444 }
445
446 /// Set the client ID for durable subscriptions (builder style).
447 ///
448 /// Required by some brokers (e.g., ActiveMQ) for durable topic subscriptions.
449 pub fn client_id(mut self, id: impl Into<String>) -> Self {
450 self.client_id = Some(id.into());
451 self
452 }
453
454 /// Set the virtual host (builder style).
455 ///
456 /// Defaults to "/" if not set.
457 pub fn host(mut self, host: impl Into<String>) -> Self {
458 self.host = Some(host.into());
459 self
460 }
461
462 /// Add a custom header to the CONNECT frame (builder style).
463 pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
464 self.headers.push((key.into(), value.into()));
465 self
466 }
467
468 /// Set how long [`Connection::close`] waits for the broker to confirm the
469 /// DISCONNECT (builder style).
470 ///
471 /// Defaults to [`Connection::DEFAULT_DISCONNECT_TIMEOUT`]. The socket is
472 /// torn down when the timeout expires regardless, so this bounds how long a
473 /// clean shutdown may take against an unresponsive broker; it does not
474 /// decide whether the connection closes.
475 ///
476 /// # Example
477 ///
478 /// ```ignore
479 /// let options = ConnectOptions::default()
480 /// .disconnect_timeout(Duration::from_secs(2));
481 /// ```
482 pub fn disconnect_timeout(mut self, timeout: Duration) -> Self {
483 self.disconnect_timeout = Some(timeout);
484 self
485 }
486
487 /// Bound the initial connect and STOMP handshake (builder style).
488 ///
489 /// By default [`Connection::connect_with_options`] retries an unreachable
490 /// broker indefinitely with exponential backoff, which is the right choice
491 /// for a long-lived service that should wait for its broker to come up. It
492 /// is the wrong choice for a CLI tool or one-shot script pointed at a
493 /// misconfigured address: without a bound it hangs forever. Set this to cap
494 /// the whole operation.
495 ///
496 /// When the timeout expires, `connect_with_options` returns the last error
497 /// it encountered — a [`ConnError::Io`], or a [`ConnError::Protocol`] such
498 /// as a broker that closed the socket mid-handshake — or a synthesized
499 /// [`std::io::ErrorKind::TimedOut`] if no attempt had produced one yet. A
500 /// `ConnError::ServerRejected` still fails immediately, before the bound is
501 /// consulted, because retrying bad credentials is pointless.
502 ///
503 /// # Example
504 ///
505 /// ```ignore
506 /// let options = ConnectOptions::default()
507 /// .connect_timeout(Duration::from_secs(60));
508 /// ```
509 pub fn connect_timeout(mut self, timeout: Duration) -> Self {
510 self.connect_timeout = Some(timeout);
511 self
512 }
513
514 /// Set the largest inbound frame to accept, in bytes (builder style).
515 ///
516 /// Defaults to [`crate::parser::DEFAULT_MAX_FRAME_SIZE`] (16 MiB). A frame
517 /// exceeding this — an oversized `content-length`, or a body that never
518 /// terminates — is rejected rather than buffered or allocated, so a
519 /// malicious or buggy broker cannot exhaust client memory. Raise it if you
520 /// legitimately exchange larger messages.
521 ///
522 /// # Example
523 ///
524 /// ```ignore
525 /// let options = ConnectOptions::default()
526 /// .max_frame_size(64 * 1024 * 1024);
527 /// ```
528 pub fn max_frame_size(mut self, max_frame_size: usize) -> Self {
529 self.max_frame_size = Some(max_frame_size);
530 self
531 }
532
533 /// Set a channel to receive heartbeat notifications (builder style).
534 ///
535 /// When set, the connection will send a `()` on this channel each time
536 /// a heartbeat is received from the server. This is useful for CLI tools
537 /// or monitoring applications that want to display heartbeat status.
538 ///
539 /// # Note
540 ///
541 /// Notifications are sent using `try_send()` to avoid blocking the
542 /// connection's background task. If the channel buffer is full,
543 /// notifications will be silently dropped. Use a sufficiently sized
544 /// channel buffer (e.g., 16) to avoid missing notifications.
545 ///
546 /// # Example
547 ///
548 /// ```ignore
549 /// use tokio::sync::mpsc;
550 /// use iridium_stomp::ConnectOptions;
551 ///
552 /// let (tx, mut rx) = mpsc::channel(16);
553 /// let options = ConnectOptions::default()
554 /// .heartbeat_notify(tx);
555 ///
556 /// // In another task:
557 /// while rx.recv().await.is_some() {
558 /// println!("Heartbeat received!");
559 /// }
560 /// ```
561 pub fn heartbeat_notify(mut self, tx: mpsc::Sender<()>) -> Self {
562 self.heartbeat_tx = Some(tx);
563 self
564 }
565}
566
567/// Parse the STOMP `heart-beat` header value (format: "cx,cy").
568///
569/// Parameters
570/// - `header`: header string from the server or client (for example
571/// "10000,10000"). The values represent milliseconds.
572///
573/// Returns a tuple `(cx, cy)` where each value is the heartbeat interval in
574/// milliseconds. Missing or invalid fields default to `0`.
575pub fn parse_heartbeat_header(header: &str) -> (u64, u64) {
576 let mut parts = header.split(',');
577 let cx = parts
578 .next()
579 .and_then(|s| s.trim().parse::<u64>().ok())
580 .unwrap_or(0);
581 let cy = parts
582 .next()
583 .and_then(|s| s.trim().parse::<u64>().ok())
584 .unwrap_or(0);
585 (cx, cy)
586}
587
588/// Negotiate heartbeat intervals between client and server.
589///
590/// Parameters
591/// - `client_out`: client's desired outgoing heartbeat interval in
592/// milliseconds (how often the client will send heartbeats).
593/// - `client_in`: client's desired incoming heartbeat interval in
594/// milliseconds (how often the client expects to receive heartbeats).
595/// - `server_out`: server's advertised outgoing interval in milliseconds.
596/// - `server_in`: server's advertised incoming interval in milliseconds.
597///
598/// Returns `(outgoing, incoming)` where each element is `Some(Duration)` if
599/// heartbeats are enabled in that direction, or `None` if disabled. The
600/// negotiated interval uses the STOMP rule of taking the maximum of the
601/// corresponding client and server values.
602pub fn negotiate_heartbeats(
603 client_out: u64,
604 client_in: u64,
605 server_out: u64,
606 server_in: u64,
607) -> (Option<Duration>, Option<Duration>) {
608 let negotiated_out_ms = std::cmp::max(client_out, server_in);
609 let negotiated_in_ms = std::cmp::max(client_in, server_out);
610
611 let outgoing = if negotiated_out_ms == 0 {
612 None
613 } else {
614 Some(Duration::from_millis(negotiated_out_ms))
615 };
616 let incoming = if negotiated_in_ms == 0 {
617 None
618 } else {
619 Some(Duration::from_millis(negotiated_in_ms))
620 };
621 (outgoing, incoming)
622}
623
624/// Extract the destination from an ERROR frame.
625///
626/// Tries multiple strategies:
627/// 1. Check for a `destination` header (some brokers include it)
628/// 2. Parse the error message/body for `/topic/...` or `/queue/...` patterns
629///
630/// Returns `None` if no destination can be identified.
631fn extract_destination_from_error(frame: &Frame) -> Option<String> {
632 // Strategy 1: Check for destination header
633 if let Some(dest) = frame.get_header("destination") {
634 return Some(dest.to_string());
635 }
636
637 // Strategy 2: Look for destination pattern in message header or body
638 let message = frame.get_header("message").unwrap_or("");
639 let body = String::from_utf8_lossy(&frame.body);
640
641 // Combine message and body for searching
642 let text = format!("{} {}", message, body);
643
644 // Look for /topic/ or /queue/ patterns
645 for prefix in ["/topic/", "/queue/"] {
646 if let Some(start) = text.find(prefix) {
647 // Extract until whitespace, comma, quote, or end of string
648 let rest = &text[start..];
649 let end = rest
650 .find(|c: char| c.is_whitespace() || c == ',' || c == '"' || c == '\'')
651 .unwrap_or(rest.len());
652 if end > prefix.len() {
653 return Some(rest[..end].to_string());
654 }
655 }
656 }
657
658 None
659}
660
661/// Extract subscription ID from an ERROR frame message.
662///
663/// Looks for patterns like "subscription 1" or "subscription sub-1" in the
664/// error message or body. Artemis uses this format for subscription errors.
665fn extract_subscription_id_from_error(frame: &Frame) -> Option<String> {
666 let message = frame.get_header("message").unwrap_or("");
667 let body = String::from_utf8_lossy(&frame.body);
668 let text = format!("{} {}", message, body);
669
670 // Look for "subscription X" pattern (Artemis format)
671 if let Some(idx) = text.to_lowercase().find("subscription ") {
672 let rest = &text[idx + 13..]; // "subscription " is 13 chars
673 // Extract the subscription ID (could be numeric or alphanumeric like "sub-1")
674 let end = rest
675 .find(|c: char| c.is_whitespace() || c == ',' || c == '"' || c == '\'')
676 .unwrap_or(rest.len());
677 if end > 0 {
678 return Some(rest[..end].to_string());
679 }
680 }
681
682 None
683}
684
685/// Look up a destination by subscription ID in the subscriptions map.
686async fn lookup_destination_by_sub_id(
687 sub_id: &str,
688 subscriptions: &Arc<Mutex<Subscriptions>>,
689) -> Option<String> {
690 let map = subscriptions.lock().await;
691 for (dest, entries) in map.iter() {
692 for entry in entries {
693 if entry.id == sub_id {
694 return Some(dest.clone());
695 }
696 }
697 }
698 None
699}
700
701/// Deliver a MESSAGE frame to one subscriber and report whether its entry
702/// should be kept.
703///
704/// The distinction the two delivery paths previously got wrong: a `Full`
705/// channel is a slow-but-live consumer — drop the message, keep the
706/// subscription — while a `Closed` channel means the receiving `Subscription`
707/// was dropped, so the entry is dead and must be pruned. Returning `false` only
708/// on `Closed` gives both paths one consistent rule and is the backstop that
709/// reaps a dropped subscription whose `Drop` could not take the registry lock.
710fn deliver_and_keep(entry: &SubscriptionEntry, frame: &Frame) -> bool {
711 match entry.sender.try_send(frame.clone()) {
712 Ok(()) => true,
713 Err(mpsc::error::TrySendError::Full(_)) => true,
714 Err(mpsc::error::TrySendError::Closed(_)) => false,
715 }
716}
717
718/// High-level connection object that manages a single TCP/STOMP connection.
719///
720/// The `Connection` spawns a background task that maintains the TCP transport,
721/// sends/receives STOMP frames using `StompCodec`, negotiates heartbeats, and
722/// performs simple reconnect logic with exponential backoff.
723#[derive(Clone)]
724pub struct Connection {
725 outbound_tx: mpsc::Sender<StompItem>,
726 /// The inbound receiver is shared behind a mutex so the `Connection`
727 /// handle may be cloned and callers can call `next_frame` concurrently.
728 inbound_rx: Arc<Mutex<mpsc::Receiver<Frame>>>,
729 shutdown_tx: broadcast::Sender<()>,
730 /// Map of destination -> list of (subscription id, sender) for dispatching
731 /// inbound MESSAGE frames to subscribers.
732 subscriptions: Arc<Mutex<Subscriptions>>,
733 /// Monotonic counter used to allocate subscription ids.
734 sub_id_counter: Arc<AtomicU64>,
735 /// Pending messages awaiting ACK/NACK from the application.
736 ///
737 /// Organized by subscription id. For `client` ack mode the ACK is
738 /// cumulative: acknowledging message `M` for subscription `S` acknowledges
739 /// all messages previously delivered for `S` up to and including `M`.
740 /// For `client-individual` the ACK/NACK applies only to the single
741 /// message.
742 pending: Arc<Mutex<PendingMap>>,
743 /// Pending receipt confirmations.
744 ///
745 /// When a frame is sent with a `receipt` header, the receipt-id is stored
746 /// here with a oneshot sender. When the server responds with a RECEIPT
747 /// frame, the sender is notified.
748 pending_receipts: Arc<Mutex<PendingReceipts>>,
749 /// How long `close` waits for the broker's RECEIPT after DISCONNECT.
750 disconnect_timeout: Duration,
751}
752
753/// A pending receipt confirmation for a frame sent with
754/// [`Connection::send_frame_with_receipt`].
755///
756/// The handle owns the receiving half of the confirmation channel from the
757/// moment the frame is queued for sending. A RECEIPT that arrives before
758/// [`wait`](ReceiptHandle::wait) is called is therefore buffered in the channel
759/// rather than dropped, so the confirmation cannot be lost to a fast broker.
760///
761/// Handles are independent, so several frames may be sent before any of them is
762/// awaited:
763///
764/// ```ignore
765/// let mut handles = Vec::new();
766/// for order in orders {
767/// handles.push(conn.send_frame_with_receipt(order).await?);
768/// }
769/// for handle in handles {
770/// handle.wait(Duration::from_secs(5)).await?;
771/// }
772/// ```
773#[derive(Debug)]
774pub struct ReceiptHandle {
775 /// The client-generated receipt id carried by the sent frame.
776 receipt_id: String,
777 /// Resolves when the broker answers with RECEIPT or a matching ERROR.
778 rx: oneshot::Receiver<Result<(), ServerError>>,
779 /// Shared registry, used to deregister this receipt if the wait times out.
780 pending_receipts: Arc<Mutex<PendingReceipts>>,
781}
782
783impl ReceiptHandle {
784 /// The receipt id the client generated for this frame.
785 ///
786 /// This is the value sent in the frame's `receipt` header and echoed by the
787 /// broker in the `receipt-id` header of its response.
788 pub fn receipt_id(&self) -> &str {
789 &self.receipt_id
790 }
791
792 /// Wait for the broker to confirm the frame.
793 ///
794 /// # Parameters
795 /// - `timeout`: maximum time to wait for the broker's response.
796 ///
797 /// # Returns
798 /// `Ok(())` if the broker sent a RECEIPT, `Err(ConnError::FrameRejected)`
799 /// if it answered with an ERROR carrying this receipt id, or
800 /// `Err(ConnError::ReceiptTimeout)` if the timeout expired first.
801 pub async fn wait(self, timeout: Duration) -> Result<(), ConnError> {
802 let ReceiptHandle {
803 receipt_id,
804 rx,
805 pending_receipts,
806 } = self;
807
808 match tokio::time::timeout(timeout, rx).await {
809 Ok(Ok(Ok(()))) => Ok(()),
810 Ok(Ok(Err(err))) => Err(ConnError::FrameRejected(err)),
811 Ok(Err(_)) => {
812 // Sender dropped without a response - connection likely gone.
813 Err(ConnError::Protocol(
814 "receipt channel closed unexpectedly".into(),
815 ))
816 }
817 Err(_) => {
818 let mut receipts = pending_receipts.lock().await;
819 receipts.remove(&receipt_id);
820 Err(ConnError::ReceiptTimeout(receipt_id))
821 }
822 }
823 }
824}
825
826impl Connection {
827 /// Heartbeat value that disables heartbeats entirely.
828 ///
829 /// Use this when you don't want the client or server to send heartbeats.
830 /// Note that some brokers may still require heartbeats for long-lived connections.
831 ///
832 /// # Example
833 ///
834 /// ```ignore
835 /// let conn = Connection::connect(
836 /// "localhost:61613",
837 /// "guest",
838 /// "guest",
839 /// Connection::NO_HEARTBEAT,
840 /// ).await?;
841 /// ```
842 pub const NO_HEARTBEAT: &'static str = "0,0";
843
844 /// Default heartbeat value: 10 seconds for both send and receive.
845 ///
846 /// This is a reasonable default for most applications. The actual heartbeat
847 /// interval will be negotiated with the server (taking the maximum of client
848 /// and server preferences).
849 ///
850 /// # Example
851 ///
852 /// ```ignore
853 /// let conn = Connection::connect(
854 /// "localhost:61613",
855 /// "guest",
856 /// "guest",
857 /// Connection::DEFAULT_HEARTBEAT,
858 /// ).await?;
859 /// ```
860 pub const DEFAULT_HEARTBEAT: &'static str = "10000,10000";
861
862 /// How long [`close`](Connection::close) waits for the broker to confirm the
863 /// DISCONNECT before giving up and tearing the socket down anyway.
864 ///
865 /// Override per connection with
866 /// [`ConnectOptions::disconnect_timeout`].
867 pub const DEFAULT_DISCONNECT_TIMEOUT: Duration = Duration::from_secs(5);
868
869 /// Establish a connection to the STOMP server at `addr` with the given
870 /// credentials and heartbeat header string (e.g. "10000,10000").
871 ///
872 /// This is a convenience wrapper around `connect_with_options()` that uses
873 /// default options (STOMP 1.2, host="/", no client-id).
874 ///
875 /// If the broker is unreachable, this method retries with exponential
876 /// backoff (1s → 2s → 4s → … → 30s cap). Authentication errors
877 /// (`ConnError::ServerRejected`) fail immediately. See
878 /// [`connect_with_options`](Self::connect_with_options) for full details.
879 ///
880 /// Parameters
881 /// - `addr`: TCP address (host:port) of the STOMP server.
882 /// - `login`: login username for STOMP `CONNECT`.
883 /// - `passcode`: passcode for STOMP `CONNECT`.
884 /// - `client_hb`: client's `heart-beat` header value ("cx,cy" in
885 /// milliseconds) that will be sent in the `CONNECT` frame.
886 ///
887 /// Returns a `Connection` which provides `send`, `send_frame`,
888 /// `next_frame`, and `close` helpers. The detailed connection handling
889 /// (I/O, heartbeats, reconnects) runs on a background task spawned by
890 /// this method.
891 pub async fn connect(
892 addr: &str,
893 login: &str,
894 passcode: &str,
895 client_hb: &str,
896 ) -> Result<Self, ConnError> {
897 Self::connect_with_options(addr, login, passcode, client_hb, ConnectOptions::default())
898 .await
899 }
900
901 /// Establish a connection to the STOMP server with custom options.
902 ///
903 /// Use this method when you need to set a custom `client-id` (for durable
904 /// subscriptions), specify a virtual host, negotiate different STOMP
905 /// versions, or add custom CONNECT headers.
906 ///
907 /// Parameters
908 /// - `addr`: TCP address (host:port) of the STOMP server.
909 /// - `login`: login username for STOMP `CONNECT`.
910 /// - `passcode`: passcode for STOMP `CONNECT`.
911 /// - `client_hb`: client's `heart-beat` header value ("cx,cy" in
912 /// milliseconds) that will be sent in the `CONNECT` frame.
913 /// - `options`: custom connection options (version, host, client-id, etc.).
914 ///
915 /// # Connection Behavior
916 ///
917 /// If the broker is unreachable, the method retries with exponential
918 /// backoff (1s → 2s → 4s → … → 30s cap) — the same strategy used for
919 /// reconnection after a connection drop. This means your application can
920 /// start before the broker is available and will connect once it comes up.
921 ///
922 /// This retry is unbounded by default. Set
923 /// [`ConnectOptions::connect_timeout`] to cap it — useful for CLI tools and
924 /// one-shot scripts that must not hang forever on a misconfigured address.
925 ///
926 /// # Errors
927 ///
928 /// Returns an error immediately (no retry) if:
929 /// - The server rejects the connection, e.g., due to invalid credentials
930 /// (`ConnError::ServerRejected`)
931 ///
932 /// All other errors (TCP refused, connection closed mid-handshake, I/O
933 /// failures) are retried with backoff.
934 ///
935 /// # Example
936 ///
937 /// ```ignore
938 /// use iridium_stomp::{Connection, ConnectOptions};
939 ///
940 /// // Connect with a client-id for durable subscriptions
941 /// let options = ConnectOptions::default()
942 /// .client_id("my-app-instance-1");
943 ///
944 /// let conn = Connection::connect_with_options(
945 /// "localhost:61613",
946 /// "guest",
947 /// "guest",
948 /// Connection::DEFAULT_HEARTBEAT,
949 /// options,
950 /// ).await?;
951 /// ```
952 pub async fn connect_with_options(
953 addr: &str,
954 login: &str,
955 passcode: &str,
956 client_hb: &str,
957 options: ConnectOptions,
958 ) -> Result<Self, ConnError> {
959 let (out_tx, mut out_rx) = mpsc::channel::<StompItem>(32);
960 let (in_tx, in_rx) = mpsc::channel::<Frame>(32);
961 let subscriptions: Arc<Mutex<Subscriptions>> = Arc::new(Mutex::new(HashMap::new()));
962 let sub_id_counter = Arc::new(AtomicU64::new(1));
963 let (shutdown_tx, _) = broadcast::channel::<()>(1);
964 let pending: Arc<Mutex<PendingMap>> = Arc::new(Mutex::new(HashMap::new()));
965 let pending_clone = pending.clone();
966 let pending_receipts: Arc<Mutex<PendingReceipts>> = Arc::new(Mutex::new(HashMap::new()));
967 let pending_receipts_clone = pending_receipts.clone();
968
969 let addr = addr.to_string();
970 let login = login.to_string();
971 let passcode = passcode.to_string();
972 let client_hb = client_hb.to_string();
973
974 // Extract options into owned values for the spawned task
975 let accept_version = options.accept_version.unwrap_or_else(|| "1.2".to_string());
976 let host = options.host.unwrap_or_else(|| "/".to_string());
977 let client_id = options.client_id;
978 let custom_headers = options.headers;
979 let heartbeat_notify_tx = options.heartbeat_tx;
980 let connect_timeout = options.connect_timeout;
981 let max_frame_size = options.max_frame_size.unwrap_or(DEFAULT_MAX_FRAME_SIZE);
982
983 // Perform initial connection and STOMP handshake before spawning
984 // background task. Retries with exponential backoff on I/O and
985 // protocol errors (broker unreachable or crashing mid-handshake)
986 // using the same strategy as reconnection. Only ServerRejected
987 // (authentication failure) fails immediately.
988 //
989 // `last_err` remembers the most recent failure so that, if a
990 // `connect_timeout` bound elapses, the caller gets that error rather
991 // than a bare "timed out". It is written by the retry arms and read
992 // only after the attempt future below has been dropped.
993 let mut backoff_secs: u64 = 1;
994 let mut last_err: Option<ConnError> = None;
995 let attempt = async {
996 loop {
997 let stream = match TcpStream::connect(&addr).await {
998 Ok(s) => s,
999 Err(e) => {
1000 tracing::warn!(
1001 addr = %addr,
1002 error = %e,
1003 backoff_secs,
1004 "initial connect failed, retrying in {}s",
1005 backoff_secs,
1006 );
1007 last_err = Some(ConnError::Io(e));
1008 tokio::time::sleep(Duration::from_secs(backoff_secs)).await;
1009 backoff_secs = (backoff_secs * 2).min(30);
1010 continue;
1011 }
1012 };
1013 let mut framed =
1014 Framed::new(stream, StompCodec::with_max_frame_size(max_frame_size));
1015
1016 let connect = Self::build_connect_frame(
1017 &accept_version,
1018 &host,
1019 &login,
1020 &passcode,
1021 &client_hb,
1022 &client_id,
1023 &custom_headers,
1024 );
1025
1026 if let Err(e) = framed.send(StompItem::Frame(connect)).await {
1027 tracing::warn!(
1028 addr = %addr,
1029 error = %e,
1030 backoff_secs,
1031 "failed to send CONNECT frame, retrying in {}s",
1032 backoff_secs,
1033 );
1034 last_err = Some(ConnError::Io(e));
1035 tokio::time::sleep(Duration::from_secs(backoff_secs)).await;
1036 backoff_secs = (backoff_secs * 2).min(30);
1037 continue;
1038 }
1039
1040 match Self::await_connected_response(&mut framed).await {
1041 Ok(server_hb) => {
1042 tracing::info!(addr = %addr, "connected to broker");
1043 let (cx, cy) = parse_heartbeat_header(&client_hb);
1044 let (sx, sy) = parse_heartbeat_header(&server_hb);
1045 let (si, ri) = negotiate_heartbeats(cx, cy, sx, sy);
1046 return Ok::<_, ConnError>((framed, si, ri));
1047 }
1048 // Auth errors fail immediately — bad config should not be retried
1049 Err(e @ ConnError::ServerRejected(_)) => {
1050 return Err(e);
1051 }
1052 // I/O and protocol errors during handshake (e.g., broker
1053 // crashed or closed mid-handshake) — retry with backoff
1054 Err(e) => {
1055 tracing::warn!(
1056 addr = %addr,
1057 error = %e,
1058 backoff_secs,
1059 "handshake failed, retrying in {}s",
1060 backoff_secs,
1061 );
1062 last_err = Some(e);
1063 tokio::time::sleep(Duration::from_secs(backoff_secs)).await;
1064 backoff_secs = (backoff_secs * 2).min(30);
1065 continue;
1066 }
1067 }
1068 }
1069 };
1070
1071 let (framed, send_interval, recv_interval) = match connect_timeout {
1072 Some(timeout) => match tokio::time::timeout(timeout, attempt).await {
1073 Ok(result) => result?,
1074 Err(_elapsed) => {
1075 // The bound elapsed. Hand back the last error seen, or
1076 // synthesize a timeout if the very first attempt was still
1077 // in flight and had not recorded one yet.
1078 return Err(last_err.unwrap_or_else(|| {
1079 ConnError::Io(std::io::Error::new(
1080 std::io::ErrorKind::TimedOut,
1081 format!("connect to {} timed out after {:?}", addr, timeout),
1082 ))
1083 }));
1084 }
1085 },
1086 None => attempt.await?,
1087 };
1088
1089 // Now spawn background task for ongoing I/O and reconnection.
1090 //
1091 // Subscribe before spawning, and keep the one receiver for the task's
1092 // whole life. A broadcast drops a message when nobody is subscribed and
1093 // delivers only to receivers that existed when it was sent, so
1094 // subscribing inside the task would lose a shutdown signalled before the
1095 // task is first polled, and re-subscribing per iteration would discard
1096 // one that arrived during the reconnect backoff.
1097 let mut shutdown_sub = shutdown_tx.subscribe();
1098 let subscriptions_clone = subscriptions.clone();
1099
1100 tokio::spawn(async move {
1101 let mut backoff_secs: u64 = 1;
1102
1103 // Use the already-established connection for the first iteration
1104 let mut current_framed = Some(framed);
1105 let mut current_send_interval = send_interval;
1106 let mut current_recv_interval = recv_interval;
1107 let mut first_iteration = true;
1108 // Track subscription errors across reconnections. If a subscription
1109 // receives too many consecutive errors, we remove it to prevent
1110 // error loops (e.g., Artemis sending repeated permission errors).
1111 let mut subscription_errors: HashMap<String, u32> = HashMap::new();
1112 // Track subscription IDs that have been abandoned so we can ignore
1113 // subsequent errors for them.
1114 let mut abandoned_sub_ids: std::collections::HashSet<String> =
1115 std::collections::HashSet::new();
1116 const SUBSCRIPTION_ERROR_THRESHOLD: u32 = 3;
1117
1118 loop {
1119 // Check for shutdown before attempting connection
1120 tokio::select! {
1121 biased;
1122 _ = shutdown_sub.recv() => break,
1123 _ = future::ready(()) => {},
1124 }
1125
1126 // Either use existing connection or establish new one (reconnect)
1127 let framed = if let Some(f) = current_framed.take() {
1128 f
1129 } else {
1130 // Reconnection attempt
1131 match TcpStream::connect(&addr).await {
1132 Ok(stream) => {
1133 let mut framed = Framed::new(
1134 stream,
1135 StompCodec::with_max_frame_size(max_frame_size),
1136 );
1137
1138 let connect = Self::build_connect_frame(
1139 &accept_version,
1140 &host,
1141 &login,
1142 &passcode,
1143 &client_hb,
1144 &client_id,
1145 &custom_headers,
1146 );
1147
1148 if let Err(e) = framed.send(StompItem::Frame(connect)).await {
1149 tracing::warn!(
1150 addr = %addr,
1151 error = %e,
1152 backoff_secs,
1153 "reconnect: failed to send CONNECT frame, retrying in {}s",
1154 backoff_secs,
1155 );
1156 tokio::time::sleep(Duration::from_secs(backoff_secs)).await;
1157 backoff_secs = (backoff_secs * 2).min(30);
1158 continue;
1159 }
1160
1161 match Self::await_connected_response(&mut framed).await {
1162 Ok(server_hb) => {
1163 tracing::info!(addr = %addr, "reconnected to broker");
1164 let (cx, cy) = parse_heartbeat_header(&client_hb);
1165 let (sx, sy) = parse_heartbeat_header(&server_hb);
1166 let (si, ri) = negotiate_heartbeats(cx, cy, sx, sy);
1167 current_send_interval = si;
1168 current_recv_interval = ri;
1169 framed
1170 }
1171 Err(e) => {
1172 tracing::warn!(
1173 addr = %addr,
1174 error = %e,
1175 backoff_secs,
1176 "reconnect: handshake failed, retrying in {}s",
1177 backoff_secs,
1178 );
1179 tokio::time::sleep(Duration::from_secs(backoff_secs)).await;
1180 backoff_secs = (backoff_secs * 2).min(30);
1181 continue;
1182 }
1183 }
1184 }
1185 Err(e) => {
1186 tracing::warn!(
1187 addr = %addr,
1188 error = %e,
1189 backoff_secs,
1190 "reconnect: broker unreachable, retrying in {}s",
1191 backoff_secs,
1192 );
1193 tokio::time::sleep(Duration::from_secs(backoff_secs)).await;
1194 backoff_secs = (backoff_secs * 2).min(30);
1195 continue;
1196 }
1197 }
1198 };
1199
1200 let (send_interval, recv_interval) = (current_send_interval, current_recv_interval);
1201
1202 let last_received = Arc::new(AtomicU64::new(current_millis()));
1203 let writer_last_sent = Arc::new(AtomicU64::new(current_millis()));
1204
1205 let (mut sink, mut stream) = framed.split();
1206 let in_tx = in_tx.clone();
1207 let subscriptions = subscriptions_clone.clone();
1208
1209 // Clear pending message map on reconnect — messages that were
1210 // outstanding before the disconnect are considered lost and
1211 // will be redelivered by the server as appropriate.
1212 {
1213 let mut p = pending_clone.lock().await;
1214 p.clear();
1215 }
1216
1217 // Resubscribe any existing subscriptions after reconnect.
1218 // We snapshot the subscription entries while holding the lock
1219 // and then issue SUBSCRIBE frames using the sink.
1220 if first_iteration {
1221 first_iteration = false;
1222 } else {
1223 let subs_snapshot: Vec<ResubEntry> = {
1224 let map = subscriptions.lock().await;
1225 let mut v: Vec<ResubEntry> = Vec::new();
1226 for (dest, vec) in map.iter() {
1227 for entry in vec.iter() {
1228 v.push((
1229 dest.clone(),
1230 entry.id.clone(),
1231 entry.ack.clone(),
1232 entry.headers.clone(),
1233 ));
1234 }
1235 }
1236 v
1237 };
1238
1239 for (dest, id, ack, headers) in subs_snapshot {
1240 let mut sf = Frame::new("SUBSCRIBE");
1241 sf = sf
1242 .header("id", &id)
1243 .header("destination", &dest)
1244 .header("ack", &ack);
1245 for (k, v) in headers {
1246 sf = sf.header(&k, &v);
1247 }
1248 if let Err(e) = sink.send(StompItem::Frame(sf)).await {
1249 tracing::warn!(
1250 destination = %dest,
1251 subscription_id = %id,
1252 error = %e,
1253 "failed to resubscribe after reconnect",
1254 );
1255 }
1256 }
1257 }
1258 let mut hb_tick = match send_interval {
1259 Some(d) => tokio::time::interval(d),
1260 None => tokio::time::interval(Duration::from_secs(86400)),
1261 };
1262 let watchdog_half = recv_interval.map(|d| d / 2);
1263
1264 let conn_start = tokio::time::Instant::now();
1265
1266 // Set when the branch below takes the shutdown signal. The
1267 // reconnect check after this loop cannot re-read it from
1268 // `shutdown_sub`, which is drained by then.
1269 let mut shutting_down = false;
1270
1271 'conn: loop {
1272 tokio::select! {
1273 _ = shutdown_sub.recv() => { let _ = sink.close().await; shutting_down = true; break 'conn; }
1274 maybe = out_rx.recv() => {
1275 match maybe {
1276 Some(item) => if sink.send(item).await.is_err() { break 'conn } else { writer_last_sent.store(current_millis(), Ordering::SeqCst); }
1277 None => break 'conn,
1278 }
1279 }
1280 item = stream.next() => {
1281 match item {
1282 Some(Ok(StompItem::Heartbeat)) => {
1283 last_received.store(current_millis(), Ordering::SeqCst);
1284 if let Some(ref tx) = heartbeat_notify_tx {
1285 let _ = tx.try_send(());
1286 }
1287 }
1288 Some(Ok(StompItem::Frame(f))) => {
1289 last_received.store(current_millis(), Ordering::SeqCst);
1290 // Dispatch MESSAGE frames to any matching subscribers.
1291 if f.command == "MESSAGE" {
1292 // try to find destination, subscription and message-id headers
1293 let mut dest_opt: Option<String> = None;
1294 let mut sub_opt: Option<String> = None;
1295 let mut msg_id_opt: Option<String> = None;
1296 for (k, v) in &f.headers {
1297 let kl = k.to_lowercase();
1298 if kl == "destination" {
1299 dest_opt = Some(v.clone());
1300 } else if kl == "subscription" {
1301 sub_opt = Some(v.clone());
1302 } else if kl == "message-id" {
1303 msg_id_opt = Some(v.clone());
1304 }
1305 }
1306
1307 // Determine whether we need to track this message as pending
1308 let mut need_pending = false;
1309 if let Some(sub_id) = &sub_opt {
1310 let map = subscriptions.lock().await;
1311 for vec in map.values() {
1312 for entry in vec.iter() {
1313 if &entry.id == sub_id && entry.ack != "auto" {
1314 need_pending = true;
1315 }
1316 }
1317 }
1318 } else if let Some(dest) = &dest_opt {
1319 let map = subscriptions.lock().await;
1320 if let Some(vec) = map.get(dest) {
1321 for entry in vec.iter() {
1322 if entry.ack != "auto" {
1323 need_pending = true;
1324 break;
1325 }
1326 }
1327 }
1328 }
1329
1330 // If required, add to pending map (per-subscription) before
1331 // delivery so ACK/NACK requests from the application can
1332 // reference the message. We require a `message-id` header
1333 // to track messages; if missing, we cannot support ACK/NACK.
1334 if let Some(msg_id) = msg_id_opt.clone().filter(|_| need_pending) {
1335 // If the server provided a subscription id in the
1336 // MESSAGE, store pending under that subscription.
1337 if let Some(sub_id) = &sub_opt {
1338 let mut p = pending_clone.lock().await;
1339 let q = p
1340 .entry(sub_id.clone())
1341 .or_insert_with(VecDeque::new);
1342 q.push_back((msg_id.clone(), f.clone()));
1343 } else if let Some(dest) = &dest_opt {
1344 // Destination-based delivery: add the message to
1345 // the pending queue for each matching
1346 // subscription on that destination.
1347 let map = subscriptions.lock().await;
1348 if let Some(vec) = map.get(dest) {
1349 let mut p = pending_clone.lock().await;
1350 for entry in vec.iter() {
1351 let q = p
1352 .entry(entry.id.clone())
1353 .or_insert_with(VecDeque::new);
1354 q.push_back((msg_id.clone(), f.clone()));
1355 }
1356 }
1357 }
1358 }
1359
1360 // Deliver to subscribers. Both paths use
1361 // `deliver_and_keep` so a dropped
1362 // subscriber (Closed channel) is pruned
1363 // and a slow one (Full channel) is kept,
1364 // then any emptied destination key is
1365 // removed.
1366 if let Some(sub_id) = sub_opt {
1367 let mut map = subscriptions.lock().await;
1368 for vec in map.values_mut() {
1369 vec.retain(|entry| {
1370 entry.id != sub_id || deliver_and_keep(entry, &f)
1371 });
1372 }
1373 map.retain(|_, vec| !vec.is_empty());
1374 } else if let Some(dest) = dest_opt {
1375 let mut map = subscriptions.lock().await;
1376 if let Some(vec) = map.get_mut(&dest) {
1377 vec.retain(|entry| deliver_and_keep(entry, &f));
1378 if vec.is_empty() {
1379 map.remove(&dest);
1380 }
1381 }
1382 }
1383 } else if f.command == "RECEIPT" {
1384 // Handle RECEIPT frame: notify any waiting callers
1385 if let Some(receipt_id) = f.get_header("receipt-id") {
1386 let mut receipts = pending_receipts_clone.lock().await;
1387 if let Some(sender) = receipts.remove(receipt_id) {
1388 let _ = sender.send(Ok(()));
1389 }
1390 }
1391 // Don't forward RECEIPT frames to inbound channel
1392 continue;
1393 } else if f.command == "ERROR" {
1394 // An ERROR with receipt-id is the failed response to a
1395 // frame that requested a receipt. Wake that waiter with
1396 // the broker error, then continue forwarding the frame.
1397 if let Some(receipt_id) = f.get_header("receipt-id") {
1398 let mut receipts =
1399 pending_receipts_clone.lock().await;
1400 if let Some(sender) = receipts.remove(receipt_id) {
1401 let _ = sender
1402 .send(Err(ServerError::from_frame(f.clone())));
1403 }
1404 }
1405
1406 // Track subscription-related errors. If we see repeated
1407 // errors for the same destination, remove the subscription
1408 // to prevent error loops.
1409 //
1410 // First, check if this error is for an already-abandoned
1411 // subscription (Artemis keeps sending errors after we abandon).
1412 let sub_id = extract_subscription_id_from_error(&f);
1413 if let Some(ref id) = sub_id
1414 && abandoned_sub_ids.contains(id)
1415 {
1416 // Skip this error - subscription already abandoned
1417 continue;
1418 }
1419
1420 // Try to identify the destination:
1421 // 1. Extract directly from ERROR frame
1422 // 2. Look up by subscription ID (Artemis uses "subscription N")
1423 let dest = if let Some(d) = extract_destination_from_error(&f)
1424 {
1425 Some(d)
1426 } else if let Some(ref id) = sub_id {
1427 lookup_destination_by_sub_id(id, &subscriptions).await
1428 } else {
1429 None
1430 };
1431
1432 if let Some(dest) = dest {
1433 let count = {
1434 let c = subscription_errors
1435 .entry(dest.clone())
1436 .or_insert(0);
1437 *c += 1;
1438 *c
1439 };
1440
1441 if count >= SUBSCRIPTION_ERROR_THRESHOLD {
1442 // Remove the subscription from auto-resubscribe
1443 let mut map = subscriptions.lock().await;
1444 if map.remove(&dest).is_some() {
1445 // Track the subscription ID as abandoned
1446 if let Some(id) = sub_id {
1447 abandoned_sub_ids.insert(id);
1448 }
1449 // Send abandonment notification
1450 let msg = format!(
1451 "Subscription abandoned: {} errors for {}",
1452 count, dest
1453 );
1454 let abandon_frame = Frame::new("ERROR")
1455 .header("message", &msg)
1456 .header("destination", &dest)
1457 .header("x-abandoned", "true");
1458 let _ = in_tx.send(abandon_frame).await;
1459 }
1460 }
1461 }
1462 }
1463
1464 let _ = in_tx.send(f).await;
1465 }
1466 Some(Err(_)) | None => break 'conn,
1467 }
1468 }
1469 _ = hb_tick.tick() => {
1470 if let Some(dur) = send_interval {
1471 let last = writer_last_sent.load(Ordering::SeqCst);
1472 if current_millis().saturating_sub(last) >= dur.as_millis() as u64 {
1473 if sink.send(StompItem::Heartbeat).await.is_err() { break 'conn; }
1474 writer_last_sent.store(current_millis(), Ordering::SeqCst);
1475 }
1476 }
1477 }
1478 _ = async { if let Some(interval) = watchdog_half { tokio::time::sleep(interval).await } else { future::pending::<()>().await } } => {
1479 if let Some(recv_dur) = recv_interval {
1480 let last = last_received.load(Ordering::SeqCst);
1481 if current_millis().saturating_sub(last) > (recv_dur.as_millis() as u64 * 2) {
1482 let _ = sink.close().await; break 'conn;
1483 }
1484 }
1485 }
1486 }
1487 }
1488
1489 if shutting_down || shutdown_sub.try_recv().is_ok() {
1490 break;
1491 }
1492 let stable_duration = conn_start.elapsed();
1493 if stable_duration >= Duration::from_secs(backoff_secs.max(5)) {
1494 // Connection was stable — reset backoff
1495 backoff_secs = 1;
1496 tracing::info!(
1497 addr = %addr,
1498 stable_secs = stable_duration.as_secs(),
1499 "connection dropped after stable session, reconnecting in 1s",
1500 );
1501 } else {
1502 // Connection died quickly — increase backoff
1503 backoff_secs = (backoff_secs * 2).min(30);
1504 tracing::warn!(
1505 addr = %addr,
1506 stable_secs = stable_duration.as_secs(),
1507 backoff_secs,
1508 "connection dropped quickly, reconnecting in {}s",
1509 backoff_secs,
1510 );
1511 }
1512 tokio::time::sleep(Duration::from_secs(backoff_secs)).await;
1513 }
1514 });
1515
1516 Ok(Connection {
1517 outbound_tx: out_tx,
1518 inbound_rx: Arc::new(Mutex::new(in_rx)),
1519 shutdown_tx,
1520 subscriptions,
1521 sub_id_counter,
1522 pending,
1523 pending_receipts,
1524 disconnect_timeout: options
1525 .disconnect_timeout
1526 .unwrap_or(Self::DEFAULT_DISCONNECT_TIMEOUT),
1527 })
1528 }
1529
1530 /// Build a CONNECT frame with all specified headers.
1531 fn build_connect_frame(
1532 accept_version: &str,
1533 host: &str,
1534 login: &str,
1535 passcode: &str,
1536 heartbeat: &str,
1537 client_id: &Option<String>,
1538 custom_headers: &[(String, String)],
1539 ) -> Frame {
1540 let mut connect = Frame::new("CONNECT")
1541 .header("accept-version", accept_version)
1542 .header("host", host)
1543 .header("login", login)
1544 .header("passcode", passcode)
1545 .header("heart-beat", heartbeat);
1546
1547 if let Some(id) = client_id {
1548 connect = connect.header("client-id", id);
1549 }
1550
1551 // Reserved headers that custom_headers cannot override
1552 let reserved = [
1553 "accept-version",
1554 "host",
1555 "login",
1556 "passcode",
1557 "heart-beat",
1558 "client-id",
1559 ];
1560
1561 for (k, v) in custom_headers {
1562 if !reserved.contains(&k.to_lowercase().as_str()) {
1563 connect = connect.header(k, v);
1564 }
1565 }
1566
1567 connect
1568 }
1569
1570 /// Wait for CONNECTED or ERROR response from the server.
1571 ///
1572 /// Returns the server's heartbeat header value on success, or an error
1573 /// if the server sends an ERROR frame or closes the connection.
1574 async fn await_connected_response(
1575 framed: &mut Framed<TcpStream, StompCodec>,
1576 ) -> Result<String, ConnError> {
1577 loop {
1578 match framed.next().await {
1579 Some(Ok(StompItem::Frame(f))) => {
1580 if f.command == "CONNECTED" {
1581 // Extract heartbeat from server
1582 let server_hb = f.get_header("heart-beat").unwrap_or("0,0").to_string();
1583 return Ok(server_hb);
1584 } else if f.command == "ERROR" {
1585 // Server rejected connection (e.g., invalid credentials)
1586 return Err(ConnError::ServerRejected(ServerError::from_frame(f)));
1587 }
1588 // Ignore other frames during CONNECT phase
1589 }
1590 Some(Ok(StompItem::Heartbeat)) => {
1591 // Ignore heartbeats during handshake
1592 continue;
1593 }
1594 Some(Err(e)) => {
1595 return Err(ConnError::Io(e));
1596 }
1597 None => {
1598 return Err(ConnError::Protocol(
1599 "connection closed before CONNECTED received".to_string(),
1600 ));
1601 }
1602 }
1603 }
1604 }
1605
1606 /// Send a text message to a destination.
1607 ///
1608 /// This is a convenience wrapper around [`send_frame`](Self::send_frame)
1609 /// for the common case of sending a string payload with no extra headers.
1610 ///
1611 /// # Example
1612 /// ```ignore
1613 /// conn.send("/queue/test", "hello").await?;
1614 /// ```
1615 pub async fn send(&self, destination: &str, body: impl AsRef<str>) -> Result<(), ConnError> {
1616 let frame = Frame::new("SEND")
1617 .header("destination", destination)
1618 .set_body(body.as_ref().as_bytes().to_vec());
1619 self.send_frame(frame).await
1620 }
1621
1622 /// Send an arbitrary STOMP frame to the broker.
1623 ///
1624 /// Use this when you need full control over the frame (custom headers,
1625 /// binary body, receipt requests, etc.). For simple text messages, prefer
1626 /// [`send`](Self::send).
1627 pub async fn send_frame(&self, frame: Frame) -> Result<(), ConnError> {
1628 // Send a frame to the background writer task.
1629 //
1630 // Parameters
1631 // - `frame`: ownership of the `Frame` to send. The frame is converted
1632 // into a `StompItem::Frame` and sent over the internal mpsc channel.
1633 self.outbound_tx
1634 .send(StompItem::Frame(frame))
1635 .await
1636 .map_err(|_| ConnError::Protocol("send channel closed".into()))
1637 }
1638
1639 /// Generate a unique receipt ID.
1640 fn generate_receipt_id() -> String {
1641 static RECEIPT_COUNTER: AtomicU64 = AtomicU64::new(1);
1642 format!("rcpt-{}", RECEIPT_COUNTER.fetch_add(1, Ordering::SeqCst))
1643 }
1644
1645 /// Send a frame with a receipt request and return a handle to its
1646 /// confirmation.
1647 ///
1648 /// This method adds a unique `receipt` header to the frame, registers the
1649 /// receipt id for tracking, and returns a [`ReceiptHandle`]. Call
1650 /// [`ReceiptHandle::wait`] to await the broker's RECEIPT response.
1651 ///
1652 /// Any `receipt` header already present on `frame` is ignored in favour of
1653 /// the generated id; use [`ReceiptHandle::receipt_id`] to read it back.
1654 ///
1655 /// The returned handle owns the confirmation channel from the moment the
1656 /// frame is queued, so the broker's response cannot arrive before there is
1657 /// somewhere to put it. Awaiting may be deferred freely - see
1658 /// [`ReceiptHandle`] for sending several frames before awaiting any.
1659 ///
1660 /// # Parameters
1661 /// - `frame`: the frame to send. A `receipt` header will be added.
1662 ///
1663 /// # Returns
1664 /// A [`ReceiptHandle`] for the sent frame.
1665 ///
1666 /// # Example
1667 /// ```ignore
1668 /// let handle = conn.send_frame_with_receipt(frame).await?;
1669 /// handle.wait(Duration::from_secs(5)).await?;
1670 /// ```
1671 pub async fn send_frame_with_receipt(&self, frame: Frame) -> Result<ReceiptHandle, ConnError> {
1672 let receipt_id = Self::generate_receipt_id();
1673
1674 // Create the oneshot channel for notification. The receiver goes into
1675 // the returned handle, so it stays alive for as long as the caller
1676 // cares about the response.
1677 let (tx, rx) = oneshot::channel();
1678
1679 // Register the pending receipt before sending, so the background task
1680 // can never see the response with nothing registered for it.
1681 {
1682 let mut receipts = self.pending_receipts.lock().await;
1683 receipts.insert(receipt_id.clone(), tx);
1684 }
1685
1686 // Drop any caller-supplied receipt header before adding ours. `Frame::header`
1687 // appends rather than overwrites, so leaving one in place would put two
1688 // receipt headers on the wire; brokers honour the first, which would never
1689 // match the id registered above. Matched case-insensitively, as header
1690 // lookup is.
1691 let mut frame = frame;
1692 frame
1693 .headers
1694 .retain(|(key, _)| !key.eq_ignore_ascii_case("receipt"));
1695
1696 // Add receipt header and send the frame
1697 let frame_with_receipt = frame.receipt(&receipt_id);
1698 if let Err(err) = self.send_frame(frame_with_receipt).await {
1699 // The frame never went out; deregister rather than leave an entry
1700 // that nothing will ever answer.
1701 let mut receipts = self.pending_receipts.lock().await;
1702 receipts.remove(&receipt_id);
1703 return Err(err);
1704 }
1705
1706 Ok(ReceiptHandle {
1707 receipt_id,
1708 rx,
1709 pending_receipts: self.pending_receipts.clone(),
1710 })
1711 }
1712
1713 /// Send a frame and wait for server confirmation via RECEIPT.
1714 ///
1715 /// This is a convenience method that combines
1716 /// [`send_frame_with_receipt`](Connection::send_frame_with_receipt) and
1717 /// [`ReceiptHandle::wait`]. Use this when you want to ensure a frame was
1718 /// processed by the server before continuing.
1719 ///
1720 /// Any `receipt` header already present on `frame` is ignored in favour of
1721 /// a generated id. Reach for `send_frame_with_receipt` directly when you
1722 /// need to send several frames before awaiting any of them.
1723 ///
1724 /// # Parameters
1725 /// - `frame`: the frame to send.
1726 /// - `timeout`: maximum time to wait for the receipt.
1727 ///
1728 /// # Returns
1729 /// `Ok(())` if the frame was sent and receipt confirmed, or an error if
1730 /// sending failed or the receipt timed out.
1731 ///
1732 /// # Example
1733 /// ```ignore
1734 /// let frame = Frame::new("SEND")
1735 /// .header("destination", "/queue/orders")
1736 /// .set_body(b"order data".to_vec());
1737 ///
1738 /// conn.send_frame_confirmed(frame, Duration::from_secs(5)).await?;
1739 /// println!("Order sent and confirmed!");
1740 /// ```
1741 pub async fn send_frame_confirmed(
1742 &self,
1743 frame: Frame,
1744 timeout: Duration,
1745 ) -> Result<(), ConnError> {
1746 self.send_frame_with_receipt(frame)
1747 .await?
1748 .wait(timeout)
1749 .await
1750 }
1751
1752 /// Subscribe to a destination.
1753 ///
1754 /// Parameters
1755 /// - `destination`: the STOMP destination to subscribe to (e.g. "/queue/foo").
1756 /// - `ack`: acknowledgement mode to request from the server.
1757 ///
1758 /// Returns a tuple `(subscription_id, receiver)` where `subscription_id` is
1759 /// the opaque id assigned locally for this subscription and `receiver` is a
1760 /// `mpsc::Receiver<Frame>` which will yield incoming MESSAGE frames for the
1761 /// destination. The caller should read from the receiver to handle messages.
1762 /// Subscribe to a destination using optional extra headers.
1763 ///
1764 /// This variant accepts additional headers which are stored locally and
1765 /// re-sent on reconnect. Use `subscribe` as a convenience wrapper when no
1766 /// extra headers are needed.
1767 pub async fn subscribe_with_headers(
1768 &self,
1769 destination: &str,
1770 ack: AckMode,
1771 extra_headers: Vec<(String, String)>,
1772 ) -> Result<crate::subscription::Subscription, ConnError> {
1773 let id = self
1774 .sub_id_counter
1775 .fetch_add(1, Ordering::SeqCst)
1776 .to_string();
1777 let (tx, rx) = mpsc::channel::<Frame>(16);
1778 {
1779 let mut map = self.subscriptions.lock().await;
1780 map.entry(destination.to_string())
1781 .or_insert_with(Vec::new)
1782 .push(SubscriptionEntry {
1783 id: id.clone(),
1784 sender: tx.clone(),
1785 ack: ack.as_str().to_string(),
1786 headers: extra_headers.clone(),
1787 });
1788 }
1789
1790 let mut f = Frame::new("SUBSCRIBE");
1791 f = f
1792 .header("id", &id)
1793 .header("destination", destination)
1794 .header("ack", ack.as_str());
1795 for (k, v) in &extra_headers {
1796 f = f.header(k, v);
1797 }
1798 self.outbound_tx
1799 .send(StompItem::Frame(f))
1800 .await
1801 .map_err(|_| ConnError::Protocol("send channel closed".into()))?;
1802
1803 Ok(crate::subscription::Subscription::new(
1804 id,
1805 destination.to_string(),
1806 rx,
1807 self.clone(),
1808 ))
1809 }
1810
1811 /// Convenience wrapper without extra headers.
1812 pub async fn subscribe(
1813 &self,
1814 destination: &str,
1815 ack: AckMode,
1816 ) -> Result<crate::subscription::Subscription, ConnError> {
1817 self.subscribe_with_headers(destination, ack, Vec::new())
1818 .await
1819 }
1820
1821 /// Subscribe with a typed `SubscriptionOptions` structure.
1822 ///
1823 /// `SubscriptionOptions.headers` are forwarded to the broker and persisted
1824 /// for automatic resubscribe after reconnect. Durable subscriptions are
1825 /// requested through those headers - see the module docs for
1826 /// [`SubscriptionOptions`](crate::subscription::SubscriptionOptions).
1827 pub async fn subscribe_with_options(
1828 &self,
1829 destination: &str,
1830 ack: AckMode,
1831 options: crate::subscription::SubscriptionOptions,
1832 ) -> Result<crate::subscription::Subscription, ConnError> {
1833 self.subscribe_with_headers(destination, ack, options.headers)
1834 .await
1835 }
1836
1837 /// Unsubscribe a previously created subscription by its local subscription id.
1838 pub async fn unsubscribe(&self, subscription_id: &str) -> Result<(), ConnError> {
1839 let mut found = false;
1840 {
1841 let mut map = self.subscriptions.lock().await;
1842 let mut remove_keys: Vec<String> = Vec::new();
1843 for (dest, vec) in map.iter_mut() {
1844 if let Some(pos) = vec.iter().position(|entry| entry.id == subscription_id) {
1845 vec.remove(pos);
1846 found = true;
1847 }
1848 if vec.is_empty() {
1849 remove_keys.push(dest.clone());
1850 }
1851 }
1852 for k in remove_keys {
1853 map.remove(&k);
1854 }
1855 }
1856
1857 if !found {
1858 return Err(ConnError::Protocol("subscription id not found".into()));
1859 }
1860
1861 let mut f = Frame::new("UNSUBSCRIBE");
1862 f = f.header("id", subscription_id);
1863 self.outbound_tx
1864 .send(StompItem::Frame(f))
1865 .await
1866 .map_err(|_| ConnError::Protocol("send channel closed".into()))?;
1867
1868 Ok(())
1869 }
1870
1871 /// Best-effort UNSUBSCRIBE for use from `Subscription`'s `Drop`.
1872 ///
1873 /// `Drop` cannot `.await`, so this cannot use the async paths: it
1874 /// `try_lock`s the registry to remove the entry and `try_send`s the
1875 /// UNSUBSCRIBE frame, and silently gives up on either if it would block. A
1876 /// dropped subscription whose registry removal loses the lock race is still
1877 /// reaped by [`deliver_and_keep`] on the next message, since its receiver is
1878 /// now closed; the frame is the only part that can be genuinely missed (when
1879 /// the outbound channel is full or gone), which is acceptable for a handle
1880 /// that is being discarded rather than closed explicitly.
1881 pub(crate) fn unsubscribe_best_effort(&self, subscription_id: &str) {
1882 if let Ok(mut map) = self.subscriptions.try_lock() {
1883 for vec in map.values_mut() {
1884 vec.retain(|entry| entry.id != subscription_id);
1885 }
1886 map.retain(|_, vec| !vec.is_empty());
1887 }
1888
1889 let f = Frame::new("UNSUBSCRIBE").header("id", subscription_id);
1890 let _ = self.outbound_tx.try_send(StompItem::Frame(f));
1891 }
1892
1893 /// Acknowledge a message previously received in `client` or
1894 /// `client-individual` ack modes.
1895 ///
1896 /// STOMP ack semantics:
1897 /// - `auto`: server considers message delivered immediately; the client
1898 /// should not ack.
1899 /// - `client`: cumulative acknowledgements. ACKing message `M` for
1900 /// subscription `S` acknowledges all messages delivered to `S` up to
1901 /// and including `M`.
1902 /// - `client-individual`: only the named message is acknowledged.
1903 ///
1904 /// Parameters
1905 /// - `subscription_id`: the local subscription id returned by
1906 /// `Connection::subscribe`. This disambiguates which subscription's
1907 /// pending queue to advance for cumulative ACKs.
1908 /// - `message_id`: the `message-id` header value from the received
1909 /// MESSAGE frame to acknowledge.
1910 ///
1911 /// Behavior
1912 /// - The pending queue for `subscription_id` is searched for `message_id`.
1913 /// If the subscription used `client` ack mode, all pending messages up to
1914 /// and including the matched message are removed. If the subscription
1915 /// used `client-individual`, only the matched message is removed.
1916 /// - An `ACK` frame is sent to the server with `id=<message_id>` and
1917 /// `subscription=<subscription_id>` headers.
1918 #[allow(clippy::collapsible_if, clippy::collapsible_else_if)]
1919 pub async fn ack(&self, subscription_id: &str, message_id: &str) -> Result<(), ConnError> {
1920 // Remove from the local pending queue according to subscription ack mode.
1921 let mut removed_any = false;
1922 {
1923 let mut p = self.pending.lock().await;
1924 if let Some(queue) = p.get_mut(subscription_id) {
1925 if let Some(pos) = queue.iter().position(|(mid, _)| mid == message_id) {
1926 // Determine ack mode for this subscription (default to client).
1927 let mut ack_mode = "client".to_string();
1928 {
1929 let map = self.subscriptions.lock().await;
1930 'outer: for vec in map.values() {
1931 for entry in vec.iter() {
1932 if entry.id == subscription_id {
1933 ack_mode = entry.ack.clone();
1934 break 'outer;
1935 }
1936 }
1937 }
1938 }
1939
1940 if ack_mode == "client" {
1941 // cumulative: remove up to and including pos
1942 for _ in 0..=pos {
1943 queue.pop_front();
1944 removed_any = true;
1945 }
1946 } else if queue.remove(pos).is_some() {
1947 // client-individual: remove only the specific message
1948 removed_any = true;
1949 }
1950
1951 if queue.is_empty() {
1952 p.remove(subscription_id);
1953 }
1954 }
1955 }
1956 }
1957
1958 // Send ACK to server (include subscription header for clarity)
1959 let mut f = Frame::new("ACK");
1960 f = f
1961 .header("id", message_id)
1962 .header("subscription", subscription_id);
1963 self.outbound_tx
1964 .send(StompItem::Frame(f))
1965 .await
1966 .map_err(|_| ConnError::Protocol("send channel closed".into()))?;
1967
1968 // If message wasn't found locally, still send ACK to server; server
1969 // may ignore or treat it as no-op.
1970 let _ = removed_any;
1971 Ok(())
1972 }
1973
1974 /// Negative-acknowledge a message (NACK).
1975 ///
1976 /// Parameters
1977 /// - `subscription_id`: the local subscription id the message was delivered under.
1978 /// - `message_id`: the `message-id` header value from the received MESSAGE.
1979 ///
1980 /// Behavior
1981 /// - Removes the message from the local pending queue (cumulatively if the
1982 /// subscription used `client` ack mode, otherwise only the single
1983 /// message). Sends a `NACK` frame to the server with `id` and
1984 /// `subscription` headers.
1985 #[allow(clippy::collapsible_if, clippy::collapsible_else_if)]
1986 pub async fn nack(&self, subscription_id: &str, message_id: &str) -> Result<(), ConnError> {
1987 // Mirror ack removal semantics for pending map.
1988 let mut removed_any = false;
1989 {
1990 let mut p = self.pending.lock().await;
1991 if let Some(queue) = p.get_mut(subscription_id) {
1992 if let Some(pos) = queue.iter().position(|(mid, _)| mid == message_id) {
1993 let mut ack_mode = "client".to_string();
1994 {
1995 let map = self.subscriptions.lock().await;
1996 'outer2: for vec in map.values() {
1997 for entry in vec.iter() {
1998 if entry.id == subscription_id {
1999 ack_mode = entry.ack.clone();
2000 break 'outer2;
2001 }
2002 }
2003 }
2004 }
2005
2006 if ack_mode == "client" {
2007 for _ in 0..=pos {
2008 queue.pop_front();
2009 removed_any = true;
2010 }
2011 } else if queue.remove(pos).is_some() {
2012 removed_any = true;
2013 }
2014
2015 if queue.is_empty() {
2016 p.remove(subscription_id);
2017 }
2018 }
2019 }
2020 }
2021
2022 let mut f = Frame::new("NACK");
2023 f = f
2024 .header("id", message_id)
2025 .header("subscription", subscription_id);
2026 self.outbound_tx
2027 .send(StompItem::Frame(f))
2028 .await
2029 .map_err(|_| ConnError::Protocol("send channel closed".into()))?;
2030
2031 let _ = removed_any;
2032 Ok(())
2033 }
2034
2035 /// Helper to send a transaction frame (BEGIN, COMMIT, or ABORT).
2036 async fn send_transaction_frame(
2037 &self,
2038 command: &str,
2039 transaction_id: &str,
2040 ) -> Result<(), ConnError> {
2041 let f = Frame::new(command).header("transaction", transaction_id);
2042 self.outbound_tx
2043 .send(StompItem::Frame(f))
2044 .await
2045 .map_err(|_| ConnError::Protocol("send channel closed".into()))
2046 }
2047
2048 /// Begin a transaction.
2049 ///
2050 /// Parameters
2051 /// - `transaction_id`: unique identifier for the transaction. The caller is
2052 /// responsible for ensuring uniqueness within the connection.
2053 ///
2054 /// Behavior
2055 /// - Sends a `BEGIN` frame to the server with `transaction:<transaction_id>`
2056 /// header. Subsequent `SEND`, `ACK`, and `NACK` frames may include this
2057 /// transaction id to group them into the transaction. The transaction must
2058 /// be finalized with either `commit` or `abort`.
2059 pub async fn begin(&self, transaction_id: &str) -> Result<(), ConnError> {
2060 self.send_transaction_frame("BEGIN", transaction_id).await
2061 }
2062
2063 /// Commit a transaction.
2064 ///
2065 /// Parameters
2066 /// - `transaction_id`: the transaction identifier previously passed to `begin`.
2067 ///
2068 /// Behavior
2069 /// - Sends a `COMMIT` frame to the server with `transaction:<transaction_id>`
2070 /// header. All operations within the transaction are applied atomically.
2071 pub async fn commit(&self, transaction_id: &str) -> Result<(), ConnError> {
2072 self.send_transaction_frame("COMMIT", transaction_id).await
2073 }
2074
2075 /// Abort a transaction.
2076 ///
2077 /// Parameters
2078 /// - `transaction_id`: the transaction identifier previously passed to `begin`.
2079 ///
2080 /// Behavior
2081 /// - Sends an `ABORT` frame to the server with `transaction:<transaction_id>`
2082 /// header. All operations within the transaction are discarded.
2083 pub async fn abort(&self, transaction_id: &str) -> Result<(), ConnError> {
2084 self.send_transaction_frame("ABORT", transaction_id).await
2085 }
2086
2087 /// Receive the next frame from the server.
2088 ///
2089 /// Returns `Some(ReceivedFrame::Frame(..))` for normal frames (MESSAGE, etc.),
2090 /// `Some(ReceivedFrame::Error(..))` for ERROR frames, or `None` if the
2091 /// connection has been closed.
2092 ///
2093 /// # Example
2094 ///
2095 /// ```ignore
2096 /// use iridium_stomp::ReceivedFrame;
2097 ///
2098 /// while let Some(received) = conn.next_frame().await {
2099 /// match received {
2100 /// ReceivedFrame::Frame(frame) => {
2101 /// println!("Got {}: {:?}", frame.command, frame.body);
2102 /// }
2103 /// ReceivedFrame::Error(err) => {
2104 /// eprintln!("Server error: {}", err);
2105 /// break;
2106 /// }
2107 /// }
2108 /// }
2109 /// ```
2110 pub async fn next_frame(&self) -> Option<ReceivedFrame> {
2111 let mut rx = self.inbound_rx.lock().await;
2112 let frame = rx.recv().await?;
2113
2114 // Convert ERROR frames to ServerError for better ergonomics
2115 if frame.command == "ERROR" {
2116 Some(ReceivedFrame::Error(ServerError::from_frame(frame)))
2117 } else {
2118 Some(ReceivedFrame::Frame(frame))
2119 }
2120 }
2121
2122 /// Gracefully shut down the connection.
2123 ///
2124 /// Performs the STOMP 1.2 shutdown sequence: sends a DISCONNECT frame
2125 /// carrying a `receipt` header, waits for the broker's RECEIPT, then stops
2126 /// the background task and closes the socket.
2127 ///
2128 /// Because frames are written in the order they are submitted, and the
2129 /// broker only answers a DISCONNECT once it has processed what came before,
2130 /// a confirmed close also proves that everything previously sent on this
2131 /// connection reached the broker.
2132 ///
2133 /// # Returns
2134 /// `Ok(())` once the broker has confirmed the DISCONNECT.
2135 /// `Err(ConnError::ReceiptTimeout)` if it did not answer within the
2136 /// disconnect timeout, or another `ConnError` if the DISCONNECT could not be
2137 /// submitted or was rejected.
2138 ///
2139 /// **The connection is torn down either way.** An error reports that the
2140 /// shutdown was not clean - that the broker may not have run whatever it
2141 /// does on a protocol-level disconnect, such as transactional rollback or
2142 /// durable subscription cleanup - not that the connection is still open.
2143 /// Callers with nothing to do about that may discard the result.
2144 ///
2145 /// The wait is bounded by [`ConnectOptions::disconnect_timeout`], defaulting
2146 /// to [`DEFAULT_DISCONNECT_TIMEOUT`](Connection::DEFAULT_DISCONNECT_TIMEOUT),
2147 /// so an unresponsive broker cannot make `close` hang.
2148 ///
2149 /// # Example
2150 /// ```ignore
2151 /// // Report an unclean shutdown.
2152 /// conn.close().await?;
2153 ///
2154 /// // Or shut down best-effort.
2155 /// let _ = conn.close().await;
2156 /// ```
2157 pub async fn close(self) -> Result<(), ConnError> {
2158 // Queued frames are written before this one, and the broker answers the
2159 // receipt only after processing them, so awaiting it drains the outbound
2160 // queue rather than abandoning it at the shutdown signal below.
2161 let result = match self.send_frame_with_receipt(Frame::new("DISCONNECT")).await {
2162 Ok(handle) => handle.wait(self.disconnect_timeout).await,
2163 Err(err) => Err(err),
2164 };
2165
2166 // Tear down regardless of how the broker answered: the caller asked for
2167 // the connection to close, and `result` reports only whether it got to
2168 // do so cleanly.
2169 let _ = self.shutdown_tx.send(());
2170
2171 result
2172 }
2173}
2174
2175fn current_millis() -> u64 {
2176 use std::time::{SystemTime, UNIX_EPOCH};
2177 SystemTime::now()
2178 .duration_since(UNIX_EPOCH)
2179 .map(|d| d.as_millis() as u64)
2180 .unwrap_or(0)
2181}
2182
2183#[cfg(test)]
2184mod tests {
2185 use super::*;
2186 use std::io::{Read, Write};
2187 use std::net::{SocketAddr, TcpListener, TcpStream};
2188 use std::thread;
2189 use tokio::sync::mpsc;
2190
2191 // Helper to build a MESSAGE frame with given message-id and subscription/destination headers
2192 fn make_message(
2193 message_id: &str,
2194 subscription: Option<&str>,
2195 destination: Option<&str>,
2196 ) -> Frame {
2197 let mut f = Frame::new("MESSAGE");
2198 f = f.header("message-id", message_id);
2199 if let Some(s) = subscription {
2200 f = f.header("subscription", s);
2201 }
2202 if let Some(d) = destination {
2203 f = f.header("destination", d);
2204 }
2205 f
2206 }
2207
2208 fn read_stomp_frame(stream: &mut TcpStream) -> String {
2209 let mut bytes = Vec::new();
2210 let mut byte = [0u8; 1];
2211 loop {
2212 stream.read_exact(&mut byte).unwrap();
2213 bytes.push(byte[0]);
2214 if byte[0] == 0 {
2215 break;
2216 }
2217 }
2218 String::from_utf8(bytes).unwrap()
2219 }
2220
2221 /// Connect with a short disconnect timeout. These stubs do not answer a
2222 /// DISCONNECT, so the default would make every teardown wait out the full
2223 /// receipt timeout.
2224 async fn connect_for_test(addr: &str) -> Connection {
2225 Connection::connect_with_options(
2226 addr,
2227 "guest",
2228 "guest",
2229 "0,0",
2230 ConnectOptions::default().disconnect_timeout(Duration::from_millis(50)),
2231 )
2232 .await
2233 .unwrap()
2234 }
2235
2236 /// Read a header from a raw frame the way a broker does: the first
2237 /// occurrence wins, and the name is matched case-insensitively.
2238 fn header_value<'a>(frame: &'a str, name: &str) -> &'a str {
2239 frame
2240 .lines()
2241 .find_map(|line| {
2242 let (key, value) = line.split_once(':')?;
2243 key.eq_ignore_ascii_case(name).then_some(value)
2244 })
2245 .unwrap()
2246 }
2247
2248 fn start_receipt_rejection_server(
2249 message: &'static str,
2250 body: &'static str,
2251 response_delay: Duration,
2252 ) -> (SocketAddr, thread::JoinHandle<()>) {
2253 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
2254 let addr = listener.local_addr().unwrap();
2255
2256 let handle = thread::spawn(move || {
2257 if let Ok((mut stream, _)) = listener.accept() {
2258 let _connect = read_stomp_frame(&mut stream);
2259 stream
2260 .write_all(b"CONNECTED\nversion:1.2\nheart-beat:0,0\n\n\0")
2261 .unwrap();
2262 stream.flush().unwrap();
2263
2264 let send_frame = read_stomp_frame(&mut stream);
2265 let receipt_id = header_value(&send_frame, "receipt").to_string();
2266 thread::sleep(response_delay);
2267 let error_frame = format!(
2268 "ERROR\nreceipt-id:{}\nmessage:{}\n\n{}\0",
2269 receipt_id, message, body
2270 );
2271 stream.write_all(error_frame.as_bytes()).unwrap();
2272 stream.flush().unwrap();
2273
2274 thread::sleep(Duration::from_millis(100));
2275 }
2276 });
2277
2278 (addr, handle)
2279 }
2280
2281 fn start_receipt_success_server(
2282 response_delay: Duration,
2283 ) -> (SocketAddr, thread::JoinHandle<()>) {
2284 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
2285 let addr = listener.local_addr().unwrap();
2286
2287 let handle = thread::spawn(move || {
2288 if let Ok((mut stream, _)) = listener.accept() {
2289 let _connect = read_stomp_frame(&mut stream);
2290 stream
2291 .write_all(b"CONNECTED\nversion:1.2\nheart-beat:0,0\n\n\0")
2292 .unwrap();
2293 stream.flush().unwrap();
2294
2295 let send_frame = read_stomp_frame(&mut stream);
2296 let receipt_id = header_value(&send_frame, "receipt").to_string();
2297 thread::sleep(response_delay);
2298 let receipt_frame = format!("RECEIPT\nreceipt-id:{}\n\n\0", receipt_id);
2299 stream.write_all(receipt_frame.as_bytes()).unwrap();
2300 stream.flush().unwrap();
2301
2302 thread::sleep(Duration::from_millis(100));
2303 }
2304 });
2305
2306 (addr, handle)
2307 }
2308
2309 #[tokio::test]
2310 async fn connect_timeout_bounds_an_unreachable_broker() {
2311 // Bind, then drop the listener so the port is closed. A connect there
2312 // is refused immediately, and without a bound the retry loop would back
2313 // off and try again forever (#68). The bound must cut it short and hand
2314 // back the last I/O error it saw, not hang.
2315 let addr = {
2316 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
2317 listener.local_addr().unwrap()
2318 };
2319
2320 let start = std::time::Instant::now();
2321 let result = Connection::connect_with_options(
2322 &addr.to_string(),
2323 "guest",
2324 "guest",
2325 "0,0",
2326 ConnectOptions::default().connect_timeout(Duration::from_millis(200)),
2327 )
2328 .await;
2329 let elapsed = start.elapsed();
2330
2331 // `Connection` is not `Debug`, so inspect the error side only.
2332 let err = result.err();
2333 assert!(
2334 matches!(err, Some(ConnError::Io(_))),
2335 "expected an Io error, got {:?}",
2336 err
2337 );
2338 // The first refusal is instant; the bound fires during the 1s backoff
2339 // sleep that follows, well under a second.
2340 assert!(
2341 elapsed < Duration::from_secs(1),
2342 "connect should have given up near the 200ms bound, took {:?}",
2343 elapsed
2344 );
2345 }
2346
2347 #[tokio::test]
2348 async fn connect_timeout_surfaces_the_last_handshake_error() {
2349 // A broker that accepts the TCP connection then closes without ever
2350 // sending CONNECTED — a TLS-only port, say. The handshake fails with
2351 // ConnError::Protocol ("closed before CONNECTED received"), and the
2352 // bound must surface that real reason rather than a synthesized
2353 // timeout.
2354 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
2355 let addr = listener.local_addr().unwrap();
2356 let handle = thread::spawn(move || {
2357 if let Ok((mut stream, _)) = listener.accept() {
2358 let _connect = read_stomp_frame(&mut stream);
2359 // Drop the stream without answering: closed mid-handshake.
2360 }
2361 });
2362
2363 let result = Connection::connect_with_options(
2364 &addr.to_string(),
2365 "guest",
2366 "guest",
2367 "0,0",
2368 ConnectOptions::default().connect_timeout(Duration::from_millis(200)),
2369 )
2370 .await;
2371
2372 let err = result.err();
2373 assert!(
2374 matches!(err, Some(ConnError::Protocol(_))),
2375 "expected the handshake Protocol error, got {:?}",
2376 err
2377 );
2378 handle.join().unwrap();
2379 }
2380
2381 #[tokio::test]
2382 async fn connect_timeout_does_not_disturb_a_reachable_broker() {
2383 // A generous bound must not interfere with a broker that answers
2384 // promptly: the connection still succeeds.
2385 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
2386 let addr = listener.local_addr().unwrap();
2387 let handle = thread::spawn(move || {
2388 if let Ok((mut stream, _)) = listener.accept() {
2389 let _connect = read_stomp_frame(&mut stream);
2390 stream
2391 .write_all(b"CONNECTED\nversion:1.2\nheart-beat:0,0\n\n\0")
2392 .unwrap();
2393 stream.flush().unwrap();
2394 thread::sleep(Duration::from_millis(100));
2395 }
2396 });
2397
2398 let result = Connection::connect_with_options(
2399 &addr.to_string(),
2400 "guest",
2401 "guest",
2402 "0,0",
2403 ConnectOptions::default()
2404 .connect_timeout(Duration::from_secs(5))
2405 .disconnect_timeout(Duration::from_millis(50)),
2406 )
2407 .await;
2408
2409 assert!(
2410 result.is_ok(),
2411 "expected a connection, got error {:?}",
2412 result.as_ref().err()
2413 );
2414 handle.join().unwrap();
2415 }
2416
2417 #[test]
2418 fn deliver_and_keep_keeps_full_prunes_closed() {
2419 // A full channel is a slow-but-live consumer: drop the message, keep
2420 // the subscription. Fill a capacity-1 channel so the next send is Full.
2421 let (tx, _rx) = mpsc::channel::<Frame>(1);
2422 tx.try_send(Frame::new("MESSAGE")).unwrap();
2423 let full = SubscriptionEntry {
2424 id: "1".into(),
2425 sender: tx,
2426 ack: "auto".into(),
2427 headers: vec![],
2428 };
2429 assert!(
2430 deliver_and_keep(&full, &Frame::new("MESSAGE")),
2431 "a full (slow-consumer) channel must be kept, not pruned"
2432 );
2433
2434 // A closed channel means the receiving Subscription was dropped: prune.
2435 let (tx, rx) = mpsc::channel::<Frame>(1);
2436 drop(rx);
2437 let closed = SubscriptionEntry {
2438 id: "2".into(),
2439 sender: tx,
2440 ack: "auto".into(),
2441 headers: vec![],
2442 };
2443 assert!(
2444 !deliver_and_keep(&closed, &Frame::new("MESSAGE")),
2445 "a closed channel (dropped receiver) must be pruned"
2446 );
2447 }
2448
2449 #[tokio::test]
2450 async fn receipt_arriving_before_the_wait_is_not_lost() {
2451 // The broker answers with no delay, so the RECEIPT lands while the
2452 // caller is still holding the handle. Before #82 the background task
2453 // fired an orphaned sender and removed the registration, so the wait
2454 // below timed out on a frame the broker had already confirmed.
2455 let (addr, server) = start_receipt_success_server(Duration::ZERO);
2456 let addr = addr.to_string();
2457
2458 let conn = connect_for_test(&addr).await;
2459
2460 let handle = conn
2461 .send_frame_with_receipt(
2462 Frame::new("SEND")
2463 .header("destination", "/queue/fast")
2464 .set_body(b"payload".to_vec()),
2465 )
2466 .await
2467 .unwrap();
2468
2469 // Let the response arrive and be dispatched before anyone awaits it.
2470 tokio::time::sleep(Duration::from_millis(200)).await;
2471
2472 handle
2473 .wait(Duration::from_secs(2))
2474 .await
2475 .expect("RECEIPT delivered before the wait began must still resolve it");
2476
2477 let _ = conn.close().await;
2478 server.join().unwrap();
2479 }
2480
2481 #[tokio::test]
2482 async fn caller_supplied_receipt_header_is_replaced_not_appended() {
2483 // The stub echoes back the first receipt header it reads. If a
2484 // caller-set header survived, the broker would answer with that id
2485 // while the client tracked the generated one, and the wait would time
2486 // out. Mixed case, since header lookup is case-insensitive.
2487 let (addr, server) = start_receipt_success_server(Duration::ZERO);
2488 let addr = addr.to_string();
2489
2490 let conn = connect_for_test(&addr).await;
2491
2492 let handle = conn
2493 .send_frame_with_receipt(
2494 Frame::new("SEND")
2495 .header("destination", "/queue/test")
2496 .header("Receipt", "caller-set")
2497 .set_body(b"payload".to_vec()),
2498 )
2499 .await
2500 .unwrap();
2501
2502 assert_ne!(handle.receipt_id(), "caller-set");
2503
2504 handle
2505 .wait(Duration::from_secs(2))
2506 .await
2507 .expect("generated receipt id must be the only one on the wire");
2508
2509 let _ = conn.close().await;
2510 server.join().unwrap();
2511 }
2512
2513 #[tokio::test]
2514 async fn error_with_receipt_id_rejects_confirmed_send_and_stays_inbound() {
2515 let (addr, server) =
2516 start_receipt_rejection_server("publish denied", "not allowed", Duration::ZERO);
2517 let addr = addr.to_string();
2518
2519 let conn = connect_for_test(&addr).await;
2520
2521 let result = conn
2522 .send_frame_confirmed(
2523 Frame::new("SEND")
2524 .header("destination", "/queue/forbidden")
2525 .set_body(b"payload".to_vec()),
2526 Duration::from_secs(2),
2527 )
2528 .await;
2529
2530 let rejected_receipt_id = match result {
2531 Err(ConnError::FrameRejected(err)) => {
2532 assert_eq!(err.message, "publish denied");
2533 assert_eq!(err.body, Some("not allowed".to_string()));
2534 err.receipt_id
2535 .expect("frame rejection should include receipt-id")
2536 }
2537 other => panic!("expected FrameRejected, got {:?}", other),
2538 };
2539
2540 let received = tokio::time::timeout(Duration::from_secs(1), conn.next_frame())
2541 .await
2542 .unwrap();
2543 match received {
2544 Some(ReceivedFrame::Error(err)) => {
2545 assert_eq!(err.message, "publish denied");
2546 assert_eq!(err.body, Some("not allowed".to_string()));
2547 assert_eq!(err.receipt_id, Some(rejected_receipt_id));
2548 }
2549 other => panic!("expected forwarded ERROR frame, got {:?}", other),
2550 }
2551
2552 let _ = conn.close().await;
2553 server.join().unwrap();
2554 }
2555
2556 #[tokio::test]
2557 async fn wait_for_receipt_reports_frame_rejection_and_keeps_error_inbound() {
2558 let (addr, server) = start_receipt_rejection_server(
2559 "receipt rejected",
2560 "permission denied",
2561 Duration::from_millis(100),
2562 );
2563 let addr = addr.to_string();
2564
2565 let conn = connect_for_test(&addr).await;
2566
2567 let handle = conn
2568 .send_frame_with_receipt(
2569 Frame::new("SEND")
2570 .header("destination", "/queue/forbidden")
2571 .set_body(b"payload".to_vec()),
2572 )
2573 .await
2574 .unwrap();
2575
2576 let receipt_id = handle.receipt_id().to_string();
2577 let result = handle.wait(Duration::from_secs(2)).await;
2578
2579 match result {
2580 Err(ConnError::FrameRejected(err)) => {
2581 assert_eq!(err.message, "receipt rejected");
2582 assert_eq!(err.body, Some("permission denied".to_string()));
2583 assert_eq!(err.receipt_id, Some(receipt_id.clone()));
2584 }
2585 other => panic!("expected FrameRejected, got {:?}", other),
2586 }
2587
2588 let received = tokio::time::timeout(Duration::from_secs(1), conn.next_frame())
2589 .await
2590 .unwrap();
2591 match received {
2592 Some(ReceivedFrame::Error(err)) => {
2593 assert_eq!(err.message, "receipt rejected");
2594 assert_eq!(err.body, Some("permission denied".to_string()));
2595 assert_eq!(err.receipt_id, Some(receipt_id));
2596 }
2597 other => panic!("expected forwarded ERROR frame, got {:?}", other),
2598 }
2599
2600 let _ = conn.close().await;
2601 server.join().unwrap();
2602 }
2603
2604 #[tokio::test]
2605 async fn test_cumulative_ack_removes_prefix() {
2606 // setup channels
2607 let (out_tx, mut out_rx) = mpsc::channel::<StompItem>(8);
2608 let (_in_tx, in_rx) = mpsc::channel::<Frame>(8);
2609 let (shutdown_tx, _) = broadcast::channel::<()>(1);
2610
2611 let subscriptions: Arc<Mutex<Subscriptions>> = Arc::new(Mutex::new(HashMap::new()));
2612 let pending: Arc<Mutex<PendingMap>> = Arc::new(Mutex::new(HashMap::new()));
2613
2614 let sub_id_counter = Arc::new(AtomicU64::new(1));
2615
2616 // create a subscription entry s1 with client (cumulative) ack
2617 let (sub_sender, _sub_rx) = mpsc::channel::<Frame>(4);
2618 {
2619 let mut map = subscriptions.lock().await;
2620 map.insert(
2621 "/queue/x".to_string(),
2622 vec![SubscriptionEntry {
2623 id: "s1".to_string(),
2624 sender: sub_sender,
2625 ack: "client".to_string(),
2626 headers: Vec::new(),
2627 }],
2628 );
2629 }
2630
2631 // fill pending queue for s1: m1,m2,m3
2632 {
2633 let mut p = pending.lock().await;
2634 let mut q = VecDeque::new();
2635 q.push_back((
2636 "m1".to_string(),
2637 make_message("m1", Some("s1"), Some("/queue/x")),
2638 ));
2639 q.push_back((
2640 "m2".to_string(),
2641 make_message("m2", Some("s1"), Some("/queue/x")),
2642 ));
2643 q.push_back((
2644 "m3".to_string(),
2645 make_message("m3", Some("s1"), Some("/queue/x")),
2646 ));
2647 p.insert("s1".to_string(), q);
2648 }
2649
2650 let conn = Connection {
2651 outbound_tx: out_tx,
2652 inbound_rx: Arc::new(Mutex::new(in_rx)),
2653 shutdown_tx,
2654 subscriptions: subscriptions.clone(),
2655 sub_id_counter,
2656 pending: pending.clone(),
2657 pending_receipts: Arc::new(Mutex::new(HashMap::new())),
2658 disconnect_timeout: Connection::DEFAULT_DISCONNECT_TIMEOUT,
2659 };
2660
2661 // ack m2 cumulatively: should remove m1 and m2, leaving m3
2662 conn.ack("s1", "m2").await.expect("ack failed");
2663
2664 // verify pending for s1 contains only m3
2665 {
2666 let p = pending.lock().await;
2667 let q = p.get("s1").expect("missing s1");
2668 assert_eq!(q.len(), 1);
2669 assert_eq!(q.front().unwrap().0, "m3");
2670 }
2671
2672 // verify an ACK frame was emitted
2673 if let Some(item) = out_rx.recv().await {
2674 match item {
2675 StompItem::Frame(f) => assert_eq!(f.command, "ACK"),
2676 _ => panic!("expected frame"),
2677 }
2678 } else {
2679 panic!("no outbound frame sent")
2680 }
2681 }
2682
2683 #[tokio::test]
2684 async fn test_client_individual_ack_removes_only_one() {
2685 // setup channels
2686 let (out_tx, mut out_rx) = mpsc::channel::<StompItem>(8);
2687 let (_in_tx, in_rx) = mpsc::channel::<Frame>(8);
2688 let (shutdown_tx, _) = broadcast::channel::<()>(1);
2689
2690 let subscriptions: Arc<Mutex<Subscriptions>> = Arc::new(Mutex::new(HashMap::new()));
2691 let pending: Arc<Mutex<PendingMap>> = Arc::new(Mutex::new(HashMap::new()));
2692
2693 let sub_id_counter = Arc::new(AtomicU64::new(1));
2694
2695 // create a subscription entry s2 with client-individual ack
2696 let (sub_sender, _sub_rx) = mpsc::channel::<Frame>(4);
2697 {
2698 let mut map = subscriptions.lock().await;
2699 map.insert(
2700 "/queue/y".to_string(),
2701 vec![SubscriptionEntry {
2702 id: "s2".to_string(),
2703 sender: sub_sender,
2704 ack: "client-individual".to_string(),
2705 headers: Vec::new(),
2706 }],
2707 );
2708 }
2709
2710 // fill pending queue for s2: a,b,c
2711 {
2712 let mut p = pending.lock().await;
2713 let mut q = VecDeque::new();
2714 q.push_back((
2715 "a".to_string(),
2716 make_message("a", Some("s2"), Some("/queue/y")),
2717 ));
2718 q.push_back((
2719 "b".to_string(),
2720 make_message("b", Some("s2"), Some("/queue/y")),
2721 ));
2722 q.push_back((
2723 "c".to_string(),
2724 make_message("c", Some("s2"), Some("/queue/y")),
2725 ));
2726 p.insert("s2".to_string(), q);
2727 }
2728
2729 let conn = Connection {
2730 outbound_tx: out_tx,
2731 inbound_rx: Arc::new(Mutex::new(in_rx)),
2732 shutdown_tx,
2733 subscriptions: subscriptions.clone(),
2734 sub_id_counter,
2735 pending: pending.clone(),
2736 pending_receipts: Arc::new(Mutex::new(HashMap::new())),
2737 disconnect_timeout: Connection::DEFAULT_DISCONNECT_TIMEOUT,
2738 };
2739
2740 // ack only 'b' individually
2741 conn.ack("s2", "b").await.expect("ack failed");
2742
2743 // verify pending for s2 contains a and c
2744 {
2745 let p = pending.lock().await;
2746 let q = p.get("s2").expect("missing s2");
2747 assert_eq!(q.len(), 2);
2748 assert_eq!(q[0].0, "a");
2749 assert_eq!(q[1].0, "c");
2750 }
2751
2752 // verify an ACK frame was emitted
2753 if let Some(item) = out_rx.recv().await {
2754 match item {
2755 StompItem::Frame(f) => assert_eq!(f.command, "ACK"),
2756 _ => panic!("expected frame"),
2757 }
2758 } else {
2759 panic!("no outbound frame sent")
2760 }
2761 }
2762
2763 #[tokio::test]
2764 async fn test_subscription_receive_delivers_message() {
2765 // setup channels
2766 let (out_tx, _out_rx) = mpsc::channel::<StompItem>(8);
2767 let (_in_tx, in_rx) = mpsc::channel::<Frame>(8);
2768 let (shutdown_tx, _) = broadcast::channel::<()>(1);
2769
2770 let subscriptions: Arc<Mutex<Subscriptions>> = Arc::new(Mutex::new(HashMap::new()));
2771 let pending: Arc<Mutex<PendingMap>> = Arc::new(Mutex::new(HashMap::new()));
2772
2773 let sub_id_counter = Arc::new(AtomicU64::new(1));
2774
2775 let conn = Connection {
2776 outbound_tx: out_tx,
2777 inbound_rx: Arc::new(Mutex::new(in_rx)),
2778 shutdown_tx,
2779 subscriptions: subscriptions.clone(),
2780 sub_id_counter,
2781 pending: pending.clone(),
2782 pending_receipts: Arc::new(Mutex::new(HashMap::new())),
2783 disconnect_timeout: Connection::DEFAULT_DISCONNECT_TIMEOUT,
2784 };
2785
2786 // subscribe
2787 let subscription = conn
2788 .subscribe("/queue/test", AckMode::Auto)
2789 .await
2790 .expect("subscribe failed");
2791
2792 // find the sender stored in the subscriptions map and push a message
2793 {
2794 let map = conn.subscriptions.lock().await;
2795 let vec = map.get("/queue/test").expect("missing subscription vec");
2796 let sender = &vec[0].sender;
2797 let f = make_message("m1", Some(&vec[0].id), Some("/queue/test"));
2798 sender.try_send(f).expect("send to subscription failed");
2799 }
2800
2801 // consume from the subscription receiver
2802 let mut rx = subscription.into_receiver();
2803 if let Some(received) = rx.recv().await {
2804 assert_eq!(received.command, "MESSAGE");
2805 // message-id header should be present
2806 let mut found = false;
2807 for (k, _v) in &received.headers {
2808 if k.to_lowercase() == "message-id" {
2809 found = true;
2810 break;
2811 }
2812 }
2813 assert!(found, "message-id header missing");
2814 } else {
2815 panic!("no message received on subscription")
2816 }
2817 }
2818
2819 #[tokio::test]
2820 async fn test_subscription_ack_removes_pending_and_sends_ack() {
2821 // setup channels
2822 let (out_tx, mut out_rx) = mpsc::channel::<StompItem>(8);
2823 let (_in_tx, in_rx) = mpsc::channel::<Frame>(8);
2824 let (shutdown_tx, _) = broadcast::channel::<()>(1);
2825
2826 let subscriptions: Arc<Mutex<Subscriptions>> = Arc::new(Mutex::new(HashMap::new()));
2827 let pending: Arc<Mutex<PendingMap>> = Arc::new(Mutex::new(HashMap::new()));
2828
2829 let sub_id_counter = Arc::new(AtomicU64::new(1));
2830
2831 let conn = Connection {
2832 outbound_tx: out_tx,
2833 inbound_rx: Arc::new(Mutex::new(in_rx)),
2834 shutdown_tx,
2835 subscriptions: subscriptions.clone(),
2836 sub_id_counter,
2837 pending: pending.clone(),
2838 pending_receipts: Arc::new(Mutex::new(HashMap::new())),
2839 disconnect_timeout: Connection::DEFAULT_DISCONNECT_TIMEOUT,
2840 };
2841
2842 // subscribe with client ack
2843 let subscription = conn
2844 .subscribe("/queue/ack", AckMode::Client)
2845 .await
2846 .expect("subscribe failed");
2847
2848 let sub_id = subscription.id().to_string();
2849
2850 // drain any initial outbound frames (SUBSCRIBE) emitted by subscribe()
2851 while out_rx.try_recv().is_ok() {}
2852
2853 // populate pending queue for this subscription
2854 {
2855 let mut p = conn.pending.lock().await;
2856 let mut q = VecDeque::new();
2857 q.push_back((
2858 "mid-1".to_string(),
2859 make_message("mid-1", Some(&sub_id), Some("/queue/ack")),
2860 ));
2861 p.insert(sub_id.clone(), q);
2862 }
2863
2864 // ack the message via the subscription helper
2865 subscription.ack("mid-1").await.expect("ack failed");
2866
2867 // ensure pending queue no longer contains the message
2868 {
2869 let p = conn.pending.lock().await;
2870 assert!(p.get(&sub_id).is_none() || p.get(&sub_id).unwrap().is_empty());
2871 }
2872
2873 // verify an ACK frame was emitted
2874 if let Some(item) = out_rx.recv().await {
2875 match item {
2876 StompItem::Frame(f) => assert_eq!(f.command, "ACK"),
2877 _ => panic!("expected frame"),
2878 }
2879 } else {
2880 panic!("no outbound frame sent")
2881 }
2882 }
2883
2884 // Helper function to create a test connection and output receiver
2885 fn setup_test_connection() -> (Connection, mpsc::Receiver<StompItem>) {
2886 let (out_tx, out_rx) = mpsc::channel::<StompItem>(8);
2887 let (_in_tx, in_rx) = mpsc::channel::<Frame>(8);
2888 let (shutdown_tx, _) = broadcast::channel::<()>(1);
2889
2890 let subscriptions: Arc<Mutex<Subscriptions>> = Arc::new(Mutex::new(HashMap::new()));
2891 let pending: Arc<Mutex<PendingMap>> = Arc::new(Mutex::new(HashMap::new()));
2892 let sub_id_counter = Arc::new(AtomicU64::new(1));
2893
2894 let conn = Connection {
2895 outbound_tx: out_tx,
2896 inbound_rx: Arc::new(Mutex::new(in_rx)),
2897 shutdown_tx,
2898 subscriptions,
2899 sub_id_counter,
2900 pending,
2901 pending_receipts: Arc::new(Mutex::new(HashMap::new())),
2902 disconnect_timeout: Connection::DEFAULT_DISCONNECT_TIMEOUT,
2903 };
2904
2905 (conn, out_rx)
2906 }
2907
2908 // Helper function to verify a frame with a transaction header
2909 fn verify_transaction_frame(frame: Frame, expected_command: &str, expected_tx_id: &str) {
2910 assert_eq!(frame.command, expected_command);
2911 assert!(
2912 frame
2913 .headers
2914 .iter()
2915 .any(|(k, v)| k == "transaction" && v == expected_tx_id),
2916 "transaction header with id '{}' not found",
2917 expected_tx_id
2918 );
2919 }
2920
2921 #[tokio::test]
2922 async fn test_begin_transaction_sends_frame() {
2923 let (conn, mut out_rx) = setup_test_connection();
2924
2925 conn.begin("tx1").await.expect("begin failed");
2926
2927 // verify BEGIN frame was emitted
2928 if let Some(StompItem::Frame(f)) = out_rx.recv().await {
2929 verify_transaction_frame(f, "BEGIN", "tx1");
2930 } else {
2931 panic!("no outbound frame sent")
2932 }
2933 }
2934
2935 #[tokio::test]
2936 async fn test_commit_transaction_sends_frame() {
2937 let (conn, mut out_rx) = setup_test_connection();
2938
2939 conn.commit("tx1").await.expect("commit failed");
2940
2941 // verify COMMIT frame was emitted
2942 if let Some(StompItem::Frame(f)) = out_rx.recv().await {
2943 verify_transaction_frame(f, "COMMIT", "tx1");
2944 } else {
2945 panic!("no outbound frame sent")
2946 }
2947 }
2948
2949 #[tokio::test]
2950 async fn test_abort_transaction_sends_frame() {
2951 let (conn, mut out_rx) = setup_test_connection();
2952
2953 conn.abort("tx1").await.expect("abort failed");
2954
2955 // verify ABORT frame was emitted
2956 if let Some(StompItem::Frame(f)) = out_rx.recv().await {
2957 verify_transaction_frame(f, "ABORT", "tx1");
2958 } else {
2959 panic!("no outbound frame sent")
2960 }
2961 }
2962
2963 #[tokio::test]
2964 async fn test_send_convenience_produces_correct_frame() {
2965 let (conn, mut out_rx) = setup_test_connection();
2966
2967 conn.send("/queue/events", "hello world")
2968 .await
2969 .expect("send failed");
2970
2971 if let Some(StompItem::Frame(f)) = out_rx.recv().await {
2972 assert_eq!(f.command, "SEND");
2973 assert_eq!(f.get_header("destination"), Some("/queue/events"));
2974 assert_eq!(f.body, b"hello world");
2975 } else {
2976 panic!("no outbound frame sent")
2977 }
2978 }
2979
2980 #[test]
2981 fn test_extract_destination_from_error_header() {
2982 // When ERROR frame has destination header, extract it directly
2983 let frame = Frame::new("ERROR")
2984 .header("message", "AMQ339016: Error creating STOMP subscription")
2985 .header("destination", "/topic/test.restricted");
2986
2987 let dest = extract_destination_from_error(&frame);
2988 assert_eq!(dest, Some("/topic/test.restricted".to_string()));
2989 }
2990
2991 #[test]
2992 fn test_extract_destination_from_error_message() {
2993 // When destination is in message header text
2994 let frame = Frame::new("ERROR").header(
2995 "message",
2996 "AMQ339016: Error creating subscription for /topic/test.restricted",
2997 );
2998
2999 let dest = extract_destination_from_error(&frame);
3000 assert_eq!(dest, Some("/topic/test.restricted".to_string()));
3001 }
3002
3003 #[test]
3004 fn test_extract_destination_from_error_body() {
3005 // When destination is in body text
3006 let frame = Frame::new("ERROR")
3007 .header("message", "AMQ339016: Error creating subscription")
3008 .set_body(b"User guest is not authorized for /queue/orders".to_vec());
3009
3010 let dest = extract_destination_from_error(&frame);
3011 assert_eq!(dest, Some("/queue/orders".to_string()));
3012 }
3013
3014 #[test]
3015 fn test_extract_destination_from_error_none() {
3016 // When no destination can be identified
3017 let frame = Frame::new("ERROR").header("message", "Generic error without destination info");
3018
3019 let dest = extract_destination_from_error(&frame);
3020 assert_eq!(dest, None);
3021 }
3022
3023 #[test]
3024 fn test_extract_destination_from_error_with_trailing_punct() {
3025 // When destination has trailing punctuation
3026 let frame = Frame::new("ERROR").header(
3027 "message",
3028 "Error for /topic/events, please check permissions",
3029 );
3030
3031 let dest = extract_destination_from_error(&frame);
3032 assert_eq!(dest, Some("/topic/events".to_string()));
3033 }
3034
3035 #[test]
3036 fn test_extract_subscription_id_from_error_artemis_format() {
3037 // Artemis format: "AMQ339016 Error creating subscription 1"
3038 let frame =
3039 Frame::new("ERROR").header("message", "AMQ339016 Error creating subscription 1");
3040
3041 let sub_id = extract_subscription_id_from_error(&frame);
3042 assert_eq!(sub_id, Some("1".to_string()));
3043 }
3044
3045 #[test]
3046 fn test_extract_subscription_id_from_error_numeric() {
3047 // Multiple digit subscription ID
3048 let frame = Frame::new("ERROR").header("message", "Error for subscription 123 on server");
3049
3050 let sub_id = extract_subscription_id_from_error(&frame);
3051 assert_eq!(sub_id, Some("123".to_string()));
3052 }
3053
3054 #[test]
3055 fn test_extract_subscription_id_from_error_none() {
3056 // No subscription ID in error
3057 let frame = Frame::new("ERROR").header("message", "Generic connection error");
3058
3059 let sub_id = extract_subscription_id_from_error(&frame);
3060 assert_eq!(sub_id, None);
3061 }
3062
3063 #[tokio::test]
3064 async fn test_lookup_destination_by_sub_id() {
3065 let subscriptions: Arc<Mutex<Subscriptions>> = Arc::new(Mutex::new(HashMap::new()));
3066 let (sender, _rx) = mpsc::channel::<Frame>(4);
3067
3068 // Add a subscription
3069 {
3070 let mut map = subscriptions.lock().await;
3071 map.insert(
3072 "/topic/test.restricted".to_string(),
3073 vec![SubscriptionEntry {
3074 id: "1".to_string(),
3075 sender,
3076 ack: "auto".to_string(),
3077 headers: Vec::new(),
3078 }],
3079 );
3080 }
3081
3082 // Should find the destination
3083 let dest = lookup_destination_by_sub_id("1", &subscriptions).await;
3084 assert_eq!(dest, Some("/topic/test.restricted".to_string()));
3085
3086 // Should not find non-existent subscription
3087 let dest = lookup_destination_by_sub_id("999", &subscriptions).await;
3088 assert_eq!(dest, None);
3089 }
3090}