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)]
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    pub fn update_asset(&mut self, asset_config: AssetConfiguration, new_asset: Asset) {
138        let mut registry = self.registry.write();
139
140        let asset = registry.entry(asset_config).or_insert_with(|| AssetState {
141            asset: Asset::Pending,
142            users: AssetUsers::Listeners(Rc::default()),
143        });
144
145        asset.asset = new_asset;
146
147        // Reruns those listening components
148        if let AssetUsers::Listeners(listeners) = &asset.users {
149            for sub in listeners.borrow().iter() {
150                sub.notify();
151            }
152        }
153    }
154
155    /// Try to clean an asset with no more listeners given a [AssetConfiguration].
156    pub fn try_clean(&mut self, asset_config: &AssetConfiguration) {
157        let mut registry = self.registry;
158
159        let spawn_clear_task = {
160            let mut registry = registry.write();
161
162            let entry = registry.get_mut(asset_config);
163            if let Some(asset_state) = entry {
164                match &mut asset_state.users {
165                    AssetUsers::Listeners(listeners) => {
166                        // Only spawn a clear-task if there are no more listeners using this asset
167                        listeners.borrow().is_empty()
168                    }
169                    AssetUsers::ClearTask(task) => {
170                        // This case should never happen but... we leave it here anyway.
171                        task.cancel();
172                        true
173                    }
174                }
175            } else {
176                false
177            }
178        };
179
180        if spawn_clear_task {
181            // Only clear the asset if a duration was specified
182            if let AssetAge::Duration(duration) = asset_config.age {
183                let clear_task = spawn_forever({
184                    let asset_config = asset_config.clone();
185                    async move {
186                        Timer::after(duration).await;
187                        registry.write().remove(&asset_config);
188                    }
189                });
190
191                // Registry the clear-task
192                let mut registry = registry.write();
193                if let Some(entry) = registry.get_mut(asset_config) {
194                    entry.users = AssetUsers::ClearTask(clear_task);
195                } else {
196                    #[cfg(debug_assertions)]
197                    tracing::info!(
198                        "Failed to spawn clear task to remove cache of {}",
199                        asset_config.id
200                    )
201                }
202            }
203        }
204    }
205
206    pub(crate) fn listen(&self, mut rc: ReactiveContext, asset_config: AssetConfiguration) {
207        let mut registry = self.registry.write_unchecked();
208
209        let asset = registry.entry(asset_config).or_insert_with(|| AssetState {
210            asset: Asset::Pending,
211            users: AssetUsers::Listeners(Rc::default()),
212        });
213
214        match &mut asset.users {
215            AssetUsers::Listeners(users) => {
216                rc.subscribe(users);
217            }
218            AssetUsers::ClearTask(clear_task) => {
219                clear_task.cancel();
220                let listeners = Rc::default();
221                rc.subscribe(&listeners);
222                asset.users = AssetUsers::Listeners(listeners);
223            }
224        }
225    }
226
227    /// Read the size of the cache registry.
228    pub fn size(&self) -> usize {
229        self.registry.read().len()
230    }
231}
232
233/// Start listening to an asset given a [AssetConfiguration].
234pub fn use_asset(asset_config: &AssetConfiguration) -> Asset {
235    let mut asset_cacher = use_hook(AssetCacher::get);
236
237    use_drop({
238        let asset_config = asset_config.clone();
239        move || {
240            // Try to clean in the next async tick, when this scope will already be dropped
241            spawn_forever(async move {
242                asset_cacher.try_clean(&asset_config);
243            });
244        }
245    });
246
247    let mut prev = use_state::<Option<AssetConfiguration>>(|| None);
248    {
249        let mut prev = prev.write();
250        if prev.as_ref() != Some(asset_config) {
251            if let Some(prev) = &*prev
252                && prev != asset_config
253            {
254                // Try to clean the previous asset
255                asset_cacher.try_clean(asset_config);
256            }
257            prev.replace(asset_config.clone());
258        }
259        asset_cacher.listen(ReactiveContext::current(), asset_config.clone());
260    }
261
262    asset_cacher
263        .read_asset(asset_config)
264        .expect("Asset should be be cached by now.")
265}