use std::collections::HashMap;
use std::io::Write;
use std::path::{Path, PathBuf};
use crate::graph::CodeGraph;
pub const CACHE_VERSION: u32 = 6;
pub const CACHE_DIR: &str = ".code-graph";
pub const CACHE_FILE: &str = "graph.bin";
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct FileMeta {
pub mtime_secs: u64,
pub size: u64,
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct CacheEnvelope {
pub version: u32,
pub project_root: PathBuf,
pub file_mtimes: HashMap<PathBuf, FileMeta>,
pub graph: CodeGraph,
}
pub fn cache_path(project_root: &Path) -> PathBuf {
project_root.join(CACHE_DIR).join(CACHE_FILE)
}
pub fn collect_file_mtimes(graph: &CodeGraph) -> HashMap<PathBuf, FileMeta> {
let mut mtimes = HashMap::new();
for path in graph.file_index.keys() {
if let Ok(metadata) = std::fs::metadata(path) {
let mtime_secs = metadata
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs())
.unwrap_or(0);
mtimes.insert(
path.clone(),
FileMeta {
mtime_secs,
size: metadata.len(),
},
);
}
}
mtimes
}
pub fn save_cache(project_root: &Path, graph: &CodeGraph) -> anyhow::Result<()> {
let cache_dir = project_root.join(CACHE_DIR);
std::fs::create_dir_all(&cache_dir)?;
let file_mtimes = collect_file_mtimes(graph);
let envelope = CacheEnvelope {
version: CACHE_VERSION,
project_root: project_root.to_path_buf(),
file_mtimes,
graph: graph.clone(),
};
let target = cache_path(project_root);
let mut tmp = tempfile::NamedTempFile::new_in(&cache_dir)?;
bincode::serde::encode_into_std_write(&envelope, &mut tmp, bincode::config::standard())?;
tmp.as_file().flush()?;
tmp.persist(&target)?;
Ok(())
}
pub fn load_cache(project_root: &Path) -> Option<CacheEnvelope> {
let target = cache_path(project_root);
let bytes = std::fs::read(&target).ok()?;
let result =
bincode::serde::decode_from_slice::<CacheEnvelope, _>(&bytes, bincode::config::standard());
match result {
Ok((envelope, _)) if envelope.version == CACHE_VERSION => Some(envelope),
_ => None, }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::node::{SymbolInfo, SymbolKind};
#[test]
fn test_roundtrip_cache() {
let mut graph = CodeGraph::new();
let tmp_dir = tempfile::tempdir().unwrap();
let fake_file = tmp_dir.path().join("test.ts");
std::fs::write(&fake_file, "// test").unwrap();
let f = graph.add_file(fake_file.clone(), "typescript");
graph.add_symbol(
f,
SymbolInfo {
name: "hello".into(),
kind: SymbolKind::Function,
line: 1,
..Default::default()
},
);
save_cache(tmp_dir.path(), &graph).unwrap();
let loaded = load_cache(tmp_dir.path()).expect("cache should load");
assert_eq!(loaded.version, CACHE_VERSION);
assert_eq!(loaded.graph.file_count(), 1);
assert_eq!(loaded.graph.symbol_count(), 1);
assert!(loaded.file_mtimes.contains_key(&fake_file));
}
#[test]
fn test_load_missing_cache_returns_none() {
let tmp_dir = tempfile::tempdir().unwrap();
assert!(load_cache(tmp_dir.path()).is_none());
}
}