Skip to main content

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}