Ревизии и отмена своих изменений
«Черновик» уже имеет общую сохранённую копию и серверные роли. Теперь пользователям нужны два других действия: отменить собственную недавнюю правку и сохранить именованный этап согласования. Эти действия часто ошибочно объединяют словом «история». В этом уроке разделим локальный UndoManager и серверные ревизии: первое меняет текущий общий документ, второе фиксирует отдельный пользовательский снимок для последующего рассмотрения.
Состояние lesson-11 добавляет кнопки отмены, повтора и сохранения ревизии. Имена источников INPUT и TOOL, введённые раньше, становятся фильтром отмены. Ревизия получает собственный случайный идентификатор, серверное время, подтверждённого автора и label «Перед согласованием». Двоичный снимок хранится вместе с записью, но не отправляется в общую таблицу интерфейса.
Чья операция отменяется
Y.UndoManager наблюдает выбранные общие типы и собирает изменения, которые подходят его настройкам. В «Черновике» областью отмены является текст markdown, а trackedOrigins содержит локальный ввод и команду панели. Обновления, пришедшие через provider, и восстановление из IndexedDB не входят в этот набор. Поэтому кнопка «Отменить своё» не должна превращаться в отмену последнего чужого пакета.
Важно, что источник локальный. Он помогает текущей вкладке группировать свои действия, но не подтверждает автора в серверном журнале. Если Анна открыла новую вкладку, она получает новый UndoManager и новую локальную историю отмены. Серверный документ при этом остаётся тем же. Нельзя обещать восстановление всего стека undo после перезагрузки только потому, что IndexedDB восстановил CRDT-структуру.
Ниже показан точный фрагмент подключения отмены в src/app.js. text, provider, INPUT и TOOL уже определены в полном файле. captureTimeout задаёт временную группировку близких локальных операций. Для команды, которая должна образовать отдельный шаг, можно дополнительно применить stopCapturing на выбранной границе интерфейса.
const undo=new Y.UndoManager(text,{trackedOrigins:new Set([INPUT,TOOL]),captureTimeout:500});
$('undo').onclick=()=>undo.undo();$('redo').onclick=()=>undo.redo();
$('revision').onclick=()=>{if(role==='editor')provider.send({type:'revision',label:'Перед согласованием'});};
let disposed=false;
После самостоятельного ввода фразы Анной кнопка отмены должна выполнить обратную операцию относительно её отслеживаемой локальной правки. Результат также передаётся другим репликам как обычное изменение документа. Это не скрытая локальная смена экрана: остальные участники должны увидеть изменённый текст. Конкретный эффект при одновременно изменённом диапазоне требует самостоятельного наблюдения; авторский запуск не выполнялся.
Отмена после чужой правки
Представьте, что Анна ввела пункт, а Борис позже добавил в него уточнение. Отмена работает с общей структурой и собственными отслеживаемыми изменениями, а не с копией всей строки «до Анны». Это помогает не перезаписывать весь документ прежним текстом. Однако пользователь должен понимать, что отмена в совместном редакторе взаимодействует с последующими изменениями и не обещает произвольного возвращения прошлого экрана.
Повтор также является действием над текущим документом. Между отменой и повтором другая реплика может измениться. Если продукт хочет запретить определённую отмену после утверждения документа, одного UndoManager недостаточно: серверная политика должна ограничить дальнейшие updates или перевести работу в другой этап. Кнопка UI не устанавливает общую бизнес-блокировку для всех участников.
Область text не включает карту meta. Поэтому отмена заголовка в учебном варианте не обязана вернуть значение lastAction. Это намеренно показывает важность scope. Если команда должна отменять связанные изменения и текста, и карты, менеджеру нужно передать оба типа и определить понятную семантику. Простое добавление карты в scope также требует объяснить, какие служебные изменения теперь попадут в историю.
Ревизия как запись продукта
Ревизия создаётся по явному сообщению редактора. Сервер сохраняет свой текущий двоичный снимок, назначает автора из сессии и собственное время. Эти данные не берутся из поля origin или клиентской часов. Label ограничен по длине и описывает этап для человека; он не является уникальным ключом. Две ревизии с одинаковой подписью всё равно получают разные идентификаторы.
В учебном контейнере список ограничен пятьюдесятью записями. Это простая политика объёма, которую нужно раскрыть пользователю: самые старые записи удаляются из этого списка при дальнейшем сохранении. Она не является нормативной политикой аудита и не предоставляет неизменяемое хранение. Для значимых документов срок, доступ к удалению и аудит изменения журнала определяются отдельно.
После самостоятельного нажатия «Сохранить ревизию» редактор должен получить список с новым идентификатором, label, автором и временем. Передача ответа происходит после сохранения контейнера. Это ожидаемый эффект; файловое хранение и UI не запускались при авторстве. Потерянный ответ способен оставить неопределённость: сервер мог создать запись, а клиент ещё не знает её ID.
Открытие и восстановление снимка
Снимок ревизии можно применить к новому отдельному Y.Doc для просмотра. Это полезно сделать вне текущей комнаты, чтобы чтение прошлого состояния не меняло рабочую реплику. Учебный интерфейс показывает метаданные и хранит снимок на сервере, но не реализует полноценную страницу сравнения. Не следует выдавать список записей за готовую систему визуального diff и восстановления.
Особенно важно не путать восстановление с applyUpdate старого снимка поверх текущего документа. Такое применение объединит известные операции и не удалит автоматически все более новые изменения. CRDT-снимок — не команда «сделать всё как раньше». Для восстановления этапа продукт должен выбрать отдельную новую ветку документа или явную преобразующую операцию с правами и понятным предупреждением о результате.
Кроме того, UndoManager не является резервной копией. Его стек живёт в текущем процессе и описывает выбранные операции интерфейса. Серверная ревизия находится в том же учебном файле, что и текущая копия, поэтому потеря единственного носителя способна уничтожить обе. Независимый backup должен храниться отдельно и иметь проверяемый путь восстановления.
Результат урока — три различимые модели: текущий CRDT-документ, локальная отмена действий и именованный серверный снимок. Для самостоятельного анализа сопоставьте смену вкладки, нажатие отмены и сохранение новой ревизии. Они затрагивают разные объекты и дают разные гарантии. Финальная глава соберёт их в контракт учебной поставки и опишет освобождение ресурсов при завершении редактора.
Оглавление серии · Исходные состояния проекта.
API сверены с первичной документацией 11 октября 2026 года. Пример имеет статус not_run: фактический запуск и наблюдение результата читателю предстоит выполнить самостоятельно.