Skip to main content

fob_bundler/cache/
mod.rs

1//! Persistent build cache for fob-bundler.
2//!
3//! Provides content-addressed caching of build results to skip Rolldown
4//! execution on cache hits. The cache is embedder-controlled - the user
5//! specifies where cache files are stored.
6//!
7//! # Architecture
8//!
9//! - **Content-addressed**: Cache keys are BLAKE3 hashes of build inputs
10//! - **Automatic invalidation**: Key changes when any input changes
11//! - **redb backend**: Single database file with ACID transactions
12//!
13//! # Usage
14//!
15//! ```rust,no_run
16//! use fob_bundler::BuildOptions;
17//!
18//! # async fn example() -> fob_bundler::Result<()> {
19//! let result = BuildOptions::new("src/index.ts")
20//!     .cache_dir(".cache/fob")
21//!     .build()
22//!     .await?;
23//! # Ok(())
24//! # }
25//! ```
26
27pub mod changes;
28pub mod incremental;
29mod key;
30pub mod serialize;
31mod storage;
32
33pub use changes::{ChangeDetector, ChangeSet};
34pub use incremental::IncrementalCache;
35pub use key::CacheKey;
36pub use serialize::{CacheMetadata, CachedBuild};
37pub use storage::{CacheError, CacheStore};
38
39use std::path::{Path, PathBuf};
40
41/// Configuration for persistent build caching.
42///
43/// The embedder controls where cache files are stored. Cache keys are
44/// content-addressed (BLAKE3 hash of inputs), so invalidation is automatic.
45#[derive(Debug, Clone)]
46pub struct CacheConfig {
47    /// Directory where cache database is stored.
48    ///
49    /// The cache uses a single redb database file at `<dir>/cache.redb`.
50    pub dir: PathBuf,
51
52    /// Force rebuild even if cache exists.
53    ///
54    /// When true, the cache is bypassed for reads but still written to
55    /// after the build completes. Useful for CI or refreshing the cache.
56    pub force_rebuild: bool,
57
58    /// Environment variables that affect the cache key.
59    ///
60    /// If your build output depends on environment variables (e.g., NODE_ENV),
61    /// add them here so the cache key changes when they change.
62    pub env_vars: Vec<String>,
63}
64
65impl CacheConfig {
66    /// Create a new cache config with the given directory.
67    pub fn new(dir: impl Into<PathBuf>) -> Self {
68        Self {
69            dir: dir.into(),
70            force_rebuild: false,
71            env_vars: Vec::new(),
72        }
73    }
74
75    /// Set the force rebuild flag.
76    pub fn with_force_rebuild(mut self, force: bool) -> Self {
77        self.force_rebuild = force;
78        self
79    }
80
81    /// Add environment variables to include in the cache key.
82    pub fn with_env_vars(mut self, vars: impl IntoIterator<Item = impl Into<String>>) -> Self {
83        self.env_vars = vars.into_iter().map(Into::into).collect();
84        self
85    }
86
87    /// Check if force rebuild is requested via environment variable.
88    pub fn should_force_rebuild(&self) -> bool {
89        self.force_rebuild || std::env::var_os("FOB_FORCE_REBUILD").is_some()
90    }
91}
92
93impl Default for CacheConfig {
94    fn default() -> Self {
95        Self::new(".cache/fob")
96    }
97}
98
99/// Result type for cache operations.
100pub type CacheResult<T> = std::result::Result<T, CacheError>;
101
102/// Attempt to load a cached build from the cache store.
103///
104/// Returns `Ok(cached)` if the cache key exists and is valid,
105/// or `Err(CacheError::CacheMiss)` if not found.
106pub fn try_load(store: &CacheStore, key: &CacheKey) -> CacheResult<CachedBuild> {
107    store.get(key)
108}
109
110/// Save a build result to the cache store.
111///
112/// This is non-fatal - errors are logged but don't fail the build.
113pub fn try_save(store: &CacheStore, key: &CacheKey, build: &CachedBuild) -> CacheResult<()> {
114    store.put(key, build)
115}
116
117/// Compute a cache key for the given build plan.
118///
119/// The key is a BLAKE3 hash of:
120/// 1. Rolldown version
121/// 2. Sorted entry paths + content hashes
122/// 3. Serialized build options (excluding cache config)
123/// 4. Virtual files (sorted path + content hash)
124/// 5. Specified environment variables
125pub(crate) fn compute_cache_key(
126    plan: &crate::builders::common::BundlePlan,
127    config: &CacheConfig,
128) -> CacheResult<CacheKey> {
129    key::compute_cache_key(plan, config)
130}
131
132/// Open or create a cache store at the given directory.
133///
134/// Creates the directory if it doesn't exist.
135pub fn open_store(cache_dir: &Path) -> CacheResult<CacheStore> {
136    CacheStore::open(cache_dir)
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn test_cache_config_defaults() {
145        let config = CacheConfig::default();
146        assert_eq!(config.dir, PathBuf::from(".cache/fob"));
147        assert!(!config.force_rebuild);
148        assert!(config.env_vars.is_empty());
149    }
150
151    #[test]
152    fn test_cache_config_builder() {
153        let config = CacheConfig::new("/tmp/cache")
154            .with_force_rebuild(true)
155            .with_env_vars(["NODE_ENV", "DEBUG"]);
156
157        assert_eq!(config.dir, PathBuf::from("/tmp/cache"));
158        assert!(config.force_rebuild);
159        assert_eq!(config.env_vars, vec!["NODE_ENV", "DEBUG"]);
160    }
161}