Skip to main content

whatsapp_rust/
cache_store.rs

1//! Typed cache wrapper that dispatches to either the in-process
2//! [`Cache`] or a custom [`CacheStore`] backend (e.g., Redis).
3//!
4//! [`TypedCache`] presents the same interface regardless of the backing store.
5//! Keys are serialised via [`Display`]; values are serialised with `serde_json`
6//! only on the custom-store path — the in-process path has zero extra overhead.
7
8use std::borrow::Borrow;
9use std::fmt::Display;
10use std::marker::PhantomData;
11use std::sync::Arc;
12use std::time::Duration;
13
14use crate::cache::Cache;
15use serde::{Serialize, de::DeserializeOwned};
16
17pub use wacore::store::cache::CacheStore;
18
19// ── Internal storage variant ──────────────────────────────────────────────────
20
21enum Inner<K, V> {
22    Local(Cache<K, V>),
23    Custom {
24        store: Arc<dyn CacheStore>,
25        namespace: &'static str,
26        ttl: Option<Duration>,
27        _marker: PhantomData<fn(K, V)>,
28    },
29}
30
31// ── TypedCache ─────────────────────────────────────────────────────────────────
32
33/// A cache over `K → V` backed by either the in-process cache or any [`CacheStore`].
34///
35/// The in-process path has **zero extra overhead** — values are stored in
36/// memory without any serialisation.  The custom-store path serialises values
37/// with `serde_json` and keys via [`Display`].
38pub struct TypedCache<K, V> {
39    inner: Inner<K, V>,
40}
41
42impl<K, V> TypedCache<K, V>
43where
44    K: std::hash::Hash + Eq + Clone + Send + Sync + 'static,
45    V: Clone + Send + Sync + 'static,
46{
47    /// Wrap an in-process [`Cache`] (zero overhead vs. using the cache directly).
48    pub fn from_local(cache: Cache<K, V>) -> Self {
49        Self {
50            inner: Inner::Local(cache),
51        }
52    }
53}
54
55impl<K, V> TypedCache<K, V>
56where
57    K: std::hash::Hash + Eq + Clone + Display + Send + Sync + 'static,
58    V: Clone + Serialize + DeserializeOwned + Send + Sync + 'static,
59{
60    /// Create a cache backed by a custom store.
61    ///
62    /// - `namespace` — unique string for this cache (e.g., `"group"`)
63    /// - `ttl` — forwarded to [`CacheStore::set`]; `None` means no expiry
64    pub fn from_store(
65        store: Arc<dyn CacheStore>,
66        namespace: &'static str,
67        ttl: Option<Duration>,
68    ) -> Self {
69        Self {
70            inner: Inner::Custom {
71                store,
72                namespace,
73                ttl,
74                _marker: PhantomData,
75            },
76        }
77    }
78
79    /// Look up a value.
80    ///
81    /// Accepts borrowed keys (`&str` for `String`, `&Jid` for `Jid`, etc.)
82    /// following the same pattern as [`std::collections::HashMap::get`].
83    ///
84    /// Cache misses and deserialisation failures both return `None`; the
85    /// caller re-fetches from the authoritative source.
86    pub async fn get<Q>(&self, key: &Q) -> Option<V>
87    where
88        K: Borrow<Q>,
89        Q: std::hash::Hash + Eq + Display + ?Sized,
90    {
91        match &self.inner {
92            Inner::Local(cache) => cache.get(key).await,
93            Inner::Custom {
94                store, namespace, ..
95            } => {
96                let key_str = key.to_string();
97                match store.get(namespace, &key_str).await {
98                    Ok(Some(bytes)) => serde_json::from_slice(&bytes)
99                        .inspect_err(|e| {
100                            log::warn!(
101                                "TypedCache[{namespace}]: deserialise failed for {key_str}: {e}"
102                            );
103                        })
104                        .ok(),
105                    Ok(None) => None,
106                    Err(e) => {
107                        log::warn!("TypedCache[{namespace}]: get({key_str}) error: {e}");
108                        None
109                    }
110                }
111            }
112        }
113    }
114
115    /// Insert or update a value (takes ownership of key and value).
116    pub async fn insert(&self, key: K, value: V) {
117        match &self.inner {
118            Inner::Local(cache) => cache.insert(key, value).await,
119            Inner::Custom {
120                store,
121                namespace,
122                ttl,
123                ..
124            } => {
125                let key_str = key.to_string();
126                match serde_json::to_vec(&value) {
127                    Ok(bytes) => {
128                        if let Err(e) = store.set(namespace, &key_str, &bytes, *ttl).await {
129                            log::warn!("TypedCache[{namespace}]: set({key_str}) error: {e}");
130                        }
131                    }
132                    Err(e) => {
133                        log::warn!("TypedCache[{namespace}]: serialise failed for {key_str}: {e}");
134                    }
135                }
136            }
137        }
138    }
139
140    /// Remove a single key.
141    ///
142    /// Accepts borrowed keys following the same pattern as `get`.
143    pub async fn invalidate<Q>(&self, key: &Q)
144    where
145        K: Borrow<Q>,
146        Q: std::hash::Hash + Eq + Display + ?Sized,
147    {
148        match &self.inner {
149            Inner::Local(cache) => cache.invalidate(key).await,
150            Inner::Custom {
151                store, namespace, ..
152            } => {
153                let key_str = key.to_string();
154                if let Err(e) = store.delete(namespace, &key_str).await {
155                    log::warn!("TypedCache[{namespace}]: delete({key_str}) error: {e}");
156                }
157            }
158        }
159    }
160
161    /// Remove all entries.
162    ///
163    /// For the in-process backend this is synchronous.
164    /// For the custom backend this spawns a fire-and-forget task via
165    /// [`tokio::runtime::Handle::try_current`] (requires `tokio-runtime`
166    /// feature) to avoid panicking if called outside a Tokio runtime.
167    /// Without `tokio-runtime`, the clear is skipped with a warning.
168    pub fn invalidate_all(&self) {
169        match &self.inner {
170            Inner::Local(cache) => cache.invalidate_all(),
171            Inner::Custom {
172                store, namespace, ..
173            } => {
174                let _store = store.clone();
175                let _ns = *namespace;
176                #[cfg(all(not(target_arch = "wasm32"), feature = "tokio-runtime"))]
177                match tokio::runtime::Handle::try_current() {
178                    Ok(handle) => {
179                        handle.spawn(async move {
180                            if let Err(e) = _store.clear(_ns).await {
181                                log::warn!("TypedCache[{_ns}]: clear() error: {e}");
182                            }
183                        });
184                    }
185                    Err(_) => {
186                        log::warn!("TypedCache[{_ns}]: clear() skipped: no runtime");
187                    }
188                }
189                #[cfg(all(not(target_arch = "wasm32"), not(feature = "tokio-runtime")))]
190                log::warn!("TypedCache[{_ns}]: clear() skipped: tokio-runtime feature not enabled");
191            }
192        }
193    }
194
195    /// Remove all entries, awaiting completion for custom backends.
196    pub async fn clear(&self) {
197        match &self.inner {
198            Inner::Local(cache) => cache.clear().await,
199            Inner::Custom {
200                store, namespace, ..
201            } => {
202                if let Err(e) = store.clear(namespace).await {
203                    log::warn!("TypedCache[{namespace}]: clear() error: {e}");
204                }
205            }
206        }
207    }
208
209    /// Run any pending internal housekeeping tasks (in-process backend only).
210    ///
211    /// For the in-process backend this evicts expired entries so a subsequent
212    /// [`entry_count`](Self::entry_count) reflects them. For custom backends
213    /// this is a no-op.
214    pub async fn run_pending_tasks(&self) {
215        if let Inner::Local(cache) = &self.inner {
216            cache.run_pending_tasks().await;
217        }
218    }
219
220    /// Iterate the in-process backend's entries. `None` for custom stores,
221    /// whose entries live outside this process (memory reports treat them as
222    /// zero retained bytes for the same reason).
223    pub fn iter_local(&self) -> Option<std::vec::IntoIter<(Arc<K>, V)>> {
224        match &self.inner {
225            Inner::Local(cache) => Some(cache.iter()),
226            Inner::Custom { .. } => None,
227        }
228    }
229
230    /// Entry count plus estimated retained bytes, summing `per_entry` over the
231    /// in-process backend (a reliable by-reference walk — no clones, cannot
232    /// degrade to an empty snapshot under write contention). Custom stores
233    /// report zero: their entries live outside this process.
234    pub async fn memory_stats(
235        &self,
236        per_entry: impl FnMut(&K, &V) -> usize,
237    ) -> wacore::stats::CollectionStats {
238        match &self.inner {
239            Inner::Local(cache) => cache.memory_stats(per_entry).await,
240            Inner::Custom { .. } => wacore::stats::CollectionStats::default(),
241        }
242    }
243
244    /// Approximate entry count (sync). Returns `0` for custom backends.
245    ///
246    /// For diagnostics that need custom backend counts, use
247    /// [`entry_count_async`](Self::entry_count_async) instead.
248    pub fn entry_count(&self) -> u64 {
249        match &self.inner {
250            Inner::Local(cache) => cache.entry_count(),
251            Inner::Custom { .. } => 0,
252        }
253    }
254
255    /// Approximate entry count, delegating to the custom backend if available.
256    pub async fn entry_count_async(&self) -> u64 {
257        match &self.inner {
258            Inner::Local(cache) => cache.entry_count(),
259            Inner::Custom {
260                store, namespace, ..
261            } => store.entry_count(namespace).await.unwrap_or(0),
262        }
263    }
264}