# Refactoring Roadmap Этот документ содержит список технического долга и планов по рефакторингу кодовой базы. ## Приоритет 1: Критичные улучшения ### 1. Схлопнуть состояния чата в enum **Проблема**: Сейчас состояния чата хранятся как отдельные boolean поля в `App`: ```rust is_message_selection_mode: bool, is_editing_mode: bool, is_reply_mode: bool, is_forward_mode: bool, is_delete_confirmation: bool, is_reaction_picker_mode: bool, is_profile_mode: bool, is_search_in_chat_mode: bool, ``` **Решение**: Создать enum `ChatState`: ```rust enum ChatState { Normal, MessageSelection { selected_message_id: i64, }, Editing { message_id: i64, original_text: String, }, Reply { message_id: i64, preview_text: String, }, Forward { message_id: i64, selected_chat_index: usize, }, DeleteConfirmation { message_id: i64, }, ReactionPicker { message_id: i64, available_reactions: Vec, selected_index: usize, }, Profile { info: ProfileInfo, }, SearchInChat { query: String, results: Vec, current_index: usize, }, } ``` **Преимущества**: - Невозможно иметь несколько состояний одновременно (type-safe) - Проще обрабатывать переходы между состояниями - Меньше полей в `App` - Данные, связанные с состоянием, хранятся вместе с ним **Затронутые файлы**: - `src/app/mod.rs` (добавить enum, убрать boolean поля) - `src/input/main_input.rs` (изменить логику обработки на match) - `src/ui/messages.rs` (изменить рендеринг на match) --- ### 2. Разделить TdClient на несколько модулей **Проблема**: `TdClient` в `src/tdlib/client.rs` (~1500+ строк) делает слишком много: - Авторизация - Управление чатами - Управление сообщениями - Кеширование пользователей - Реакции - Network state **Решение**: Разделить на модули: ``` src/tdlib/ ├── mod.rs # Экспорт публичных типов ├── client.rs # Основной TdClient ├── auth.rs # AuthManager ├── chats.rs # ChatManager ├── messages.rs # MessageManager ├── users.rs # UserCache └── reactions.rs # ReactionManager ``` **Преимущества**: - Принцип единственной ответственности - Проще тестировать отдельные модули - Легче найти и изменить код --- ### 3. Вынести константы в отдельный модуль **Проблема**: Магические числа разбросаны по всему коду: ```rust // В разных местах: 500 // MAX_MESSAGES_IN_CHAT 500 // MAX_USER_CACHE_SIZE 200 // MAX_CHATS 8 // Emoji picker columns 10 // Max input height 16 // Poll timeout (60 FPS) ``` **Решение**: Создать `src/constants.rs`: ```rust // Memory limits pub const MAX_MESSAGES_IN_CHAT: usize = 500; pub const MAX_USER_CACHE_SIZE: usize = 500; pub const MAX_CHATS: usize = 200; pub const MAX_CHAT_USER_IDS: usize = 500; // UI constants pub const EMOJI_PICKER_COLUMNS: usize = 8; pub const EMOJI_PICKER_ROWS: usize = 6; pub const MAX_INPUT_HEIGHT: usize = 10; pub const MIN_TERMINAL_WIDTH: u16 = 80; pub const MIN_TERMINAL_HEIGHT: u16 = 20; // Performance pub const POLL_TIMEOUT_MS: u64 = 16; // 60 FPS pub const SHUTDOWN_TIMEOUT_SECS: u64 = 2; pub const LAZY_LOAD_USERS_PER_TICK: usize = 5; // TDLib pub const TDLIB_CHAT_LIMIT: i32 = 50; pub const TDLIB_MESSAGE_LIMIT: i32 = 50; ``` **Преимущества**: - Единое место для всех констант - Проще изменить значения - Самодокументирующийся код --- ## Приоритет 2: Улучшение типобезопасности ### 4. Newtype pattern для ID ✅ ЗАВЕРШЕНО! **Статус**: ЗАВЕРШЕНО (2026-01-31) **Проблема**: Везде используется `i64` для `chat_id`, `message_id`, `user_id` — легко перепутать. **Решение**: ✅ Реализовано в `src/types.rs`: ```rust #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct ChatId(pub i64); impl ChatId { pub fn new(id: i64) -> Self { Self(id) } pub fn as_i64(&self) -> i64 { self.0 } } impl From for ChatId { fn from(id: i64) -> Self { ChatId(id) } } impl Display for ChatId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } // Аналогично для MessageId и UserId ``` **Что сделано**: - ✅ Создан `src/types.rs` с тремя типами: `ChatId`, `MessageId`, `UserId` - ✅ Добавлены методы `new()`, `as_i64()`, `From`, `Display` - ✅ Реализованы traits: `Hash`, `Eq`, `Serialize`, `Deserialize` - ✅ Обновлены 15+ модулей: - `tdlib/types.rs`, `tdlib/chats.rs`, `tdlib/messages.rs`, `tdlib/users.rs` - `tdlib/reactions.rs`, `tdlib/client.rs` - `app/mod.rs`, `app/chat_state.rs`, `input/main_input.rs` - Test helpers: `app_builder.rs`, `test_data.rs` - ✅ Исправлены 53 ошибки компиляции - ✅ Код компилируется успешно **Преимущества**: - ✅ Невозможно случайно передать message_id вместо chat_id - ✅ Компилятор ловит ошибки на этапе компиляции - ✅ Улучшенная читаемость кода - ✅ Самодокументирующиеся типы --- ### 5. Создать enum для ошибок **Проблема**: Везде используется `Result` — теряется контекст ошибок. **Решение**: Создать `src/error.rs`: ```rust #[derive(Debug, thiserror::Error)] pub enum TeletuiError { #[error("TDLib error: {0}")] TdLib(String), #[error("Configuration error: {0}")] Config(String), #[error("Network error: {0}")] Network(String), #[error("Authentication error: {0}")] Auth(String), #[error("Invalid timezone format: {0}")] InvalidTimezone(String), #[error("Invalid color: {0}")] InvalidColor(String), #[error("IO error: {0}")] Io(#[from] std::io::Error), } pub type Result = std::result::Result; ``` **Зависимости**: `thiserror = "1.0"` **Преимущества**: - Типобезопасная обработка ошибок - Понятные сообщения об ошибках - Возможность pattern matching --- ### 6. Группировка полей MessageInfo ✅ ЗАВЕРШЕНО! **Статус**: ЗАВЕРШЕНО (2026-01-31) **Проблема**: `MessageInfo` имеет слишком много плоских полей (~15+). **Решение**: ✅ Реализовано - группировка в логические структуры: ```rust pub struct MessageInfo { pub metadata: MessageMetadata, pub content: MessageContent, pub state: MessageState, pub interactions: MessageInteractions, } pub struct MessageMetadata { pub id: MessageId, pub chat_id: ChatId, pub sender_id: UserId, pub date: i32, } pub struct MessageContent { pub text: String, pub formatted_text: Option, pub media_type: Option, } pub struct MessageState { pub is_outgoing: bool, pub is_edited: bool, pub is_pinned: bool, } pub struct MessageInteractions { pub reply_to_message_id: Option, pub forward_info: Option, pub reactions: Vec, pub read_count: i32, } ``` **Что сделано**: - ✅ Созданы 4 структуры: MessageMetadata, MessageContent, MessageState, MessageInteractions - ✅ Обновлена MessageInfo для использования новых структур - ✅ Добавлен конструктор MessageInfo::new() - ✅ Добавлены getter методы (id(), text(), sender_name(), и др.) - ✅ Обновлены 14 файлов (~200+ обращений): - ui/messages.rs: рендеринг (100+ изменений) - app/mod.rs: логика приложения - input/main_input.rs: обработка ввода - tdlib/client.rs: обработка updates - Все тестовые файлы - ✅ Код компилируется успешно **Преимущества**: - ✅ Логическая группировка данных - ✅ Проще добавлять новые поля - ✅ Улучшенная читаемость кода - ✅ Меньше параметров в конструкторах (используется new()) --- ### MessageBuilder pattern ✅ ЗАВЕРШЕНО! **Статус**: ЗАВЕРШЕНО (2026-01-31) **Проблема**: MessageInfo::new() принимает 14 параметров, что неудобно и подвержено ошибкам. **Решение**: ✅ Реализован MessageBuilder с fluent API: ```rust let message = MessageBuilder::new(MessageId::new(123)) .sender_name("Alice") .text("Hello, world!") .outgoing() .read() .build(); ``` **Что сделано**: - ✅ Создана структура MessageBuilder в tdlib/types.rs - ✅ Реализовано 16 методов fluent API: - Базовые: sender_name, text, entities, date, edit_date - Флаги: outgoing, incoming, read, unread, edited - Права: editable, deletable_for_self, deletable_for_all - Дополнительно: reply_to, forward_from, reactions, add_reaction - ✅ Обновлён convert_message() для использования builder - ✅ Добавлены 6 unit тестов - ✅ Код компилируется успешно **Преимущества**: - ✅ Более читабельный код - ✅ Самодокументирующийся API - ✅ Гибкость в установке опциональных полей - ✅ Проще поддерживать и расширять **🎉 Priority 2 ЗАВЕРШЁН НА 100%! 🎉** --- ## Приоритет 3: Архитектурные улучшения ### 7. Выделить UI компоненты ✅ ЗАВЕРШЕНО! **Статус**: ЗАВЕРШЕНО (5/5 компонентов, 2026-02-02) **Проблема**: Код рендеринга дублируется, сложно переиспользовать. **Решение**: ✅ Создано `src/ui/components/`: ``` src/ui/components/ ├── mod.rs ✅ ├── modal.rs ✅ (87 строк, полностью реализовано) ├── input_field.rs ✅ (54 строк, полностью реализовано) ├── message_bubble.rs ⚠️ (27 строк, placeholder, блокируется P3.8 и P3.9) ├── chat_list_item.rs ✅ (78 строк, полностью реализовано) └── emoji_picker.rs ✅ (112 строк, полностью реализовано) ``` **Что сделано**: - ✅ Создана структура модулей `src/ui/components/` - ✅ Реализовано 5 из 5 компонентов: - `modal.rs` — базовые модалки с центрированием (87 строк) - `input_field.rs` — текстовое поле с курсором (54 строки) - `chat_list_item.rs` — элемент списка чатов (78 строк) - `emoji_picker.rs` — picker реакций (112 строк) - `message_bubble.rs` — рендеринг сообщений (437 строк) ✅ **ЗАВЕРШЕНО 2026-02-02** - ✅ Все компоненты используются в UI - ✅ `messages.rs` использует `message_grouping` и компоненты **Преимущества**: - ✅ Переиспользуемые компоненты - ✅ Консистентный UI - ✅ Проще тестировать --- ### 8. Вынести форматирование в отдельный модуль **Проблема**: Markdown форматирование захардкожено в `messages.rs` (~200+ строк). **Решение**: Создать `src/formatting.rs`: ```rust pub struct FormattedSpan { pub text: String, pub style: Style, } pub fn format_text_entities( text: &str, entities: &[TextEntity], ) -> Vec { // Вся логика форматирования } ``` **Преимущества**: - Разделение ответственности - Можно тестировать отдельно - Переиспользование в других местах --- ### 9. Вынести логику группировки сообщений ✅ ЗАВЕРШЕНО! **Статус**: ЗАВЕРШЕНО (2026-01-31) **Проблема**: Логика группировки сообщений смешана с рендерингом в `messages.rs`. **Решение**: ✅ Создан `src/message_grouping.rs`: ```rust pub enum MessageGroup { DateSeparator(i32), SenderHeader { is_outgoing: bool, sender_name: String }, Message(MessageInfo), } pub fn group_messages(messages: &[MessageInfo]) -> Vec { // Логика группировки по дате и отправителю } ``` **Что сделано**: - ✅ Создан модуль `src/message_grouping.rs` (255 строк) - ✅ Реализован enum `MessageGroup` с тремя вариантами - ✅ Реализована функция `group_messages()` для группировки по дате и отправителю - ✅ Добавлена полная документация с примерами - ✅ Написано 5 unit тестов (все проходят) - ✅ Модуль добавлен в `src/lib.rs` - ✅ Код компилируется успешно **Преимущества**: - ✅ Чистое разделение логики и представления - ✅ Легче тестировать группировку (покрыто тестами) - ✅ Можно переиспользовать - ✅ Готово для интеграции в `messages.rs` --- ### 10. Hotkey mapping в конфиг ✅ ЗАВЕРШЕНО! **Статус**: ЗАВЕРШЕНО (2026-01-31) **Проблема**: Хоткеи захардкожены в коде, нельзя настроить. **Решение**: ✅ Добавлено в `config.toml`: ```toml [hotkeys] # Навигация (vim + русские + стрелки) up = ["k", "р", "Up"] down = ["j", "о", "Down"] left = ["h", "р", "Left"] right = ["l", "д", "Right"] # Действия (англ + русские) reply = ["r", "к"] forward = ["f", "а"] delete = ["d", "в", "Delete"] copy = ["y", "н"] react = ["e", "у"] profile = ["i", "ш"] ``` **Что сделано**: - ✅ Создана структура `HotkeysConfig` в `src/config.rs` - ✅ Добавлены поля для всех действий (10 hotkeys) - ✅ Реализован метод `matches(key: KeyCode, action: &str) -> bool` - ✅ Поддержка символьных клавиш (англ + русские) - ✅ Поддержка специальных клавиш (Up, Down, Left, Right, Delete, Enter, Esc) - ✅ Добавлены дефолтные значения для всех hotkeys - ✅ Написано 9 unit тестов (all passing ✅) - ✅ Добавлена полная rustdoc документация - ✅ Config::default() включает hotkeys **Примеры использования**: ```rust let config = Config::default(); // Проверяем английскую клавишу if config.hotkeys.matches(KeyCode::Char('r'), "reply") { // Начать ответ } // Проверяем русскую клавишу if config.hotkeys.matches(KeyCode::Char('к'), "reply") { // Начать ответ (та же логика) } // Проверяем стрелку if config.hotkeys.matches(KeyCode::Up, "up") { // Вверх по списку } ``` **Преимущества**: - ✅ Пользовательская настройка хоткеев через config.toml - ✅ Проще добавлять новые действия - ✅ Документация хоткеев в конфиге - ✅ Централизованное управление клавишами - ✅ Поддержка русской раскладки out of the box **🎉 Priority 3 ЗАВЕРШЁН НА 100%! 🎉** --- ## Приоритет 4: Качество кода ### 11. Добавить юнит-тесты ✅ ЗАВЕРШЕНО! **Статус**: ЗАВЕРШЕНО 100% (+106 строк тестов, 2026-02-01) **Что сделано**: - ✅ Добавлены 9 unit тестов в `src/utils.rs` (в секции `#[cfg(test)]`) - ✅ Покрыты все edge cases для форматирования времени - ✅ Тестирование приватных функций через публичный API - ✅ Все 54 unit теста проходят (было 45, +9 новых) **Добавленные тесты**: - `format_timestamp_with_tz` - положительный offset (+03:00) - `format_timestamp_with_tz` - отрицательный offset (-05:00) - `format_timestamp_with_tz` - нулевой offset (UTC) - `format_timestamp_with_tz` - переход через полночь - `format_timestamp_with_tz` - невалидный timezone (fallback) - `get_day` - расчет дня из timestamp - `get_day_grouping` - группировка сообщений по дням - `format_datetime` - полная дата и время с MSK - `parse_timezone_offset` - через публичный API (приватная функция) **Примеры**: ```rust #[test] fn test_format_timestamp_with_tz_positive_offset() { let timestamp = 1640000000; // 2021-12-20 11:33:20 UTC assert_eq!(format_timestamp_with_tz(timestamp, "+03:00"), "14:33"); } #[test] fn test_get_day_grouping() { let msg1 = 1640000000; // 2021-12-20 09:33:20 let msg2 = 1640040000; // 2021-12-20 20:40:00 assert_eq!(get_day(msg1), get_day(msg2)); // Один день } ``` **Запуск**: `cargo test --lib utils::tests` --- ### 12. Добавить rustdoc комментарии ✅ ЗАВЕРШЕНО! **Статус**: ЗАВЕРШЕНО 100% (+900 строк документации, 2026-02-01) **Что сделано**: - ✅ Документированы все TDLib модули (auth, chats, messages, reactions, users) - ✅ Документированы все публичные структуры и методы - ✅ Добавлены примеры использования (34 doctests) - ✅ Документация для Config и утилит (formatting) - ✅ Все doctests работают (30 ignored для async, 4 compiled) **Модули с документацией**: - `src/tdlib/auth.rs` - AuthManager, AuthState (6 doctests) - `src/tdlib/chats.rs` - ChatManager (8 doctests) - `src/tdlib/messages.rs` - MessageManager, 14 методов (6 doctests) - `src/tdlib/reactions.rs` - ReactionManager (3 doctests) - `src/tdlib/users.rs` - UserCache, LruCache (2 doctests) - `src/config.rs` - Config, ColorsConfig, GeneralConfig (4 doctests) - `src/formatting.rs` - Форматирование текста (2 doctests) - `src/tdlib/client.rs` - TdClient (1 doctest) - `src/app/mod.rs` - App (1 doctest) - `src/message_grouping.rs` - Группировка (1 doctest) - `src/tdlib/types.rs` - MessageBuilder (1 doctest) **Примеры**: ```rust /// Менеджер авторизации TDLib. /// /// # Examples /// /// ```ignore /// let mut auth_manager = AuthManager::new(client_id); /// auth_manager.send_phone_number("+1234567890".to_string()).await?; /// auth_manager.send_code("12345".to_string()).await?; /// ``` pub struct AuthManager { ... } ``` **Генерация**: `cargo doc --open` --- ### 13. Config валидация ✅ ЗАВЕРШЕНО! **Статус**: ЗАВЕРШЕНО 100% (+149 строк тестов, 2026-02-01) **Что сделано**: - ✅ Валидация уже была реализована в `config.rs:344-389` - ✅ Вызов валидации в `Config::load():450-456` - ✅ Добавлено 15 comprehensive тестов для полного покрытия - ✅ Все 23 config теста проходят (8 существующих + 15 новых) **Добавленные тесты**: - Валидация дефолтного конфига - Timezone: валидный (+03:00, -05:00), невалидный (без знака) - Цвета: все 18 стандартных ratatui цветов - Невалидные цвета (rainbow, purple, pink) - Case-insensitive парсинг (RED, Green, YELLOW) - parse_color() для всех вариантов (standard, light, gray/grey) - Fallback к White для невалидных цветов **Реализация**: Уже была добавлена ранее: ```rust impl Config { pub fn validate(&self) -> Result<(), TeletuiError> { // Проверка timezone if !self.general.timezone.starts_with('+') && !self.general.timezone.starts_with('-') { return Err(TeletuiError::InvalidTimezone( format!("Timezone must start with + or -: {}", self.general.timezone) )); } // Проверка цветов let valid_colors = [ "black", "red", "green", "yellow", "blue", "magenta", "cyan", "gray", "white", "darkgray", "lightred", "lightgreen", "lightyellow", "lightblue", "lightmagenta", "lightcyan" ]; for color_name in [ &self.colors.incoming_message, &self.colors.outgoing_message, &self.colors.selected_message, &self.colors.reaction_chosen, &self.colors.reaction_other, ] { if !valid_colors.contains(&color_name.to_lowercase().as_str()) { return Err(TeletuiError::InvalidColor( format!("Unknown color: {}", color_name) )); } } Ok(()) } } ``` Вызывать при загрузке: ```rust pub fn load() -> Self { let config = // ... загрузка из файла if let Err(e) = config.validate() { eprintln!("Config validation error: {}", e); return Self::default(); } config } ``` --- ### 14. Async/await консистентность ✅ ЗАВЕРШЕНО! **Статус**: ЗАВЕРШЕНО 100% (проверка кода, 2026-02-01) **Проверка показала**: Код уже соответствует требованиям! **Что проверено**: - ✅ `std::fs` используется только в `Config::load()` при старте (не в async runtime) - ✅ `std::thread::sleep` - не найдено ни разу - ✅ `tokio::time::sleep` используется в async функциях (messages.rs) - ✅ `tokio::time::timeout` используется (auth.rs, main_input.rs, main.rs) - ✅ Все файловые операции вызываются синхронно при инициализации **Детали**: ```rust // ✓ ПРАВИЛЬНО: Config::load() при старте, перед async runtime #[tokio::main] async fn main() -> Result<(), io::Error> { let config = config::Config::load(); // Синхронно, при инициализации // ... async runtime начинается позже } // ✓ ПРАВИЛЬНО: tokio::time::sleep в async функциях async fn load_messages() { use tokio::time::{sleep, Duration}; sleep(Duration::from_millis(100)).await; // Не блокирует } ``` **Вывод**: Блокирующих вызовов в async контексте нет. Код async-clean. --- ## Приоритет 5: Опциональные улучшения ### 15. Feature flags для зависимостей ✅ ЗАВЕРШЕНО **Проблема**: Все зависимости всегда включены. **Решение**: В `Cargo.toml`: ```toml [features] default = ["clipboard", "url-open"] clipboard = ["dep:arboard"] url-open = ["dep:open"] [dependencies] arboard = { version = "3.4", optional = true } open = { version = "5.0", optional = true } ``` **Реализовано**: - ✅ Добавлены feature flags в Cargo.toml - ✅ Зависимости `arboard` и `open` сделаны опциональными - ✅ Условная компиляция в `src/input/main_input.rs`: - `#[cfg(feature = "url-open")]` для `open::that()` - `#[cfg(feature = "clipboard")]` / `#[cfg(not(feature = "clipboard"))]` для `copy_to_clipboard()` - ✅ Условная компиляция в `tests/copy.rs`: - `#[cfg(all(test, feature = "clipboard"))]` для clipboard тестов **Преимущества**: - ✅ Уменьшение размера бинарника - ✅ Опциональная функциональность - ✅ Graceful degradation при отключении фич --- ### 16. LRU cache обобщение ✅ ЗАВЕРШЕНО **Проблема**: Отдельные LRU кеши для `user_names` и `user_statuses`. **Решение**: Создать обобщённый `LruCache` или использовать готовый крейт `lru = "0.12"`. **Реализовано**: - ✅ Обобщённая структура `LruCache` в `src/tdlib/users.rs` - ✅ Type parameters: - `K: Eq + Hash + Clone + Copy` — тип ключа - `V: Clone` — тип значения - ✅ Обновлена `UserCache`: - `user_usernames: LruCache` - `user_names: LruCache` - `user_statuses: LruCache` - ✅ Все методы обобщены: `get()`, `peek()`, `insert()`, `contains_key()`, `len()` **Преимущества**: - ✅ Переиспользуемая реализация для любых типов ключей - ✅ Type-safe кеширование - ✅ Без дополнительных зависимостей --- ### 17. Tracing вместо println! ✅ ЗАВЕРШЕНО **Проблема**: Используется `eprintln!` для логов. **Решение**: Использовать `tracing`: ```rust use tracing::{info, warn, error, debug}; // Вместо eprintln!("Warning: Could not load config: {}", e); // Использовать warn!("Could not load config: {}", e); ``` **Реализовано**: - ✅ Добавлены зависимости в `Cargo.toml`: - `tracing = "0.1"` - `tracing-subscriber = { version = "0.3", features = ["env-filter"] }` - ✅ Инициализирован subscriber в `main.rs`: - Уровень логов по умолчанию: `warn` - Настраивается через переменную окружения `RUST_LOG` - ✅ Заменены все `eprintln!` на tracing макросы в `src/config.rs`: - 4× `warn!()` для предупреждений - 1× `error!()` для ошибок валидации - 1× `warn!()` для fallback на дефолтную конфигурацию **Преимущества**: - ✅ Структурированное логирование - ✅ Настраиваемые уровни логов (через `RUST_LOG`) - ✅ Лучшая интеграция с async кодом - ✅ Единый подход к логированию во всём проекте --- ## Метрики прогресса - [x] Priority 1: 3/3 задач ✅ ЗАВЕРШЕНО! - [x] P1.1 — ChatState enum - [x] P1.2 — Разделить TdClient - [x] P1.3 — Константы - [x] Priority 2: 5/5 задач ✅ ЗАВЕРШЕНО! 🎉 - [x] P2.5 — Error enum - [x] P2.3 — Config validation - [x] P2.4 — Newtype для ID - [x] P2.6 — MessageInfo реструктуризация - [x] P2.7 — MessageBuilder pattern - [x] Priority 3: 4/4 задач ✅ ЗАВЕРШЕНО! 🎉🎉 - [x] P3.7 — UI компоненты (5/5) ✅ ПОЛНОСТЬЮ ЗАВЕРШЕНО 2026-02-02! - [x] P3.8 — Formatting модуль ✅ - [x] P3.9 — Message Grouping ✅ - [x] P3.10 — Hotkey Mapping ✅ - [x] Priority 4: 4/4 задач ✅ ЗАВЕРШЕНО! 🎉🎉🎉 - [x] P4.11 — Unit tests ✅ - [x] P4.12 — Rustdoc ✅ - [x] P4.13 — Config validation ✅ - [x] P4.14 — Async/await consistency ✅ - [x] Priority 5: 3/3 задач ✅ ЗАВЕРШЕНО! 🎉🎉🎉 - [x] P5.15 — Feature flags ✅ - [x] P5.16 — LRU cache обобщение ✅ - [x] P5.17 — Tracing ✅ - [x] Priority 6: 1/1 задач ✅ ЗАВЕРШЕНО! 🎉🎉🎉🎉 - [x] P6.1 — Dependency Injection для TdClient (ВСЕ 8 этапов завершены!) **Всего**: 21/21 задач (100%) 🎊🎉 РЕФАКТОРИНГ ПОЛНОСТЬЮ ЗАВЕРШЁН! --- ## Предусловие: Тесты **ВАЖНО**: Перед началом рефакторинга необходимо написать тесты! См. [TESTING_ROADMAP.md](TESTING_ROADMAP.md) для плана покрытия тестами. Минимальное покрытие для начала рефакторинга: - ✅ Фаза 0: Инфраструктура (helpers, fake client) - ✅ Snapshot тесты для основных экранов (chat list, messages) - ✅ Integration тесты для критичных flow (send, edit, navigation) **Зачем**: Тесты гарантируют, что рефакторинг не сломает функциональность. --- ## Порядок выполнения Рекомендуется выполнять в следующем порядке: 1. **P1.3** — Константы (быстро, малый риск) 2. **P1.1** — ChatState enum (высокий impact) 3. **P2.5** — Error enum (улучшает весь код) 4. **P4.11** — Тесты для utils (базовая проверка) 5. **P1.2** — Разделить TdClient (большой рефакторинг) 6. **P2.4** — Newtype для ID (широкие изменения) 7. **P3.7** — UI компоненты (постепенно) 8. **P3.8** — Форматирование (изоляция логики) 9. **P3.9** — Группировка сообщений (изоляция логики) 10. Остальные по необходимости --- ## Принципы рефакторинга 1. **Один PR = одна задача** — не смешивать рефакторинг разных областей 2. **Тесты прежде всего** — добавить тесты перед рефакторингом 3. **Обратная совместимость** — сохранять работоспособность на каждом шаге 4. **Маленькие шаги** — лучше 10 маленьких PR, чем 1 огромный 5. **Документация** — обновлять документацию после изменений --- ## Приоритет 6: Улучшение тестируемости ### P6.1 — Dependency Injection для TdClient ✅ ЗАВЕРШЕНО! **Статус**: ✅ ЗАВЕРШЕНО (ВСЕ 8 этапов завершены!) - 2026-02-02 **Проблема**: В текущей реализации тесты создают **настоящий** `TdClient`, который вызывает `tdlib_rs::create_client()`. Это приводит к: 1. **Зависанию тестов** — TDLib не инициализирован и блокирует async вызовы 2. **Verbose логи** — TDLib выводит много логов при создании клиента 3. **Медленные тесты** — создание TDLib клиента занимает время 4. **Хаки в продакшн коде** — пришлось добавить `tokio::time::timeout(100ms)` для всех вызовов TDLib чтобы тесты не зависали **Проблемные места** (src/input/main_input.rs): ```rust // Строка 867-870: timeout для send_chat_action при вводе символов let _ = tokio::time::timeout( Duration::from_millis(100), app.td_client.send_chat_action(ChatId::new(chat_id), ChatAction::Typing) ).await; // Строка 683-686: timeout для set_draft_message при закрытии чата let _ = tokio::time::timeout( Duration::from_millis(100), app.td_client.set_draft_message(chat_id, draft_text) ).await; // Строка 592-594: timeout для send_chat_action Cancel при отправке сообщения let _ = tokio::time::timeout( Duration::from_millis(100), app.td_client.send_chat_action(ChatId::new(chat_id), ChatAction::Cancel) ).await; ``` **Решения**: #### Вариант 1: Trait-based Dependency Injection (рекомендуется) Создать trait `TdClientTrait` и сделать `App` generic: ```rust // src/tdlib/trait.rs #[async_trait] pub trait TdClientTrait { async fn send_chat_action(&self, chat_id: ChatId, action: ChatAction); async fn set_draft_message(&self, chat_id: ChatId, text: String) -> Result<()>; async fn get_chat_history(&mut self, chat_id: ChatId, limit: i32) -> Result>; async fn send_message(&mut self, chat_id: ChatId, text: String, reply_to: Option, reply_info: Option) -> Result; async fn edit_message(&mut self, chat_id: ChatId, message_id: MessageId, text: String) -> Result; async fn delete_messages(&mut self, chat_id: ChatId, message_ids: Vec, revoke: bool) -> Result<()>; async fn forward_messages(&mut self, to_chat_id: ChatId, from_chat_id: ChatId, message_ids: Vec) -> Result<()>; async fn toggle_reaction(&self, chat_id: ChatId, message_id: MessageId, emoji: String) -> Result<()>; async fn get_message_available_reactions(&self, chat_id: ChatId, message_id: MessageId) -> Result>; async fn search_messages(&self, chat_id: ChatId, query: &str) -> Result>; async fn get_profile_info(&self, chat_id: ChatId) -> Result; async fn leave_chat(&self, chat_id: ChatId) -> Result<()>; async fn load_chats(&mut self, limit: usize) -> Result, String>; async fn load_folder_chats(&mut self, folder_id: i32, limit: usize) -> Result<(), String>; async fn get_pinned_messages(&self, chat_id: ChatId) -> Result, String>; async fn load_current_pinned_message(&mut self, chat_id: ChatId); async fn fetch_missing_reply_info(&mut self); // ... все остальные методы // Синхронные методы fn current_chat_messages(&self) -> &[MessageInfo]; fn current_chat_messages_mut(&mut self) -> &mut Vec; fn set_current_chat_id(&mut self, chat_id: Option); fn folders(&self) -> &[FolderInfo]; fn network_state(&self) -> NetworkState; fn typing_status(&self) -> Option<(i64, String)>; fn current_pinned_message(&self) -> Option<&MessageInfo>; fn push_message(&mut self, message: MessageInfo); fn set_typing_status(&mut self, status: Option<(i64, String)>); fn set_current_pinned_message(&mut self, message: Option); } // Real implementation #[async_trait] impl TdClientTrait for TdClient { // Реализация всех методов, делегируя к существующим async fn send_chat_action(&self, chat_id: ChatId, action: ChatAction) { self.send_chat_action(chat_id, action).await } // ... остальные методы } // Fake implementation для тестов #[async_trait] impl TdClientTrait for FakeTdClient { // Реализация для тестов (уже есть в tests/helpers/fake_tdclient.rs) async fn send_chat_action(&self, chat_id: ChatId, action: ChatAction) { self.chat_actions.lock().unwrap().push((chat_id.as_i64(), action.to_string())); } // ... остальные методы } // App становится generic pub struct App { pub td_client: T, pub config: Config, // ... остальные поля } impl App { pub fn new(config: Config, td_client: T) -> Self { // ... } // ... все остальные методы } // Специализация для продакшена impl App { pub fn new_default(config: Config) -> Self { Self::new(config, TdClient::new()) } } // TestAppBuilder для тестов impl TestAppBuilder { pub fn build(self) -> App { let td_client = FakeTdClient::new() .with_chats(self.chats) .with_messages(self.selected_chat_id.unwrap_or(0), self.messages); App::new(self.config, td_client) } } ``` **Плюсы**: - ✅ Чистая архитектура, настоящий dependency injection - ✅ Тесты не создают реальный TDLib — **быстрые и тихие** - ✅ Убираем timeout'ы из продакшн кода — **чистота** - ✅ Легко мокировать для unit-тестов - ✅ Соответствует принципам SOLID (Dependency Inversion) **Минусы**: - ❌ Большой рефакторинг (~50+ файлов) - ❌ Усложнение кода (generics везде: `App`, `handle_input`) - ❌ Потеря простоты для небольшого проекта - ❌ Нужна библиотека `async-trait` для async методов в trait **Затронутые файлы**: - `src/tdlib/trait.rs` (новый) — trait определение - `src/tdlib/client.rs` — impl TdClientTrait for TdClient - `src/tdlib/mod.rs` — экспорт trait - `src/app/mod.rs` — App - `src/input/main_input.rs` — функции становятся generic - `src/input/auth.rs` — функции становятся generic - `src/ui/*.rs` — функции рендеринга становятся generic - `src/main.rs` — использовать App - `tests/helpers/fake_tdclient.rs` — impl TdClientTrait for FakeTdClient - `tests/helpers/app_builder.rs` — build() возвращает App - Все интеграционные тесты (~15 файлов) **Оценка трудозатрат**: ~2-3 дня работы --- #### Вариант 2: Enum Dispatch (компромисс) ```rust // src/tdlib/wrapper.rs pub enum TdClientWrapper { Real(TdClient), Fake(FakeTdClient), } impl TdClientWrapper { async fn send_chat_action(&self, chat_id: ChatId, action: ChatAction) { match self { Self::Real(c) => c.send_chat_action(chat_id, action).await, Self::Fake(c) => c.send_chat_action(chat_id, action).await, } } // ... все остальные методы с match на обе ветки } // App использует wrapper pub struct App { pub td_client: TdClientWrapper, // ... } ``` **Плюсы**: - ✅ Меньше изменений чем trait (нет generics) - ✅ Тесты используют Fake - ✅ Проще понять чем trait + generics **Минусы**: - ❌ Всё равно много boilerplate (каждый метод требует match) - ❌ Runtime dispatch overhead (небольшой) - ❌ Не такой чистый как trait - ❌ В продакшене всегда Real, но проверка match всё равно есть **Затронутые файлы**: ~20-30 файлов (меньше чем Вариант 1) **Оценка трудозатрат**: ~1 день работы --- #### Вариант 3: Оставить как есть (текущее состояние) **Обоснование**: - Timeout'ы — это не "хак", а **защита от зависания UI** - Даже в продакшене UI не должен зависать если TDLib глючит - 100ms timeout на typing action и draft — нормально, это не критичные операции - Защищает от deadlock'ов и network issues - Простота важнее для небольшого проекта **Плюсы**: - ✅ Нет дополнительной работы - ✅ Код остаётся простым - ✅ Timeout'ы улучшают надёжность даже в продакшене - ✅ Тесты работают (хоть и создают TDLib) **Минусы**: - ⚠️ Verbose логи TDLib в тестах (можно игнорировать) - ⚠️ Тесты чуть медленнее (~0.1s на тест из-за инициализации TDLib) - ⚠️ Timeout'ы в продакшн коде (но это не обязательно плохо) --- **Рекомендация**: - **Для прототипа/MVP**: Вариант 3 (текущее состояние) ✅ - **Для production-ready проекта**: Вариант 1 (trait injection) ⭐ - **Для быстрого улучшения**: Вариант 2 (enum dispatch) **Финальное решение** (2026-02-02): Реализован **Вариант 1 (trait injection)** ✅🎉 После завершения всех 8 этапов рефакторинга: - ✅ Создан `TdClientTrait` с 40+ методами - ✅ Реализован trait для `TdClient` и `FakeTdClient` - ✅ `App` стал generic: `App` - ✅ Все UI и input handlers обновлены на generic - ✅ Тесты используют `FakeTdClient` (быстро, без логов TDLib) - ✅ Продакшн использует `TdClient` (реальный TDLib) - ✅ Timeout'ы убраны из продакшн кода - ✅ Исправлен stack overflow в 6 методах trait реализации - ✅ Все 196+ тестов проходят **Преимущества реализации**: - 🛡️ Чистая архитектура без timeout хаков - ⚡ Быстрые тесты (FakeTdClient работает мгновенно) - 📝 Нет verbose логов TDLib в тестах - 🔧 Type-safe dependency injection - 🎯 Легко добавлять новые реализации trait --- ## Примечания - Этот документ живой и будет обновляться - Новые пункты добавляются по мере обнаружения - После завершения задачи отмечать в метриках - При появлении блокеров — документировать в соответствующей секции