Skip to main content

freya_html/
cache.rs

1use std::{
2    collections::VecDeque,
3    sync::{
4        Arc,
5        Mutex,
6    },
7};
8
9use blitz_traits::net::Bytes;
10use freya_core::prelude::GlobalContexts;
11use url::Url;
12
13#[derive(Default)]
14struct CachedResources {
15    entries: VecDeque<(Url, Bytes)>,
16    bytes: usize,
17}
18
19impl CachedResources {
20    fn remove(&mut self, url: &Url) -> Option<(Url, Bytes)> {
21        let index = self
22            .entries
23            .iter()
24            .position(|(cached_url, _)| cached_url == url)?;
25        let entry = self.entries.remove(index)?;
26        self.bytes -= entry.1.len();
27        Some(entry)
28    }
29}
30
31/// Shared LRU cache with a 32 MiB default body budget.
32#[derive(Clone)]
33pub struct ResourceCache {
34    resources: Arc<Mutex<CachedResources>>,
35    max_bytes: usize,
36}
37
38impl Default for ResourceCache {
39    fn default() -> Self {
40        Self::new(32 * 1024 * 1024)
41    }
42}
43
44impl ResourceCache {
45    /// Limits cached bodies to `max_bytes`, with zero disabling caching.
46    ///
47    /// ```no_run
48    /// use freya_core::prelude::GlobalContexts;
49    /// use freya_html::ResourceCache;
50    ///
51    /// // Configure before creating HTML viewers.
52    /// GlobalContexts::get().insert_context(ResourceCache::new(64 * 1024 * 1024));
53    /// ```
54    pub fn new(max_bytes: usize) -> Self {
55        Self {
56            resources: Arc::new(Mutex::new(CachedResources::default())),
57            max_bytes,
58        }
59    }
60
61    /// Retrieves or initializes the shared cache in the current app context.
62    pub fn get() -> Self {
63        GlobalContexts::get().get_context_or_insert(Self::default)
64    }
65
66    /// Returns cached bytes and marks the resource as recently used.
67    pub fn read(&self, url: &Url) -> Option<Bytes> {
68        let mut resources = self
69            .resources
70            .lock()
71            .unwrap_or_else(|error| error.into_inner());
72        let index = resources
73            .entries
74            .iter()
75            .position(|(cached_url, _)| cached_url == url)?;
76        let entry = resources.entries.remove(index)?;
77        let bytes = entry.1.clone();
78        resources.entries.push_back(entry);
79        Some(bytes)
80    }
81
82    /// Stores resource bytes by URL, evicting least recently used entries to fit the budget.
83    pub fn set(&self, url: Url, bytes: Bytes) {
84        let mut resources = self
85            .resources
86            .lock()
87            .unwrap_or_else(|error| error.into_inner());
88        resources.remove(&url);
89        if bytes.is_empty() || bytes.len() > self.max_bytes {
90            return;
91        }
92
93        while resources.bytes > self.max_bytes - bytes.len()
94            && let Some((_, evicted)) = resources.entries.pop_front()
95        {
96            resources.bytes -= evicted.len();
97        }
98        resources.bytes += bytes.len();
99        resources.entries.push_back((url, bytes));
100    }
101
102    pub fn invalidate(&self, url: &Url) {
103        self.resources
104            .lock()
105            .unwrap_or_else(|error| error.into_inner())
106            .remove(url);
107    }
108
109    pub fn clear(&self) {
110        *self
111            .resources
112            .lock()
113            .unwrap_or_else(|error| error.into_inner()) = CachedResources::default();
114    }
115}