use std::collections::HashMap;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
pub type TouchCache = HashMap<String, (usize, Option<u32>)>;
const MAX_CACHED_SHAS: usize = 50;
fn cache_path(repo_root: &Path) -> PathBuf {
crate::snapshot::hotspots_dir(repo_root).join("touch-cache.json.zst")
}
pub fn cache_key(sha: &str, file: &str, start: u32, end: u32) -> String {
format!("{}:{}:{}:{}", sha, file, start, end)
}
pub fn read_touch_cache(repo_root: &Path) -> Option<TouchCache> {
let path = cache_path(repo_root);
if !path.exists() {
return None;
}
match load_compressed_json(&path) {
Ok(cache) => Some(cache),
Err(e) => {
eprintln!("warning: failed to load touch cache (proceeding cold): {e}");
None
}
}
}
fn load_compressed_json(path: &Path) -> Result<TouchCache> {
let compressed = std::fs::read(path)
.with_context(|| format!("failed to read touch cache: {}", path.display()))?;
let bytes = zstd::decode_all(compressed.as_slice())
.with_context(|| format!("failed to decompress touch cache: {}", path.display()))?;
let json = std::str::from_utf8(&bytes).context("touch cache is not valid UTF-8")?;
serde_json::from_str(json).context("failed to parse touch cache JSON")
}
pub fn write_touch_cache(repo_root: &Path, cache: &TouchCache) -> Result<()> {
let path = cache_path(repo_root);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("failed to create directory: {}", parent.display()))?;
}
let json = serde_json::to_string(cache).context("failed to serialize touch cache")?;
let compressed =
zstd::encode_all(json.as_bytes(), 3).context("failed to compress touch cache")?;
std::fs::write(&path, &compressed)
.with_context(|| format!("failed to write touch cache: {}", path.display()))
}
pub fn evict_old_entries(cache: &mut TouchCache, known_shas: &[String]) {
let allowed: std::collections::HashSet<&str> = known_shas
.iter()
.take(MAX_CACHED_SHAS)
.map(String::as_str)
.collect();
cache.retain(|key, _| {
key.split(':')
.next()
.is_some_and(|sha| allowed.contains(sha))
});
}