Сервер синхронизации для Dexie.js обычно строится вокруг модели событийной репликации, где клиентские изменения представляют собой поток операций, а сервер выступает как агрегатор, валидатор и распределитель дельт между устройствами и пользователями. В отличие от классических CRUD-backend’ов, здесь центральной задачей становится не хранение текущего состояния, а корректная обработка истории изменений, включая оффлайн-модификации, конфликты и повторную доставку событий.
Система синхронизации строится вокруг трёх основных компонентов:
Ключевая идея заключается в том, что клиент никогда не зависит от постоянного соединения. Все изменения фиксируются локально и затем отправляются на сервер в виде операций.
Операции обычно имеют форму:
Каждая операция содержит:
Серверная схема редко совпадает с клиентской 1:1. Чаще используется слой журналирования:
type SyncOperation = {
id: string;
userId: string;
table: string;
type: "create" | "update" | "delete";
payload: any;
updatedAt: number;
deviceId: string;
baseVersion?: number;
};
Дополнительно хранится текущая версия состояния:
type ServerEntity = {
id: string;
dat a: any;
version: number;
deleted: boolean;
};
Важно разделять:
Такой подход упрощает восстановление и позволяет делать replay при конфликтных ситуациях.
Типичный цикл выглядит следующим образом:
Локальная очередь является критическим компонентом. Она должна обеспечивать:
Пример структуры очереди:
type OutboxItem = {
op: SyncOperation;
status: "pending" | "sent" | "failed";
retryCount: number;
};
Отправка обычно реализуется через batching:
Сервер должен обеспечивать идемпотентность. Это достигается через уникальные идентификаторы операций.
Алгоритм обработки:
Пример Node.js обработчика:
async function applyOperation(op) {
const exists = await db.ops.findOne({ id: op.id });
if (exists) return;
const entity = await db.entities.findOne({ id: op.payload.id });
if (op.type === "update") {
if (entity.version !== op.baseVersion) {
return handleConflict(op, entity);
}
entity.data = { ...entity.data, ...op.payload.data };
entity.version += 1;
}
if (op.type === "create") {
await db.entities.insert({
id: op.payload.id,
data: op.payload.data,
version: 1,
deleted: false
});
}
if (op.type === "delete") {
entity.deleted = true;
entity.version += 1;
}
await db.ops.insert(op);
await db.entities.save(entity);
}
Конфликты неизбежны при оффлайн-работе. Основные стратегии:
Самый простой подход. Побеждает операция с более поздним timestamp.
Недостатки:
Используется baseVersion:
Более сложная стратегия:
Для минимизации задержек используется постоянное соединение:
Пример события:
{
"type": "delta",
"table": "notes",
"op": "update",
"payload": {
"id": "123",
"changes": {
"title": "new title"
},
"version": 5
}
}
Клиент применяет изменения только если локальная версия меньше серверной.
Вместо полной синхронизации используется механизм “since cursor”:
GET /sync?since=1700000000
Сервер возвращает:
Это уменьшает трафик и ускоряет синхронизацию больших наборов данных.
Удаление не означает физическое удаление записи. Используются tombstones:
{
id: "123",
deleted: true,
version: 10
}
Причины:
Сервер синхронизации обязательно включает:
Пример middleware:
function auth(req, res, next) {
const token = req.headers.authorization;
const user = verifyJWT(token);
req.user = user;
next();
}
При росте нагрузки появляются узкие места:
Решение:
Проблема:
Решение:
Используются кэш-слои:
На стороне Dexie.js важны:
Типичная логика merge:
Сервер синхронизации должен поддерживать эволюцию структуры данных:
Пример:
if (op.schemaVersion < 2) {
op.payload = migrateV1toV2(op.payload);
}
Сетевые сбои приводят к повторной отправке операций. Поэтому:
В распределённой синхронизации обычно используется eventual consistency:
Такая модель оптимальна для оффлайн-first приложений, построенных на Dexie.js, где приоритет отдан скорости локальной работы, а не мгновенной глобальной согласованности.