Real-Time Backend

Yjs на практике

Figma поддерживает 100+ одновременных редакторов в одном файле без единого конфликта - как это работает без центрального lock-менеджера?

  • Tldraw (whiteboard-инструмент, 2M+ пользователей) использует Y.Text и Y.Map для синхронизации canvas-объектов между участниками в реальном времени с latency < 50ms
  • Heptabase (knowledge management, $5M ARR) строит коллаборативные карточки на Yjs + y-redis поверх Redis Cluster, обрабатывая >1M операций/день без серверного конфликт-резолвера
  • Loom (видео-мессенджер, приобретен Atlassian за $975M) применяет Yjs для совместного редактирования субтитров и транскриптов - Y.Text поглощает параллельные правки без merge-конфликтов
  • Relm (3D-коллаборация) хранит всю сцену в Y.Array через y-webrtc, синхронизируя позиции объектов P2P без выделенного сервера

Yjs Shared Types

Yjs строит CRDT-коллаборацию поверх нескольких разделяемых типов данных, каждый из которых автоматически разрешает конфликты без координации с сервером. `Y.Doc` - корневой контейнер; все операции собираются в него и затем синхронизируются между пирами.

Основные типы

  • `Y.Text` - строка с позиционными маркерами; используется Tldraw, Loom, Heptabase для совместного редактирования текста без конфликтов
  • `Y.Map` - словарь с Last-Write-Wins семантикой на уровне ключа; подходит для метаданных и конфигурации
  • `Y.Array` - список с CRDT-позиционированием элементов; Relm (3D-коллаборация) хранит в нём сцену
  • `Y.XmlFragment` / `Y.XmlElement` - DOM-дерево с CRDT; используется ProseMirror-y-binding и TipTap

Все типы берутся через `doc.getText(name)` / `doc.getMap(name)` / `doc.getArray(name)` - имя работает как namespace внутри документа. Один и тот же `doc` может содержать произвольное число именованных коллекций.

Два пользователя одновременно вставляют текст в один и тот же `Y.Text` в позицию 3. Что произойдет после синхронизации?

Yjs Awareness

Awareness - эфемерный слой поверх Y.Doc для состояния, которое не нужно персистить: курсоры, онлайн-индикаторы, выделения. В отличие от CRDT-операций, awareness-состояния не накапливаются в истории - они живут только пока клиент подключён.

Tldraw и Heptabase используют awareness для отображения курсоров других пользователей в реальном времени. Каждый клиент публикует своё состояние; provider автоматически рассылает изменения всем подключённым пирам.

Awareness использует heartbeat: если клиент не отправлял обновлений 30 секунд (по умолчанию), провайдер удаляет его запись из общего состояния. Это решает проблему ghost-cursors при неожиданном разрыве соединения.

Чем принципиально отличается Awareness от Y.Map для хранения курсоров пользователей?

Yjs Persistence

Yjs разделяет transport (кто доставляет изменения) и storage (где они хранятся). Persistence - слой хранения: IndexedDB на клиенте, PostgreSQL/Redis на сервере. Ключевой паттерн - загрузить локальный state до подключения к серверу, чтобы документ открывался мгновенно даже offline.

Yjs хранит всю историю операций (updates) в binary формате. Со временем история растет: 10k операций в `Y.Text` могут занять ~500KB. `Y.encodeStateAsUpdate(doc)` создает минимальный snapshot текущего состояния - это используют для периодической компакции.

Зачем `IndexeddbPersistence` инициализируется ДО `WebsocketProvider` в offline-first приложении?

Yjs Providers

Provider - транспортный адаптер, который доставляет Yjs updates между пирами. Один `Y.Doc` может подключить несколько провайдеров одновременно: например WebSocket для онлайн-синхронизации и IndexedDB для offline. Updates проходят через все подключённые провайдеры автоматически.

Основные провайдеры

  • `y-websocket` - клиент-серверный провайдер; y-websocket server поддерживает до 10k одновременных комнат на одном Node.js процессе (используется Loom, Tldraw в dev)
  • `y-webrtc` - P2P через WebRTC DataChannel с signaling сервером; Relm использует для 3D-коллаборации без центрального сервера
  • `y-redis` - серверный провайдер на базе Redis Streams; Heptabase использует его поверх Redis Cluster для горизонтального масштабирования
  • `y-leveldb` - серверный persistence через LevelDB; подходит для single-node деплоя без Redis

Yjs uses `stateVector` для дедупликации: каждый update содержит `(clientID, clock)`, и провайдер не применяет update, уже присутствующий в `doc.store`. Поэтому дублирование updates через несколько провайдеров безопасно - идемпотентность встроена в протокол.

Нужно выбрать один провайдер и использовать только его, иначе данные задублируются

Несколько провайдеров для одного Y.Doc - рекомендуемый production-паттерн: WebSocket для основного канала, IndexedDB для offline, WebRTC как P2P fallback

Yjs идемпотентен по дизайну: state vector гарантирует, что каждый update применяется ровно один раз независимо от числа транспортных каналов. Heptabase, Loom и Tldraw используют 2-3 провайдера одновременно именно для resilience

Производственное приложение использует одновременно `y-websocket` и `y-webrtc` для одного Y.Doc. Клиент получает одно и то же update через оба канала. Что произойдет?

Итоги

  • Y.Doc - корневой контейнер CRDT-документа; Y.Text, Y.Map, Y.Array - типизированные разделяемые структуры с автоматическим разрешением конфликтов
  • Awareness - эфемерный слой для курсоров и online-состояния: не персистится, автоматически удаляется при отключении клиента по heartbeat (30s)
  • Persistence отделена от транспорта: IndexedDB на клиенте для offline-first, y-redis/y-leveldb на сервере для хранения истории операций
  • Несколько провайдеров для одного doc - production-норма: state vector гарантирует идемпотентность, дубликаты updates отбрасываются автоматически

Связанные темы

Yjs реализует теоретические концепции, которые изучаются в смежных уроках:

  • CRDT: алгоритмы без конфликтов — Yjs - конкретная реализация CRDT (алгоритм YATA для Y.Text); понимание теории помогает предсказывать поведение при concurrent edits
  • WebSocket и реальное время — y-websocket provider использует WebSocket как транспорт; понимание ping/pong и reconnect-логики объясняет behaviour провайдера при нестабильной сети
  • Eventual Consistency — Yjs гарантирует strong eventual consistency: все реплики сойдутся к одному состоянию после получения всех updates, без coordination

Вопросы для размышления

  • Если Y.Text хранит всю историю операций навсегда - как приложение должно управлять ростом размера документа при долгой жизни коллаборации?
  • В каком сценарии y-webrtc (P2P) предпочтительнее y-websocket (клиент-сервер) и где это архитектурное решение может создать проблемы?
  • Awareness удаляет состояние через 30 секунд после последнего heartbeat - как это влияет на UX при нестабильном мобильном соединении и как это можно компенсировать?

Связанные уроки

  • ds-10-crdts
Yjs на практике

0

1

Войти