1use crate::{namespace, AppStore, KvStore};
4use bytes::Bytes;
5use serde::de::DeserializeOwned;
6use serde::Serialize;
7use std::future::Future;
8use std::sync::Arc;
9use std::time::{Duration, Instant};
10use thiserror::Error;
11
12#[derive(Debug, Error)]
13pub enum CacheError {
14 #[error("serialize: {0}")]
15 Serialize(String),
16 #[error("deserialize: {0}")]
17 Deserialize(String),
18 #[error("{0}")]
19 Msg(String),
20}
21
22#[derive(Clone)]
24pub struct Cache {
25 store: Arc<dyn KvStore>,
26}
27
28fn trunc(key: &str) -> String {
29 const MAX: usize = 120;
30 if key.len() <= MAX {
31 key.to_string()
32 } else {
33 format!("{}…", &key[..MAX - 1])
34 }
35}
36
37fn rid() -> Option<String> {
38 sova_core::current_request_id()
39}
40
41impl Cache {
42 pub fn new(store: Arc<dyn KvStore>) -> Self {
43 Self { store }
44 }
45
46 pub async fn get_json<T: DeserializeOwned>(&self, key: &str) -> Option<T> {
47 let started = Instant::now();
48 let bytes = self.store.get(key).await;
49 let hit = bytes.is_some();
50 let n = bytes.as_ref().map(|b| b.len() as u64);
51 let ms = started.elapsed().as_secs_f64() * 1000.0;
52 tracing::debug!(
53 target: "sova.store",
54 op = "get",
55 backend = "cache",
56 key = %trunc(key),
57 hit,
58 bytes = n,
59 duration_ms = ms,
60 request_id = rid().as_deref().unwrap_or(""),
61 "sova.store"
62 );
63 let bytes = bytes?;
64 serde_json::from_slice(&bytes).ok()
65 }
66
67 pub async fn set_json<T: Serialize>(
68 &self,
69 key: &str,
70 val: &T,
71 ttl: Option<Duration>,
72 ) -> Result<(), CacheError> {
73 let started = Instant::now();
74 let raw = serde_json::to_vec(val).map_err(|e| CacheError::Serialize(e.to_string()))?;
75 let n = raw.len() as u64;
76 self.store.set(key, Bytes::from(raw), ttl).await;
77 let ms = started.elapsed().as_secs_f64() * 1000.0;
78 tracing::debug!(
79 target: "sova.store",
80 op = "set",
81 backend = "cache",
82 key = %trunc(key),
83 bytes = n,
84 duration_ms = ms,
85 request_id = rid().as_deref().unwrap_or(""),
86 "sova.store"
87 );
88 Ok(())
89 }
90
91 pub async fn remember<T, F, Fut>(
93 &self,
94 key: &str,
95 ttl: Option<Duration>,
96 f: F,
97 ) -> Result<T, CacheError>
98 where
99 T: Serialize + DeserializeOwned,
100 F: FnOnce() -> Fut,
101 Fut: Future<Output = Result<T, CacheError>>,
102 {
103 if let Some(hit) = self.get_json::<T>(key).await {
104 return Ok(hit);
105 }
106 let val = f().await?;
107 self.set_json(key, &val, ttl).await?;
108 Ok(val)
109 }
110
111 pub async fn invalidate(&self, key: &str) {
112 let started = Instant::now();
113 self.store.remove(key).await;
114 let ms = started.elapsed().as_secs_f64() * 1000.0;
115 tracing::debug!(
116 target: "sova.store",
117 op = "remove",
118 backend = "cache",
119 key = %trunc(key),
120 duration_ms = ms,
121 request_id = rid().as_deref().unwrap_or(""),
122 "sova.store"
123 );
124 }
125}
126
127impl AppStore {
128 pub fn cache(&self) -> Cache {
130 Cache::new(Arc::new(namespace(Arc::clone(&self.inner), "cache")))
131 }
132}