use crate::constants::{LAZY_LOAD_USERS_PER_TICK, MAX_CHAT_USER_IDS, MAX_USER_CACHE_SIZE}; use crate::types::{ChatId, UserId}; use std::collections::HashMap; use tdlib_rs::enums::{User, UserStatus}; use tdlib_rs::functions; use super::types::UserOnlineStatus; /// LRU (Least Recently Used) кэш с фиксированной ёмкостью. /// /// Автоматически удаляет самые давно использованные элементы при достижении лимита. /// Основан на HashMap для быстрого доступа и Vec для отслеживания порядка использования. /// /// # Type Parameters /// /// * `K` - Тип ключа (должен реализовывать `Eq + Hash + Clone + Copy`) /// * `V` - Тип значения (должен реализовывать `Clone`) /// /// # Examples /// /// ```ignore /// let mut cache = LruCache::::new(100); /// cache.insert(UserId::new(1), "Alice".to_string()); /// assert_eq!(cache.get(&UserId::new(1)), Some(&"Alice".to_string())); /// ``` pub struct LruCache { /// Хранилище ключ-значение. map: HashMap, /// Порядок доступа: последний элемент — самый недавно использованный. order: Vec, /// Максимальная ёмкость кэша. capacity: usize, } impl LruCache where K: Eq + std::hash::Hash + Clone + Copy, V: Clone, { /// Создает новый LRU кэш с заданной ёмкостью. pub fn new(capacity: usize) -> Self { Self { map: HashMap::with_capacity(capacity), order: Vec::with_capacity(capacity), capacity, } } /// Получает значение и обновляет порядок доступа (помечает как использованное). pub fn get(&mut self, key: &K) -> Option<&V> { if self.map.contains_key(key) { // Перемещаем ключ в конец (самый недавно использованный) self.order.retain(|k| k != key); self.order.push(*key); self.map.get(key) } else { None } } /// Получить значение без обновления порядка (для read-only доступа) pub fn peek(&self, key: &K) -> Option<&V> { self.map.get(key) } /// Вставить значение pub fn insert(&mut self, key: K, value: V) { if self.map.contains_key(&key) { // Обновляем существующее значение self.map.insert(key, value); self.order.retain(|k| *k != key); self.order.push(key); } else { // Если кэш полон, удаляем самый старый элемент if self.map.len() >= self.capacity { if let Some(oldest) = self.order.first().copied() { self.order.remove(0); self.map.remove(&oldest); } } self.map.insert(key, value); self.order.push(key); } } /// Проверить наличие ключа pub fn contains_key(&self, key: &K) -> bool { self.map.contains_key(key) } } /// Кэш информации о пользователях Telegram. /// /// Хранит данные пользователей (имена, usernames, статусы) в LRU-кэшах /// для быстрого доступа без повторных запросов к TDLib. /// /// # Возможности /// /// - Кэширование имен пользователей (first_name + last_name) /// - Кэширование usernames (@username) /// - Кэширование онлайн-статусов /// - Связь chat_id → user_id для приватных чатов /// - Ленивая загрузка данных пользователей порциями /// /// # Examples /// /// ```ignore /// let mut cache = UserCache::new(client_id); /// /// // Обработать обновление пользователя /// cache.handle_user_update(&user_enum); /// /// // Получить имя /// let name = cache.get_user_name(user_id).await; /// ``` pub struct UserCache { /// LRU-кэш usernames: user_id → username. pub user_usernames: LruCache, /// LRU-кэш имён: user_id → display_name (first_name + last_name). pub user_names: LruCache, /// Связь chat_id → user_id для приватных чатов. pub chat_user_ids: HashMap, /// Очередь user_id для ленивой загрузки имён. pub pending_user_ids: Vec, /// LRU-кэш онлайн-статусов: user_id → status. pub user_statuses: LruCache, /// ID клиента TDLib для API вызовов. client_id: i32, } impl UserCache { /// Создает новый кэш пользователей. /// /// # Arguments /// /// * `client_id` - ID клиента TDLib для API вызовов pub fn new(client_id: i32) -> Self { Self { user_usernames: LruCache::new(MAX_USER_CACHE_SIZE), user_names: LruCache::new(MAX_USER_CACHE_SIZE), chat_user_ids: HashMap::with_capacity(MAX_CHAT_USER_IDS), pending_user_ids: Vec::new(), user_statuses: LruCache::new(MAX_USER_CACHE_SIZE), client_id, } } /// Получить статус пользователя по chat_id pub fn get_status_by_chat_id(&self, chat_id: ChatId) -> Option<&UserOnlineStatus> { let user_id = self.chat_user_ids.get(&chat_id)?; self.user_statuses.peek(user_id) } /// Обрабатывает обновление пользователя от TDLib. /// /// Сохраняет username, имя и статус пользователя в соответствующие кэши. /// /// # Arguments /// /// * `user_enum` - Обновление пользователя от TDLib pub fn handle_user_update(&mut self, user_enum: &User) { let User::User(user) = user_enum; let user_id = user.id; // Сохраняем username if let Some(username) = user.usernames.as_ref().map(|u| u.editable_username.clone()) { self.user_usernames.insert(UserId::new(user_id), username); } // Сохраняем имя let display_name = format!("{} {}", user.first_name, user.last_name) .trim() .to_string(); self.user_names.insert(UserId::new(user_id), display_name); // Обновляем статус self.update_status(UserId::new(user_id), &user.status); } /// Обновляет онлайн-статус пользователя. /// /// # Arguments /// /// * `user_id` - ID пользователя /// * `status` - Новый статус от TDLib pub fn update_status(&mut self, user_id: UserId, status: &UserStatus) { let online_status = match status { UserStatus::Online(_) => UserOnlineStatus::Online, UserStatus::Recently(_) => UserOnlineStatus::Recently, UserStatus::LastWeek(_) => UserOnlineStatus::LastWeek, UserStatus::LastMonth(_) => UserOnlineStatus::LastMonth, UserStatus::Offline(s) => UserOnlineStatus::Offline(s.was_online), _ => return, }; self.user_statuses.insert(user_id, online_status); } /// Получает имя пользователя из кэша или загружает из TDLib. /// /// Сначала проверяет кэш, затем при необходимости загружает из API. /// /// # Arguments /// /// * `user_id` - ID пользователя /// /// # Returns /// /// Имя пользователя (first_name + last_name) или "User {id}" если не найден. #[allow(dead_code)] pub async fn get_user_name(&self, user_id: UserId) -> String { // Сначала пытаемся получить из кэша if let Some(name) = self.user_names.peek(&user_id) { return name.clone(); } // Загружаем пользователя match functions::get_user(user_id.as_i64(), self.client_id).await { Ok(User::User(user)) => { let name = format!("{} {}", user.first_name, user.last_name) .trim() .to_string(); name } _ => format!("User {}", user_id.as_i64()), } } /// Обрабатывает очередь отложенных user_ids для ленивой загрузки. /// /// Загружает данные пользователей небольшими порциями (по [`LAZY_LOAD_USERS_PER_TICK`]) /// для избежания блокировки UI. /// /// # Note /// /// Вызывайте периодически в основном цикле приложения. pub async fn process_pending_user_ids(&mut self) { if self.pending_user_ids.is_empty() { return; } // Берём первые N user_ids для загрузки let batch: Vec = self .pending_user_ids .drain(..self.pending_user_ids.len().min(LAZY_LOAD_USERS_PER_TICK)) .collect(); for user_id in batch { if self.user_names.contains_key(&user_id) { continue; // Уже в кэше } match functions::get_user(user_id.as_i64(), self.client_id).await { Ok(user_enum) => { self.handle_user_update(&user_enum); } Err(_) => { // Если не удалось загрузить, сохраняем placeholder self.user_names.insert(user_id, format!("User {}", user_id)); } } } } }