Files
telegram-tui/src/tdlib/users.rs
Mikhail Kilin 67739583f3 refactor: generalize LruCache to support any key type (P5.16)
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>
2026-02-01 02:26:03 +03:00

294 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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));
}
}
}
}
}