clone-stream 0.4.1

Turn any Stream into a cloneable stream where each clone receives all items independently.
Documentation
use std::task::Waker;

use log::{trace, warn};

use crate::{
    error::{CloneStreamError, Result},
    states::CloneState,
};

#[derive(Debug)]
pub struct CloneRegistry {
    clones: Vec<Option<CloneState>>,
    available_indices: Vec<usize>,
    max_clone_count: usize,
}

impl CloneRegistry {
    pub(crate) const fn new(max_clone_count: usize) -> Self {
        Self {
            clones: Vec::new(),
            available_indices: Vec::new(),
            max_clone_count,
        }
    }

    pub(crate) fn register(&mut self) -> Result<usize> {
        if self.count() >= self.max_clone_count {
            return Err(CloneStreamError::MaxClonesExceeded {
                current_count: self.count(),
                max_allowed: self.max_clone_count,
            });
        }

        if let Some(reused_id) = self.available_indices.pop() {
            trace!("Registering clone {reused_id} (reused index).");
            self.clones[reused_id] = Some(CloneState::default());
            Ok(reused_id)
        } else {
            let clone_id = self.clones.len();
            trace!("Registering clone {clone_id} (new index).");
            self.clones.push(Some(CloneState::default()));
            Ok(clone_id)
        }
    }

    pub(crate) fn unregister(&mut self, clone_id: usize) {
        trace!("Unregistering clone {clone_id}.");

        if !self.exists(clone_id) {
            warn!("Attempted to unregister clone {clone_id} that was not registered");
            return;
        }

        self.clones[clone_id] = None;
        self.available_indices.push(clone_id);
        trace!("Unregister of clone {clone_id} complete.");
    }

    pub(crate) fn take(&mut self, clone_id: usize) -> Option<CloneState> {
        self.clones.get_mut(clone_id)?.take()
    }

    pub(crate) fn restore(&mut self, clone_id: usize, state: CloneState) -> Result<()> {
        if clone_id >= self.clones.len() {
            warn!("Attempted to restore clone {clone_id} with invalid ID (out of bounds)");
            return Err(CloneStreamError::InvalidCloneId { clone_id });
        }

        if self.clones[clone_id].is_some() {
            warn!("Attempted to restore clone {clone_id} that is already active");
            return Err(CloneStreamError::CloneAlreadyActive { clone_id });
        }

        self.clones[clone_id] = Some(state);
        trace!("Restored clone {clone_id}");
        Ok(())
    }

    pub(crate) fn exists(&self, clone_id: usize) -> bool {
        clone_id < self.clones.len() && self.clones[clone_id].is_some()
    }

    pub(crate) fn count(&self) -> usize {
        self.clones.iter().filter(|s| s.is_some()).count()
    }

    pub(crate) fn iter_active_with_ids(&self) -> impl Iterator<Item = (usize, &CloneState)> {
        self.clones
            .iter()
            .enumerate()
            .filter_map(|(id, state_opt)| state_opt.as_ref().map(|state| (id, state)))
    }

    pub(crate) fn iter_active(&self) -> impl Iterator<Item = &CloneState> {
        self.clones
            .iter()
            .filter_map(|state_opt| state_opt.as_ref())
    }

    pub(crate) fn collect_wakers_needing_base_item(&self) -> Vec<Waker> {
        trace!("Collecting wakers for clones needing base item.");
        self.iter_active()
            .filter(|state| state.should_still_see_base_item())
            .filter_map(CloneState::waker)
            .collect()
    }

    pub(crate) fn has_other_clones_waiting(&self, exclude_clone_id: usize) -> bool {
        self.clones.iter().enumerate().any(|(clone_id, state_opt)| {
            clone_id != exclude_clone_id
                && state_opt
                    .as_ref()
                    .is_some_and(CloneState::should_still_see_base_item)
        })
    }

    pub(crate) fn get_clone_state(&self, clone_id: usize) -> Option<&CloneState> {
        self.clones.get(clone_id).and_then(|opt| opt.as_ref())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_register_respects_max_clone_limit_with_index_reuse() {
        let mut registry = CloneRegistry::new(1);

        trace!("Register and immediately unregister to create available_indices");
        let id1 = registry.register().unwrap();
        registry.unregister(id1);

        trace!("At this point: count = 0, available_indices = [0]");

        trace!("Register again to get to max capacity");
        let _id2 = registry.register().unwrap();
        assert_eq!(
            registry.count(),
            1,
            "Registry should have exactly 1 active clone after registering with reused index"
        );

        match registry.register() {
            Ok(_) => panic!("Should have failed - already at max capacity!"),
            Err(CloneStreamError::MaxClonesExceeded {
                current_count,
                max_allowed,
            }) => {
                assert_eq!(current_count, 1, "Error should report current count of 1");
                assert_eq!(max_allowed, 1, "Error should report max allowed of 1");
            }
            Err(e) => panic!("Unexpected error: {e:?}"),
        }
    }

    #[test]
    fn test_index_reuse_works_when_under_limit() {
        let mut registry = CloneRegistry::new(2);
        let a = registry.register().unwrap();
        let _b = registry.register().unwrap();
        trace!("Creates available index");
        registry.unregister(a);

        trace!("Now at count=1, available_indices=[0], max=2");
        trace!("Should reuse index 0");
        let _c = registry.register().unwrap();
        assert_eq!(
            registry.count(),
            2,
            "Registry should have exactly 2 active clones after index reuse"
        );

        match registry.register() {
            Ok(_) => panic!("Should have failed - we're at max capacity!"),
            Err(CloneStreamError::MaxClonesExceeded {
                current_count,
                max_allowed,
            }) => {
                assert_eq!(current_count, 2, "Error should report current count of 2");
                assert_eq!(max_allowed, 2, "Error should report max allowed of 2");
            }
            Err(e) => panic!("Unexpected error: {e:?}"),
        }
    }
}