Real-Time Backend
Automerge
Два пользователя редактируют один документ офлайн на самолёте. Приземляются - изменения нужно слить. Без CRDT это ручной конфликт. С Automerge - автоматически за миллисекунды.
- Logseq - граф заметок с 300k+ активными пользователями использует Automerge для синхронизации между устройствами без центрального сервера; пользователь редактирует ноутбук и телефон офлайн - при подключении конфликты решаются автоматически
- Actual Budget хранит финансовые транзакции как Automerge-документ; sync-сервер видит только зашифрованные бинарные блобы без доступа к данным - privacy-first архитектура на CRDT
- Ink & Switch Research Lab (создатели Automerge) опубликовали манифест local-first software (2019, ~50k прочтений); их PushPin collaborative canvas доказал, что P2P приложения с CRDT работают надёжнее централизованных при нестабильном соединении
- Figma использует собственную CRDT-систему для совместного редактирования в реальном времени с задержкой <100ms при 50+ одновременных пользователях - коммерческое подтверждение масштабируемости подхода
Что такое Automerge
**Automerge** - это библиотека CRDT, которая превращает обычный JavaScript-объект в структуру данных, поддерживающую автоматическое слияние конфликтующих изменений. Ink & Switch Research Lab создали её как фундамент для local-first приложений: данные хранятся на устройстве, синхронизируются между пирами без центрального сервера.
Ключевая идея: каждое изменение документа получает уникальный идентификатор (actor ID + sequence counter). При слиянии двух версий документа Automerge применяет детерминированные правила, гарантирующие одинаковый результат независимо от порядка получения изменений.
Automerge используется в таких проектах как PushPin (collaborative canvas от Ink & Switch), Logseq (локальная база знаний с 300k+ пользователями) и Actual Budget (финансовое приложение с локальными данными). Библиотека v2.0 переписана на Rust с WebAssembly биндингами - бинарный формат хранения в 10x меньше JSON.
Что гарантирует детерминированное слияние в Automerge при одновременных изменениях на двух клиентах?
JSON CRDT внутри Automerge
Automerge реализует **JSON CRDT** - расширение операционных CRDT для произвольных JSON-структур. Каждый тип данных в документе имеет свою стратегию слияния: Map использует LWW (last-write-wins) по actor ID для скалярных значений, List реализует RGA (Replicated Growable Array) для сохранения относительного порядка вставок.
| Тип данных | CRDT алгоритм | Поведение при конфликте |
|---|---|---|
| Map (объект) | Multi-Value Register | Оба значения сохраняются, приложение выбирает |
| List (массив) | RGA (Replicated Growable Array) | Порядок определяется по actor ID вставившего |
| Text | RGA с character granularity | Символы вставляются по логической позиции |
| Counter | Increment-only CRDT | Все инкременты суммируются |
RGA (Replicated Growable Array) - алгоритм, разработанный Hyun-Gul Roh et al. в 2011 году. Каждый элемент списка получает уникальный идентификатор. При вставке нового элемента указывается идентификатор предшественника. Это позволяет корректно обрабатывать concurrent inserts без координации.
Два пользователя одновременно вставляют элемент после 'a' в список ['a', 'c']. Как Automerge определяет итоговый порядок?
Протокол синхронизации Automerge
Automerge v2 включает встроенный **sync protocol** на основе bloom filters и операций обмена недостающими изменениями. Два пира обмениваются минимальным количеством сообщений для синхронизации, не передавая весь документ целиком.
Протокол работает через `SyncState` - объект, хранящий информацию о том, что уже знает пир. При каждом раунде обмена генерируется `SyncMessage` с bloom filter известных изменений. Получив его, другой пир вычисляет, какие изменения нужно отправить.
Bloom filter в sync protocol позволяет пиру компактно закодировать множество известных изменений (100k операций = ~10KB фильтр). Это critical optimization: без него пришлось бы передавать список всех ID изменений, что для большого документа - мегабайты. Ложноположительные срабатывания bloom filter безопасны - они лишь означают ненужную передачу уже известного изменения.
- SyncState создаётся отдельно для каждого peer-соединения
- generateSyncMessage производит null когда синхронизация завершена
- receiveSyncMessage атомарна: либо применяет изменения, либо нет
- Протокол работает поверх любого транспорта: WebSocket, WebRTC, HTTP
Зачем Automerge sync protocol использует bloom filter вместо полного списка ID изменений?
Хранение документов Automerge
Automerge v2 использует **бинарный формат** на основе columnar encoding (аналогично Apache Arrow). Документ хранит не только текущее состояние, но и полную историю изменений - это позволяет синхронизироваться с пирами, которые пропустили часть обновлений.
Для production-приложений критично управление размером истории. Automerge поддерживает **compaction**: создание нового документа с текущим состоянием без истории. После compaction нельзя синхронизироваться с пирами, у которых есть более старые версии - нужно передать полный снапшот.
automerge-repo (официальная высокоуровневая библиотека) берёт на себя хранение, синхронизацию и управление документами. Адаптеры хранилищ: IndexedDB (браузер), SQLite (Node.js), filesystem. Сетевые адаптеры: WebSocket, WebRTC, BroadcastChannel. Реальный кейс: Actual Budget хранит финансовые данные пользователя как Automerge документ, синхронизируя между устройствами через собственный sync-сервер без доступа к данным.
Automerge автоматически удаляет старую историю и всегда держит документ компактным
История изменений накапливается бесконечно - без явного compaction документ растёт пропорционально числу операций
История нужна для синхронизации с пирами, которые могли быть офлайн. Automerge не знает, какие изменения уже получили все пиры, поэтому хранит всё. Compaction - явное решение разработчика с трейдоффом совместимости синхронизации.
Документ Automerge вырос до 50MB из-за накопленной истории изменений. Что происходит после compaction (save + load)?
Итоги
- Automerge - JSON CRDT библиотека: каждый JavaScript-объект может стать автоматически сливаемым документом с полной историей изменений
- Map использует Multi-Value Register (конфликты доступны через getConflicts), List - RGA алгоритм с детерминированным порядком concurrent вставок по actor ID
- Sync protocol на bloom filters минимизирует трафик: вместо передачи полных списков ID изменений - компактные фильтры; обычно 2-3 раунда обмена достаточно
- История изменений накапливается и требует явного compaction (save+load); после этого отставшие пиры синхронизируются только через полный снапшот
Связанные темы
Automerge строится на фундаментальных концепциях распределённых систем и используется в local-first архитектурах.
- CRDT (Conflict-free Replicated Data Types) — Automerge - конкретная реализация операционных CRDT для JSON-документов
- Operational Transformation — Альтернативный подход к collaborative editing; OT требует центрального сервера, CRDT - нет
- Vector Clocks — Automerge использует логические часы (actor ID + sequence) для упорядочивания операций
- Local-first Software — Automerge создавался как техническая база для local-first архитектуры - данные на устройстве, синхронизация по возможности
Вопросы для размышления
- Actual Budget использует Automerge для хранения финансовых данных офлайн. Какие трейдоффы возникают при росте истории изменений за несколько лет использования приложения?
- Figma реализовала собственную CRDT-систему вместо Automerge. В каких ситуациях стоит строить кастомный CRDT вместо использования готовой библиотеки?
- Automerge сохраняет конфликтующие значения через getConflicts и оставляет разрешение приложению. Как бы выглядел UI для текстового редактора, показывающего конфликты нескольких авторов?