use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
pub(crate) const QUIET_WINDOW_MS: u64 = 500;
pub(crate) const WATER_LEVEL: usize = 5000;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WatchEvent {
Upsert(PathBuf),
UpsertDir(PathBuf),
Remove(PathBuf),
RemoveDir(PathBuf),
Rename {
from: PathBuf,
to: PathBuf,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PendingOp {
Upsert,
UpsertTree,
Remove,
RemoveTree,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PendingChange {
pub path: PathBuf,
pub op: PendingOp,
}
pub struct Debouncer {
roots: Vec<PathBuf>,
pending: BTreeMap<PathBuf, PendingOp>,
}
impl Debouncer {
pub fn new(roots: Vec<PathBuf>) -> Self {
Self {
roots,
pending: BTreeMap::new(),
}
}
fn in_scope(&self, path: &Path) -> bool {
self.roots.is_empty() || self.roots.iter().any(|r| path.starts_with(r))
}
pub fn push(&mut self, event: WatchEvent) -> bool {
match event {
WatchEvent::Upsert(p) => {
self.pending.insert(p, PendingOp::Upsert);
}
WatchEvent::UpsertDir(p) => {
self.pending.insert(p, PendingOp::UpsertTree);
}
WatchEvent::Remove(p) => {
self.pending.insert(p, PendingOp::Remove);
}
WatchEvent::RemoveDir(p) => {
self.pending.insert(p, PendingOp::RemoveTree);
}
WatchEvent::Rename { from, to } => {
if self.in_scope(&from) {
self.pending.insert(from, PendingOp::Remove);
}
if self.in_scope(&to) {
self.pending.insert(to, PendingOp::Upsert);
}
}
}
self.pending.len() >= WATER_LEVEL
}
pub fn is_empty(&self) -> bool {
self.pending.is_empty()
}
pub fn len(&self) -> usize {
self.pending.len()
}
pub fn drain(&mut self) -> Vec<PendingChange> {
std::mem::take(&mut self.pending)
.into_iter()
.map(|(path, op)| PendingChange { path, op })
.collect()
}
pub fn requeue(&mut self, changes: Vec<PendingChange>) {
for change in changes {
self.pending.entry(change.path).or_insert(change.op);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn root() -> PathBuf {
if cfg!(windows) {
PathBuf::from(r"C:\watch")
} else {
PathBuf::from("/watch")
}
}
fn under(root: &Path, rel: &str) -> PathBuf {
root.join(rel)
}
#[test]
fn multiple_events_on_same_path_merge_into_one() {
let r = root();
let mut d = Debouncer::new(vec![r.clone()]);
let p = under(&r, "a.md");
d.push(WatchEvent::Upsert(p.clone()));
d.push(WatchEvent::Upsert(p.clone()));
d.push(WatchEvent::Remove(p.clone()));
d.push(WatchEvent::Upsert(p.clone()));
let batch = d.drain();
assert_eq!(
batch,
vec![PendingChange {
path: p,
op: PendingOp::Upsert,
}],
"同一 path 的多次事件应合并成一条,保留最后状态"
);
assert!(d.is_empty(), "drain 之后队列应清空");
}
#[test]
fn create_then_delete_ends_as_remove() {
let r = root();
let mut d = Debouncer::new(vec![r.clone()]);
let p = under(&r, "tmp.md");
d.push(WatchEvent::Upsert(p.clone()));
d.push(WatchEvent::Remove(p.clone()));
assert_eq!(
d.drain(),
vec![PendingChange {
path: p,
op: PendingOp::Remove
}]
);
}
#[test]
fn rename_both_sides_in_scope_becomes_remove_old_upsert_new() {
let r = root();
let mut d = Debouncer::new(vec![r.clone()]);
let from = under(&r, "old.md");
let to = under(&r, "new.md");
d.push(WatchEvent::Rename {
from: from.clone(),
to: to.clone(),
});
let batch = d.drain();
assert_eq!(batch.len(), 2, "双边都在监听内:删旧 + 加新,共两条");
assert!(batch.contains(&PendingChange {
path: from,
op: PendingOp::Remove
}));
assert!(batch.contains(&PendingChange {
path: to,
op: PendingOp::Upsert
}));
}
#[test]
fn rename_only_target_in_scope_becomes_upsert() {
let r = root();
let mut d = Debouncer::new(vec![r.clone()]);
let outside = if cfg!(windows) {
PathBuf::from(r"C:\elsewhere\old.md")
} else {
PathBuf::from("/elsewhere/old.md")
};
let to = under(&r, "moved-in.md");
d.push(WatchEvent::Rename {
from: outside,
to: to.clone(),
});
assert_eq!(
d.drain(),
vec![PendingChange {
path: to,
op: PendingOp::Upsert
}],
"只有新名在监听内,应只产生一条 Upsert"
);
}
#[test]
fn rename_only_source_in_scope_becomes_remove() {
let r = root();
let mut d = Debouncer::new(vec![r.clone()]);
let from = under(&r, "moved-out.md");
let outside = if cfg!(windows) {
PathBuf::from(r"C:\elsewhere\new.md")
} else {
PathBuf::from("/elsewhere/new.md")
};
d.push(WatchEvent::Rename {
from: from.clone(),
to: outside,
});
assert_eq!(
d.drain(),
vec![PendingChange {
path: from,
op: PendingOp::Remove
}],
"只有旧名在监听内,应只产生一条 Remove"
);
}
#[test]
fn water_level_triggers_forced_flush() {
let r = root();
let mut d = Debouncer::new(vec![r.clone()]);
let mut forced = false;
for i in 0..WATER_LEVEL {
forced = d.push(WatchEvent::Upsert(under(&r, &format!("f{i}.md"))));
}
assert!(
forced,
"攒到水位阈值时最后一次 push 应返回 true 提示强制刷批"
);
assert_eq!(d.len(), WATER_LEVEL);
let mut d2 = Debouncer::new(vec![r.clone()]);
let mut forced2 = false;
for i in 0..(WATER_LEVEL - 1) {
forced2 = d2.push(WatchEvent::Upsert(under(&r, &format!("g{i}.md"))));
}
assert!(!forced2, "水位以下不应触发强制刷批");
}
#[test]
fn remove_dir_maps_to_remove_tree() {
let r = root();
let mut d = Debouncer::new(vec![r.clone()]);
let dir = under(&r, "sub");
d.push(WatchEvent::RemoveDir(dir.clone()));
assert_eq!(
d.drain(),
vec![PendingChange {
path: dir,
op: PendingOp::RemoveTree
}]
);
}
#[test]
fn upsert_dir_maps_to_upsert_tree() {
let r = root();
let mut d = Debouncer::new(vec![r.clone()]);
let dir = under(&r, "moved-in-dir");
d.push(WatchEvent::UpsertDir(dir.clone()));
assert_eq!(
d.drain(),
vec![PendingChange {
path: dir,
op: PendingOp::UpsertTree
}]
);
}
}