1use crate::{App, AppContext, EntityId, SharedString, SharedUri, Task};
2use collections::FxHashSet;
3use futures::{Future, TryFutureExt};
4
5use std::cell::RefCell;
6use std::fmt::Debug;
7use std::hash::{BuildHasher, Hash};
8use std::marker::PhantomData;
9use std::mem;
10use std::path::{Path, PathBuf};
11use std::rc::Rc;
12use std::sync::Arc;
13
14pub(crate) struct CachedLoad<T> {
15 state: Rc<RefCell<CachedLoadState<T>>>,
16 _task: Task<()>,
18}
19
20enum CachedLoadState<T> {
21 Loading(FxHashSet<EntityId>),
22 Loaded(T),
23}
24
25impl<T: Clone + Send + 'static> CachedLoad<T> {
26 pub(crate) fn new(future: impl Future<Output = T> + Send + 'static, cx: &App) -> Self {
27 let state = Rc::new(RefCell::new(CachedLoadState::Loading(FxHashSet::default())));
28 let task = cx.background_executor().spawn(future);
29 let task = cx.spawn({
30 let state = Rc::downgrade(&state);
31 async move |cx| {
32 let result = task.await;
33 let Some(state) = state.upgrade() else {
34 return;
35 };
36 let previous =
37 mem::replace(&mut *state.borrow_mut(), CachedLoadState::Loaded(result));
38 let CachedLoadState::Loading(views) = previous else {
39 unreachable!("a cached load completes only once");
40 };
41 cx.update(|cx| {
42 for view in views {
43 cx.notify(view);
44 }
45 });
46 }
47 });
48 Self { state, _task: task }
49 }
50
51 pub(crate) fn get(&self) -> Option<T> {
52 match &*self.state.borrow() {
53 CachedLoadState::Loading(_) => None,
54 CachedLoadState::Loaded(result) => Some(result.clone()),
55 }
56 }
57
58 pub(crate) fn use_by(&self, view: EntityId) -> Option<T> {
59 match &mut *self.state.borrow_mut() {
60 CachedLoadState::Loading(views) => {
61 views.insert(view);
62 None
63 }
64 CachedLoadState::Loaded(result) => Some(result.clone()),
65 }
66 }
67}
68
69#[derive(Debug, PartialEq, Eq, Hash, Clone)]
73pub enum Resource {
74 Uri(SharedUri),
76 Path(Arc<Path>),
79 Embedded(SharedString),
82}
83
84impl From<SharedUri> for Resource {
85 fn from(value: SharedUri) -> Self {
86 Self::Uri(value)
87 }
88}
89
90impl From<PathBuf> for Resource {
91 fn from(value: PathBuf) -> Self {
92 Self::Path(value.into())
93 }
94}
95
96impl From<Arc<Path>> for Resource {
97 fn from(value: Arc<Path>) -> Self {
98 Self::Path(value)
99 }
100}
101
102pub trait Asset: 'static {
110 type Source: Clone + Hash + Send;
112
113 type Output: Clone + Send;
116
117 fn load(
122 source: Self::Source,
123 cx: &mut App,
124 ) -> impl Future<Output = Self::Output> + Send + 'static;
125}
126
127pub enum AssetLogger<T> {
132 #[doc(hidden)]
133 _Phantom(PhantomData<T>, &'static dyn crate::seal::Sealed),
134}
135
136impl<T, R, E> Asset for AssetLogger<T>
137where
138 T: Asset<Output = Result<R, E>>,
139 R: Clone + Send,
140 E: Clone + Send + Debug,
141{
142 type Source = T::Source;
143
144 type Output = T::Output;
145
146 fn load(
147 source: Self::Source,
148 cx: &mut App,
149 ) -> impl Future<Output = Self::Output> + Send + 'static {
150 let load = T::load(source, cx);
151 load.inspect_err(|e| log::error!("Failed to load asset: {:?}", e))
152 }
153}
154
155pub fn hash<T: Hash>(data: &T) -> u64 {
159 collections::FxBuildHasher.hash_one(data)
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::{AppContext, TestAppContext};
166 use futures::channel::oneshot;
167 use std::{
168 cell::Cell,
169 collections::VecDeque,
170 hash::{Hash, Hasher},
171 rc::Rc,
172 sync::{
173 Arc, Mutex,
174 atomic::{AtomicUsize, Ordering},
175 },
176 };
177
178 #[gpui::test]
179 fn cached_load_caches_success_and_error(cx: &mut TestAppContext) {
180 let successful_load =
181 cx.update(|cx| CachedLoad::new(async { Ok::<_, &'static str>(42) }, cx));
182 let failed_load =
183 cx.update(|cx| CachedLoad::new(async { Err::<i32, _>("load failed") }, cx));
184
185 assert_eq!(successful_load.get(), None);
186 assert_eq!(failed_load.get(), None);
187
188 cx.run_until_parked();
189
190 assert_eq!(successful_load.get(), Some(Ok(42)));
191 assert_eq!(successful_load.get(), Some(Ok(42)));
192 assert_eq!(failed_load.get(), Some(Err("load failed")));
193 assert_eq!(failed_load.get(), Some(Err("load failed")));
194 }
195
196 #[gpui::test]
197 fn completed_load_without_subscribers_is_cached_and_deduplicated(cx: &mut TestAppContext) {
198 let (sender, receiver) = oneshot::channel();
199 let source = TestAssetSource::new(1, [receiver]);
200
201 assert_eq!(cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)), None);
202 assert_eq!(cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)), None);
203 assert_eq!(source.load_count(), 1);
204
205 assert!(sender.send(Ok(42)).is_ok());
206 cx.run_until_parked();
207
208 assert_eq!(
209 cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)),
210 Some(Ok(42))
211 );
212 assert_eq!(
213 cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)),
214 Some(Ok(42))
215 );
216 assert_eq!(source.load_count(), 1);
217 }
218
219 #[gpui::test]
220 fn load_completion_notifies_each_observing_entity_once(cx: &mut TestAppContext) {
221 let (sender, receiver) = oneshot::channel::<i32>();
222 let load = cx.update(|cx| {
223 CachedLoad::new(
224 async move { receiver.await.expect("test sends a result") },
225 cx,
226 )
227 });
228 let first_notification_count = Rc::new(Cell::new(0));
229 let second_notification_count = Rc::new(Cell::new(0));
230 let (first_entity, second_entity) = cx.update(|cx| {
231 let first_entity = cx.new(|_| ());
232 let second_entity = cx.new(|_| ());
233 cx.observe(&first_entity, {
234 let first_notification_count = first_notification_count.clone();
235 move |_, _| first_notification_count.set(first_notification_count.get() + 1)
236 })
237 .detach();
238 cx.observe(&second_entity, {
239 let second_notification_count = second_notification_count.clone();
240 move |_, _| second_notification_count.set(second_notification_count.get() + 1)
241 })
242 .detach();
243 (first_entity, second_entity)
244 });
245
246 assert_eq!(load.use_by(first_entity.entity_id()), None);
247 assert_eq!(load.use_by(first_entity.entity_id()), None);
248 assert_eq!(load.use_by(second_entity.entity_id()), None);
249
250 assert!(sender.send(42).is_ok());
251 cx.run_until_parked();
252
253 assert_eq!(first_notification_count.get(), 1);
254 assert_eq!(second_notification_count.get(), 1);
255 assert_eq!(load.use_by(first_entity.entity_id()), Some(42));
256 assert_eq!(load.use_by(second_entity.entity_id()), Some(42));
257 }
258
259 #[gpui::test]
260 fn removing_pending_asset_cancels_it_and_replacement_ignores_stale_completion(
261 cx: &mut TestAppContext,
262 ) {
263 let (stale_sender, stale_receiver) = oneshot::channel();
264 let (replacement_sender, replacement_receiver) = oneshot::channel();
265 let source = TestAssetSource::new(2, [stale_receiver, replacement_receiver]);
266
267 assert_eq!(cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)), None);
268 assert_eq!(source.load_count(), 1);
269 cx.run_until_parked();
270
271 cx.update(|cx| cx.remove_asset::<TestAsset>(&source));
272 assert!(!cx.update(|cx| cx.has_asset::<TestAsset>(&source)));
273 cx.run_until_parked();
274 assert!(stale_sender.send(Ok(1)).is_err());
275
276 assert_eq!(cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)), None);
277 assert_eq!(source.load_count(), 2);
278
279 assert!(replacement_sender.send(Err("replacement failed")).is_ok());
280 cx.run_until_parked();
281
282 assert_eq!(
283 cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)),
284 Some(Err("replacement failed"))
285 );
286 assert_eq!(source.load_count(), 2);
287 }
288
289 #[gpui::test]
290 fn eviction_after_worker_completion_does_not_publish_into_replacement(cx: &mut TestAppContext) {
291 let (sender, receiver) = oneshot::channel();
292 let (replacement_sender, replacement_receiver) = oneshot::channel();
293 let source = TestAssetSource::new(3, [receiver, replacement_receiver]);
294
295 assert_eq!(cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)), None);
296 assert!(sender.send(Ok(1)).is_ok());
297 while source.completion_count.load(Ordering::SeqCst) == 0 {
298 assert!(cx.background_executor.tick());
299 }
300 assert_eq!(cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)), None);
301
302 cx.update(|cx| {
303 cx.remove_asset::<TestAsset>(&source);
304 assert_eq!(cx.fetch_asset::<TestAsset>(&source), None);
305 });
306 cx.run_until_parked();
307 assert_eq!(cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)), None);
308
309 assert!(replacement_sender.send(Ok(2)).is_ok());
310 cx.run_until_parked();
311 assert_eq!(
312 cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)),
313 Some(Ok(2))
314 );
315 assert_eq!(source.load_count(), 2);
316 }
317
318 struct TestAsset;
319
320 #[derive(Clone)]
321 struct TestAssetSource {
322 id: usize,
323 load_count: Arc<AtomicUsize>,
324 completion_count: Arc<AtomicUsize>,
325 receivers: Arc<Mutex<VecDeque<oneshot::Receiver<Result<i32, &'static str>>>>>,
326 }
327
328 impl TestAssetSource {
329 fn new(
330 id: usize,
331 receivers: impl IntoIterator<Item = oneshot::Receiver<Result<i32, &'static str>>>,
332 ) -> Self {
333 Self {
334 id,
335 load_count: Arc::new(AtomicUsize::new(0)),
336 completion_count: Arc::new(AtomicUsize::new(0)),
337 receivers: Arc::new(Mutex::new(receivers.into_iter().collect())),
338 }
339 }
340
341 fn load_count(&self) -> usize {
342 self.load_count.load(Ordering::SeqCst)
343 }
344 }
345
346 impl Hash for TestAssetSource {
347 fn hash<H: Hasher>(&self, state: &mut H) {
348 self.id.hash(state);
349 }
350 }
351
352 impl Asset for TestAsset {
353 type Source = TestAssetSource;
354 type Output = Result<i32, &'static str>;
355
356 fn load(
357 source: Self::Source,
358 _cx: &mut App,
359 ) -> impl Future<Output = Self::Output> + Send + 'static {
360 source.load_count.fetch_add(1, Ordering::SeqCst);
361 let receiver = source
362 .receivers
363 .lock()
364 .expect("test receiver mutex should not be poisoned")
365 .pop_front()
366 .expect("each test load should have a receiver");
367 async move {
368 let result = match receiver.await {
369 Ok(result) => result,
370 Err(_) => Err("cancelled"),
371 };
372 source.completion_count.fetch_add(1, Ordering::SeqCst);
373 result
374 }
375 }
376 }
377}