DarkRiDDeR13 мин

Плохая сеть: не показывать успех, пока действие не подтверждено

FrontendНадёжность

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

В июле 2022 года я бы начал не с offline-first лозунга и не с эмулятора сети. Сначала нужен видимый контракт одного действия: черновик отдельно от подтверждённого результата, явное состояние сети как вход интерфейса, ключ логического запроса и ключ конкретной попытки. Ниже — учебная in-memory fixture. Она не вызывает Fetch, не запускает service worker и не получает ответ сервера. Поэтому её состояния offline и unknown-outcome — проектные labels, а не отчёт о реальной сети.

Четыре состояния, которые нельзя склеивать

У отправки есть как минимум четыре разных факта. Первый: текст черновика сохранён локально в выбранном хранилище. Второй: интерфейс сейчас считает сеть недоступной или доступной. Третий: конкретная попытка ожидает прикладное подтверждение. Четвёртый: серверное подтверждение с идентификатором предметной операции действительно принято клиентской логикой. Если заменить эти факты одним boolean isSaved, UI обязательно назовёт один из них другим.

Видимый контракт отправки черновика
Состояние интерфейсаЧто известноЧто нельзя утверждатьДействие
Черновик готовесть сохранённая локальная копия и её версиячто сервер видел текстдать редактировать и показать дату/версию локальной копии
Ожидаем подтверждениесоздан request key и одна attempt keyчто операция уже завершиласьне показывать success; защитить повторный клик
Результат неизвестендля попытки нет прикладного acknowledgementчто операция не дошла до серверасохранить черновик, дать повторить или перейти к восстановлению
Подтвержденополучен payload с request key, версией и submission idчто более новый черновик можно удалитьочистить только подтверждённую версию, новый draft оставить

Почему «запрос упал» не равно «ничего не произошло»

Fetch Standard описывает request, response и network error, но у прикладного экрана другая задача: он должен решить, что показать человеку без домысла о чужой стороне. Между началом операции и сообщением UI могли существовать промежуточные границы, которых экран не видит. Поэтому здесь не используется фраза «сервер точно не сохранил». Вместо неё есть более узкое и проверяемое условие: текущая модель не получила acknowledgement, связанный с текущими requestKey и attemptKey.

Это продолжает июньский разбор ошибок формы. Там важно было не перезаписать исправленное поле поздней ошибкой. Здесь важно не стереть текст и не объявить победу поздним или отсутствующим подтверждением. В обоих случаях источник проблемы — два разных состояния получают одно имя «сохранено». Версия черновика, попытка доставки и признанный результат должны жить рядом, но не заменять друг друга.

Видимый текст должен следовать этой модели. «Черновик сохранён на устройстве» и «изменение подтверждено» — разные сообщения с разными условиями показа. Первое можно показать после успешного шага persistence выбранного приложения, второе — только после принятого acknowledgement. Если данные для первого сообщения ещё не записаны, интерфейс обязан показать это как отдельную ошибку, а не использовать сетевой значок вместо результата.

Учебная fixture: ключ запроса стабилен, ключ попытки меняется

В модели пользователь пишет черновик версии 1. Для него создаётся requestKey: draft-v1. Первая попытка получает attempt-01. Если модель не получает acknowledgement, она показывает result-unknown и сохраняет черновик как источник восстановления. После явно объявленного возвращения в online разрешён один retry: он сохраняет тот же request key, но использует attempt-02. Это защищает от параллельного повтора и позволяет отличить поздний ответ первой попытки от ответа второй.

# Запускается только in-memory учебная модель.
node web/scripts/upgrade-2022-07.mjs --verify-fixture

# PASS fixture: 15/15 assertions
# Нет Fetch, HTTP, service worker, DOM, localStorage, таймера или trace.

Пятнадцать assertions проверяют именно эту границу. Они подтверждают, что fixture не вызывает Fetch и не создаёт browser API; offline блокирует планирование новой попытки, но не удаляет persisted draft; retry не меняет logical request key; stale acknowledgement не коммитит состояние. Такой тест не доказывает доступность сети и не заменяет интеграционный сценарий. Он делает явными правила UI до того, как эти правила разъедутся между click handler, storage и toast.

