frp-signal 0.1.0

Lifecycle and telemetry signal contracts for frp persistence flows.
Documentation
use std::{
    collections::HashSet,
    sync::{Arc, Mutex},
};

// ---------------------------------------------------------------------------
// DirtySet
// ---------------------------------------------------------------------------

/// A cheap-to-clone, thread-safe set of "dirty" numeric IDs.
///
/// Internally backed by an `Arc<Mutex<HashSet<u64>>>` so all clones share the
/// same underlying state — useful for passing a single `DirtySet` handle to
/// multiple subscribers.
#[derive(Clone, Default)]
pub struct DirtySet {
    inner: Arc<Mutex<HashSet<u64>>>,
}

impl DirtySet {
    /// Create a new, empty `DirtySet`.
    pub fn new() -> Self {
        Self::default()
    }

    /// Mark `id` as dirty.
    pub fn mark_dirty(&self, id: u64) {
        self.inner.lock().expect("DirtySet mutex poisoned").insert(id);
    }

    /// Returns `true` if `id` is currently marked dirty.
    pub fn is_dirty(&self, id: u64) -> bool {
        self.inner.lock().expect("DirtySet mutex poisoned").contains(&id)
    }

    /// Clear the dirty flag for `id` only.
    pub fn clear(&self, id: u64) {
        self.inner.lock().expect("DirtySet mutex poisoned").remove(&id);
    }

    /// Remove and return all currently dirty IDs.
    pub fn drain_dirty(&self) -> Vec<u64> {
        let mut guard = self.inner.lock().expect("DirtySet mutex poisoned");
        guard.drain().collect()
    }

    /// Number of dirty IDs currently tracked.
    pub fn len(&self) -> usize {
        self.inner.lock().expect("DirtySet mutex poisoned").len()
    }

    /// Returns `true` if no IDs are dirty.
    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }
}

impl std::fmt::Debug for DirtySet {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let guard = self.inner.lock().expect("DirtySet mutex poisoned");
        f.debug_set().entries(guard.iter()).finish()
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    #[test]
    fn mark_and_check() {
        let ds = DirtySet::new();
        assert!(!ds.is_dirty(1));
        ds.mark_dirty(1);
        assert!(ds.is_dirty(1));
    }

    #[test]
    fn clear_single() {
        let ds = DirtySet::new();
        ds.mark_dirty(1);
        ds.mark_dirty(2);
        ds.clear(1);
        assert!(!ds.is_dirty(1));
        assert!(ds.is_dirty(2));
    }

    #[test]
    fn drain_returns_all_and_clears() {
        let ds = DirtySet::new();
        ds.mark_dirty(1);
        ds.mark_dirty(2);
        ds.mark_dirty(3);
        let mut drained = ds.drain_dirty();
        drained.sort();
        assert_eq!(drained, vec![1, 2, 3]);
        assert!(ds.is_empty());
    }

    #[test]
    fn clone_shares_state() {
        let ds = DirtySet::new();
        let ds2 = ds.clone();
        ds.mark_dirty(42);
        assert!(ds2.is_dirty(42));
    }
}