mecha10-core 0.6.5

Mecha10 core runtime and foundational types
Documentation
//! `zip` combinator: pairing two receivers into one combined stream.

use super::types::{Receiver, ReceiverInner};
use tokio::sync::mpsc;

/// Sender type used by [`Receiver::zip`].
enum ZippedSender<T> {
    Unbounded(mpsc::UnboundedSender<T>),
    Bounded(mpsc::Sender<T>),
}

impl<T> Receiver<T> {
    /// Combine two streams by pairing their messages
    ///
    /// Creates a new stream that emits pairs of messages from two input streams.
    /// The combined stream emits `(T, U)` tuples when both streams have messages available.
    ///
    /// # Behavior
    ///
    /// - **Paired Emission**: Waits for both streams to have messages, then emits the pair
    /// - **Buffering**: Buffers messages from faster stream until slower stream catches up
    /// - **Termination**: Stops when either stream ends
    /// - **Capacity**: Uses the smaller capacity of the two input streams
    ///
    /// # Use Cases
    ///
    /// - Combining sensor readings (e.g., camera + LiDAR)
    /// - Synchronizing multiple data sources
    /// - Correlating events from different streams
    /// - Sensor fusion
    ///
    /// # Example
    ///
    /// ```rust
    /// use mecha10_core::prelude::*;
    ///
    /// # async fn example(ctx: &Context) -> Result<()> {
    /// // Subscribe to two sensor streams
    /// let camera = ctx.subscribe("/sensor/camera").await?;
    /// let lidar = ctx.subscribe("/sensor/lidar").await?;
    ///
    /// // Combine them
    /// let mut fused = camera.zip(lidar);
    ///
    /// // Process paired messages
    /// while let Some((img, scan)) = fused.recv().await {
    ///     // Both sensors have data - process together
    ///     process_sensor_fusion(&img, &scan).await?;
    /// }
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// # Performance
    ///
    /// - **Memory**: Buffers messages from faster stream (bounded by capacity)
    /// - **Latency**: Adds latency equal to slower stream's inter-message delay
    /// - **CPU**: Minimal overhead for pairing logic
    ///
    /// # Caveats
    ///
    /// - If streams have very different rates, slower stream will be the bottleneck
    /// - Buffer can fill up if rate difference is sustained
    /// - Consider `.debounce()` on faster stream if rate mismatch is severe
    pub fn zip<U>(self, other: Receiver<U>) -> Receiver<(T, U)>
    where
        T: Send + 'static,
        U: Send + 'static,
    {
        fn effective_capacity<X>(inner: &ReceiverInner<X>) -> Option<usize> {
            match inner {
                ReceiverInner::Unbounded(_) => None,
                ReceiverInner::Bounded(rx) => Some(rx.max_capacity()),
                ReceiverInner::DropOldest(rx) => Some(rx.capacity()),
            }
        }

        let capacity = match (effective_capacity(&self.inner), effective_capacity(&other.inner)) {
            (None, None) => None,
            (Some(cap), None) | (None, Some(cap)) => Some(cap),
            (Some(a), Some(b)) => Some(a.min(b)),
        };

        let (tx, rx) = if let Some(cap) = capacity {
            let (tx, rx) = mpsc::channel(cap);
            (ZippedSender::Bounded(tx), ReceiverInner::Bounded(rx))
        } else {
            let (tx, rx) = mpsc::unbounded_channel();
            (ZippedSender::Unbounded(tx), ReceiverInner::Unbounded(rx))
        };

        let mut receiver_a = self;
        let mut receiver_b = other;

        tokio::spawn(async move {
            loop {
                let msg_a = receiver_a.recv().await;
                let msg_b = receiver_b.recv().await;

                match (msg_a, msg_b) {
                    (Some(a), Some(b)) => {
                        let send_result = match &tx {
                            ZippedSender::Unbounded(tx) => tx.send((a, b)).map_err(|_| ()),
                            ZippedSender::Bounded(tx) => tx.send((a, b)).await.map_err(|_| ()),
                        };

                        if send_result.is_err() {
                            break;
                        }
                    }
                    _ => {
                        break;
                    }
                }
            }
        });

        Receiver { inner: rx }
    }
}