baton/docs/adr/ADR-006-offline-ios-constraints.md
2026-03-20 21:12:43 +02:00

9.3 KiB
Raw Permalink Blame History

ADR-006: Офлайн-устойчивость v1 и ограничения iOS

Дата: 2026-03-20 Статус: Accepted Автор: Architect Agent (Kin pipeline, BATON-003) Решения: #1016, #1019, #1024, #1025


Контекст

Два связанных вопроса:

  1. Offline queue: если пользователь нажал SOS без сети — что происходит? Нужна ли очередь с повторной отправкой?
  2. 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-007 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-007).

Обоснование (offline)

  1. #1019: cache-first SW без offline queue достаточен для low-load PWA v1. Приложение открывается мгновенно (кешированные статические файлы). Если сети нет — пользователь видит ошибку и повторяет попытку когда сеть вернётся.

  2. Частота использования: ~1 раз/неделю. Вероятность "нажал SOS без сети и забыл повторить" — крайне низкая. Экстренная ситуация мотивирует повторную попытку.

  3. navigator.onLine проверка + UX:

    button.addEventListener('click', async () => {
      if (!navigator.onLine) {
        showError('Нет подключения. Проверьте сеть и попробуйте снова.');
        return;
      }
      try {
        await sendSignal();
        showSuccess();
      } catch (e) {
        showError('Ошибка отправки. Попробуйте снова.');
      }
    });
    

    Даже если navigator.onLine ненадёжен — try/catch ловит ошибку fetch.

  4. Вариант C (IndexedDB + BackgroundSync) — зарезервирован для v2. ADR-007 содержит полную спецификацию. Переход потребует ~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:

  1. manifest.json: "display": "standalone""display": "browser"

    • Приложение откроется в Safari tab вместо fullscreen
    • Функциональность сохраняется полностью
    • UX деградирует: видна адресная строка
  2. Telegram Web App (альтернатива):

    • Baton как Telegram Mini App
    • Не зависит от PWA standalone
    • Ограничение: работает только внутри Telegram
  3. Нативная обёртка (крайний случай):

    • 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 → повторная регистрация Средняя

Митигация

  1. Подсказка при установке: "Открывайте приложение хотя бы раз в неделю для надёжной работы"
  2. UUID на сервере: при потере localStorage UUID — пользователь регистрируется заново. Это неидеально, но приемлемо (знаем имя, не знаем что это тот же человек).
  3. 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, новая регистрация
  • Это ожидаемое поведение. Пользователь в приватном режиме осознанно выбирает непостоянство данных.

Последствия

  1. v1 offline = show error. Минимальная сложность, максимальная прозрачность для пользователя.
  2. iOS DMA = no action needed. Standalone работает. Workaround задокументирован.
  3. 7-day cache = acceptable risk. Для экстренного приложения пользователь скорее всего открывает его чаще 1 раза в неделю (хотя бы для проверки).
  4. Private mode = degraded but functional. UUID временный, но SOS работает.