Skip to content

Repository files navigation

Sync5 Chat

Многопользовательский чат с каналами и инкрементальной синхронизацией данных (Sync Engine).

Архитектура

┌─────────────────────────────────────────────────────────────────┐
│                         SYNC5 CHAT                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌──────────────────┐         ┌──────────────────────────────┐  │
│  │   Web Client     │◄───────►│         Server               │  │
│  │   (React + Vite) │   REST  │     (Hono + Drizzle)         │  │
│  │                  │   API   │                              │  │
│  │  ┌────────────┐  │         │  ┌────────────────────────┐  │  │
│  │  │   Dexie    │  │◄───────►│  │   SQLite Database      │  │  │
│  │  │  (IndexDB) │  │  WebSocket │                        │  │  │
│  │  └────────────┘  │         │  │  - Users               │  │  │
│  │                  │         │  │  - Channels            │  │  │
│  │  ┌────────────┐  │         │  │  - Messages            │  │  │
│  │  │ TanStack   │  │         │  │  - server_version      │  │  │
│  │  │   Query    │  │         │  └────────────────────────┘  │  │
│  │  └────────────┘  │         │                              │  │
│  └──────────────────┘         └──────────────────────────────┘  │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Структура проекта

sync5/
├── apps/
│   ├── server/          # Hono API сервер
│   │   ├── src/
│   │   │   ├── db/      # Drizzle ORM схема и подключение
│   │   │   ├── lib/     # Утилиты, auth, websocket
│   │   │   └── routes/  # API роуты (channels, messages, sync)
│   │   └── data/        # SQLite база данных
│   └── web/             # React клиент
│       └── src/
│           ├── components/  # UI компоненты
│           ├── hooks/       # React хуки (WebSocket)
│           ├── lib/         # DB, auth client
│           ├── pages/       # Страницы (Login, Register)
│           └── stores/      # Zustand stores (sync)
└── packages/
    ├── eslint-config/   # Общая ESLint конфигурация
    └── typescript-config/ # Общие tsconfig

Быстрый старт

# Установка зависимостей
pnpm install

# Инициализация базы данных
pnpm --filter @sync5/server db:push

# Запуск в режиме разработки
pnpm dev

Технологический стек

Сервер

  • Hono — легковесный веб-фреймворк
  • Drizzle ORM — типобезопасный ORM для SQLite
  • better-auth — аутентификация (email/password)
  • Zod OpenAPI — генерация Swagger документации
  • WebSocket (ws) — real-time уведомления

Клиент

  • React 19 + Vite
  • TanStack Query — управление серверным состоянием
  • Dexie — IndexedDB обёртка для локального хранения
  • Zustand — глобальное состояние (sync status)
  • TailwindCSS — стилизация
  • Vite PWA — offline поддержка

Sync Engine: Подробное описание

Концепция

Sync Engine решает проблему синхронизации данных между сервером и множеством клиентов с поддержкой:

  • Offline-first — приложение работает без интернета
  • Optimistic UI — мгновенная реакция на действия пользователя
  • Инкрементальная синхронизация — передача только изменённых данных
  • Conflict resolution — разрешение конфликтов "последняя запись побеждает"

Механизм версионирования

Server Version

Глобальный инкрементальный счётчик на сервере:

CREATE TABLE server_version (
  id INTEGER PRIMARY KEY,
  version INTEGER NOT NULL DEFAULT 0
);

Логика:

  1. При каждой CRUD операции счётчик увеличивается на 1
  2. Текущее значение записывается в поле last_write_server_version изменённой сущности
// Пример создания сообщения
const serverVer = incrementServerVersion(); // 50123 -> 50124

await db.insert(messages).values({
  id: messageId,
  content: "Hello!",
  lastWriteServerVersion: serverVer, // 50124
  // ...
});

Sync Endpoint

GET /api/sync?since=50100&channelId=ch123

Ответ:

{
  "toVersion": 50124,
  "changes": [
    {
      "entity": "message",
      "recordId": "msg456",
      "data": {
        "id": "msg456",
        "content": "Hello!",
        "lastWriteServerVersion": 50124
      },
      "deleted": false
    },
    {
      "entity": "channel",
      "recordId": "ch789",
      "data": { ... },
      "deleted": true
    }
  ]
}

Клиентская синхронизация

Локальная база данных (Dexie/IndexedDB)

// lib/db.ts
class SyncDatabase extends Dexie {
  channels!: Table<LocalChannel>;
  messages!: Table<LocalMessage>;
  syncMetadata!: Table<SyncMetadata>;

  constructor() {
    super("sync5-chat");
    this.version(1).stores({
      channels: "id, lastWriteServerVersion, deleted",
      messages: "id, channelId, lastWriteServerVersion, deleted, pendingSync",
      syncMetadata: "id",
    });
  }
}

Процесс синхронизации

