hyphae 1.0.3

Reactive cells and runtime primitives for rship
Documentation
use std::collections::VecDeque;

use super::{CellValue, MapExt, ScanExt, Watchable};
use crate::{
    cell::{Cell, CellImmutable},
    pipeline::{Definite, MaterializeDefinite, Pipeline, PipelineSeed},
};

#[allow(private_bounds)]
pub trait WindowExt<T: CellValue>: Pipeline<T, Definite> + PipelineSeed<T> + Watchable<T> {
    /// Collect values into a sliding window of size `count`.
    ///
    /// Emits a `Vec<T>` containing the most recent `count` values each time
    /// a new value arrives. Before `count` values are collected, emits the
    /// values collected so far.
    ///
    /// # Example
    ///
    /// ```
    /// use hyphae::{Cell, Mutable, Gettable, WindowExt};
    ///
    /// let source = Cell::new(0);
    /// let windowed = source.window(3);
    ///
    /// assert_eq!(windowed.get(), vec![0]);  // Initial value
    ///
    /// source.set(1);
    /// assert_eq!(windowed.get(), vec![0, 1]);  // Growing window
    ///
    /// source.set(2);
    /// assert_eq!(windowed.get(), vec![0, 1, 2]);  // Full window
    ///
    /// source.set(3);
    /// assert_eq!(windowed.get(), vec![1, 2, 3]);  // Sliding window
    /// ```
    #[track_caller]
    fn window(&self, count: usize) -> Cell<Vec<T>, CellImmutable>
    where
        T: CellValue,
        Self: Clone + Send + Sync + 'static,
    {
        assert!(count > 0, "window size must be positive");

        self.clone()
            .scan(VecDeque::with_capacity(count), move |acc, value| {
                let mut new_acc = acc.clone();
                new_acc.push_back(value.clone());
                if new_acc.len() > count {
                    new_acc.pop_front();
                }
                new_acc
            })
            .map(|deque| deque.iter().cloned().collect())
            .materialize()
    }
}

impl<T: CellValue, W: Pipeline<T, Definite> + PipelineSeed<T> + Watchable<T>> WindowExt<T> for W {}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{Gettable, Mutable};

    #[test]
    fn test_window_sliding() {
        let source = Cell::new(0);
        let windowed = source.window(3);

        assert_eq!(windowed.get(), vec![0]); // Initial

        source.set(1);
        assert_eq!(windowed.get(), vec![0, 1]);

        source.set(2);
        assert_eq!(windowed.get(), vec![0, 1, 2]); // Full window

        source.set(3);
        assert_eq!(windowed.get(), vec![1, 2, 3]); // Slides

        source.set(4);
        assert_eq!(windowed.get(), vec![2, 3, 4]); // Slides
    }

    #[test]
    fn test_window_size_one() {
        let source = Cell::new(10);
        let windowed = source.window(1);

        assert_eq!(windowed.get(), vec![10]);

        source.set(20);
        assert_eq!(windowed.get(), vec![20]);

        source.set(30);
        assert_eq!(windowed.get(), vec![30]);
    }
}