Диаграмма учебных состояний отправки: черновик готов, offline блокирует попытку и оставляет копию; online создаёт ожидающую попытку; отсутствие model acknowledgement ведёт в результат неизвестен; подтверждение возвращает статус подтверждён, а новый черновик остаётся отдельно.
Схема показывает пользовательское состояние, а не сетевую трассу. Стрелки «offline» и «unknown» — входы учебной модели, не измерение соединения.

Маршрут: симптом → причина → проверка → действие

  1. Симптом. После нажатия Save интерфейс быстро показывает успех, повторяет запрос при повторном клике либо открывается с пустым текстом после offline.
  2. Причина. Черновик, транспортная попытка и подтверждённая операция записываются в одно поле состояния. Отсутствие ответа трактуется как failure или success без отдельного правила.
  3. Проверка данных. Выпишите отдельно draft version, persisted draft, request key, attempt key, phase и acknowledgement payload. Если одно поле отвечает за два пункта, найдите владельца каждого перехода.
  4. Проверка неизвестного исхода. Смоделируйте ветку без acknowledgement. В ней не должно быть success и не должен исчезать persisted draft. В реальном проекте эту ветку затем проверяют отдельным контролируемым сценарием, а не выдают модель за trace.
  5. Действие. До acknowledgement показывайте «результат пока неизвестен», отключайте параллельный retry и разрешайте один осознанный повтор с тем же logical request key.
  6. Восстановление. Дайте безопасный путь оставить черновик для проверки. Автоматически очищайте только версию, которую признал payload; более новый текст не принадлежит старой попытке.

Где хранить черновик — отдельное решение

Fixture называет persistence in-memory-copy-only. Это намеренное ограничение: переменная в учебной модели переживает только её выполнение, а не перезагрузку, вкладки или сбой процесса. Реальное приложение должно отдельно выбрать хранилище, срок жизни, шифрование, правила очистки и поведение при нескольких вкладках. Service Workers CRD от 12 июля 2022 года показывает, что сервисный worker — событийный контекст с собственным жизненным циклом; это не основание предполагать, что он всегда запущен или уже спас конкретный черновик.

Полезно начать с малого контракта: «после каждого изменения черновик либо сохранён вместе с версией, либо UI честно показывает, что локальной копии нет». Потом добавляется реальное хранилище и отдельный тест его ошибок. Не стоит прятать это решение под названием offline cache. Для текста профиля и для перевода денег различаются срок хранения, чувствительность данных, право на повтор и даже допустимое сообщение на экране.

Ограничение и следующий проверяемый шаг

Эта модель не выбирает HTTP-метод, не реализует idempotency endpoint, не выполняет request, не хранит данные после рестарта и не решает конфликт двух устройств. RFC 9110 говорит о свойствах методов, но прикладной requestKey остаётся вашим контрактом: он должен быть понятен серверу и связан с предметной операцией, если она допускает повтор. Нельзя перенести строку draft-v1 из fixture в production как готовый механизм защиты.

Следующий шаг — взять одну настоящую форму и написать рядом с ней таблицу из четырёх состояний. Затем добавить локальный тест: редактирование создаёт новую version; отсутствие acknowledgement сохраняет черновик; поздний ответ не перезаписывает новый текст. После этого команда может провести отдельный browser и network сценарий с названными версиями среды. Его результаты будут новым артефактом, а не обещанием этой статьи.

Историческая граница июля 2022

Здесь использованы неизменяемые источники, доступные к июлю 2022 года: Fetch Review Draft от 19 декабря 2021 года, Service Workers Candidate Recommendation Draft от 12 июля 2022 года и RFC 9110 июня 2022 года. Ни один из них не используется как доказательство конкретного offline-прогона. Все ключи, версии, acknowledgement и видимые тексты — данные учебной модели пакета.

Проверяемые источники

  • WHATWG Fetch Standard, Review Draft от 19 декабря 2021 года — датированный первичный снимок Fetch Standard. Он задаёт модель request, response и network error; он не превращает отсутствие application acknowledgement в доказательство, что серверное действие не произошло.
  • W3C Service Workers, Candidate Recommendation Draft от 12 июля 2022 года — неизменяемая версия периода: service worker событийный и может быть остановлен user agent. В этой партии service worker намеренно не создаётся; документ используется только как историческая граница платформы.
  • RFC 9110: HTTP Semantics, июнь 2022 года — нормативный RFC, доступный к июлю 2022 года. Он различает свойства HTTP-методов и не заменяет прикладной ключ запроса или подтверждение предметной операции.