Сравнение версий и ручное объединение
Мы уже обнаруживаем несовпадение локальной и серверной редакции. Теперь конфликт нужно превратить в действие, понятное автору заметки. В этой главе «Блокнот» покажет обе версии и предложит принять серверную либо сохранить подготовленное вручную объединение. Результат — решение, которое не маскируется под обычный retry и не перезаписывает чужую работу без выбора пользователя.
Конечный интерфейс находится в public/index.html и app.js, локальный переход — в resolveConflict файла db.js. Автор не выполнял сценарий одновременных изменений. Приведённые тексты и состояния ожидаемые; подробное сравнение позволяет читателю в будущем отличить корректный результат от незаметной потери одной из редакций.
Конфликт относится к исходному состоянию
Оба устройства начали с «Разобрать HTTP», revision один. Первое добавило кеширование и получило revision два. Второе офлайн дописало cookies, продолжая считать исходной первую revision. Оба текста полезны, но сервер не может понять, следует ли объединить темы, заменить одну другой или оставить два отдельных плана. Поэтому он возвращает текущую каноническую заметку, сохраняя решение за человеком.
Проверка revision обнаруживает возможную потерю работы; она не измеряет качество редакций. Даже одинаковый итоговый body после разных локальных действий может потребовать определённой политики. Наш протокол выбирает простой отказ при несовпадении исходной revision. Это делает результат объяснимым и не вводит скрытые правила по времени, длине текста или имени устройства.
Локальный conflict содержит remote и reason. Reason сообщает, где обнаружено расхождение: reply push или применение pull. Для пользователя оба случая ведут к одному сравнению. Notes сохраняет текущую рабочую редакцию, outbox становится blocked. Отправитель продолжает другие заметки, поэтому один конфликт не превращает весь аккаунт в недоступную базу.
Показываем содержимое без скрытой отправки
При выборе конфликтной заметки форма заполняется локальными title и body, а отдельный блок показывает JSON серверной версии. Это скромный учебный интерфейс: он показывает revision и deleted, чтобы состояние можно было разобрать полностью. В рабочем продукте лучше представить понятное сравнение полей, но семантика двух вариантов должна остаться видимой.
Текст remote выводится через textContent, а не innerHTML. Содержимое заметки может включать угловые скобки или фрагмент JavaScript; оно должно остаться текстом. Сравнение пользовательских данных не даёт оснований выполнять их как разметку. Так обычный выбор редакции сохраняет ту же границу, что и вывод заголовков списка.
Кнопка «Сохранить локально» продолжает работать во время конфликта. Она обновляет локальные поля, сохраняя remote и blocked операцию. Человек может собрать «Разобрать HTTP, cookies и кеширование» и проверить формулировку, не отправляя её сразу. Затем отдельная кнопка «Отправить текущую локальную версию» явно разрешает новое намерение.
Принимаем серверную редакцию
Если пользователь выбирает remote, resolveConflict удаляет прежнюю blocked операцию, копирует title, body и deleted канонической note, обновляет revision и снимает conflict. Новая отправка не создаётся: человек согласился с уже существующим серверным вариантом. LocalVersion увеличивается, чтобы открытая устаревшая форма другой вкладки не могла незаметно вернуть прежний текст.
Существенная часть перехода из db.js выглядит так. Next уже содержит прежний id, обновлённые revision/localVersion и очищенный conflict; вся операция выполняется внутри одной транзакции.
s.outbox.delete(id);
if (choice === 'remote') {
Object.assign(next, {
title: remote.title,
body: remote.body,
deleted: remote.deleted
});
} else {
s.outbox.put(operation(next));
}
s.notes.put(next);
Ожидаемый результат выбора remote — список больше не помечает заметку конфликтной, очередь для неё пустая, форма показывает принятый текст. Если remote.deleted true, заметка исчезает из обычного списка. Это не случайная потеря строки, а явное согласие с удалением, которое также нужно показывать при сравнении.
Такой выбор не стирает запись другой стороны на сервере и не увеличивает её revision. Изменяется только локальное отношение к уже принятому состоянию. Если бы кнопка remote отправляла новый push, она создала бы лишнюю серверную ревизию без нового прикладного содержания и усложнила бы объяснение журнала.
Отправляем подготовленное объединение
Для своего варианта сначала нужно сохранить отредактированные поля формы. Кнопка разрешения использует current notes из базы, а не несохранённый DOM текст. В интерфейсе есть явная подсказка: изменить поля, сохранить локально, затем выбрать свою версию. Такое разделение сохраняет атомарность и не заставляет обработчик конфликта читать непроверенные промежуточные поля из разных мест.
ResolveConflict обновляет revision до remote.revision, очищает конфликт и создаёт operation(next) с новым opId. Прежняя операция уже имела серверный результат conflict, поэтому повторное использование её ID запрещено. Новая baseRevision обозначает, что человек прочитал именно этот канонический вариант и подготовил намерение относительно него.
Если сервер за это время не изменился, новый push будет принят. Если третье устройство уже создало revision три, объединение относительно двух получит ещё один конфликт. Это правильный результат: человеческое решение не удерживает серверную блокировку на всё время редактирования. Приложение должно сохранить объединённый текст и снова показать более свежую remote, позволяя продолжить сравнение.
Не следует скрывать повторный conflict как «плохое соединение». Сеть могла работать прекрасно; изменилось исходное прикладное состояние. Повтор с увеличенной baseRevision без чтения удалил бы защиту, ради которой протокол построен. Пользовательская подпись должна направлять к сравнению, а сетевой retry — сохранять неизменяемую операцию и ждать доступного сервера.
Удаление и поздний выбор
Конфликт возможен между редактированием и удалением. Одна копия удаляет «План урока», другая дописывает его офлайн. Remote содержит tombstone с новой revision, local — текст. Выбор remote принимает удаление. Выбор local создаёт явное восстановление текста относительно ревизии tombstone. В нашей лаборатории это разрешено, поскольку заметки не имеют отдельной политики необратимого удаления.
Производственное приложение может запретить восстановление определённым ролям или потребовать создание нового документа. Тогда правило нужно добавить на сервер, а интерфейс должен объяснить допустимый вариант. Само наличие ручного выбора в браузере не предоставляет полномочий. Серверный контракт остаётся конечной границей решения о том, какие изменения вообще можно принять.
Открытый экран сравнения также может устареть после очередного pull. Db.js хранит последнюю полученную remote в conflict, а refresh перечитывает этот объект. Обработчик выбора читает базу в своей транзакции, поэтому не берёт revision из старого визуального блока. Человек должен видеть актуальное сравнение перед окончательным выбором; для сложного интерфейса полезно дополнительно предупреждать об обновлении remote во время редактирования.
Теперь конфликт стал сохраняемым рабочим состоянием, а не общей ошибкой. Вы можете продолжить локальную работу, принять канонический вариант или подготовить новый снимок с явной исходной revision. Следующая глава рассмотрит удаление подробнее: почему исчезнувшая заметка должна оставить серверное событие и как tombstone защищает от возвращения старой офлайн-копии.