1use async_trait::async_trait;
7use std::collections::HashMap;
8use std::sync::RwLock;
9use std::time::{Duration, Instant};
10
11use super::store::CacheStore;
12use crate::error::FrameworkError;
13
14#[derive(Clone)]
16struct CacheEntry {
17 value: String,
18 expires_at: Option<Instant>,
19}
20
21impl CacheEntry {
22 fn is_expired(&self) -> bool {
23 self.expires_at
24 .map(|t| Instant::now() > t)
25 .unwrap_or(false)
26 }
27}
28
29pub struct InMemoryCache {
42 store: RwLock<HashMap<String, CacheEntry>>,
43 prefix: String,
44}
45
46impl InMemoryCache {
47 pub fn new() -> Self {
49 Self {
50 store: RwLock::new(HashMap::new()),
51 prefix: "kit_cache:".to_string(),
52 }
53 }
54
55 pub fn with_prefix(prefix: impl Into<String>) -> Self {
57 Self {
58 store: RwLock::new(HashMap::new()),
59 prefix: prefix.into(),
60 }
61 }
62
63 fn prefixed_key(&self, key: &str) -> String {
64 format!("{}{}", self.prefix, key)
65 }
66}
67
68impl Default for InMemoryCache {
69 fn default() -> Self {
70 Self::new()
71 }
72}
73
74#[async_trait]
75impl CacheStore for InMemoryCache {
76 async fn get_raw(&self, key: &str) -> Result<Option<String>, FrameworkError> {
77 let key = self.prefixed_key(key);
78
79 let store = self.store.read().map_err(|_| {
80 FrameworkError::internal("Cache lock poisoned")
81 })?;
82
83 match store.get(&key) {
84 Some(entry) if !entry.is_expired() => Ok(Some(entry.value.clone())),
85 _ => Ok(None),
86 }
87 }
88
89 async fn put_raw(
90 &self,
91 key: &str,
92 value: &str,
93 ttl: Option<Duration>,
94 ) -> Result<(), FrameworkError> {
95 let key = self.prefixed_key(key);
96
97 let entry = CacheEntry {
98 value: value.to_string(),
99 expires_at: ttl.map(|d| Instant::now() + d),
100 };
101
102 let mut store = self.store.write().map_err(|_| {
103 FrameworkError::internal("Cache lock poisoned")
104 })?;
105
106 store.insert(key, entry);
107 Ok(())
108 }
109
110 async fn has(&self, key: &str) -> Result<bool, FrameworkError> {
111 let key = self.prefixed_key(key);
112
113 let store = self.store.read().map_err(|_| {
114 FrameworkError::internal("Cache lock poisoned")
115 })?;
116
117 Ok(store.get(&key).map(|e| !e.is_expired()).unwrap_or(false))
118 }
119
120 async fn forget(&self, key: &str) -> Result<bool, FrameworkError> {
121 let key = self.prefixed_key(key);
122
123 let mut store = self.store.write().map_err(|_| {
124 FrameworkError::internal("Cache lock poisoned")
125 })?;
126
127 Ok(store.remove(&key).is_some())
128 }
129
130 async fn flush(&self) -> Result<(), FrameworkError> {
131 let mut store = self.store.write().map_err(|_| {
132 FrameworkError::internal("Cache lock poisoned")
133 })?;
134
135 store.clear();
136 Ok(())
137 }
138
139 async fn increment(&self, key: &str, amount: i64) -> Result<i64, FrameworkError> {
140 let key = self.prefixed_key(key);
141
142 let mut store = self.store.write().map_err(|_| {
143 FrameworkError::internal("Cache lock poisoned")
144 })?;
145
146 let current: i64 = store
147 .get(&key)
148 .filter(|e| !e.is_expired())
149 .and_then(|e| e.value.parse().ok())
150 .unwrap_or(0);
151
152 let new_value = current + amount;
153
154 store.insert(
155 key,
156 CacheEntry {
157 value: new_value.to_string(),
158 expires_at: None,
159 },
160 );
161
162 Ok(new_value)
163 }
164
165 async fn decrement(&self, key: &str, amount: i64) -> Result<i64, FrameworkError> {
166 self.increment(key, -amount).await
167 }
168}