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}