martin_core/resources/sprites/
cache.rs

1use actix_web::web::Bytes;
2use moka::future::Cache;
3
4/// Sprite cache for storing generated sprite sheets.
5#[derive(Clone)]
6pub struct SpriteCache {
7    cache: Cache<SpriteCacheKey, Bytes>,
8}
9
10impl std::fmt::Debug for SpriteCache {
11    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
12        f.debug_struct("SpriteCache")
13            .field("entry_count", &self.cache.entry_count())
14            .field("weighted_size", &self.cache.weighted_size())
15            .finish()
16    }
17}
18
19impl SpriteCache {
20    /// Creates a new sprite cache with the specified maximum size in bytes.
21    #[must_use]
22    pub fn new(max_size_bytes: u64) -> Self {
23        Self {
24            cache: Cache::builder()
25                .name("sprite_cache")
26                .weigher(|key: &SpriteCacheKey, value: &Bytes| -> u32 {
27                    size_of_val(key).try_into().unwrap_or(u32::MAX)
28                        + value.len().try_into().unwrap_or(u32::MAX)
29                })
30                .max_capacity(max_size_bytes)
31                .build(),
32        }
33    }
34
35    /// Retrieves a sprite sheet from cache if present.
36    async fn get(&self, key: &SpriteCacheKey) -> Option<Bytes> {
37        let result = self.cache.get(key).await;
38
39        if result.is_some() {
40            log::trace!(
41                "Sprite cache HIT for {key:?} (entries={}, size={})",
42                self.cache.entry_count(),
43                self.cache.weighted_size()
44            );
45        } else {
46            log::trace!("Sprite cache MISS for {key:?}");
47        }
48
49        result
50    }
51
52    /// Gets a json sprite sheet from cache or computes it using the provided function.
53    pub async fn get_or_insert<F, Fut, E>(
54        &self,
55        ids: String,
56        as_sdf: bool,
57        as_json: bool,
58        compute: F,
59    ) -> Result<Bytes, E>
60    where
61        F: FnOnce() -> Fut,
62        Fut: Future<Output = Result<Bytes, E>>,
63    {
64        let key = SpriteCacheKey::new(ids, as_sdf, as_json);
65        if let Some(data) = self.get(&key).await {
66            return Ok(data);
67        }
68
69        let data = compute().await?;
70        self.cache.insert(key, data.clone()).await;
71        Ok(data)
72    }
73
74    /// Invalidates all cached sprites that use the specified source ID.
75    pub fn invalidate_source(&self, source_id: &str) {
76        let source_id_owned = source_id.to_string();
77        self.cache
78            .invalidate_entries_if(move |key, _| key.ids.contains(&source_id_owned))
79            .expect("invalidate_entries_if predicate should not error");
80        log::info!("Invalidated sprite cache for source: {source_id}");
81    }
82
83    /// Invalidates all cached sprites.
84    pub fn invalidate_all(&self) {
85        self.cache.invalidate_all();
86        log::info!("Invalidated all sprite cache entries");
87    }
88
89    /// Returns the number of cached entries.
90    #[must_use]
91    pub fn entry_count(&self) -> u64 {
92        self.cache.entry_count()
93    }
94
95    /// Returns the total size of cached data in bytes.
96    #[must_use]
97    pub fn weighted_size(&self) -> u64 {
98        self.cache.weighted_size()
99    }
100}
101
102/// Optional wrapper for `SpriteCache`.
103pub type OptSpriteCache = Option<SpriteCache>;
104
105/// Constant representing no sprite cache configuration.
106pub const NO_SPRITE_CACHE: OptSpriteCache = None;
107
108/// Cache key for sprite data.
109#[derive(Debug, Hash, PartialEq, Eq, Clone)]
110struct SpriteCacheKey {
111    ids: String,
112    as_sdf: bool,
113    as_json: bool,
114}
115
116impl SpriteCacheKey {
117    fn new(ids: String, as_sdf: bool, as_json: bool) -> Self {
118        Self {
119            ids,
120            as_sdf,
121            as_json,
122        }
123    }
124}