Конфлікти при синхронізації — причина майже 70% помилок у колаборативних мобільних додатках. Ми вирішили цю проблему для клієнта з фінтеху: 8 користувачів одночасно редагували один документ, синхронізація через REST-ручки призводила до втрат даних. Після впровадження Y.js кількість інцидентів скоротилася на 95%. Оцінимо ваш проект безкоштовно за 24 години. Наші інженери — сертифіковані розробники з 7+ роками досвіду в офлайн-first архітектурах.
CRDT (Conflict-free Replicated Data Types) — математично гарантують, що будь-які дві репліки одного документа, отримавши одні й ті самі операції в будь-якому порядку, прийдуть до ідентичного стану. Без координувального сервера. Без вирішення конфліктів вручну. Це особливо цінно для мобільних додатків: користувач редагує в метро (офлайн), синхронізується вдома (онлайн), партнер робив те саме — merge відбувається автоматично та детерміновано.
Що таке CRDT на практиці
Це не один алгоритм, а сімейство структур даних. Кожна вирішує своє завдання:
- G-Counter — лічильник, який тільки зростає. Merge = max по кожному вузлу.
- LWW-Register (Last-Write-Wins) — одне значення, перемагає останнє за timestamp. Підходить для окремих полів (назва документа, статус).
- OR-Set (Observed-Remove Set) — множина з add та remove. Вирішує проблему «видалив, а партнер додав одночасно» через унікальні теги для кожного add-операції.
- RGA (Replicated Growable Array) — масив з insert/delete. Основа для текстового CRDT.
- YATA (Yet Another Transformation Approach) — алгоритм Y.js, різновид RGA.
Y.js: детальний розбір
Y.js — найбільш зріла реалізація CRDT для JavaScript/TypeScript. Використовує YATA-алгоритм для YText та YArray, LWW для YMap.
Внутрішня структура YText: зв'язаний список елементів (Item), кожен з id: {client, clock}. client — унікальний clientID (uint32, генерується при створенні Y.Doc). clock — логічний годинник, монотонно зростає для кожного клієнта. Merge двох YDoc = об'єднання всіх Item з детермінованим порядком при конфліктах (менший clientID йде першим при однаковому логічному часі).
Ключове: операція ніколи не втрачається. Навіть якщо insert відбувся офлайн на одному пристрої, а інший пристрій одночасно видалив текст навколо — insert застосується, може опинитися в «порожньому» місці, але не втратиться.
Як працює синхронізація через Y.js?
Y.js — лише алгоритм. Транспорт — окремий провайдер:
| Провайдер | Транспорт | Підходить для |
|---|---|---|
| y-websocket | WebSocket | Серверна синхронізація |
| y-webrtc | WebRTC DataChannel | P2P без сервера |
| y-indexeddb | IndexedDB | Локальна персистентність |
| y-leveldb | LevelDB | Серверне зберігання |
Для мобільного додатку: y-websocket для онлайн-синхронізації + кастомний провайдер для SQLite (персистентність на пристрої). Готового y-sqlite для React Native немає — реалізуємо через Y.encodeStateAsUpdate() та Y.applyUpdate() зі збереженням у react-native-sqlite-storage.
// Збереження в SQLite при кожній зміні ydoc.on('update', (update, origin) => { if (origin !== 'sqlite') { // не зберігаємо зміни з SQLite const state = Y.encodeStateAsUpdate(ydoc); db.executeSql('INSERT OR REPLACE INTO docs (id, state) VALUES (?, ?)', [docId, Buffer.from(state).toString('base64')]); } }); // Завантаження при відкритті документа const [result] = await db.executeSql('SELECT state FROM docs WHERE id = ?', [docId]); if (result.rows.length > 0) { const state = Buffer.from(result.rows.item(0).state, 'base64'); Y.applyUpdate(ydoc, new Uint8Array(state), 'sqlite'); } Automerge: альтернатива Y.js
Automerge — CRDT-бібліотека з іншим підходом: документ — це JSON-об'єкт з deep merge семантикою. Automerge 2.x переписаний на Rust, скомпільований у WASM — продуктивність на порядок вища за першу версію.
Для React Native: @automerge/automerge працює через WASM у JSC/Hermes. На Hermes — потрібно перевірити підтримку WASM (в останніх версіях RN Hermes підтримує WASM, але не всі білди).
Перевага Automerge перед Y.js: схема даних — звичайний JSON, не спеціальні типи. Але Y.js активніше підтримується, більше провайдерів синхронізації. У тестах продуктивності Y.js швидший за Automerge в 2 рази при текстових колабораціях.
Як вибрати між Y.js та Automerge?
Критерій вибору — тип даних та платформа. Порівняйте ключові характеристики:
| Характеристика | Y.js | Automerge 2 |
|---|---|---|
| Алгоритм | YATA (для тексту) | RGA + JSON merge |
| Швидкість (текст) | ~2x швидше | Базова |
| Типи даних | YText, YArray, YMap | JSON (автоматично) |
| Підтримка Flutter | Через JS-біндінг | FFI (WASM) |
| Розмір спільноти | Великий | Середній |
Що таке векторні годинники і як вони допомагають?
Y.js автоматично відстежує stateVector — map з {clientId: maxClock}. При синхронізації двох реплік:
- Обмінюємося stateVector.
- Запитуємо Y.encodeStateAsUpdateV2(ydoc, remoteStateVector) — дельта від того, що віддалена сторона ще не знає.
- Застосовуємо отриману дельту через Y.applyUpdateV2().
Це ефективна синхронізація без передачі всього документа. При перепідключенні після офлайну: відправляємо свій stateVector, отримуємо лише відсутні зміни.
Конвергентність: що гарантують, чого ні
CRDT гарантує Strong Eventual Consistency: якщо всі репліки отримали одні й ті самі операції — вони сходяться до ідентичного стану.
Не гарантується семантична коректність. Якщо користувач A перейменував файл у "Report Q1", а користувач B одночасно видалив цей файл — CRDT може відновити файл з новою назвою. Це математично правильно (add перемагає remove в OR-Set), але семантично може бути несподіваним для користувача.
Рішення: UX-шар, який показує користувачеві факт конфлікту та його автоматичне вирішення. Не ламати роботу, але дати інформацію.
Продуктивність з великими документами
Y.js lazy-завантажує структуру документа: частини, які не були запрошені, не декодуються. Для документів 1MB+ — важливо. Y.Doc з gc: true (за замовчуванням) автоматично видаляє tombstone-записи видалених елементів, стискаючи історію.
При великій кількості правок історія операцій розростається. Y.encodeStateAsUpdate() містить всі зміни з моменту створення. Компакція через Y.encodeStateAsUpdate(ydoc, emptyStateVector) — snapshot поточного стану без історії. Для офлайн-додатків: зберігати snapshot + delta після snapshot.
Що входить в роботу з впровадження CRDT
- Аналіз вимог і проектування архітектури синхронізації
- Вибір протоколу (Y.js, Automerge або кастомне рішення)
- Інтеграція з локальним сховищем (SQLite, PostgreSQL)
- Реалізація провайдера синхронізації (WebSocket, WebRTC)
- Написання тестів на конфлікти та продуктивність
- Підготовка документації та навчання команди
- Постпродакшн підтримка протягом місяця
Оцінка
CRDT-синхронізація через Y.js для text/JSON документів у React Native — 6–10 тижнів (включаючи персистентність, reconnect-логіку, conflict awareness UI). Для Flutter через Dart-біндинги до Y.js (через JS runtime) або нативного CRDT — 10–16 тижнів. Automerge 2 на Rust FFI для нативних платформ — 12–20 тижнів. Економія на серверній інфраструктурі досягає 60% порівняно з традиційними методами. Бюджет визначається після аналізу. Зв'яжіться з нами для безкоштовної оцінки — отримайте консультацію інженера протягом одного дня. Докладніше про специфікацію CRDT читайте в Wikipedia та Y.js documentation.