┌─────────────────────────────────────────────────────────────────┐
│                     SYNC FLOW                                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  1. Клиент запрашивает: GET /sync?since={lastSyncedVersion}     │
│                                                                  │
│  2. Сервер возвращает все записи где:                           │
│     last_write_server_version > since                           │
│                                                                  │
│  3. Клиент применяет изменения к локальной БД:                  │
│     - Если запись новая → INSERT                                │
│     - Если serverVersion > localVersion → UPDATE                │
│     - Если deleted=true → помечаем удалённой                    │
│                                                                  │
│  4. Клиент сохраняет toVersion как lastSyncedVersion            │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
// stores/sync.ts
async function sync(channelId?: string) {
  const lastSyncedVersion = await getLastSyncedVersion();

  const response = await fetch(`/api/sync?since=${lastSyncedVersion}`);
  const { toVersion, changes } = await response.json();

  // Применяем изменения
  await applySyncChanges(changes);

  // Сохраняем версию
  await setLastSyncedVersion(toVersion);
}

Optimistic UI

При отправке сообщения:

async function handleSend() {
  const tempId = `temp-${Date.now()}`;

  // 1. Немедленно добавляем в локальную БД
  await db.messages.put({
    id: tempId,
    content: message,
    pendingSync: true, // Флаг ожидания синхронизации
    // ...
  });

  // 2. UI сразу показывает сообщение

  // 3. Отправляем на сервер
  const response = await fetch("/api/channels/ch1/messages", {
    method: "POST",
    body: JSON.stringify({ content: message }),
  });

  // 4. Заменяем временное сообщение на серверное
  const serverMessage = await response.json();
  await db.messages.delete(tempId);
  await db.messages.put({
    ...serverMessage,
    pendingSync: false,
  });
}

Real-time обновления (WebSocket)

┌─────────────────────────────────────────────────────────────────┐
│                   WEBSOCKET FLOW                                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Client A                Server               Client B           │
│     │                      │                      │              │
│     │──── auth ──────────►│                      │              │
│     │◄─── auth_success ───│                      │              │
│     │                      │                      │              │
│     │── subscribe(ch1) ──►│                      │              │
│     │◄─── subscribed ─────│                      │              │
│     │                      │◄── subscribe(ch1) ──│              │
│     │                      │──── subscribed ────►│              │
│     │                      │                      │              │
│     │── POST message ────►│                      │              │
│     │                      │── new_message ─────►│              │
│     │                      │                      │              │
│     │                      │  Client B applies   │              │
│     │                      │  change to local DB │              │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Conflict Resolution

Стратегия: Last Write Wins (LWW)

async function applySyncChanges(changes) {
  for (const change of changes) {
    const existing = await db.messages.get(change.recordId);

    // Обновляем только если серверная версия новее
    if (
      !existing ||
      change.data.lastWriteServerVersion > existing.lastWriteServerVersion
    ) {
      await db.messages.put({
        ...change.data,
        deleted: change.deleted,
      });
    }
  }
}

Soft Delete

Для синхронизации удалений используется поле deleted_at:

// Сервер: soft delete
await db.update(messages)
  .set({
    deletedAt: new Date(),
    lastWriteServerVersion: incrementServerVersion(),
  })
  .where(eq(messages.id, messageId));

// Клиент получает при синхронизации
{
  "entity": "message",
  "recordId": "msg123",
  "data": { ... },
  "deleted": true  // Флаг удаления
}

Offline Support

PWA конфигурация для работы без сети:

// vite.config.ts
VitePWA({
  registerType: "autoUpdate",
  workbox: {
    runtimeCaching: [
      {
        urlPattern: /^https:\/\/localhost:4000\/api\/.*/i,
        handler: "NetworkFirst",
        options: {
          cacheName: "api-cache",
          networkTimeoutSeconds: 10,
        },
      },
    ],
  },
});

API Reference

Authentication

Endpoint Method Description
/api/auth/sign-up/email POST Регистрация
/api/auth/sign-in/email POST Вход
/api/auth/sign-out POST Выход
/api/auth/session GET Текущая сессия

Channels

Endpoint Method Description
/api/channels GET Список каналов пользователя
/api/channels POST Создать канал
/api/channels/:id GET Получить канал
/api/channels/:id PATCH Обновить канал
/api/channels/:id DELETE Удалить канал (soft delete)
/api/channels/:id/join POST Присоединиться к каналу
/api/channels/:id/members GET Участники канала

Messages

Endpoint Method Description
/api/channels/:id/messages GET Сообщения в канале
/api/channels/:id/messages POST Отправить сообщение
/api/channels/:id/messages/:msgId PATCH Редактировать
/api/channels/:id/messages/:msgId DELETE Удалить

Sync

Endpoint Method Description
/api/sync?since=N GET Получить изменения с версии N
/api/sync/version GET Текущая версия сервера

Запуск тестов

# Тест синхронизации
# 1. Откройте приложение в двух браузерах/вкладках
# 2. Войдите под разными пользователями
# 3. Создайте канал и добавьте второго пользователя
# 4. Отправляйте сообщения — они появятся в обоих окнах в реальном времени
# 5. Отключите сеть (DevTools → Network → Offline)
# 6. Отправьте сообщение — оно появится локально с меткой "Sending..."
# 7. Включите сеть — сообщение синхронизируется

Возможные улучшения

  1. Vector Clocks — для более точного разрешения конфликтов
  2. CRDT — бесконфликтные типы данных для совместного редактирования
  3. Delta Sync — передача только изменённых полей, а не всей записи
  4. Compression — сжатие данных при синхронизации
  5. Pagination — постраничная загрузка при большом количестве изменений
  6. Retry Queue — очередь повторных попыток для failed операций

Лицензия

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages