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 вставившего
TextRGA с character granularityСимволы вставляются по логической позиции
CounterIncrement-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 для текстового редактора, показывающего конфликты нескольких авторов?

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

  • ds-10-crdts
Automerge

0

1

Войти