Skip to main content

vector_core/db/
id_cache.rs

1//! ID cache — maps chat identifiers and npubs to SQLite row IDs.
2//!
3//! All lookups are cached in memory after first DB hit. Caches are
4//! preloaded at boot and cleared on account switch.
5
6use std::collections::HashMap;
7use std::sync::{Arc, LazyLock, RwLock};
8
9static CHAT_ID_CACHE: LazyLock<Arc<RwLock<HashMap<String, i64>>>> =
10    LazyLock::new(|| Arc::new(RwLock::new(HashMap::new())));
11
12static USER_ID_CACHE: LazyLock<Arc<RwLock<HashMap<String, i64>>>> =
13    LazyLock::new(|| Arc::new(RwLock::new(HashMap::new())));
14
15/// Drop a chat's cached identifier→id mapping. Call after deleting a chat row so
16/// a later recreate doesn't reuse the stale (now-deleted) integer id.
17pub fn forget_chat_id(chat_identifier: &str) {
18    CHAT_ID_CACHE.write().unwrap().remove(chat_identifier);
19}
20
21/// Lookup-only: get integer chat ID from identifier. Errors if not found.
22pub fn get_chat_id_by_identifier(chat_identifier: &str) -> Result<i64, String> {
23    // Fast path: cache hit
24    {
25        let cache = CHAT_ID_CACHE.read().unwrap();
26        if let Some(&id) = cache.get(chat_identifier) {
27            return Ok(id);
28        }
29    }
30
31    // Cache miss: query DB
32    let conn = super::get_db_connection_guard_static()?;
33    let id: i64 = conn.query_row(
34        "SELECT id FROM chats WHERE chat_identifier = ?1",
35        rusqlite::params![chat_identifier],
36        |row| row.get(0)
37    ).map_err(|_| format!("Chat not found: {}", chat_identifier))?;
38
39    // Update cache
40    {
41        let mut cache = CHAT_ID_CACHE.write().unwrap();
42        cache.insert(chat_identifier.to_string(), id);
43    }
44
45    Ok(id)
46}
47
48/// Get or create integer chat ID from identifier.
49pub fn get_or_create_chat_id(chat_identifier: &str) -> Result<i64, String> {
50    // Fast path: cache hit
51    {
52        let cache = CHAT_ID_CACHE.read().unwrap();
53        if let Some(&id) = cache.get(chat_identifier) {
54            return Ok(id);
55        }
56    }
57
58    let conn = super::get_db_connection_guard_static()?;
59
60    // Try existing
61    let existing: Option<i64> = conn.query_row(
62        "SELECT id FROM chats WHERE chat_identifier = ?1",
63        rusqlite::params![chat_identifier],
64        |row| row.get(0)
65    ).ok();
66
67    let id = if let Some(id) = existing {
68        id
69    } else {
70        // Create stub chat entry. Discriminant must match ChatType::to_i32:
71        // 0 = DirectMessage (npub), 2 = Community (non-npub). Value 1 was the
72        // retired MlsGroup variant and is dropped by the get_all_chats load filter,
73        // so a non-npub stub MUST be 2 or the chat vanishes on reload.
74        let now = std::time::SystemTime::now()
75            .duration_since(std::time::UNIX_EPOCH).unwrap()
76            .as_secs() as i64;
77        let chat_type: i32 = if chat_identifier.starts_with("npub1") { 0 } else { 2 };
78
79        conn.execute(
80            "INSERT INTO chats (chat_identifier, chat_type, participants, created_at) VALUES (?1, ?2, '[]', ?3)",
81            rusqlite::params![chat_identifier, chat_type, now],
82        ).map_err(|e| format!("Failed to create chat stub: {}", e))?;
83
84        conn.last_insert_rowid()
85    };
86
87    // Update cache
88    {
89        let mut cache = CHAT_ID_CACHE.write().unwrap();
90        cache.insert(chat_identifier.to_string(), id);
91    }
92
93    Ok(id)
94}
95
96/// Get or create integer user ID from npub. Returns None for empty npub.
97pub fn get_or_create_user_id(npub: &str) -> Result<Option<i64>, String> {
98    if npub.is_empty() {
99        return Ok(None);
100    }
101
102    // Fast path: cache hit
103    {
104        let cache = USER_ID_CACHE.read().unwrap();
105        if let Some(&id) = cache.get(npub) {
106            return Ok(Some(id));
107        }
108    }
109
110    let conn = super::get_db_connection_guard_static()?;
111
112    let existing: Option<i64> = conn.query_row(
113        "SELECT id FROM profiles WHERE npub = ?1",
114        rusqlite::params![npub],
115        |row| row.get(0)
116    ).ok();
117
118    let id = if let Some(id) = existing {
119        id
120    } else {
121        conn.execute(
122            "INSERT INTO profiles (npub, name, display_name) VALUES (?1, '', '')",
123            rusqlite::params![npub],
124        ).map_err(|e| format!("Failed to create profile stub: {}", e))?;
125        conn.last_insert_rowid()
126    };
127
128    // Update cache
129    {
130        let mut cache = USER_ID_CACHE.write().unwrap();
131        cache.insert(npub.to_string(), id);
132    }
133
134    Ok(Some(id))
135}
136
137/// Preload all ID mappings into memory cache (call at boot).
138pub fn preload_id_caches() -> Result<(), String> {
139    let conn = match super::get_db_connection_guard_static() {
140        Ok(c) => c,
141        Err(_) => return Ok(()), // No DB yet, skip
142    };
143
144    // Load chat ID mappings
145    {
146        let mut stmt = conn.prepare("SELECT chat_identifier, id FROM chats")
147            .map_err(|e| format!("Failed to prepare chat query: {}", e))?;
148        let rows = stmt.query_map([], |row| {
149            Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
150        }).map_err(|e| format!("Failed to query chats: {}", e))?;
151
152        let mut cache = CHAT_ID_CACHE.write().unwrap();
153        for row in rows.flatten() {
154            cache.insert(row.0, row.1);
155        }
156    }
157
158    // Load user ID mappings
159    {
160        let mut stmt = conn.prepare("SELECT npub, id FROM profiles")
161            .map_err(|e| format!("Failed to prepare user query: {}", e))?;
162        let rows = stmt.query_map([], |row| {
163            Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
164        }).map_err(|e| format!("Failed to query profiles: {}", e))?;
165
166        let mut cache = USER_ID_CACHE.write().unwrap();
167        for row in rows.flatten() {
168            cache.insert(row.0, row.1);
169        }
170    }
171
172    Ok(())
173}
174
175/// Clear all ID caches (call on account switch).
176pub fn clear_id_caches() {
177    CHAT_ID_CACHE.write().unwrap().clear();
178    USER_ID_CACHE.write().unwrap().clear();
179}