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 async_io::Timer;
15use freya_core::{
16    integration::FxHashSet,
17    prelude::*,
18};
19
20/// Defines the duration for which an Asset will remain cached after it's user has stopped using it.
21/// The default is 1h (3600s).
22#[derive(Hash, PartialEq, Eq, Clone, Copy)]
23pub enum AssetAge {
24    /// Asset will be cached for the specified duration
25    Duration(Duration),
26    /// Asset will be cached until app is closed
27    Unspecified,
28}
29
30impl Default for AssetAge {
31    fn default() -> Self {
32        Self::Duration(Duration::from_secs(3600)) // 1h
33    }
34}
35
36impl AssetAge {
37    /// Asset will be cleaned as soon as it stops being used.
38    pub fn zero() -> Self {
39        Self::Duration(Duration::ZERO)
40    }
41}
42
43impl From<Duration> for AssetAge {
44    fn from(value: Duration) -> Self {
45        Self::Duration(value)
46    }
47}
48
49/// Configuration for a given Asset.
50#[derive(Hash, PartialEq, Eq, Clone)]
51pub struct AssetConfiguration {
52    /// Asset age.
53    pub age: AssetAge,
54    /// The ID of the asset.
55    pub id: u64,
56}
57
58impl AssetConfiguration {
59    pub fn new(id: impl Hash, age: AssetAge) -> Self {
60        let mut state = DefaultHasher::default();
61        id.hash(&mut state);
62        let id = state.finish();
63        Self { id, age }
64    }
65}
66
67enum AssetUsers {
68    Listeners(Rc<RefCell<FxHashSet<ReactiveContext>>>),
69    ClearTask(TaskHandle),
70}
71
72#[derive(Clone)]
73pub enum Asset {
74    /// Asset is cached.
75    Cached(Rc<dyn Any>),
76    /// Asset is currently being fetched.
77    Loading,
78    /// Asset has yet to be fetched.
79    Pending,
80    /// Failed to fetch asset.
81    Error(String),
82}
83
84impl Asset {
85    /// Try to get asset.
86    pub fn try_get(&self) -> Option<&Rc<dyn Any>> {
87        match self {
88            Self::Cached(asset) => Some(asset),
89            _ => None,
90        }
91    }
92}
93
94struct AssetState {
95    users: AssetUsers,
96    asset: Asset,
97}
98
99#[derive(Clone, Copy, PartialEq)]
100pub struct AssetCacher {
101    registry: State<HashMap<AssetConfiguration, AssetState>>,
102}
103
104impl AssetCacher {
105    pub fn create() -> Self {
106        Self {
107            registry: State::create(HashMap::new()),
108        }
109    }
110
111    pub fn try_get() -> Option<Self> {
112        try_consume_root_context()
113    }
114
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_forever({
185                    let asset_config = asset_config.clone();
186                    async move {
187                        Timer::after(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 size of the cache registry.
229    pub fn size(&self) -> usize {
230        self.registry.read().len()
231    }
232}
233
234/// Start listening to an asset given a [AssetConfiguration].
235pub fn use_asset(asset_config: &AssetConfiguration) -> Asset {
236    let mut asset_cacher = use_hook(AssetCacher::get);
237
238    use_drop({
239        let asset_config = asset_config.clone();
240        move || {
241            // Try to clean in the next async tick, when this scope will already be dropped
242            spawn_forever(async move {
243                asset_cacher.try_clean(&asset_config);
244            });
245        }
246    });
247
248    let mut prev = use_state::<Option<AssetConfiguration>>(|| None);
249    {
250        let mut prev = prev.write();
251        if prev.as_ref() != Some(asset_config) {
252            if let Some(prev) = &*prev
253                && prev != asset_config
254            {
255                // Try to clean the previous asset
256                asset_cacher.try_clean(prev);
257            }
258            prev.replace(asset_config.clone());
259        }
260        asset_cacher.listen(ReactiveContext::current(), asset_config.clone());
261    }
262
263    asset_cacher
264        .read_asset(asset_config)
265        .expect("Asset should be be cached by now.")
266}