Made LruCache generic over key type K, not just UserId: - LruCache<V> → LruCache<K, V> - Added trait bounds: K: Eq + Hash + Clone + Copy - Updated UserCache field types: * user_usernames: LruCache<UserId, String> * user_names: LruCache<UserId, String> * user_statuses: LruCache<UserId, UserOnlineStatus> Benefits: - Reusable cache implementation for any key types - Type-safe caching - No additional dependencies Progress: Priority 5: 2/3 tasks, Total: 18/20 (90%) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
294 lines
11 KiB
Rust
294 lines
11 KiB
Rust
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::<UserId, String>::new(100);
|
||
/// cache.insert(UserId::new(1), "Alice".to_string());
|
||
/// assert_eq!(cache.get(&UserId::new(1)), Some(&"Alice".to_string()));
|
||
/// ```
|
||
pub struct LruCache<K, V> {
|
||
/// Хранилище ключ-значение.
|
||
map: HashMap<K, V>,
|
||
|
||
/// Порядок доступа: последний элемент — самый недавно использованный.
|
||
order: Vec<K>,
|
||
|
||
/// Максимальная ёмкость кэша.
|
||
capacity: usize,
|
||
}
|
||
|
||
impl<K, V> LruCache<K, V>
|
||
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)
|
||
}
|
||
|
||
/// Количество элементов
|
||
#[allow(dead_code)]
|
||
pub fn len(&self) -> usize {
|
||
self.map.len()
|
||
}
|
||
}
|
||
|
||
/// Кэш информации о пользователях 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<UserId, String>,
|
||
|
||
/// LRU-кэш имён: user_id → display_name (first_name + last_name).
|
||
pub user_names: LruCache<UserId, String>,
|
||
|
||
/// Связь chat_id → user_id для приватных чатов.
|
||
pub chat_user_ids: HashMap<ChatId, UserId>,
|
||
|
||
/// Очередь user_id для ленивой загрузки имён.
|
||
pub pending_user_ids: Vec<UserId>,
|
||
|
||
/// LRU-кэш онлайн-статусов: user_id → status.
|
||
pub user_statuses: LruCache<UserId, UserOnlineStatus>,
|
||
|
||
/// 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,
|
||
}
|
||
}
|
||
|
||
/// Получить username пользователя
|
||
pub fn get_username(&mut self, user_id: &UserId) -> Option<&String> {
|
||
self.user_usernames.get(user_id)
|
||
}
|
||
|
||
/// Получить имя пользователя
|
||
pub fn get_name(&mut self, user_id: &UserId) -> Option<&String> {
|
||
self.user_names.get(user_id)
|
||
}
|
||
|
||
/// Получить user_id по chat_id
|
||
pub fn get_user_id_by_chat(&self, chat_id: ChatId) -> Option<UserId> {
|
||
self.chat_user_ids.get(&chat_id).copied()
|
||
}
|
||
|
||
/// Получить статус пользователя по 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) {
|
||
if 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);
|
||
}
|
||
|
||
/// Сохранить связь chat_id -> user_id
|
||
pub fn register_private_chat(&mut self, chat_id: ChatId, user_id: UserId) {
|
||
self.chat_user_ids.insert(chat_id, user_id);
|
||
}
|
||
|
||
/// Получает имя пользователя из кэша или загружает из TDLib.
|
||
///
|
||
/// Сначала проверяет кэш, затем при необходимости загружает из API.
|
||
///
|
||
/// # Arguments
|
||
///
|
||
/// * `user_id` - ID пользователя
|
||
///
|
||
/// # Returns
|
||
///
|
||
/// Имя пользователя (first_name + last_name) или "User {id}" если не найден.
|
||
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<UserId> = 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));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|