lean_ctx/core/
graph_cache.rs1use std::collections::HashMap;
12use std::sync::{Arc, Mutex, OnceLock};
13use std::time::SystemTime;
14
15use crate::core::graph_index::ProjectIndex;
16
17#[derive(Clone, Copy, PartialEq, Eq, Default)]
21struct Fingerprint {
22 mtime: Option<SystemTime>,
23 size: u64,
24}
25
26struct Entry {
27 index: Arc<ProjectIndex>,
28 fingerprint: Fingerprint,
29}
30
31static CACHE: OnceLock<Mutex<HashMap<String, Entry>>> = OnceLock::new();
32
33fn cache() -> &'static Mutex<HashMap<String, Entry>> {
34 CACHE.get_or_init(|| Mutex::new(HashMap::new()))
35}
36
37fn index_fingerprint(project_root: &str) -> Fingerprint {
39 let Some(dir) = ProjectIndex::index_dir(project_root) else {
40 return Fingerprint::default();
41 };
42 for name in ["index.json.zst", "index.json"] {
43 if let Ok(meta) = std::fs::metadata(dir.join(name)) {
44 return Fingerprint {
45 mtime: meta.modified().ok(),
46 size: meta.len(),
47 };
48 }
49 }
50 Fingerprint::default()
51}
52
53pub fn get_cached(project_root: &str) -> Option<Arc<ProjectIndex>> {
57 let fingerprint = index_fingerprint(project_root);
58
59 {
60 let map = cache()
61 .lock()
62 .unwrap_or_else(std::sync::PoisonError::into_inner);
63 if let Some(entry) = map.get(project_root) {
64 if entry.fingerprint == fingerprint {
65 return Some(Arc::clone(&entry.index));
66 }
67 }
68 }
69
70 let idx = ProjectIndex::load(project_root).filter(|i| !i.files.is_empty())?;
71 let arc = Arc::new(idx);
72
73 let mut map = cache()
74 .lock()
75 .unwrap_or_else(std::sync::PoisonError::into_inner);
76 map.insert(
77 project_root.to_string(),
78 Entry {
79 index: Arc::clone(&arc),
80 fingerprint,
81 },
82 );
83 Some(arc)
84}
85
86pub fn invalidate(project_root: Option<&str>) {
88 let mut map = cache()
89 .lock()
90 .unwrap_or_else(std::sync::PoisonError::into_inner);
91 match project_root {
92 Some(root) => {
93 map.remove(root);
94 }
95 None => map.clear(),
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn returns_none_without_index() {
105 let tmp = tempfile::tempdir().unwrap();
106 invalidate(None);
107 assert!(get_cached(tmp.path().to_str().unwrap()).is_none());
108 }
109}