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}