1use std::sync::{Arc, Mutex};
2
3use async_trait::async_trait;
4use rusqlite::Connection;
5use synaptic_core::{ChatResponse, SynapticError};
6
7#[derive(Debug, Clone)]
9pub struct SqliteCacheConfig {
10 pub path: String,
12 pub ttl: Option<u64>,
15}
16
17impl SqliteCacheConfig {
18 pub fn new(path: impl Into<String>) -> Self {
20 Self {
21 path: path.into(),
22 ttl: None,
23 }
24 }
25
26 pub fn in_memory() -> Self {
28 Self {
29 path: ":memory:".to_string(),
30 ttl: None,
31 }
32 }
33
34 pub fn with_ttl(mut self, seconds: u64) -> Self {
36 self.ttl = Some(seconds);
37 self
38 }
39}
40
41pub struct SqliteCache {
47 conn: Arc<Mutex<Connection>>,
48 ttl: Option<u64>,
49}
50
51impl SqliteCache {
52 pub fn new(config: SqliteCacheConfig) -> Result<Self, SynapticError> {
57 let conn = Connection::open(&config.path)
58 .map_err(|e| SynapticError::Cache(format!("SQLite open error: {e}")))?;
59
60 conn.execute(
61 "CREATE TABLE IF NOT EXISTS llm_cache (
62 key TEXT PRIMARY KEY,
63 value TEXT NOT NULL,
64 created_at INTEGER NOT NULL DEFAULT (unixepoch())
65 )",
66 [],
67 )
68 .map_err(|e| SynapticError::Cache(format!("SQLite create table error: {e}")))?;
69
70 Ok(Self {
71 conn: Arc::new(Mutex::new(conn)),
72 ttl: config.ttl,
73 })
74 }
75}
76
77#[async_trait]
78impl synaptic_core::LlmCache for SqliteCache {
79 async fn get(&self, key: &str) -> Result<Option<ChatResponse>, SynapticError> {
80 let conn = self.conn.clone();
81 let ttl = self.ttl;
82 let key = key.to_string();
83
84 tokio::task::spawn_blocking(move || {
85 let conn = conn
86 .lock()
87 .map_err(|e| SynapticError::Cache(format!("lock error: {e}")))?;
88
89 let query = if ttl.is_some() {
90 "SELECT value FROM llm_cache WHERE key = ?1 AND created_at + ?2 > unixepoch()"
91 } else {
92 "SELECT value FROM llm_cache WHERE key = ?1"
93 };
94
95 let mut stmt = conn
96 .prepare(query)
97 .map_err(|e| SynapticError::Cache(format!("SQLite prepare error: {e}")))?;
98
99 let result = if let Some(ttl) = ttl {
100 stmt.query_row(rusqlite::params![key, ttl as i64], |row| {
101 row.get::<_, String>(0)
102 })
103 } else {
104 stmt.query_row(rusqlite::params![key], |row| row.get::<_, String>(0))
105 };
106
107 match result {
108 Ok(json_str) => {
109 let response: ChatResponse = serde_json::from_str(&json_str).map_err(|e| {
110 SynapticError::Cache(format!("JSON deserialize error: {e}"))
111 })?;
112 Ok(Some(response))
113 }
114 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
115 Err(e) => Err(SynapticError::Cache(format!("SQLite query error: {e}"))),
116 }
117 })
118 .await
119 .map_err(|e| SynapticError::Cache(format!("spawn_blocking error: {e}")))?
120 }
121
122 async fn put(&self, key: &str, response: &ChatResponse) -> Result<(), SynapticError> {
123 let conn = self.conn.clone();
124 let key = key.to_string();
125 let value = serde_json::to_string(response)
126 .map_err(|e| SynapticError::Cache(format!("JSON serialize error: {e}")))?;
127
128 tokio::task::spawn_blocking(move || {
129 let conn = conn
130 .lock()
131 .map_err(|e| SynapticError::Cache(format!("lock error: {e}")))?;
132
133 conn.execute(
134 "INSERT OR REPLACE INTO llm_cache (key, value, created_at) VALUES (?1, ?2, unixepoch())",
135 rusqlite::params![key, value],
136 )
137 .map_err(|e| SynapticError::Cache(format!("SQLite insert error: {e}")))?;
138
139 Ok(())
140 })
141 .await
142 .map_err(|e| SynapticError::Cache(format!("spawn_blocking error: {e}")))?
143 }
144
145 async fn clear(&self) -> Result<(), SynapticError> {
146 let conn = self.conn.clone();
147
148 tokio::task::spawn_blocking(move || {
149 let conn = conn
150 .lock()
151 .map_err(|e| SynapticError::Cache(format!("lock error: {e}")))?;
152
153 conn.execute("DELETE FROM llm_cache", [])
154 .map_err(|e| SynapticError::Cache(format!("SQLite delete error: {e}")))?;
155
156 Ok(())
157 })
158 .await
159 .map_err(|e| SynapticError::Cache(format!("spawn_blocking error: {e}")))?
160 }
161}