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>
1121 lines
43 KiB
Markdown
1121 lines
43 KiB
Markdown
# 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<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. Вынести константы в отдельный модуль
|
||
|
||
**Проблема**: Магические числа разбросаны по всему коду:
|
||
```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<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.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<T, String>` — теряется контекст ошибок.
|
||
|
||
**Решение**: Создать `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<T> = std::result::Result<T, TeletuiError>;
|
||
```
|
||
|
||
**Зависимости**: `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<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:
|
||
```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<FormattedSpan> {
|
||
// Вся логика форматирования
|
||
}
|
||
```
|
||
|
||
**Преимущества**:
|
||
- Разделение ответственности
|
||
- Можно тестировать отдельно
|
||
- Переиспользование в других местах
|
||
|
||
---
|
||
|
||
### 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<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`:
|
||
```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<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`:
|
||
```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<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 TdClient
|
||
- `src/tdlib/mod.rs` — экспорт trait
|
||
- `src/app/mod.rs` — App<T: TdClientTrait>
|
||
- `src/input/main_input.rs` — функции становятся generic
|
||
- `src/input/auth.rs` — функции становятся generic
|
||
- `src/ui/*.rs` — функции рендеринга становятся generic
|
||
- `src/main.rs` — использовать App<TdClient>
|
||
- `tests/helpers/fake_tdclient.rs` — impl TdClientTrait for FakeTdClient
|
||
- `tests/helpers/app_builder.rs` — build() возвращает App<FakeTdClient>
|
||
- Все интеграционные тесты (~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<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
|
||
|
||
---
|
||
|
||
## Примечания
|
||
|
||
- Этот документ живой и будет обновляться
|
||
- Новые пункты добавляются по мере обнаружения
|
||
- После завершения задачи отмечать в метриках
|
||
- При появлении блокеров — документировать в соответствующей секции
|