Отзыв с iPhone после первого живого прогона. Масштабирование запрещено (ADR-066): viewport как в соседнем sixlines плюс viewport-fit=cover, мета-теги установленного приложения, touch-action против двойного тапа и страховка на JS — гашение жестов iOS, потому что мета-строку Safari вправе проигнорировать. Размеры шрифтов не тронуты: 14 px из брифа остаются, поднимать поля до 16 px владелец отклонил. Цена — доступность для тех, кому зум нужен — записана в ADR и в ui.md честно. Версия в подвале (ADR-065, 067): GET /api/config отдаёт короткую ревизию и время коммита. Именно коммита, а не сборки: штамп компиляции в бинаре means каждая пересборка даёт новый хеш, и обещание ADR-022 «бинарь собирается из тега байт в байт» перестаёт проверяться. Самообновление PWA (ADR-068, 070): в режиме приложения на iPhone перезагрузки нет, и старая версия жила бы вечно. Проверка при возврате в приложение, включение ждущей оболочки, перезагрузка — но не тогда, когда в строке набран текст. Замок от петли сравнивает версию оболочки, а не считает перезагрузки: счётчик отменял саму фичу, долгоживущая вкладка переставала обновляться. Замерено: было 30, 60, 120, 240 секунд между релизами, стало ровно 30. Строка ввода — редактируемый блок (ADR-069): iOS рисует над клавиатурой полосу со стрелками и «готово» для полей формы, и убрать её со страницы нельзя. Для contenteditable не рисует. Режим plaintext-only с проверкой, что он применился, и запасным путём с разбором вставки: невалидное значение атрибута в части браузеров делает блок нередактируемым вовсе. Чтение и запись только через textContent, innerHTML в клиенте по-прежнему нет. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015DbCjVfTFq4ZFG8juD45YJ
34 lines
8.4 KiB
Markdown
34 lines
8.4 KiB
Markdown
# ADR-068: Приложение обновляется само
|
||
|
||
Меняет политику обновления из [ADR-023](023-push-and-service-worker.md): новая оболочка забирает управление, не дожидаясь закрытия всех вкладок.
|
||
|
||
Изменён [ADR-070](070-update-compares-shell-version.md): второй замок — растущая пауза и счётчик `bare-updates` — заменён сверкой версии оболочки.
|
||
|
||
Уточнён [ADR-072](072-deferred-update-returns-by-timer.md): к отложенному набранным вкладка возвращается ещё и по таймеру — поле пустеет и молча.
|
||
|
||
## Контекст
|
||
|
||
`sw.js` обновлялся по умолчанию браузера: новая версия устанавливается, ждёт, и берёт управление, когда закрыты все вкладки приложения. Для сайта это правильно — страница не перезагружается под руками.
|
||
|
||
Для установленного на «Домой» приложения это означает «никогда». Его не закрывают: iOS держит его в списке приложений неделями. Строки адреса и кнопки перезагрузки в нём нет — обновить оболочку человеку нечем вовсе. Он остаётся на версии, которая была при установке, и починенное на сервере до него не доезжает.
|
||
|
||
Перезагрузка при этом не бесплатна: она уносит набранное в строке ввода и в формах. Пароль в форме входа теряется так же, как черновик сообщения.
|
||
|
||
## Решение
|
||
|
||
- Клиент спрашивает сервер об обновлении (`registration.update()`) при запуске и при каждом возвращении в приложение (`visibilitychange` → `visible`), но не чаще раза в минуту. Проверка — один условный запрос за `/sw.js`. Без сети запрос падает молча: офлайн не сбой.
|
||
- Установленная версия не включается сама. Её включает страница: `postMessage("skip-waiting")` воркеру, который ждёт. Воркер отвечает `skipWaiting()`, `activate` забирает клиентов (`clients.claim` там уже есть), браузер шлёт `controllerchange`, страница перезагружается. Просьба уходит только тогда, когда страницей уже кто-то управляет: первая установка проходит через то же состояние «установлен», но включать там нечего — воркер активируется сам, а страница и так свежая.
|
||
- Набранное откладывает всё: и просьбу включиться, и перезагрузку. Проверяется весь документ, а не строка ввода: клиент — одна страница, и знать про экраны здесь незачем. Набранным считается непустое `textarea` и непустое поле, куда набирают текст (`text`, `password`, `search`, `email`, `url`, `tel`, `number`); чекбокс, файл, скрытое поле и кнопка не в счёт — у них значение непусто по определению, и одно такое поле запретило бы обновление насовсем. Отложенное трогается с места, когда поля опустели (событие `input`) или когда человек вернулся в приложение.
|
||
- Ждёт каждая вкладка за себя. `skipWaiting()` меняет контроллёра у всех клиентов профиля сразу, а вкладок одного профиля бывает несколько (ADR-035): вкладка с пустыми полями включает новую оболочку, и соседняя с недописанным сообщением остаётся под ней на старой странице — до своей перезагрузки, которой она ждёт по своим полям. Договариваться вкладкам не о чем: терять нечего никому, а пересчёт голосов стоил бы протокола между ними.
|
||
- Петля закрыта двумя замками. Первый: `controllerchange` перезагружает страницу один раз за её жизнь. Первую установку он отсекает — там контроллёр появляется впервые, `clients.claim` меняет его и без обновления, а страница и так свежая; замок эту смену съедает и запоминает, что страница стала управляемой. Дальше смена контроллёра означает новую оболочку, чью бы просьбу воркер ни исполнял — свою или соседней вкладки. Второй: в `sessionStorage` лежат время последней перезагрузки ради обновления (`bare-updated-at`) и их число в этой вкладке (`bare-updates`), а пауза перед следующей удваивается — 30 секунд, минута, две, четыре и дальше. Второй замок нужен против сервера, отдающего новый `sw.js` на каждый запрос: постоянная пауза задавала бы такой петле только темп, а растущая гасит её — после десятой перезагрузки ждать четыре часа, после двенадцатой сутки, дальше пауза перестаёт расти за ненадобностью. Обычные релизы её не замечают: между ними проходит больше.
|
||
- Версия кэша в `sw.js` по-прежнему меняется при релизе, и по-прежнему руками (`docs/deploy.md`): именно её смена делает файл воркера другим и запускает всё описанное.
|
||
|
||
## Следствия
|
||
|
||
- Установленное приложение доезжает до новой версии само — при следующем открытии или в течение минуты после того, как его открыли.
|
||
- Перезагрузка случается посреди сеанса. Терять ей нечего: набранное её откладывает, история в IndexedDB её переживает, открытый чат задан адресом и открывается снова.
|
||
- Человек, оставивший недописанное сообщение, остаётся на старой версии, пока не допишет или не сотрёт. Это правильный размен: версия важнее черновика только на словах.
|
||
- Вкладка с недописанным сообщением какое-то время исполняет старую оболочку под новым воркером: соседняя вкладка включила его, не спросив. Это ничего не стоит — модули страницы уже загружены, догружать их некому (`import()` в клиенте нет), а `/api/` мимо кэша идёт всегда.
|
||
- У клиента появилось хранилище кроме IndexedDB — два ключа в `sessionStorage`, живущие не дольше вкладки. Записано в `docs/storage.md`.
|
||
- Отката к «ждать закрытия всех вкладок» не предвидится: на iOS это состояние недостижимо.
|