573 lines
30 KiB
Markdown
573 lines
30 KiB
Markdown
# Tech Research Raw: Baton PWA
|
||
**Дата:** 2026-03-20
|
||
**Статус:** Полное исследование — все 6 требований покрыты
|
||
**Источники:** web.dev, MDN, caniuse.com, core.telegram.org, sqlite.org, caniuse.com
|
||
|
||
---
|
||
|
||
## ТРЕБОВАНИЕ 1: PWA на главный экран iOS/Android
|
||
|
||
### manifest.json — обязательные поля
|
||
|
||
Минимум для Android Chrome (Add to Home Screen / A2HS):
|
||
- `name` или `short_name` — строка, отображается под иконкой
|
||
- `start_url` — относительный путь к начальной странице
|
||
- `icons` — массив, минимум одна запись с размером 192×192
|
||
- `display` — одно из: `standalone`, `fullscreen`, `minimal-ui`
|
||
|
||
Без `display: standalone` — не устанавливается как PWA (остаётся закладкой).
|
||
|
||
Дополнительно рекомендованы:
|
||
- `background_color` — цвет сплэш-экрана при запуске
|
||
- `theme_color` — цвет браузерного хромирования
|
||
- `description` — для магазинов и SEO
|
||
|
||
### Иконки: обязательные размеры
|
||
|
||
**Android (Chrome, Samsung Internet):**
|
||
- `192×192` px PNG — минимум для установки
|
||
- `512×512` px PNG — для экрана загрузки (splash screen)
|
||
- Маскируемая иконка: `"purpose": "any maskable"` — без неё ОС обрезает иконку в круг
|
||
- Отдельный файл с отступом ~10% с каждой стороны
|
||
|
||
**iOS (Safari, Chrome на iOS):**
|
||
- manifest.json НЕ используется для иконки на главном экране iOS
|
||
- Нужен HTML-тег: `<link rel="apple-touch-icon" sizes="180x180" href="/icon-180.png">`
|
||
- Стандарт 2025: 180×180 px — покрывает все современные iPhone и iPad
|
||
- Без apple-touch-icon iOS берёт скриншот страницы как иконку
|
||
|
||
**Рекомендованный набор файлов (покрывает все платформы):**
|
||
- `icon-180.png` — iOS/Safari
|
||
- `icon-192.png` — Android/Chrome (manifest, обязателен)
|
||
- `icon-512.png` — Android/Chrome splash (manifest, обязателен)
|
||
- `icon-384.png` — дополнительно
|
||
- `icon-1024.png` — дополнительно для высокого разрешения
|
||
|
||
### HTTPS — подтверждение
|
||
|
||
PWA installability требует HTTPS — подтверждено MDN, web.dev, Apple Developer Docs. HTTP-страница не может быть установлена как PWA ни на iOS, ни на Android. Исключение: `localhost` для разработки.
|
||
|
||
### Ограничения iOS
|
||
|
||
**iOS 16.4+ (2023):**
|
||
- Push-уведомления для PWA: добавлены в iOS 16.4 через Web Push API
|
||
- Только если PWA **добавлена на главный экран** → только тогда можно запросить permission
|
||
- Нет тихих уведомлений (silent push) для PWA на iOS
|
||
- Только текст и иконки в уведомлениях (без rich media)
|
||
|
||
**iOS 17.4 (EU-регион):**
|
||
- Восстановлен в iOS 17.4.1+ после давления EU/OWA, но push notifications в EU всё ещё недоступны
|
||
|
||
**Хранилище на iOS:**
|
||
- Квота кэша: ~50 МБ
|
||
- Хранилище очищается автоматически через несколько недель при неиспользовании
|
||
- 7-дневный лимит на script-writable storage (IndexedDB, localStorage)
|
||
|
||
**Background execution на iOS:**
|
||
- Фоновое выполнение скриптов: не поддерживается
|
||
- Service Worker работает только когда PWA активна в foreground или при push-событии
|
||
|
||
### Как установить на iOS
|
||
|
||
- Пользователь открывает Safari → Share → «На экран Домой»
|
||
- Начиная с iOS 16.4 также работает из Chrome, Edge, Firefox на iOS
|
||
- Нет автоматического install prompt (как на Android) — только ручная установка
|
||
|
||
---
|
||
|
||
## ТРЕБОВАНИЕ 2: Офлайн через Service Worker + precache
|
||
|
||
### Background Sync API — реальная поддержка (2026)
|
||
|
||
Источник: caniuse.com, данные на март 2026:
|
||
|
||
**Глобальный охват: 78.75%**
|
||
|
||
Поддерживают:
|
||
- Chrome 49+ (десктоп и Android)
|
||
- Edge 79+
|
||
- Opera 42+
|
||
- Samsung Internet 5+
|
||
- UC Browser для Android 15.5+
|
||
- Android Browser 97+
|
||
|
||
**НЕ поддерживают:**
|
||
- Safari (все версии, десктоп и iOS)
|
||
- Firefox (все версии, включая Android)
|
||
- Opera Mini
|
||
- IE
|
||
|
||
Итого: ~21.25% пользователей без поддержки Background Sync API.
|
||
|
||
> Решение #1001 указывало ~15% без поддержки. Актуальные данные caniuse (март 2026): ~21% без поддержки. Разница значительна — ручной fallback обязателен.
|
||
|
||
### Workbox vs ручной Service Worker
|
||
|
||
**Workbox:**
|
||
- Используется на 54% мобильных сайтов (web.dev 2025)
|
||
- Модульная архитектура (import только нужное)
|
||
- Встроенные стратегии: CacheFirst, NetworkFirst, StaleWhileRevalidate
|
||
- Встроенный BackgroundSync модуль: автоматическая очередь + повтор
|
||
- Размер: базовый модуль ~6-10 KB gzip
|
||
- Плюсы: готовые паттерны, меньше ошибок, активная поддержка
|
||
- Минусы: зависимость, для 3-5 файлов «тяжеловато» (но размер приемлем)
|
||
|
||
**Ручной SW:**
|
||
- Полный контроль
|
||
- Для 3-5 файлов: ~30-50 строк кода
|
||
- Минусы: нужно вручную реализовать cache versioning, cleanup, retry логику
|
||
- Ошибки сложнее отловить (SW обновляется с задержкой)
|
||
|
||
**Для минимального приложения (5 файлов):**
|
||
- Ручной SW достаточен для кэширования статики
|
||
- Для BackgroundSync + outbox лучше Workbox (workbox-background-sync)
|
||
|
||
### Cache-first стратегия для статики
|
||
|
||
```javascript
|
||
// Ручной SW — cache-first для статических файлов
|
||
const CACHE_NAME = 'baton-v1';
|
||
const PRECACHE_URLS = ['/', '/index.html', '/app.js', '/style.css', '/manifest.json'];
|
||
|
||
self.addEventListener('install', event => {
|
||
event.waitUntil(
|
||
caches.open(CACHE_NAME).then(cache => cache.addAll(PRECACHE_URLS))
|
||
);
|
||
self.skipWaiting();
|
||
});
|
||
|
||
self.addEventListener('fetch', event => {
|
||
if (PRECACHE_URLS.some(url => event.request.url.endsWith(url))) {
|
||
event.respondWith(
|
||
caches.match(event.request).then(cached => cached || fetch(event.request))
|
||
);
|
||
}
|
||
});
|
||
```
|
||
|
||
### IndexedDB Outbox + Dual Triggers (решение #1006)
|
||
|
||
**Trigger 1: online event**
|
||
```javascript
|
||
// В main thread (app.js)
|
||
window.addEventListener('online', () => flushOutbox());
|
||
|
||
async function flushOutbox() {
|
||
const items = await getAllFromOutbox(); // читаем из IndexedDB
|
||
for (const item of items) {
|
||
try {
|
||
await fetch('/signal', { method: 'POST', body: JSON.stringify(item) });
|
||
await removeFromOutbox(item.id);
|
||
} catch (e) { /* останется в очереди */ }
|
||
}
|
||
}
|
||
```
|
||
|
||
**Trigger 2: Background Sync (SW)**
|
||
```javascript
|
||
// В service worker
|
||
self.addEventListener('sync', event => {
|
||
if (event.tag === 'flush-outbox') {
|
||
event.waitUntil(flushOutboxFromSW());
|
||
}
|
||
});
|
||
```
|
||
|
||
**Регистрация синка:**
|
||
```javascript
|
||
// Когда добавляем в outbox:
|
||
if ('serviceWorker' in navigator && 'sync' in registration) {
|
||
await registration.sync.register('flush-outbox');
|
||
} else {
|
||
// Fallback: пробуем сразу если online
|
||
if (navigator.onLine) flushOutbox();
|
||
}
|
||
```
|
||
|
||
**Edge cases:**
|
||
- SW lifecycle: при обновлении SW (новая версия) старый SW остаётся активным до закрытия всех вкладок
|
||
- Если SW обновляется во время flush — запрос может быть потерян → нужен idempotent ключ (timestamp + random)
|
||
- IndexedDB: доступна как из main thread, так и из SW → разделяемое хранилище
|
||
- iOS: Background Sync не работает → только Trigger 1 (online event) и ручная кнопка retry
|
||
|
||
### Ручной Fallback (обязателен для iOS и Firefox)
|
||
|
||
При отправке сигнала:
|
||
1. Попробовать fetch()
|
||
2. При ошибке → сохранить в IndexedDB
|
||
3. На каждый `window.addEventListener('online')` → попытка flush
|
||
4. Кнопка в UI «Повторить отправку» → явный flush
|
||
|
||
---
|
||
|
||
## ТРЕБОВАНИЕ 3: POST → Сервер → Telegram
|
||
|
||
### Telegram Bot API — sendMessage
|
||
|
||
**Эндпоинт:**
|
||
```
|
||
POST https://api.telegram.org/bot{TOKEN}/sendMessage
|
||
```
|
||
|
||
**Обязательные параметры:**
|
||
| Параметр | Тип | Описание |
|
||
|----------|-----|----------|
|
||
| `chat_id` | Integer или String | ID группы (отрицательное число) или @username |
|
||
| `text` | String | Текст сообщения, 1-4096 символов |
|
||
|
||
**Опциональные параметры:**
|
||
| Параметр | Тип | Описание |
|
||
|----------|-----|----------|
|
||
| `parse_mode` | String | `Markdown`, `MarkdownV2`, или `HTML` |
|
||
| `disable_notification` | Boolean | Без звукового уведомления |
|
||
| `reply_to_message_id` | Integer | Ответить на сообщение |
|
||
| `message_thread_id` | Integer | Тред в супергруппе |
|
||
|
||
**Пример payload:**
|
||
```json
|
||
{
|
||
"chat_id": -1001234567890,
|
||
"text": "🚨 Экстренный сигнал от пользователя abc123\nВремя: 2026-03-20T10:30:00Z\nГеолокация: 55.7558, 37.6173",
|
||
"parse_mode": "HTML"
|
||
}
|
||
|
||
```
|
||
|
||
**Ответ при успехе:**
|
||
```json
|
||
{
|
||
"ok": true,
|
||
"result": { "message_id": 42, "chat": {...}, "text": "..." }
|
||
}
|
||
```
|
||
|
||
**При ошибке:** `{ "ok": false, "error_code": 429, "description": "Too Many Requests", "parameters": { "retry_after": 30 } }`
|
||
|
||
### Rate Limits Telegram Bot API (официальные, 2026)
|
||
|
||
| Сценарий | Лимит |
|
||
|----------|-------|
|
||
| Один чат (любой) | 1 сообщение/секунду |
|
||
| Группа | 20 сообщений/минуту |
|
||
| Глобально (все чаты) | 30 сообщений/секунду |
|
||
| С платными broadcast | до 1000 сообщений/секунду |
|
||
|
||
**Критично для Baton:**
|
||
- 400 пользователей нажали кнопку одновременно = 400 `sendMessage` запросов
|
||
- В одну группу: лимит 20/минуту
|
||
- **Проблема:** 400 сообщений в одну группу за 1 минуту превышает лимит в 20 раз
|
||
- Решение: не отправлять по одному сообщению на пользователя — агрегировать сигналы
|
||
|
||
### setWebhook vs getUpdates — уточнение для Baton
|
||
|
||
**Ключевое:** Baton ОТПРАВЛЯЕТ сообщения в Telegram, не принимает. Поэтому:
|
||
- `setWebhook` — НЕ нужен. Webhook нужен только если бот принимает команды от пользователей.
|
||
- `getUpdates` — НЕ нужен. Polling нужен только для получения обновлений.
|
||
- Для отправки: просто `POST /sendMessage` с токеном бота в URL.
|
||
- Токен бота = Bearer-авторизация через URL: `api.telegram.org/bot{TOKEN}/sendMessage`
|
||
|
||
**Если в будущем понадобится принимать ответы** → setWebhook и getUpdates взаимоисключающие (решение #1009). Выбрать что-то одно.
|
||
|
||
### X-Telegram-Bot-Api-Secret-Token (решение #1010)
|
||
|
||
Актуально **только** если мы принимаем webhook-запросы от Telegram на наш сервер.
|
||
Для исходящих sendMessage — не применимо.
|
||
|
||
### HTTPS для эндпоинта (решение #1011)
|
||
|
||
Актуально только для webhook-эндпоинта (если бот принимает входящие). Для исходящих вызовов Bot API — Telegram API сам является HTTPS, TLS на стороне клиента.
|
||
|
||
---
|
||
|
||
## ТРЕБОВАНИЕ 4: Stateless авторизация UUID v4
|
||
|
||
### Паттерн реализации (решение #1013)
|
||
|
||
```javascript
|
||
// Инициализация при первом запуске
|
||
function getOrCreateUserId() {
|
||
let userId = localStorage.getItem('baton_user_id');
|
||
if (!userId) {
|
||
userId = crypto.randomUUID(); // UUID v4, встроен в браузер (ES2022+)
|
||
localStorage.setItem('baton_user_id', userId);
|
||
}
|
||
return userId;
|
||
}
|
||
|
||
// Отправка сигнала
|
||
async function sendSignal(geo) {
|
||
const userId = getOrCreateUserId();
|
||
return fetch('/signal', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ user_id: userId, geo, timestamp: Date.now() })
|
||
});
|
||
}
|
||
```
|
||
|
||
**`crypto.randomUUID()`:** Доступен в современных браузерах (Chrome 92+, Firefox 95+, Safari 15.4+). Требует HTTPS или localhost.
|
||
|
||
### UUID v4 — энтропия и безопасность
|
||
|
||
- UUID v4: 122 бита случайности (6 бит зарезервированы для версии/варианта)
|
||
- `crypto.randomUUID()` использует CSPRNG (криптографически безопасный ГПСЧ)
|
||
- Вероятность коллизии при 400 пользователях: практически нулевая
|
||
|
||
**Это НЕ секретный токен в классическом смысле, а постоянный идентификатор устройства/пользователя.** Безопасность: UUID не может быть угадан, но может быть украден через XSS.
|
||
|
||
### Риски localStorage
|
||
|
||
| Риск | Описание | Вероятность |
|
||
|------|----------|-------------|
|
||
| XSS-кража | Скрипт на странице читает `localStorage.getItem('baton_user_id')` | Средняя (если есть XSS) |
|
||
| Clear browsing data | Пользователь сбросил браузер → UUID потерян | Средняя |
|
||
| Приватный режим iOS Safari | `localStorage.setItem()` выбрасывает исключение (блокирует запись) | Высокая на iOS |
|
||
| Приватный режим Chrome/Firefox | Работает в сессии, очищается при закрытии вкладки | Средняя |
|
||
| Другое устройство | Новое устройство → новый UUID, идентификатор не переносится | Ожидаемо |
|
||
| Замена UUID | Пользователь вручную заменил значение в DevTools | Низкая (намеренное действие) |
|
||
|
||
**Ограничение приватного режима iOS Safari:**
|
||
- `localStorage` в приватном режиме Safari бросает `SecurityError` при попытке записи
|
||
- Нужен try/catch и fallback на `sessionStorage` или переменную в памяти (UUID не сохраняется между сессиями)
|
||
|
||
**Это — ЖЁСТКОЕ требование проекта** (решение #1003: не понижать до nice-to-have).
|
||
|
||
### Поведение при очистке хранилища
|
||
|
||
- `Clear browsing data` / «Очистить данные сайта» → UUID удаляется → следующий визит генерирует новый
|
||
- iOS: автоматическая очистка после нескольких недель неиспользования
|
||
|
||
---
|
||
|
||
## ТРЕБОВАНИЕ 5: 300-400 пользователей, разные страны
|
||
|
||
### SQLite WAL — анализ нагрузки
|
||
|
||
**WAL Mode характеристики:**
|
||
- Чтение: одновременные read-транзакции, без блокировок
|
||
- Запись: **один writer в любой момент времени** — все write-операции сериализуются
|
||
- `busy_timeout=5000`: ожидать до 5 секунд перед возвратом SQLITE_BUSY
|
||
- `synchronous=NORMAL`: батчинг fsync вместо вызова после каждой транзакции (вместе с WAL — обязательно, решение #1005)
|
||
|
||
**Расчёт worst case — 400 одновременных нажатий:**
|
||
- 400 POST /signal за 1 секунду
|
||
- Каждый запрос = 1 INSERT в БД
|
||
- SQLite WAL: серилизует все 400 INSERT — они встанут в очередь
|
||
- Каждый INSERT (простой, без joins): ~0.1-1 мс в WAL+NORMAL режиме
|
||
- 400 INSERT × 1 мс = ~400 мс до завершения последней транзакции
|
||
- С `busy_timeout=5000`: все 400 запросов получат ответ в течение 5 секунд (не сразу упадут с ошибкой)
|
||
- Telegram rate limit (20 сообщений/минуту в группу) — бутылочное горлышко раньше SQLite
|
||
|
||
**Бенчмарки SQLite WAL (из исследований):**
|
||
- 150,000 rows/second с 100 INSERT/транзакцию (full synchronous mode)
|
||
- 400 одиночных INSERT: ~0.4 секунды суммарно при busy_timeout=5000
|
||
|
||
**Вывод:** SQLite WAL с busy_timeout=5000 + synchronous=NORMAL справится с 400 одновременными записями без потери данных. Задержка ответа может вырасти до 500 мс для последних в очереди.
|
||
|
||
### Telegram Rate Limits при массовой отправке
|
||
|
||
| Сценарий | Лимит | При 400 юзерах |
|
||
|----------|-------|----------------|
|
||
| Сообщения в 1 группу | 20/минуту | 400 > 20 = превышение в 20 раз |
|
||
| Глобально | 30/секунду | 400 > 30 = нужна очередь |
|
||
|
||
**Критическая проблема:** отправка 400 отдельных сообщений в одну группу невозможна без throttling.
|
||
|
||
**Возможные решения (факты, не рекомендации):**
|
||
1. Агрегация: собирать сигналы за 1 минуту → одно сообщение «N сигналов получено»
|
||
2. Throttling: очередь отправки с задержкой 3 секунды между сообщениями
|
||
3. Несколько групп: распределить оповещения по разным чатам
|
||
|
||
### CDN / Геораспределение
|
||
|
||
Для 400 пользователей из разных стран:
|
||
- Статика (HTML/JS/CSS): CDN снизит latency для первого посещения
|
||
- API запросы: без CDN (CDN для API требует сложной настройки Edge Functions)
|
||
- Без CDN: для 400 юзеров — один сервер в центральной локации достаточен
|
||
- Latency без CDN: Европа→Европа ~20-50 мс, США→Европа ~100-150 мс, Азия→Европа ~200-300 мс
|
||
|
||
---
|
||
|
||
## ТРЕБОВАНИЕ 6: Геолокация (опциональна в v1)
|
||
|
||
### Geolocation API — базовые факты
|
||
|
||
**HTTPS:** Обязателен. Chrome 50+, Firefox 55+, Safari 10.1+ заблокировали Geolocation на HTTP. Исключение: `localhost`.
|
||
|
||
**User Permission:** Обязателен. Нет способа получить координаты без явного разрешения пользователя.
|
||
|
||
**API:**
|
||
```javascript
|
||
// Получить текущую позицию
|
||
navigator.geolocation.getCurrentPosition(
|
||
(pos) => {
|
||
const lat = pos.coords.latitude; // Float
|
||
const lon = pos.coords.longitude; // Float
|
||
const acc = pos.coords.accuracy; // метры
|
||
},
|
||
(err) => { /* PERMISSION_DENIED, POSITION_UNAVAILABLE, TIMEOUT */ },
|
||
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 }
|
||
);
|
||
```
|
||
|
||
### Точность по методу
|
||
|
||
| Метод | Точность | Время получения |
|
||
|-------|----------|-----------------|
|
||
| GPS (enableHighAccuracy: true) | 3-5 м (95% случаев) | 5-60 секунд (cold start) |
|
||
| WiFi positioning | 10-20 м | 1-3 секунды |
|
||
| Cell towers triangulation | 300-3000 м | 1-2 секунды |
|
||
| IP-based | 5-50 км | Мгновенно |
|
||
|
||
Браузер выбирает метод автоматически исходя из `enableHighAccuracy`. На мобильных: при `true` → GPS; при `false` → WiFi/Cell.
|
||
|
||
### Формат передачи в POST body
|
||
|
||
```json
|
||
{
|
||
"user_id": "550e8400-e29b-41d4-a716-446655440000",
|
||
"timestamp": 1742471400000,
|
||
"geo": {
|
||
"lat": 55.7558,
|
||
"lon": 37.6173,
|
||
"accuracy": 5.0
|
||
}
|
||
}
|
||
```
|
||
|
||
Когда геолокация недоступна или пользователь отказал: `"geo": null`
|
||
|
||
### Поддержка браузерами
|
||
|
||
Geolocation API: 98%+ глобальная поддержка (caniuse). Доступен во всех современных браузерах при HTTPS.
|
||
|
||
---
|
||
|
||
## СРАВНЕНИЕ БЭКЕНД-СТЕКОВ
|
||
|
||
### FastAPI (Python)
|
||
|
||
| Параметр | Значение |
|
||
|----------|----------|
|
||
| Runtime | Python 3.11+, требует установки |
|
||
| Deployment | venv/virtualenv или Docker образ ~200-400 MB |
|
||
| SQLite binding | `aiosqlite` (async) или `sqlite3` (sync через executor) |
|
||
| Async | asyncio, нативно |
|
||
| bcrypt | Нужен `run_in_executor` (#1004) |
|
||
| Знакомость команде | Да (используется в Kin) |
|
||
| Производительность (vs Go) | В 2-3x медленнее Go по RPS |
|
||
| SQLite + Python | sqlite3 (stdlib), aiosqlite для async |
|
||
|
||
### Express/Fastify (Node.js)
|
||
|
||
| Параметр | Значение |
|
||
|----------|----------|
|
||
| Runtime | Node.js 20+, требует установки |
|
||
| Deployment | node_modules + ~200-300 MB Docker |
|
||
| SQLite binding | `better-sqlite3` (sync, самый быстрый) или `node-sqlite3` |
|
||
| Async | Eventloop, I/O async |
|
||
| bcrypt | `bcryptjs` или `bcrypt` (нативный) — без проблем с eventloop |
|
||
| Единый язык с фронтом | Да (vanilla JS → Node.js) |
|
||
| Производительность | Fastify ~24% быстрее FastAPI по RPS в тестах |
|
||
|
||
**Примечание:** `better-sqlite3` работает синхронно, но это преимущество для SQLite (не блокирует eventloop нестандартно, быстрее async биндингов).
|
||
|
||
### Go (net/http)
|
||
|
||
| Параметр | Значение |
|
||
|----------|----------|
|
||
| Runtime | Нет. Компилируется в статический бинарь |
|
||
| Deployment | Один файл ~8-15 MB |
|
||
| SQLite binding | `modernc.org/sqlite` (CGO-free) или `mattn/go-sqlite3` (CGO) |
|
||
| Async | Горутины, нативный concurrency |
|
||
| bcrypt | `golang.org/x/crypto/bcrypt` — не блокирует горутины |
|
||
| Знакомость команде | Нет |
|
||
| Производительность | В 2-3x быстрее Python, ~2x быстрее Node.js |
|
||
| Cross-compile | `GOARCH=amd64 GOOS=linux go build` |
|
||
|
||
**modernc.org/sqlite (CGO-free):** В 10-100% медленнее нативного SQLite (CGO), но кросс-компиляция без C toolchain.
|
||
|
||
### Сравнительная таблица
|
||
|
||
| Критерий | FastAPI | Express/Fastify | Go |
|
||
|----------|---------|-----------------|-----|
|
||
| Знакомость | ✅ | ⚠️ (JS фронт) | ❌ |
|
||
| Размер деплоя | ~300 MB | ~200 MB | ~10 MB |
|
||
| Производительность | Базовая | +24% vs FastAPI | +200% vs FastAPI |
|
||
| SQLite-интеграция | Хорошая | Отличная (better-sqlite3) | Хорошая (modernc) |
|
||
| Сложность деплоя | Средняя | Средняя | Низкая (единый бинарь) |
|
||
| 400 конк. запросов | Справится | Справится | Справится |
|
||
|
||
---
|
||
|
||
## СРАВНЕНИЕ ОФЛАЙН-ПАТТЕРНОВ
|
||
|
||
### Вариант 1: IndexedDB outbox + Background Sync + manual fallback (решение #1006)
|
||
|
||
**Схема:**
|
||
1. Пользователь нажал кнопку → сохраняем в IndexedDB
|
||
2. Если online → немедленная попытка отправки
|
||
3. Если offline → регистрируем BackgroundSync tag
|
||
4. При появлении сети: SW получает `sync` event → flush очереди
|
||
5. Fallback: `window.addEventListener('online', flush)` + кнопка retry
|
||
|
||
**Плюсы:**
|
||
- Персистентность: IndexedDB не очищается при закрытии вкладки
|
||
- BackgroundSync: браузер сам управляет повтором (даже без открытой вкладки — в Chrome)
|
||
- Работает в SW контексте (shared между main thread и SW)
|
||
- Размер: IndexedDB не имеет лимита по умолчанию (квота браузера, обычно GB)
|
||
|
||
**Минусы:**
|
||
- IndexedDB API — громоздкий (нужна обёртка или `idb` библиотека ~1.9 KB)
|
||
- BackgroundSync: не работает в Safari/Firefox (~21% юзеров) → ручной fallback обязателен
|
||
- Сложнее отлаживать
|
||
|
||
### Вариант 2: localStorage queue + online event listener
|
||
|
||
**Схема:**
|
||
1. Сохраняем в `localStorage` как JSON-массив
|
||
2. `window.addEventListener('online', flush)` → при появлении сети отправляем всё
|
||
|
||
**Плюсы:**
|
||
- Простая синхронная реализация (~20 строк)
|
||
- Нет зависимостей
|
||
- Работает в iOS Safari (при HTTPS, в non-private mode)
|
||
|
||
**Минусы:**
|
||
- Лимит localStorage: 5 МБ (достаточно для outbox, но риск при больших данных)
|
||
- Недоступна в SW контексте — нет синхронизации между main thread и SW
|
||
- В iOS приватном режиме: бросает исключение при записи → нужен try/catch + memory fallback
|
||
- Не персистентна между сессиями в приватном режиме Chrome
|
||
|
||
### Вариант 3: Cache API + Request replay
|
||
|
||
**Схема:**
|
||
- Перехватываем failed запросы в SW → сохраняем в Cache API
|
||
- При появлении сети → повторяем запросы
|
||
|
||
**Плюсы:**
|
||
- Нативная интеграция с SW fetch event
|
||
- Не требует отдельного хранилища
|
||
|
||
**Минусы:**
|
||
- Cache API спроектирован для Response (кэш ответов), не для Request replay
|
||
- Нет гарантий персистентности очереди (Cache может быть очищен браузером)
|
||
- Workbox Background Sync использует IndexedDB внутри, не Cache API
|
||
- Нестандартный подход, мало документации для outbox паттерна
|
||
|
||
---
|
||
|
||
## ПОКРЫТИЕ ТРЕБОВАНИЙ — СВОДНАЯ ТАБЛИЦА
|
||
|
||
| # | Требование | Статус | Ключевые факты |
|
||
|---|-----------|--------|----------------|
|
||
| 1 | PWA на главный экран | RESEARCHED | manifest: name, start_url, icons (192+512), display:standalone. iOS: apple-touch-icon 180px. HTTPS обязателен. |
|
||
| 2 | Офлайн через SW | RESEARCHED | Background Sync: 78.75% охват (не 85% как в #1001). Ручной fallback обязателен для Safari/Firefox/iOS. |
|
||
| 3 | POST → Telegram | RESEARCHED | sendMessage: chat_id + text. Лимит группы: 20/мин. 400 одновременных > лимит. Webhook не нужен. |
|
||
| 4 | Stateless UUID auth | RESEARCHED | crypto.randomUUID(). iOS приватный режим: пишет исключение. Clear data = потеря UUID. Это ЖЁСТКОЕ требование (#1003). |
|
||
| 5 | 300-400 юзеров | RESEARCHED | SQLite WAL: справится. Telegram 20/мин в группу — критическое ограничение. |
|
||
| 6 | Геолокация (optional) | RESEARCHED | HTTPS + permission. GPS: 3-5м, 5-60с. WiFi: 10-20м, 1-3с. POST body: geo: {lat, lon, accuracy} или null. |
|
||
|
||
---
|
||
|
||
*Источники: [web.dev/learn/pwa](https://web.dev/learn/pwa/web-app-manifest), [MDN PWA](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Making_PWAs_installable), [caniuse Background Sync](https://caniuse.com/background-sync), [Telegram Bot FAQ](https://core.telegram.org/bots/faq), [SQLite WAL](https://sqlite.org/wal.html), [firt.dev iOS PWA](https://firt.dev/notes/pwa-ios/)*
|