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    /// Check if the socket is currently connected.
96    #[inline]
97    pub fn is_connected(&self) -> bool {
98        self.inner.is_connected()
99    }
100
101    /// Try to reconnect to the stored endpoint, re-sending all active subscriptions.
102    pub async fn try_reconnect(&mut self) -> io::Result<()> {
103        self.inner.try_reconnect().await
104    }
105
106    /// Receive with automatic reconnection on EOF or network error.
107    pub async fn recv_with_reconnect(&mut self) -> io::Result<Option<Vec<bytes::Bytes>>> {
108        self.inner.recv_with_reconnect().await
109    }
110
111    /// Connect to a PUB peer via IPC (Unix domain sockets).
112    ///
113    /// Unix-only. Accepts IPC paths with or without `ipc://` prefix:
114    /// - `"ipc:///tmp/socket.sock"`
115    /// - `"/tmp/socket.sock"`
116    ///
117    /// # Example
118    ///
119    /// ```rust,no_run
120    /// # #[cfg(unix)]
121    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
122    /// use monocoque::zmq::SubSocket;
123    ///
124    /// let mut socket = SubSocket::connect_ipc("/tmp/pubsub.sock").await?;
125    /// socket.subscribe(b"");
126    /// # Ok(())
127    /// # }
128    /// ```
129    #[cfg(unix)]
130    pub async fn connect_ipc(path: &str) -> io::Result<SubSocket<monocoque_core::rt::UnixStream>> {
131        use std::path::PathBuf;
132
133        // Strip "ipc://" prefix if present
134        let clean_path = path.strip_prefix("ipc://").unwrap_or(path);
135        let ipc_path = PathBuf::from(clean_path);
136
137        let stream = monocoque_core::ipc::connect(&ipc_path).await?;
138        let sock = SubSocket::from_unix_stream(stream).await?;
139        sock.emit_event(SocketEvent::Connected(
140            monocoque_core::endpoint::Endpoint::Ipc(ipc_path),
141        ));
142        Ok(sock)
143    }
144
145    /// Create a SUB socket from a TCP stream with TCP_NODELAY enabled.
146    pub async fn from_tcp(stream: TcpStream) -> io::Result<Self> {
147        Ok(Self {
148            inner: InternalSub::from_tcp(stream).await?,
149            monitor: None,
150        })
151    }
152
153    /// Create a SUB socket from a TCP stream with custom options.
154    ///
155    /// # Example
156    ///
157    /// ```rust,no_run
158    /// use monocoque::zmq::{SubSocket, SocketOptions};
159    /// use monocoque_core::rt::TcpStream;
160    ///
161    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
162    /// let stream = TcpStream::connect("127.0.0.1:5555").await?;
163    /// let socket = SubSocket::from_tcp_with_options(
164    ///     stream,
165    ///     SocketOptions::default()
166    ///         .with_recv_hwm(500)
167    ///         .with_buffer_sizes(4096, 4096)
168    /// ).await?;
169    /// # Ok(())
170    /// # }
171    /// ```
172    pub async fn from_tcp_with_options(
173        stream: TcpStream,
174        options: monocoque_core::options::SocketOptions,
175    ) -> io::Result<Self> {
176        Ok(Self {
177            inner: InternalSub::from_tcp_with_options(stream, options).await?,
178            monitor: None,
179        })
180    }
181
182    /// Create a SUB socket from any stream with custom options.
183    pub async fn with_options<Stream>(
184        stream: Stream,
185        options: monocoque_core::options::SocketOptions,
186    ) -> io::Result<SubSocket<Stream>>
187    where
188        Stream: compio_io::AsyncRead + compio_io::AsyncWrite + Unpin,
189    {
190        Ok(SubSocket {
191            inner: InternalSub::with_options(stream, options).await?,
192            monitor: None,
193        })
194    }
195}
196
197// Generic impl - works with any stream type
198impl<S> SubSocket<S>
199where
200    S: compio_io::AsyncRead + compio_io::AsyncWrite + Unpin,
201{
202    /// Enable monitoring for this socket.
203    ///
204    /// Returns a receiver for socket lifecycle events.
205    pub fn monitor(&mut self) -> SocketMonitor {
206        let (sender, receiver) = create_monitor();
207        self.monitor = Some(sender);
208        receiver
209    }
210
211    /// Helper to emit monitoring events (if monitoring is enabled).
212    fn emit_event(&self, event: SocketEvent) {
213        if let Some(monitor) = &self.monitor {
214            monocoque_core::monitor::emit(monitor, event);
215        }
216    }
217
218    /// Subscribe to messages matching the given topic prefix.
219    ///
220    /// Empty topic subscribes to all messages.
221    ///
222    /// This sends a subscription message to the PUB socket.
223    pub async fn subscribe(&mut self, topic: &[u8]) -> io::Result<()> {
224        self.inner.subscribe(Bytes::copy_from_slice(topic)).await
225    }
226
227    /// Unsubscribe from messages matching the given topic prefix.
228    ///
229    /// This sends an unsubscription message to the PUB socket.
230    pub async fn unsubscribe(&mut self, topic: &[u8]) -> io::Result<()> {
231        self.inner.unsubscribe(&Bytes::copy_from_slice(topic)).await
232    }
233
234    /// Receive a multipart message.
235    ///
236    /// Only messages matching subscribed topics will be received.
237    /// Returns `None` if the connection is closed.
238    pub async fn recv(&mut self) -> io::Result<Option<Vec<Bytes>>> {
239        self.inner.recv().await
240    }
241
242    /// Get the socket type.
243    ///
244    /// # ZeroMQ Compatibility
245    ///
246    /// Corresponds to `ZMQ_TYPE` (16) option.
247    #[inline]
248    pub const fn socket_type() -> SocketType {
249        SocketType::Sub
250    }
251
252    /// Get the endpoint this socket is connected/bound to, if available.
253    ///
254    /// Returns `None` if the socket was created from a raw stream.
255    ///
256    /// # ZeroMQ Compatibility
257    ///
258    /// Corresponds to `ZMQ_LAST_ENDPOINT` (32) option.
259    #[inline]
260    pub fn last_endpoint(&self) -> Option<&monocoque_core::endpoint::Endpoint> {
261        self.inner.last_endpoint()
262    }
263
264    /// Check if the last received message has more frames coming.
265    ///
266    /// Returns `true` if there are more frames in the current multipart message.
267    ///
268    /// # ZeroMQ Compatibility
269    ///
270    /// Corresponds to `ZMQ_RCVMORE` (13) option.
271    #[inline]
272    pub fn has_more(&self) -> bool {
273        self.inner.has_more()
274    }
275
276    /// Get the event state of the socket.
277    ///
278    /// Returns a bitmask indicating ready-to-receive and ready-to-send states.
279    ///
280    /// # Returns
281    ///
282    /// - `1` (POLLIN) - Socket is ready to receive
283    /// - `2` (POLLOUT) - Socket is ready to send
284    /// - `3` (POLLIN | POLLOUT) - Socket is ready for both
285    ///
286    /// # ZeroMQ Compatibility
287    ///
288    /// Corresponds to `ZMQ_EVENTS` (15) option.
289    #[inline]
290    pub fn events(&self) -> u32 {
291        self.inner.events()
292    }
293
294    /// Get a mutable reference to this socket's options.
295    #[inline]
296    pub fn options_mut(&mut self) -> &mut SocketOptions {
297        self.inner.options_mut()
298    }
299}
300
301// Unix-specific impl for IPC support
302#[cfg(unix)]
303impl SubSocket<monocoque_core::rt::UnixStream> {
304    /// Create a SUB socket from an existing Unix domain socket stream (IPC).
305    pub async fn from_unix_stream(stream: monocoque_core::rt::UnixStream) -> io::Result<Self> {
306        Ok(Self {
307            inner: InternalSub::new(stream).await?,
308            monitor: None,
309        })
310    }
311
312    /// Create a SUB socket from an existing Unix stream with custom options.
313    ///
314    /// This method provides full control over socket behavior through SocketOptions.
315    pub async fn from_unix_stream_with_options(
316        stream: monocoque_core::rt::UnixStream,
317        options: monocoque_core::options::SocketOptions,
318    ) -> io::Result<Self> {
319        Ok(Self {
320            inner: InternalSub::with_options(stream, options).await?,
321            monitor: None,
322        })
323    }
324}