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 при нестабильном мобильном соединении и как это можно компенсировать?