В распределённых и офлайн-ориентированных приложениях конфликты данных возникают неизбежно. Dexie.js как высокоуровневая обёртка над IndexedDB предоставляет механизмы для управления конкурентным доступом, но не навязывает единую стратегию разрешения конфликтов. Это делает архитектурные решения полностью зависимыми от модели синхронизации приложения, особенно при использовании Dexie Cloud или кастомных серверных слоёв.
Конфликт в контексте Dexie.js — это ситуация, когда одна и та же сущность данных была изменена в двух или более независимых потоках до того, как изменения были синхронизированы между ними. Типичный сценарий: офлайн-изменения на клиенте и параллельное обновление той же записи на сервере.
Dexie.js поддерживает атомарные транзакции поверх IndexedDB. Однако при неправильной архитектуре возможно логическое пересечение изменений:
Хотя IndexedDB гарантирует атомарность транзакций, он не предотвращает логические конфликты уровня приложения.
Наиболее частый источник конфликтов связан с offline-first архитектурой:
В таких случаях Dexie.js выступает как локальное хранилище, а не как система разрешения конфликтов.
Один и тот же пользователь может работать с несколькими вкладками или устройствами:
Без стратегии разрешения конфликтов возможна потеря данных.
Наиболее простая стратегия — «последняя запись побеждает». Она широко используется в простых синхронизационных системах.
Ключевой механизм:
upd atedAtif (local.updatedAt > remote.updatedAt) {
return local;
} else {
return remote;
}
Преимущества:
Недостатки:
Более надёжный подход основан на версионности записей.
Каждая сущность содержит:
versionrevisionПри обновлении:
vvПример логики:
if (server.version !== client.version) {
throw new ConflictError();
}
В Dexie.js это часто моделируется через дополнительное поле:
db.items.update(id, {
value: newValue,
version: oldVersion + 1
});
Преимущество:
Недостаток:
Для структурированных данных применяется стратегия слияния.
Пример: объект пользователя с частично независимыми полями.
const merged = {
...remote,
...local,
preferences: {
...remote.preferences,
...local.preferences
}
};
Этот подход особенно эффективен для:
Риски:
Conflict-free Replicated Data Types применяются в сложных распределённых системах. Dexie.js не предоставляет CRDT из коробки, но может использоваться как локальный слой хранения CRDT-структур.
Примеры структур:
Принцип:
Пример логики счётчика:
counter = counter + increment;
или более сложный вариант:
state = merge(localState, remoteState);
Dexie.js предоставляет транзакционный API:
db.transaction('rw', db.items, async () => {
const item = await db.items.get(id);
item.value += 1;
await db.items.put(item);
});
Однако при параллельных транзакциях возможны:
Один из базовых паттернов:
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 может применять:
Типичный resolver:
function resolveConflict(local, remote) {
return {
...remote,
...local,
updatedAt: Math.max(local.updatedAt, remote.updatedAt)
};
}
Используется, когда:
Недостаток — потеря промежуточных изменений.
Каждое поле рассматривается отдельно:
Конфликт решается по источнику:
Пример:
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);
}
});
Чем короче транзакция, тем ниже шанс конфликтов.
Состояние читается как снимок:
Локальное состояние обновляется сразу, конфликт обрабатывается позже.
db.items.update(id, { value: newValue })
.catch(handleConflict);
Все изменения помещаются в очередь:
Хранится две версии:
localStateserverStateИ вычисляется разница:
diff = localState - serverState;
Типичные индикаторы:
Отсутствие versioning приводит к:
Когда merge-логика встроена в UI слой, система становится трудно поддерживаемой.
Изменение вложенных объектов без копирования приводит к непредсказуемым конфликтам.
item.settings.theme.color = 'red'; // рискованно
Конфликт всегда является следствием одной из трёх причин:
Dexie.js обеспечивает только локальную консистентность, тогда как стратегия разрешения конфликтов определяется архитектурой приложения и выбранной моделью синхронизации.