Отзыв с 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
8.4 KiB
ADR-068: Приложение обновляется само
Меняет политику обновления из ADR-023: новая оболочка забирает управление, не дожидаясь закрытия всех вкладок.
Изменён ADR-070: второй замок — растущая пауза и счётчик bare-updates — заменён сверкой версии оболочки.
Уточнён ADR-072: к отложенному набранным вкладка возвращается ещё и по таймеру — поле пустеет и молча.
Контекст
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 это состояние недостижимо.