Skip to main content

freya_components/
cache.rs

1use std::{
2    any::Any,
3    cell::RefCell,
4    collections::HashMap,
5    hash::{
6        DefaultHasher,
7        Hash,
8        Hasher,
9    },
10    rc::Rc,
11    time::Duration,
12};
13
14use freya_core::{
15    integration::FxHashSet,
16    prelude::*,
17};
18
19/// Defines the duration for which an Asset will remain cached after it's user has stopped using it.
20/// The default is 1h (3600s).
21#[derive(Hash, PartialEq, Eq, Clone, Copy)]
22pub enum AssetAge {
23    /// Asset will be cached for the specified duration
24    Duration(Duration),
25    /// Asset will be cached until app is closed
26    Unspecified,
27}
28
29impl Default for AssetAge {
30    fn default() -> Self {
31        Self::Duration(Duration::from_secs(3600)) // 1h
32    }
33}
34
35impl AssetAge {
36    /// Asset will be cleaned as soon as it stops being used.
37    pub fn zero() -> Self {
38        Self::Duration(Duration::ZERO)
39    }
40}
41
42impl From<Duration> for AssetAge {
43    fn from(value: Duration) -> Self {
44        Self::Duration(value)
45    }
46}
47
48/// Configuration for a given Asset.
49#[derive(Hash, PartialEq, Eq, Clone)]
50pub struct AssetConfiguration {
51    /// Asset age.
52    pub age: AssetAge,
53    /// The ID of the asset.
54    pub id: u64,
55}
56
57impl AssetConfiguration {
58    pub fn new(id: impl Hash, age: AssetAge) -> Self {
59        let mut state = DefaultHasher::default();
60        id.hash(&mut state);
61        let id = state.finish();
62        Self { id, age }
63    }
64}
65
66enum AssetUsers {
67    Listeners(Rc<RefCell<FxHashSet<ReactiveContext>>>),
68    ClearTask(TaskHandle),
69}
70
71#[derive(Clone)]
72pub enum Asset {
73    /// Asset is cached.
74    Cached(Rc<dyn Any>),
75    /// Asset is currently being fetched.
76    Loading,
77    /// Asset has yet to be fetched.
78    Pending,
79    /// Failed to fetch asset.
80    Error(String),
81}
82
83impl Asset {
84    /// Try to get asset.
85    pub fn try_get(&self) -> Option<&Rc<dyn Any>> {
86        match self {
87            Self::Cached(asset) => Some(asset),
88            _ => None,
89        }
90    }
91}
92
93struct AssetState {
94    users: AssetUsers,
95    asset: Asset,
96}
97
98#[derive(Clone, Copy, PartialEq)]
99pub struct AssetCacher {
100    registry: State<HashMap<AssetConfiguration, AssetState>>,
101}
102
103impl AssetCacher {
104    pub fn create() -> Self {
105        Self {
106            registry: State::create(HashMap::new()),
107        }
108    }
109
110    pub fn try_get() -> Option<Self> {
111        try_consume_root_context()
112    }
113
114    #[track_caller]
115    pub fn get() -> Self {
116        consume_root_context()
117    }
118
119    /// Attempt to resolve a [Asset] given a [AssetConfiguration].
120    pub fn read_asset(&self, asset_config: &AssetConfiguration) -> Option<Asset> {
121        self.registry
122            .peek()
123            .get(asset_config)
124            .map(|a| a.asset.clone())
125    }
126
127    /// Subscribes to a [Asset] given a [AssetConfiguration].
128    pub fn subscribe_asset(&self, asset_config: &AssetConfiguration) -> Option<Asset> {
129        self.listen(ReactiveContext::current(), asset_config.clone());
130        self.registry
131            .peek()
132            .get(asset_config)
133            .map(|a| a.asset.clone())
134    }
135
136    /// Update an [Asset] given a [AssetConfiguration].
137    ///
138    /// Does nothing if the entry is not present.
139    pub fn update_asset(&mut self, asset_config: AssetConfiguration, new_asset: Asset) {
140        let mut registry = self.registry.write();
141
142        let Some(asset) = registry.get_mut(&asset_config) else {
143            return;
144        };
145
146        asset.asset = new_asset;
147
148        // Reruns those listening components
149        if let AssetUsers::Listeners(listeners) = &asset.users {
150            for sub in listeners.borrow().iter() {
151                sub.notify();
152            }
153        }
154    }
155
156    /// Try to clean an asset with no more listeners given a [AssetConfiguration].
157    pub fn try_clean(&mut self, asset_config: &AssetConfiguration) {
158        let mut registry = self.registry;
159
160        let spawn_clear_task = {
161            let mut registry = registry.write();
162
163            let entry = registry.get_mut(asset_config);
164            if let Some(asset_state) = entry {
165                match &mut asset_state.users {
166                    AssetUsers::Listeners(listeners) => {
167                        // Only spawn a clear-task if there are no more listeners using this asset
168                        listeners.borrow().is_empty()
169                    }
170                    AssetUsers::ClearTask(task) => {
171                        // This case should never happen but... we leave it here anyway.
172                        task.cancel();
173                        true
174                    }
175                }
176            } else {
177                false
178            }
179        };
180
181        if spawn_clear_task {
182            // Only clear the asset if a duration was specified
183            if let AssetAge::Duration(duration) = asset_config.age {
184                let clear_task = spawn_in_window({
185                    let asset_config = asset_config.clone();
186                    async move {
187                        timer(duration).await;
188                        registry.write().remove(&asset_config);
189                    }
190                });
191
192                // Registry the clear-task
193                let mut registry = registry.write();
194                if let Some(entry) = registry.get_mut(asset_config) {
195                    entry.users = AssetUsers::ClearTask(clear_task);
196                } else {
197                    #[cfg(debug_assertions)]
198                    tracing::info!(
199                        "Failed to spawn clear task to remove cache of {}",
200                        asset_config.id
201                    )
202                }
203            }
204        }
205    }
206
207    pub(crate) fn listen(&self, mut rc: ReactiveContext, asset_config: AssetConfiguration) {
208        let mut registry = self.registry.write_unchecked();
209
210        let asset = registry.entry(asset_config).or_insert_with(|| AssetState {
211            asset: Asset::Pending,
212            users: AssetUsers::Listeners(Rc::default()),
213        });
214
215        match &mut asset.users {
216            AssetUsers::Listeners(users) => {
217                rc.subscribe(users);
218            }
219            AssetUsers::ClearTask(clear_task) => {
220                clear_task.cancel();
221                let listeners = Rc::default();
222                rc.subscribe(&listeners);
223                asset.users = AssetUsers::Listeners(listeners);
224            }
225        }
226    }
227
228    /// Read the number of successfully cached assets.
229    pub fn cached_size(&self) -> usize {
230        self.registry
231            .read()
232            .values()
233            .filter(|asset_state| matches!(&asset_state.asset, Asset::Cached(_)))
234            .count()
235    }
236
237    /// Read the size of the cache registry.
238    pub fn size(&self) -> usize {
239        self.registry.read().len()
240    }
241}
242
243/// Start listening to an asset given a [AssetConfiguration].
244pub fn use_asset(asset_config: &AssetConfiguration) {
245    let mut asset_cacher = use_hook(AssetCacher::get);
246
247    let mut current_config = use_state::<Option<AssetConfiguration>>(|| None);
248
249    use_drop(move || {
250        let Some(asset_config) = current_config.peek().clone() else {
251            return;
252        };
253        // Try to clean in the next async tick, when this scope will already be dropped
254        spawn_in_window(async move {
255            asset_cacher.try_clean(&asset_config);
256        });
257    });
258
259    {
260        let mut current_config = current_config.write();
261        if current_config.as_ref() != Some(asset_config) {
262            if let Some(prev) = &*current_config
263                && prev != asset_config
264            {
265                // Try to clean the previous asset
266                asset_cacher.try_clean(prev);
267            }
268            current_config.replace(asset_config.clone());
269        }
270        asset_cacher.listen(ReactiveContext::current(), asset_config.clone());
271    }
272}