Перейти к содержанию

Удаление заметки и tombstone

Обычная команда удалить строку из локальной базы кажется достаточной, пока приложение работает на одном устройстве. В «Блокноте» другая копия может месяц хранить старый текст и позднее попытаться его отправить. Поэтому удаление становится прикладным изменением с revision и событием журнала. Результат урока — tombstone, который скрывает заметку из списка, но сохраняет информацию, нужную для согласования.

Кнопка удаления конечного примера вызывает ту же saveNote, передавая deleted true. Сервер и браузерные сценарии автором не запускались. Описанные состояния ожидаемые; они продолжают UUID заметки, неизменяемый outbox и ручной conflict, уже введённые в предыдущих главах.

Отсутствие объекта не объясняет удаление

Пусть Alice создала «План урока», и обе копии получили revision два. Первая просто удаляет серверный объект. Вторая остаётся офлайн с прежним id, title и body. Когда она снова отправляет изменение, сервер не знает, была ли эта заметка удалена, никогда не существовала или потерялась из-за восстановления данных. Одна пустая позиция notes не несёт достаточного прикладного смысла.

Tombstone сохраняет тот же id и новую revision, выставляя deleted true. Серверный journal получает обычное принятое событие. Вторая копия может увидеть, что её baseRevision два устарела относительно удаления с revision три. В выбранном протоколе это вызывает conflict, а не случайное создание заметки с нуля.

Название «надгробная запись» иногда выглядит необычно, но обозначает конкретную функцию: объект сохраняет след удаления для участников, которые ещё не узнали о нём. Это не копия всей базы и не резервное восстановление. Tombstone помогает согласовать смысл отсутствия документа; доступность прежнего body и право восстановления относятся к политике продукта.

Локальное удаление сначала меняет состояние

SaveNote принимает title, body и deleted. При нажатии кнопки пользовательское намерение записывается атомарно в notes и, если текущей операции нет, в outbox. LocalVersion увеличивается так же, как при редактировании. Если прежняя операция ещё существует, удаление остаётся текущим состоянием notes и будет превращено в новый снимок после её подтверждения.

Следующий фрагмент app.js показывает вызов, а не самостоятельную программу. Selected и version относятся к выбранной заметке; поля формы остаются частью её текущей редакции.

const note = await saveNote(
  db, selected,
  $('title').value, $('body').value,
  true, version
);
version = note.localVersion;
changed();

Список скрывает note.deleted, если conflict отсутствует. Это означает локальное завершение удаления: строка исчезает, хотя сервер может ещё не знать об изменении. Outbox сохраняет обязательство доставки. Общая строка прогресса и экспорт позволяют увидеть неподтверждённое состояние; полное исчезновение информации о таком удалении сделало бы сетевую ошибку неразличимой для пользователя.

Если удаление конфликтует, запись вновь доступна через список как конфликтная. Иначе человек не смог бы выбрать между локальным удалением и серверным редактированием. Условие видимости поэтому отличается от простого filter not deleted. Интерфейс показывает объект, когда требуется решение, даже если локальный вариант означает его отсутствие.

Сервер принимает удаление как новую ревизию

Push не имеет отдельного маршрута delete. Поле deleted участвует в неизменяемом снимке наряду с title и body. Проверка baseRevision остаётся прежней. Если серверная note ещё revision два, удаление получает три; ledger сохраняет accepted, changes получает событие. Повтор того же opId возвращает прежнее подтверждение, не создавая четвёртую ревизию.

Это связывает удаление с теми же правилами retry, что и обычную правку. Клиент не должен угадывать, существует ли объект после обрыва специального DELETE. Он повторяет известное намерение. Унификация здесь полезна, поскольку основная трудность — потерянный ответ и конкурирующая редакция — не меняется из-за названия действия.

Удаление новой локальной заметки также следует контракту. Если первоначальный create уже стоит в outbox, программа сначала доставит его, затем сформирует deleted относительно полученной revision. На сервере кратко появится создание и последующее удаление. Можно оптимизировать такой случай до отсутствия обоих событий только при доказанном отсутствии первой отправки, но выбранная неизменяемая очередь сознательно избегает этого усложнения.

Ожидаемый конечный результат — сервер хранит deleted true, все получившие событие копии скрывают строку, а очередь удалившего клиента пуста после accepted. Это не означает физического уничтожения текста во всех носителях: body остаётся в учебном state, старые события есть в журнале, экспорт может содержать прежние данные. Термин «удалить» в интерфейсе не должен обещать очистку всех резервных копий.

Старая офлайн-редакция получает выбор

Теперь второй профиль возвращается с правкой на основе revision два. Сервер хранит tombstone три и возвращает conflict. Local note содержит новый текст, remote — deleted true. Пользователь может принять серверную версию и оставить документ удалённым либо явно отправить свою текущую редакцию как восстановление относительно трёх.

Если вместо этого сервер считает отсутствие объекта revision ноль, ситуация становится менее ясной. Старая baseRevision два не совпадёт, но клиент не узнает, что произошло, и не сможет корректно предложить восстановление. Tombstone даёт значение revision и смысл состояния. Поэтому сохранение следа нужно не ради удобства списка, а ради сохранения истории прикладного объекта для запоздавших участников.

Обратный конфликт тоже возможен: первая копия пытается удалить revision два, пока другая уже сохранила полезный revision три. Автоматическое удаление новой редакции лишило бы второго автора работы. Наш интерфейс покажет его body и потребует выбора. Если человек подтверждает локальное удаление, новый opId использует прочитанную третью revision и сервер может принять четвёртую tombstone.

Почему след нельзя очищать произвольно

В лаборатории tombstones не удаляются. Это расходует место, но сохраняет простой контракт для любого ещё поддерживаемого cursor. Чтобы освобождать их безопасно, сервер должен знать, какие клиенты могут возвращаться со старым состоянием, либо установить границу истории и процедуру полной пересинхронизации. Одной отметки времени удаления недостаточно, если офлайн-работа допускается дольше выбранного срока.

Представьте очистку через семь дней без нового протокола. Восьмой день отсутствовавший клиент продолжает считать заметку действующей. Если сервер уже забыл и tombstone, и ledger прежних операций, он может принять повтор или предложить неверное начальное состояние. Производственный дизайн должен отдельно решить, что происходит с слишком старым устройством: отказ cursor, экспорт локальных правок, получение нового полного снимка и ручное сопоставление.

Политика сохранения удалённого body может отличаться от сохранения минимального следа. Сервер способен хранить только id, revision и deleted, очищая пользовательский текст согласно принятому правилу. Тогда ручное сравнение удаления не показывает прежнее содержимое, но всё ещё распознаёт историю ключа. Наша маленькая реализация сохраняет body ради прозрачного обучения, не утверждая пригодность такой политики для любых данных.

После этой главы удаление перестало быть исчезновением строки без объяснения. Оно сохраняется локально, передаётся неизменяемой операцией, проходит проверку revision и доходит до других устройств через journal. Дальше добавим устойчивый порядок повторов: временная недоступность сервера должна замедлять отправку, сохраняя заметки и давая понятный прогресс.

Оглавление серии