refraction-types 0.1.1

Zero-dependency bounded ring buffer for networking workloads with predictable overwrite-on-full semantics, minimal allocations, and an ergonomic byte-oriented API.
Documentation
use crate::{
    BOUNDED_RING_BUFFER_DEFAULT_SIZE,
    ring_iter::{IntoIter, Iter, IterMut},
};

#[derive(Debug, Clone)]
pub struct BoundedRingBuffer<T> {
    pub(crate) data: Vec<T>,
    pub(crate) get_idx: usize,
    pub(crate) put_idx: usize,
    pub(crate) capacity: usize,
    pub(crate) len: usize,
}

impl<T> BoundedRingBuffer<T>
where
    T: Copy + Default,
{
    /// Creates a buffer with the default capacity.
    #[must_use]
    pub fn new() -> Self {
        Self::with_capacity(BOUNDED_RING_BUFFER_DEFAULT_SIZE)
    }

    /// Creates a buffer with a fixed capacity.
    ///
    /// # Panics
    ///
    /// Panics if `cap` is `0`.
    #[must_use]
    pub fn with_capacity(cap: usize) -> Self {
        assert_ne!(cap, 0, "Capacity cannot be zero!");
        Self {
            data: vec![T::default(); cap],
            put_idx: 0,
            get_idx: 0,
            capacity: cap,
            len: 0,
        }
    }

    /// Returns the number of currently stored elements.
    #[must_use]
    pub fn len(&self) -> usize {
        self.len
    }

    /// Returns the maximum number of elements the buffer can store.
    #[must_use]
    pub fn capacity(&self) -> usize {
        self.capacity
    }

    /// Returns the current read index.
    #[must_use]
    pub fn get_idx(&self) -> usize {
        self.get_idx
    }

    /// Returns the current write index.
    #[must_use]
    pub fn put_idx(&self) -> usize {
        self.put_idx
    }

    /// Returns `true` when the buffer has no readable elements.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.get_idx == self.put_idx && self.len == 0
    }

    /// Returns an iterator over readable elements in logical queue order.
    #[must_use]
    pub fn iter(&self) -> Iter<'_, T> {
        if self.is_empty() {
            return Iter([].iter().chain([].iter()));
        }

        let (first, second) = self.as_slice();

        let second_iter = match second {
            Some(slice) => slice.iter(),
            None => [].iter(),
        };

        Iter(first.iter().chain(second_iter))
    }

    /// Returns a mutable iterator over readable elements in logical queue order.
    #[must_use]
    pub fn iter_mut(&mut self) -> IterMut<'_, T> {
        if self.is_empty() {
            return IterMut([].iter_mut().chain([].iter_mut()));
        }

        let (first, second) = self.as_mut_slice();

        let second_iter = match second {
            Some(slice) => slice.iter_mut(),
            None => [].iter_mut(),
        };

        IterMut(first.iter_mut().chain(second_iter))
    }

    // into_iter is implemented via IntoIterator trait below

    /// Returns `true` when all capacity is occupied.
    #[must_use]
    pub fn full(&self) -> bool {
        self.get_idx == self.put_idx && self.len == self.capacity
    }

    /// Returns a shared reference to the element at the current write index.
    ///
    /// This does not modify buffer state.
    #[must_use]
    pub fn back(&self) -> Option<&T> {
        if self.is_empty() {
            None
        } else {
            Some(&self.data[(self.put_idx + self.capacity - 1) % self.capacity])
        }
    }

    /// Returns a mutable reference to the element at the current write index - 1.
    ///
    /// This does not modify indices or length.
    #[must_use]
    pub fn back_mut(&mut self) -> Option<&mut T> {
        if self.is_empty() {
            None
        } else {
            Some(&mut self.data[(self.put_idx + self.capacity - 1) % self.capacity])
        }
    }

    /// Returns a shared reference to the front readable element.
    #[must_use]
    pub fn front(&self) -> Option<&T> {
        if self.is_empty() {
            None
        } else {
            Some(&self.data[self.get_idx])
        }
    }

    /// Returns a mutable reference to the front readable element.
    #[must_use]
    pub fn front_mut(&mut self) -> Option<&mut T> {
        if self.is_empty() {
            None
        } else {
            let idx = self.get_idx;
            Some(&mut self.data[idx])
        }
    }

    /// Enqueues a single element.
    ///
    /// When the buffer is full, the oldest element is overwritten.
    pub fn enqueue(&mut self, data: T) {
        if self.full() {
            self.get_idx = (self.get_idx + 1) % self.capacity;
        } else {
            self.len += 1;
        }

        self.data[self.put_idx] = data;
        self.put_idx = (self.put_idx + 1) % self.capacity;
    }

    /// Dequeues and returns the front element, or `None` if empty.
    #[must_use]
    pub fn dequeue(&mut self) -> Option<T> {
        if self.is_empty() {
            return None;
        }

        let item = self.data[self.get_idx];
        self.get_idx = (self.get_idx + 1) % self.capacity;
        self.len -= 1;

        Some(item)
    }

    /// Advances the write cursor by `size` slots as if `size` items were written.
    ///
    /// If this exceeds available space, the oldest elements are dropped.
    pub fn advance(&mut self, size: usize) {
        let size = size.min(self.capacity);

        let new_put_idx = (self.put_idx + size) % self.capacity;
        let remaining_space = self.capacity - self.len;
        let offset = size.saturating_sub(remaining_space);
        let new_get_idx = (self.get_idx + offset) % self.capacity;

        self.get_idx = new_get_idx;
        self.put_idx = new_put_idx;
        self.len = (self.len + size).min(self.capacity);
    }

    /// Enqueues a slice of elements preserving order.
    ///
    /// This is the recommended high-throughput API for streaming workloads.
    ///
    /// If `data` is larger than capacity, only the last `capacity` elements are kept.
    pub fn enqueue_slice(&mut self, data: &[T]) {
        match data.len().cmp(&self.capacity) {
            std::cmp::Ordering::Less => {
                let new_put_idx = (self.put_idx + data.len()) % self.capacity;
                let remaining_space = self.capacity - self.len;
                let offset = data.len().saturating_sub(remaining_space);
                let new_get_idx = (self.get_idx + offset) % self.capacity;

                //we are wrapped now
                if new_put_idx < self.put_idx {
                    //copy from put to the end
                    let middle = self.capacity - self.put_idx;
                    self.data[self.put_idx..].copy_from_slice(&data[0..middle]);
                    self.data[..new_put_idx].copy_from_slice(&data[middle..]);
                } else {
                    self.data[self.put_idx..self.put_idx + data.len()].copy_from_slice(data);
                }

                self.get_idx = new_get_idx;
                self.put_idx = new_put_idx;
                self.len = (self.len + data.len()).min(self.capacity);
            }
            std::cmp::Ordering::Equal => {
                self.data.copy_from_slice(data);
                self.put_idx = 0;
                self.get_idx = 0;
                self.len = data.len();
            }
            std::cmp::Ordering::Greater => {
                let new_data_len = data.len();
                let start = new_data_len - self.capacity;
                let new_data = &data[start..new_data_len];
                self.enqueue_slice(new_data);
            }
        }
    }

    /// Dequeues up to `dequeue_size` elements into `out_buffer`.
    ///
    /// This is the recommended high-throughput API for draining buffered data.
    ///
    /// Returns the number of elements actually written to `out_buffer`.
    ///
    /// # Panics
    ///
    /// Panics if `out_buffer.len() < dequeue_size`.
    #[must_use]
    pub fn dequeue_slice(&mut self, out_buffer: &mut [T], dequeue_size: usize) -> usize {
        if self.is_empty() {
            return 0;
        }

        assert!(
            out_buffer.len() >= dequeue_size,
            "Dequeue size cannot be greater than out buffer"
        );

        match dequeue_size.cmp(&self.len) {
            std::cmp::Ordering::Less => {
                let new_get_idx = (self.get_idx + dequeue_size) % self.capacity;

                if new_get_idx < self.get_idx {
                    let middle = self.capacity - self.get_idx;
                    out_buffer[0..middle].copy_from_slice(&self.data[self.get_idx..]);
                    out_buffer[middle..dequeue_size].copy_from_slice(&self.data[..new_get_idx]);
                } else {
                    out_buffer[0..dequeue_size]
                        .copy_from_slice(&self.data[self.get_idx..new_get_idx]);
                }

                self.get_idx = new_get_idx;
                self.len -= dequeue_size;

                dequeue_size
            }
            std::cmp::Ordering::Equal => {
                if self.get_idx >= self.put_idx {
                    let middle = self.capacity - self.get_idx;
                    out_buffer[0..middle].copy_from_slice(&self.data[self.get_idx..]);
                    out_buffer[middle..dequeue_size].copy_from_slice(&self.data[..self.put_idx]);
                } else {
                    out_buffer[0..dequeue_size]
                        .copy_from_slice(&self.data[self.get_idx..self.put_idx]);
                }
                self.get_idx = self.put_idx;
                self.len = 0;
                dequeue_size
            }
            std::cmp::Ordering::Greater => {
                self.dequeue_slice(&mut out_buffer[0..self.len], self.len)
            }
        }
    }

    /// Returns readable data as one contiguous slice plus an optional wrapped slice.
    #[must_use]
    pub fn as_slice(&self) -> (&[T], Option<&[T]>) {
        if self.get_idx < self.put_idx {
            (&self.data[self.get_idx..self.put_idx], None)
        } else {
            (&self.data[self.get_idx..], Some(&self.data[..self.put_idx]))
        }
    }

    /// Returns mutable readable data as one contiguous slice plus an optional wrapped slice.
    #[must_use]
    pub fn as_mut_slice(&mut self) -> (&mut [T], Option<&mut [T]>) {
        if self.get_idx < self.put_idx {
            (&mut self.data[self.get_idx..self.put_idx], None)
        } else {
            let (left, right) = self.data.split_at_mut(self.get_idx);
            (right, Some(&mut left[..self.put_idx]))
        }
    }
}
impl<T> IntoIterator for BoundedRingBuffer<T>
where
    T: Copy + Default,
{
    type Item = T;
    type IntoIter = IntoIter<T>;

    fn into_iter(self) -> Self::IntoIter {
        IntoIter(self)
    }
}

impl<'a, T> IntoIterator for &'a BoundedRingBuffer<T>
where
    T: Copy + Default,
{
    type Item = &'a T;
    type IntoIter = Iter<'a, T>;

    fn into_iter(self) -> Self::IntoIter {
        self.iter()
    }
}

impl<'a, T> IntoIterator for &'a mut BoundedRingBuffer<T>
where
    T: Copy + Default,
{
    type Item = &'a mut T;
    type IntoIter = IterMut<'a, T>;

    fn into_iter(self) -> Self::IntoIter {
        self.iter_mut()
    }
}