use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use parking_lot::Mutex;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::mpsc;
pub type ReloadCallback = Arc<dyn Fn(&Path) + Send + Sync>;
pub struct HotReloadManager {
watcher: Arc<Mutex<Option<RecommendedWatcher>>>,
callbacks: Arc<Mutex<Vec<ReloadCallback>>>,
watched_paths: Arc<Mutex<HashMap<PathBuf, Instant>>>,
debounce_duration: Duration,
event_tx: Arc<Mutex<Option<mpsc::UnboundedSender<Event>>>>,
}
impl HotReloadManager {
pub fn new() -> Self {
Self {
watcher: Arc::new(Mutex::new(None)),
callbacks: Arc::new(Mutex::new(Vec::new())),
watched_paths: Arc::new(Mutex::new(HashMap::new())),
debounce_duration: Duration::from_millis(100),
event_tx: Arc::new(Mutex::new(None)),
}
}
pub fn with_debounce(debounce_duration: Duration) -> Self {
Self {
watcher: Arc::new(Mutex::new(None)),
callbacks: Arc::new(Mutex::new(Vec::new())),
watched_paths: Arc::new(Mutex::new(HashMap::new())),
debounce_duration,
event_tx: Arc::new(Mutex::new(None)),
}
}
pub fn on_reload(&self, callback: ReloadCallback) {
self.callbacks.lock().push(callback);
}
pub async fn watch(&self, path: &Path) -> Result<(), String> {
let canonical_path = path
.canonicalize()
.map_err(|e| format!("Failed to canonicalize path {:?}: {}", path, e))?;
self.ensure_watcher().await?;
let mut watcher_guard = self.watcher.lock();
if let Some(watcher) = watcher_guard.as_mut() {
watcher
.watch(&canonical_path, RecursiveMode::NonRecursive)
.map_err(|e| format!("Failed to watch path {:?}: {}", canonical_path, e))?;
drop(watcher_guard);
let initial_timestamp = Instant::now()
.checked_sub(self.debounce_duration)
.unwrap_or_else(Instant::now);
self.watched_paths
.lock()
.insert(canonical_path.clone(), initial_timestamp);
Ok(())
} else {
Err("Watcher not initialized".to_string())
}
}
pub async fn unwatch(&self, path: &Path) -> Result<(), String> {
let canonical_path = path
.canonicalize()
.map_err(|e| format!("Failed to canonicalize path {:?}: {}", path, e))?;
let mut watcher_guard = self.watcher.lock();
if let Some(watcher) = watcher_guard.as_mut() {
watcher
.unwatch(&canonical_path)
.map_err(|e| format!("Failed to unwatch path {:?}: {}", canonical_path, e))?;
drop(watcher_guard);
self.watched_paths.lock().remove(&canonical_path);
Ok(())
} else {
Err("Watcher not initialized".to_string())
}
}
pub async fn stop(&self) -> Result<(), String> {
*self.watcher.lock() = None;
*self.event_tx.lock() = None;
self.watched_paths.lock().clear();
Ok(())
}
pub fn watched_paths(&self) -> Vec<PathBuf> {
self.watched_paths.lock().keys().cloned().collect()
}
async fn ensure_watcher(&self) -> Result<(), String> {
let mut watcher_guard = self.watcher.lock();
if watcher_guard.is_some() {
return Ok(());
}
let (tx, mut rx) = mpsc::unbounded_channel();
*self.event_tx.lock() = Some(tx.clone());
let watcher = RecommendedWatcher::new(
move |result: notify::Result<Event>| {
if let Ok(event) = result {
let _ = tx.send(event);
}
},
Config::default(),
)
.map_err(|e| format!("Failed to create watcher: {}", e))?;
*watcher_guard = Some(watcher);
drop(watcher_guard);
let callbacks = self.callbacks.clone();
let watched_paths = self.watched_paths.clone();
let debounce_duration = self.debounce_duration;
tokio::spawn(async move {
while let Some(event) = rx.recv().await {
Self::process_event(event, &callbacks, &watched_paths, debounce_duration).await;
}
});
Ok(())
}
async fn process_event(
event: Event,
callbacks: &Arc<Mutex<Vec<ReloadCallback>>>,
watched_paths: &Arc<Mutex<HashMap<PathBuf, Instant>>>,
debounce_duration: Duration,
) {
if !matches!(
event.kind,
EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)
) {
return;
}
for path in event.paths {
let canonical_path = match path.canonicalize() {
Ok(p) => p,
Err(_) => continue, };
let mut paths_guard = watched_paths.lock();
if let Some(last_event) = paths_guard.get_mut(&canonical_path) {
let now = Instant::now();
if now.duration_since(*last_event) < debounce_duration {
continue;
}
*last_event = now;
drop(paths_guard);
let callbacks_guard = callbacks.lock();
for callback in callbacks_guard.iter() {
callback(&canonical_path);
}
}
}
}
}
impl Default for HotReloadManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[tokio::test]
async fn test_new_manager() {
let manager = HotReloadManager::new();
assert_eq!(manager.watched_paths().len(), 0);
}
#[tokio::test]
async fn test_with_custom_debounce() {
let manager = HotReloadManager::with_debounce(Duration::from_millis(500));
assert_eq!(manager.debounce_duration, Duration::from_millis(500));
}
#[tokio::test]
async fn test_watch_file() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.txt");
fs::write(&file_path, "initial content").unwrap();
let manager = HotReloadManager::new();
manager.watch(&file_path).await.unwrap();
let watched = manager.watched_paths();
assert_eq!(watched.len(), 1);
}
#[tokio::test]
async fn test_unwatch_file() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.txt");
fs::write(&file_path, "initial content").unwrap();
let manager = HotReloadManager::new();
manager.watch(&file_path).await.unwrap();
assert_eq!(manager.watched_paths().len(), 1);
manager.unwatch(&file_path).await.unwrap();
assert_eq!(manager.watched_paths().len(), 0);
}
#[tokio::test]
async fn test_stop_watching() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.txt");
fs::write(&file_path, "initial content").unwrap();
let manager = HotReloadManager::new();
manager.watch(&file_path).await.unwrap();
manager.stop().await.unwrap();
assert_eq!(manager.watched_paths().len(), 0);
}
#[tokio::test]
async fn test_watch_nonexistent_file() {
let manager = HotReloadManager::new();
let result = manager.watch(Path::new("/nonexistent/file.txt")).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_default_implementation() {
let manager = HotReloadManager::default();
assert_eq!(manager.watched_paths().len(), 0);
assert_eq!(manager.debounce_duration, Duration::from_millis(100));
}
}