use std::any::TypeId;
use std::future::Future;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::{Arc, RwLock};
use crate::kit::{AsyncAutoBuilder, AsyncKit, ModuleMeta};
use super::capability::DaemonRunner;
use super::{Daemon, DaemonError, IndexObserver, DEFAULT_DEBOUNCE_MS};
use crate::index::IndexFacade;
#[derive(Debug, Clone)]
pub struct DaemonConfig {
pub db_path: PathBuf,
pub debounce_ms: u64,
pub verbose_events: bool,
pub impact_notify: bool,
#[cfg(feature = "hub")]
pub notify_webhook: Option<String>,
}
impl DaemonConfig {
#[must_use]
pub fn new(db_path: PathBuf) -> Self {
Self {
db_path,
debounce_ms: DEFAULT_DEBOUNCE_MS,
verbose_events: false,
impact_notify: false,
#[cfg(feature = "hub")]
notify_webhook: None,
}
}
}
pub struct DaemonModule;
impl ModuleMeta for DaemonModule {
const NAME: &'static str = "daemon";
fn dependencies() -> &'static [(&'static str, TypeId)] {
&[]
}
}
impl AsyncAutoBuilder for DaemonModule {
type Capability = Arc<dyn DaemonRunner>;
type Error = DaemonError;
fn build<'a>(
kit: &'a AsyncKit,
) -> Pin<Box<dyn Future<Output = Result<Self::Capability, Self::Error>> + Send + 'a>> {
Box::pin(async move {
let config = kit
.config::<DaemonConfig>()
.map_err(|e| DaemonError::Io(std::io::Error::other(e.to_string())))?;
Self::build_cap(&config)
})
}
}
impl DaemonModule {
pub(crate) fn build_cap(config: &DaemonConfig) -> Result<Arc<dyn DaemonRunner>, DaemonError> {
Ok(Arc::new(DaemonCapability {
db_path: config.db_path.clone(),
config: Arc::new(RwLock::new(config.clone())),
}))
}
}
struct DaemonCapability {
db_path: PathBuf,
config: Arc<RwLock<DaemonConfig>>,
}
impl DaemonRunner for DaemonCapability {
fn start(&self, watch_path: &Path, project_name: &str) -> Result<(), DaemonError> {
let config_guard = self.config.read().ok();
let (debounce_ms, verbose_events, impact_notify) = config_guard
.as_ref()
.map(|c| (c.debounce_ms, c.verbose_events, c.impact_notify))
.unwrap_or((DEFAULT_DEBOUNCE_MS, false, false));
#[cfg(feature = "hub")]
let notify_webhook = config_guard.as_ref().and_then(|c| c.notify_webhook.clone());
let facade = IndexFacade::new(&self.db_path)
.map_err(|e| std::io::Error::other(format!("IndexFacade::new: {e}")))?;
let mut daemon = Daemon::new(watch_path, project_name, debounce_ms, &self.db_path);
daemon.set_verbose(verbose_events);
let mut observer =
IndexObserver::new(facade, project_name.to_string(), watch_path.to_path_buf());
if impact_notify {
let notify_observer =
crate::daemon::impact_observer::ImpactNotifyObserver::new(self.db_path.clone());
#[cfg(feature = "hub")]
let notify_observer = notify_observer.with_webhook(notify_webhook);
observer.add_completion_observer(Box::new(notify_observer));
}
daemon.add_observer(Box::new(observer));
daemon.run()
}
fn update_debounce_ms(&self, new_ms: u64) {
if let Ok(mut cfg) = self.config.write() {
cfg.debounce_ms = new_ms;
}
}
fn update_impact_notify(&self, enabled: bool) {
if let Ok(mut cfg) = self.config.write() {
cfg.impact_notify = enabled;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kit::{AsyncKit, DaemonModule};
#[test]
fn build_returns_send_sync_capability() {
let cap = DaemonModule::build_cap(&DaemonConfig::new(PathBuf::from(":memory:")))
.expect("DaemonModule::build_cap");
fn _assert_send_sync<T: Send + Sync>(_: &T) {}
_assert_send_sync(&cap);
}
#[test]
fn capability_start_nonexistent_watch_path_returns_error() {
let cap = DaemonModule::build_cap(&DaemonConfig::new(PathBuf::from(":memory:")))
.expect("DaemonModule::build_cap");
let result = cap.start(Path::new("/nonexistent/path/xyz/abc"), "demo");
assert!(
result.is_err(),
"nonexistent watch path should fail immediately, got {result:?}"
);
let err = result.err().unwrap();
assert!(
matches!(err, DaemonError::Notify(_)),
"expected DaemonError::Notify, got {err:?}"
);
}
#[tokio::test]
async fn kit_registration_flow() {
let mut kit = AsyncKit::new();
kit.set_config(DaemonConfig::new(PathBuf::from(":memory:")));
kit.register::<DaemonModule>()
.expect("register::<DaemonModule>");
let kit = kit.build().await.expect("build");
assert!(kit.contains::<DaemonModule>(), "DaemonModule missing");
let _required = kit
.require::<DaemonModule>()
.expect("require::<DaemonModule>");
}
#[test]
fn daemon_config_new_uses_default_debounce() {
let cfg = DaemonConfig::new(PathBuf::from("/tmp/db.lbug"));
assert_eq!(cfg.db_path, PathBuf::from("/tmp/db.lbug"));
assert_eq!(cfg.debounce_ms, DEFAULT_DEBOUNCE_MS);
}
#[test]
fn update_debounce_ms_changes_shared_config() {
let config = DaemonConfig::new(PathBuf::from(":memory:"));
let cap = DaemonCapability {
db_path: config.db_path.clone(),
config: Arc::new(RwLock::new(config)),
};
{
let cfg = cap.config.read().unwrap();
assert_eq!(cfg.debounce_ms, DEFAULT_DEBOUNCE_MS);
}
cap.update_debounce_ms(500);
{
let cfg = cap.config.read().unwrap();
assert_eq!(cfg.debounce_ms, 500);
}
}
}