Реалізація CRDT/OT для real-time синхронізації на сайті

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.
Розробка та обслуговування будь-яких видів сайтів:
Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація CRDT/OT для real-time синхронізації на сайті
Складна
~2-4 тижні
Часті питання
Наші компетенції:
Етапи розробки
Останні роботи
  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація CRDT/OT для real-time синхронізації на сайті

Real-time синхронізація кількох клієнтів без конфліктів — задача, що на практиці складніша, ніж виглядає. Проста трансляція подій через WebSocket працює поки два користувачі не редагують один фрагмент одночасно. Без формальної моделі даних — каша.

OT проти CRDT: принципіальна різниця

Operational Transformation (OT) — алгоритм, що трансформує операції з урахуванням конкуруючих змін. Застосовується в Google Docs з 2006 року. Суть: якщо користувач A вставив символ на позицію 5, а користувач B видалив символ на позиції 3, то операція A повинна бути трансформована перед застосуванням у B.

Алгоритм працює коректно лише при наявності центрального сервера-арбітра, який упорядковує операції. Без нього реалізація OT для 2+ клієнтів стає експоненціально складною.

CRDT (Conflict-free Replicated Data Types) — структури даних, що математично гарантують eventual consistency без координації. Операції комутативні та ідемпотентні — порядок застосування не впливає на результат.

Критерій OT CRDT
Центральний сервер Обов'язковий Опціональний (P2P можливий)
Offline-підтримка Складна Нативно
Продуктивність документа Висока Залежить від типу
Реалізація Складна, багато edge cases Простіша з бібліотеками
Rich text Google Docs, Quill Yjs, Automerge

CRDT на практиці: Yjs

Найзрілішою CRDT-бібліотека для браузера та Node.js. Будується з Y.Doc, що містить shared types: Y.Text, Y.Map, Y.Array.

npm install yjs y-websocket y-protocols

Сервер (y-websocket):

const { setupWSConnection } = require('y-websocket/bin/utils');
const http = require('http');
const { WebSocketServer } = require('ws');

const server = http.createServer();
const wss = new WebSocketServer({ server });

wss.on('connection', (ws, req) => {
  setupWSConnection(ws, req, {
    docName: req.url.slice(1),
    gc: true,
  });
});

server.listen(1234);

Клієнт з інтеграцією редактора:

import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
import { QuillBinding } from 'y-quill';
import Quill from 'quill';

const ydoc = new Y.Doc();
const provider = new WebsocketProvider(
  'ws://localhost:1234',
  'my-document-room',
  ydoc,
  { connect: true }
);

provider.on('status', ({ status }) => {
  console.log('WS status:', status); // 'connected' | 'disconnected'
});

provider.on('sync', (isSynced: boolean) => {
  if (isSynced) console.log('Document synced from server');
});

const ytext = ydoc.getText('quill-content');
const quill = new Quill('#editor', { theme: 'snow' });
const binding = new QuillBinding(ytext, quill, provider.awareness);

provider.awareness.setLocalStateField('user', {
  name: 'Ivan Petrov',
  color: '#4a9eff',
});

Y.Map та Y.Array для структурованих даних

Текстові редактори — окремий випадок. CRDT застосовується ширше: синхронізація стану форм, kanban дошки, діаграми.

// Синхронізована карта задач (Kanban-дошка)
const ytasks = ydoc.getMap<Y.Map<unknown>>('tasks');

function createTask(id: string, title: string, status: string) {
  const task = new Y.Map<unknown>();
  task.set('id', id);
  task.set('title', title);
  task.set('status', status);
  task.set('createdAt', Date.now());
  ytasks.set(id, task);
}

function moveTask(id: string, newStatus: string) {
  const task = ytasks.get(id);
  if (task) {
    task.set('status', newStatus);
  }
}

