use std::any::TypeId;
use std::future::Future;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::Arc;
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,
}
impl DaemonConfig {
#[must_use]
pub fn new(db_path: PathBuf) -> Self {
Self {
db_path,
debounce_ms: DEFAULT_DEBOUNCE_MS,
}
}
}
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(),
debounce_ms: config.debounce_ms,
}))
}
}
struct DaemonCapability {
db_path: PathBuf,
debounce_ms: u64,
}
impl DaemonRunner for DaemonCapability {
fn start(&self, watch_path: &Path, project_name: &str) -> Result<(), DaemonError> {
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, self.debounce_ms, &self.db_path);
let observer =
IndexObserver::new(facade, project_name.to_string(), watch_path.to_path_buf());
daemon.add_observer(Box::new(observer));
daemon.run()
}
}
#[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);
}
}