diff --git a/client-mobile/.gradle/8.5/executionHistory/executionHistory.bin b/client-mobile/.gradle/8.5/executionHistory/executionHistory.bin index b4b880e..7284c77 100644 Binary files a/client-mobile/.gradle/8.5/executionHistory/executionHistory.bin and b/client-mobile/.gradle/8.5/executionHistory/executionHistory.bin differ diff --git a/client-mobile/.gradle/8.5/executionHistory/executionHistory.lock b/client-mobile/.gradle/8.5/executionHistory/executionHistory.lock index ad47d2a..3b140c9 100644 Binary files a/client-mobile/.gradle/8.5/executionHistory/executionHistory.lock and b/client-mobile/.gradle/8.5/executionHistory/executionHistory.lock differ diff --git a/client-mobile/.gradle/8.5/fileHashes/fileHashes.bin b/client-mobile/.gradle/8.5/fileHashes/fileHashes.bin index f3e92f7..8788b1d 100644 Binary files a/client-mobile/.gradle/8.5/fileHashes/fileHashes.bin and b/client-mobile/.gradle/8.5/fileHashes/fileHashes.bin differ diff --git a/client-mobile/.gradle/8.5/fileHashes/fileHashes.lock b/client-mobile/.gradle/8.5/fileHashes/fileHashes.lock index 98936df..fec8e56 100644 Binary files a/client-mobile/.gradle/8.5/fileHashes/fileHashes.lock and b/client-mobile/.gradle/8.5/fileHashes/fileHashes.lock differ diff --git a/client-mobile/.gradle/8.5/fileHashes/resourceHashesCache.bin b/client-mobile/.gradle/8.5/fileHashes/resourceHashesCache.bin index 826cc24..3af06e7 100644 Binary files a/client-mobile/.gradle/8.5/fileHashes/resourceHashesCache.bin and b/client-mobile/.gradle/8.5/fileHashes/resourceHashesCache.bin differ diff --git a/client-mobile/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/client-mobile/.gradle/buildOutputCleanup/buildOutputCleanup.lock index 654e17e..e70bdf5 100644 Binary files a/client-mobile/.gradle/buildOutputCleanup/buildOutputCleanup.lock and b/client-mobile/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/client-mobile/.gradle/buildOutputCleanup/outputFiles.bin b/client-mobile/.gradle/buildOutputCleanup/outputFiles.bin index 32676dc..ff6e274 100644 Binary files a/client-mobile/.gradle/buildOutputCleanup/outputFiles.bin and b/client-mobile/.gradle/buildOutputCleanup/outputFiles.bin differ diff --git a/client-mobile/SYNC_IMPROVEMENTS.md b/client-mobile/SYNC_IMPROVEMENTS.md deleted file mode 100644 index 4741b1c..0000000 --- a/client-mobile/SYNC_IMPROVEMENTS.md +++ /dev/null @@ -1,186 +0,0 @@ -# Умная синхронизация сообщений при восстановлении сети - -## Проблема -При восстановлении соединения после офлайна, приложение запрашивало **все сообщения заново** (последние 50 для каждого чата), вместо того чтобы получить только **новые сообщения**, которые пришли пока клиент был офлайн. - -## Решение -Добавлен новый параметр API `afterSequenceId` который позволяет запрашивать только сообщения с sequenceId > указанного. - -## Изменения - -### Бэкенд - -#### 1. IMessageRepository.cs -Добавлен метод для получения сообщений ПОСЛЕ указанного sequenceId: -```csharp -Task> GetChatMessagesAfterAsync(Guid chatId, long sequenceId, int limit, CancellationToken cancellationToken); -``` - -#### 2. MessageRepository.cs -Реализация метода: -```csharp -public async Task> GetChatMessagesAfterAsync(Guid chatId, long sequenceId, int limit, CancellationToken cancellationToken) -{ - var builder = Builders.Filter; - var filter = builder.And( - builder.Eq(m => m.ChatId, chatId), - builder.Gt(m => m.SequenceId, sequenceId) - ); - - return await _messages.Find(filter) - .SortBy(m => m.SequenceId) - .Limit(limit) - .ToListAsync(cancellationToken); -} -``` - -#### 3. GetMessagesQuery.cs -Добавлен параметр `AfterSequenceId`: -```csharp -public record GetMessagesQuery( - Guid UserId, - Guid ChatId, - string? Cursor, - long? Pivot = null, - long? AfterSequenceId = null, // НОВЫЙ ПАРАМЕТР - int? Limit = null -) : IQuery>; -``` - -Обработчик использует новый метод: -```csharp -if (request.AfterSequenceId.HasValue) -{ - messages = await _messageRepository.GetChatMessagesAfterAsync( - request.ChatId, - request.AfterSequenceId.Value, - queryLimit, - cancellationToken - ); -} -``` - -#### 4. MessagesEndpoints.cs -Добавлен query параметр в endpoint: -```csharp -group.MapGet("chat/{chatId:guid}", async ( - [FromRoute] Guid chatId, - [FromQuery] string? cursor, - [FromQuery] long? afterSequenceId, // НОВЫЙ ПАРАМЕТР - [FromQuery] long? pivot, - [FromQuery] int? limit, - ISender sender, - IUserContext userContext, - CancellationToken ct) => { ... } -); -``` - -### Мобильное приложение - -#### 1. ChatApi.kt -Добавлен параметр в API: -```kotlin -@GET("messages/chat/{chatId}") -suspend fun getMessages( - @Path("chatId") chatId: String, - @Query("cursor") cursor: String? = null, - @Query("pivot") pivot: Long? = null, - @Query("afterSequenceId") afterSequenceId: Long? = null, // НОВЫЙ ПАРАМЕТР - @Query("limit") limit: Int? = 50 -): List -``` - -#### 2. ChatRepository.kt -Добавлен метод: -```kotlin -suspend fun getMessages( - chatId: String, - cursor: String? = null, - pivot: Long? = null, - afterSequenceId: Long? = null, // НОВЫЙ ПАРАМЕТР - limit: Int? = null -): List -``` - -Добавлен метод для получения последнего sequenceId: -```kotlin -suspend fun getLastKnownSequenceId(chatId: String): Int? -``` - -#### 3. ChatRepositoryImpl.kt -Реализация: -```kotlin -override suspend fun getLastKnownSequenceId(chatId: String): Int? { - return try { - messageDao.getMaxSequenceId(chatId) - } catch (e: Exception) { - Log.e(TAG, "Failed to get last sequenceId", e) - null - } -} -``` - -#### 4. SignalRNotificationObserver.kt -Умная синхронизация: -```kotlin -private fun syncMissedMessages() { - scope.launch { - val chats = chatRepository.getChats() - - chats.forEach { chat -> - val lastSequenceId = chatRepository.getLastKnownSequenceId(chat.id) - - if (lastSequenceId != null) { - // Запрашиваем только сообщения ПОСЛЕ последнего известного - chatRepository.getMessages( - chatId = chat.id, - afterSequenceId = lastSequenceId.toLong(), - limit = 100 - ) - } else { - // Нет локальных сообщений - загружаем последние 50 - chatRepository.getMessages(chatId = chat.id, limit = 50) - } - } - } -} -``` - -## Преимущества - -### До изменений -- Запрашивались **все последние 50 сообщений** для каждого чата -- При 20 чатах: 20 × 50 = **1000 сообщений** из сети -- Дублирование данных, медленная синхронизация - -### После изменений -- Запрашиваются **только новые сообщения** с последнего sequenceId -- При 20 чатах и 5 новых сообщениях в каждом: 20 × 5 = **100 сообщений** -- **В 10 раз меньше трафика** -- **Быстрая синхронизация** - -## Пример использования API - -```http -GET /api/messages/chat/{chatId}?afterSequenceId=12345&limit=100 -``` - -Ответит только с сообщениями где `sequenceId > 12345`. - -## Тестирование - -1. Откройте приложение, загрузите чаты -2. Отключите сеть -3. Отправьте несколько сообщений с другого устройства -4. Включите сеть -5. Проверьте логи: должно быть `fetching newer...` и загрузка только новых сообщений - -## Логи - -``` -SignalRNtfObserver: Starting missed messages sync... -SignalRNtfObserver: Syncing 5 chats for missed messages -SignalRNtfObserver: Chat abc-123: last known seqId=100, fetching newer... -SignalRNtfObserver: Chat abc-123: fetched 3 new messages -SignalRNtfObserver: Synced chat abc-123 -``` diff --git a/client-mobile/chats/data/remote/signalr/SignalRNotificationObserver.kt b/client-mobile/chats/data/remote/signalr/SignalRNotificationObserver.kt index 18e0a84..3314e4f 100644 --- a/client-mobile/chats/data/remote/signalr/SignalRNotificationObserver.kt +++ b/client-mobile/chats/data/remote/signalr/SignalRNotificationObserver.kt @@ -89,7 +89,6 @@ class SignalRNotificationObserver @Inject constructor( // При восстановлении соединения SignalR обновляем список чатов и вступаем в них signalrClient.status .filter { it == ConnectionStatus.CONNECTED } - .distinctUntilChanged() .onEach { android.util.Log.d("SignalRNtfObserver", "SignalR connected, refreshing chats and joining rooms") onSignalRConnected()