// Реактивне оновлення UI
ytasks.observe((event) => {
  event.changes.keys.forEach((change, key) => {
    if (change.action === 'add') {
      renderTask(ytasks.get(key));
    } else if (change.action === 'delete') {
      removeTaskFromUI(key);
    } else if (change.action === 'update') {
      updateTaskInUI(key, ytasks.get(key));
    }
  });
});

Персистентність: зберігання Y.Doc на сервері

Ephemeral y-websocket втрачає документ при перезапуску. Production потребує persistence layer.

// Варіант 1: y-leveldb (single-node)
const { LeveldbPersistence } = require('y-leveldb');
const persistence = new LeveldbPersistence('./data');

setupWSConnection(ws, req, {
  docName,
  gc: true,
  persistence,
});

// Варіант 2: Redis (multi-node via y-redis)
import { createRedisStorage } from 'y-redis';

const redisStorage = createRedisStorage({
  host: 'localhost',
  port: 6379,
});

// Варіант 3: PostgreSQL з Hocuspocus
import { Server } from '@hocuspocus/server';
import { Database } from '@hocuspocus/extension-database';
import { Pool } from 'pg';

const pool = new Pool({ connectionString: process.env.DATABASE_URL });

const server = Server.configure({
  port: 1234,
  extensions: [
    new Database({
      fetch: async ({ documentName }) => {
        const { rows } = await pool.query(
          'SELECT data FROM documents WHERE name = $1',
          [documentName]
        );
        return rows[0]?.data ?? null;
      },
      store: async ({ documentName, state }) => {
        await pool.query(
          `INSERT INTO documents (name, data, updated_at)
           VALUES ($1, $2, NOW())
           ON CONFLICT (name)
           DO UPDATE SET data = $2, updated_at = NOW()`,
          [documentName, Buffer.from(state)]
        );
      },
    }),
  ],
});

server.listen();

Automerge як альтернатива

Automerge 2.x (Rust/WASM) краща продуктивність для великих документів, нативна підтримка JSON-подібних структур:

import * as Automerge from '@automerge/automerge';

let doc = Automerge.init<{ items: string[] }>();

doc = Automerge.change(doc, 'Add item', (d) => {
  if (!d.items) d.items = [];
  d.items.push('new item');
});

const binary = Automerge.save(doc);

const [newDoc, patch] = Automerge.applyChanges(doc, [remoteChange]);

Розрішення конфліктів у CRDT

CRDT не устраняє конфлікти — визначає детермінований переможця. Для Last-Write-Wins Map (LWW-Map) перемагає операція з пізнішим timestamp. Для текста Yjs позиція вставки визначається сусідами, не індексом — стійко до конкуруючих вставок.

Граничний випадок: два користувачі одночасно видаляють і редагують один елемент. У LWW видалення перемагає, але Yjs зберігає контент як "надгробок" (tombstone) — дозволяє корректно застосувати зміни, зроблені до видалення.

Масштабування: multi-node синхронізація

Один WebSocket-сервер не масштабується горизонтально — кожен клієнт підключений до конкретного процесу. Рішення:

Sticky sessions на рівні nginx (document_id → upstream):

upstream yjs_backend {
    hash $arg_room consistent;
    server yjs1:1234;
    server yjs2:1234;
    server yjs3:1234;
}

Pub/Sub через Redis — кожен сервер публікує оновлення у Redis канал, решта підписана. Hocuspocus з розширенням Redis підтримує з коробки.

Liveblocks/PartyKit — managed інфраструктура для CRDT, якщо немає бажання підтримувати власний кластер.

Терміни реалізації

Базова CRDT-синхронізація текстового редактора на Yjs + y-websocket: 3–5 днів. Додавання персистентності через PostgreSQL/Redis: ще 2–3 дні. Multi-node з Redis pub/sub та тестуванням під навантаженням: плюс тиждень. Кастомні типи даних (Kanban, діаграми) поверх Y.Map: залежить від UI-складності.