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