6.1 KiB
6.1 KiB
Архитектура Offline-first для мессенджера Knot
Обзор
Система кэширования истории чатов реализует паттерн Offline-first с использованием:
- Room - локальная база данных
- Paging 3 - пагинация с RemoteMediator
- WorkManager - фоновая синхронизация
- SignalR - real-time обновления
Компоненты
1. Data Layer
MessageEntity
@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 3upsertMessage()- Вставка/обновление с разрешением конфликтовmarkAsDeletedLocally()- Пометка на удалениеmarkAsEditedLocally()- Пометка на редактированиеgetPendingSyncMessages()- Получение сообщений для синхронизации
2. Pagination (Paging 3)
MessageRemoteMediator
Управляет загрузкой данных:
- REFRESH - первая загрузка последних сообщений
- APPEND - загрузка более старых сообщений (прокрутка вниз)
- PREPEND - загрузка более новых сообщений (прокрутка вверх)
Логика:
- Проверяет наличие данных в Room
- При необходимости загружает из API
- Сохраняет в Room
- 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
Приоритет данных:
- Сообщения в процессе отправки (SYNCING) - локальные данные имеют приоритет
- Сообщения в процессе редактирования - локальные данные имеют приоритет
- Все остальные случаи - серверные данные имеют приоритет
Схема работы
Отправка сообщения
User → sendMessage() → Сохранение в Room (SYNCING) → UI показывает сообщение
→ WorkManager планирует синхронизацию
→ Отправка на сервер
→ Обновление статуса (SYNCED)
Получение сообщений
UI ← getMessagesPaging() ← Room ← RemoteMediator ← API
↑
└─── SignalR обновления
Удаление сообщения
User → deleteLocalMessage() → Пометка (isDeletedLocally = true)
→ WorkManager удаляет на сервере
→ Удаление из Room
Использование
Paging 3 в ViewModel
@HiltViewModel
class ChatViewModel @Inject constructor(
private val repository: ChatRepository
) : ViewModel() {
val messages: Flow<PagingData<Message>> =
repository.getMessagesPaging(chatId)
.cachedIn(viewModelScope)
}
Офлайн отправка
// Сообщение сразу появится в UI
val message = repository.sendMessage(
chatId = chatId,
content = "Hello"
)
// Синхронизация произойдёт в фоне
Миграции
При обновлении схемы БД используется миграция MIGRATION_1_2:
- Добавляет поля синхронизации
- Сохраняет существующие данные
- Устанавливает значения по умолчанию
Тестирование
Юнит-тесты
- MessageDao тесты
- MessageRemoteMediator тесты
- ChatRepositoryImpl тесты
Интеграционные тесты
- Синхронизация с сервером
- Обработка конфликтов
- WorkManager сценарии