pushwire-client 0.1.1

Generic multiplexed push client with WebSocket and SSE transports
Documentation
use dashmap::DashMap;
use pushwire_core::ChannelKind;
use std::collections::HashMap;

/// Result of advancing a cursor.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CursorResult {
    /// Cursor advanced sequentially (expected value).
    Sequential,
    /// Gap detected — frames were missed.
    GapDetected { expected: u64, got: u64 },
    /// Duplicate cursor — frame already seen, skip.
    Duplicate,
}

/// Per-channel cursor tracker with gap detection.
pub struct CursorTracker<C: ChannelKind> {
    cursors: DashMap<C, u64>,
}

impl<C: ChannelKind> CursorTracker<C> {
    pub fn new() -> Self {
        Self {
            cursors: DashMap::new(),
        }
    }

    /// Process an incoming cursor value. Returns whether it was sequential,
    /// a gap, or a duplicate.
    pub fn advance(&self, channel: C, cursor: u64) -> CursorResult {
        let prev = self.cursors.get(&channel).map(|v| *v).unwrap_or(0);
        if cursor == prev + 1 || prev == 0 {
            self.cursors.insert(channel, cursor);
            CursorResult::Sequential
        } else if cursor > prev + 1 {
            self.cursors.insert(channel, cursor);
            CursorResult::GapDetected {
                expected: prev + 1,
                got: cursor,
            }
        } else {
            CursorResult::Duplicate
        }
    }

    /// Export all cursors for resume handshake.
    pub fn export(&self) -> HashMap<C, u64> {
        self.cursors
            .iter()
            .map(|entry| (*entry.key(), *entry.value()))
            .collect()
    }

    /// Reset all cursors (e.g., on full reconnect without resume).
    pub fn reset(&self) {
        self.cursors.clear();
    }
}

impl<C: ChannelKind> Default for CursorTracker<C> {
    fn default() -> Self {
        Self::new()
    }
}