187 lines
6.2 KiB
Markdown
187 lines
6.2 KiB
Markdown
# Умная синхронизация сообщений при восстановлении сети
|
|
|
|
## Проблема
|
|
При восстановлении соединения после офлайна, приложение запрашивало **все сообщения заново** (последние 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
|
|
```
|