Skip to main content

peat_mesh/
config.rs

1//! Configuration types for the PeatMesh facade.
2
3use crate::topology::TopologyConfig;
4use crate::transport::TransportManagerConfig;
5use std::path::PathBuf;
6use std::time::Duration;
7
8/// Configuration for the Iroh networking layer.
9#[derive(Debug, Clone)]
10pub struct IrohConfig {
11    /// Explicit bind address for the Iroh endpoint.
12    /// When `None`, Iroh picks an available port automatically.
13    pub bind_addr: Option<std::net::SocketAddr>,
14    /// Relay server URLs for NAT traversal.
15    /// Empty means use Iroh's default relay infrastructure.
16    pub relay_urls: Vec<String>,
17    /// Deterministic Iroh identity seed (32 bytes).
18    /// When `Some`, used as `SecretKey::from_bytes()` for the Iroh endpoint,
19    /// giving the node a stable, reproducible identity.
20    pub secret_key: Option<[u8; 32]>,
21    /// Timeout for binding the QUIC endpoint (default: 10s).
22    pub bind_timeout: Duration,
23    /// Timeout for graceful shutdown (default: 5s).
24    pub shutdown_timeout: Duration,
25    /// Timeout for blob download operations from remote peers (default: 30s).
26    pub download_timeout: Duration,
27}
28
29impl Default for IrohConfig {
30    fn default() -> Self {
31        Self {
32            bind_addr: None,
33            relay_urls: Vec::new(),
34            secret_key: None,
35            bind_timeout: Duration::from_secs(10),
36            shutdown_timeout: Duration::from_secs(5),
37            download_timeout: Duration::from_secs(30),
38        }
39    }
40}
41
42/// Top-level configuration for PeatMesh.
43///
44/// Composes topology, discovery, and security settings into a single
45/// configuration struct that drives the [`crate::mesh::PeatMesh`] facade.
46#[derive(Debug, Clone, Default)]
47pub struct MeshConfig {
48    /// Node identifier. Auto-generated (UUID v4) if `None`.
49    pub node_id: Option<String>,
50    /// Optional path for persistent storage.
51    pub storage_path: Option<PathBuf>,
52    /// Topology formation configuration.
53    pub topology: TopologyConfig,
54    /// Peer discovery configuration.
55    pub discovery: MeshDiscoveryConfig,
56    /// Security configuration.
57    pub security: SecurityConfig,
58    /// Transport manager configuration for multi-transport selection.
59    pub transport_manager: Option<TransportManagerConfig>,
60    /// Iroh networking configuration.
61    pub iroh: IrohConfig,
62    /// Automerge CRDT compaction configuration.
63    pub compaction: CompactionConfig,
64}
65
66/// Configuration for periodic Automerge document compaction.
67///
68/// Compaction discards CRDT revision history via `fork()`, keeping only the
69/// current document state. This bounds memory growth on long-running nodes
70/// at the cost of a full-state sync on the next peer exchange.
71#[derive(Debug, Clone)]
72pub struct CompactionConfig {
73    /// Enable automatic background compaction. Default: `false`.
74    pub enabled: bool,
75    /// How often the compaction sweep runs. Default: 5 minutes.
76    /// Clamped to a minimum of 10 seconds to prevent busy-looping.
77    pub interval: Duration,
78    /// Only compact documents larger than this threshold. Default: 64 KiB.
79    pub size_threshold_bytes: usize,
80    /// Collections to compact. When empty and compaction is enabled,
81    /// auto-derives from `SyncModeRegistry` (all `LatestOnly` collections).
82    /// Only `LatestOnly` collections are safe to compact — compacting
83    /// `FullHistory` collections destroys change history needed for delta sync.
84    pub collections: Vec<String>,
85}
86
87/// Minimum compaction interval to prevent busy-looping (10 seconds).
88const MIN_COMPACTION_INTERVAL: Duration = Duration::from_secs(10);
89
90impl CompactionConfig {
91    /// Returns the effective interval, clamped to at least 10 seconds.
92    pub fn effective_interval(&self) -> Duration {
93        self.interval.max(MIN_COMPACTION_INTERVAL)
94    }
95}
96
97impl Default for CompactionConfig {
98    fn default() -> Self {
99        Self {
100            enabled: false,
101            interval: Duration::from_secs(300),
102            size_threshold_bytes: 64 * 1024,
103            collections: Vec::new(),
104        }
105    }
106}
107
108/// Discovery settings for mesh peer discovery.
109#[derive(Debug, Clone)]
110pub struct MeshDiscoveryConfig {
111    /// Enable mDNS-based peer discovery.
112    pub mdns_enabled: bool,
113    /// Service name advertised during discovery.
114    pub service_name: String,
115    /// Discovery broadcast interval.
116    pub interval: Duration,
117}
118
119impl Default for MeshDiscoveryConfig {
120    fn default() -> Self {
121        Self {
122            mdns_enabled: true,
123            service_name: "peat-mesh".to_string(),
124            interval: Duration::from_secs(30),
125        }
126    }
127}
128
129/// Security settings for mesh communications.
130#[derive(Debug, Clone)]
131pub struct SecurityConfig {
132    /// Enable encryption for mesh communications.
133    pub encryption_enabled: bool,
134    /// Require cryptographic peer identity verification.
135    pub require_peer_verification: bool,
136}
137
138impl Default for SecurityConfig {
139    fn default() -> Self {
140        Self {
141            encryption_enabled: true,
142            require_peer_verification: false,
143        }
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    // ── MeshConfig defaults ──────────────────────────────────────
152
153    #[test]
154    fn test_mesh_config_default_node_id_is_none() {
155        let cfg = MeshConfig::default();
156        assert!(cfg.node_id.is_none());
157    }
158
159    #[test]
160    fn test_mesh_config_default_storage_path_is_none() {
161        let cfg = MeshConfig::default();
162        assert!(cfg.storage_path.is_none());
163    }
164
165    #[test]
166    fn test_mesh_config_default_topology() {
167        let cfg = MeshConfig::default();
168        // TopologyConfig default reevaluation_interval is 30s
169        assert_eq!(
170            cfg.topology.reevaluation_interval,
171            Some(Duration::from_secs(30))
172        );
173    }
174
175    #[test]
176    fn test_mesh_config_default_discovery() {
177        let cfg = MeshConfig::default();
178        assert!(cfg.discovery.mdns_enabled);
179        assert_eq!(cfg.discovery.service_name, "peat-mesh");
180    }
181
182    #[test]
183    fn test_mesh_config_default_security() {
184        let cfg = MeshConfig::default();
185        assert!(cfg.security.encryption_enabled);
186        assert!(!cfg.security.require_peer_verification);
187    }
188
189    #[test]
190    fn test_mesh_config_custom_values() {
191        let cfg = MeshConfig {
192            node_id: Some("custom-node".to_string()),
193            storage_path: Some(PathBuf::from("/tmp/mesh")),
194            discovery: MeshDiscoveryConfig {
195                mdns_enabled: false,
196                service_name: "my-mesh".to_string(),
197                interval: Duration::from_secs(10),
198            },
199            security: SecurityConfig {
200                encryption_enabled: false,
201                require_peer_verification: true,
202            },
203            ..Default::default()
204        };
205        assert_eq!(cfg.node_id.as_deref(), Some("custom-node"));
206        assert_eq!(
207            cfg.storage_path.as_deref(),
208            Some(std::path::Path::new("/tmp/mesh"))
209        );
210        assert!(!cfg.discovery.mdns_enabled);
211        assert_eq!(cfg.discovery.service_name, "my-mesh");
212        assert_eq!(cfg.discovery.interval, Duration::from_secs(10));
213        assert!(!cfg.security.encryption_enabled);
214        assert!(cfg.security.require_peer_verification);
215    }
216
217    #[test]
218    fn test_mesh_config_clone() {
219        let cfg = MeshConfig {
220            node_id: Some("cloned".to_string()),
221            ..Default::default()
222        };
223        let cloned = cfg.clone();
224        assert_eq!(cloned.node_id, cfg.node_id);
225    }
226
227    #[test]
228    fn test_mesh_config_debug() {
229        let cfg = MeshConfig::default();
230        let debug = format!("{:?}", cfg);
231        assert!(debug.contains("MeshConfig"));
232    }
233
234    // ── MeshDiscoveryConfig defaults ─────────────────────────────
235
236    #[test]
237    fn test_discovery_config_default_mdns_enabled() {
238        let cfg = MeshDiscoveryConfig::default();
239        assert!(cfg.mdns_enabled);
240    }
241
242    #[test]
243    fn test_discovery_config_default_service_name() {
244        let cfg = MeshDiscoveryConfig::default();
245        assert_eq!(cfg.service_name, "peat-mesh");
246    }
247
248    #[test]
249    fn test_discovery_config_default_interval() {
250        let cfg = MeshDiscoveryConfig::default();
251        assert_eq!(cfg.interval, Duration::from_secs(30));
252    }
253
254    #[test]
255    fn test_discovery_config_custom() {
256        let cfg = MeshDiscoveryConfig {
257            mdns_enabled: false,
258            service_name: "custom".to_string(),
259            interval: Duration::from_secs(5),
260        };
261        assert!(!cfg.mdns_enabled);
262        assert_eq!(cfg.service_name, "custom");
263        assert_eq!(cfg.interval, Duration::from_secs(5));
264    }
265
266    #[test]
267    fn test_discovery_config_clone() {
268        let cfg = MeshDiscoveryConfig::default();
269        let cloned = cfg.clone();
270        assert_eq!(cloned.service_name, cfg.service_name);
271    }
272
273    #[test]
274    fn test_discovery_config_debug() {
275        let cfg = MeshDiscoveryConfig::default();
276        let debug = format!("{:?}", cfg);
277        assert!(debug.contains("MeshDiscoveryConfig"));
278    }
279
280    // ── IrohConfig defaults ───────────────────────────────────────
281
282    #[test]
283    fn test_iroh_config_default() {
284        let cfg = IrohConfig::default();
285        assert!(cfg.bind_addr.is_none());
286        assert!(cfg.relay_urls.is_empty());
287        assert_eq!(cfg.bind_timeout, Duration::from_secs(10));
288        assert_eq!(cfg.shutdown_timeout, Duration::from_secs(5));
289        assert_eq!(cfg.download_timeout, Duration::from_secs(30));
290    }
291
292    #[test]
293    fn test_iroh_config_custom_bind_addr() {
294        let cfg = IrohConfig {
295            bind_addr: Some("0.0.0.0:4433".parse().unwrap()),
296            relay_urls: vec!["https://relay.example.com".to_string()],
297            ..Default::default()
298        };
299        assert_eq!(cfg.bind_addr.unwrap().port(), 4433);
300        assert_eq!(cfg.relay_urls.len(), 1);
301    }
302
303    #[test]
304    fn test_iroh_config_secret_key() {
305        let key = [42u8; 32];
306        let cfg = IrohConfig {
307            secret_key: Some(key),
308            ..Default::default()
309        };
310        assert_eq!(cfg.secret_key.unwrap(), [42u8; 32]);
311    }
312
313    #[test]
314    fn test_mesh_config_default_iroh() {
315        let cfg = MeshConfig::default();
316        assert!(cfg.iroh.bind_addr.is_none());
317        assert!(cfg.iroh.relay_urls.is_empty());
318    }
319
320    // ── SecurityConfig defaults ──────────────────────────────────
321
322    #[test]
323    fn test_security_config_default_encryption_enabled() {
324        let cfg = SecurityConfig::default();
325        assert!(cfg.encryption_enabled);
326    }
327
328    #[test]
329    fn test_security_config_default_peer_verification_disabled() {
330        let cfg = SecurityConfig::default();
331        assert!(!cfg.require_peer_verification);
332    }
333
334    #[test]
335    fn test_security_config_custom() {
336        let cfg = SecurityConfig {
337            encryption_enabled: false,
338            require_peer_verification: true,
339        };
340        assert!(!cfg.encryption_enabled);
341        assert!(cfg.require_peer_verification);
342    }
343
344    #[test]
345    fn test_security_config_clone() {
346        let cfg = SecurityConfig {
347            encryption_enabled: true,
348            require_peer_verification: true,
349        };
350        let cloned = cfg.clone();
351        assert_eq!(cloned.encryption_enabled, cfg.encryption_enabled);
352        assert_eq!(
353            cloned.require_peer_verification,
354            cfg.require_peer_verification
355        );
356    }
357
358    #[test]
359    fn test_security_config_debug() {
360        let cfg = SecurityConfig::default();
361        let debug = format!("{:?}", cfg);
362        assert!(debug.contains("SecurityConfig"));
363    }
364
365    // ── CompactionConfig defaults ────────────────────────────────
366
367    #[test]
368    fn test_compaction_config_default_disabled() {
369        let cfg = CompactionConfig::default();
370        assert!(!cfg.enabled);
371    }
372
373    #[test]
374    fn test_compaction_config_default_interval() {
375        let cfg = CompactionConfig::default();
376        assert_eq!(cfg.interval, Duration::from_secs(300));
377    }
378
379    #[test]
380    fn test_compaction_config_default_threshold() {
381        let cfg = CompactionConfig::default();
382        assert_eq!(cfg.size_threshold_bytes, 64 * 1024);
383    }
384
385    #[test]
386    fn test_compaction_config_custom() {
387        let cfg = CompactionConfig {
388            enabled: true,
389            interval: Duration::from_secs(60),
390            size_threshold_bytes: 1024,
391            collections: vec!["beacons".to_string()],
392        };
393        assert!(cfg.enabled);
394        assert_eq!(cfg.interval, Duration::from_secs(60));
395        assert_eq!(cfg.size_threshold_bytes, 1024);
396        assert_eq!(cfg.collections.len(), 1);
397    }
398
399    #[test]
400    fn test_compaction_config_clone() {
401        let cfg = CompactionConfig {
402            enabled: false,
403            ..Default::default()
404        };
405        let cloned = cfg.clone();
406        assert_eq!(cloned.enabled, cfg.enabled);
407        assert_eq!(cloned.interval, cfg.interval);
408    }
409
410    #[test]
411    fn test_compaction_config_debug() {
412        let cfg = CompactionConfig::default();
413        let debug = format!("{:?}", cfg);
414        assert!(debug.contains("CompactionConfig"));
415    }
416
417    #[test]
418    fn test_mesh_config_default_compaction() {
419        let cfg = MeshConfig::default();
420        assert!(!cfg.compaction.enabled);
421        assert_eq!(cfg.compaction.interval, Duration::from_secs(300));
422        assert!(cfg.compaction.collections.is_empty());
423    }
424
425    #[test]
426    fn test_compaction_config_zero_interval_clamped() {
427        let cfg = CompactionConfig {
428            interval: Duration::from_secs(0),
429            ..Default::default()
430        };
431        assert_eq!(cfg.effective_interval(), Duration::from_secs(10));
432    }
433
434    #[test]
435    fn test_compaction_config_tiny_interval_clamped() {
436        let cfg = CompactionConfig {
437            interval: Duration::from_secs(3),
438            ..Default::default()
439        };
440        assert_eq!(cfg.effective_interval(), Duration::from_secs(10));
441    }
442
443    #[test]
444    fn test_compaction_config_large_interval_unchanged() {
445        let cfg = CompactionConfig {
446            interval: Duration::from_secs(600),
447            ..Default::default()
448        };
449        assert_eq!(cfg.effective_interval(), Duration::from_secs(600));
450    }
451
452    #[test]
453    fn test_compaction_config_exactly_min_interval() {
454        let cfg = CompactionConfig {
455            interval: Duration::from_secs(10),
456            ..Default::default()
457        };
458        assert_eq!(cfg.effective_interval(), Duration::from_secs(10));
459    }
460
461    #[test]
462    fn test_compaction_config_with_collections() {
463        let cfg = CompactionConfig {
464            enabled: true,
465            collections: vec!["beacons".to_string(), "platforms".to_string()],
466            ..Default::default()
467        };
468        assert!(cfg.enabled);
469        assert_eq!(cfg.collections.len(), 2);
470        assert_eq!(cfg.collections[0], "beacons");
471    }
472}