freya_components/
cache.rs1use 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 freya_core::{
15 integration::FxHashSet,
16 prelude::*,
17};
18
19#[derive(Hash, PartialEq, Eq, Clone, Copy)]
22pub enum AssetAge {
23 Duration(Duration),
25 Unspecified,
27}
28
29impl Default for AssetAge {
30 fn default() -> Self {
31 Self::Duration(Duration::from_secs(3600)) }
33}
34
35impl AssetAge {
36 pub fn zero() -> Self {
38 Self::Duration(Duration::ZERO)
39 }
40}
41
42impl From<Duration> for AssetAge {
43 fn from(value: Duration) -> Self {
44 Self::Duration(value)
45 }
46}
47
48#[derive(Hash, PartialEq, Eq, Clone)]
50pub struct AssetConfiguration {
51 pub age: AssetAge,
53 pub id: u64,
55}
56
57impl AssetConfiguration {
58 pub fn new(id: impl Hash, age: AssetAge) -> Self {
59 let mut state = DefaultHasher::default();
60 id.hash(&mut state);
61 let id = state.finish();
62 Self { id, age }
63 }
64}
65
66enum AssetUsers {
67 Listeners(Rc<RefCell<FxHashSet<ReactiveContext>>>),
68 ClearTask(TaskHandle),
69}
70
71#[derive(Clone)]
72pub enum Asset {
73 Cached(Rc<dyn Any>),
75 Loading,
77 Pending,
79 Error(String),
81}
82
83impl Asset {
84 pub fn try_get(&self) -> Option<&Rc<dyn Any>> {
86 match self {
87 Self::Cached(asset) => Some(asset),
88 _ => None,
89 }
90 }
91}
92
93struct AssetState {
94 users: AssetUsers,
95 asset: Asset,
96}
97
98#[derive(Clone, Copy, PartialEq)]
99pub struct AssetCacher {
100 registry: State<HashMap<AssetConfiguration, AssetState>>,
101}
102
103impl AssetCacher {
104 pub fn create() -> Self {
105 Self {
106 registry: State::create(HashMap::new()),
107 }
108 }
109
110 pub fn try_get() -> Option<Self> {
111 try_consume_root_context()
112 }
113
114 #[track_caller]
115 pub fn get() -> Self {
116 consume_root_context()
117 }
118
119 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 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 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 if let AssetUsers::Listeners(listeners) = &asset.users {
150 for sub in listeners.borrow().iter() {
151 sub.notify();
152 }
153 }
154 }
155
156 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 listeners.borrow().is_empty()
169 }
170 AssetUsers::ClearTask(task) => {
171 task.cancel();
173 true
174 }
175 }
176 } else {
177 false
178 }
179 };
180
181 if spawn_clear_task {
182 if let AssetAge::Duration(duration) = asset_config.age {
184 let clear_task = spawn_in_window({
185 let asset_config = asset_config.clone();
186 async move {
187 timer(duration).await;
188 registry.write().remove(&asset_config);
189 }
190 });
191
192 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 pub fn cached_size(&self) -> usize {
230 self.registry
231 .read()
232 .values()
233 .filter(|asset_state| matches!(&asset_state.asset, Asset::Cached(_)))
234 .count()
235 }
236
237 pub fn size(&self) -> usize {
239 self.registry.read().len()
240 }
241}
242
243pub fn use_asset(asset_config: &AssetConfiguration) {
245 let mut asset_cacher = use_hook(AssetCacher::get);
246
247 let mut current_config = use_state::<Option<AssetConfiguration>>(|| None);
248
249 use_drop(move || {
250 let Some(asset_config) = current_config.peek().clone() else {
251 return;
252 };
253 spawn_in_window(async move {
255 asset_cacher.try_clean(&asset_config);
256 });
257 });
258
259 {
260 let mut current_config = current_config.write();
261 if current_config.as_ref() != Some(asset_config) {
262 if let Some(prev) = &*current_config
263 && prev != asset_config
264 {
265 asset_cacher.try_clean(prev);
267 }
268 current_config.replace(asset_config.clone());
269 }
270 asset_cacher.listen(ReactiveContext::current(), asset_config.clone());
271 }
272}