Skip to main content

sonos_stream/
config.rs

1//! Configuration types for the sonos-stream crate
2//!
3//! This module defines configuration structures that control the behavior
4//! of the EventBroker, including firewall detection, polling intervals,
5//! and event processing settings.
6
7use std::time::Duration;
8
9/// Configuration for the EventBroker
10///
11/// This struct controls all aspects of the event broker's behavior, from
12/// callback server settings to polling intervals and firewall detection.
13#[derive(Debug, Clone)]
14pub struct BrokerConfig {
15    /// Port range for the callback server
16    /// Default: (3400, 3500)
17    pub callback_port_range: (u16, u16),
18
19    /// Timeout for detecting event failures (fallback after proactive detection)
20    /// Default: 30 seconds
21    pub event_timeout: Duration,
22
23    /// Delay after proactive firewall detection before activating polling
24    /// Default: 5 seconds
25    pub polling_activation_delay: Duration,
26
27    /// Base interval for polling operations
28    /// Default: 5 seconds
29    pub base_polling_interval: Duration,
30
31    /// Maximum interval for adaptive polling
32    /// Default: 30 seconds
33    pub max_polling_interval: Duration,
34
35    /// Timeout for UPnP subscriptions
36    /// Default: 1800 seconds (30 minutes)
37    pub subscription_timeout: Duration,
38
39    /// Buffer size for event channels
40    /// Default: 1000
41    pub event_buffer_size: usize,
42
43    /// Maximum number of concurrent polling tasks
44    /// Default: 50
45    pub max_concurrent_polls: usize,
46
47    /// Enable proactive firewall detection
48    /// Default: true
49    pub enable_proactive_firewall_detection: bool,
50
51    /// Timeout for waiting for first event to determine firewall status
52    /// Default: 15 seconds
53    pub firewall_event_wait_timeout: Duration,
54
55    /// Enable per-device firewall detection caching
56    /// Default: true
57    pub enable_firewall_caching: bool,
58
59    /// Maximum number of cached device firewall states
60    /// Default: 100
61    pub max_cached_device_states: usize,
62
63    /// Maximum number of registrations allowed
64    /// Default: 1000
65    pub max_registrations: usize,
66
67    /// Enable adaptive polling intervals based on change frequency
68    /// Default: true
69    pub adaptive_polling: bool,
70
71    /// Threshold for subscription renewal (time before expiration)
72    /// Default: 5 minutes
73    pub renewal_threshold: Duration,
74
75    /// Force polling mode — skip UPnP subscriptions and go straight to polling
76    /// Simulates a firewall that blocks all callback traffic. Useful for testing.
77    /// Default: false
78    pub force_polling_mode: bool,
79}
80
81impl Default for BrokerConfig {
82    fn default() -> Self {
83        Self {
84            callback_port_range: (3400, 3500),
85            event_timeout: Duration::from_secs(30),
86            polling_activation_delay: Duration::from_secs(5),
87            base_polling_interval: Duration::from_secs(5),
88            max_polling_interval: Duration::from_secs(30),
89            subscription_timeout: Duration::from_secs(1800), // 30 minutes
90            event_buffer_size: 1000,
91            max_concurrent_polls: 50,
92            enable_proactive_firewall_detection: true,
93            firewall_event_wait_timeout: Duration::from_secs(15),
94            enable_firewall_caching: true,
95            max_cached_device_states: 100,
96            max_registrations: 1000,
97            adaptive_polling: true,
98            renewal_threshold: Duration::from_secs(300), // 5 minutes
99            force_polling_mode: false,
100        }
101    }
102}
103
104impl BrokerConfig {
105    /// Create a new BrokerConfig with default values
106    pub fn new() -> Self {
107        Self::default()
108    }
109
110    /// Create a BrokerConfig optimized for fast polling fallback
111    pub fn fast_polling() -> Self {
112        Self {
113            base_polling_interval: Duration::from_secs(2),
114            max_polling_interval: Duration::from_secs(10),
115            polling_activation_delay: Duration::from_secs(1),
116            event_timeout: Duration::from_secs(15),
117            firewall_event_wait_timeout: Duration::from_secs(5), // Faster detection
118            ..Default::default()
119        }
120    }
121
122    /// Create a BrokerConfig optimized for resource efficiency
123    pub fn resource_efficient() -> Self {
124        Self {
125            base_polling_interval: Duration::from_secs(10),
126            max_polling_interval: Duration::from_secs(60),
127            event_buffer_size: 100,
128            max_concurrent_polls: 10,
129            max_registrations: 100,
130            max_cached_device_states: 50, // Fewer cached devices for resource efficiency
131            ..Default::default()
132        }
133    }
134
135    /// Create a BrokerConfig with firewall detection disabled
136    pub fn no_firewall_detection() -> Self {
137        Self {
138            enable_proactive_firewall_detection: false,
139            ..Default::default()
140        }
141    }
142
143    /// Create a BrokerConfig that simulates a firewall by forcing polling mode
144    /// Useful for testing firewall fallback behavior without a real firewall
145    pub fn firewall_simulation() -> Self {
146        Self {
147            force_polling_mode: true,
148            base_polling_interval: Duration::from_secs(2),
149            max_polling_interval: Duration::from_secs(10),
150            ..Default::default()
151        }
152    }
153
154    /// Validate the configuration and return any issues
155    pub fn validate(&self) -> Result<(), crate::BrokerError> {
156        if self.callback_port_range.0 >= self.callback_port_range.1 {
157            return Err(crate::BrokerError::Configuration(
158                "Invalid callback port range: start must be less than end".to_string(),
159            ));
160        }
161
162        if self.base_polling_interval >= self.max_polling_interval {
163            return Err(crate::BrokerError::Configuration(
164                "Invalid polling interval: base must be less than max".to_string(),
165            ));
166        }
167
168        if self.event_buffer_size == 0 {
169            return Err(crate::BrokerError::Configuration(
170                "Event buffer size must be greater than 0".to_string(),
171            ));
172        }
173
174        if self.max_concurrent_polls == 0 {
175            return Err(crate::BrokerError::Configuration(
176                "Max concurrent polls must be greater than 0".to_string(),
177            ));
178        }
179
180        if self.max_registrations == 0 {
181            return Err(crate::BrokerError::Configuration(
182                "Max registrations must be greater than 0".to_string(),
183            ));
184        }
185
186        if self.max_cached_device_states == 0 {
187            return Err(crate::BrokerError::Configuration(
188                "Max cached device states must be greater than 0".to_string(),
189            ));
190        }
191
192        if self.firewall_event_wait_timeout == Duration::ZERO {
193            return Err(crate::BrokerError::Configuration(
194                "Firewall event wait timeout must be greater than 0".to_string(),
195            ));
196        }
197
198        Ok(())
199    }
200
201    /// Builder pattern methods for fluent configuration
202    pub fn with_callback_ports(mut self, start: u16, end: u16) -> Self {
203        self.callback_port_range = (start, end);
204        self
205    }
206
207    pub fn with_polling_interval(mut self, base: Duration, max: Duration) -> Self {
208        self.base_polling_interval = base;
209        self.max_polling_interval = max;
210        self
211    }
212
213    pub fn with_event_timeout(mut self, timeout: Duration) -> Self {
214        self.event_timeout = timeout;
215        self
216    }
217
218    pub fn with_buffer_size(mut self, size: usize) -> Self {
219        self.event_buffer_size = size;
220        self
221    }
222
223    pub fn with_firewall_detection(mut self, enabled: bool) -> Self {
224        self.enable_proactive_firewall_detection = enabled;
225        self
226    }
227
228    pub fn with_force_polling(mut self, enabled: bool) -> Self {
229        self.force_polling_mode = enabled;
230        self
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn test_default_config() {
240        let config = BrokerConfig::default();
241        assert_eq!(config.callback_port_range, (3400, 3500));
242        assert_eq!(config.event_timeout, Duration::from_secs(30));
243        assert!(config.enable_proactive_firewall_detection);
244        assert!(!config.force_polling_mode);
245        assert!(config.validate().is_ok());
246    }
247
248    #[test]
249    fn test_config_validation() {
250        let invalid_config = BrokerConfig {
251            callback_port_range: (3500, 3400), // Invalid: start > end
252            ..Default::default()
253        };
254        assert!(invalid_config.validate().is_err());
255
256        let invalid_polling = BrokerConfig {
257            base_polling_interval: Duration::from_secs(30),
258            max_polling_interval: Duration::from_secs(10), // Invalid: base > max
259            ..Default::default()
260        };
261        assert!(invalid_polling.validate().is_err());
262    }
263
264    #[test]
265    fn test_config_presets() {
266        let fast = BrokerConfig::fast_polling();
267        assert_eq!(fast.base_polling_interval, Duration::from_secs(2));
268        assert!(fast.validate().is_ok());
269
270        let efficient = BrokerConfig::resource_efficient();
271        assert_eq!(efficient.max_concurrent_polls, 10);
272        assert!(efficient.validate().is_ok());
273
274        let no_fw = BrokerConfig::no_firewall_detection();
275        assert!(!no_fw.enable_proactive_firewall_detection);
276        assert!(no_fw.validate().is_ok());
277
278        let fw_sim = BrokerConfig::firewall_simulation();
279        assert!(fw_sim.force_polling_mode);
280        assert_eq!(fw_sim.base_polling_interval, Duration::from_secs(2));
281        assert!(fw_sim.validate().is_ok());
282    }
283
284    #[test]
285    fn test_builder_pattern() {
286        let config = BrokerConfig::new()
287            .with_callback_ports(4000, 4100)
288            .with_polling_interval(Duration::from_secs(3), Duration::from_secs(15))
289            .with_event_timeout(Duration::from_secs(45))
290            .with_buffer_size(2000)
291            .with_firewall_detection(false);
292
293        assert_eq!(config.callback_port_range, (4000, 4100));
294        assert_eq!(config.base_polling_interval, Duration::from_secs(3));
295        assert_eq!(config.event_buffer_size, 2000);
296        assert!(!config.enable_proactive_firewall_detection);
297        assert!(config.validate().is_ok());
298    }
299}