168 lines
6.1 KiB
Markdown
168 lines
6.1 KiB
Markdown
# Архитектура Offline-first для мессенджера Knot
|
||
|
||
## Обзор
|
||
|
||
Система кэширования истории чатов реализует паттерн **Offline-first** с использованием:
|
||
- **Room** - локальная база данных
|
||
- **Paging 3** - пагинация с RemoteMediator
|
||
- **WorkManager** - фоновая синхронизация
|
||
- **SignalR** - real-time обновления
|
||
|
||
## Компоненты
|
||
|
||
### 1. Data Layer
|
||
|
||
#### MessageEntity
|
||
```kotlin
|
||
@Entity(tableName = "messages")
|
||
data class MessageEntity(
|
||
@PrimaryKey val id: String,
|
||
val chatId: String,
|
||
val senderId: String,
|
||
val content: String?,
|
||
val sequenceId: Int,
|
||
val createdAt: String,
|
||
|
||
// Поля синхронизации
|
||
val syncStatus: SyncStatus, // SYNCED, SYNCING, FAILED
|
||
val isDeletedLocally: Boolean, // Помечено на удаление
|
||
val isEditedLocally: Boolean, // Помечено на редактирование
|
||
val editedContent: String?, // Новое содержимое
|
||
val lastUpdated: Long // Время последнего изменения
|
||
)
|
||
```
|
||
|
||
#### MessageDao
|
||
Основные методы:
|
||
- `getMessagesPagingSource()` - PagingSource для Paging 3
|
||
- `upsertMessage()` - Вставка/обновление с разрешением конфликтов
|
||
- `markAsDeletedLocally()` - Пометка на удаление
|
||
- `markAsEditedLocally()` - Пометка на редактирование
|
||
- `getPendingSyncMessages()` - Получение сообщений для синхронизации
|
||
|
||
### 2. Pagination (Paging 3)
|
||
|
||
#### MessageRemoteMediator
|
||
Управляет загрузкой данных:
|
||
- **REFRESH** - первая загрузка последних сообщений
|
||
- **APPEND** - загрузка более старых сообщений (прокрутка вниз)
|
||
- **PREPEND** - загрузка более новых сообщений (прокрутка вверх)
|
||
|
||
Логика:
|
||
1. Проверяет наличие данных в Room
|
||
2. При необходимости загружает из API
|
||
3. Сохраняет в Room
|
||
4. Paging читает из локальной базы
|
||
|
||
### 3. Background Sync (WorkManager)
|
||
|
||
#### MessageSyncWorker
|
||
Обрабатывает отложенную синхронизацию:
|
||
- Отправка новых сообщений (SYNCING)
|
||
- Обновление отредактированных (isEditedLocally = true)
|
||
- Удаление помеченных (isDeletedLocally = true)
|
||
- Повтор при ошибках (FAILED)
|
||
|
||
Политика повторных попыток:
|
||
- Экспоненциальная задержка
|
||
- Максимум 3 попытки
|
||
- Требуется подключение к сети
|
||
|
||
### 4. Real-time Updates (SignalR)
|
||
|
||
#### MessageSignalRHandler
|
||
Обрабатывает события:
|
||
- `new_message` - новое сообщение
|
||
- `message_edited` - редактирование
|
||
- `message_deleted` - удаление
|
||
- `messages_read` - прочтение
|
||
- `reaction_added/removed` - реакции
|
||
|
||
Все изменения сразу записываются в Room → UI обновляется через Flow
|
||
|
||
### 5. Repository
|
||
|
||
#### ChatRepositoryImpl
|
||
Единая точка входа для ViewModel:
|
||
- `getMessagesPaging()` - Paging 3 поток
|
||
- `getMessagesFlow()` - простой Flow списка
|
||
- `sendMessage()` - отправка с локальным сохранением
|
||
- `deleteLocalMessage()` - локальное удаление
|
||
- `editLocalMessage()` - локальное редактирование
|
||
|
||
## Conflict Resolution
|
||
|
||
Приоритет данных:
|
||
1. **Сообщения в процессе отправки (SYNCING)** - локальные данные имеют приоритет
|
||
2. **Сообщения в процессе редактирования** - локальные данные имеют приоритет
|
||
3. **Все остальные случаи** - серверные данные имеют приоритет
|
||
|
||
## Схема работы
|
||
|
||
### Отправка сообщения
|
||
```
|
||
User → sendMessage() → Сохранение в Room (SYNCING) → UI показывает сообщение
|
||
→ WorkManager планирует синхронизацию
|
||
→ Отправка на сервер
|
||
→ Обновление статуса (SYNCED)
|
||
```
|
||
|
||
### Получение сообщений
|
||
```
|
||
UI ← getMessagesPaging() ← Room ← RemoteMediator ← API
|
||
↑
|
||
└─── SignalR обновления
|
||
```
|
||
|
||
### Удаление сообщения
|
||
```
|
||
User → deleteLocalMessage() → Пометка (isDeletedLocally = true)
|
||
→ WorkManager удаляет на сервере
|
||
→ Удаление из Room
|
||
```
|
||
|
||
## Использование
|
||
|
||
### Paging 3 в ViewModel
|
||
```kotlin
|
||
@HiltViewModel
|
||
class ChatViewModel @Inject constructor(
|
||
private val repository: ChatRepository
|
||
) : ViewModel() {
|
||
|
||
val messages: Flow<PagingData<Message>> =
|
||
repository.getMessagesPaging(chatId)
|
||
.cachedIn(viewModelScope)
|
||
}
|
||
```
|
||
|
||
### Офлайн отправка
|
||
```kotlin
|
||
// Сообщение сразу появится в UI
|
||
val message = repository.sendMessage(
|
||
chatId = chatId,
|
||
content = "Hello"
|
||
)
|
||
|
||
// Синхронизация произойдёт в фоне
|
||
```
|
||
|
||
## Миграции
|
||
|
||
При обновлении схемы БД используется миграция `MIGRATION_1_2`:
|
||
- Добавляет поля синхронизации
|
||
- Сохраняет существующие данные
|
||
- Устанавливает значения по умолчанию
|
||
|
||
## Тестирование
|
||
|
||
### Юнит-тесты
|
||
- MessageDao тесты
|
||
- MessageRemoteMediator тесты
|
||
- ChatRepositoryImpl тесты
|
||
|
||
### Интеграционные тесты
|
||
- Синхронизация с сервером
|
||
- Обработка конфликтов
|
||
- WorkManager сценарии
|