use std::collections::{BTreeMap, HashMap};
use std::path::Path;
use std::sync::{LazyLock, Mutex};
use crate::directory::is_code_graph_data_dir;
use crate::extraction::grammars::is_source_file;
use crate::utils::normalize_path;
use super::backend::{WatchStartError, create_watch_handle, supports_recursive_watch};
use super::types::{EXHAUSTION_REASON, PendingFile, PendingInfo};
use super::util::{now_ms, watch_registry_key};
static FACADE_WATCHERS: LazyLock<Mutex<HashMap<std::path::PathBuf, FacadeWatchState>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
pub fn register_facade_watcher(project_root: impl AsRef<Path>, debounce_ms: u64) -> bool {
let root = watch_registry_key(project_root.as_ref());
let mut state = FacadeWatchState {
active: true,
debounce_ms,
pending_files: BTreeMap::new(),
degraded_reason: None,
auto_clear_after_debounce: true,
};
match create_watch_handle(&root, supports_recursive_watch()) {
Ok(mut handle) => handle.close(),
Err(WatchStartError::ResourceExhaustion(_)) => {
state.active = false;
state.degraded_reason = Some(EXHAUSTION_REASON.to_string());
}
Err(WatchStartError::InotifyExhaustion(message) | WatchStartError::Other(message)) => {
state.active = false;
state.degraded_reason = Some(message);
}
}
let active = state.active;
if let Ok(mut watchers) = FACADE_WATCHERS.lock() {
watchers.insert(root, state);
}
active
}
pub fn register_facade_runtime_watcher(project_root: impl AsRef<Path>, debounce_ms: u64) {
let root = watch_registry_key(project_root.as_ref());
let state = FacadeWatchState {
active: true,
debounce_ms,
pending_files: BTreeMap::new(),
degraded_reason: None,
auto_clear_after_debounce: false,
};
if let Ok(mut watchers) = FACADE_WATCHERS.lock() {
watchers.insert(root, state);
}
}
pub fn update_facade_runtime_watcher(
project_root: impl AsRef<Path>,
active: bool,
degraded_reason: Option<String>,
pending_files: Vec<PendingFile>,
) {
let key = watch_registry_key(project_root.as_ref());
let Ok(mut watchers) = FACADE_WATCHERS.lock() else {
return;
};
let Some(state) = watchers.get_mut(&key) else {
return;
};
state.active = active;
state.degraded_reason = degraded_reason;
state.pending_files = pending_files
.into_iter()
.map(|pending| {
(
pending.path,
PendingInfo {
first_seen_ms: pending.first_seen_ms,
last_seen_ms: pending.last_seen_ms,
indexing: pending.indexing,
},
)
})
.collect();
}
pub fn unregister_facade_watcher(project_root: impl AsRef<Path>) {
if let Ok(mut watchers) = FACADE_WATCHERS.lock() {
watchers.remove(&watch_registry_key(project_root.as_ref()));
}
}
pub fn facade_pending_files(project_root: impl AsRef<Path>) -> Vec<PendingFile> {
let Ok(mut watchers) = FACADE_WATCHERS.lock() else {
return Vec::new();
};
let key = watch_registry_key(project_root.as_ref());
let Some(state) = watchers.get_mut(&key) else {
return Vec::new();
};
flush_facade_due(state);
state
.pending_files
.iter()
.map(|(path, info)| PendingFile {
path: path.clone(),
first_seen_ms: info.first_seen_ms,
last_seen_ms: info.last_seen_ms,
indexing: info.indexing,
})
.collect()
}
pub fn facade_degraded_reason(project_root: impl AsRef<Path>) -> Option<String> {
let key = watch_registry_key(project_root.as_ref());
FACADE_WATCHERS.lock().ok().and_then(|watchers| {
watchers
.get(&key)
.and_then(|state| state.degraded_reason.clone())
})
}
pub(super) fn ingest_facade_event_for_tests(project_root: &Path, rel_path: &str) -> bool {
let Ok(mut watchers) = FACADE_WATCHERS.lock() else {
return false;
};
let Some(state) = watchers.get_mut(project_root) else {
return false;
};
if !state.active || state.degraded_reason.is_some() {
return false;
}
let rel = normalize_path(rel_path);
if rel.is_empty()
|| rel == "."
|| rel.starts_with("..")
|| rel.split('/').next().is_some_and(is_code_graph_data_dir)
|| !is_source_file(&rel)
{
return false;
}
let now = now_ms();
let first_seen_ms = state
.pending_files
.get(&rel)
.map(|info| info.first_seen_ms)
.unwrap_or(now);
state.pending_files.insert(
rel,
PendingInfo {
first_seen_ms,
last_seen_ms: now,
indexing: false,
},
);
true
}
fn flush_facade_due(state: &mut FacadeWatchState) {
if !state.auto_clear_after_debounce
|| state.pending_files.is_empty()
|| state.degraded_reason.is_some()
{
return;
}
let newest = state
.pending_files
.values()
.map(|info| info.last_seen_ms)
.max()
.unwrap_or(0);
if now_ms().saturating_sub(newest) >= state.debounce_ms as i64 {
state.pending_files.clear();
}
}
#[derive(Debug, Default)]
struct FacadeWatchState {
active: bool,
debounce_ms: u64,
pending_files: BTreeMap<String, PendingInfo>,
degraded_reason: Option<String>,
auto_clear_after_debounce: bool,
}