freya_hooks/
use_asset_cacher.rs

1use std::{
2    collections::{
3        HashMap,
4        HashSet,
5    },
6    time::Duration,
7};
8
9use bytes::Bytes;
10use dioxus_core::{
11    prelude::{
12        current_scope_id,
13        spawn_forever,
14        ScopeId,
15        Task,
16    },
17    schedule_update_any,
18};
19use dioxus_hooks::{
20    use_context,
21    use_context_provider,
22};
23use dioxus_signals::{
24    Readable,
25    Signal,
26    Writable,
27};
28use tokio::time::sleep;
29use tracing::info;
30
31/// Defines the duration for which an Asset will remain cached after it's user has stopped using it.
32/// The default is 1h (3600s).
33#[derive(Hash, PartialEq, Eq, Clone)]
34pub enum AssetAge {
35    /// Asset will be cached for the specified duration
36    Duration(Duration),
37    /// Asset will be cached until app is closed
38    Unspecified,
39}
40
41impl Default for AssetAge {
42    fn default() -> Self {
43        Self::Duration(Duration::from_secs(3600)) // 1h
44    }
45}
46
47impl From<Duration> for AssetAge {
48    fn from(value: Duration) -> Self {
49        Self::Duration(value)
50    }
51}
52
53/// Configuration for a given Asset.
54#[derive(Hash, PartialEq, Eq, Clone)]
55pub struct AssetConfiguration {
56    /// Asset age.
57    pub age: AssetAge,
58    /// The ID of the asset. For example: For images their source URL or path can be used as ID.
59    pub id: String,
60}
61
62enum AssetUsers {
63    Scopes(HashSet<ScopeId>),
64    ClearTask(Task),
65}
66
67struct AssetState {
68    users: AssetUsers,
69    asset_bytes: Bytes,
70}
71
72#[derive(Clone, Copy, Default)]
73pub struct AssetCacher {
74    registry: Signal<HashMap<AssetConfiguration, AssetState>>,
75}
76
77impl AssetCacher {
78    /// Cache the given [`AssetConfiguration`]. If it already exists and has a pending clear-task, it will get cancelled.
79    pub fn cache_asset(
80        &mut self,
81        asset_config: AssetConfiguration,
82        asset_bytes: Bytes,
83        subscribe: bool,
84    ) {
85        // Invalidate previous caches
86        if let Some(asset_state) = self.registry.write().remove(&asset_config) {
87            if let AssetUsers::ClearTask(task) = asset_state.users {
88                task.cancel();
89                info!("Clear task of asset with ID '{}' has been cancelled as the asset has been revalidated", asset_config.id);
90            }
91        }
92
93        // Insert the asset into the cache
94        let current_scope_id = current_scope_id().unwrap();
95
96        self.registry.write().insert(
97            asset_config.clone(),
98            AssetState {
99                asset_bytes,
100                users: AssetUsers::Scopes(if subscribe {
101                    HashSet::from([current_scope_id])
102                } else {
103                    HashSet::default()
104                }),
105            },
106        );
107
108        schedule_update_any()(current_scope_id);
109    }
110
111    /// Stop using an asset. It will get removed after the specified duration if it's not used until then.
112    pub fn unuse_asset(&mut self, asset_config: AssetConfiguration) {
113        let mut registry = self.registry;
114
115        let spawn_clear_task = {
116            let mut registry = registry.write();
117
118            let entry = registry.get_mut(&asset_config);
119            if let Some(asset_state) = entry {
120                match &mut asset_state.users {
121                    AssetUsers::Scopes(scopes) => {
122                        // Unsub
123                        scopes.remove(&current_scope_id().unwrap());
124
125                        // Only spawn a clear-task if there are no more scopes using this asset
126                        scopes.is_empty()
127                    }
128                    AssetUsers::ClearTask(task) => {
129                        // This case should never happen but... we leave it here anyway.
130                        task.cancel();
131                        true
132                    }
133                }
134            } else {
135                false
136            }
137        };
138
139        if spawn_clear_task {
140            // Only clear the asset if a duration was specified
141            if let AssetAge::Duration(duration) = asset_config.age {
142                let clear_task = spawn_forever({
143                    let asset_config = asset_config.clone();
144                    async move {
145                        info!("Waiting asset with ID '{}' to be cleared", asset_config.id);
146                        sleep(duration).await;
147                        registry.write().remove(&asset_config);
148                        info!("Cleared asset with ID '{}'", asset_config.id);
149                    }
150                })
151                .unwrap();
152
153                // Registry the clear-task
154                let mut registry = registry.write();
155                let entry = registry.get_mut(&asset_config).unwrap();
156                entry.users = AssetUsers::ClearTask(clear_task);
157            }
158        }
159    }
160
161    /// Start using an Asset. Your scope will get subscribed, to stop using an asset use [`Self::unuse_asset`]
162    pub fn use_asset(&mut self, asset_config: &AssetConfiguration) -> Option<Bytes> {
163        let mut registry = self.registry.write();
164        if let Some(asset_state) = registry.get_mut(asset_config) {
165            match &mut asset_state.users {
166                AssetUsers::ClearTask(task) => {
167                    // Cancel clear-task
168                    task.cancel();
169                    info!(
170                        "Clear task of asset with ID '{}' has been cancelled",
171                        asset_config.id
172                    );
173
174                    // Start using this asset
175                    asset_state.users =
176                        AssetUsers::Scopes(HashSet::from([current_scope_id().unwrap()]));
177                }
178                AssetUsers::Scopes(scopes) => {
179                    // Start using this asset
180                    scopes.insert(current_scope_id().unwrap());
181                }
182            }
183
184            // Reruns those subscribed components
185            if let AssetUsers::Scopes(scopes) = &asset_state.users {
186                let schedule = schedule_update_any();
187                for scope in scopes {
188                    schedule(*scope);
189                }
190                info!(
191                    "Reran {} scopes subscribed to asset with id '{}'",
192                    scopes.len(),
193                    asset_config.id
194                );
195            }
196        }
197
198        registry.get(asset_config).map(|s| s.asset_bytes.clone())
199    }
200
201    /// Read the size of the cache registry.
202    pub fn size(&self) -> usize {
203        self.registry.read().len()
204    }
205}
206
207/// Get access to the global cache of assets.
208pub fn use_asset_cacher() -> AssetCacher {
209    use_context()
210}
211
212/// Initialize the global cache of assets.
213///
214/// This is a **low level** hook that **runs by default** in all Freya apps, you don't need it.
215pub fn use_init_asset_cacher() {
216    use_context_provider(AssetCacher::default);
217}