use std::collections::HashMap;
use std::path::Path;
use std::sync::{Arc, LazyLock, Mutex, RwLock};
use std::time::{Duration, Instant};
use crate::repo_map::RepoMap;
static GLOBAL: LazyLock<Arc<ProjectCacheManager>> =
LazyLock::new(|| Arc::new(ProjectCacheManager::new()));
pub fn global() -> &'static Arc<ProjectCacheManager> {
&GLOBAL
}
pub fn snapshot(project_dir: &str) -> RepoMapSnapshot {
global().get_or_build(project_dir).snapshot()
}
pub struct RepoMapSnapshot {
pub map_string: String,
pub file_count: usize,
pub symbol_count: usize,
}
pub struct ProjectCache {
project_dir: String,
repo_map: RwLock<RepoMap>,
last_access: Mutex<Instant>,
pending_invalidations: Mutex<Vec<String>>,
}
const EVICTION_TTL: Duration = Duration::from_secs(30 * 60);
const FLUSH_DEBOUNCE: Duration = Duration::from_millis(500);
const MAINTENANCE_INTERVAL: Duration = Duration::from_secs(300);
impl ProjectCache {
fn build(project_dir: &str) -> Self {
let mut repo_map = RepoMap::new(Path::new(project_dir));
repo_map.rebuild();
Self {
project_dir: project_dir.to_string(),
repo_map: RwLock::new(repo_map),
last_access: Mutex::new(Instant::now()),
pending_invalidations: Mutex::new(Vec::new()),
}
}
fn touch(&self) {
if let Ok(mut ts) = self.last_access.lock() {
*ts = Instant::now();
}
}
fn idle_duration(&self) -> Duration {
self.last_access
.lock()
.map(|ts| ts.elapsed())
.unwrap_or(Duration::ZERO)
}
pub fn map_string(&self) -> String {
self.touch();
self.repo_map.read().unwrap().to_map_string()
}
pub fn file_count(&self) -> usize {
self.repo_map.read().unwrap().file_count()
}
pub fn symbol_count(&self) -> usize {
self.repo_map.read().unwrap().symbol_count()
}
pub fn snapshot(&self) -> RepoMapSnapshot {
self.touch();
let map = self.repo_map.read().unwrap();
RepoMapSnapshot {
map_string: map.to_map_string(),
file_count: map.file_count(),
symbol_count: map.symbol_count(),
}
}
#[allow(dead_code)]
pub fn is_ready(&self) -> bool {
self.repo_map.read().unwrap().is_ready()
}
#[allow(dead_code)]
pub fn relevant_files_for_query(&self, query: &str, top_n: usize) -> Vec<(String, f64)> {
self.touch();
self.repo_map
.read()
.unwrap()
.relevant_files_for_query(query, top_n)
}
#[allow(dead_code)]
pub fn search(&self, query: &str, top_n: usize) -> Vec<crate::repo_map::bm25::SearchResult> {
self.touch();
self.repo_map.read().unwrap().search(query, top_n)
}
#[allow(dead_code)]
pub fn snapshot_with_query(&self, query: &str, top_n: usize) -> RepoMapSnapshot {
self.touch();
let map = self.repo_map.read().unwrap();
let mut map_string = map.to_map_string();
let file_count = map.file_count();
let symbol_count = map.symbol_count();
if map.is_ready() {
let intent = crate::repo_map::bm25::classify_query(query);
if intent != crate::repo_map::bm25::QueryIntent::Skip {
let relevant = map.relevant_files_for_query(query, top_n);
if !relevant.is_empty() {
map_string.push_str("\n[Query-relevant files]\n");
for (path, score) in &relevant {
let _ = std::fmt::Write::write_fmt(
&mut map_string,
format_args!(" {path} (relevance: {score:.2})\n"),
);
}
}
}
}
RepoMapSnapshot {
map_string,
file_count,
symbol_count,
}
}
pub fn queue_invalidation(&self, path: String) {
if let Ok(mut pending) = self.pending_invalidations.lock()
&& !pending.contains(&path)
{
pending.push(path);
}
}
fn flush_invalidations(&self) {
let paths: Vec<String> = {
let mut pending = self.pending_invalidations.lock().unwrap();
if pending.is_empty() {
return;
}
std::mem::take(&mut *pending)
};
let mut map = self.repo_map.write().unwrap();
for path in &paths {
map.invalidate(Path::new(path));
}
let updated = map.rebuild();
if updated > 0 {
tracing::debug!(
project = %self.project_dir,
invalidated = paths.len(),
re_parsed = updated,
"ProjectCache: incremental rebuild"
);
}
}
fn full_rebuild(&self) {
let mut map = self.repo_map.write().unwrap();
let count = map.rebuild();
tracing::debug!(
project = %self.project_dir,
re_parsed = count,
"ProjectCache: periodic rebuild"
);
}
}
pub struct ProjectCacheManager {
caches: Mutex<HashMap<String, Arc<ProjectCache>>>,
bg_started: std::sync::atomic::AtomicBool,
}
impl Default for ProjectCacheManager {
fn default() -> Self {
Self::new()
}
}
impl ProjectCacheManager {
pub fn new() -> Self {
Self {
caches: Mutex::new(HashMap::new()),
bg_started: std::sync::atomic::AtomicBool::new(false),
}
}
pub fn ensure_background_tasks(self: &Arc<Self>) {
if self
.bg_started
.swap(true, std::sync::atomic::Ordering::SeqCst)
{
return; }
self.spawn_maintenance();
self.spawn_flush_loop();
}
pub fn get_or_build(&self, project_dir: &str) -> Arc<ProjectCache> {
{
let caches = self.caches.lock().unwrap();
if let Some(cache) = caches.get(project_dir) {
cache.touch();
return Arc::clone(cache);
}
}
let cache = Arc::new(ProjectCache::build(project_dir));
let mut caches = self.caches.lock().unwrap();
caches
.entry(project_dir.to_string())
.or_insert_with(|| Arc::clone(&cache));
Arc::clone(caches.get(project_dir).unwrap())
}
pub fn notify_file_modified(&self, file_path: &str) {
let caches = self.caches.lock().unwrap();
for (project_dir, cache) in caches.iter() {
if file_path.starts_with(project_dir.as_str()) {
cache.queue_invalidation(file_path.to_string());
return;
}
}
}
fn flush_all(&self) {
let caches: Vec<Arc<ProjectCache>> =
{ self.caches.lock().unwrap().values().cloned().collect() };
for cache in caches {
cache.flush_invalidations();
}
}
fn evict_stale(&self) -> usize {
let mut caches = self.caches.lock().unwrap();
let before = caches.len();
caches.retain(|_, cache| cache.idle_duration() < EVICTION_TTL);
let evicted = before - caches.len();
if evicted > 0 {
tracing::info!(
evicted,
remaining = caches.len(),
"ProjectCache: LRU eviction"
);
}
evicted
}
fn maintenance(&self) {
self.flush_all();
self.evict_stale();
let caches: Vec<Arc<ProjectCache>> =
{ self.caches.lock().unwrap().values().cloned().collect() };
for cache in caches {
cache.full_rebuild();
}
}
pub fn spawn_maintenance(self: &Arc<Self>) {
let mgr = Arc::clone(self);
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(60)).await;
loop {
tokio::time::sleep(MAINTENANCE_INTERVAL).await;
let mgr = Arc::clone(&mgr);
let _ = tokio::task::spawn_blocking(move || mgr.maintenance()).await;
}
});
}
pub fn spawn_flush_loop(self: &Arc<Self>) {
let mgr = Arc::clone(self);
tokio::spawn(async move {
loop {
tokio::time::sleep(FLUSH_DEBOUNCE).await;
let mgr = Arc::clone(&mgr);
let _ = tokio::task::spawn_blocking(move || mgr.flush_all()).await;
}
});
}
}