mecha10-core 0.6.2

Mecha10 core runtime and foundational types
Documentation
//! `debounce` and `window` time-based combinators.

use super::relay::matching_channel;
use super::types::Receiver;
use std::time::Duration;
use tokio::time::{sleep, Instant};

impl<T> Receiver<T> {
    /// Debounce (rate-limit) messages to prevent rapid bursts
    ///
    /// Creates a new receiver that only emits a message if a certain duration has passed
    /// since the last emission. This is useful for reducing update frequency of high-rate
    /// sensors or preventing UI update storms.
    ///
    /// **Strategy:** "Emit latest after quiet period"
    /// - Messages are buffered during the debounce period
    /// - Only the most recent message is emitted after the period expires
    /// - Earlier messages within the period are dropped
    ///
    /// # Arguments
    ///
    /// * `duration` - Minimum time between emissions
    ///
    /// # Example
    ///
    /// ```rust
    /// # use mecha10::prelude::*;
    /// # use std::time::Duration;
    /// # async fn example(ctx: &Context) -> Result<()> {
    /// // Reduce camera updates to max 10 Hz (100ms debounce)
    /// let mut debounced_images = ctx.subscribe(sensor::CAMERA_RGB)
    ///     .await?
    ///     .debounce(Duration::from_millis(100));
    ///
    /// while let Some(image) = debounced_images.recv().await {
    ///     // Process at most 10 images/sec
    /// }
    ///
    /// // Prevent rapid motor status updates
    /// let mut stable_status = ctx.subscribe(motor::STATUS)
    ///     .await?
    ///     .debounce(Duration::from_millis(50));
    ///
    /// while let Some(status) = stable_status.recv().await {
    ///     // Only get updates every 50ms
    /// }
    /// # Ok(())
    /// # }
    /// ```
    pub fn debounce(self, duration: Duration) -> Receiver<T>
    where
        T: Send + 'static,
    {
        let (tx, rx) = matching_channel::<T, T>(&self.inner);

        // Spawn task to debounce messages
        let mut receiver = self;
        tokio::spawn(async move {
            let mut last_emit = Instant::now();
            let mut pending_msg: Option<T> = None;

            loop {
                // Calculate time until next allowed emission
                let elapsed = last_emit.elapsed();
                let remaining = if elapsed >= duration {
                    Duration::from_millis(0)
                } else {
                    duration - elapsed
                };

                // Wait for either a message or debounce timeout
                tokio::select! {
                    // New message received
                    msg = receiver.recv() => {
                        match msg {
                            Some(new_msg) => {
                                // Store as pending (replaces any previous pending)
                                pending_msg = Some(new_msg);

                                // If debounce period passed, emit immediately
                                if remaining.is_zero() {
                                    if let Some(msg_to_send) = pending_msg.take() {
                                        if tx.send(msg_to_send).await.is_err() {
                                            break; // Receiver dropped
                                        }

                                        last_emit = Instant::now();
                                    }
                                }
                                // Otherwise, message stays pending until timeout
                            }
                            None => {
                                // Source closed, send any pending message and exit
                                if let Some(msg_to_send) = pending_msg.take() {
                                    let _ = tx.send(msg_to_send).await;
                                }
                                break;
                            }
                        }
                    }

                    // Debounce timeout - emit pending message if any
                    _ = sleep(remaining), if pending_msg.is_some() && !remaining.is_zero() => {
                        if let Some(msg_to_send) = pending_msg.take() {
                            if tx.send(msg_to_send).await.is_err() {
                                break; // Receiver dropped
                            }

                            last_emit = Instant::now();
                        }
                    }
                }
            }
        });

        Receiver { inner: rx }
    }

    /// Window messages over a time period
    ///
    /// Creates a new receiver that collects messages over a fixed time window and emits
    /// them as a batch (Vec) at the end of each window. This is useful for:
    /// - Aggregating sensor readings (averaging, min/max)
    /// - Batch processing for efficiency
    /// - Time-series analysis
    /// - Rate calculation (count messages per window)
    ///
    /// # Behavior
    ///
    /// - **Fixed Windows**: Each window has a fixed duration
    /// - **Non-overlapping**: Windows don't overlap (tumbling window)
    /// - **Batch Emission**: All messages in window emitted as Vec at window end
    /// - **Empty Windows**: Empty Vec emitted if no messages received
    /// - **Termination**: Emits final partial window when source ends
    ///
    /// # Arguments
    ///
    /// * `window_duration` - Duration of each time window
    ///
    /// # Example
    ///
    /// ```rust
    /// # use mecha10::prelude::*;
    /// # use std::time::Duration;
    /// # async fn example(ctx: &Context) -> Result<()> {
    /// // Average IMU readings over 100ms windows
    /// let mut windowed = ctx.subscribe(sensor::IMU)
    ///     .await?
    ///     .window(Duration::from_millis(100));
    ///
    /// while let Some(batch) = windowed.recv().await {
    ///     if !batch.is_empty() {
    ///         let avg = batch.iter().map(|imu| imu.accel_x).sum::<f32>() / batch.len() as f32;
    ///         println!("Average accel: {} (n={})", avg, batch.len());
    ///     }
    /// }
    ///
    /// // Count messages per second
    /// let mut counts = ctx.subscribe("/events")
    ///     .await?
    ///     .window(Duration::from_secs(1))
    ///     .map(|batch| batch.len());
    ///
    /// while let Some(count) = counts.recv().await {
    ///     println!("Events/sec: {}", count);
    /// }
    ///
    /// // Batch process for efficiency
    /// let mut batches = ctx.subscribe("/data")
    ///     .await?
    ///     .window(Duration::from_millis(50));
    ///
    /// while let Some(batch) = batches.recv().await {
    ///     // Process entire batch at once (more efficient than one-by-one)
    ///     process_batch(&batch).await?;
    /// }
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// # Performance
    ///
    /// - **Memory**: Buffers all messages within window (use reasonable window sizes)
    /// - **Latency**: Adds latency equal to window duration
    /// - **CPU**: Minimal overhead for window management
    ///
    /// # Caveats
    ///
    /// - Window size affects memory usage (smaller windows = less memory)
    /// - All messages are delivered at window end (batch latency)
    /// - For sliding windows, consider multiple overlapping `.window()` subscriptions
    pub fn window(self, window_duration: Duration) -> Receiver<Vec<T>>
    where
        T: Send + 'static,
    {
        let (tx, rx) = matching_channel::<T, Vec<T>>(&self.inner);

        // Spawn task to window messages
        let mut receiver = self;
        tokio::spawn(async move {
            let mut buffer: Vec<T> = Vec::new();
            let mut interval = tokio::time::interval(window_duration);
            interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);

            loop {
                tokio::select! {
                    // New message received
                    msg = receiver.recv() => {
                        match msg {
                            Some(new_msg) => {
                                buffer.push(new_msg);
                            }
                            None => {
                                // Source ended - emit final window and exit
                                if !buffer.is_empty() {
                                    let _ = tx.send(buffer).await;
                                }
                                break;
                            }
                        }
                    }

                    // Window duration elapsed
                    _ = interval.tick() => {
                        // Emit current window (even if empty)
                        let batch = std::mem::take(&mut buffer);
                        if tx.send(batch).await.is_err() {
                            // Receiver dropped, stop windowing
                            break;
                        }
                    }
                }
            }
        });

        Receiver { inner: rx }
    }
}