refactor: implement trait-based DI for TdClient and fix stack overflow
Implement complete trait-based dependency injection pattern for TdClient to enable testing with FakeTdClient mock. Fix critical stack overflow bugs caused by infinite recursion in trait implementations. Breaking Changes: - App is now generic: App<T: TdClientTrait = TdClient> - All UI and input handlers are generic over TdClientTrait - TdClient methods now accessed through trait interface New Files: - src/tdlib/trait.rs: TdClientTrait definition with 40+ methods - src/tdlib/client_impl.rs: TdClientTrait impl for TdClient - tests/helpers/fake_tdclient_impl.rs: TdClientTrait impl for FakeTdClient Critical Fixes: - Fix stack overflow in send_message, edit_message, delete_messages - Fix stack overflow in forward_messages, current_chat_messages - Fix stack overflow in current_pinned_message - All methods now call message_manager directly to avoid recursion Testing: - FakeTdClient supports configurable auth_state for auth screen tests - Added pinned message support in FakeTdClient - All 196+ tests passing (188 tests + 8 benchmarks) Dependencies: - Added async-trait = "0.1" Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
use crate::app::App;
|
||||
use crate::tdlib::TdClientTrait;
|
||||
use crate::input::handlers::{
|
||||
copy_to_clipboard, format_message_for_clipboard, get_available_actions_count,
|
||||
handle_global_commands,
|
||||
@@ -9,7 +10,7 @@ use crate::utils::{with_timeout, with_timeout_msg};
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
pub async fn handle<T: TdClientTrait>(app: &mut App<T>, key: KeyEvent) {
|
||||
// Глобальные команды (работают всегда)
|
||||
if handle_global_commands(app, key).await {
|
||||
return;
|
||||
@@ -445,7 +446,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
{
|
||||
Ok(messages) => {
|
||||
// Сохраняем загруженные сообщения
|
||||
*app.td_client.current_chat_messages_mut() = messages;
|
||||
app.td_client.set_current_chat_messages(messages);
|
||||
// ВАЖНО: Устанавливаем current_chat_id ТОЛЬКО ПОСЛЕ сохранения истории
|
||||
// Это предотвращает race condition с Update::NewMessage
|
||||
app.td_client.set_current_chat_id(Some(ChatId::new(chat_id)));
|
||||
@@ -589,10 +590,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
app.last_typing_sent = None;
|
||||
|
||||
// Отменяем typing status
|
||||
let _ = tokio::time::timeout(
|
||||
Duration::from_millis(100),
|
||||
app.td_client.send_chat_action(ChatId::new(chat_id), ChatAction::Cancel)
|
||||
).await;
|
||||
app.td_client.send_chat_action(ChatId::new(chat_id), ChatAction::Cancel).await;
|
||||
|
||||
match with_timeout_msg(
|
||||
Duration::from_secs(5),
|
||||
@@ -633,7 +631,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
{
|
||||
Ok(messages) => {
|
||||
// Сохраняем загруженные сообщения
|
||||
*app.td_client.current_chat_messages_mut() = messages;
|
||||
app.td_client.set_current_chat_messages(messages);
|
||||
// ВАЖНО: Устанавливаем current_chat_id ТОЛЬКО ПОСЛЕ сохранения истории
|
||||
// Это предотвращает race condition с Update::NewMessage
|
||||
app.td_client.set_current_chat_id(Some(ChatId::new(chat_id)));
|
||||
@@ -680,17 +678,10 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
if let Some(chat_id) = app.selected_chat_id {
|
||||
if !app.message_input.is_empty() && !app.is_editing() && !app.is_replying() {
|
||||
let draft_text = app.message_input.clone();
|
||||
// Timeout чтобы не блокировать UI в тестах
|
||||
let _ = tokio::time::timeout(
|
||||
Duration::from_millis(100),
|
||||
app.td_client.set_draft_message(chat_id, draft_text)
|
||||
).await;
|
||||
let _ = app.td_client.set_draft_message(chat_id, draft_text).await;
|
||||
} else if app.message_input.is_empty() {
|
||||
// Очищаем черновик если инпут пустой
|
||||
let _ = tokio::time::timeout(
|
||||
Duration::from_millis(100),
|
||||
app.td_client.set_draft_message(chat_id, String::new())
|
||||
).await;
|
||||
let _ = app.td_client.set_draft_message(chat_id, String::new()).await;
|
||||
}
|
||||
}
|
||||
app.close_chat();
|
||||
@@ -733,7 +724,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
KeyCode::Char('y') | KeyCode::Char('н') => {
|
||||
// Копировать сообщение
|
||||
if let Some(msg) = app.get_selected_message() {
|
||||
let text = format_message_for_clipboard(msg);
|
||||
let text = format_message_for_clipboard(&msg);
|
||||
match copy_to_clipboard(&text) {
|
||||
Ok(_) => {
|
||||
app.status_message = Some("Сообщение скопировано".to_string());
|
||||
@@ -864,11 +855,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
.unwrap_or(true);
|
||||
if should_send_typing {
|
||||
if let Some(chat_id) = app.get_selected_chat_id() {
|
||||
// Используем короткий timeout чтобы не блокировать UI (особенно в тестах)
|
||||
let _ = tokio::time::timeout(
|
||||
Duration::from_millis(100),
|
||||
app.td_client.send_chat_action(ChatId::new(chat_id), ChatAction::Typing)
|
||||
).await;
|
||||
app.td_client.send_chat_action(ChatId::new(chat_id), ChatAction::Typing).await;
|
||||
app.last_typing_sent = Some(Instant::now());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user