Разрешение конфликтов

В распределённых и офлайн-ориентированных приложениях конфликты данных возникают неизбежно. Dexie.js как высокоуровневая обёртка над IndexedDB предоставляет механизмы для управления конкурентным доступом, но не навязывает единую стратегию разрешения конфликтов. Это делает архитектурные решения полностью зависимыми от модели синхронизации приложения, особенно при использовании Dexie Cloud или кастомных серверных слоёв.

Конфликт в контексте Dexie.js — это ситуация, когда одна и та же сущность данных была изменена в двух или более независимых потоках до того, как изменения были синхронизированы между ними. Типичный сценарий: офлайн-изменения на клиенте и параллельное обновление той же записи на сервере.


Основные источники конфликтов

Параллельные транзакции в одном клиенте

Dexie.js поддерживает атомарные транзакции поверх IndexedDB. Однако при неправильной архитектуре возможно логическое пересечение изменений:

  • несколько транзакций пишут в одну и ту же запись
  • одна транзакция основывается на устаревшем состоянии
  • асинхронные цепочки чтения/записи без изоляции состояния

Хотя IndexedDB гарантирует атомарность транзакций, он не предотвращает логические конфликты уровня приложения.


Оффлайн-режим и последующая синхронизация

Наиболее частый источник конфликтов связан с offline-first архитектурой:

  • пользователь изменяет данные без подключения к сети
  • сервер изменяет те же данные параллельно
  • при синхронизации возникает расхождение версий

В таких случаях Dexie.js выступает как локальное хранилище, а не как система разрешения конфликтов.


Конкурирующие клиенты

Один и тот же пользователь может работать с несколькими вкладками или устройствами:

  • вкладка A изменяет запись
  • вкладка B изменяет ту же запись
  • изменения приходят на сервер в разном порядке

Без стратегии разрешения конфликтов возможна потеря данных.


Модели разрешения конфликтов

Last Write Wins (LWW)

Наиболее простая стратегия — «последняя запись побеждает». Она широко используется в простых синхронизационных системах.

Ключевой механизм:

  • каждая запись имеет upd atedAt
  • при конфликте выбирается более новая версия
if (local.updatedAt > remote.updatedAt) {
    return local;
} else {
    return remote;
}

Преимущества:

  • простота реализации
  • предсказуемость поведения
  • высокая производительность

Недостатки:

  • возможна потеря данных
  • не учитывает частичные изменения

Версионное управление (Optimistic Concurrency Control)

Более надёжный подход основан на версионности записей.

Каждая сущность содержит:

  • version
  • или revision
  • или хэш состояния

При обновлении:

  1. клиент читает запись с версией v
  2. отправляет обновление с указанием v
  3. сервер проверяет соответствие версии
  4. при несовпадении возникает конфликт

Пример логики:

if (server.version !== client.version) {
    throw new ConflictError();
}

В Dexie.js это часто моделируется через дополнительное поле:

db.items.update(id, {
    value: newValue,
    version: oldVersion + 1
});

Преимущество:

  • предотвращает случайную потерю данных
  • даёт контроль над конфликтом

Недостаток:

  • требует обработки ошибок на уровне приложения

Слияние изменений (Merge Strategy)

Для структурированных данных применяется стратегия слияния.

Пример: объект пользователя с частично независимыми полями.

const merged = {
    ...remote,
    ...local,
    preferences: {
        ...remote.preferences,
        ...local.preferences
    }
};

Этот подход особенно эффективен для:

  • пользовательских настроек
  • форм с независимыми полями
  • JSON-документов

Риски:

  • некорректное объединение вложенных структур
  • необходимость строгой схемы данных

CRDT-подходы

Conflict-free Replicated Data Types применяются в сложных распределённых системах. Dexie.js не предоставляет CRDT из коробки, но может использоваться как локальный слой хранения CRDT-структур.

Примеры структур:

  • счётчики (G-Counter, PN-Counter)
  • наборы (OR-Se t)
  • текстовые CRDT

Принцип:

  • изменения коммутативны
  • порядок доставки не важен
  • итоговое состояние сходится

Пример логики счётчика:

counter = counter + increment;

