use fob_graph::ModuleGraph;
use rustc_hash::FxHashMap as HashMap;
use std::fs;
use std::path::Path;
use super::CacheError;
#[derive(Debug, Clone)]
pub struct IncrementalCache {
pub graph: Option<ModuleGraph>,
pub module_hashes: HashMap<fob_graph::ModuleId, [u8; 32]>,
pub rolldown_version: String,
pub format_version: u32,
}
impl IncrementalCache {
pub const FORMAT_VERSION: u32 = 1;
pub fn new() -> Self {
Self {
graph: None,
module_hashes: HashMap::default(),
rolldown_version: get_rolldown_version(),
format_version: Self::FORMAT_VERSION,
}
}
pub fn load(dir: &Path) -> Result<Option<Self>, CacheError> {
let cache_path = dir.join("incremental.bin");
if !cache_path.exists() {
return Ok(None);
}
let bytes = fs::read(&cache_path)?;
#[derive(serde::Deserialize)]
struct SerializedCache {
format_version: u32,
rolldown_version: String,
module_hashes: HashMap<fob_graph::ModuleId, [u8; 32]>,
graph_bytes: Vec<u8>,
}
let cache: SerializedCache = bincode::deserialize(&bytes).map_err(|e| {
CacheError::DeserializationError(format!(
"Failed to deserialize incremental cache: {}",
e
))
})?;
if cache.format_version != Self::FORMAT_VERSION {
return Err(CacheError::Corrupted(format!(
"Incompatible incremental cache version: expected {}, got {}",
Self::FORMAT_VERSION,
cache.format_version
)));
}
let graph = ModuleGraph::from_bytes(&cache.graph_bytes).map_err(|e| {
CacheError::DeserializationError(format!("Failed to deserialize module graph: {}", e))
})?;
Ok(Some(Self {
graph: Some(graph),
module_hashes: cache.module_hashes,
rolldown_version: cache.rolldown_version,
format_version: cache.format_version,
}))
}
pub fn save(&self, dir: &Path) -> Result<(), CacheError> {
fs::create_dir_all(dir)?;
let cache_path = dir.join("incremental.bin");
let graph_bytes = if let Some(ref graph) = self.graph {
graph.to_bytes().map_err(|e| {
CacheError::SerializationError(format!("Failed to serialize graph: {}", e))
})?
} else {
return Err(CacheError::Corrupted(
"Cannot save incremental cache without a graph".to_string(),
));
};
#[derive(serde::Serialize)]
struct SerializedCache<'a> {
format_version: u32,
rolldown_version: &'a str,
module_hashes: &'a HashMap<fob_graph::ModuleId, [u8; 32]>,
graph_bytes: &'a [u8],
}
let cache = SerializedCache {
format_version: self.format_version,
rolldown_version: &self.rolldown_version,
module_hashes: &self.module_hashes,
graph_bytes: &graph_bytes,
};
let bytes = bincode::serialize(&cache).map_err(|e| {
CacheError::SerializationError(format!("Failed to encode cache: {}", e))
})?;
let temp_path = cache_path.with_extension("tmp");
fs::write(&temp_path, bytes)?;
fs::rename(&temp_path, &cache_path)?;
Ok(())
}
pub fn is_valid_for(&self, entries: &[std::path::PathBuf]) -> bool {
if self.rolldown_version != get_rolldown_version() {
return false;
}
let Some(ref graph) = self.graph else {
return false;
};
let Ok(graph_entries) = graph.entry_points() else {
return false;
};
let entry_ids: Vec<_> = entries
.iter()
.filter_map(|path| fob_graph::ModuleId::new(path).ok())
.collect();
if entry_ids.len() != graph_entries.len() {
return false;
}
entry_ids.iter().all(|id| graph_entries.contains(id))
}
}
impl Default for IncrementalCache {
fn default() -> Self {
Self::new()
}
}
fn get_rolldown_version() -> String {
"0.5.1".to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_cache() {
let cache = IncrementalCache::new();
assert!(cache.graph.is_none());
assert!(cache.module_hashes.is_empty());
assert_eq!(cache.format_version, IncrementalCache::FORMAT_VERSION);
}
#[test]
fn test_is_valid_empty_entries() {
let cache = IncrementalCache::new();
assert!(!cache.is_valid_for(&[]));
}
#[test]
fn test_rolldown_version() {
let version = get_rolldown_version();
assert!(!version.is_empty());
}
}