use std::path::Path;
use std::time::Duration;
use notify::{EventKind, RecommendedWatcher, RecursiveMode};
use notify_debouncer_full::{new_debouncer, DebounceEventResult, Debouncer, RecommendedCache};
use tokio::sync::mpsc;
use super::reconciler::request::ReconcileRequest;
const TAMPER_DEBOUNCE_MS: u64 = 500;
pub fn spawn_watcher(
settings_path: &Path,
tx: mpsc::Sender<ReconcileRequest>,
) -> Result<Debouncer<RecommendedWatcher, RecommendedCache>, notify::Error> {
let dir = settings_path
.parent()
.expect("settings.json has a parent directory")
.to_path_buf();
let target_name = settings_path
.file_name()
.expect("settings.json has a filename")
.to_os_string();
let mut debouncer = new_debouncer(
Duration::from_millis(TAMPER_DEBOUNCE_MS),
None,
move |res: DebounceEventResult| {
if let Ok(events) = res {
let hit = events
.iter()
.any(|ev| event_targets_settings(ev, &target_name));
if hit && tx.try_send(ReconcileRequest::Fs).is_err() {
tracing::warn!("reconcile channel full — skipping fs-triggered reconcile");
}
}
},
)?;
debouncer.watch(&dir, RecursiveMode::NonRecursive)?;
Ok(debouncer)
}
fn event_targets_settings(
ev: ¬ify_debouncer_full::DebouncedEvent,
target_name: &std::ffi::OsStr,
) -> bool {
let is_relevant_kind = matches!(
ev.event.kind,
EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)
);
if !is_relevant_kind {
return false;
}
ev.event
.paths
.iter()
.any(|p| p.file_name() == Some(target_name))
}
pub fn spawn_poll_fallback(tx: mpsc::Sender<ReconcileRequest>) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(30));
interval.tick().await;
loop {
interval.tick().await;
if tx.send(ReconcileRequest::Poll).await.is_err() {
break;
}
}
})
}