ddns_a/monitor/hybrid/
monitor.rs

1//! Hybrid monitor configuration.
2//!
3//! This module provides [`HybridMonitor`], the builder/configuration struct
4//! for creating hybrid IP address monitors that combine API events with polling.
5
6use super::super::DebouncePolicy;
7use super::super::listener::ApiListener;
8use super::stream::HybridStream;
9use crate::network::AddressFetcher;
10use crate::time::{Clock, SystemClock};
11use std::time::Duration;
12
13/// Hybrid IP address monitor combining API events with polling fallback.
14///
15/// The hybrid monitor uses platform API events (e.g., `NotifyIpInterfaceChange`)
16/// for immediate notification of IP changes, with periodic polling as a safety net.
17///
18/// # Degradation Behavior
19///
20/// If the API listener fails (returns an error), the monitor automatically
21/// degrades to polling-only mode. This degradation is permanent for the
22/// lifetime of the stream - no automatic recovery is attempted.
23///
24/// # Type Parameters
25///
26/// * `F` - The [`AddressFetcher`] implementation for retrieving adapter snapshots
27/// * `L` - The [`ApiListener`] implementation for platform event notifications
28/// * `C` - The [`Clock`] implementation for timestamps (defaults to [`SystemClock`])
29///
30/// # Example
31///
32/// ```ignore
33/// use ddns_a::monitor::{HybridMonitor, DebouncePolicy};
34/// use ddns_a::monitor::platform::PlatformListener;
35/// use std::time::Duration;
36///
37/// let fetcher = MyFetcher::new();
38/// let listener = PlatformListener::new()?;
39/// let monitor = HybridMonitor::new(fetcher, listener, Duration::from_secs(60))
40///     .with_debounce(DebouncePolicy::default());
41///
42/// let mut stream = monitor.into_stream();
43/// while let Some(changes) = stream.next().await {
44///     for change in changes {
45///         println!("{:?}", change);
46///     }
47/// }
48/// ```
49#[derive(Debug)]
50pub struct HybridMonitor<F, L, C = SystemClock> {
51    fetcher: F,
52    api_listener: L,
53    clock: C,
54    poll_interval: Duration,
55    debounce: Option<DebouncePolicy>,
56}
57
58impl<F, L> HybridMonitor<F, L, SystemClock>
59where
60    F: AddressFetcher,
61    L: ApiListener,
62{
63    /// Creates a new hybrid monitor with system clock.
64    ///
65    /// # Arguments
66    ///
67    /// * `fetcher` - The address fetcher to use for polling
68    /// * `api_listener` - The platform API listener for event notifications
69    /// * `poll_interval` - The interval between polls (safety net for missed events)
70    #[must_use]
71    pub const fn new(fetcher: F, api_listener: L, poll_interval: Duration) -> Self {
72        Self::with_clock(fetcher, api_listener, SystemClock, poll_interval)
73    }
74}
75
76impl<F, L, C> HybridMonitor<F, L, C>
77where
78    F: AddressFetcher,
79    L: ApiListener,
80    C: Clock,
81{
82    /// Creates a new hybrid monitor with a custom clock.
83    ///
84    /// This constructor allows injecting a mock clock for testing.
85    ///
86    /// # Arguments
87    ///
88    /// * `fetcher` - The address fetcher to use for polling
89    /// * `api_listener` - The platform API listener for event notifications
90    /// * `clock` - The clock to use for timestamps
91    /// * `poll_interval` - The interval between polls
92    #[must_use]
93    pub const fn with_clock(
94        fetcher: F,
95        api_listener: L,
96        clock: C,
97        poll_interval: Duration,
98    ) -> Self {
99        Self {
100            fetcher,
101            api_listener,
102            clock,
103            poll_interval,
104            debounce: None,
105        }
106    }
107
108    /// Configures debounce policy for this monitor.
109    ///
110    /// When debounce is enabled, rapid consecutive changes within the
111    /// debounce window are merged, with cancelling changes (add then remove
112    /// of the same IP) being eliminated.
113    ///
114    /// **Note**: The debounce window is fixed-duration from the first change;
115    /// subsequent changes within the window do not extend the timer.
116    ///
117    /// # Arguments
118    ///
119    /// * `policy` - The debounce policy to apply
120    #[must_use]
121    pub const fn with_debounce(mut self, policy: DebouncePolicy) -> Self {
122        self.debounce = Some(policy);
123        self
124    }
125
126    /// Returns the configured polling interval.
127    #[must_use]
128    pub const fn poll_interval(&self) -> Duration {
129        self.poll_interval
130    }
131
132    /// Returns the configured debounce policy, if any.
133    #[must_use]
134    pub const fn debounce(&self) -> Option<&DebouncePolicy> {
135        self.debounce.as_ref()
136    }
137
138    /// Converts this monitor into a stream of IP changes.
139    ///
140    /// The returned stream will:
141    /// - React to API events for immediate change detection
142    /// - Poll at the configured interval as a safety net
143    /// - Yield batches of [`crate::monitor::IpChange`] events whenever addresses change
144    ///
145    /// If the API listener fails, the stream automatically degrades to
146    /// polling-only mode without terminating.
147    ///
148    /// The stream never terminates on its own; use `take_until` with
149    /// a shutdown signal to stop it gracefully.
150    #[must_use]
151    pub fn into_stream(self) -> HybridStream<F, L::Stream, C> {
152        let api_stream = self.api_listener.into_stream();
153        HybridStream::new(
154            self.fetcher,
155            api_stream,
156            self.clock,
157            self.poll_interval,
158            self.debounce,
159        )
160    }
161}