Skip to main content

monocoque_zmtp/
xpub.rs

1//! XPUB (Extended Publisher) socket implementation
2//!
3//! XPUB extends PUB by receiving subscription messages from subscribers,
4//! enabling manual subscription control, last value cache patterns, and
5//! subscription forwarding in message brokers.
6//!
7//! # Use Cases
8//!
9//! - **Message brokers**: Forward subscriptions between frontend and backend
10//! - **Last value cache (LVC)**: Track subscriptions and replay latest values
11//! - **Subscription auditing**: Monitor what topics subscribers are interested in
12//! - **Manual control**: Explicitly approve/deny subscriptions
13//!
14//! # Pattern
15//!
16//! ```text
17//! Subscriber 1 ──subscribe("topic.a")──> ┐
18//! Subscriber 2 ──subscribe("topic.b")──> ├─> XPUB (receives subscription events)
19//! Subscriber 3 ──unsubscribe("topic.a")─> ┘        │
20//!                                                   │
21//!                                      XPUB ────────┴──> Forwards subscriptions
22//! ```
23
24use bytes::Bytes;
25use monocoque_core::options::SocketOptions;
26use monocoque_core::rt::{TcpListener, TcpStream};
27use monocoque_core::subscription::{SubscriptionEvent, SubscriptionTrie};
28use smallvec::SmallVec;
29use std::collections::{HashMap, HashSet};
30use std::fmt;
31use std::io;
32use tracing::{debug, trace};
33
34use crate::handshake::perform_handshake_with_options;
35use crate::session::SocketType;
36use crate::xsub::XSubSocket;
37
38/// Unique identifier for each subscriber connection
39type SubscriberId = u64;
40
41/// Per-subscriber state managed by XPUB
42struct XPubSubscriber {
43    id: SubscriberId,
44    stream: TcpStream,
45    subscriptions: SubscriptionTrie,
46    recv_buf: monocoque_core::buffer::SegmentedBuffer,
47    decoder: crate::codec::ZmtpDecoder,
48    curve_cipher: Option<crate::security::curve::CurveMessageCipher>,
49}
50
51impl XPubSubscriber {
52    /// Check if message matches subscriber's subscriptions
53    fn matches(&self, msg: &[Bytes]) -> bool {
54        // Check first frame against subscription prefixes
55        if let Some(first_frame) = msg.first() {
56            self.subscriptions.matches(first_frame)
57        } else {
58            false
59        }
60    }
61}
62
63/// XPUB (Extended Publisher) socket.
64///
65/// Receives subscription events and broadcasts messages to matching subscribers.
66///
67/// # Features
68///
69/// - **Subscription tracking**: Know what topics subscribers want
70/// - **Verbose mode**: Report all subscriptions (including duplicates)
71/// - **Manual mode**: Explicit subscription control
72/// - **Welcome messages**: Send initial message to new subscribers
73///
74/// # Examples
75///
76/// ```no_run
77/// use monocoque_zmtp::xpub::XPubSocket;
78/// use bytes::Bytes;
79///
80/// # async fn example() -> std::io::Result<()> {
81/// let mut xpub = XPubSocket::bind("127.0.0.1:5555").await?;
82///     
83///     loop {
84///         // Receive subscription events from subscribers
85///         if let Some(event) = xpub.recv_subscription().await? {
86///             println!("Subscription event: {:?}", event);
87///         }
88///         
89///         // Broadcast messages to matching subscribers
90///         xpub.send(vec![Bytes::from("topic"), Bytes::from("data")]).await?;
91///     }
92/// # }
93/// ```
94pub struct XPubSocket {
95    listener: TcpListener,
96    subscribers: HashMap<SubscriberId, XPubSubscriber>,
97    next_id: SubscriberId,
98    options: SocketOptions,
99    /// Pending subscription events to deliver
100    pending_events: SmallVec<[SubscriptionEvent; 8]>,
101    /// Optional upstream connection for manual-mode subscription forwarding.
102    ///
103    /// When set, `send_subscription()` writes subscription events to this
104    /// connection so they propagate to the upstream publisher.
105    upstream: Option<XSubSocket<TcpStream>>,
106    /// Tracks which unique topic prefixes currently have at least one subscriber.
107    ///
108    /// Used in non-verbose mode to deliver an event only the FIRST time a topic
109    /// is subscribed (and when it transitions back to zero subscribers).
110    seen_topics: HashSet<Vec<u8>>,
111    /// Reference-count of active subscriptions per topic prefix.
112    ///
113    /// Maps topic prefix → number of active subscribers interested in it.
114    /// When the count drops to zero, the topic is removed from `seen_topics`
115    /// and an Unsubscribe event is delivered.
116    topic_refcount: HashMap<Vec<u8>, usize>,
117}
118
119impl XPubSocket {
120    /// Bind to an address and start listening for subscribers.
121    ///
122    /// # Examples
123    ///
124    /// ```no_run
125    /// # use monocoque_zmtp::xpub::XPubSocket;
126    /// # async fn example() -> std::io::Result<()> {
127    /// let xpub = XPubSocket::bind("127.0.0.1:5555").await?;
128    /// # Ok(())
129    /// # }
130    /// ```
131    pub async fn bind(addr: &str) -> io::Result<Self> {
132        Self::bind_with_options(addr, SocketOptions::default()).await
133    }
134
135    /// Bind with custom socket options.
136    pub async fn bind_with_options(addr: &str, options: SocketOptions) -> io::Result<Self> {
137        let listener = TcpListener::bind(addr).await?;
138        let local_addr = listener.local_addr()?;
139        debug!("[XPUB] Bound to {}", local_addr);
140
141        Ok(Self {
142            listener,
143            subscribers: HashMap::new(),
144            next_id: 1,
145            options,
146            pending_events: SmallVec::new(),
147            upstream: None,
148            seen_topics: HashSet::new(),
149            topic_refcount: HashMap::new(),
150        })
151    }
152
153    /// Accept new subscriber connections (non-blocking).
154    ///
155    /// Call this periodically to accept new subscribers.
156    pub async fn accept(&mut self) -> io::Result<()> {
157        match self.listener.accept().await {
158            Ok((mut stream, addr)) => {
159                debug!("[XPUB] New subscriber from {}", addr);
160
161                // Enable TCP_NODELAY (and keepalive) on the accepted subscriber,
162                // matching PUB's accept path. One-time setsockopt at accept, not
163                // in the publish hot path.
164                crate::utils::configure_tcp_stream(&stream, &self.options, "XPUB")?;
165
166                // Perform ZMTP handshake
167                let handshake_result = perform_handshake_with_options(
168                    &mut stream,
169                    SocketType::Xpub,
170                    None,
171                    Some(self.options.handshake_timeout),
172                    &self.options,
173                )
174                .await?;
175
176                debug!(
177                    peer_socket_type = ?handshake_result.peer_socket_type,
178                    "[XPUB] Handshake complete with subscriber"
179                );
180
181                // Add subscriber
182                let id = self.next_id;
183                self.next_id += 1;
184
185                let mut curve_cipher = handshake_result.curve_cipher;
186
187                // Send welcome message if configured
188                if let Some(ref welcome_msg) = self.options.xpub_welcome_msg.clone() {
189                    use bytes::BytesMut;
190                    use compio_buf::BufResult;
191                    use compio_io::AsyncWriteExt;
192
193                    let wire = if let Some(ref mut cipher) = curve_cipher {
194                        let mut buf = BytesMut::new();
195                        let body = cipher.encrypt_frame(welcome_msg, false).map_err(|e| {
196                            io::Error::new(io::ErrorKind::InvalidData, e.to_string())
197                        })?;
198                        crate::base::append_zmtp_cmd_frame(&mut buf, &body);
199                        buf.freeze()
200                    } else {
201                        let mut buf = BytesMut::with_capacity(welcome_msg.len() + 9);
202                        crate::codec::encode_multipart(std::slice::from_ref(welcome_msg), &mut buf);
203                        buf.freeze()
204                    };
205
206                    let BufResult(result, _) = stream.write_all(wire).await;
207                    if let Err(e) = result {
208                        trace!(
209                            "[XPUB] Failed to send welcome message to subscriber {}: {}",
210                            id, e
211                        );
212                    }
213                }
214
215                self.subscribers.insert(
216                    id,
217                    XPubSubscriber {
218                        id,
219                        stream,
220                        subscriptions: SubscriptionTrie::new(),
221                        recv_buf: monocoque_core::buffer::SegmentedBuffer::new(),
222                        decoder: self.options.max_msg_size.map_or_else(
223                            crate::codec::ZmtpDecoder::new,
224                            crate::codec::ZmtpDecoder::with_max_frame_size,
225                        ),
226                        curve_cipher,
227                    },
228                );
229
230                debug!(
231                    "[XPUB] Subscriber {} added (total: {})",
232                    id,
233                    self.subscribers.len()
234                );
235                Ok(())
236            }
237            Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
238                // No pending connections
239                Ok(())
240            }
241            Err(e) => Err(e),
242        }
243    }
244
245    /// Receive a subscription event from subscribers (non-blocking).
246    ///
247    /// Returns `None` if no events are available.
248    ///
249    /// # Examples
250    ///
251    /// ```no_run
252    /// # use monocoque_zmtp::xpub::XPubSocket;
253    /// # async fn example(mut xpub: XPubSocket) -> std::io::Result<()> {
254    /// if let Some(event) = xpub.recv_subscription().await? {
255    ///     match event {
256    ///         monocoque_core::subscription::SubscriptionEvent::Subscribe(topic) => {
257    ///             println!("New subscription: {:?}", topic);
258    ///         }
259    ///         monocoque_core::subscription::SubscriptionEvent::Unsubscribe(topic) => {
260    ///             println!("Unsubscription: {:?}", topic);
261    ///         }
262    ///     }
263    /// }
264    /// # Ok(())
265    /// # }
266    /// ```
267    #[allow(clippy::too_many_lines)]
268    pub async fn recv_subscription(&mut self) -> io::Result<Option<SubscriptionEvent>> {
269        use compio_buf::BufResult;
270        use compio_io::AsyncRead;
271        use monocoque_core::rt::timeout;
272        use std::time::Duration;
273
274        // Return pending events first
275        if !self.pending_events.is_empty() {
276            return Ok(Some(self.pending_events.remove(0)));
277        }
278
279        // NOTE: Don't call accept() here - it blocks waiting for new connections
280        // The caller should call accept() separately to handle new connections
281
282        // Poll all subscribers for subscription messages
283        trace!(
284            "[XPUB] Polling {} subscribers for subscription events",
285            self.subscribers.len()
286        );
287        for sub in self.subscribers.values_mut() {
288            let buf = vec![0u8; 256];
289
290            // Use a short timeout to avoid blocking
291            let read_result = timeout(Duration::from_millis(1), sub.stream.read(buf)).await;
292
293            match read_result {
294                Ok(BufResult(Ok(n), buf)) if n > 0 => {
295                    trace!("[XPUB] Received {} bytes from subscriber {}", n, sub.id);
296                    sub.recv_buf.push(bytes::Bytes::from(buf[..n].to_vec()));
297
298                    // Drain all complete ZMTP frames from the buffer
299                    loop {
300                        match sub.decoder.decode(&mut sub.recv_buf) {
301                            Ok(Some(frame)) => {
302                                // Resolve the subscription payload, handling CURVE decryption.
303                                let payload = if frame.is_command() {
304                                    if let Some(ref mut cipher) = sub.curve_cipher {
305                                        if crate::security::curve::CurveMessageCipher::is_curve_message(&frame.payload) {
306                                            match cipher.decrypt_frame(&frame.payload) {
307                                                Ok((_more, data)) => data,
308                                                Err(_) => continue,
309                                            }
310                                        } else {
311                                            // Non-MESSAGE command (e.g. PING): handle and skip.
312                                            if crate::base::is_ping_payload(&frame.payload) {
313                                                use compio_io::AsyncWriteExt;
314                                                let pong = crate::base::build_pong_frame();
315                                                let BufResult(result, _) = sub.stream.write_all(pong).await;
316                                                let _ = result;
317                                            }
318                                            continue;
319                                        }
320                                    } else {
321                                        if crate::base::is_ping_payload(&frame.payload) {
322                                            use compio_io::AsyncWriteExt;
323                                            let pong = crate::base::build_pong_frame();
324                                            let BufResult(result, _) =
325                                                sub.stream.write_all(pong).await;
326                                            let _ = result;
327                                        }
328                                        continue;
329                                    }
330                                } else {
331                                    frame.payload
332                                };
333                                if let Some(event) = SubscriptionEvent::from_message(&payload) {
334                                    trace!(
335                                        "[XPUB] Subscription event from subscriber {}: {:?}",
336                                        sub.id, event
337                                    );
338
339                                    let should_deliver = if self.options.xpub_verbose {
340                                        // Verbose mode: always deliver every event
341                                        match &event {
342                                            SubscriptionEvent::Subscribe(prefix) => {
343                                                sub.subscriptions.subscribe(prefix.clone());
344                                                let key = prefix.to_vec();
345                                                *self
346                                                    .topic_refcount
347                                                    .entry(key.clone())
348                                                    .or_insert(0) += 1;
349                                                self.seen_topics.insert(key);
350                                            }
351                                            SubscriptionEvent::Unsubscribe(prefix) => {
352                                                sub.subscriptions.unsubscribe(prefix);
353                                                let key = prefix.to_vec();
354                                                let count = self
355                                                    .topic_refcount
356                                                    .entry(key.clone())
357                                                    .or_insert(0);
358                                                if *count > 0 {
359                                                    *count -= 1;
360                                                }
361                                                if *count == 0 {
362                                                    self.seen_topics.remove(&key);
363                                                    self.topic_refcount.remove(&key);
364                                                }
365                                            }
366                                        }
367                                        true
368                                    } else {
369                                        // Non-verbose mode: deliver only on first subscribe / last unsubscribe
370                                        match &event {
371                                            SubscriptionEvent::Subscribe(prefix) => {
372                                                sub.subscriptions.subscribe(prefix.clone());
373                                                let key = prefix.to_vec();
374                                                let count = self
375                                                    .topic_refcount
376                                                    .entry(key.clone())
377                                                    .or_insert(0);
378                                                *count += 1;
379                                                if *count == 1 {
380                                                    // First subscriber for this topic
381                                                    self.seen_topics.insert(key);
382                                                    true
383                                                } else {
384                                                    false
385                                                }
386                                            }
387                                            SubscriptionEvent::Unsubscribe(prefix) => {
388                                                sub.subscriptions.unsubscribe(prefix);
389                                                let key = prefix.to_vec();
390                                                let count = self
391                                                    .topic_refcount
392                                                    .entry(key.clone())
393                                                    .or_insert(0);
394                                                if *count > 0 {
395                                                    *count -= 1;
396                                                }
397                                                if *count == 0 {
398                                                    // Last subscriber gone for this topic
399                                                    self.seen_topics.remove(&key);
400                                                    self.topic_refcount.remove(&key);
401                                                    true
402                                                } else {
403                                                    false
404                                                }
405                                            }
406                                        }
407                                    };
408
409                                    if should_deliver {
410                                        self.pending_events.push(event);
411                                    }
412                                }
413                            }
414                            Ok(None) => break,
415                            Err(_) => break,
416                        }
417                    }
418                }
419                Ok(BufResult(Ok(_), _)) => {}
420                Ok(BufResult(Err(e), _)) => {
421                    if e.kind() != std::io::ErrorKind::WouldBlock {
422                        debug!("[XPUB] Error reading from subscriber {}: {}", sub.id, e);
423                    }
424                }
425                Err(_) => {
426                    // Timeout  -  no data available from this subscriber
427                }
428            }
429        }
430
431        // Return any events collected from this poll round
432        if !self.pending_events.is_empty() {
433            return Ok(Some(self.pending_events.remove(0)));
434        }
435
436        Ok(None)
437    }
438
439    /// Broadcast a message to all matching subscribers.
440    ///
441    /// Only subscribers whose subscriptions match the message's first frame
442    /// will receive it.
443    ///
444    /// # Examples
445    ///
446    /// ```no_run
447    /// # use monocoque_zmtp::xpub::XPubSocket;
448    /// # use bytes::Bytes;
449    /// # async fn example(mut xpub: XPubSocket) -> std::io::Result<()> {
450    /// xpub.send(vec![
451    ///     Bytes::from("topic.temperature"),
452    ///     Bytes::from("23.5"),
453    /// ]).await?;
454    /// # Ok(())
455    /// # }
456    /// ```
457    pub async fn send(&mut self, msg: Vec<Bytes>) -> io::Result<()> {
458        use bytes::BytesMut;
459        use compio_buf::BufResult;
460        use compio_io::AsyncWriteExt;
461
462        trace!("[XPUB] Broadcasting message with {} frames", msg.len());
463
464        // Pre-encode once for plaintext subscribers (shared via O(1) clone).
465        // Encrypted subscribers get per-subscriber encoding below.
466        let mut plain_wire: Option<bytes::Bytes> = None;
467
468        let mut dead_subs = Vec::new();
469
470        for sub in self.subscribers.values_mut() {
471            if !sub.matches(&msg) {
472                continue;
473            }
474
475            let wire = if let Some(ref mut cipher) = sub.curve_cipher {
476                let last = msg.len().saturating_sub(1);
477                let mut buf = BytesMut::new();
478                let mut ok = true;
479                for (i, frame) in msg.iter().enumerate() {
480                    if let Ok(body) = cipher.encrypt_frame(frame, i < last) {
481                        crate::base::append_zmtp_cmd_frame(&mut buf, &body);
482                    } else {
483                        ok = false;
484                        break;
485                    }
486                }
487                if !ok {
488                    dead_subs.push(sub.id);
489                    continue;
490                }
491                buf.freeze()
492            } else {
493                plain_wire
494                    .get_or_insert_with(|| {
495                        let mut buf = BytesMut::new();
496                        crate::codec::encode_multipart(&msg, &mut buf);
497                        buf.freeze()
498                    })
499                    .clone()
500            };
501
502            let BufResult(result, _) = sub.stream.write_all(wire).await;
503            if let Err(e) = result {
504                debug!("[XPUB] Failed to send to subscriber {}: {}", sub.id, e);
505                dead_subs.push(sub.id);
506            } else {
507                trace!("[XPUB] Sent to subscriber {}", sub.id);
508            }
509        }
510
511        for id in dead_subs {
512            self.subscribers.remove(&id);
513            debug!("[XPUB] Removed dead subscriber {}", id);
514        }
515
516        Ok(())
517    }
518
519    /// Get the number of active subscribers.
520    pub fn subscriber_count(&self) -> usize {
521        self.subscribers.len()
522    }
523
524    /// Get the local address.
525    pub fn local_addr(&self) -> io::Result<std::net::SocketAddr> {
526        self.listener.local_addr()
527    }
528
529    /// Get the socket type.
530    pub const fn socket_type(&self) -> SocketType {
531        SocketType::Xpub
532    }
533
534    /// Check if the last received message has more frames coming.
535    ///
536    /// For XPUB, subscription events are always single-frame.
537    ///
538    /// # ZeroMQ Compatibility
539    ///
540    /// Corresponds to `ZMQ_RCVMORE` (13) option.
541    #[inline]
542    pub fn has_more(&self) -> bool {
543        !self.pending_events.is_empty()
544    }
545
546    /// Get the event state of the socket.
547    ///
548    /// Returns a bitmask indicating ready-to-receive and ready-to-send states.
549    ///
550    /// # Returns
551    ///
552    /// - `1` (POLLIN) - Socket is ready to receive (has pending subscription events)
553    /// - `2` (POLLOUT) - Socket is ready to send (has active subscribers)
554    /// - `3` (POLLIN | POLLOUT) - Socket is ready for both
555    ///
556    /// # ZeroMQ Compatibility
557    ///
558    /// Corresponds to `ZMQ_EVENTS` (15) option.
559    #[inline]
560    pub fn events(&self) -> u32 {
561        let mut events = 0;
562        if !self.pending_events.is_empty() {
563            events |= 1; // POLLIN
564        }
565        if !self.subscribers.is_empty() {
566            events |= 2; // POLLOUT
567        }
568        events
569    }
570
571    /// Set verbose mode.
572    ///
573    /// When enabled, all subscription messages are reported (including duplicates).
574    pub fn set_verbose(&mut self, verbose: bool) {
575        self.options.xpub_verbose = verbose;
576    }
577
578    /// Set manual mode.
579    ///
580    /// When enabled, subscriptions must be explicitly approved by calling `send_subscription()`.
581    pub fn set_manual(&mut self, manual: bool) {
582        self.options.xpub_manual = manual;
583    }
584
585    /// Connect to an upstream publisher so that subscription events can be forwarded.
586    ///
587    /// The upstream is typically a PUB or XSUB socket.  After calling this method,
588    /// `send_subscription()` (manual mode) writes subscription messages to the upstream
589    /// connection, causing the upstream publisher to start or stop delivering matching
590    /// messages.
591    ///
592    /// # Examples
593    ///
594    /// ```no_run
595    /// # use monocoque_zmtp::xpub::XPubSocket;
596    /// # use monocoque_core::subscription::SubscriptionEvent;
597    /// # use bytes::Bytes;
598    /// # async fn example() -> std::io::Result<()> {
599    /// let mut xpub = XPubSocket::bind("127.0.0.1:5556").await?;
600    /// xpub.set_manual(true);
601    /// xpub.connect_upstream("127.0.0.1:5555").await?;
602    ///
603    /// // Receive a subscription from a downstream client and forward it upstream.
604    /// if let Some(event) = xpub.recv_subscription().await? {
605    ///     xpub.send_subscription(event).await?;
606    /// }
607    /// # Ok(())
608    /// # }
609    /// ```
610    pub async fn connect_upstream(&mut self, addr: &str) -> io::Result<()> {
611        debug!("[XPUB] Connecting upstream to {}", addr);
612        let xsub = XSubSocket::connect(addr).await?;
613        self.upstream = Some(xsub);
614        debug!("[XPUB] Upstream connected");
615        Ok(())
616    }
617
618    /// Manually send a subscription event to the upstream connection.
619    ///
620    /// Requires both manual mode (`set_manual(true)`) and an upstream connection
621    /// (`connect_upstream()`).  Writes the subscription message directly to the
622    /// upstream publisher so it starts (or stops) delivering matching messages.
623    pub async fn send_subscription(&mut self, event: SubscriptionEvent) -> io::Result<()> {
624        if !self.options.xpub_manual {
625            return Err(io::Error::new(
626                io::ErrorKind::InvalidInput,
627                "Manual mode not enabled",
628            ));
629        }
630
631        let upstream = self.upstream.as_mut().ok_or_else(|| {
632            io::Error::new(
633                io::ErrorKind::NotConnected,
634                "No upstream connection; call connect_upstream() first",
635            )
636        })?;
637
638        trace!("[XPUB] Forwarding subscription upstream: {:?}", event);
639        upstream.send_subscription_event(event).await
640    }
641}
642
643impl fmt::Debug for XPubSocket {
644    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
645        f.debug_struct("XPubSocket")
646            .field("subscribers", &self.subscribers.len())
647            .field("verbose", &self.options.xpub_verbose)
648            .field("manual", &self.options.xpub_manual)
649            .field("has_upstream", &self.upstream.is_some())
650            .finish()
651    }
652}
653
654// Implement Socket trait for XPubSocket (non-generic)
655#[async_trait::async_trait(?Send)]
656impl crate::Socket for XPubSocket {
657    async fn send(&mut self, msg: Vec<Bytes>) -> io::Result<()> {
658        self.send(msg).await
659    }
660
661    async fn recv(&mut self) -> io::Result<Option<Vec<Bytes>>> {
662        // XPUB receives subscription events
663        self.recv_subscription()
664            .await
665            .map(|opt| opt.map(|event| vec![event.to_message()]))
666    }
667
668    fn socket_type(&self) -> SocketType {
669        SocketType::Xpub
670    }
671}
672
673#[cfg(test)]
674mod tests {
675    use super::*;
676    use crate::publisher::PubSocket as InternalPub;
677
678    /// XPUB must set TCP_NODELAY on accepted subscriber connections, matching
679    /// PUB. Connects a real XSUB peer, then reads TCP_NODELAY off the stored
680    /// subscriber's socket fd. One-time setsockopt at accept - off the hot path.
681    #[cfg(unix)]
682    #[test]
683    fn xpub_accept_sets_tcp_nodelay() {
684        use monocoque_core::rt::LocalRuntime;
685        use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
686        use std::sync::mpsc;
687        use std::thread;
688
689        fn fd_nodelay(fd: RawFd) -> bool {
690            let sock = unsafe { socket2::Socket::from_raw_fd(fd) };
691            let nd = sock.nodelay().expect("query TCP_NODELAY");
692            std::mem::forget(sock); // borrowed fd - do not close it
693            nd
694        }
695
696        let (port_tx, port_rx) = mpsc::channel::<u16>();
697        let (done_tx, done_rx) = mpsc::channel::<()>();
698
699        // XSUB client: connect once the port is known, then hold the connection.
700        let client = thread::spawn(move || {
701            let rt = LocalRuntime::new().unwrap();
702            rt.block_on(async move {
703                let port = port_rx.recv().unwrap();
704                let _xsub = crate::xsub::XSubSocket::connect(&format!("127.0.0.1:{port}"))
705                    .await
706                    .unwrap();
707                done_rx.recv().unwrap();
708            });
709        });
710
711        let rt = LocalRuntime::new().unwrap();
712        let nodelay = rt.block_on(async move {
713            let mut xpub = XPubSocket::bind("127.0.0.1:0").await.unwrap();
714            port_tx.send(xpub.local_addr().unwrap().port()).unwrap();
715            xpub.accept().await.unwrap();
716            let sub = xpub.subscribers.values().next().expect("one subscriber");
717            fd_nodelay(sub.stream.as_raw_fd())
718        });
719        done_tx.send(()).unwrap();
720        client.join().unwrap();
721        assert!(
722            nodelay,
723            "XPUB accept must set TCP_NODELAY on subscriber sockets",
724        );
725    }
726
727    #[test]
728    fn test_xpub_bind() {
729        monocoque_core::rt::LocalRuntime::new()
730            .unwrap()
731            .block_on(test_xpub_bind_impl())
732    }
733
734    async fn test_xpub_bind_impl() {
735        let xpub = XPubSocket::bind("127.0.0.1:0").await.unwrap();
736        assert_eq!(xpub.subscriber_count(), 0);
737        let addr = xpub.local_addr().unwrap();
738        assert!(addr.port() > 0);
739    }
740
741    #[test]
742    fn test_subscription_event_encoding() {
743        let event = SubscriptionEvent::Subscribe(Bytes::from_static(b"topic"));
744        let msg = event.to_message();
745        assert_eq!(msg[0], 0x01);
746        assert_eq!(&msg[1..], b"topic");
747
748        let parsed = SubscriptionEvent::from_message(&msg).unwrap();
749        assert_eq!(parsed, event);
750    }
751
752    /// `send_subscription` errors when manual mode is off.
753    #[test]
754    fn test_send_subscription_requires_manual_mode() {
755        monocoque_core::rt::LocalRuntime::new()
756            .unwrap()
757            .block_on(test_send_subscription_requires_manual_mode_impl())
758    }
759
760    async fn test_send_subscription_requires_manual_mode_impl() {
761        let mut xpub = XPubSocket::bind("127.0.0.1:0").await.unwrap();
762        // manual mode is off by default
763        let err = xpub
764            .send_subscription(SubscriptionEvent::Subscribe(Bytes::from("topic")))
765            .await
766            .unwrap_err();
767        assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
768    }
769
770    /// `send_subscription` errors when no upstream is connected.
771    #[test]
772    fn test_send_subscription_requires_upstream() {
773        monocoque_core::rt::LocalRuntime::new()
774            .unwrap()
775            .block_on(test_send_subscription_requires_upstream_impl())
776    }
777
778    async fn test_send_subscription_requires_upstream_impl() {
779        let mut xpub = XPubSocket::bind("127.0.0.1:0").await.unwrap();
780        xpub.set_manual(true);
781        let err = xpub
782            .send_subscription(SubscriptionEvent::Subscribe(Bytes::from("topic")))
783            .await
784            .unwrap_err();
785        assert_eq!(err.kind(), std::io::ErrorKind::NotConnected);
786    }
787
788    /// `connect_upstream` + `send_subscription` forward subscription bytes to a PubSocket.
789    ///
790    /// The PubSocket's subscription reader (running inside a worker thread) picks up the
791    /// raw subscription bytes written by the upstream XSubSocket. We verify this
792    /// indirectly: after forwarding Subscribe("weather"), publishing a "weather" message
793    /// reaches the upstream connection (the XSubSocket), confirming the PUB socket
794    /// started delivering matching messages.
795    #[test]
796    fn test_connect_upstream_and_forward_subscription() {
797        monocoque_core::rt::LocalRuntime::new()
798            .unwrap()
799            .block_on(test_connect_upstream_and_forward_subscription_impl())
800    }
801
802    async fn test_connect_upstream_and_forward_subscription_impl() {
803        use monocoque_core::rt::TcpListener;
804
805        // Bind a PubSocket listener (the upstream data source).
806        let pub_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
807        let pub_addr = pub_listener.local_addr().unwrap();
808
809        // Spawn PubSocket: accept the XSubSocket upstream connection, then broadcast.
810        let pub_task = monocoque_core::rt::spawn(async move {
811            let mut pub_sock = InternalPub::new();
812            // Accept the connection that connect_upstream() will make.
813            pub_sock.accept_subscriber(&pub_listener).await.unwrap();
814            // Give the subscription reader time to process Subscribe("weather").
815            monocoque_core::rt::sleep(std::time::Duration::from_millis(50)).await;
816            // Broadcast a matching message  -  should reach the upstream XSubSocket.
817            pub_sock
818                .send(vec![Bytes::from("weather"), Bytes::from("sunny")])
819                .await
820                .unwrap();
821        });
822
823        let mut xpub = XPubSocket::bind("127.0.0.1:0").await.unwrap();
824        xpub.set_manual(true);
825
826        // Connect upstream to the PubSocket listener.
827        xpub.connect_upstream(&pub_addr.to_string()).await.unwrap();
828        assert!(xpub.upstream.is_some());
829
830        // Forward a subscription to the PubSocket.
831        xpub.send_subscription(SubscriptionEvent::Subscribe(Bytes::from("weather")))
832            .await
833            .unwrap();
834
835        // Wait for the PubSocket to broadcast.
836        monocoque_core::rt::join(pub_task).await;
837
838        // The upstream XSubSocket should have received the "weather" message.
839        let msg = xpub
840            .upstream
841            .as_mut()
842            .unwrap()
843            .recv()
844            .await
845            .unwrap()
846            .expect("upstream should have received matching message");
847
848        assert_eq!(msg[0], Bytes::from("weather"));
849        assert_eq!(msg[1], Bytes::from("sunny"));
850    }
851}