use std::sync::Arc;
use super::helpers;
use super::{MapCache, ServerState};
pub(super) async fn run_background_gc(state: Arc<ServerState>) {
let result = tokio::task::spawn_blocking(move || {
let store = state.store.blocking_read();
let referenced = crate::store_gc::collect_referenced_hashes(&store.basemind_dir)?;
crate::store_gc::gc_blobs(&store.basemind_dir, &referenced)
})
.await;
match result {
Ok(Ok(report)) if report.removed > 0 => tracing::info!(
removed = report.removed,
bytes_freed = report.bytes_freed,
"background blob GC reclaimed orphaned blobs"
),
Ok(Ok(_)) => tracing::debug!("background blob GC: nothing to reclaim"),
Ok(Err(error)) => tracing::warn!(%error, "background blob GC failed"),
Err(error) => tracing::warn!(%error, "background blob GC task panicked"),
}
}
pub(super) fn spawn_serve_watcher(state: Arc<ServerState>) {
let root = state.root.clone();
let config = Arc::clone(&state.config);
let handle = tokio::runtime::Handle::current();
let (_shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
std::thread::Builder::new()
.name("basemind-mcp-serve-watcher".to_string())
.spawn(move || {
let _keep_sender_alive = _shutdown_tx;
tracing::info!(root = %root.display(), "serve watcher armed (live incremental rescan)");
let result =
crate::watcher::watch_paths(&root, &config, shutdown_rx, |paths, _kind| {
let refresh_state = Arc::clone(&state);
match handle.block_on(helpers::scan_and_refresh(refresh_state, Some(paths))) {
Ok(report) => tracing::debug!(
scanned = report.stats.scanned,
updated = report.stats.updated,
removed = report.stats.removed,
"serve watcher: incremental rescan complete"
),
Err(error) => tracing::warn!(
%error,
"serve watcher: incremental rescan failed (watcher continues)"
),
}
});
if let Err(error) = result {
tracing::warn!(%error, "serve watcher exited with error");
}
tracing::info!("serve watcher: exiting");
})
.ok();
}
pub(super) fn spawn_view_watcher(state: Arc<ServerState>) {
let (basemind_dir, view) = {
let store = match state.store.try_read() {
Ok(g) => g,
Err(_) => return,
};
(store.basemind_dir.clone(), store.view.clone())
};
let view_dir = basemind_dir.join(crate::store::VIEWS_DIR).join(&view);
let target = view_dir.join(crate::store::INDEX_FILE);
std::thread::Builder::new()
.name("basemind-mcp-view-watcher".to_string())
.spawn(move || {
use notify_debouncer_full::new_debouncer;
use std::time::Duration;
let (tx, rx) = std::sync::mpsc::channel();
let mut debouncer = match new_debouncer(Duration::from_millis(150), None, tx) {
Ok(d) => d,
Err(e) => {
tracing::warn!(error = %e, "view watcher: failed to start debouncer");
return;
}
};
if let Err(e) = debouncer.watch(&view_dir, notify::RecursiveMode::NonRecursive) {
tracing::warn!(error = %e, dir = %view_dir.display(), "view watcher: failed to watch");
return;
}
tracing::info!(target = %target.display(), "view watcher armed");
while let Ok(result) = rx.recv() {
let events = match result {
Ok(e) => e,
Err(_) => continue,
};
let touches_index = events
.iter()
.any(|de| de.event.paths.iter().any(|p| p == &target));
if !touches_index {
continue;
}
let new_store = match crate::store::Store::open_read_only(
state.root.as_path(),
&state
.store
.try_read()
.map(|g| g.view.clone())
.unwrap_or_default(),
) {
Ok(s) => s,
Err(e) => {
tracing::warn!(error = %e, "view watcher: store reopen failed");
continue;
}
};
let new_cache = Arc::new(MapCache::build(&new_store));
tracing::info!(
files = new_cache.by_path.len(),
"view watcher: rebuilt MapCache from refreshed index"
);
state.cache.store(new_cache);
state
.cache_generation
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
tracing::info!("view watcher: channel closed; exiting");
})
.ok();
}