9.3 KiB
ADR-006: Офлайн-устойчивость v1 и ограничения iOS
Дата: 2026-03-20 Статус: Accepted Автор: Architect Agent (Kin pipeline, BATON-003) Решения: #1016, #1019, #1024, #1025
Контекст
Два связанных вопроса:
- Offline queue: если пользователь нажал SOS без сети — что происходит? Нужна ли очередь с повторной отправкой?
- iOS ограничения: PWA standalone недоступен в EU (iOS 17.4 DMA)? 7-дневная очистка кеша? Приватный режим?
Часть 1: Offline queue
Варианты
Вариант A: Показать ошибку, нет retry (v1)
Нажатие → navigator.onLine === false → "Нет подключения. Проверьте сеть."
Плюсы:
- Ноль дополнительного кода
- Предсказуемое поведение: нет сети = нет сигнала
- Пользователь знает, что сигнал НЕ отправлен (нет ложной надежды)
Минусы:
- Сигнал потерян, если пользователь не нажмёт повторно
- navigator.onLine ненадёжен (может вернуть true при captive portal)
Вариант B: localStorage queue + online event
Нажатие → try fetch → fail → JSON.stringify в localStorage → online event → flush
Плюсы:
- Простая реализация (~20 строк)
- Сигнал не теряется
Минусы:
- iOS Safari private mode: localStorage недоступен (#1015)
- localStorage недоступен в Service Worker контексте
- При закрытии вкладки до online event — сигнал не отправлен (но сохранён)
Вариант C: IndexedDB + BackgroundSync (ADR-002 plan)
Плюсы:
- Самое надёжное: IndexedDB доступен из SW, BackgroundSync работает даже при закрытой вкладке (Chromium)
- Idempotency key защищает от дубликатов
Минусы:
- IndexedDB API громоздкий
- BackgroundSync: 78.75% покрытие (Safari/Firefox не поддерживают)
- Сложность x10 по сравнению с Вариантом A
Решение (offline)
Выбран Вариант A для v1 (#1019): показать ошибку, нет retry.
Переход на Вариант C (IndexedDB + BackgroundSync) запланирован для v2 (полная спека в ADR-002).
Обоснование (offline)
-
#1019: cache-first SW без offline queue достаточен для low-load PWA v1. Приложение открывается мгновенно (кешированные статические файлы). Если сети нет — пользователь видит ошибку и повторяет попытку когда сеть вернётся.
-
Частота использования: ~1 раз/неделю. Вероятность "нажал SOS без сети и забыл повторить" — крайне низкая. Экстренная ситуация мотивирует повторную попытку.
-
navigator.onLine проверка + UX:
button.addEventListener('click', async () => { if (!navigator.onLine) { showError('Нет подключения. Проверьте сеть и попробуйте снова.'); return; } try { await sendSignal(); showSuccess(); } catch (e) { showError('Ошибка отправки. Попробуйте снова.'); } });Даже если
navigator.onLineненадёжен — try/catch ловит ошибку fetch. -
Вариант C (IndexedDB + BackgroundSync) — зарезервирован для v2. ADR-002 содержит полную спецификацию. Переход потребует ~4 часа dev-работы.
Часть 2: iOS 17.4 EU DMA (#1016)
Хронология
| Дата | Событие |
|---|---|
| Январь 2024 | Apple анонсировала удаление PWA standalone в EU (iOS 17.4 beta) |
| 1 марта 2024 | Apple отменила решение после массовой критики и EC inquiry |
| iOS 17.4 release | PWA standalone работает в EU без ограничений |
| Март 2026 (сейчас) | PWA standalone работает в EU на iOS 17.x, 18.x |
Текущий статус
PWA standalone режим РАБОТАЕТ в EU на всех актуальных версиях iOS.
Что НЕ работает в EU на iOS
| Функция | Статус в EU | Влияние на Baton |
|---|---|---|
| PWA standalone | ✅ Работает | Нет влияния |
| Push notifications | ❌ Не работает | Нет влияния (Baton не использует push в v1) |
| Background Sync | ❌ Не поддерживается Safari вообще | Нет влияния (v1 без offline queue) |
Workaround (на случай повторного удаления)
Если Apple снова уберёт standalone в EU:
-
manifest.json:
"display": "standalone"→"display": "browser"- Приложение откроется в Safari tab вместо fullscreen
- Функциональность сохраняется полностью
- UX деградирует: видна адресная строка
-
Telegram Web App (альтернатива):
- Baton как Telegram Mini App
- Не зависит от PWA standalone
- Ограничение: работает только внутри Telegram
-
Нативная обёртка (крайний случай):
- Capacitor/Cordova → IPA → TestFlight
- Для 300-400 пользователей — Enterprise distribution или Ad Hoc provisioning
- Overkill, только если Apple полностью заблокирует PWA
Рекомендация: не предпринимать действий сейчас. Мониторить iOS release notes.
Часть 3: iOS 7-дневная очистка кеша
Проблема
WebKit (Safari) очищает все website data (включая Service Worker registration, Cache API, localStorage, IndexedDB) после 7 дней непрерывного неиспользования домена.
Влияние на Baton
| Что теряется | Последствие | Серьёзность |
|---|---|---|
| SW registration | При следующем открытии — SW re-registers (нужна сеть) | Низкая |
| Cache API (precached assets) | Первое открытие после 7 дней — загрузка из сети | Низкая |
| localStorage (UUID) | Пользователь получает новый UUID → повторная регистрация | Средняя |
Митигация
- Подсказка при установке: "Открывайте приложение хотя бы раз в неделю для надёжной работы"
- UUID на сервере: при потере localStorage UUID — пользователь регистрируется заново. Это неидеально, но приемлемо (знаем имя, не знаем что это тот же человек).
- v2 mitigation: привязка Telegram user ID (через /start в боте) как persistent identifier, не зависящий от browser storage.
Часть 4: iOS Safari Private Mode (#1015)
Проблема
localStorage.setItem() бросает SecurityError в приватном режиме iOS Safari.
Решение (реализовано в ADR-003)
Цепочка fallback (#1025): localStorage → sessionStorage → in-memory
Проверка доступности через реальную запись (#1024), не typeof.
Поведение в private mode
- UUID генерируется в
sessionStorageилиin-memory - UUID живёт до закрытия вкладки (sessionStorage) или до перезагрузки (in-memory)
- При следующем открытии — новый UUID, новая регистрация
- Это ожидаемое поведение. Пользователь в приватном режиме осознанно выбирает непостоянство данных.
Последствия
- v1 offline = show error. Минимальная сложность, максимальная прозрачность для пользователя.
- iOS DMA = no action needed. Standalone работает. Workaround задокументирован.
- 7-day cache = acceptable risk. Для экстренного приложения пользователь скорее всего открывает его чаще 1 раза в неделю (хотя бы для проверки).
- Private mode = degraded but functional. UUID временный, но SOS работает.