На 31 июля 2026 года строгий аудит проходит 160 из 358 созданных материалов. Остальные 198 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
На 31 июля 2026 года строгий аудит проходит 163 из 358 созданных материалов. Остальные 195 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
- Голос: М5, frontend-системщик 2022 года. Мост от ошибок формы и идемпотентности к пользовательскому сценарию: видимое состояние, владение черновиком, граница попытки и подтверждение результата.
- Граница компетентности: нет выдуманного production SLO, инцидента, offline trace, browser-прогона, настоящего Fetch/HTTP, service worker или ответа сервера. Network labels, keys и acknowledgement — учебные значения модели.
- Fixture: только локальные объекты и массивы. Она не создаёт DOM, storage API, worker, таймер или запрос. `unknown-outcome` означает отсутствие acknowledgement в модели, а не установленный факт о сети или сервере.
## Проход 1 — факты и техника
- Историческая рамка проверена по датированному [WHATWG Fetch Review Draft, 19.12.2021](https://fetch.spec.whatwg.org/review-drafts/2021-12/), [W3C Service Workers Candidate Recommendation Draft, 12.07.2022](https://www.w3.org/TR/2022/CRD-service-workers-20220712/) и нормативному [RFC 9110, июнь 2022](https://www.rfc-editor.org/rfc/rfc9110.html). Текущие mutable-версии документации не используются как доказательство состояния 2022 года.
- Убрана ложная цепочка «нет ответа → сервер ничего не сделал». В тексте остаётся только проверяемое утверждение: текущая модель не приняла acknowledgement с ожидаемыми ключами и версией.
- Fixture разделяет `draft.version`, persisted copy, `requestKey`, `attemptKey`, phase и acknowledgement. Retry сохраняет logical request key и меняет attempt key; stale/duplicate payload не делает второй commit; acknowledgement старой версии не очищает более новый draft.
- Первые два абзаца каждой статьи называют наблюдаемый сбой и цену: ложный success, дублирование действия, потерянный текст и невозможность объяснить состояние пользователю.
- Речь строится по маршруту «симптом → причина → проверка → действие». Термины acknowledgement, request key, attempt key, stale reply и recovery привязаны к одной модели, а не используются как абстрактные признаки надёжности.
- В каждой статье есть таблица, figure с содержательными alt/caption, исполнимый JS fixture, нумерованный маршрут, ограничения и проверяемый следующий шаг.
- Размер основного текста проверяется самим скриптом: 8 276 / 9 358 / 9 319 знаков без списка источников. Все три текста попадают в целевой диапазон М5 8–10 тыс. и в обязательный диапазон 5–15 тыс.
## Проход 3 — визуал и выпуск
- Три SVG отвечают на разные вопросы: user-visible network state, contract request/attempt/acknowledgement и диагностика/recovery. В них нет `script`, `foreignObject`, внешних URL или `data:image`.
- Пройдены XML-проверка, safety scan без совпадений и Sharp-рендер всех SVG на ширине 375 px; на узком экране читаются названия состояний, ключевые ветки и нижняя граница модели.
- Sidecar меняет только пять разрешённых файлов. Registry, README, `articles.json`, очередь, QUALITY_STANDARD, `docs/`, Git и чужие sidecar-файлы не затрагиваются. Интеграция и публикация не выполнялись.
## Независимый редакторский приём
### Проход 1 — источники и переходы
Проверены доступность и статус фиксированных источников: WHATWG Fetch Review
Draft декабря 2021 года, Service Workers Candidate Recommendation Draft 12
июля 2022 года и RFC 9110 отвечают `200`. Статья использует их только для
исторической границы Fetch/HTTP/service worker; прикладные keys и
acknowledgement остаются решениями учебной модели.
В fixture усилены три проверки переходов. Снимок persisted draft теперь
сохраняется сразу после offline block; stale acknowledgement сравнивает
phase/request/attempt/acknowledgement до и после обработки; принятие старой
версии сохраняет новый draft в отдельном снимке до duplicate branch. Это
исключает ложный PASS, когда последующее действие маскирует раннюю мутацию.
Ни Fetch, ни транспорт, ни сервер при этом не создаются. Fixture прошла
**15/15 assertions**.
### Проход 2 — текст и полнота
Перечитаны problem/cost, таблицы, исполнимые примеры, routes, rollback и
границы трёх материалов. `unknown-outcome` везде означает отсутствие
model acknowledgement, а не утверждение о судьбе серверного действия. Draft
<titleid="title">Диагностика unknown outcome и безопасное восстановление черновика</title>
<descid="desc">Дерево решений показывает, как от отсутствия acknowledgement перейти к проверке ключей, игнорированию старого ответа, сохранению нового черновика или ручному восстановлению без ложного отката.</desc>
<textx="42"y="46"class="h">Диагностика: не угадывать результат, сохранить контролируемые данные</text>
<rectx="324"y="84"width="252"height="90"class="box"fill="#fee2e2"/><textx="352"y="121"class="h">Нет acknowledgement</text><textx="352"y="151"class="t">visible: result unknown</text>
<rectx="631"y="257"width="215"height="112"class="box"fill="#e9d5ff"/><textx="653"y="296"class="h">Нужна сверка</text><textx="653"y="326"class="t">recovery required</text><textx="653"y="350"class="s">не transport rollback</text>
<textx="45"y="523"class="s">Вопросы относятся к state machine. Реальная сеть, серверная сверка и persistence требуют отдельного теста с условиями.</text>
<descid="desc">Схема связывает версию черновика с request key, две попытки с разными attempt key и acknowledgement, которое проходит проверку всех полей. Старый ответ отмечен как stale.</desc>
<rectx="347"y="316"width="205"height="142"class="box"fill="#dbeafe"/><textx="368"y="355"class="h">attempt-02</text><textx="368"y="387"class="t">тот же draft-v1</text><textx="368"y="417"class="s">один guard retry</text>
<titleid="title">Учебные состояния отправки черновика при плохой сети</title>
<descid="desc">Диаграмма показывает черновик, блокировку offline, ожидание acknowledgement, неизвестный результат, повтор и подтверждение. Все состояния помечены как учебные.</desc>
<defs><markerid="arrow"markerWidth="10"markerHeight="10"refX="8"refY="5"orient="auto"><pathd="M0 0L10 5L0 10z"fill="#1d4ed8"/></marker><style>.box{stroke-width:3;rx:16}.h{font:700 25px Arial,sans-serif;fill:#102a43}.t{font:18px Arial,sans-serif;fill:#243b53}.s{font:15px Arial,sans-serif;fill:#52667a}.line{stroke:#1d4ed8;stroke-width:4;fill:none;marker-end:url(#arrow)}.label{font:700 16px Arial,sans-serif;fill:#1d4ed8}</style></defs>
<rectwidth="900"height="540"fill="#f7fbff"/>
<textx="42"y="48"class="h">Учебная модель: видимое состояние важнее догадки о сети</text>
<rectx="45"y="100"width="210"height="112"class="box"fill="#e0f2fe"stroke="#0284c7"/><textx="68"y="140"class="h">Черновик готов</text><textx="68"y="172"class="t">version + local copy</text><textx="68"y="196"class="s">не равно server save</text>
<rectx="45"y="330"width="210"height="112"class="box"fill="#fef3c7"stroke="#d97706"/><textx="68"y="370"class="h">Offline</text><textx="68"y="402"class="t">попытка не планируется</text><textx="68"y="426"class="s">черновик остаётся</text>
<rectx="345"y="100"width="210"height="112"class="box"fill="#dbeafe"stroke="#2563eb"/><textx="368"y="140"class="h">Ждём ack</text><textx="368"y="172"class="t">request + attempt key</text><textx="368"y="196"class="s">success ещё запрещён</text>
<rectx="345"y="330"width="210"height="112"class="box"fill="#fee2e2"stroke="#dc2626"/><textx="368"y="370"class="h">Unknown</text><textx="368"y="402"class="t">нет model acknowledgement</text><textx="368"y="426"class="s">не факт о сервере</text>
note:'датированный первичный снимок Fetch Standard. Он задаёт модель request, response и network error; он не превращает отсутствие application acknowledgement в доказательство, что серверное действие не произошло.',
};
constserviceWorkersDraft={
title:'W3C Service Workers, Candidate Recommendation Draft от 12 июля 2022 года',
note:'неизменяемая версия периода: service worker событийный и может быть остановлен user agent. В этой партии service worker намеренно не создаётся; документ используется только как историческая граница платформы.',
note:'нормативный RFC, доступный к июлю 2022 года. Он различает свойства HTTP-методов и не заменяет прикладной ключ запроса или подтверждение предметной операции.',
excerpt:'Практический контракт для формы, которая должна сохранить черновик, честно показать неизвестный результат и не создать второй смысловой запрос при повторе.',
readingMinutes:13,
},
[
paragraph('На плохой сети кнопка «Сохранить» легко врёт. Пользователь нажал её, экран уже показал зелёное сообщение, но приложение не получило подтверждения. При следующем открытии формы черновик исчезает; при повторном нажатии команда отправляет вторую операцию. Цена ошибки не в сером индикаторе: человек не знает, можно ли закрыть страницу, нужно ли повторять действие и какая версия текста теперь считается сохранённой.'),
paragraph('В июле 2022 года я бы начал не с offline-first лозунга и не с эмулятора сети. Сначала нужен видимый контракт одного действия: черновик отдельно от подтверждённого результата, явное состояние сети как вход интерфейса, ключ логического запроса и ключ конкретной попытки. Ниже — учебная in-memory fixture. Она не вызывает Fetch, не запускает service worker и не получает ответ сервера. Поэтому её состояния <code>offline</code> и <code>unknown-outcome</code> — проектные labels, а не отчёт о реальной сети.'),
heading('Четыре состояния, которые нельзя склеивать'),
paragraph('У отправки есть как минимум четыре разных факта. Первый: текст черновика сохранён локально в выбранном хранилище. Второй: интерфейс сейчас считает сеть недоступной или доступной. Третий: конкретная попытка ожидает прикладное подтверждение. Четвёртый: серверное подтверждение с идентификатором предметной операции действительно принято клиентской логикой. Если заменить эти факты одним boolean <code>isSaved</code>, UI обязательно назовёт один из них другим.'),
dataTable(
'Видимый контракт отправки черновика',
['Состояние интерфейса','Что известно','Что нельзя утверждать','Действие'],
[
['Черновик готов','есть сохранённая локальная копия и её версия','что сервер видел текст','дать редактировать и показать дату/версию локальной копии'],
['Ожидаем подтверждение','создан request key и одна attempt key','что операция уже завершилась','не показывать success; защитить повторный клик'],
['Результат неизвестен','для попытки нет прикладного acknowledgement','что операция не дошла до сервера','сохранить черновик, дать повторить или перейти к восстановлению'],
['Подтверждено','получен payload с request key, версией и submission id','что более новый черновик можно удалить','очистить только подтверждённую версию, новый draft оставить'],
],
),
heading('Почему «запрос упал» не равно «ничего не произошло»'),
paragraph('Fetch Standard описывает request, response и network error, но у прикладного экрана другая задача: он должен решить, что показать человеку без домысла о чужой стороне. Между началом операции и сообщением UI могли существовать промежуточные границы, которых экран не видит. Поэтому здесь не используется фраза «сервер точно не сохранил». Вместо неё есть более узкое и проверяемое условие: текущая модель не получила acknowledgement, связанный с текущими <code>requestKey</code> и <code>attemptKey</code>.'),
paragraph('Это продолжает июньский разбор ошибок формы. Там важно было не перезаписать исправленное поле поздней ошибкой. Здесь важно не стереть текст и не объявить победу поздним или отсутствующим подтверждением. В обоих случаях источник проблемы — два разных состояния получают одно имя «сохранено». Версия черновика, попытка доставки и признанный результат должны жить рядом, но не заменять друг друга.'),
paragraph('Видимый текст должен следовать этой модели. «Черновик сохранён на устройстве» и «изменение подтверждено» — разные сообщения с разными условиями показа. Первое можно показать после успешного шага persistence выбранного приложения, второе — только после принятого acknowledgement. Если данные для первого сообщения ещё не записаны, интерфейс обязан показать это как отдельную ошибку, а не использовать сетевой значок вместо результата.'),
heading('Учебная fixture: ключ запроса стабилен, ключ попытки меняется'),
paragraph('В модели пользователь пишет черновик версии 1. Для него создаётся <code>requestKey: draft-v1</code>. Первая попытка получает <code>attempt-01</code>. Если модель не получает acknowledgement, она показывает <code>result-unknown</code> и сохраняет черновик как источник восстановления. После явно объявленного возвращения в <code>online</code> разрешён один retry: он сохраняет тот же request key, но использует <code>attempt-02</code>. Это защищает от параллельного повтора и позволяет отличить поздний ответ первой попытки от ответа второй.'),
codeBlock(fixtureExample),
paragraph('Пятнадцать 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» — входы учебной модели, не измерение соединения.',
),
heading('Маршрут: симптом → причина → проверка → действие'),
orderedList([
'<strong>Симптом.</strong> После нажатия Save интерфейс быстро показывает успех, повторяет запрос при повторном клике либо открывается с пустым текстом после offline.',
'<strong>Причина.</strong> Черновик, транспортная попытка и подтверждённая операция записываются в одно поле состояния. Отсутствие ответа трактуется как failure или success без отдельного правила.',
'<strong>Проверка данных.</strong> Выпишите отдельно draft version, persisted draft, request key, attempt key, phase и acknowledgement payload. Если одно поле отвечает за два пункта, найдите владельца каждого перехода.',
'<strong>Проверка неизвестного исхода.</strong> Смоделируйте ветку без acknowledgement. В ней не должно быть success и не должен исчезать persisted draft. В реальном проекте эту ветку затем проверяют отдельным контролируемым сценарием, а не выдают модель за trace.',
'<strong>Действие.</strong> До acknowledgement показывайте «результат пока неизвестен», отключайте параллельный retry и разрешайте один осознанный повтор с тем же logical request key.',
'<strong>Восстановление.</strong> Дайте безопасный путь оставить черновик для проверки. Автоматически очищайте только версию, которую признал payload; более новый текст не принадлежит старой попытке.',
paragraph('Fixture называет persistence <code>in-memory-copy-only</code>. Это намеренное ограничение: переменная в учебной модели переживает только её выполнение, а не перезагрузку, вкладки или сбой процесса. Реальное приложение должно отдельно выбрать хранилище, срок жизни, шифрование, правила очистки и поведение при нескольких вкладках. Service Workers CRD от 12 июля 2022 года показывает, что сервисный worker — событийный контекст с собственным жизненным циклом; это не основание предполагать, что он всегда запущен или уже спас конкретный черновик.'),
paragraph('Полезно начать с малого контракта: «после каждого изменения черновик либо сохранён вместе с версией, либо UI честно показывает, что локальной копии нет». Потом добавляется реальное хранилище и отдельный тест его ошибок. Не стоит прятать это решение под названием offline cache. Для текста профиля и для перевода денег различаются срок хранения, чувствительность данных, право на повтор и даже допустимое сообщение на экране.'),
heading('Ограничение и следующий проверяемый шаг'),
paragraph('Эта модель не выбирает HTTP-метод, не реализует idempotency endpoint, не выполняет request, не хранит данные после рестарта и не решает конфликт двух устройств. RFC 9110 говорит о свойствах методов, но прикладной <code>requestKey</code> остаётся вашим контрактом: он должен быть понятен серверу и связан с предметной операцией, если она допускает повтор. Нельзя перенести строку <code>draft-v1</code> из fixture в production как готовый механизм защиты.'),
paragraph('Следующий шаг — взять одну настоящую форму и написать рядом с ней таблицу из четырёх состояний. Затем добавить локальный тест: редактирование создаёт новую version; отсутствие acknowledgement сохраняет черновик; поздний ответ не перезаписывает новый текст. После этого команда может провести отдельный browser и network сценарий с названными версиями среды. Его результаты будут новым артефактом, а не обещанием этой статьи.'),
heading('Историческая граница июля 2022'),
paragraph('Здесь использованы неизменяемые источники, доступные к июлю 2022 года: Fetch Review Draft от 19 декабря 2021 года, Service Workers Candidate Recommendation Draft от 12 июля 2022 года и RFC 9110 июня 2022 года. Ни один из них не используется как доказательство конкретного offline-прогона. Все ключи, версии, acknowledgement и видимые тексты — данные учебной модели пакета.'),
],
commonSources,
);
constmechanismArticle=createRevision(
{
slug:'editorial-2022-07-mechanism-poor-network',
title:'Плохая сеть: граница между попыткой, подтверждением и черновиком',
excerpt:'Разбираем контракт, в котором повтор не создаёт новый логический запрос, старый ответ не стирает новый черновик, а неизвестный исход остаётся видимым состоянием.',
readingMinutes:14,
},
[
paragraph('Самая неприятная ошибка плохой сети выглядит аккуратно: кнопка перестала крутиться, форма закрылась, пользователь уверен, что всё сохранено. Через минуту приходит старый ответ, а в это время уже набран новый текст. Если обработчик не различает попытки, поздний payload очищает новый черновик или повторно запускает переход. Цена — потеря работы и состояние, которое невозможно объяснить по одному скриншоту.'),
paragraph('Причина обычно не в одном timeout. Экрану не принадлежит знание о том, была ли операция выполнена на другой стороне; ему принадлежит только локальная последовательность: создан черновик, запланирована попытка, принят или не принят acknowledgement. В этой статье механизм описан учебной моделью. В ней нет HTTP, реального server acknowledgement или таймера. Payload — объект JavaScript с пометкой <code>teaching-model-payload-not-server-response</code>, поэтому выводы относятся к contract, а не к сети.'),
heading('Два ключа отвечают на два разных вопроса'),
paragraph('Request key отвечает на вопрос «какую логическую операцию пытается завершить пользователь?». Attempt key отвечает на другой: «какой именно запуск этой операции сейчас ожидает ответ?». При unknown outcome повтор не должен молча создавать новую логическую операцию; ему нужен тот же request key и новый attempt key. Иначе серверная сторона не сможет сопоставить повтор с исходным намерением, а клиент — отличить поздний ответ первого запуска от актуального второго.'),
['draft.version','редактор черновика','при каждом содержательном изменении','новый текст от очистки старой версии'],
['requestKey','submit boundary','при новой версии, до первой попытки','смысл одной операции и серверную идемпотентность, если сервис её поддерживает'],
['attemptKey','retry guard','при каждой разрешённой попытке','поздний или duplicate reply от неверного commit'],
['submissionId','acknowledgement payload модели','после признанного подтверждения','видимую связь UI с подтверждённой операцией'],
],
),
heading('Почему retry guard — это не просто disabled button'),
paragraph('Кнопку можно временно заблокировать CSS-классом, но contract остаётся дырявым, если другой handler, повторный event или восстановление state могут создать ещё одну попытку. Guard должен находиться возле transition. В модели <code>beginSubmission</code> отказывает, если фаза уже <code>awaiting-ack</code> или <code>unknown-outcome</code>. <code>retryUnknownOutcome</code> проверяет именно unknown phase, объявленное online state и лимит одной повторной попытки. Это решение модели, не универсальная retry policy.'),
paragraph('Лимит в одну попытку здесь выбран, чтобы было видно место решения. Продукту может потребоваться другой лимит, ручной confirm или вовсе запрет повтора. Важно не число, а доказуемость: для каждого retry можно назвать request key, attempt key, владелец правила и сообщение пользователю. Цикл «повторяем пока не получится» особенно опасен там, где человек уже не наблюдает экран и где операция имеет внешнее последствие.'),
heading('Учебный код: unknown outcome не подменяется ошибкой'),
paragraph('После первой попытки fixture вызывает <code>markUnknownOutcome</code>. Она не получает exception и не измеряет канал связи. Функция лишь проверяет, что её attempt key совпадает с текущей, меняет фазу на <code>unknown-outcome</code> и сохраняет note о черновике. Следующий retry создаёт <code>attempt-02</code>, но оставляет <code>draft-v1</code>. Такой порядок можно выполнить как обычный JavaScript и проверить без browser sandbox.'),
codeBlock(contractExample),
paragraph('Это специально бедный пример. Он не учит, как вызвать <code>fetch()</code>, не имитирует задержку, не обрабатывает network exception и не утверждает, что сервис обработал request key. Его ценность в другом: reviewer видит, что UI не имеет права перейти в success только из того, что локальный обработчик закончил работу. Переход в success допускает только acknowledgement с совпадающими ключами и версией.'),
'Схема contract: draft version создаёт request key; первая попытка получает attempt-01; при unknown outcome retry использует прежний request key и attempt-02; acknowledgement коммитится только если совпадают оба ключа и accepted version. Старый ответ отмечен как stale и не меняет черновик.',
'Разделение request и attempt позволяет сделать поздний ответ наблюдаемым, но не опасным для текущего состояния.',
),
heading('Ack — это сообщение, а не зелёная анимация'),
paragraph('Acknowledgement модели содержит <code>submissionId</code>, <code>requestKey</code> и <code>acceptedVersion</code>. Эти поля проверяются до commit. Если пришёл ответ от <code>attempt-01</code>, когда UI ждёт <code>attempt-02</code>, результат называется <code>stale-or-invalid-acknowledgement</code> и ничего не меняет. Если та же acknowledgement приходит второй раз после commit, она называется <code>duplicate-acknowledgement</code>. Это не ошибка транспорта и не HTTP-код: это способ не позволить одному и тому же UI transition случиться дважды.'),
paragraph('Особенно важна accepted version. Пользователь может продолжить редактирование, пока первоначальная операция ожидает подтверждения. Тогда draft становится версией 2, а acknowledgement относится к версии 1. Очистить текущий текст было бы потерей данных. Fixture оставляет version 2 и показывает <code>acknowledged-newer-draft-retained</code>. Значит, экран одновременно знает две вещи: прежняя операция признана, но новая работа ещё не вошла в неё.'),
heading('Связь с HTTP без ложного вывода'),
paragraph('RFC 9110 использует точное понятие идемпотентности для HTTP-методов, но frontend не должен на этом основании выбирать любые повторы. Метод, тело запроса, предметная операция и поведение сервиса образуют один контракт. Например, даже если transport-level повтор допустим, экрану всё равно нужен способ показать пользователю, к какой версии относится подтверждение. Поэтому <code>requestKey</code> в статье не назван заголовком, endpoint-параметром или готовой схемой API: это роль, которую API и клиент должны согласовать.'),
paragraph('Исторический Fetch Review Draft полезен здесь другой границей: он описывает платформенный алгоритм выборки, но не знает бизнес-смысл «сохранить черновик». Переход из transport-level события к domain acknowledgement проект должен сделать сам. Если этот переход не оформлен, любые названия вроде <code>isSuccess</code> становятся удобной, но ложной абстракцией.'),
heading('Маршрут: симптом → причина → проверка → действие'),
orderedList([
'<strong>Симптом.</strong> Старая попытка отвечает после retry, дубль ответа повторно закрывает форму или новый текст исчезает после acknowledgement старой версии.',
'<strong>Причина.</strong> UI хранит один «pending» флаг, не связывает reply с attempt key либо считает acknowledgement равным визуальному окончанию handler-а.',
'<strong>Проверка ключей.</strong> В логике состояния найдите request key, attempt key и version. Сравните их в одном месте до каждого commit; не полагайтесь на порядок arrival.',
'<strong>Проверка guard.</strong> Запустите fixture и убедитесь, что retry до unknown phase и retry в declared offline не создают попытку. Затем добавьте аналогичный unit test рядом с реальным reducer или store.',
'<strong>Действие.</strong> Введите phase machine: idle, awaiting acknowledgement, unknown outcome, acknowledged, recovery required. Дайте каждому переходу owner и видимый текст.',
'<strong>Откат.</strong> Если результат неизвестен, оставьте persisted draft. Не пытайтесь «откатить сервер» без отдельного доменного контракта; сначала дайте человеку безопасно сохранить собственный текст.',
]),
heading('Граница service worker и фоновой работы'),
paragraph('Service Workers CRD в июле 2022 года описывает событийный worker и прямо требует учитывать, что user agent может остановить его. Это важная причина не строить UI-обещание на предположении «фоновая часть всё завершит». Но из этого не следует запрет использовать service worker в продукте. Следует более узкое правило: если конкретный механизм берёт на себя retry или persistence, его жизненный цикл, очередь, права и результат acknowledgement должны быть видны в собственном contract и тестах.'),
paragraph('Учебная модель не создаёт service worker специально. Иначе простой вопрос о владении draft был бы спрятан за API. Когда базовая state machine уже есть, команда может добавить следующий уровень: как реальное хранилище восстанавливает version, как worker передаёт факт доставки в открытый документ, что происходит при обновлении worker и когда пользователь видит «нужно проверить». Каждая из этих веток потребует нового проверяемого артефакта.'),
heading('Ограничение и следующий шаг'),
paragraph('Модель ограничена одним активным logical request и одним разрешённым retry. Она не решает несколько вкладок, auth refresh, очередь изменений, конфликт сервера, CRDT, encryption и настоящий idempotency storage. Она также не доказывает, что network state браузера соответствует строке <code>online</code>. Чем сложнее предметная операция, тем опаснее подменять эти открытые вопросы словом «offline sync».'),
paragraph('Следующий шаг — провести contract review одной формы вместе с API-владельцем. Зафиксируйте: что является logical operation, где живёт request key, какой payload подтверждает версию, как называется unknown outcome, кто ограничивает retry и когда черновик можно удалить. После этой таблицы добавьте две проверки: stale acknowledgement не меняет state, acknowledgement старой версии не очищает новый draft. Это уже практическая защита от наиболее дорогой ошибки — необъяснимой потери пользовательского текста.'),
heading('Историческая граница июля 2022'),
paragraph('Источники фиксированы датированными URL: Fetch Review Draft 2021, Service Workers CRD от 12 июля 2022 года и RFC 9110 июня 2022 года. Статья не переносит назад более поздние API и не заявляет, что service worker или HTTP-клиент этой fixture были запущены. Названия phases и ключей — проектные значения, пригодные для обсуждения и unit-проверки.'),
],
commonSources,
);
constfieldArticle=createRevision(
{
slug:'editorial-2022-07-field-poor-network',
title:'Плохая сеть: диагностировать неизвестный результат и не потерять черновик',
excerpt:'Полевой маршрут для спорного случая: пользователь не получил ответ, старая попытка отвечает поздно, а интерфейс должен сохранить текст и предложить проверяемое восстановление.',
readingMinutes:14,
},
[
paragraph('После жалобы «сохранение иногда пропадает» легко начать с увеличения timeout. Это опасная реакция: проблема может быть в раннем success toast, в очистке draft до acknowledgement, в повторной отправке или в позднем ответе, который относится к уже неактуальной попытке. Если сразу менять transport, команда не узнает, какой факт видит пользователь. Цена ошибки — потерянный текст и повторяющийся дефект, который нельзя надёжно воспроизвести из-за отсутствия состояния.'),
paragraph('Для диагностики я бы взял один учебный сценарий и запретил ему изображать реальную сеть. В fixture есть declared <code>offline</code>, затем <code>unknown-outcome</code>, затем один retry и payload acknowledgement. Нет fetch, network throttling, browser trace, таймеров и сервера. Эта узкая граница полезна: можно сначала доказать, что UI выдерживает stale reply и сохраняет новый draft, а уже потом собирать реальный стенд с версиями и условиями.'),
heading('Сначала назовите наблюдение, а не диагноз'),
dataTable(
'Матрица первичной диагностики',
['Наблюдение на экране','Нельзя заключать','Что проверить в state','Безопасное первое действие'],
[
['Пользователь не увидел результата','операция точно не выполнена','phase, request key, attempt key, acknowledgement','показать unknown outcome и оставить draft'],
['Успех показан сразу после клика','сервер подтвердил версию','кто выставляет success state и от какого payload','перенести success после validation acknowledgement'],
['После retry приходит старый ответ','это подтверждение текущей попытки','совпадает ли attempt key','проигнорировать stale reply без очистки draft'],
['Открылся пустой редактор','сохранение прошло','persisted draft и его version перед очисткой','восстановить локальную копию или честно показать её отсутствие'],
['Пользователь набрал новый текст','старый ack можно применить ко всему экрану','accepted version и current draft version','подтвердить старую операцию, но сохранить новый текст'],
],
),
heading('Один разбор: поздний ответ после повторной попытки'),
paragraph('В модели пользователь создаёт draft версии 1, запускает <code>attempt-01</code> и не получает model acknowledgement. Экран показывает результат неизвестен. Offline label блокирует retry, но draft остаётся в persisted copy. После declared online создаётся <code>attempt-02</code> с тем же <code>requestKey</code>. Затем прилетает payload для первой попытки. Он выглядит правдоподобно: та же версия и тот же request key. Но attempt key другой, значит это не разрешение текущего ожидания. Модель возвращает <code>stale-or-invalid-acknowledgement</code> и не меняет visible state.'),
paragraph('Это правило защищает не от «плохого ответа», а от неправильного владельца перехода. Старый payload может быть полезен для журнала или отдельной сверки, но он не должен закрывать форму автоматически. Сначала UI обязан решить, что он подтверждает. В учебном контракте ответ подтверждает только конкретную ожидаемую попытку. Реальный продукт может выбрать более сложную сверку через серверный статус, но тогда и это действие нужно описать отдельно, а не спрятать в catch-block.'),
heading('Fixture: поздний и duplicate payload не делают второй commit'),
paragraph('Ниже показан только вызов принятия acknowledgement. До него fixture уже проверила ключи и фазу. Payload создан вручную; поле <code>submissionId</code> — не ID реальной базы. Сначала ответ от <code>attempt-01</code> отвергается как stale. Затем acknowledgement для <code>attempt-02</code> принимает версию 1. Если после этого пришёл ещё один такой же payload, результат — <code>duplicate-acknowledgement</code>, а второй commit не происходит.'),
codeBlock(recoveryExample),
paragraph('В сценарии есть ещё одна неприятная деталь. Между retry и правильным acknowledgement пользователь меняет текст. Draft становится версией 2. Accepted payload относится к версии 1, поэтому модель выбирает <code>acknowledged-newer-draft-retained</code>. Она не скрывает прежний успех, но не уничтожает новую работу. Такой ответ интерфейса длиннее одного toast, зато он объясняет ситуацию: «предыдущее сохранение подтверждено; текущая правка ещё черновик».'),
'Диагностическая схема: отсутствие учебного acknowledgement оставляет draft и показывает unknown outcome; offline retry блокируется; online retry получает новый attempt key; reply старой попытки игнорируется; ack версии 1 не очищает новый draft версии 2; отдельная recovery-ветка сохраняет draft для ручной проверки.',
'Схема не изображает HTTP-пакеты. Она помогает выбрать действие для каждого наблюдаемого состояния, прежде чем менять сетевой слой.',
),
heading('Восстановление должно быть безопаснее догадки'),
paragraph('Когда outcome неизвестен, интерфейс часто предлагает слишком смелую кнопку: «Отправить ещё раз». Она может быть полезна, но не должна быть единственным путем. В модели <code>keepDraftForRecovery</code> переводит фазу в <code>recovery-required</code>, оставляет persisted draft и не планирует transport. Это не откат сервера и не восстановление связи. Это честный переход: пользовательский текст сохранён, а предметный результат требует отдельной проверки.'),
paragraph('Такой путь особенно важен для операций, где повтор имеет цену. Для комментария можно предусмотреть согласованный retry; для изменения реквизитов или финансового действия может потребоваться сверка со списком операций, обращение в поддержку или серверный status endpoint. Текст статьи не назначает политику за продукт. Он требует, чтобы UI не стирал единственный артефакт, который точно находится под его контролем: локально сохранённый черновик.'),
heading('Как собрать реальную проверку после модели'),
paragraph('После unit fixture нужна другая работа, и её нельзя выдать за уже сделанную. Зафиксируйте browser, версию приложения, способ включения offline или потери ответа, тип формы, содержимое тестового черновика и ожидаемые видимые states. Запишите, где лежит реальное persistence, как очищается test data и каким способом сервер выдаёт acknowledgement. Затем отдельно проведите сценарии: отправка без ответа, retry, stale reply, редактирование до позднего ответа и восстановление после перезагрузки.'),
paragraph('Результатом должен стать trace или тестовый отчёт с условиями, а не просто фраза «проверено на плохой сети». W3C Service Workers CRD напоминает, что worker — не вечный фон. Fetch Review Draft описывает слой platform fetching, но не бизнес-подтверждение. Эти источники полезны, чтобы не путать уровни, однако не заменяют наблюдение выбранного браузера и API конкретного продукта.'),
heading('Маршрут: симптом → причина → проверка → действие'),
orderedList([
'<strong>Симптом.</strong> Зафиксируйте один факт: success без acknowledgement, пустой draft, старый reply после retry или новая версия, исчезнувшая после save. Не называйте это «сетью» до проверки state.',
'<strong>Причина.</strong> Проверьте, не объединены ли draft, persisted copy, request key, attempt key и acknowledgement одним boolean или одним reducer case.',
'<strong>Проверка модели.</strong> Выполните <code>node web/scripts/upgrade-2022-07.mjs --verify-fixture</code>. PASS означает только соблюдение учебного контракта: offline block, unknown outcome, retry guard, stale/duplicate reply и retention нового draft.',
'<strong>Проверка реализации.</strong> В реальном коде найдите все места, которые очищают draft или показывают success. Для каждого запишите, каким acknowledgement и какой version он разрешён.',
'<strong>Действие.</strong> Добавьте named visible states и один безопасный путь recovery. Запретите очистку persisted draft, пока accepted version не совпала с его версией.',
'<strong>Проверка после изменения.</strong> Проведите контролируемый browser-сценарий с документированными условиями. Если он расходится с моделью, исправьте contract или зафиксируйте платформенное ограничение, а не маскируйте его повтором.',
]),
heading('Что не исправляет эта статья'),
paragraph('Не исправит проблему один глобальный indicator online/offline. Он может быть полезным сигналом, но не сообщает, приняла ли предметная операция версию текста. Не исправит проблему и бесконечный retry: он может ухудшить последствия и скрыть факт неизвестного исхода. Не исправит её один service worker: без contract для keys, persistence и acknowledgement фоновые задачи делают состояние менее, а не более понятным.'),
paragraph('Модель также не покрывает несколько вкладок, параллельные формы, undo после подтверждения, безопасность локального текста, права пользователя, обновление приложения и различие между DNS, captive portal и серверной ошибкой. Эти темы нельзя свести к одному boolean network state. Хорошая новость в том, что базовое разделение draft / attempt / acknowledgement остаётся полезным и перед следующей сложностью: оно даёт место, куда добавить новый факт, не ломая существующие переходы.'),
heading('Ограничение и следующий шаг'),
paragraph('Учебная модель не является network test и не даёт production SLO. Её ключи, retry limit, тексты и payload придуманы для детерминированной проверки. Если ваш API не умеет признать logical request или не возвращает версию, нельзя заявлять, что простая client-side защита решила дубли. Нужно сначала договориться о server contract и только затем выбирать transport и UI.'),
paragraph('Следующий практический шаг — добавить в одну форму журнал переходов для development или теста: draft version, request key, attempt key, phase, source acknowledgement и причина очистки. Не храните в нём чувствительный текст. Затем попросите reviewer пройти матрицу этой статьи на одном PR. Если он может показать, почему старый payload не меняет новый draft и где остаётся текст после unknown outcome, у изменения уже есть проверяемая граница.'),
heading('Историческая граница июля 2022'),
paragraph('Пакет опирается на Fetch Review Draft 2021 года, Service Workers Candidate Recommendation Draft от 12 июля 2022 года и RFC 9110 июня 2022 года. Все источники имеют датированные, неизменяемые URL. Они используются для терминов и границ платформы; fixture не утверждает, что в ней был реальный offline, HTTP-запрос, service worker или server acknowledgement.'),
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.