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