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