Транзакции и происхождение изменений
Теперь обычный ввод в «Черновике» меняет ограниченный диапазон Y.Text. Однако действия редактора не всегда состоят из одной операции. Кнопка может одновременно добавить заголовок и обновить служебную информацию о последней команде. В этом уроке объединяем связанные изменения транзакцией и назначаем им источник, чтобы дальнейшие слои приложения могли отличать набор текста от команды панели.
Состояние lesson-03 сохраняет привязку предыдущего урока и добавляет кнопку заголовка. Документ содержит общий текст markdown и карту meta. Результат урока состоит не в новой структуре Markdown, а в осмысленной границе одного действия: две правки относятся к одному пользовательскому намерению и должны рассматриваться вместе.
Граница действия
Транзакция Yjs группирует изменения общих типов и согласует вызовы наблюдателей. В нашем примере внутри doc.transact выполняются вставка строки «Повестка» и изменение значения lastAction. Читателю важно отличать такую транзакцию от транзакции базы данных. Она не сообщает, что данные записаны на диск, и не означает, что сервер проверил право пользователя на операцию.
В частности, исключение в пользовательском callback нельзя воспринимать как универсальную гарантию отката всех уже сделанных правок. Проверяйте условия до изменения документа: наличие редакторской роли, допустимость команды, необходимые исходные значения. Если продукт требует бизнес-операцию с серверной проверкой и долговременной атомарностью, для неё нужен отдельный серверный контракт. Локальная группировка CRDT-операций решает другую задачу.
Рассмотрим добавленную часть src/app.js. Переменные doc и text созданы в openEditor, а TOOL импортирован из binding.js. В архиве дан полный файл этого состояния; ниже показан точный фрагмент новой команды, заменять существующую привязку поля не нужно. Карта извлекается по имени внутри того же документа.
$('heading').onclick=()=>{if(role!=='editor')return;doc.transact(()=>{text.insert(0,'## Повестка\n');doc.getMap('meta').set('lastAction','heading');},TOOL);};
Если выполнить команду самостоятельно, в начале текста должен появиться Markdown-заголовок второго уровня, а карта должна получить значение heading. Обе операции принадлежат одной транзакции с источником TOOL. Видимый предварительный просмотр отражает только текст, поэтому карта не появляется на странице автоматически. Это ожидаемый результат по устройству листинга, не сообщение о выполненной проверке.
Что означает origin
Источник, или origin, — произвольное значение, которое приложение связывает с транзакцией. В нашей привязке значение INPUT означает ввод поля, а TOOL означает действие панели. Символы удобно использовать, потому что они не сталкиваются со случайными строками из данных документа. Источник не является общим пользовательским полем и не превращается в подтверждённый идентификатор автора правки.
Например, тот же документ может получать данные от сетевого provider, локального IndexedDB и кнопки импорта. Всем этим слоям полезно иметь разные источники: provider не должен повторно отправлять пакет, который сам только что применил, а отмена ввода не должна затрагивать случайное фоновое восстановление. Однако источник, известный одному клиенту, не становится доверенной подписью для удалённого сервера.
Не записывайте имя Анны в origin с ожиданием получить готовый аудит. Другой клиент способен сформировать такую же строку, а бинарное обновление само по себе не подтверждает аккаунт. Для аудита сервер связывает принятый запрос с проверенной сессией и собственной записью времени. Источник удобен для локального поведения интерфейса, тогда как ответственность за авторство находится на другом уровне.
Наблюдатели текста и документа
Наблюдатель Y.Text сообщает об изменении данного типа. Событие update документа, которое понадобится следующему уроку, предоставляет двоичное представление изменений. Это разные представления одного процесса. Для обновления текстового поля нужен текущий текст, а для передачи другой реплике нужен пакет Yjs. Не следует сериализовать объект события текста произвольным JSON и считать это эквивалентом двоичного update.
Группировка также помогает рассуждать о служебных данных. Представьте, что обработчик интерфейса смотрит на lastAction после завершения команды. Если метаданные менялись бы отдельным позже запланированным действием, промежуточное состояние могло бы выглядеть противоречиво. Транзакция выражает, что изменение заголовка и отметка команды относятся к одной завершённой локальной операции.
Но она не устанавливает бизнес-очерёдность независимых команд разных участников. Анна и Борис могут параллельно записать разные значения в lastAction. Общая карта согласует состояние по правилам своего типа, однако продукт должен отдельно решить, достаточно ли единственного последнего значения или нужен журнал событий. Для истории действий одно скалярное поле почти всегда содержит слишком мало информации.
Повторная команда
Текущая кнопка каждый раз вставляет заголовок в начало. Это намеренная простота примера: она позволяет увидеть границу действия, но не реализует идемпотентную команду «обеспечить заголовок». Повторное нажатие должно вставить ещё одну строку. Если по требованиям заголовок допускается только один, проверку его наличия следует добавить перед транзакцией и определить поведение при параллельном добавлении.
Такая проверка в браузере тоже не становится глобальной блокировкой. Два клиента способны одновременно увидеть отсутствие заголовка и оба вставить его. CRDT затем согласует обе операции, а не догадается, что авторы имели в виду один бизнес-объект. Для структурированного заголовка лучше выбрать отдельное поле схемы или серверную команду с понятной семантикой, а не искать строки в Markdown.
Для самостоятельного разбора сначала нажмите кнопку один раз, затем введите символ в текстовом поле. Если подключить наблюдение afterTransaction, источники этих действий будут различаться: первое относится к TOOL, второе к INPUT. Не сравнивайте источники после JSON-сериализации: выбранные символы предназначены для текущего процесса и не имеют переносимого текстового представления.
Наконец, избегайте бесконтрольных изменений документа внутри его наблюдателей. Служебная запись, автоматически реагирующая на любую служебную запись, легко создаёт цепочку новых транзакций. Сначала определите событие, на которое реагируете, затем проверьте источник и необходимость изменения. Хорошая транзакционная граница делает поток понятнее: пользовательская команда заканчивается, после чего интерфейс отражает результат. Следующий шаг научит передавать завершённые изменения другой реплике.
Оглавление серии · Исходные состояния проекта.
API сверены с первичной документацией 11 октября 2026 года. Пример имеет статус not_run: фактический запуск и наблюдение результата читателю предстоит выполнить самостоятельно.