Extracted duplicate code and unified timeout handling across the codebase. Changes: - Extracted open_chat_and_load_data() function (eliminates 52 lines of duplication) - Replaced manual y/н/Enter handling with handle_yes_no() from modal_handler (2 places) - Replaced 7 direct tokio::time::timeout calls with retry utils (auth, main_input, main) - Added with_timeout_ignore() for non-critical operations - Fixed modal_handler.rs bug: corrected Russian 'y' key (д → н) - Removed unused imports in handlers/mod.rs and utils/mod.rs Impact: - main_input.rs: 1164 → 958 lines (-206 lines, -18%) - Code duplication: 52 lines eliminated - Direct timeout calls: 7 → 1 (-86%) - DRY principle applied throughout Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
43 KiB
Refactoring Roadmap
Этот документ содержит список технического долга и планов по рефакторингу кодовой базы.
Приоритет 1: Критичные улучшения
1. Схлопнуть состояния чата в enum
Проблема: Сейчас состояния чата хранятся как отдельные boolean поля в App:
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:
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<String>,
selected_index: usize,
},
Profile {
info: ProfileInfo,
},
SearchInChat {
query: String,
results: Vec<i64>,
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. Вынести константы в отдельный модуль
Проблема: Магические числа разбросаны по всему коду:
// В разных местах:
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:
// 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:
#[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<i64> 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<i64>,Display - ✅ Реализованы traits:
Hash,Eq,Serialize,Deserialize - ✅ Обновлены 15+ модулей:
tdlib/types.rs,tdlib/chats.rs,tdlib/messages.rs,tdlib/users.rstdlib/reactions.rs,tdlib/client.rsapp/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<T, String> — теряется контекст ошибок.
Решение: Создать src/error.rs:
#[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<T> = std::result::Result<T, TeletuiError>;
Зависимости: thiserror = "1.0"
Преимущества:
- Типобезопасная обработка ошибок
- Понятные сообщения об ошибках
- Возможность pattern matching
6. Группировка полей MessageInfo ✅ ЗАВЕРШЕНО!
Статус: ЗАВЕРШЕНО (2026-01-31)
Проблема: MessageInfo имеет слишком много плоских полей (~15+).
Решение: ✅ Реализовано - группировка в логические структуры:
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<FormattedText>,
pub media_type: Option<String>,
}
pub struct MessageState {
pub is_outgoing: bool,
pub is_edited: bool,
pub is_pinned: bool,
}
pub struct MessageInteractions {
pub reply_to_message_id: Option<MessageId>,
pub forward_info: Option<ForwardInfo>,
pub reactions: Vec<ReactionInfo>,
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:
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:
pub struct FormattedSpan {
pub text: String,
pub style: Style,
}
pub fn format_text_entities(
text: &str,
entities: &[TextEntity],
) -> Vec<FormattedSpan> {
// Вся логика форматирования
}
Преимущества:
- Разделение ответственности
- Можно тестировать отдельно
- Переиспользование в других местах
9. Вынести логику группировки сообщений ✅ ЗАВЕРШЕНО!
Статус: ЗАВЕРШЕНО (2026-01-31)
Проблема: Логика группировки сообщений смешана с рендерингом в messages.rs.
Решение: ✅ Создан src/message_grouping.rs:
pub enum MessageGroup {
DateSeparator(i32),
SenderHeader { is_outgoing: bool, sender_name: String },
Message(MessageInfo),
}
pub fn group_messages(messages: &[MessageInfo]) -> Vec<MessageGroup> {
// Логика группировки по дате и отправителю
}
Что сделано:
- ✅ Создан модуль
src/message_grouping.rs(255 строк) - ✅ Реализован enum
MessageGroupс тремя вариантами - ✅ Реализована функция
group_messages()для группировки по дате и отправителю - ✅ Добавлена полная документация с примерами
- ✅ Написано 5 unit тестов (все проходят)
- ✅ Модуль добавлен в
src/lib.rs - ✅ Код компилируется успешно
Преимущества:
- ✅ Чистое разделение логики и представления
- ✅ Легче тестировать группировку (покрыто тестами)
- ✅ Можно переиспользовать
- ✅ Готово для интеграции в
messages.rs
10. Hotkey mapping в конфиг ✅ ЗАВЕРШЕНО!
Статус: ЗАВЕРШЕНО (2026-01-31)
Проблема: Хоткеи захардкожены в коде, нельзя настроить.
Решение: ✅ Добавлено в config.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
Примеры использования:
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- расчет дня из timestampget_day_grouping- группировка сообщений по днямformat_datetime- полная дата и время с MSKparse_timezone_offset- через публичный API (приватная функция)
Примеры:
#[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)
Примеры:
/// Менеджер авторизации 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 для невалидных цветов
Реализация: Уже была добавлена ранее:
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(())
}
}
Вызывать при загрузке:
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) - ✅ Все файловые операции вызываются синхронно при инициализации
Детали:
// ✓ ПРАВИЛЬНО: 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:
[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<K, V> или использовать готовый крейт lru = "0.12".
Реализовано:
- ✅ Обобщённая структура
LruCache<K, V>вsrc/tdlib/users.rs - ✅ Type parameters:
K: Eq + Hash + Clone + Copy— тип ключаV: Clone— тип значения
- ✅ Обновлена
UserCache:user_usernames: LruCache<UserId, String>user_names: LruCache<UserId, String>user_statuses: LruCache<UserId, UserOnlineStatus>
- ✅ Все методы обобщены:
get(),peek(),insert(),contains_key(),len()
Преимущества:
- ✅ Переиспользуемая реализация для любых типов ключей
- ✅ Type-safe кеширование
- ✅ Без дополнительных зависимостей
17. Tracing вместо println! ✅ ЗАВЕРШЕНО
Проблема: Используется eprintln! для логов.
Решение: Использовать tracing:
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 на дефолтную конфигурацию
- 4×
Преимущества:
- ✅ Структурированное логирование
- ✅ Настраиваемые уровни логов (через
RUST_LOG) - ✅ Лучшая интеграция с async кодом
- ✅ Единый подход к логированию во всём проекте
Метрики прогресса
- Priority 1: 3/3 задач ✅ ЗАВЕРШЕНО!
- P1.1 — ChatState enum
- P1.2 — Разделить TdClient
- P1.3 — Константы
- Priority 2: 5/5 задач ✅ ЗАВЕРШЕНО! 🎉
- P2.5 — Error enum
- P2.3 — Config validation
- P2.4 — Newtype для ID
- P2.6 — MessageInfo реструктуризация
- P2.7 — MessageBuilder pattern
- Priority 3: 4/4 задач ✅ ЗАВЕРШЕНО! 🎉🎉
- P3.7 — UI компоненты (5/5) ✅ ПОЛНОСТЬЮ ЗАВЕРШЕНО 2026-02-02!
- P3.8 — Formatting модуль ✅
- P3.9 — Message Grouping ✅
- P3.10 — Hotkey Mapping ✅
- Priority 4: 4/4 задач ✅ ЗАВЕРШЕНО! 🎉🎉🎉
- P4.11 — Unit tests ✅
- P4.12 — Rustdoc ✅
- P4.13 — Config validation ✅
- P4.14 — Async/await consistency ✅
- Priority 5: 3/3 задач ✅ ЗАВЕРШЕНО! 🎉🎉🎉
- P5.15 — Feature flags ✅
- P5.16 — LRU cache обобщение ✅
- P5.17 — Tracing ✅
- Priority 6: 1/1 задач ✅ ЗАВЕРШЕНО! 🎉🎉🎉🎉
- P6.1 — Dependency Injection для TdClient (ВСЕ 8 этапов завершены!)
Всего: 21/21 задач (100%) 🎊🎉 РЕФАКТОРИНГ ПОЛНОСТЬЮ ЗАВЕРШЁН!
Предусловие: Тесты
ВАЖНО: Перед началом рефакторинга необходимо написать тесты!
См. TESTING_ROADMAP.md для плана покрытия тестами.
Минимальное покрытие для начала рефакторинга:
- ✅ Фаза 0: Инфраструктура (helpers, fake client)
- ✅ Snapshot тесты для основных экранов (chat list, messages)
- ✅ Integration тесты для критичных flow (send, edit, navigation)
Зачем: Тесты гарантируют, что рефакторинг не сломает функциональность.
Порядок выполнения
Рекомендуется выполнять в следующем порядке:
- P1.3 — Константы (быстро, малый риск)
- P1.1 — ChatState enum (высокий impact)
- P2.5 — Error enum (улучшает весь код)
- P4.11 — Тесты для utils (базовая проверка)
- P1.2 — Разделить TdClient (большой рефакторинг)
- P2.4 — Newtype для ID (широкие изменения)
- P3.7 — UI компоненты (постепенно)
- P3.8 — Форматирование (изоляция логики)
- P3.9 — Группировка сообщений (изоляция логики)
- Остальные по необходимости
Принципы рефакторинга
- Один PR = одна задача — не смешивать рефакторинг разных областей
- Тесты прежде всего — добавить тесты перед рефакторингом
- Обратная совместимость — сохранять работоспособность на каждом шаге
- Маленькие шаги — лучше 10 маленьких PR, чем 1 огромный
- Документация — обновлять документацию после изменений
Приоритет 6: Улучшение тестируемости
P6.1 — Dependency Injection для TdClient ✅ ЗАВЕРШЕНО!
Статус: ✅ ЗАВЕРШЕНО (ВСЕ 8 этапов завершены!) - 2026-02-02
Проблема:
В текущей реализации тесты создают настоящий TdClient, который вызывает tdlib_rs::create_client(). Это приводит к:
- Зависанию тестов — TDLib не инициализирован и блокирует async вызовы
- Verbose логи — TDLib выводит много логов при создании клиента
- Медленные тесты — создание TDLib клиента занимает время
- Хаки в продакшн коде — пришлось добавить
tokio::time::timeout(100ms)для всех вызовов TDLib чтобы тесты не зависали
Проблемные места (src/input/main_input.rs):
// Строка 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:
// 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<Vec<MessageInfo>>;
async fn send_message(&mut self, chat_id: ChatId, text: String, reply_to: Option<MessageId>, reply_info: Option<ReplyInfo>) -> Result<MessageInfo>;
async fn edit_message(&mut self, chat_id: ChatId, message_id: MessageId, text: String) -> Result<MessageInfo>;
async fn delete_messages(&mut self, chat_id: ChatId, message_ids: Vec<MessageId>, revoke: bool) -> Result<()>;
async fn forward_messages(&mut self, to_chat_id: ChatId, from_chat_id: ChatId, message_ids: Vec<MessageId>) -> 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<Vec<String>>;
async fn search_messages(&self, chat_id: ChatId, query: &str) -> Result<Vec<MessageInfo>>;
async fn get_profile_info(&self, chat_id: ChatId) -> Result<ProfileInfo>;
async fn leave_chat(&self, chat_id: ChatId) -> Result<()>;
async fn load_chats(&mut self, limit: usize) -> Result<Vec<ChatInfo>, 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<Vec<MessageInfo>, 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<MessageInfo>;
fn set_current_chat_id(&mut self, chat_id: Option<ChatId>);
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<MessageInfo>);
}
// 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<T: TdClientTrait = TdClient> {
pub td_client: T,
pub config: Config,
// ... остальные поля
}
impl<T: TdClientTrait> App<T> {
pub fn new(config: Config, td_client: T) -> Self {
// ...
}
// ... все остальные методы
}
// Специализация для продакшена
impl App<TdClient> {
pub fn new_default(config: Config) -> Self {
Self::new(config, TdClient::new())
}
}
// TestAppBuilder для тестов
impl TestAppBuilder {
pub fn build(self) -> App<FakeTdClient> {
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<T>,handle_input<T>) - ❌ Потеря простоты для небольшого проекта
- ❌ Нужна библиотека
async-traitдля async методов в trait
Затронутые файлы:
src/tdlib/trait.rs(новый) — trait определениеsrc/tdlib/client.rs— impl TdClientTrait for TdClientsrc/tdlib/mod.rs— экспорт traitsrc/app/mod.rs— App<T: TdClientTrait>src/input/main_input.rs— функции становятся genericsrc/input/auth.rs— функции становятся genericsrc/ui/*.rs— функции рендеринга становятся genericsrc/main.rs— использовать Apptests/helpers/fake_tdclient.rs— impl TdClientTrait for FakeTdClienttests/helpers/app_builder.rs— build() возвращает App- Все интеграционные тесты (~15 файлов)
Оценка трудозатрат: ~2-3 дня работы
Вариант 2: Enum Dispatch (компромисс)
// 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<T: TdClientTrait> - ✅ Все UI и input handlers обновлены на generic
- ✅ Тесты используют
FakeTdClient(быстро, без логов TDLib) - ✅ Продакшн использует
TdClient(реальный TDLib) - ✅ Timeout'ы убраны из продакшн кода
- ✅ Исправлен stack overflow в 6 методах trait реализации
- ✅ Все 196+ тестов проходят
Преимущества реализации:
- 🛡️ Чистая архитектура без timeout хаков
- ⚡ Быстрые тесты (FakeTdClient работает мгновенно)
- 📝 Нет verbose логов TDLib в тестах
- 🔧 Type-safe dependency injection
- 🎯 Легко добавлять новые реализации trait
Примечания
- Этот документ живой и будет обновляться
- Новые пункты добавляются по мере обнаружения
- После завершения задачи отмечать в метриках
- При появлении блокеров — документировать в соответствующей секции