Appearance
Дескриптор датасета
Содержание
Проблема
Механизм синхронизации при первом включении (enable) выгружает все локальные сущности на сервер. Это ожидаемое поведение — устройство вносит свои данные в аккаунт.
Проблема возникает при смене аккаунта:
- Устройство 1 авторизовано в Аккаунте A, данные синхронизированы.
- Пользователь выходит из аккаунта. Локальные данные остаются.
- Пользователь входит в Аккаунт B — случайно или намеренно.
- При первой синхронизации все данные Аккаунта A выгружаются в Аккаунт B.
- Слитие необратимо: откатить его без ручного вмешательства невозможно.
Ориентироваться только на userId недостаточно: у пользователя может быть один и тот же набор данных в разных аккаунтах, или локальный датасет вообще без привязки к аккаунту.
Цель: до начала выгрузки обнаружить, что локальный датасет и серверный датасет — разные, независимые наборы данных, и предупредить пользователя, дав ему выбор: продолжить слитие или выйти из аккаунта.
Решение: дескриптор датасета
Каждому независимому набору данных присваивается уникальный идентификатор — дескриптор. Дескриптор хранится как на клиенте, так и на сервере. Перед первой синхронизацией в новом аккаунте клиент сравнивает свой дескриптор с серверным и принимает решение: продолжить или предупредить пользователя.
Структура дескриптора
DatasetDescriptor {
id: UUID // уникальный идентификатор датасета
ancestors: Set<UUID> // плоское множество всех предков (не только прямых)
}Множество ancestors является плоским (содержит всех предков транзитивно), а не только прямых родителей. Это позволяет корректно обрабатывать каскадные слития без рекурсивного обхода.
Жизненный цикл дескриптора
Создание нового дескриптора
Происходит при первой синхронизации, когда ни у клиента, ни у сервера нет дескриптора (полностью новый датасет):
- Клиент генерирует новый UUID →
D_id,ancestors = {}. - Клиент сохраняет дескриптор локально и отправляет на сервер.
Предварительная генерация при наличии локальных данных
Если у клиента нет дескриптора, но есть локальные сущности, дескриптор генерируется до обращения к серверу — иначе случай «клиент без дескриптора / сервер с дескриптором» неотличим от подключения чистого нового устройства.
Это гарантирует, что устройство с данными всегда приходит на проверку уже с дескриптором, и Rules 1–3 применяются корректно.
Слитие двух датасетов
Когда клиент (дескриптор A_id) синхронизируется с сервером (дескриптор B_id) и пользователь подтверждает слитие:
- Клиент генерирует новый UUID →
C_id. ancestors(C_id) = ancestors(A_id) ∪ {A_id} ∪ ancestors(B_id) ∪ {B_id}.- Клиент отправляет
C_idна сервер; оба сохраняют новый дескриптор.
Специальный случай: клиент имеет дескриптор, сервер — нет
Это означает, что существующий датасет выгружается в новый (пустой) аккаунт. Чтобы сохранить различие между исходным датасетом и его «продолжением» в новом аккаунте, клиент создаёт дескриптор-наследник:
- Клиент генерирует новый UUID →
B_id. ancestors(B_id) = ancestors(A_id) ∪ {A_id}.- Клиент локально переключается на
B_idи отправляет его на сервер.
Это позволяет устройствам из исходного аккаунта (с A_id) в дальнейшем правильно определять своё отношение к новому аккаунту (см. Правило 2 ниже), а устройствам нового аккаунта — получать предупреждение при попытке вернуться в исходный аккаунт (Правило 3).
Алгоритм проверки перед синхронизацией
Проверка выполняется один раз — перед первой синхронизацией после входа в аккаунт (enable). Обозначим дескриптор клиента как C, дескриптор сервера как S.
Правило 1 — идентичные датасеты: C.id == S.id
→ Тот же датасет, тот же аккаунт. Синхронизация продолжается без предупреждения.
Правило 2 — клиент является предком сервера: C.id ∈ S.ancestors
→ Сервер уже содержит данные клиента (был получен путём слития или наследования от клиента). Синхронизация продолжается без предупреждения: клиент «догоняет» сервер.
Правило 3 — конфликт: ни одно из предыдущих правил не выполнено
→ Датасеты независимы. Синхронизация не начинается. Пользователю показывается предупреждение о необратимом слитии с выбором: продолжить или выйти из аккаунта.
Если пользователь выбирает «продолжить», выполняется слитие (см. «Слитие двух датасетов» выше) и синхронизация продолжается.
Граничные случаи:
| Состояние клиента | Состояние сервера | Действие |
|---|---|---|
| Нет дескриптора, нет сущностей | Нет дескриптора | Создать новый дескриптор, продолжить |
| Нет дескриптора, нет сущностей | Есть дескриптор S | Принять S как свой, продолжить (чистое новое устройство) |
| Нет дескриптора, есть сущности | Любое | Сгенерировать дескриптор предварительно → применить Правила 1–3 (или миграционную логику, см. ниже) |
Есть дескриптор C | Нет дескриптора | Создать наследника C, отправить на сервер, продолжить |
Есть дескриптор C | Есть дескриптор S | Применить Правила 1–3 |
Таблица сценариев
| Сценарий | Клиент | Сервер | Правило | Результат |
|---|---|---|---|---|
| Обычная синхронизация | A_id | A_id | 1 | Продолжить |
| Второе устройство того же аккаунта | — | A_id | — | Принять A_id, продолжить |
| Устройство с A_id входит в аккаунт B (A→B слитие уже было) | A_id | C_id, ancestors: {A_id, B_id} | 2 | Продолжить |
| Устройство с B_id входит в аккаунт A | B_id | A_id | 3 | Предупреждение |
| Устройство с A_id входит в аккаунт B (без слития) | A_id | B_id | 3 | Предупреждение |
| Перенос данных в новый аккаунт | A_id | — | — | Создать B_id (ancestor: A_id), продолжить |
Осознанные компромиссы
После слития изменения из исходного аккаунта попадают в новый молча
Если Аккаунт A слился с Аккаунтом B и образовал C_id (с ancestors = {A_id, B_id}), то любое устройство с дескриптором A_id, входящее в аккаунт C_id, попадает под Правило 2 и синхронизируется без предупреждения.
Это ожидаемо: аккаунт C_id является прямым потомком A_id, и данные «родительского» аккаунта легитимно входят в состав C_id. Пользователь, подтвердивший слитие, тем самым согласился на такое поведение для всех будущих устройств с A_id.
Перенос данных в новый аккаунт делает исходный аккаунт «родителем»
При переносе данных A_id в новый пустой аккаунт создаётся B_id с предком A_id. Все устройства из аккаунта A (A_id) в дальнейшем смогут синхронизироваться с аккаунтом B без предупреждения (Правило 2). Если пользователь хочет, чтобы аккаунты были полностью независимы, ему следует не переносить данные, а начать в новом аккаунте с чистого листа.
Защита только на стороне клиента (первая версия)
Механизм реализован исключительно на клиенте. Сервер не блокирует синхронизацию при конфликте дескрипторов. Это означает, что старый клиент или намеренно модифицированный клиент может выполнить слитие без предупреждения. Данный механизм является usability-функцией для защиты от случайного слития — не защитой от злоумышленника.
Миграция существующих пользователей
Пользователи, обновившиеся со старой версии клиента, имеют локальные данные, но не имеют дескриптора. Показывать им диалог слития недопустимо — это штатное обновление, затрагивающее 100% пользователей.
При первой инициализации нового механизма на устройстве с существующими данными синхронизации клиент определяет, что находится в режиме миграции. В этом режиме проверка дескриптора проходит молча: принимается дескриптор сервера (или создаётся новый, если у сервера его ещё нет). Диалог не показывается.
Признак миграции одноразовый: после первой успешной обработки удаляется. Чистые установки (без истории синхронизации) в режим миграции не попадают — для них сразу работает стандартная механика.