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}