game_manager_lib\services/
cache.rs1use crate::constants::{
7 CACHE_RAWG_GAME_TTL_DAYS, CACHE_RAWG_LIST_TTL_DAYS, CACHE_STEAM_PLAYTIME_TTL_DAYS,
8 CACHE_STEAM_REVIEWS_TTL_DAYS, CACHE_STEAM_STORE_TTL_DAYS,
9};
10use rusqlite::{params, Connection};
11use std::time::{SystemTime, UNIX_EPOCH};
12use tracing::{info, warn};
13
14#[derive(Debug, serde::Serialize)]
16pub struct CacheStats {
17 pub total_entries: i32,
18 pub rawg_entries: i32,
19 pub steam_entries: i32,
20 pub expired_entries: i32,
21}
22
23fn current_timestamp() -> i64 {
25 SystemTime::now()
26 .duration_since(UNIX_EPOCH)
27 .unwrap()
28 .as_secs() as i64
29}
30
31pub fn initialize_cache_db(conn: &Connection) -> Result<(), String> {
33 conn.execute(
34 "CREATE TABLE IF NOT EXISTS api_cache (
35 source TEXT NOT NULL,
36 external_id TEXT NOT NULL,
37 payload TEXT NOT NULL,
38 updated_at INTEGER NOT NULL,
39 PRIMARY KEY (source, external_id)
40 )",
41 [],
42 )
43 .map_err(|e| format!("Erro ao criar tabela api_cache: {}", e))?;
44
45 conn.execute(
47 "CREATE INDEX IF NOT EXISTS idx_cache_updated
48 ON api_cache(source, updated_at)",
49 [],
50 )
51 .map_err(|e| format!("Erro ao criar índice: {}", e))?;
52
53 Ok(())
54}
55
56fn get_ttl_for_cache_type(cache_key: &str) -> i64 {
58 if cache_key.contains("_list_") {
60 return CACHE_RAWG_LIST_TTL_DAYS * 24 * 60 * 60;
61 }
62 if cache_key.starts_with("rawg_") {
63 CACHE_RAWG_GAME_TTL_DAYS * 24 * 60 * 60
64 } else if cache_key.starts_with("store_") {
65 CACHE_STEAM_STORE_TTL_DAYS * 24 * 60 * 60
66 } else if cache_key.starts_with("reviews_") {
67 CACHE_STEAM_REVIEWS_TTL_DAYS * 24 * 60 * 60
68 } else if cache_key.starts_with("playtime_") {
69 CACHE_STEAM_PLAYTIME_TTL_DAYS * 24 * 60 * 60
70 } else {
71 7 * 24 * 60 * 60 }
73}
74
75fn is_cache_expired(cache_key: &str, updated_at: i64) -> bool {
77 let now = current_timestamp();
78 let ttl_seconds = get_ttl_for_cache_type(cache_key);
79
80 (now - updated_at) > ttl_seconds
81}
82
83pub fn get_cached_api_data(conn: &Connection, source: &str, external_id: &str) -> Option<String> {
89 let result: Result<(String, i64), rusqlite::Error> = conn.query_row(
90 "SELECT payload, updated_at FROM api_cache
91 WHERE source = ?1 AND external_id = ?2",
92 params![source, external_id],
93 |row| Ok((row.get(0)?, row.get(1)?)),
94 );
95
96 match result {
97 Ok((payload, updated_at)) => {
98 let full_key = external_id;
100 if is_cache_expired(full_key, updated_at) {
101 None
102 } else {
103 Some(payload)
104 }
105 }
106 Err(rusqlite::Error::QueryReturnedNoRows) => None,
107 Err(e) => {
108 warn!("Erro ao buscar cache: {}", e);
109 None
110 }
111 }
112}
113
114pub fn save_cached_api_data(
116 conn: &Connection,
117 source: &str,
118 external_id: &str,
119 payload: &str,
120) -> Result<(), String> {
121 let now = current_timestamp();
122
123 conn.execute(
124 "INSERT OR REPLACE INTO api_cache (source, external_id, payload, updated_at)
125 VALUES (?1, ?2, ?3, ?4)",
126 params![source, external_id, payload, now],
127 )
128 .map_err(|e| format!("Erro ao salvar cache: {}", e))?;
129
130 Ok(())
131}
132
133pub fn cleanup_expired_cache(conn: &Connection) -> Result<usize, String> {
135 let now = current_timestamp();
136
137 let rawg_cutoff = now - (CACHE_RAWG_GAME_TTL_DAYS * 24 * 60 * 60);
139 let store_cutoff = now - (CACHE_STEAM_STORE_TTL_DAYS * 24 * 60 * 60);
140 let reviews_cutoff = now - (CACHE_STEAM_REVIEWS_TTL_DAYS * 24 * 60 * 60);
141 let playtime_cutoff = now - (CACHE_STEAM_PLAYTIME_TTL_DAYS * 24 * 60 * 60);
142
143 let deleted = conn
144 .execute(
145 "DELETE FROM api_cache
146 WHERE (source = 'rawg' AND external_id LIKE 'search_%' AND updated_at < ?1)
147 OR (source = 'steam' AND external_id LIKE 'store_%' AND updated_at < ?2)
148 OR (source = 'steam' AND external_id LIKE 'reviews_%' AND updated_at < ?3)
149 OR (source = 'steam' AND external_id LIKE 'playtime_%' AND updated_at < ?4)",
150 params![rawg_cutoff, store_cutoff, reviews_cutoff, playtime_cutoff],
151 )
152 .map_err(|e| format!("Erro ao limpar cache: {}", e))?;
153
154 if deleted > 0 {
155 info!("Cache cleanup: {} entradas removidas", deleted);
156 }
157
158 Ok(deleted)
159}
160
161pub fn get_stale_api_data(conn: &Connection, source: &str, external_id: &str) -> Option<String> {
165 let result: Result<String, rusqlite::Error> = conn.query_row(
166 "SELECT payload FROM api_cache
167 WHERE source = ?1 AND external_id = ?2",
168 params![source, external_id],
169 |row| row.get(0),
170 );
171
172 result.ok() }
174
175pub fn get_cache_stats(conn: &Connection) -> Result<CacheStats, String> {
177 let total: i32 = conn
178 .query_row("SELECT COUNT(*) FROM api_cache", [], |row| row.get(0))
179 .unwrap_or(0);
180
181 let rawg: i32 = conn
182 .query_row(
183 "SELECT COUNT(*) FROM api_cache WHERE source = 'rawg'",
184 [],
185 |row| row.get(0),
186 )
187 .unwrap_or(0);
188
189 let steam: i32 = conn
190 .query_row(
191 "SELECT COUNT(*) FROM api_cache WHERE source = 'steam'",
192 [],
193 |row| row.get(0),
194 )
195 .unwrap_or(0);
196
197 let now = current_timestamp();
198 let rawg_cutoff = now - (CACHE_RAWG_GAME_TTL_DAYS * 24 * 60 * 60);
199 let store_cutoff = now - (CACHE_STEAM_STORE_TTL_DAYS * 24 * 60 * 60);
200 let reviews_cutoff = now - (CACHE_STEAM_REVIEWS_TTL_DAYS * 24 * 60 * 60);
201 let playtime_cutoff = now - (CACHE_STEAM_PLAYTIME_TTL_DAYS * 24 * 60 * 60);
202
203 let expired: i32 = conn
204 .query_row(
205 "SELECT COUNT(*) FROM api_cache
206 WHERE (source = 'rawg' AND external_id LIKE 'search_%' AND updated_at < ?1)
207 OR (source = 'steam' AND external_id LIKE 'store_%' AND updated_at < ?2)
208 OR (source = 'steam' AND external_id LIKE 'reviews_%' AND updated_at < ?3)
209 OR (source = 'steam' AND external_id LIKE 'playtime_%' AND updated_at < ?4)",
210 params![rawg_cutoff, store_cutoff, reviews_cutoff, playtime_cutoff],
211 |row| row.get(0),
212 )
213 .unwrap_or(0);
214
215 Ok(CacheStats {
216 total_entries: total,
217 rawg_entries: rawg,
218 steam_entries: steam,
219 expired_entries: expired,
220 })
221}