Skip to main content

aster/map/
incremental_cache.rs

1//! 增量缓存管理器
2//!
3//! 支持增量更新,只重新分析变更的文件
4
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7
8use super::types::{CacheData, CacheEntry, ModuleNode};
9
10/// 增量缓存
11pub struct IncrementalCache {
12    cache_file: PathBuf,
13    cache: Option<CacheData>,
14    dirty: bool,
15}
16
17impl IncrementalCache {
18    pub fn new(project_root: impl AsRef<Path>) -> Self {
19        let cache_file = project_root.as_ref().join(".claude").join("map-cache.json");
20        Self {
21            cache_file,
22            cache: None,
23            dirty: false,
24        }
25    }
26
27    /// 加载缓存
28    pub fn load(&mut self) -> bool {
29        if !self.cache_file.exists() {
30            self.cache = None;
31            return false;
32        }
33
34        match std::fs::read_to_string(&self.cache_file) {
35            Ok(content) => match serde_json::from_str(&content) {
36                Ok(data) => {
37                    self.cache = Some(data);
38                    self.dirty = false;
39                    true
40                }
41                Err(_) => {
42                    self.cache = None;
43                    false
44                }
45            },
46            Err(_) => {
47                self.cache = None;
48                false
49            }
50        }
51    }
52
53    /// 保存缓存
54    pub fn save(&mut self) -> bool {
55        if self.cache.is_none() || !self.dirty {
56            return true;
57        }
58
59        if let Some(ref mut cache) = self.cache {
60            cache.generated_at = chrono::Utc::now().to_rfc3339();
61        }
62
63        if let Some(parent) = self.cache_file.parent() {
64            let _ = std::fs::create_dir_all(parent);
65        }
66
67        if let Ok(content) = serde_json::to_string_pretty(&self.cache) {
68            if std::fs::write(&self.cache_file, content).is_ok() {
69                self.dirty = false;
70                return true;
71            }
72        }
73        false
74    }
75
76    /// 检查文件是否需要重新分析
77    pub fn needs_reanalysis(&self, file_path: &Path) -> bool {
78        let cache = match &self.cache {
79            Some(c) => c,
80            None => return true,
81        };
82
83        let relative = self.get_relative_path(file_path);
84        let entry = match cache.entries.get(&relative) {
85            Some(e) => e,
86            None => return true,
87        };
88
89        match std::fs::metadata(file_path) {
90            Ok(meta) => {
91                let mtime = meta
92                    .modified()
93                    .map(|t| {
94                        t.duration_since(std::time::UNIX_EPOCH)
95                            .unwrap_or_default()
96                            .as_millis() as u64
97                    })
98                    .unwrap_or(0);
99                if entry.mtime != mtime {
100                    if let Ok(content) = std::fs::read_to_string(file_path) {
101                        let hash = self.calculate_hash(&content);
102                        return hash != entry.hash;
103                    }
104                }
105                false
106            }
107            Err(_) => true,
108        }
109    }
110
111    /// 批量检查文件
112    pub fn check_files(&self, file_paths: &[PathBuf]) -> FileCheckResult {
113        let mut changed = Vec::new();
114        let mut unchanged = Vec::new();
115        let mut removed = Vec::new();
116
117        let current_files: std::collections::HashSet<_> = file_paths
118            .iter()
119            .map(|f| self.get_relative_path(f))
120            .collect();
121
122        for path in file_paths {
123            if self.needs_reanalysis(path) {
124                changed.push(path.clone());
125            } else {
126                unchanged.push(path.clone());
127            }
128        }
129
130        if let Some(ref cache) = self.cache {
131            for cached_path in cache.entries.keys() {
132                if !current_files.contains(cached_path) {
133                    removed.push(cached_path.clone());
134                }
135            }
136        }
137
138        FileCheckResult {
139            changed,
140            unchanged,
141            removed,
142        }
143    }
144
145    /// 获取缓存的模块
146    pub fn get_cached_module(&self, file_path: &Path) -> Option<ModuleNode> {
147        let cache = self.cache.as_ref()?;
148        let relative = self.get_relative_path(file_path);
149        cache.entries.get(&relative).map(|e| e.module.clone())
150    }
151
152    /// 更新缓存条目
153    pub fn update_entry(&mut self, file_path: &Path, module: ModuleNode) {
154        if self.cache.is_none() {
155            self.cache = Some(CacheData {
156                version: "1.0.0".to_string(),
157                root_path: self
158                    .cache_file
159                    .parent()
160                    .and_then(|p| p.parent())
161                    .map(|p| p.to_string_lossy().to_string())
162                    .unwrap_or_default(),
163                generated_at: chrono::Utc::now().to_rfc3339(),
164                entries: HashMap::new(),
165            });
166        }
167
168        if let Ok(meta) = std::fs::metadata(file_path) {
169            if let Ok(content) = std::fs::read_to_string(file_path) {
170                let hash = self.calculate_hash(&content);
171                let mtime = meta
172                    .modified()
173                    .map(|t| {
174                        t.duration_since(std::time::UNIX_EPOCH)
175                            .unwrap_or_default()
176                            .as_millis() as u64
177                    })
178                    .unwrap_or(0);
179
180                let relative = self.get_relative_path(file_path);
181                if let Some(cache) = self.cache.as_mut() {
182                    cache.entries.insert(
183                        relative,
184                        CacheEntry {
185                            hash,
186                            mtime,
187                            module,
188                        },
189                    );
190                    self.dirty = true;
191                }
192            }
193        }
194    }
195
196    /// 删除缓存条目
197    pub fn remove_entry(&mut self, file_path: &Path) {
198        let relative = self.get_relative_path(file_path);
199        if let Some(ref mut cache) = self.cache {
200            if cache.entries.remove(&relative).is_some() {
201                self.dirty = true;
202            }
203        }
204    }
205
206    /// 清除所有缓存
207    pub fn clear(&mut self) {
208        self.cache = None;
209        self.dirty = false;
210        let _ = std::fs::remove_file(&self.cache_file);
211    }
212
213    /// 获取缓存统计信息
214    pub fn get_stats(&self) -> CacheStats {
215        let cache_file_size = std::fs::metadata(&self.cache_file)
216            .map(|m| m.len() as usize)
217            .unwrap_or(0);
218
219        CacheStats {
220            entry_count: self.cache.as_ref().map(|c| c.entries.len()).unwrap_or(0),
221            cache_file_size,
222            last_generated: self.cache.as_ref().map(|c| c.generated_at.clone()),
223        }
224    }
225
226    fn get_relative_path(&self, file_path: &Path) -> String {
227        if let Some(ref cache) = self.cache {
228            if let Ok(rel) = file_path.strip_prefix(&cache.root_path) {
229                return rel.to_string_lossy().replace('\\', "/");
230            }
231        }
232        if let Some(parent) = self.cache_file.parent().and_then(|p| p.parent()) {
233            if let Ok(rel) = file_path.strip_prefix(parent) {
234                return rel.to_string_lossy().replace('\\', "/");
235            }
236        }
237        file_path.to_string_lossy().replace('\\', "/")
238    }
239
240    fn calculate_hash(&self, content: &str) -> String {
241        use std::hash::{Hash, Hasher};
242        let mut hasher = std::collections::hash_map::DefaultHasher::new();
243        content.hash(&mut hasher);
244        format!("{:x}", hasher.finish())
245    }
246}
247
248/// 文件检查结果
249#[derive(Debug, Clone, Default)]
250pub struct FileCheckResult {
251    pub changed: Vec<PathBuf>,
252    pub unchanged: Vec<PathBuf>,
253    pub removed: Vec<String>,
254}
255
256/// 缓存统计
257#[derive(Debug, Clone, Default)]
258pub struct CacheStats {
259    pub entry_count: usize,
260    pub cache_file_size: usize,
261    pub last_generated: Option<String>,
262}
263
264/// 便捷函数:创建缓存管理器
265pub fn create_cache(project_root: impl AsRef<Path>) -> IncrementalCache {
266    IncrementalCache::new(project_root)
267}