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

PATCH и конфликт версии редактора

Пора соединить редактор с операцией записи. PATCH передаёт title, status и version, по которой диспетчер начал изменение. Успешный ответ содержит обновлённый Ticket; только тогда страница принимает новую основу и сообщает подтверждение. Результат главы — один контролируемый цикл сохранения с сохранённым черновиком при отказе. Мы отдельно рассмотрим 409 и переход между адресами, потому что именно они раскрывают различие между введённым текстом и актуальными предметными данными.

Начальная rq-101 имеет version 1. Если пользователь отправляет title «Заменить проектор в зале 2», status active и version 1, ожидаемый успешный ответ содержит version 2. Если другой диспетчер уже изменил заявку, version 1 больше не является текущей основой. Сервер сообщает конфликт, вместо того чтобы молча принять последнее присланное содержание. Такой договор ограничивает потерю обновлений, но не объединяет автоматически две редакторские версии.

Операция save

Ниже полный метод save из конечного DetailPage. Поля api, session, form, ticket, saving, loading, generation и destroyRef определены в том же файле архива. Generation меняется при смене route id и связывает ответ с текущей карточкой. Фрагмент не является самостоятельным компонентом: он заменяет учебный обработчик, который раньше лишь читал значения формы. Теперь результат зависит от ответа TicketsApi, а не от факта нажатия кнопки.

save() {
    if (!this.session.canEdit() || this.saving() || this.loading()) return;
    this.form.controls.title.setValue(this.form.controls.title.value.trim()); this.form.markAllAsTouched();
    const base = this.ticket(); if (this.form.invalid || !base) return;
    const generation = this.generation; this.saving.set(true); this.error.set(''); this.notice.set('');
    this.api.patch(base.id,{...this.form.getRawValue(),version:base.version}).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
      next: row => { if (generation !== this.generation) return; this.saving.set(false); this.ticket.set(row); this.form.setValue({title:row.title,status:row.status}); this.form.markAsPristine(); this.notice.set('Изменение подтверждено API.'); },
      error: error => { if (generation !== this.generation) return; this.saving.set(false); this.error.set(errorMessage(error)); }
    });
  }

Сначала проверяются роль и отсутствие текущей операции, затем title нормализуется и раскрываются ошибки формы. Если форма invalid или Ticket-основы нет, PATCH не создаётся. Когда проверка проходит, снимается старое сообщение и включается saving. Параметры request собираются из черновика и version основы. Диспетчер не получает поле для ручной правки version: это служебное значение договора конкуренции, а не часть нового заголовка или произвольный счётчик кликов.

В successful next новая row становится ticket, форма получает подтверждённые поля и markAsPristine. Именно ответ API определяет новый version и updatedAt. Клиент не вычисляет эти значения заранее, потому что время записи и атомарное изменение принадлежат серверу. В fixture этот серверный смысл моделируется внутри локальной базы, а в Nest выполняется транзакцией PostgreSQL. Ожидаемая надпись «Изменение подтверждено API» поэтому точнее, чем немедленное «Сохранено» после отправки.

Черновик при конфликте

При error меняются saving и сообщение, но form не заменяется. Это сохраняет title, который пользователь ввёл перед 409. Ticket остаётся прежней основой; повторный save без её обновления ожидаемо снова конфликтует. Кнопка reload прямо предлагает загрузить актуальные данные с заменой черновика. После такого действия новая версия станет основой следующего редактирования. Пользователь должен понимать потерю старого текста и при необходимости скопировать его; автоматическое слияние в лаборатории отсутствует.

Не следует после 409 просто увеличить локальный version на единицу. Между отказом и повтором могло пройти несколько чужих изменений, а новая заявка может иметь другой title или status. Даже точное угадывание номера не делает старый черновик основанным на новых данных. Конфликт сообщает необходимость принять решение о содержании, а не только исправить технический параметр. Это причина явного чтения и отдельного выбора дальнейшего изменения вместо бесконечной автоматической повторной отправки.

Можно представить два редактора A и B, получивших rq-101 v1. A успешно сохраняет active и получает v2. B отправляет свой title с v1 и получает 409; его локальный текст остаётся. Если B затем принимает актуальную основу, видит содержание A и повторно вносит осознанное изменение, следующий PATCH с v2 ожидаемо получает v3. Такой цикл полезен для понимания оптимистической версии, хотя реальное параллельное выполнение этой сессией не проверялось.

Ответ принадлежит текущему адресу

Пользователь может перейти от rq-101 к rq-102, пока первый PATCH ещё выполняется. Generation фиксируется до отправки, а после перехода увеличивается. Обработчики сравнивают захваченное значение с текущим. Если они различаются, ответ rq-101 не заменяет форму rq-102 и не показывает её пользователю как подтверждённый результат другой карточки. При этом запись rq-101 на сервере всё ещё могла завершиться. Механизм защищает представление, а не отменяет предметную операцию в PostgreSQL.

TakeUntilDestroyed завершает подписку при уничтожении компонента. Смена только route id может сохранить экземпляр, поэтому одного lifecycle-unsubscribe недостаточно для такого перехода. Это объясняет дополнительный generation. Loading, saving и запрет reload во время записи предотвращают другие простые пересечения внутри текущей страницы. Мы не добавляем общий глобальный счётчик запросов: каждый редактор должен самостоятельно понимать, какие результаты имеют право изменить его актуальную основу.

Кнопка сохранения отключается на время saving, а метод снова проверяет этот флаг. Поэтому второй щелчок не создаёт обычную повторную мутацию. Однако потеря ответа остаётся неоднозначной: сервер мог завершить запись, а клиент получил status0. Повтор с прежней version затем способен вернуть 409. В контракте явно сказано, что идемпотентные ключи и reconciliation не реализованы. Не интерпретируйте любое сетевое исключение как доказательство, что данные остались неизменными.

Для самостоятельного сценария сначала сохраните rq-101 один раз, затем отправьте вручную старый body с version1. Ожидается 409, а новая карточка должна читать version2. Такой прямой запрос относится к проверке API и в этой сессии не выполнялся. В fixture можно изучить форму и сообщение, но две независимые вкладки имеют разные базы в памяти, поэтому не моделируют общую конкуренцию Nest. Сравнение источников помогает выбрать правильную среду для будущего воспроизведения.

Цикл записи теперь имеет ясные этапы: независимый черновик, проверенная попытка, ожидание, подтверждённая новая основа либо сохранённый черновик с отказом. Следующий урок рассмотрит выбранную учебную роль и границы UI. Серверная проверка останется необходимой даже при аккуратном save: любой внешний клиент способен отправить тот же PATCH без кнопок и формы Angular, поэтому разрешение должно находиться у владельца данных.

Статус примера — not_run. Зависимости, приложение, SQL, HTTP и тесты не запускались. Описанные ответы и изменения интерфейса ожидаемые; чтение документации не является проверкой выполнением. Полная конечная версия находится в архиве исходников, а её ограничения и действия читателя — в README и CONTRACT.md.

Перейдите к оглавлению серии или общему маршруту интерактивной веб-разработки. Механизм этой главы сверяется с официальным первичным источником; конкретные версии и непроверенные части перечислены в договоре курса.