lean_ctx/core/
graph_cache.rs1use std::collections::HashMap;
12use std::sync::{Arc, Mutex, OnceLock};
13use std::time::{Instant, SystemTime};
14
15const MAX_ROOTS: usize = 8;
20
21use crate::core::graph_index::ProjectIndex;
22
23#[derive(Clone, Copy, PartialEq, Eq, Default)]
27struct Fingerprint {
28 mtime: Option<SystemTime>,
29 size: u64,
30}
31
32struct Entry {
33 index: Arc<ProjectIndex>,
34 fingerprint: Fingerprint,
35 last_access: Instant,
36}
37
38static CACHE: OnceLock<Mutex<HashMap<String, Entry>>> = OnceLock::new();
39
40fn cache() -> &'static Mutex<HashMap<String, Entry>> {
41 CACHE.get_or_init(|| Mutex::new(HashMap::new()))
42}
43
44fn index_fingerprint(project_root: &str) -> Fingerprint {
51 let Some(dir) = ProjectIndex::index_dir(project_root) else {
52 return Fingerprint::default();
53 };
54 for name in ["graph.meta.json", "graph.db"] {
55 if let Ok(meta) = std::fs::metadata(dir.join(name)) {
56 return Fingerprint {
57 mtime: meta.modified().ok(),
58 size: meta.len(),
59 };
60 }
61 }
62 Fingerprint::default()
63}
64
65pub fn get_cached(project_root: &str) -> Option<Arc<ProjectIndex>> {
69 let fingerprint = index_fingerprint(project_root);
70
71 {
72 let mut map = cache()
73 .lock()
74 .unwrap_or_else(std::sync::PoisonError::into_inner);
75 if let Some(entry) = map.get_mut(project_root)
76 && entry.fingerprint == fingerprint
77 {
78 entry.last_access = Instant::now();
79 return Some(Arc::clone(&entry.index));
80 }
81 }
82
83 let idx = ProjectIndex::load(project_root).filter(|i| !i.files.is_empty())?;
84 let arc = Arc::new(idx);
85
86 let mut map = cache()
87 .lock()
88 .unwrap_or_else(std::sync::PoisonError::into_inner);
89 if !map.contains_key(project_root)
92 && map.len() >= MAX_ROOTS
93 && let Some(lru_key) = map
94 .iter()
95 .min_by_key(|(_, e)| e.last_access)
96 .map(|(k, _)| k.clone())
97 {
98 map.remove(&lru_key);
99 }
100 map.insert(
101 project_root.to_string(),
102 Entry {
103 index: Arc::clone(&arc),
104 fingerprint,
105 last_access: Instant::now(),
106 },
107 );
108 Some(arc)
109}
110
111pub fn invalidate(project_root: Option<&str>) {
113 let mut map = cache()
114 .lock()
115 .unwrap_or_else(std::sync::PoisonError::into_inner);
116 match project_root {
117 Some(root) => {
118 map.remove(root);
119 }
120 None => map.clear(),
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn returns_none_without_index() {
130 let tmp = tempfile::tempdir().unwrap();
131 invalidate(None);
132 assert!(get_cached(tmp.path().to_str().unwrap()).is_none());
133 }
134}