Восстановление сети
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,186 +0,0 @@
|
|||||||
# Умная синхронизация сообщений при восстановлении сети
|
|
||||||
|
|
||||||
## Проблема
|
|
||||||
При восстановлении соединения после офлайна, приложение запрашивало **все сообщения заново** (последние 50 для каждого чата), вместо того чтобы получить только **новые сообщения**, которые пришли пока клиент был офлайн.
|
|
||||||
|
|
||||||
## Решение
|
|
||||||
Добавлен новый параметр API `afterSequenceId` который позволяет запрашивать только сообщения с sequenceId > указанного.
|
|
||||||
|
|
||||||
## Изменения
|
|
||||||
|
|
||||||
### Бэкенд
|
|
||||||
|
|
||||||
#### 1. IMessageRepository.cs
|
|
||||||
Добавлен метод для получения сообщений ПОСЛЕ указанного sequenceId:
|
|
||||||
```csharp
|
|
||||||
Task<List<Message>> GetChatMessagesAfterAsync(Guid chatId, long sequenceId, int limit, CancellationToken cancellationToken);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. MessageRepository.cs
|
|
||||||
Реализация метода:
|
|
||||||
```csharp
|
|
||||||
public async Task<List<Message>> GetChatMessagesAfterAsync(Guid chatId, long sequenceId, int limit, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var builder = Builders<Message>.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<List<MessageDetailDto>>;
|
|
||||||
```
|
|
||||||
|
|
||||||
Обработчик использует новый метод:
|
|
||||||
```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<MessageDto>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. ChatRepository.kt
|
|
||||||
Добавлен метод:
|
|
||||||
```kotlin
|
|
||||||
suspend fun getMessages(
|
|
||||||
chatId: String,
|
|
||||||
cursor: String? = null,
|
|
||||||
pivot: Long? = null,
|
|
||||||
afterSequenceId: Long? = null, // НОВЫЙ ПАРАМЕТР
|
|
||||||
limit: Int? = null
|
|
||||||
): List<Message>
|
|
||||||
```
|
|
||||||
|
|
||||||
Добавлен метод для получения последнего 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
|
|
||||||
```
|
|
||||||
@@ -89,7 +89,6 @@ class SignalRNotificationObserver @Inject constructor(
|
|||||||
// При восстановлении соединения SignalR обновляем список чатов и вступаем в них
|
// При восстановлении соединения SignalR обновляем список чатов и вступаем в них
|
||||||
signalrClient.status
|
signalrClient.status
|
||||||
.filter { it == ConnectionStatus.CONNECTED }
|
.filter { it == ConnectionStatus.CONNECTED }
|
||||||
.distinctUntilChanged()
|
|
||||||
.onEach {
|
.onEach {
|
||||||
android.util.Log.d("SignalRNtfObserver", "SignalR connected, refreshing chats and joining rooms")
|
android.util.Log.d("SignalRNtfObserver", "SignalR connected, refreshing chats and joining rooms")
|
||||||
onSignalRConnected()
|
onSignalRConnected()
|
||||||
|
|||||||
Reference in New Issue
Block a user