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 {
46 let Some(dir) = ProjectIndex::index_dir(project_root) else {
47 return Fingerprint::default();
48 };
49 for name in ["index.json.zst", "index.json"] {
50 if let Ok(meta) = std::fs::metadata(dir.join(name)) {
51 return Fingerprint {
52 mtime: meta.modified().ok(),
53 size: meta.len(),
54 };
55 }
56 }
57 Fingerprint::default()
58}
59
60pub fn get_cached(project_root: &str) -> Option<Arc<ProjectIndex>> {
64 let fingerprint = index_fingerprint(project_root);
65
66 {
67 let mut map = cache()
68 .lock()
69 .unwrap_or_else(std::sync::PoisonError::into_inner);
70 if let Some(entry) = map.get_mut(project_root)
71 && entry.fingerprint == fingerprint
72 {
73 entry.last_access = Instant::now();
74 return Some(Arc::clone(&entry.index));
75 }
76 }
77
78 let idx = ProjectIndex::load(project_root).filter(|i| !i.files.is_empty())?;
79 let arc = Arc::new(idx);
80
81 let mut map = cache()
82 .lock()
83 .unwrap_or_else(std::sync::PoisonError::into_inner);
84 if !map.contains_key(project_root)
87 && map.len() >= MAX_ROOTS
88 && let Some(lru_key) = map
89 .iter()
90 .min_by_key(|(_, e)| e.last_access)
91 .map(|(k, _)| k.clone())
92 {
93 map.remove(&lru_key);
94 }
95 map.insert(
96 project_root.to_string(),
97 Entry {
98 index: Arc::clone(&arc),
99 fingerprint,
100 last_access: Instant::now(),
101 },
102 );
103 Some(arc)
104}
105
106pub fn invalidate(project_root: Option<&str>) {
108 let mut map = cache()
109 .lock()
110 .unwrap_or_else(std::sync::PoisonError::into_inner);
111 match project_root {
112 Some(root) => {
113 map.remove(root);
114 }
115 None => map.clear(),
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn returns_none_without_index() {
125 let tmp = tempfile::tempdir().unwrap();
126 invalidate(None);
127 assert!(get_cached(tmp.path().to_str().unwrap()).is_none());
128 }
129}