You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Центральным компонентом синхронизации данных между локальным хранилищем (SQLite/Room) и удаленным облаком (Firebase Storage) является класс `SyncManagerImpl`. Система спроектирована по принципу **Offline-First**: пользователь имеет полный доступ к чтению и модификации заметок без активного сетевого соединения, а слияние состояний происходит асинхронно при восстановлении сети.
3
+
Центральным компонентом синхронизации данных между локальным хранилищем (SQLite/Room) и удаленным облаком (Firebase Storage) является интерфейс `SyncManager` и его реализация `SyncManagerImpl`.
Система спроектирована по принципу **Offline-First**: пользователь имеет полный доступ к чтению и модификации данных без активного сетевого соединения. Слияние состояний происходит асинхронно при восстановлении сети.
6
6
7
-
Процесс синхронизации запускается вызовом функции `sync(userId: String)` и состоит из двух строго изолированных фаз: синхронизации текстовых данных заметок (метаданных) и синхронизации бинарных вложений (медиафайлов).
7
+
Для соблюдения принципа единственной ответственности (SRP) сложная логика монолита разделена на три изолированные фазы: выгрузка (`SyncPusher`), загрузка (`SyncPuller`) и локальная очистка (`SyncCleaner`).
8
+
9
+
## 1. Алгоритм синхронизации (Трехфазный коммит)
10
+
11
+
Процесс запускается вызовом функции `sync(userId: String)` и состоит из последовательной обработки трех типов данных: Папки (Folders) ➔ Заметки (Notes) ➔ Бинарные медиа (Media).
8
12
9
13
```mermaid
10
14
graph TD
11
-
A[Старт sync_userId] --> B[Фаза 1: Загрузка метаданных из Firebase]
12
-
B --> C[Сравнение локальных и удаленныхUpdatedAt]
13
-
C --> D{Есть конфликты?}
14
-
D -- Удаленный новее --> E[Скачать JSON -> Обновить Room]
15
-
D -- Локальный новее --> F[Собрать JSON -> Выгрузить в Firebase]
16
-
D -- Одинаковые/Нет изменений --> G[Пропустить]
17
-
E --> H[Фаза 2: Сверка бинарных медиафайлов]
18
-
F --> H
19
-
G --> H
20
-
H --> I[Выгрузка локальных медиа с isSynced = false]
B3 --> C[SyncPuller: Скачивание обновлений из облака]
21
+
C --> C1[Pull Папок]
22
+
C1 --> C2[Pull Заметок]
23
+
C2 --> C3[Pull Медиа]
24
+
25
+
C3 --> D[SyncCleaner: Очистка удаленных данных]
26
+
D --> E[Удаление отсутствующих папок, заметок и медиа в Room]
27
+
E --> F[Завершение сессии: SyncState.Success]
23
28
```
29
+
### Детальные шаги выполнения:
30
+
Фаза Push (Выгрузка):
31
+
Через SyncPusher менеджер запрашивает все локальные записи (папки, заметки, медиа), имеющие флаг isSynced = false. Если локальная сущность помечена как удаленная (isDeleted = true), на сервер отправляется запрос на удаление соответствующего файла, после чего запись физически стирается из Room (hard delete). Остальные модифицированные записи формируются в JSON-пакеты (или потоки байтов для медиа) и отправляются в Firebase Storage. После успеха статус меняется на isSynced = true.
24
32
25
-
###Детальные шаги выполнения:
26
-
Запрос облачного слепка: Через CloudDataSource.listNoteMetadata(userId) запрашивается список всех файлов заметок, находящихся в облачной папке users/$userId/notes. Каждая запись содержит key (путь к файлу) и updatedAt (временная метка последнего изменения на стороне Firebase).
33
+
Фаза Pull (Скачивание):
34
+
SyncPuller запрашивает облачные слепки (списки метаданных) для папок, заметок и медиафайлов пользователя. Сравнивая ключи из облака с ID локальных записей, система определяет недостающие элементы. Эти элементы скачиваются, парсятся из JSON и сохраняются в локальную базу данных (через SyncDaoContainer).
27
35
28
-
Получение локального слепка: Параллельно из NoteDao запрашиваются все локальные записи (включая помеченные как удаленные через флаг isDeleted).
36
+
Фаза Clean (Очистка):
37
+
SyncCleaner берет множества (Sets) ID элементов, полученных на этапе скачивания, и вычисляет разницу с локальной БД. Если локальная запись присутствует в Room, но отсутствует в облачном слепке, она считается удаленной с другого устройства и физически стирается локально (для медиа удаляются и сами файлы из памяти устройства).
29
38
30
-
Анализ расхождений:
39
+
## 2. Управление состояниями (SyncState)
40
+
Для реактивного отображения статуса синхронизации в UI (например, для показа спиннера загрузки) менеджер предоставляет StateFlow<SyncState>.
41
+
##### Возможные состояния:
31
42
32
-
Если локальная заметка имеет флаг isDeleted = true и isSynced = false, на сервер отправляется запрос на удаление соответствующего JSON-объекта, после чего запись физически стирается из Room.
43
+
SyncState.Idle — синхронизация не запущена.
33
44
34
-
Если updatedAt в Firebase строго больше, чем локальный updatedAt, локальная запись перезаписывается скачанными данными.
45
+
SyncState.Syncing — активный процесс обмена данными (Push/Pull/Clean).
35
46
36
-
Если локальный updatedAt новее, формируется свежий JSON-пакет и отправляется в Firebase Storage.
47
+
SyncState.Success — все три фазы успешно завершены.
37
48
38
-
##2. Модель полиморфной сериализации заметок
39
-
Поскольку заметка (Note) в приложении имеет динамическую структуру и может состоять из произвольного набора контентных блоков (текст, картинки, файлы, ссылки), для её сохранения на сервере применяется полиморфная сериализация на базе kotlinx.serialization.
49
+
SyncState.Error — процесс прерван с ошибкой (содержит сообщение об ошибке).
40
50
41
-
Преобразование данных изолировано в классе NoteEntityJsonConverter. Каждая заметка упаковывается в текстовый JSON-файл следующей структуры:
51
+
## 3. Полиморфная сериализация данных
52
+
Поскольку заметки (Note) и папки (Folder) имеют динамическую структуру, для их сохранения на сервере применяется сериализация на базе kotlinx.serialization. Преобразования изолированы в классе SyncMappers (NoteEntityJsonConverter и FolderEntityJsonConverter).
42
53
54
+
Каждая сущность упаковывается в текстовый JSON-файл.
Поле content: В локальной БД Room для оптимизации поисковых запросов и быстрого рендеринга списков content хранится в виде плоской строки (NoteEntity.content). При сериализации для Firebase SyncManager парсит или собирает полноценный массив contentItems.
75
-
76
-
Изоляция путей: При передаче в Firebase локальные абсолютные пути устройства (localPath) очищаются или игнорируются на других устройствах, а привязка идет по уникальному remoteUrl, строящемуся по маске users/$userId/media/$mediaId.
77
-
78
-
##3. Фаза синхронизации медиаресурсов
79
-
После завершения сверки текстовых метаданных SyncManagerImpl переходит к обработке бинарных данных (изображения и вложенные файлы), хранящихся в таблице media.
80
-
81
-
Исходящий поток (Upload): Выбираются все записи из MediaDao.getAllMedia(), у которых isSynced = false. Менеджер открывает inputStream локального файла по адресу localPath и стримит байты напрямую в референс Firebase Storage: rootRef.child(remoteUrl). После успешного завершения статус записи в БД меняется на isSynced = true.
82
-
83
-
Входящий поток (Download): На основе метаданных, полученных из cloudDataSource.listMediaMetadata(userId), вычисляется разница между файлами на сервере и локальным диском. Для каждого скачиваемого файла:
84
-
85
-
В системной директории context.filesDir создается папка media/ (если отсутствует).
86
-
87
-
Выделяется файл с именем, соответствующим чистому ID медиаресурса: File(context.filesDir, "media/$actualMediaId").
88
-
89
-
Данные скачиваются из облака, и в таблицу media вносится новая запись MediaEntity с типом "IMAGE" или "FILE" в зависимости от его Mime-Type.
90
-
91
-
92
-
93
-
### 📂 Создай файл: `docs/sync/workmanager.md`
94
-
95
-
```markdown
96
-
# Модуль синхронизации: Фоновые задачи (WorkManager)
97
-
98
-
Для обеспечения гарантированной доставки изменений на сервер даже в случае закрытия приложения пользователем, вся инфраструктура запуска синхронизации вынесена из главного процесса UI в системный планировщик **Jetpack WorkManager**.
99
-
100
-
## 1. Планирование задач через WorkManagerSyncScheduler
101
-
102
-
Запуск фонового процесса инкапсулирован в классе `WorkManagerSyncScheduler`, реализующем доменный интерфейс `SyncScheduler`. Вместо хаотичного вызова корутин из `ViewModel`, отправка триггерится системными намерениями.
103
-
104
-
### Критерии и ограничения запуска (Constraints):
105
-
Для минимизации расхода заряда батареи и исключения ошибок холостых сетевых запросов настраиваются жесткие ограничения:
106
-
107
-
```
108
-
```kotlin
109
-
val constraints =Constraints.Builder()
110
-
.setRequiredNetworkType(NetworkType.CONNECTED) // Обязательное наличие интернета
111
-
.build()
112
-
```
113
-
114
-
115
-
###Обеспечение уникальности выполнения:
116
-
Синхронизация тяжелых файлов и JSON-структур требовательна к транзакционности. Для предотвращения состояния гонки (Race Condition), когда несколько триггеров пытаются одновременно писать данные в одну и ту же учетную запись Firebase, используется механизм уникальных воркеров:
117
-
118
-
```kotlin
119
-
workManager.enqueueUniqueWork(
120
-
"sync_work_$userId", // Уникальное имя воркера, привязанное к UID пользователя
121
-
ExistingWorkPolicy.REPLACE, // Стратегия: если прошлый воркер еще работает, заменить его новым
122
-
syncRequest
123
-
)
124
-
```
125
-
126
-
Использование ExistingWorkPolicy.REPLACE гарантирует, что если пользователь совершил серию быстрых изменений в заметках, цепочка промежуточных воркеров прервется, и запустится актуальный процесс, содержащий последний слепок локальной БД.
85
+
### Изоляция путей
86
+
При передаче медиафайлов в Firebase локальные абсолютные пути устройства (localPath) очищаются или игнорируются на других устройствах. Привязка идет по уникальному remoteUrl, строящемуся по маске users/$userId/media/${noteId}_${mediaId}. При входящем потоке (Download) скачанные данные сохраняются в context.filesDir/media/, и таблица Room обновляется актуальным локальным адресом.
0 commit comments