Skip to main content

monocoque/zmq/
subscriber.rs

1//! SUB socket implementation.
2
3use bytes::Bytes;
4use monocoque_core::monitor::{SocketEvent, SocketEventSender, SocketMonitor, create_monitor};
5use monocoque_core::options::SocketOptions;
6use monocoque_core::rt::TcpStream;
7use monocoque_zmtp::SocketType;
8use monocoque_zmtp::subscriber::SubSocket as InternalSub;
9use std::io;
10
11/// A SUB socket for receiving filtered messages.
12///
13/// SUB sockets connect to PUB peers and filter messages by topic prefix.
14/// They're used for:
15///
16/// - Event subscriptions
17/// - Topic-based message filtering
18/// - Many-to-one aggregation
19///
20/// ## ZeroMQ Compatibility
21///
22/// Compatible with `zmq::SUB` and `zmq::PUB` sockets from libzmq.
23///
24/// ## Example
25///
26/// ```rust,no_run
27/// use monocoque::zmq::SubSocket;
28///
29/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
30/// let mut socket = SubSocket::connect("127.0.0.1:5555").await?;
31///
32/// // Subscribe to topic
33/// socket.subscribe(b"topic");
34///
35/// // Receive filtered messages
36/// loop {
37///     match socket.recv().await? {
38///         Some(msg) => println!("Received: {:?}", msg),
39///         None => break, // Connection closed
40///     }
41/// }
42/// # Ok(())
43/// # }
44/// ```
45pub struct SubSocket<S = TcpStream>
46where
47    S: compio_io::AsyncRead + compio_io::AsyncWrite + Unpin,
48{
49    inner: InternalSub<S>,
50    monitor: Option<SocketEventSender>,
51}
52
53impl SubSocket {
54    /// Connect to a PUB peer and create a SUB socket.
55    ///
56    /// Accepts TCP endpoints or raw socket addresses:
57    /// - `"tcp://127.0.0.1:5555"`
58    /// - `"127.0.0.1:5555"`
59    ///
60    /// For IPC (Unix domain sockets), use [`SubSocket::connect_ipc()`].
61    ///
62    /// # Example
63    ///
64    /// ```rust,no_run
65    /// use monocoque::zmq::SubSocket;
66    ///
67    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
68    /// let mut socket = SubSocket::connect("127.0.0.1:5555").await?;
69    /// socket.subscribe(b""); // Subscribe to all messages
70    /// # Ok(())
71    /// # }
72    /// ```
73    pub async fn connect(endpoint: &str) -> io::Result<Self> {
74        // Try parsing as endpoint, fall back to raw address
75        let addr = if let Ok(monocoque_core::endpoint::Endpoint::Tcp(a)) =
76            monocoque_core::endpoint::Endpoint::parse(endpoint)
77        {
78            a
79        } else {
80            endpoint
81                .parse::<std::net::SocketAddr>()
82                .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?
83        };
84
85        let sock = Self {
86            inner: InternalSub::connect(addr).await?,
87            monitor: None,
88        };
89        sock.emit_event(SocketEvent::Connected(
90            monocoque_core::endpoint::Endpoint::Tcp(addr),
91        ));
92        Ok(sock)
93    }
94
95    /// Connect to a publisher with custom socket options.
96    ///
97    /// Counterpart to [`connect`](Self::connect) for callers that need to set
98    /// buffer sizes, timeouts, or subscriptions before the connection is made.
99    ///
100    /// # Errors
101    ///
102    /// Returns an error if the address cannot be parsed or the connection and
103    /// ZMTP handshake fail.
104    pub async fn connect_with_options(
105        addr: impl monocoque_core::rt::ToSocketAddrs,
106        options: monocoque_core::options::SocketOptions,
107    ) -> io::Result<Self> {
108        Ok(Self {
109            inner: InternalSub::connect_with_options(addr, options).await?,
110            monitor: None,
111        })
112    }
113
114    /// Check if the socket is currently connected.
115    #[inline]
116    pub fn is_connected(&self) -> bool {
117        self.inner.is_connected()
118    }
119
120    /// Try to reconnect to the stored endpoint, re-sending all active subscriptions.
121    pub async fn try_reconnect(&mut self) -> io::Result<()> {
122        self.inner.try_reconnect().await
123    }
124
125    /// Receive with automatic reconnection on EOF or network error.
126    pub async fn recv_with_reconnect(&mut self) -> io::Result<Option<Vec<bytes::Bytes>>> {
127        self.inner.recv_with_reconnect().await
128    }
129
130    /// Connect to a PUB peer via IPC (Unix domain sockets).
131    ///
132    /// Unix-only. Accepts IPC paths with or without `ipc://` prefix:
133    /// - `"ipc:///tmp/socket.sock"`
134    /// - `"/tmp/socket.sock"`
135    ///
136    /// # Example
137    ///
138    /// ```rust,no_run
139    /// # #[cfg(unix)]
140    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
141    /// use monocoque::zmq::SubSocket;
142    ///
143    /// let mut socket = SubSocket::connect_ipc("/tmp/pubsub.sock").await?;
144    /// socket.subscribe(b"");
145    /// # Ok(())
146    /// # }
147    /// ```
148    #[cfg(unix)]
149    pub async fn connect_ipc(path: &str) -> io::Result<SubSocket<monocoque_core::rt::UnixStream>> {
150        use std::path::PathBuf;
151
152        // Strip "ipc://" prefix if present
153        let clean_path = path.strip_prefix("ipc://").unwrap_or(path);
154        let ipc_path = PathBuf::from(clean_path);
155
156        let stream = monocoque_core::ipc::connect(&ipc_path).await?;
157        let sock = SubSocket::from_unix_stream(stream).await?;
158        sock.emit_event(SocketEvent::Connected(
159            monocoque_core::endpoint::Endpoint::Ipc(ipc_path),
160        ));
161        Ok(sock)
162    }
163
164    /// Create a SUB socket from a TCP stream with TCP_NODELAY enabled.
165    pub async fn from_tcp(stream: TcpStream) -> io::Result<Self> {
166        Ok(Self {
167            inner: InternalSub::from_tcp(stream).await?,
168            monitor: None,
169        })
170    }
171
172    /// Create a SUB socket from a TCP stream with custom options.
173    ///
174    /// # Example
175    ///
176    /// ```rust,no_run
177    /// use monocoque::zmq::{SubSocket, SocketOptions};
178    /// use monocoque_core::rt::TcpStream;
179    ///
180    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
181    /// let stream = TcpStream::connect("127.0.0.1:5555").await?;
182    /// let socket = SubSocket::from_tcp_with_options(
183    ///     stream,
184    ///     SocketOptions::default()
185    ///         .with_recv_hwm(500)
186    ///         .with_buffer_sizes(4096, 4096)
187    /// ).await?;
188    /// # Ok(())
189    /// # }
190    /// ```
191    pub async fn from_tcp_with_options(
192        stream: TcpStream,
193        options: monocoque_core::options::SocketOptions,
194    ) -> io::Result<Self> {
195        Ok(Self {
196            inner: InternalSub::from_tcp_with_options(stream, options).await?,
197            monitor: None,
198        })
199    }
200
201    /// Create a SUB socket from any stream with custom options.
202    pub async fn with_options<Stream>(
203        stream: Stream,
204        options: monocoque_core::options::SocketOptions,
205    ) -> io::Result<SubSocket<Stream>>
206    where
207        Stream: compio_io::AsyncRead + compio_io::AsyncWrite + Unpin,
208    {
209        Ok(SubSocket {
210            inner: InternalSub::with_options(stream, options).await?,
211            monitor: None,
212        })
213    }
214}
215
216// Generic impl - works with any stream type
217impl<S> SubSocket<S>
218where
219    S: compio_io::AsyncRead + compio_io::AsyncWrite + Unpin,
220{
221    /// Enable monitoring for this socket.
222    ///
223    /// Returns a receiver for socket lifecycle events.
224    pub fn monitor(&mut self) -> SocketMonitor {
225        let (sender, receiver) = create_monitor();
226        self.monitor = Some(sender);
227        receiver
228    }
229
230    /// Helper to emit monitoring events (if monitoring is enabled).
231    fn emit_event(&self, event: SocketEvent) {
232        if let Some(monitor) = &self.monitor {
233            monocoque_core::monitor::emit(monitor, event);
234        }
235    }
236
237    /// Subscribe to messages matching the given topic prefix.
238    ///
239    /// Empty topic subscribes to all messages.
240    ///
241    /// This sends a subscription message to the PUB socket.
242    pub async fn subscribe(&mut self, topic: &[u8]) -> io::Result<()> {
243        self.inner.subscribe(Bytes::copy_from_slice(topic)).await
244    }
245
246    /// Unsubscribe from messages matching the given topic prefix.
247    ///
248    /// This sends an unsubscription message to the PUB socket.
249    pub async fn unsubscribe(&mut self, topic: &[u8]) -> io::Result<()> {
250        self.inner.unsubscribe(&Bytes::copy_from_slice(topic)).await
251    }
252
253    /// Receive a multipart message.
254    ///
255    /// Only messages matching subscribed topics will be received.
256    /// Returns `None` if the connection is closed.
257    pub async fn recv(&mut self) -> io::Result<Option<Vec<Bytes>>> {
258        self.inner.recv().await
259    }
260
261    /// Receive a matching message into a caller-provided buffer, reusing its
262    /// allocation. Allocation-free counterpart to [`recv`](Self::recv): filtered
263    /// messages are dropped without allocating. Returns `Ok(true)` on a matching
264    /// message, `Ok(false)` on EOF.
265    pub async fn recv_into(&mut self, out: &mut Vec<Bytes>) -> io::Result<bool> {
266        self.inner.recv_into(out).await
267    }
268
269    /// Try to receive a matching message into `out` without a kernel read.
270    pub fn try_recv_into(&mut self, out: &mut Vec<Bytes>) -> io::Result<bool> {
271        self.inner.try_recv_into(out)
272    }
273
274    /// Get the socket type.
275    ///
276    /// # ZeroMQ Compatibility
277    ///
278    /// Corresponds to `ZMQ_TYPE` (16) option.
279    #[inline]
280    pub const fn socket_type() -> SocketType {
281        SocketType::Sub
282    }
283
284    /// Get the endpoint this socket is connected/bound to, if available.
285    ///
286    /// Returns `None` if the socket was created from a raw stream.
287    ///
288    /// # ZeroMQ Compatibility
289    ///
290    /// Corresponds to `ZMQ_LAST_ENDPOINT` (32) option.
291    #[inline]
292    pub fn last_endpoint(&self) -> Option<&monocoque_core::endpoint::Endpoint> {
293        self.inner.last_endpoint()
294    }
295
296    /// Check if the last received message has more frames coming.
297    ///
298    /// Returns `true` if there are more frames in the current multipart message.
299    ///
300    /// # ZeroMQ Compatibility
301    ///
302    /// Corresponds to `ZMQ_RCVMORE` (13) option.
303    #[inline]
304    pub fn has_more(&self) -> bool {
305        self.inner.has_more()
306    }
307
308    /// Get the event state of the socket.
309    ///
310    /// Returns a bitmask indicating ready-to-receive and ready-to-send states.
311    ///
312    /// # Returns
313    ///
314    /// - `1` (POLLIN) - Socket is ready to receive
315    /// - `2` (POLLOUT) - Socket is ready to send
316    /// - `3` (POLLIN | POLLOUT) - Socket is ready for both
317    ///
318    /// # ZeroMQ Compatibility
319    ///
320    /// Corresponds to `ZMQ_EVENTS` (15) option.
321    #[inline]
322    pub fn events(&self) -> u32 {
323        self.inner.events()
324    }
325
326    /// Get a mutable reference to this socket's options.
327    #[inline]
328    pub fn options_mut(&mut self) -> &mut SocketOptions {
329        self.inner.options_mut()
330    }
331}
332
333// Unix-specific impl for IPC support
334#[cfg(unix)]
335impl SubSocket<monocoque_core::rt::UnixStream> {
336    /// Create a SUB socket from an existing Unix domain socket stream (IPC).
337    pub async fn from_unix_stream(stream: monocoque_core::rt::UnixStream) -> io::Result<Self> {
338        Ok(Self {
339            inner: InternalSub::new(stream).await?,
340            monitor: None,
341        })
342    }
343
344    /// Create a SUB socket from an existing Unix stream with custom options.
345    ///
346    /// This method provides full control over socket behavior through SocketOptions.
347    pub async fn from_unix_stream_with_options(
348        stream: monocoque_core::rt::UnixStream,
349        options: monocoque_core::options::SocketOptions,
350    ) -> io::Result<Self> {
351        Ok(Self {
352            inner: InternalSub::with_options(stream, options).await?,
353            monitor: None,
354        })
355    }
356}