use std::collections::{HashMap, VecDeque};
use std::sync::Mutex;
use std::sync::mpsc::{SendError, Sender};
use serde::{Deserialize, Serialize};
use crate::events::WatchEvent;
pub(crate) type VolumeKey = String;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct UsnCursor {
pub journal_id: u64,
pub next_usn: i64,
}
pub(crate) fn cursor_is_usable(
cursor: UsnCursor,
live_journal_id: u64,
lowest_valid_usn: i64,
) -> bool {
cursor.journal_id == live_journal_id && cursor.next_usn >= lowest_valid_usn
}
pub(crate) struct CursorSync {
inner: Mutex<CursorSyncState>,
}
#[derive(Default)]
struct CursorSyncState {
pending: VecDeque<(VolumeKey, i64)>,
read: HashMap<VolumeKey, i64>,
safe: HashMap<VolumeKey, i64>,
}
impl CursorSync {
pub fn new() -> Self {
Self {
inner: Mutex::new(CursorSyncState::default()),
}
}
pub fn record_and_send(
&self,
volume: VolumeKey,
usn: i64,
event: WatchEvent,
tx: &Sender<WatchEvent>,
) -> Result<(), SendError<WatchEvent>> {
let mut guard = self.inner.lock().expect("cursor sync mutex poisoned");
guard.pending.push_back((volume, usn));
tx.send(event)
}
pub fn on_received(&self) {
let mut guard = self.inner.lock().expect("cursor sync mutex poisoned");
if let Some((volume, usn)) = guard.pending.pop_front() {
guard.read.insert(volume, usn);
}
}
pub fn on_committed(&self) -> HashMap<VolumeKey, i64> {
let mut guard = self.inner.lock().expect("cursor sync mutex poisoned");
for (volume, usn) in guard.read.clone() {
guard.safe.insert(volume, usn);
}
guard.safe.clone()
}
}
impl Default for CursorSync {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use std::sync::mpsc;
fn dummy_event(name: &str) -> WatchEvent {
WatchEvent::Upsert(PathBuf::from(name))
}
#[test]
fn cursor_usable_when_journal_id_matches_and_within_retention() {
let cursor = UsnCursor {
journal_id: 42,
next_usn: 1000,
};
assert!(cursor_is_usable(cursor, 42, 500));
assert!(cursor_is_usable(cursor, 42, 1000)); }
#[test]
fn cursor_unusable_when_journal_id_changed() {
let cursor = UsnCursor {
journal_id: 42,
next_usn: 1000,
};
assert!(!cursor_is_usable(cursor, 43, 0));
}
#[test]
fn cursor_unusable_when_expired_past_retention() {
let cursor = UsnCursor {
journal_id: 42,
next_usn: 1000,
};
assert!(!cursor_is_usable(cursor, 42, 1001));
}
#[test]
fn committed_snapshot_reflects_all_received_events_in_order() {
let sync = CursorSync::new();
let (tx, rx) = mpsc::channel();
sync.record_and_send("C:".to_string(), 10, dummy_event("a"), &tx)
.unwrap();
sync.record_and_send("C:".to_string(), 20, dummy_event("b"), &tx)
.unwrap();
rx.recv().unwrap();
sync.on_received(); rx.recv().unwrap();
sync.on_received();
let safe = sync.on_committed();
assert_eq!(safe.get("C:"), Some(&20));
}
#[test]
fn uncommitted_reads_are_not_exposed_as_safe() {
let sync = CursorSync::new();
let (tx, rx) = mpsc::channel();
sync.record_and_send("C:".to_string(), 10, dummy_event("a"), &tx)
.unwrap();
rx.recv().unwrap();
sync.on_received();
let safe_before = {
let guard = sync.inner.lock().unwrap();
guard.safe.clone()
};
assert!(safe_before.is_empty());
}
#[test]
fn multi_volume_interleaving_tracks_each_volume_independently() {
let sync = CursorSync::new();
let (tx, rx) = mpsc::channel();
sync.record_and_send("C:".to_string(), 100, dummy_event("a"), &tx)
.unwrap();
sync.record_and_send("D:".to_string(), 200, dummy_event("b"), &tx)
.unwrap();
sync.record_and_send("C:".to_string(), 101, dummy_event("c"), &tx)
.unwrap();
rx.recv().unwrap();
sync.on_received(); rx.recv().unwrap();
sync.on_received(); let safe = sync.on_committed();
assert_eq!(safe.get("C:"), Some(&100));
assert_eq!(safe.get("D:"), Some(&200));
rx.recv().unwrap();
sync.on_received(); let safe2 = sync.on_committed();
assert_eq!(safe2.get("C:"), Some(&101));
assert_eq!(safe2.get("D:"), Some(&200));
}
#[test]
fn events_with_no_pending_record_do_not_advance_cursor() {
let sync = CursorSync::new();
sync.on_received();
let safe = sync.on_committed();
assert!(safe.is_empty());
}
#[test]
fn record_and_send_preserves_pending_order_matching_send_order() {
let sync = CursorSync::new();
let (tx, rx) = mpsc::channel();
for i in 0..5 {
sync.record_and_send("C:".to_string(), i, dummy_event(&format!("f{i}")), &tx)
.unwrap();
}
for expected_usn in 0..5 {
let event = rx.recv().unwrap();
assert_eq!(event, dummy_event(&format!("f{expected_usn}")));
sync.on_received();
}
let safe = sync.on_committed();
assert_eq!(safe.get("C:"), Some(&4));
}
}