Skip to main content

nms_cache/
freshness.rs

1//! Cache freshness checking.
2//!
3//! Compares the modification time of the save file against the cache file
4//! to determine if the cache is still valid.
5
6use std::fs;
7use std::path::Path;
8use std::time::SystemTime;
9
10/// Check if the cache file is fresh relative to the save file.
11///
12/// Returns `true` if the cache exists and is newer than the save file.
13/// Returns `false` if the cache is missing, older than the save, or if
14/// timestamps can't be read.
15pub fn is_cache_fresh(cache_path: &Path, save_path: &Path) -> bool {
16    let cache_mtime = match file_mtime(cache_path) {
17        Some(t) => t,
18        None => return false,
19    };
20    let save_mtime = match file_mtime(save_path) {
21        Some(t) => t,
22        None => return false,
23    };
24    cache_mtime > save_mtime
25}
26
27/// Get the modification time of a file.
28fn file_mtime(path: &Path) -> Option<SystemTime> {
29    fs::metadata(path).ok()?.modified().ok()
30}
31
32/// Load a model from cache if fresh, otherwise parse the save file.
33///
34/// This is the primary entry point for the startup path.
35/// Returns `(GalaxyModel, was_cached)`.
36pub fn load_or_rebuild(
37    cache_path: &Path,
38    save_path: &Path,
39    no_cache: bool,
40) -> Result<(nms_graph::GalaxyModel, bool), Box<dyn std::error::Error>> {
41    // Try cache first (unless --no-cache)
42    if !no_cache && is_cache_fresh(cache_path, save_path) {
43        match crate::read_cache(cache_path) {
44            Ok(data) => {
45                let model = crate::rebuild_model(&data);
46                return Ok((model, true));
47            }
48            Err(e) => {
49                eprintln!("Warning: cache read failed ({e}), rebuilding from save");
50            }
51        }
52    }
53
54    // Parse save file
55    let save = nms_save::parse_save_file(save_path)?;
56    let model = nms_graph::GalaxyModel::from_save(&save);
57
58    // Write cache for next time
59    if !no_cache {
60        let data = crate::extract_cache_data(&model, save.version);
61        if let Err(e) = crate::write_cache(&data, cache_path) {
62            eprintln!("Warning: could not write cache ({e})");
63        }
64    }
65
66    Ok((model, false))
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use std::thread;
73    use std::time::Duration;
74
75    #[test]
76    fn test_fresh_cache_returns_true() {
77        let dir = tempfile::tempdir().unwrap();
78        let save_path = dir.path().join("save.json");
79        let cache_path = dir.path().join("galaxy.rkyv");
80
81        fs::write(&save_path, "save data").unwrap();
82        thread::sleep(Duration::from_millis(50));
83        fs::write(&cache_path, "cache data").unwrap();
84
85        assert!(is_cache_fresh(&cache_path, &save_path));
86    }
87
88    #[test]
89    fn test_stale_cache_returns_false() {
90        let dir = tempfile::tempdir().unwrap();
91        let save_path = dir.path().join("save.json");
92        let cache_path = dir.path().join("galaxy.rkyv");
93
94        fs::write(&cache_path, "cache data").unwrap();
95        thread::sleep(Duration::from_millis(50));
96        fs::write(&save_path, "updated save").unwrap();
97
98        assert!(!is_cache_fresh(&cache_path, &save_path));
99    }
100
101    #[test]
102    fn test_missing_cache_returns_false() {
103        let dir = tempfile::tempdir().unwrap();
104        let save_path = dir.path().join("save.json");
105        let cache_path = dir.path().join("galaxy.rkyv");
106
107        fs::write(&save_path, "save data").unwrap();
108        assert!(!is_cache_fresh(&cache_path, &save_path));
109    }
110
111    #[test]
112    fn test_missing_save_returns_false() {
113        let dir = tempfile::tempdir().unwrap();
114        let save_path = dir.path().join("save.json");
115        let cache_path = dir.path().join("galaxy.rkyv");
116
117        fs::write(&cache_path, "cache data").unwrap();
118        assert!(!is_cache_fresh(&cache_path, &save_path));
119    }
120}