или более сложный вариант:

state = merge(localState, remoteState);

Конфликты на уровне Dexie транзакций

Dexie.js предоставляет транзакционный API:

db.transaction('rw', db.items, async () => {
    const item = await db.items.get(id);
    item.value += 1;
    await db.items.put(item);
});

Однако при параллельных транзакциях возможны:

  • потеря обновлений (lost update)
  • перезапись устаревших данных
  • race condition между чтением и записью

Защита через повторное чтение

Один из базовых паттернов:

await db.transaction('rw', db.items, async () => {
    const item = await db.items.get(id);

    const latest = await db.items.get(id);
    if (latest.version !== item.version) {
        throw new Error('Conflict detected');
    }

    item.value += 1;
    item.version++;

    await db.items.put(item);
});

Конфликты при синхронизации Dexie Cloud

В системах, использующих Dexie Cloud, конфликты возникают между локальной базой и сервером.

Основные сценарии:

  • локальное изменение до синхронизации
  • серверное изменение той же сущности
  • параллельные изменения на разных устройствах

Стратегии Dexie Cloud

Dexie Cloud может применять:

  • автоматическое слияние
  • LWW-стратегию
  • пользовательские resolver-функции

Типичный resolver:

function resolveConflict(local, remote) {
    return {
        ...remote,
        ...local,
        updatedAt: Math.max(local.updatedAt, remote.updatedAt)
    };
}

Политики разрешения конфликтов

Полное перезаписывание

Используется, когда:

  • данные легко восстанавливаются
  • важна скорость синхронизации

Недостаток — потеря промежуточных изменений.


Полевая стратегия (Field-level resolution)

Каждое поле рассматривается отдельно:

  • текстовые поля могут использовать LWW
  • списки — merge
  • критические поля — version check

Приоритет источника

Конфликт решается по источнику:

  • server wins
  • client wins
  • authoritative device wins

Пример:

if (remote.source === 'server') return remote;
return local;

Идемпотентность операций

Для корректного разрешения конфликтов важна идемпотентность:

  • повторное применение операции не должно изменять результат
  • операции должны иметь уникальные идентификаторы

Пример:

await db.operations.put({
    id: opId,
    type: 'increment',
    applied: true
});

Если операция уже применена, повтор игнорируется.


Оптимизация конфликтов

Батчинг изменений

Группировка изменений снижает вероятность конфликтов:

await db.transaction('rw', db.items, async () => {
    for (const change of changes) {
        await db.items.put(change);
    }
});

Минимизация времени жизни транзакции

Чем короче транзакция, тем ниже шанс конфликтов.


Использование snapshot-модели

Состояние читается как снимок:

  • все изменения основаны на одной версии данных
  • предотвращается дрейф состояния

Практические паттерны

Оптимистическое обновление UI

Локальное состояние обновляется сразу, конфликт обрабатывается позже.

db.items.update(id, { value: newValue })
    .catch(handleConflict);

Очередь изменений

Все изменения помещаются в очередь:

  • гарантируется порядок применения
  • упрощается повторная синхронизация

Shadow state

Хранится две версии:

  • localState
  • serverState

И вычисляется разница:

diff = localState - serverState;

Диагностика конфликтов

Типичные индикаторы:

  • частые rollback операций
  • несоответствие версий
  • потеря пользовательских изменений
  • увеличение latency синхронизации

Антипаттерны

Игнорирование версий

Отсутствие versioning приводит к:

  • silent overwrite
  • потерям данных

Смешивание бизнес-логики и синхронизации

Когда merge-логика встроена в UI слой, система становится трудно поддерживаемой.


Глубокие мутации без контроля

Изменение вложенных объектов без копирования приводит к непредсказуемым конфликтам.

item.settings.theme.color = 'red'; // рискованно

Структурное поведение конфликтов

Конфликт всегда является следствием одной из трёх причин:

  • нарушение последовательности событий
  • отсутствие общего состояния синхронизации
  • параллельная модификация одного объекта без координации

Dexie.js обеспечивает только локальную консистентность, тогда как стратегия разрешения конфликтов определяется архитектурой приложения и выбранной моделью синхронизации.