Skip to main content

monocoque_core/
options.rs

1//! Socket configuration options
2//!
3//! This module provides configuration options for `ZeroMQ` sockets, similar to
4//! libzmq's socket options (`zmq_setsockopt/zmq_getsockopt`).
5
6use std::{fmt, time::Duration};
7
8/// Smallest usable read buffer.
9///
10/// A read buffer of 0 (or a byte or two) turns the read loop into a spin or a
11/// false EOF, so every setter floors to this.
12const MIN_READ_BUFFER_SIZE: usize = 64;
13
14/// Clamp a caller-supplied read buffer size into the usable range.
15///
16/// Both [`SocketOptions::with_read_buffer_size`] and
17/// [`SocketOptions::with_buffer_sizes`] write the same field, so they share one
18/// clamp rather than each carrying its own and drifting apart.
19const fn clamp_read_buffer_size(size: usize) -> usize {
20    if size < MIN_READ_BUFFER_SIZE {
21        MIN_READ_BUFFER_SIZE
22    } else if size > crate::io::READ_SLAB_SIZE {
23        crate::io::READ_SLAB_SIZE
24    } else {
25        size
26    }
27}
28
29/// Socket configuration options.
30///
31/// These options control socket behavior including timeouts, buffer sizes,
32/// and reliability features. This struct consolidates all socket configuration
33/// in one place, following the `MongoDB` Rust driver pattern.
34///
35/// # Examples
36///
37/// ```
38/// use monocoque_core::options::SocketOptions;
39/// use std::time::Duration;
40///
41/// // Simple case: use defaults
42/// let opts = SocketOptions::default();
43///
44/// // Customize timeouts and buffers
45/// let opts = SocketOptions::default()
46///     .with_recv_timeout(Duration::from_secs(5))
47///     .with_send_timeout(Duration::from_secs(5))
48///     .with_buffer_sizes(65536, 16384);  // widen the read batch for bulk transfers
49/// ```
50#[derive(Clone)]
51pub struct SocketOptions {
52    /// Read buffer size (bytes)
53    ///
54    /// Size of the per-read carve taken from the shared read slab.
55    /// - Default: 32768 (32KB) - cuts recv syscalls on bulk transfers while
56    ///   staying at half the 64KB slab so small-read trickle still reclaims
57    /// - Small (4KB): Low-latency with small messages (< 1KB)
58    /// - Large (up to 65536, the slab size): fewer recv syscalls on bulk
59    ///   transfers, at the cost of one slab carve per read
60    ///
61    /// Values below 64 are raised to 64 and values above 65536 are lowered to
62    /// 65536. Note that anything under the 32KB default trades throughput away,
63    /// so reach for a smaller buffer only to cut latency, not to "tune".
64    pub read_buffer_size: usize,
65
66    /// Write buffer size (bytes)
67    ///
68    /// Initial capacity of `BytesMut` buffer for sending data.
69    /// - Default: 8192 (8KB) - balanced for most workloads
70    /// - Small (4KB): Low-latency with small messages
71    /// - Large (16KB): High-throughput with large messages
72    pub write_buffer_size: usize,
73
74    /// Receive timeout (`ZMQ_RCVTIMEO`)
75    ///
76    /// Maximum time to wait for a receive operation.
77    /// - `None`: Block indefinitely (default)
78    /// - `Some(Duration::ZERO)`: Non-blocking (return immediately with EAGAIN)
79    /// - `Some(duration)`: Wait up to duration before returning EAGAIN
80    pub recv_timeout: Option<Duration>,
81
82    /// Send timeout (`ZMQ_SNDTIMEO`)
83    ///
84    /// Maximum time to wait for a send operation.
85    /// - `None`: Block indefinitely (default)
86    /// - `Some(Duration::ZERO)`: Non-blocking (return immediately with EAGAIN)
87    /// - `Some(duration)`: Wait up to duration before returning EAGAIN
88    pub send_timeout: Option<Duration>,
89
90    /// Handshake timeout (`ZMQ_HANDSHAKE_IVL`)
91    ///
92    /// Maximum time to complete ZMTP handshake after connection.
93    /// - Default: 30 seconds
94    /// - Set to `Duration::ZERO` to disable timeout
95    pub handshake_timeout: Duration,
96
97    /// Linger timeout (`ZMQ_LINGER`)
98    ///
99    /// Time to wait for pending messages to be sent before closing socket.
100    /// - `None`: Close immediately, discard pending messages
101    /// - `Some(Duration::ZERO)`: Same as None
102    /// - `Some(duration)`: Wait up to duration for messages to be sent
103    pub linger: Option<Duration>,
104
105    /// Reconnect interval (`ZMQ_RECONNECT_IVL`)
106    ///
107    /// Initial reconnection delay after connection loss.
108    /// - Default: 100ms
109    /// - Use with `reconnect_ivl_max` for exponential backoff
110    pub reconnect_ivl: Duration,
111
112    /// Maximum reconnect interval (`ZMQ_RECONNECT_IVL_MAX`)
113    ///
114    /// Maximum reconnection delay for exponential backoff.
115    /// - Default: 0 (no maximum, use `reconnect_ivl` always)
116    /// - When > 0: Doubles `reconnect_ivl` up to this value
117    pub reconnect_ivl_max: Duration,
118
119    /// Connection timeout (`ZMQ_CONNECT_TIMEOUT`)
120    ///
121    /// Maximum time to wait for TCP connection to complete.
122    /// - Default: 0 (use OS default)
123    pub connect_timeout: Duration,
124
125    /// High water mark for receiving (`ZMQ_RCVHWM`)
126    ///
127    /// Maximum number of messages to queue for receiving.
128    /// When reached, socket will block or drop messages depending on socket type.
129    /// - Default: 1000 messages
130    pub recv_hwm: usize,
131
132    /// High water mark for sending (`ZMQ_SNDHWM`)
133    ///
134    /// Maximum number of messages to queue for sending.
135    /// When reached, socket will block or drop messages depending on socket type.
136    /// - Default: 1000 messages
137    pub send_hwm: usize,
138
139    /// Enable immediate connect mode (`ZMQ_IMMEDIATE`)
140    ///
141    /// - `false` (default): Queue messages while connecting
142    /// - `true`: Report error if no connection established
143    pub immediate: bool,
144
145    /// Maximum message size (`ZMQ_MAXMSGSIZE`)
146    ///
147    /// Maximum size of a single message in bytes.
148    /// - `None`: No limit (default)
149    /// - `Some(size)`: Reject messages larger than size
150    pub max_msg_size: Option<usize>,
151
152    /// Socket identity / routing ID (`ZMQ_ROUTING_ID` / `ZMQ_IDENTITY`)
153    ///
154    /// Identity for ROUTER addressing. If None, a random UUID is generated.
155    /// - Default: None (auto-generate)
156    /// - Custom: Set for stable identity across reconnections
157    pub routing_id: Option<bytes::Bytes>,
158
159    /// Connect routing ID (`ZMQ_CONNECT_ROUTING_ID`)
160    ///
161    /// Identity to assign to the next outgoing connection.
162    /// Used by ROUTER sockets to assign a specific identity to a peer.
163    /// - Default: None (auto-generate)
164    /// - Custom: Assign explicit identity to next connection
165    /// - Consumed after each connect operation
166    pub connect_routing_id: Option<bytes::Bytes>,
167
168    /// ROUTER mandatory mode (`ZMQ_ROUTER_MANDATORY`)
169    ///
170    /// - `false` (default): Silently drop messages to unknown peers
171    /// - `true`: Return error when sending to unknown peer
172    pub router_mandatory: bool,
173
174    /// ROUTER handover mode (`ZMQ_ROUTER_HANDOVER`)
175    ///
176    /// - `false` (default): Disconnect old peer when new peer with same identity connects
177    /// - `true`: Hand over pending messages to new peer with same identity
178    pub router_handover: bool,
179
180    /// Probe ROUTER on connect (`ZMQ_PROBE_ROUTER`)
181    ///
182    /// - `false` (default): Normal operation
183    /// - `true`: Send empty message on connect to probe ROUTER identity
184    pub probe_router: bool,
185
186    /// XPUB verbose mode (`ZMQ_XPUB_VERBOSE`)
187    ///
188    /// - `false` (default): Only report new subscriptions
189    /// - `true`: Report all subscription messages (including duplicates)
190    pub xpub_verbose: bool,
191
192    /// XPUB manual mode (`ZMQ_XPUB_MANUAL`)
193    ///
194    /// - `false` (default): Automatic subscription management
195    /// - `true`: Manual subscription control via `send()`
196    pub xpub_manual: bool,
197
198    /// XPUB welcome message (`ZMQ_XPUB_WELCOME_MSG`)
199    ///
200    /// Message to send to new subscribers on connection.
201    /// Useful for last value cache (LVC) patterns.
202    pub xpub_welcome_msg: Option<bytes::Bytes>,
203
204    /// XSUB verbose unsubscribe (`ZMQ_XSUB_VERBOSE_UNSUBSCRIBE`)
205    ///
206    /// - `false` (default): Don't send explicit unsubscribe messages
207    /// - `true`: Send unsubscribe messages upstream
208    pub xsub_verbose_unsubs: bool,
209
210    /// Conflate messages (`ZMQ_CONFLATE`)
211    ///
212    /// - `false` (default): Queue all messages
213    /// - `true`: Keep only last message (overwrite queue)
214    pub conflate: bool,
215
216    /// TCP keepalive (`ZMQ_TCP_KEEPALIVE`)
217    ///
218    /// - `-1` (default): Use OS default
219    /// - `0`: Disable TCP keepalive
220    /// - `1`: Enable TCP keepalive
221    pub tcp_keepalive: i32,
222
223    /// TCP keepalive count (`ZMQ_TCP_KEEPALIVE_CNT`)
224    ///
225    /// Number of keepalive probes before considering connection dead.
226    /// - `-1` (default): Use OS default
227    /// - `> 0`: Number of probes
228    pub tcp_keepalive_cnt: i32,
229
230    /// TCP keepalive idle (`ZMQ_TCP_KEEPALIVE_IDLE`)
231    ///
232    /// Time in seconds before starting keepalive probes.
233    /// - `-1` (default): Use OS default
234    /// - `> 0`: Idle time in seconds
235    pub tcp_keepalive_idle: i32,
236
237    /// TCP keepalive interval (`ZMQ_TCP_KEEPALIVE_INTVL`)
238    ///
239    /// Time in seconds between keepalive probes.
240    /// - `-1` (default): Use OS default
241    /// - `> 0`: Interval in seconds
242    pub tcp_keepalive_intvl: i32,
243
244    /// REQ correlate mode (`ZMQ_REQ_CORRELATE`)
245    ///
246    /// Match replies to requests using message envelope.
247    /// - `false` (default): Accept any reply
248    /// - `true`: Match reply envelope to request
249    pub req_correlate: bool,
250
251    /// REQ relaxed mode (`ZMQ_REQ_RELAXED`)
252    ///
253    /// Allow multiple outstanding requests without strict alternation.
254    /// - `false` (default): Strict send-recv-send-recv pattern
255    /// - `true`: Allow send-send-recv-recv pattern
256    pub req_relaxed: bool,
257
258    /// Multicast rate in kilobits per second (`ZMQ_RATE`)
259    ///
260    /// Maximum send or receive data rate for multicast transports (PGM/EPGM).
261    /// - Default: 100 kbps
262    pub rate: i32,
263
264    /// Multicast recovery interval (`ZMQ_RECOVERY_IVL`)
265    ///
266    /// Maximum time to recover lost messages on multicast transports.
267    /// - Default: 10 seconds
268    pub recovery_ivl: Duration,
269
270    /// OS-level send buffer size (`ZMQ_SNDBUF`)
271    ///
272    /// Size of kernel send buffer. 0 = OS default.
273    /// - Default: 0 (use OS default)
274    pub sndbuf: i32,
275
276    /// OS-level receive buffer size (`ZMQ_RCVBUF`)
277    ///
278    /// Size of kernel receive buffer. 0 = OS default.
279    /// - Default: 0 (use OS default)
280    pub rcvbuf: i32,
281
282    /// Bind listeners with `SO_REUSEPORT` (Unix only).
283    ///
284    /// When `true`, a socket that creates its own listener binds it with
285    /// `SO_REUSEPORT` so multiple acceptors can share one port with in-kernel
286    /// load balancing (scaling accept for high-connection ROUTER/PULL/XPUB).
287    /// - `false` (default): a single listener per address.
288    pub reuse_port: bool,
289
290    /// Multicast TTL (`ZMQ_MULTICAST_HOPS`)
291    ///
292    /// Time-to-live for multicast packets.
293    /// - Default: 1 (local network only)
294    pub multicast_hops: i32,
295
296    /// IP Type of Service (`ZMQ_TOS`)
297    ///
298    /// Sets the `ToS` field in IP headers for `QoS`.
299    /// - Default: 0 (normal service)
300    pub tos: i32,
301
302    /// Maximum multicast transmission unit (`ZMQ_MULTICAST_MAXTPDU`)
303    ///
304    /// Maximum transport data unit for multicast.
305    /// - Default: 1500 bytes
306    pub multicast_maxtpdu: i32,
307
308    /// IPv6 support (`ZMQ_IPV6`)
309    ///
310    /// Enable IPv6 on socket.
311    /// - `false` (default): IPv4 only
312    /// - `true`: IPv6 support enabled
313    pub ipv6: bool,
314
315    /// Bind to device (`ZMQ_BINDTODEVICE`)
316    ///
317    /// Bind socket to specific network interface (Linux only).
318    /// - Default: None (bind to all interfaces)
319    pub bind_to_device: Option<String>,
320
321    // --- Security Options ---
322    /// PLAIN server mode (`ZMQ_PLAIN_SERVER`)
323    ///
324    /// Enable PLAIN authentication as server.
325    /// - `false` (default): Client mode
326    /// - `true`: Server mode (validate credentials)
327    pub plain_server: bool,
328
329    /// PLAIN username (`ZMQ_PLAIN_USERNAME`)
330    ///
331    /// Username for PLAIN authentication (client side).
332    /// - Default: None (no authentication)
333    pub plain_username: Option<String>,
334
335    /// PLAIN password (`ZMQ_PLAIN_PASSWORD`)
336    ///
337    /// Password for PLAIN authentication (client side).
338    /// - Default: None (no authentication)
339    ///
340    /// Wrapped in `Zeroizing` so the password buffer is scrubbed when these
341    /// options (or a clone) are dropped. Read it via [`Self::plain_password`].
342    pub plain_password: Option<zeroize::Zeroizing<String>>,
343
344    /// CURVE server mode (`ZMQ_CURVE_SERVER`)
345    ///
346    /// Enable CURVE encryption as server.
347    /// - `false` (default): Client mode
348    /// - `true`: Server mode (provide server key)
349    pub curve_server: bool,
350
351    /// CURVE public key (`ZMQ_CURVE_PUBLICKEY`)
352    ///
353    /// Local public key for CURVE (32 bytes).
354    /// - Default: None (no encryption)
355    pub curve_publickey: Option<[u8; 32]>,
356
357    /// CURVE secret key (`ZMQ_CURVE_SECRETKEY`)
358    ///
359    /// Local secret key for CURVE (32 bytes).
360    /// - Default: None (no encryption)
361    ///
362    /// Wrapped in `Zeroizing` so the key bytes are scrubbed when these options
363    /// (or a clone) are dropped. Read it via [`Self::curve_secretkey`].
364    pub curve_secretkey: Option<zeroize::Zeroizing<[u8; 32]>>,
365
366    /// CURVE server key (`ZMQ_CURVE_SERVERKEY`)
367    ///
368    /// Server's public key for CURVE client (32 bytes).
369    /// - Default: None (no encryption)
370    /// - Client must set this to verify server identity
371    pub curve_serverkey: Option<[u8; 32]>,
372
373    /// ZAP domain (`ZMQ_ZAP_DOMAIN`)
374    ///
375    /// Security domain for ZAP authentication.
376    /// - Default: "" (global domain)
377    pub zap_domain: String,
378
379    /// Subscriptions (`ZMQ_SUBSCRIBE`)
380    ///
381    /// Subscription filters for SUB/XSUB sockets.
382    /// - Empty vec: No subscriptions (default) - won't receive any messages
383    /// - vec![b""] or vec![`Bytes::new()`]: Subscribe to all messages
384    /// - vec![b"topic1", b"topic2"]: Subscribe to specific topics
385    ///
386    /// Note: SUB sockets MUST subscribe to at least one topic to receive messages.
387    pub subscriptions: Vec<bytes::Bytes>,
388
389    /// Unsubscriptions (`ZMQ_UNSUBSCRIBE`)
390    ///
391    /// Subscription filters to remove for SUB/XSUB sockets.
392    /// Applied after subscriptions during socket configuration.
393    pub unsubscriptions: Vec<bytes::Bytes>,
394
395    /// Maximum reconnection attempts (`ZMQ_RECONNECT_STOP`)
396    ///
397    /// Maximum number of times to attempt reconnection after a disconnect.
398    /// - `None`: Retry indefinitely (default, matches libzmq behaviour)
399    /// - `Some(n)`: Give up and return `NotConnected` after n attempts
400    pub max_reconnect_attempts: Option<u32>,
401
402    /// ZMTP heartbeat interval (`ZMQ_HEARTBEAT_IVL` = 75)
403    ///
404    /// How often to send PING heartbeat commands on an otherwise idle connection.
405    /// - `None`: Disabled (default)
406    /// - `Some(dur)`: Send PING every `dur` of inactivity
407    pub heartbeat_ivl: Option<Duration>,
408
409    /// ZMTP heartbeat TTL (`ZMQ_HEARTBEAT_TTL` = 76)
410    ///
411    /// Time-to-live for the remote peer's heartbeat (sent in PING command).
412    /// The remote will disconnect if it doesn't receive a heartbeat within this interval.
413    /// - `None`: Use `heartbeat_ivl` (default)
414    /// - `Some(dur)`: Override TTL sent to peer
415    pub heartbeat_ttl: Option<Duration>,
416
417    /// ZMTP heartbeat timeout (`ZMQ_HEARTBEAT_TIMEOUT` = 77)
418    ///
419    /// How long to wait for a PONG reply before considering the connection dead.
420    /// - `None`: Use `heartbeat_ivl` (default)
421    /// - `Some(dur)`: Custom timeout (recommended: 2-5x `heartbeat_ivl`)
422    pub heartbeat_timeout: Option<Duration>,
423
424    /// ROUTER raw mode (`ZMQ_ROUTER_RAW` = 41)
425    ///
426    /// Put ROUTER socket into raw mode (no ZMTP handshake, acts like STREAM).
427    /// - `false` (default): Normal ZMTP routing
428    /// - `true`: Raw TCP bridging mode
429    pub router_raw: bool,
430
431    /// STREAM connect/disconnect notifications (`ZMQ_STREAM_NOTIFY` = 73)
432    ///
433    /// Send empty notification frames on connect and disconnect.
434    /// - `true` (default): Send notification frames
435    /// - `false`: Suppress notification frames
436    pub stream_notify: bool,
437
438    /// XPUB no-drop mode (`ZMQ_XPUB_NODROP` = 69)
439    ///
440    /// - `false` (default): Drop messages silently when HWM is reached
441    /// - `true`: Return error (`EAGAIN`) instead of dropping
442    pub xpub_nodrop: bool,
443
444    /// Invert topic matching (`ZMQ_INVERT_MATCHING` = 74)
445    ///
446    /// Invert the subscription filter logic for PUB/SUB and XPUB/XSUB.
447    /// - `false` (default): Deliver messages matching subscriptions
448    /// - `true`: Deliver messages NOT matching any subscription
449    pub invert_matching: bool,
450
451    /// Write coalescing: batch multiple `send()` calls before writing to the kernel.
452    ///
453    /// When enabled, `send()` accumulates encoded messages in an internal buffer
454    /// and only flushes to the kernel when `write_coalesce_threshold` bytes have
455    /// accumulated or when `flush()` is called explicitly.
456    ///
457    /// - `false` (default): each `send()` writes immediately (lowest latency)
458    /// - `true`: messages are batched (higher throughput for small messages)
459    ///
460    /// Always call `flush()` after the last `send()` in a burst to ensure
461    /// all buffered data reaches the peer.
462    pub write_coalescing: bool,
463
464    /// Byte threshold at which the coalesce buffer is flushed automatically.
465    ///
466    /// Only relevant when `write_coalescing` is enabled. The internal send
467    /// buffer is written to the kernel as a single syscall once it reaches
468    /// this many bytes.
469    ///
470    /// - Default: 65536 (64 KB) - one typical TCP segment on loopback
471    pub write_coalesce_threshold: usize,
472
473    /// Frame-body size at or above which the send path switches to a vectored
474    /// write (`writev`) instead of copying the body into the userspace send
475    /// buffer.
476    ///
477    /// For large frames the body copy into the coalescing buffer is the
478    /// dominant per-byte cost on the core. Above this threshold the frame header
479    /// and the refcounted `Bytes` body are written as an iovec, so the body is
480    /// handed straight to the kernel with no intermediate copy. Small frames
481    /// stay on the copy path, where a single `write` of one contiguous buffer
482    /// beats the per-iovec bookkeeping.
483    ///
484    /// Only applies in eager mode (write coalescing disabled) and when the
485    /// connection is not CURVE-encrypted (encryption must transform the body
486    /// into a fresh buffer regardless).
487    ///
488    /// - Default: 32768 (32 KB) - the measured crossover on loopback below which
489    ///   copying the body into one contiguous buffer beats a two-segment
490    ///   `writev`; tune for your hardware and message sizes
491    pub vectored_write_threshold: usize,
492}
493
494impl fmt::Debug for SocketOptions {
495    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
496        f.debug_struct("SocketOptions")
497            .field("read_buffer_size", &self.read_buffer_size())
498            .field("write_buffer_size", &self.write_buffer_size)
499            .field("recv_timeout", &self.recv_timeout)
500            .field("send_timeout", &self.send_timeout)
501            .field("handshake_timeout", &self.handshake_timeout)
502            .field("linger", &self.linger)
503            .field("reconnect_ivl", &self.reconnect_ivl)
504            .field("reconnect_ivl_max", &self.reconnect_ivl_max)
505            .field("connect_timeout", &self.connect_timeout)
506            .field("recv_hwm", &self.recv_hwm)
507            .field("send_hwm", &self.send_hwm)
508            .field("immediate", &self.immediate)
509            .field("max_msg_size", &self.max_msg_size)
510            .field("routing_id", &self.routing_id)
511            .field("connect_routing_id", &self.connect_routing_id)
512            .field("router_mandatory", &self.router_mandatory)
513            .field("router_handover", &self.router_handover)
514            .field("probe_router", &self.probe_router)
515            .field("xpub_verbose", &self.xpub_verbose)
516            .field("xpub_manual", &self.xpub_manual)
517            .field("xpub_welcome_msg", &self.xpub_welcome_msg)
518            .field("xsub_verbose_unsubs", &self.xsub_verbose_unsubs)
519            .field("conflate", &self.conflate)
520            .field("tcp_keepalive", &self.tcp_keepalive)
521            .field("tcp_keepalive_cnt", &self.tcp_keepalive_cnt)
522            .field("tcp_keepalive_idle", &self.tcp_keepalive_idle)
523            .field("tcp_keepalive_intvl", &self.tcp_keepalive_intvl)
524            .field("req_correlate", &self.req_correlate)
525            .field("req_relaxed", &self.req_relaxed)
526            .field("rate", &self.rate)
527            .field("recovery_ivl", &self.recovery_ivl)
528            .field("sndbuf", &self.sndbuf)
529            .field("rcvbuf", &self.rcvbuf)
530            .field("reuse_port", &self.reuse_port)
531            .field("multicast_hops", &self.multicast_hops)
532            .field("tos", &self.tos)
533            .field("multicast_maxtpdu", &self.multicast_maxtpdu)
534            .field("ipv6", &self.ipv6)
535            .field("bind_to_device", &self.bind_to_device)
536            .field("plain_server", &self.plain_server)
537            .field("plain_username", &self.plain_username)
538            .field(
539                "plain_password",
540                &self.plain_password.as_ref().map(|_| "[REDACTED]"),
541            )
542            .field("curve_server", &self.curve_server)
543            .field("curve_publickey", &self.curve_publickey)
544            .field(
545                "curve_secretkey",
546                &self.curve_secretkey.as_ref().map(|_| "[REDACTED]"),
547            )
548            .field("curve_serverkey", &self.curve_serverkey)
549            .field("zap_domain", &self.zap_domain)
550            .field("subscriptions", &self.subscriptions)
551            .field("unsubscriptions", &self.unsubscriptions)
552            .field("max_reconnect_attempts", &self.max_reconnect_attempts)
553            .field("heartbeat_ivl", &self.heartbeat_ivl)
554            .field("heartbeat_ttl", &self.heartbeat_ttl)
555            .field("heartbeat_timeout", &self.heartbeat_timeout)
556            .field("router_raw", &self.router_raw)
557            .field("stream_notify", &self.stream_notify)
558            .field("xpub_nodrop", &self.xpub_nodrop)
559            .field("invert_matching", &self.invert_matching)
560            .field("write_coalescing", &self.write_coalescing)
561            .field("write_coalesce_threshold", &self.write_coalesce_threshold)
562            .field("vectored_write_threshold", &self.vectored_write_threshold)
563            .finish()
564    }
565}
566
567impl Default for SocketOptions {
568    fn default() -> Self {
569        Self {
570            recv_timeout: None, // Block indefinitely
571            send_timeout: None, // Block indefinitely
572            handshake_timeout: Duration::from_secs(30),
573            linger: Some(Duration::from_secs(30)), // Wait 30s for pending messages
574            reconnect_ivl: Duration::from_millis(100),
575            reconnect_ivl_max: Duration::ZERO, // No maximum
576            connect_timeout: Duration::ZERO,   // Use OS default
577            recv_hwm: 1000,
578            send_hwm: 1000,
579            immediate: false,
580            max_msg_size: None, // No limit
581            // 32 KiB read batch (half the 64 KiB slab). Larger batches cut recv
582            // syscalls on bulk transfers roughly in proportion to their size
583            // (measured over a 64 MiB PUSH/PULL flow: 8 KiB -> 8379 recvfrom,
584            // 16 KiB -> 4178, 32 KiB -> 2104, 64 KiB -> 1037), while the reclaim
585            // keeps short reads from wasting the slab. It is capped below the
586            // slab size: a batch equal to the slab would carve the whole slab per
587            // read, so an uncoalesced trickle of small reads would allocate a
588            // fresh slab each time. 32 KiB leaves room for ~512 small reads per
589            // slab and does not raise the per-connection footprint.
590            read_buffer_size: 32768,
591            write_buffer_size: 8192, // 8KB - balanced default
592            routing_id: None,
593            connect_routing_id: None,
594            router_mandatory: false,
595            router_handover: false,
596            probe_router: false,
597            xpub_verbose: false,
598            xpub_manual: false,
599            xpub_welcome_msg: None,
600            xsub_verbose_unsubs: false,
601            conflate: false,
602            tcp_keepalive: -1,       // OS default
603            tcp_keepalive_cnt: -1,   // OS default
604            tcp_keepalive_idle: -1,  // OS default
605            tcp_keepalive_intvl: -1, // OS default
606            req_correlate: false,
607            req_relaxed: false,
608            rate: 100, // 100 kbps
609            recovery_ivl: Duration::from_secs(10),
610            sndbuf: 0, // OS default
611            rcvbuf: 0, // OS default
612            reuse_port: false,
613            multicast_hops: 1,       // Local network only
614            tos: 0,                  // Normal service
615            multicast_maxtpdu: 1500, // Standard MTU
616            ipv6: false,             // IPv4 only
617            bind_to_device: None,    // All interfaces
618            // Security
619            plain_server: false,
620            plain_username: None,
621            plain_password: None,
622            curve_server: false,
623            curve_publickey: None,
624            curve_secretkey: None,
625            curve_serverkey: None,
626            zap_domain: String::new(),    // Global domain
627            subscriptions: Vec::new(),    // No subscriptions
628            unsubscriptions: Vec::new(),  // No unsubscriptions
629            max_reconnect_attempts: None, // Retry indefinitely
630            heartbeat_ivl: None,
631            heartbeat_ttl: None,
632            heartbeat_timeout: None,
633            router_raw: false,
634            stream_notify: true,
635            xpub_nodrop: false,
636            invert_matching: false,
637            write_coalescing: false,
638            write_coalesce_threshold: 65536,
639            vectored_write_threshold: 32768,
640        }
641    }
642}
643
644impl SocketOptions {
645    /// Create new socket options with default values (8KB buffers).
646    #[must_use]
647    pub fn new() -> Self {
648        Self::default()
649    }
650
651    /// Create socket options optimized for small messages (< 1KB).
652    ///
653    /// Sets 4KB buffers, suitable for low-latency request-reply patterns.
654    ///
655    /// # Examples
656    ///
657    /// ```
658    /// use monocoque_core::options::SocketOptions;
659    ///
660    /// let opts = SocketOptions::small();  // 4KB buffers for REQ/REP
661    /// ```
662    #[must_use]
663    #[deprecated(
664        since = "0.4.1",
665        note = "sets a 4KB read buffer, which is below the 32KB default. Use SocketOptions::new().with_read_buffer_size(4096) if that is really what you want."
666    )]
667    pub fn small() -> Self {
668        Self {
669            read_buffer_size: 4096,
670            write_buffer_size: 4096,
671            ..Self::default()
672        }
673    }
674
675    /// Create socket options optimized for large messages (> 8KB).
676    ///
677    /// Sets 16KB buffers, suitable for high-throughput async patterns.
678    ///
679    /// # Examples
680    ///
681    /// ```
682    /// use monocoque_core::options::SocketOptions;
683    ///
684    /// let opts = SocketOptions::large();  // 16KB buffers for DEALER/ROUTER
685    /// ```
686    #[must_use]
687    #[deprecated(
688        since = "0.4.1",
689        note = "sets a 16KB read buffer, which is below the 32KB default and so is slower, not faster. Use SocketOptions::new() and set what your workload needs."
690    )]
691    pub fn large() -> Self {
692        Self {
693            read_buffer_size: 16384,
694            write_buffer_size: 16384,
695            ..Self::default()
696        }
697    }
698
699    /// Set receive timeout.
700    ///
701    /// # Examples
702    ///
703    /// ```
704    /// use monocoque_core::options::SocketOptions;
705    /// use std::time::Duration;
706    ///
707    /// // Non-blocking receive
708    /// let opts = SocketOptions::new().with_recv_timeout(Duration::ZERO);
709    ///
710    /// // 5 second timeout
711    /// let opts = SocketOptions::new().with_recv_timeout(Duration::from_secs(5));
712    /// ```
713    pub const fn with_recv_timeout(mut self, timeout: Duration) -> Self {
714        self.recv_timeout = Some(timeout);
715        self
716    }
717
718    /// Set send timeout.
719    pub const fn with_send_timeout(mut self, timeout: Duration) -> Self {
720        self.send_timeout = Some(timeout);
721        self
722    }
723
724    /// Set handshake timeout.
725    pub const fn with_handshake_timeout(mut self, timeout: Duration) -> Self {
726        self.handshake_timeout = timeout;
727        self
728    }
729
730    /// Set linger timeout.
731    pub const fn with_linger(mut self, linger: Option<Duration>) -> Self {
732        self.linger = linger;
733        self
734    }
735
736    /// Set reconnection interval.
737    pub const fn with_reconnect_ivl(mut self, ivl: Duration) -> Self {
738        self.reconnect_ivl = ivl;
739        self
740    }
741
742    /// Set maximum reconnection interval for exponential backoff.
743    pub const fn with_reconnect_ivl_max(mut self, max: Duration) -> Self {
744        self.reconnect_ivl_max = max;
745        self
746    }
747
748    /// Set maximum number of reconnection attempts.
749    ///
750    /// `None` retries indefinitely (default); `Some(n)` gives up after n attempts.
751    pub const fn with_max_reconnect_attempts(mut self, max: Option<u32>) -> Self {
752        self.max_reconnect_attempts = max;
753        self
754    }
755
756    /// Set connection timeout.
757    pub const fn with_connect_timeout(mut self, timeout: Duration) -> Self {
758        self.connect_timeout = timeout;
759        self
760    }
761
762    /// Set heartbeat interval (`ZMQ_HEARTBEAT_IVL`).
763    pub const fn with_heartbeat_ivl(mut self, ivl: Duration) -> Self {
764        self.heartbeat_ivl = Some(ivl);
765        self
766    }
767
768    /// Set heartbeat TTL (`ZMQ_HEARTBEAT_TTL`).
769    pub const fn with_heartbeat_ttl(mut self, ttl: Duration) -> Self {
770        self.heartbeat_ttl = Some(ttl);
771        self
772    }
773
774    /// Set heartbeat timeout (`ZMQ_HEARTBEAT_TIMEOUT`).
775    pub const fn with_heartbeat_timeout(mut self, timeout: Duration) -> Self {
776        self.heartbeat_timeout = Some(timeout);
777        self
778    }
779
780    /// Enable or disable ROUTER raw mode (`ZMQ_ROUTER_RAW`).
781    pub const fn with_router_raw(mut self, raw: bool) -> Self {
782        self.router_raw = raw;
783        self
784    }
785
786    /// Enable or disable STREAM connect/disconnect notifications (`ZMQ_STREAM_NOTIFY`).
787    pub const fn with_stream_notify(mut self, notify: bool) -> Self {
788        self.stream_notify = notify;
789        self
790    }
791
792    /// Enable XPUB no-drop mode (`ZMQ_XPUB_NODROP`).
793    pub const fn with_xpub_nodrop(mut self, nodrop: bool) -> Self {
794        self.xpub_nodrop = nodrop;
795        self
796    }
797
798    /// Enable inverted topic matching (`ZMQ_INVERT_MATCHING`).
799    pub const fn with_invert_matching(mut self, invert: bool) -> Self {
800        self.invert_matching = invert;
801        self
802    }
803
804    /// Enable or disable write coalescing.
805    ///
806    /// When enabled, consecutive `send()` calls accumulate in an internal buffer
807    /// and are written to the kernel in one syscall, reducing per-message overhead
808    /// for small-message workloads.  Call `flush()` after the last send in a burst.
809    pub const fn with_write_coalescing(mut self, enabled: bool) -> Self {
810        self.write_coalescing = enabled;
811        self
812    }
813
814    /// Set the byte threshold at which the coalesce buffer flushes automatically.
815    pub const fn with_write_coalesce_threshold(mut self, threshold: usize) -> Self {
816        self.write_coalesce_threshold = threshold;
817        self
818    }
819
820    /// Set the frame-body size at or above which the eager send path uses a
821    /// vectored write (`writev`) instead of copying the body into the send
822    /// buffer. See [`SocketOptions::vectored_write_threshold`]. Set to
823    /// `usize::MAX` to disable vectored writes entirely.
824    pub const fn with_vectored_write_threshold(mut self, threshold: usize) -> Self {
825        self.vectored_write_threshold = threshold;
826        self
827    }
828
829    /// Get the configured PLAIN password, if any.
830    pub fn plain_password(&self) -> Option<&str> {
831        self.plain_password.as_ref().map(|p| p.as_str())
832    }
833
834    /// Get the configured CURVE secret key, if any.
835    pub fn curve_secretkey(&self) -> Option<&[u8; 32]> {
836        self.curve_secretkey.as_deref()
837    }
838
839    /// Get the configured read buffer size after applying the read-slab cap.
840    pub const fn read_buffer_size(&self) -> usize {
841        if self.read_buffer_size > crate::io::READ_SLAB_SIZE {
842            crate::io::READ_SLAB_SIZE
843        } else {
844            self.read_buffer_size
845        }
846    }
847
848    /// Set receive high water mark.
849    pub const fn with_recv_hwm(mut self, hwm: usize) -> Self {
850        self.recv_hwm = hwm;
851        self
852    }
853
854    /// Set send high water mark.
855    pub const fn with_send_hwm(mut self, hwm: usize) -> Self {
856        self.send_hwm = hwm;
857        self
858    }
859
860    /// Enable or disable immediate mode.
861    pub const fn with_immediate(mut self, immediate: bool) -> Self {
862        self.immediate = immediate;
863        self
864    }
865
866    /// Set maximum message size.
867    pub const fn with_max_msg_size(mut self, size: Option<usize>) -> Self {
868        self.max_msg_size = size;
869        self
870    }
871
872    /// Set read buffer size.
873    ///
874    /// # Examples
875    ///
876    /// ```
877    /// use monocoque_core::options::SocketOptions;
878    ///
879    /// // Small buffers for low latency
880    /// let opts = SocketOptions::new().with_read_buffer_size(4096);
881    ///
882    /// // Large buffers for throughput
883    /// let opts = SocketOptions::new().with_read_buffer_size(16384);
884    /// ```
885    pub const fn with_read_buffer_size(mut self, size: usize) -> Self {
886        self.read_buffer_size = clamp_read_buffer_size(size);
887        self
888    }
889
890    /// Set write buffer size.
891    pub const fn with_write_buffer_size(mut self, size: usize) -> Self {
892        self.write_buffer_size = size;
893        self
894    }
895
896    /// Set both read and write buffer sizes (convenience method).
897    ///
898    /// # Examples
899    ///
900    /// ```
901    /// use monocoque_core::options::SocketOptions;
902    ///
903    /// // Small buffers for both
904    /// let opts = SocketOptions::new().with_buffer_sizes(4096, 4096);
905    /// ```
906    pub const fn with_buffer_sizes(mut self, read_size: usize, write_size: usize) -> Self {
907        self.read_buffer_size = clamp_read_buffer_size(read_size);
908        self.write_buffer_size = write_size;
909        self
910    }
911
912    /// Set socket routing ID / identity.
913    ///
914    /// # Examples
915    ///
916    /// ```
917    /// use monocoque_core::options::SocketOptions;
918    /// use bytes::Bytes;
919    ///
920    /// let opts = SocketOptions::new()
921    ///     .with_routing_id(Bytes::from_static(b"worker-01"));
922    /// ```
923    pub fn with_routing_id(mut self, id: bytes::Bytes) -> Self {
924        self.routing_id = Some(id);
925        self
926    }
927
928    /// Set connect routing ID for the next connection.
929    ///
930    /// This option is consumed after each connect operation and must be set
931    /// again for subsequent connections.
932    ///
933    /// # Examples
934    ///
935    /// ```
936    /// use monocoque_core::options::SocketOptions;
937    /// use bytes::Bytes;
938    ///
939    /// let opts = SocketOptions::new()
940    ///     .with_connect_routing_id(Bytes::from_static(b"client-001"));
941    /// ```
942    pub fn with_connect_routing_id(mut self, id: bytes::Bytes) -> Self {
943        self.connect_routing_id = Some(id);
944        self
945    }
946
947    /// Enable ROUTER mandatory mode.
948    pub const fn with_router_mandatory(mut self, enabled: bool) -> Self {
949        self.router_mandatory = enabled;
950        self
951    }
952
953    /// Enable ROUTER handover mode.
954    pub const fn with_router_handover(mut self, enabled: bool) -> Self {
955        self.router_handover = enabled;
956        self
957    }
958
959    /// Enable ROUTER probe on connect.
960    pub const fn with_probe_router(mut self, enabled: bool) -> Self {
961        self.probe_router = enabled;
962        self
963    }
964
965    /// Enable XPUB verbose mode.
966    pub const fn with_xpub_verbose(mut self, enabled: bool) -> Self {
967        self.xpub_verbose = enabled;
968        self
969    }
970
971    /// Enable XPUB manual mode.
972    pub const fn with_xpub_manual(mut self, enabled: bool) -> Self {
973        self.xpub_manual = enabled;
974        self
975    }
976
977    /// Set XPUB welcome message.
978    pub fn with_xpub_welcome_msg(mut self, msg: bytes::Bytes) -> Self {
979        self.xpub_welcome_msg = Some(msg);
980        self
981    }
982
983    /// Enable XSUB verbose unsubscribe.
984    pub const fn with_xsub_verbose_unsubs(mut self, enabled: bool) -> Self {
985        self.xsub_verbose_unsubs = enabled;
986        self
987    }
988
989    /// Enable message conflation (keep only last message).
990    pub const fn with_conflate(mut self, enabled: bool) -> Self {
991        self.conflate = enabled;
992        self
993    }
994
995    /// Set TCP keepalive mode.
996    ///
997    /// # Arguments
998    ///
999    /// * `mode` - `-1` for OS default, `0` to disable, `1` to enable
1000    pub const fn with_tcp_keepalive(mut self, mode: i32) -> Self {
1001        self.tcp_keepalive = mode;
1002        self
1003    }
1004
1005    /// Set TCP keepalive count (number of probes before timeout).
1006    ///
1007    /// # Arguments
1008    ///
1009    /// * `count` - `-1` for OS default, `> 0` for specific count
1010    pub const fn with_tcp_keepalive_cnt(mut self, count: i32) -> Self {
1011        self.tcp_keepalive_cnt = count;
1012        self
1013    }
1014
1015    /// Set TCP keepalive idle time (seconds before first probe).
1016    ///
1017    /// # Arguments
1018    ///
1019    /// * `seconds` - `-1` for OS default, `> 0` for specific idle time
1020    pub const fn with_tcp_keepalive_idle(mut self, seconds: i32) -> Self {
1021        self.tcp_keepalive_idle = seconds;
1022        self
1023    }
1024
1025    /// Set TCP keepalive interval (seconds between probes).
1026    ///
1027    /// # Arguments
1028    ///
1029    /// * `seconds` - `-1` for OS default, `> 0` for specific interval
1030    pub const fn with_tcp_keepalive_intvl(mut self, seconds: i32) -> Self {
1031        self.tcp_keepalive_intvl = seconds;
1032        self
1033    }
1034
1035    /// Enable REQ correlation mode (match replies to requests).
1036    pub const fn with_req_correlate(mut self, enabled: bool) -> Self {
1037        self.req_correlate = enabled;
1038        self
1039    }
1040
1041    /// Enable REQ relaxed mode (allow multiple outstanding requests).
1042    pub const fn with_req_relaxed(mut self, enabled: bool) -> Self {
1043        self.req_relaxed = enabled;
1044        self
1045    }
1046
1047    /// Set multicast rate (`ZMQ_RATE`).
1048    pub const fn with_rate(mut self, rate: i32) -> Self {
1049        self.rate = rate;
1050        self
1051    }
1052
1053    /// Set multicast recovery interval (`ZMQ_RECOVERY_IVL`).
1054    pub const fn with_recovery_ivl(mut self, interval: Duration) -> Self {
1055        self.recovery_ivl = interval;
1056        self
1057    }
1058
1059    /// Set OS send buffer size (`ZMQ_SNDBUF`).
1060    pub const fn with_sndbuf(mut self, size: i32) -> Self {
1061        self.sndbuf = size;
1062        self
1063    }
1064
1065    /// Set OS receive buffer size (`ZMQ_RCVBUF`).
1066    pub const fn with_rcvbuf(mut self, size: i32) -> Self {
1067        self.rcvbuf = size;
1068        self
1069    }
1070
1071    /// Bind listeners with `SO_REUSEPORT` (Unix only) so multiple acceptors can
1072    /// share one port. See [`SocketOptions::reuse_port`].
1073    pub const fn with_reuse_port(mut self, enabled: bool) -> Self {
1074        self.reuse_port = enabled;
1075        self
1076    }
1077
1078    /// Set multicast TTL/hops (`ZMQ_MULTICAST_HOPS`).
1079    pub const fn with_multicast_hops(mut self, hops: i32) -> Self {
1080        self.multicast_hops = hops;
1081        self
1082    }
1083
1084    /// Set IP Type of Service (`ZMQ_TOS`).
1085    pub const fn with_tos(mut self, tos: i32) -> Self {
1086        self.tos = tos;
1087        self
1088    }
1089
1090    /// Set multicast maximum TPU (`ZMQ_MULTICAST_MAXTPDU`).
1091    pub const fn with_multicast_maxtpdu(mut self, mtu: i32) -> Self {
1092        self.multicast_maxtpdu = mtu;
1093        self
1094    }
1095
1096    /// Enable IPv6 support (`ZMQ_IPV6`).
1097    pub const fn with_ipv6(mut self, enabled: bool) -> Self {
1098        self.ipv6 = enabled;
1099        self
1100    }
1101
1102    /// Bind to specific device (`ZMQ_BINDTODEVICE`) - Linux only.
1103    pub fn with_bind_to_device(mut self, device: impl Into<String>) -> Self {
1104        self.bind_to_device = Some(device.into());
1105        self
1106    }
1107
1108    // --- Security Options ---
1109
1110    /// Enable PLAIN server mode.
1111    ///
1112    /// # Examples
1113    ///
1114    /// ```
1115    /// use monocoque_core::options::SocketOptions;
1116    ///
1117    /// let opts = SocketOptions::new().with_plain_server(true);
1118    /// ```
1119    pub const fn with_plain_server(mut self, enabled: bool) -> Self {
1120        self.plain_server = enabled;
1121        self
1122    }
1123
1124    /// Set PLAIN client credentials.
1125    ///
1126    /// # Examples
1127    ///
1128    /// ```
1129    /// use monocoque_core::options::SocketOptions;
1130    ///
1131    /// let opts = SocketOptions::new()
1132    ///     .with_plain_credentials("admin", "secret123");
1133    /// ```
1134    pub fn with_plain_credentials(
1135        mut self,
1136        username: impl Into<String>,
1137        password: impl Into<String>,
1138    ) -> Self {
1139        self.plain_username = Some(username.into());
1140        self.plain_password = Some(zeroize::Zeroizing::new(password.into()));
1141        self
1142    }
1143
1144    /// Enable CURVE server mode.
1145    ///
1146    /// # Examples
1147    ///
1148    /// ```
1149    /// use monocoque_core::options::SocketOptions;
1150    ///
1151    /// let opts = SocketOptions::new().with_curve_server(true);
1152    /// ```
1153    pub const fn with_curve_server(mut self, enabled: bool) -> Self {
1154        self.curve_server = enabled;
1155        self
1156    }
1157
1158    /// Set CURVE client keys (public + secret).
1159    ///
1160    /// # Examples
1161    ///
1162    /// ```
1163    /// use monocoque_core::options::SocketOptions;
1164    ///
1165    /// let public = [0u8; 32];  // Replace with actual key
1166    /// let secret = [0u8; 32];  // Replace with actual key
1167    /// let opts = SocketOptions::new().with_curve_keypair(public, secret);
1168    /// ```
1169    pub fn with_curve_keypair(mut self, publickey: [u8; 32], secretkey: [u8; 32]) -> Self {
1170        self.curve_publickey = Some(publickey);
1171        self.curve_secretkey = Some(zeroize::Zeroizing::new(secretkey));
1172        self
1173    }
1174
1175    /// Set CURVE server public key (for client).
1176    ///
1177    /// # Examples
1178    ///
1179    /// ```
1180    /// use monocoque_core::options::SocketOptions;
1181    ///
1182    /// let server_key = [0u8; 32];  // Server's public key
1183    /// let opts = SocketOptions::new().with_curve_serverkey(server_key);
1184    /// ```
1185    pub const fn with_curve_serverkey(mut self, serverkey: [u8; 32]) -> Self {
1186        self.curve_serverkey = Some(serverkey);
1187        self
1188    }
1189
1190    /// Set ZAP domain for authentication.
1191    ///
1192    /// # Examples
1193    ///
1194    /// ```
1195    /// use monocoque_core::options::SocketOptions;
1196    ///
1197    /// let opts = SocketOptions::new().with_zap_domain("production");
1198    /// ```
1199    pub fn with_zap_domain(mut self, domain: impl Into<String>) -> Self {
1200        self.zap_domain = domain.into();
1201        self
1202    }
1203
1204    /// Add a subscription filter for SUB/XSUB sockets (`ZMQ_SUBSCRIBE`).
1205    ///
1206    /// SUB sockets MUST subscribe to at least one topic to receive messages.
1207    /// An empty filter (b"" or `Bytes::new()`) subscribes to all messages.
1208    ///
1209    /// # Examples
1210    ///
1211    /// ```
1212    /// use monocoque_core::options::SocketOptions;
1213    /// use bytes::Bytes;
1214    ///
1215    /// // Subscribe to all messages
1216    /// let opts = SocketOptions::new().with_subscribe(Bytes::new());
1217    ///
1218    /// // Subscribe to specific topics
1219    /// let opts = SocketOptions::new()
1220    ///     .with_subscribe(Bytes::from("weather."))
1221    ///     .with_subscribe(Bytes::from("stocks."));
1222    /// ```
1223    pub fn with_subscribe(mut self, filter: bytes::Bytes) -> Self {
1224        self.subscriptions.push(filter);
1225        self
1226    }
1227
1228    /// Add multiple subscription filters for SUB/XSUB sockets.
1229    ///
1230    /// Convenience method to subscribe to multiple topics at once.
1231    ///
1232    /// # Examples
1233    ///
1234    /// ```
1235    /// use monocoque_core::options::SocketOptions;
1236    /// use bytes::Bytes;
1237    ///
1238    /// let opts = SocketOptions::new()
1239    ///     .with_subscriptions(vec![
1240    ///         Bytes::from("weather."),
1241    ///         Bytes::from("stocks."),
1242    ///     ]);
1243    /// ```
1244    pub fn with_subscriptions(mut self, filters: Vec<bytes::Bytes>) -> Self {
1245        self.subscriptions.extend(filters);
1246        self
1247    }
1248
1249    /// Add an unsubscription filter for SUB/XSUB sockets (`ZMQ_UNSUBSCRIBE`).
1250    ///
1251    /// Removes a previously added subscription filter.
1252    ///
1253    /// # Examples
1254    ///
1255    /// ```
1256    /// use monocoque_core::options::SocketOptions;
1257    /// use bytes::Bytes;
1258    ///
1259    /// let opts = SocketOptions::new()
1260    ///     .with_subscribe(Bytes::new())  // Subscribe to all
1261    ///     .with_unsubscribe(Bytes::from("admin.")); // Except admin topics
1262    /// ```
1263    pub fn with_unsubscribe(mut self, filter: bytes::Bytes) -> Self {
1264        self.unsubscriptions.push(filter);
1265        self
1266    }
1267
1268    // --- Query Methods ---
1269
1270    /// Check if receive operation should be non-blocking.
1271    pub const fn is_recv_nonblocking(&self) -> bool {
1272        matches!(self.recv_timeout, Some(d) if d.is_zero())
1273    }
1274
1275    /// Check if send operation should be non-blocking.
1276    pub const fn is_send_nonblocking(&self) -> bool {
1277        matches!(self.send_timeout, Some(d) if d.is_zero())
1278    }
1279
1280    /// Validate routing ID for use with ROUTER sockets.
1281    ///
1282    /// ROUTER socket identities must:
1283    /// - Be 1-255 bytes long
1284    /// - Not start with null byte (0x00) which is reserved for auto-generated IDs
1285    pub fn validate_router_identity(id: &[u8]) -> std::io::Result<()> {
1286        if id.is_empty() {
1287            return Err(std::io::Error::new(
1288                std::io::ErrorKind::InvalidInput,
1289                "routing ID cannot be empty",
1290            ));
1291        }
1292
1293        if id.len() > 255 {
1294            return Err(std::io::Error::new(
1295                std::io::ErrorKind::InvalidInput,
1296                format!("routing ID cannot exceed 255 bytes (got {})", id.len()),
1297            ));
1298        }
1299
1300        if id[0] == 0x00 {
1301            return Err(std::io::Error::new(
1302                std::io::ErrorKind::InvalidInput,
1303                "routing ID cannot start with null byte (reserved for auto-generated IDs)",
1304            ));
1305        }
1306
1307        Ok(())
1308    }
1309
1310    /// Validate general routing ID (for DEALER, REQ, REP).
1311    ///
1312    /// Less strict than ROUTER identities - allows null prefix.
1313    pub fn validate_routing_id(id: &[u8]) -> std::io::Result<()> {
1314        if id.len() > 255 {
1315            return Err(std::io::Error::new(
1316                std::io::ErrorKind::InvalidInput,
1317                format!("routing ID cannot exceed 255 bytes (got {})", id.len()),
1318            ));
1319        }
1320        Ok(())
1321    }
1322
1323    /// Get the current reconnection interval with exponential backoff.
1324    ///
1325    /// Returns the interval to use, considering exponential backoff
1326    /// and the maximum interval setting.
1327    pub fn next_reconnect_ivl(&self, attempt: u32) -> Duration {
1328        // Match reconnect::ReconnectState::next_delay so the two do not diverge:
1329        // max == 0 disables exponential backoff, and a max at or below the base
1330        // leaves no room to grow. Both cases hold at the base interval rather
1331        // than (as before) clamping the growing value down to a smaller max.
1332        if self.reconnect_ivl_max.is_zero() || self.reconnect_ivl_max <= self.reconnect_ivl {
1333            return self.reconnect_ivl;
1334        }
1335
1336        // Calculate exponential backoff: base * 2^attempt
1337        let backoff = self
1338            .reconnect_ivl
1339            .saturating_mul(2u32.saturating_pow(attempt));
1340
1341        // Cap at maximum interval
1342        backoff.min(self.reconnect_ivl_max)
1343    }
1344}
1345
1346#[cfg(test)]
1347mod tests {
1348    use super::*;
1349
1350    #[test]
1351    fn test_default_options() {
1352        let opts = SocketOptions::default();
1353        assert!(opts.recv_timeout.is_none());
1354        assert!(opts.send_timeout.is_none());
1355        assert_eq!(opts.handshake_timeout, Duration::from_secs(30));
1356        assert_eq!(opts.reconnect_ivl, Duration::from_millis(100));
1357        assert_eq!(opts.recv_hwm, 1000);
1358        assert_eq!(opts.send_hwm, 1000);
1359    }
1360
1361    #[test]
1362    fn test_builder_pattern() {
1363        let opts = SocketOptions::new()
1364            .with_recv_timeout(Duration::from_secs(5))
1365            .with_send_timeout(Duration::from_secs(10))
1366            .with_recv_hwm(2000);
1367
1368        assert_eq!(opts.recv_timeout, Some(Duration::from_secs(5)));
1369        assert_eq!(opts.send_timeout, Some(Duration::from_secs(10)));
1370        assert_eq!(opts.recv_hwm, 2000);
1371    }
1372
1373    #[test]
1374    fn test_nonblocking_checks() {
1375        let blocking = SocketOptions::new();
1376        assert!(!blocking.is_recv_nonblocking());
1377        assert!(!blocking.is_send_nonblocking());
1378
1379        let nonblocking = SocketOptions::new()
1380            .with_recv_timeout(Duration::ZERO)
1381            .with_send_timeout(Duration::ZERO);
1382        assert!(nonblocking.is_recv_nonblocking());
1383        assert!(nonblocking.is_send_nonblocking());
1384    }
1385
1386    #[test]
1387    fn test_exponential_backoff() {
1388        let opts = SocketOptions::new()
1389            .with_reconnect_ivl(Duration::from_millis(100))
1390            .with_reconnect_ivl_max(Duration::from_secs(10));
1391
1392        // First attempt: 100ms
1393        assert_eq!(opts.next_reconnect_ivl(0), Duration::from_millis(100));
1394
1395        // Second attempt: 200ms
1396        assert_eq!(opts.next_reconnect_ivl(1), Duration::from_millis(200));
1397
1398        // Third attempt: 400ms
1399        assert_eq!(opts.next_reconnect_ivl(2), Duration::from_millis(400));
1400
1401        // Eventually caps at 10s
1402        assert_eq!(opts.next_reconnect_ivl(10), Duration::from_secs(10));
1403    }
1404
1405    #[test]
1406    fn test_no_exponential_backoff() {
1407        let opts = SocketOptions::new().with_reconnect_ivl(Duration::from_millis(100));
1408        // reconnect_ivl_max is 0 by default
1409
1410        // Always returns base interval
1411        assert_eq!(opts.next_reconnect_ivl(0), Duration::from_millis(100));
1412        assert_eq!(opts.next_reconnect_ivl(1), Duration::from_millis(100));
1413        assert_eq!(opts.next_reconnect_ivl(10), Duration::from_millis(100));
1414    }
1415
1416    #[test]
1417    fn test_reconnect_ivl_max_below_base_holds_at_base() {
1418        // A max below the base leaves no room to grow: hold at base rather than
1419        // clamping down to the smaller max. This matches ReconnectState and is
1420        // the invariant next_reconnect_ivl previously violated.
1421        let opts = SocketOptions::new()
1422            .with_reconnect_ivl(Duration::from_millis(100))
1423            .with_reconnect_ivl_max(Duration::from_millis(50));
1424
1425        for attempt in 0..5 {
1426            assert_eq!(
1427                opts.next_reconnect_ivl(attempt),
1428                Duration::from_millis(100),
1429                "max below base must hold at the base interval"
1430            );
1431        }
1432    }
1433
1434    #[test]
1435    fn test_routing_id_validation() {
1436        // Valid ROUTER identities
1437        assert!(SocketOptions::validate_router_identity(b"client-001").is_ok());
1438        assert!(SocketOptions::validate_router_identity(&[0x01; 255]).is_ok());
1439
1440        // Invalid: empty
1441        assert!(SocketOptions::validate_router_identity(b"").is_err());
1442
1443        // Invalid: too long
1444        assert!(SocketOptions::validate_router_identity(&[0x01; 256]).is_err());
1445
1446        // Invalid: starts with null byte
1447        assert!(SocketOptions::validate_router_identity(b"\x00client").is_err());
1448    }
1449
1450    #[test]
1451    fn test_general_routing_id_validation() {
1452        // Valid
1453        assert!(SocketOptions::validate_routing_id(b"").is_ok()); // Empty allowed
1454        assert!(SocketOptions::validate_routing_id(b"\x00client").is_ok()); // Null prefix allowed
1455        assert!(SocketOptions::validate_routing_id(&[0x00; 255]).is_ok());
1456
1457        // Invalid: too long
1458        assert!(SocketOptions::validate_routing_id(&[0x01; 256]).is_err());
1459    }
1460
1461    #[test]
1462    fn test_connect_routing_id() {
1463        let opts =
1464            SocketOptions::new().with_connect_routing_id(bytes::Bytes::from_static(b"peer-123"));
1465
1466        assert_eq!(
1467            opts.connect_routing_id,
1468            Some(bytes::Bytes::from_static(b"peer-123"))
1469        );
1470    }
1471
1472    #[test]
1473    fn debug_output_redacts_security_options() {
1474        let opts = SocketOptions::new()
1475            .with_plain_credentials("alice", "super-secret-password")
1476            .with_curve_keypair([1u8; 32], [7u8; 32]);
1477
1478        let debug = format!("{opts:?}");
1479
1480        assert!(
1481            !debug.contains("super-secret-password"),
1482            "SocketOptions Debug output exposes the PLAIN password"
1483        );
1484        assert!(
1485            !debug.contains("curve_secretkey: Some([7, 7, 7"),
1486            "SocketOptions Debug output exposes the CURVE secret key"
1487        );
1488    }
1489
1490    #[test]
1491    fn read_buffer_size_cannot_exceed_read_slab_size() {
1492        let opts = SocketOptions::new().with_read_buffer_size(crate::io::READ_SLAB_SIZE + 1);
1493
1494        assert!(
1495            opts.read_buffer_size <= crate::io::READ_SLAB_SIZE,
1496            "SocketOptions allowed a read buffer size larger than the read slab"
1497        );
1498    }
1499
1500    #[test]
1501    fn both_read_buffer_setters_clamp_identically() {
1502        // `with_buffer_sizes` writes the same field as `with_read_buffer_size`
1503        // but used to skip the lower bound, so a zero read buffer slipped
1504        // through it and turned the read loop into a spin or a false EOF.
1505        for size in [
1506            0,
1507            1,
1508            63,
1509            64,
1510            4096,
1511            crate::io::READ_SLAB_SIZE + 1,
1512            usize::MAX,
1513        ] {
1514            let via_single = SocketOptions::new().with_read_buffer_size(size);
1515            let via_pair = SocketOptions::new().with_buffer_sizes(size, 8192);
1516
1517            assert_eq!(
1518                via_single.read_buffer_size, via_pair.read_buffer_size,
1519                "the two read-buffer setters disagree for {size}"
1520            );
1521            assert!(
1522                via_pair.read_buffer_size >= MIN_READ_BUFFER_SIZE
1523                    && via_pair.read_buffer_size <= crate::io::READ_SLAB_SIZE,
1524                "with_buffer_sizes({size}) left the read buffer out of range"
1525            );
1526        }
1527    }
1528
1529    #[test]
1530    fn test_router_options() {
1531        let opts = SocketOptions::new()
1532            .with_router_mandatory(true)
1533            .with_router_handover(true);
1534
1535        assert!(opts.router_mandatory);
1536        assert!(opts.router_handover);
1537    }
1538
1539    #[test]
1540    fn test_subscription_options() {
1541        // Test with_subscribe
1542        let opts = SocketOptions::new()
1543            .with_subscribe(bytes::Bytes::new()) // Subscribe to all
1544            .with_subscribe(bytes::Bytes::from("weather."))
1545            .with_subscribe(bytes::Bytes::from("stocks."));
1546
1547        assert_eq!(opts.subscriptions.len(), 3);
1548        assert_eq!(opts.subscriptions[0], bytes::Bytes::new());
1549        assert_eq!(opts.subscriptions[1], bytes::Bytes::from("weather."));
1550        assert_eq!(opts.subscriptions[2], bytes::Bytes::from("stocks."));
1551
1552        // Test with_subscriptions
1553        let opts2 = SocketOptions::new().with_subscriptions(vec![
1554            bytes::Bytes::from("topic1"),
1555            bytes::Bytes::from("topic2"),
1556        ]);
1557
1558        assert_eq!(opts2.subscriptions.len(), 2);
1559
1560        // Test with_unsubscribe
1561        let opts3 = opts.with_unsubscribe(bytes::Bytes::from("admin."));
1562        assert_eq!(opts3.unsubscriptions.len(), 1);
1563        assert_eq!(opts3.unsubscriptions[0], bytes::Bytes::from("admin."));
1564    }
1565}