refactor: Web Push Phase 1 - Critical fixes for AbortError

✅ КРИТИЧЕСКИЕ ИСПРАВЛЕНИЯ:
1. Упростил _checkSubscription() - теперь ТОЛЬКО чтение
   - Убрана автоматическая синхронизация с сервером
   - Убрана автоматическая переподписка при ошибках
   - Метод не модифицирует состояние

2. Исправил subscribe() - убран принудительный unsubscribe
   - Браузер сам управляет существующими подписками
   - update_or_create на backend обновит запись
   - Избегаем конфликтов Push Service

3. Улучшен resetEverything() для recovery
   - Полная очистка: unsubscribe + unregister + localStorage
   - Задержка 2 сек для обработки Push Service
   - Подробное логирование каждого шага
   - Инструкции для пользователя

4. Добавлена детекция AbortError
   - Понятное сообщение в консоли
   - Инструкция по recovery
   - Ссылка на resetEverything()

�� Добавлен полный аудит:
- backend/docs/reports/WEB_PUSH_AUDIT_2026_01_06.md
- Детальный анализ всех компонентов
- План рефакторинга (4 фазы)
- Чек-листы для тестирования

Связано с: #AbortError #WebPush #VAPID
This commit is contained in:
Игорь Надеин 2026-01-06 08:26:50 +03:00
parent 4a139180f7
commit 836b7f052d
2 changed files with 1138 additions and 44 deletions

File diff suppressed because it is too large Load Diff

View File

@ -116,6 +116,7 @@ class PushNotificationsManager {
/**
* Проверяет текущий статус подписки
* ТОЛЬКО ЧТЕНИЕ - не модифицирует подписку
* @private
*/
async _checkSubscription() {
@ -123,40 +124,9 @@ class PushNotificationsManager {
this.isSubscribed = subscription !== null;
if (this.isSubscribed) {
// Проверяем, нужна ли синхронизация с сервером
const lastEndpoint = localStorage.getItem('push_endpoint');
const currentEndpoint = subscription.endpoint;
// Отправляем на сервер только если endpoint изменился или не сохранён
if (lastEndpoint !== currentEndpoint) {
try {
await this._sendSubscriptionToServer(subscription);
localStorage.setItem('push_endpoint', currentEndpoint);
} catch (error) {
console.warn('[PushNotifications] Ошибка синхронизации, переподписываемся:', error);
// Если ошибка - возможно VAPID ключи изменились
// Удаляем старую подписку и создаём новую
try {
await subscription.unsubscribe();
console.log('[PushNotifications] Старая подписка удалена');
// Создаём новую подписку с новыми VAPID ключами
const newSubscription = await this.swRegistration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: this._urlBase64ToUint8Array(this.vapidPublicKey)
});
await this._sendSubscriptionToServer(newSubscription);
localStorage.setItem('push_endpoint', newSubscription.endpoint);
console.log('[PushNotifications] ✅ Автоматически переподписались');
this.isSubscribed = true;
} catch (resubscribeError) {
console.error('[PushNotifications] Ошибка переподписки:', resubscribeError);
this.isSubscribed = false;
}
}
}
console.log('[PushNotifications] ✅ Активная подписка найдена');
} else {
console.log('[PushNotifications] ℹ️ Подписка отсутствует');
}
}
@ -219,28 +189,39 @@ class PushNotificationsManager {
}
}
// Проверяем, не подписаны ли уже
if (this.isSubscribed) {
console.log('[PushNotifications] ℹ️ Уже подписан, пропускаем');
return true;
}
try {
// Удаляем старую подписку, если есть
const existingSubscription = await this.swRegistration.pushManager.getSubscription();
if (existingSubscription) {
await existingSubscription.unsubscribe();
}
// Создаем новую подписку
// Создаем push-подписку (браузер сам управляет существующими подписками)
const subscription = await this.swRegistration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: this._urlBase64ToUint8Array(this.vapidPublicKey)
});
// Отправляем подписку на сервер
console.log('[PushNotifications] ✅ Push-подписка создана:', subscription.endpoint.substring(0, 50) + '...');
// Отправляем подписку на сервер (update_or_create)
await this._sendSubscriptionToServer(subscription);
this.isSubscribed = true;
console.log('[PushNotifications] ✅ Подписка успешна');
return true;
} catch (error) {
console.error('[PushNotifications] Ошибка подписки:', error);
console.error('[PushNotifications] ❌ Ошибка подписки:', error.name, error.message);
// Если AbortError - показываем инструкцию по recovery
if (error.name === 'AbortError') {
console.error(
'[PushNotifications] ⚠️ AbortError обнаружен!\n' +
'Это означает конфликт с предыдущей подпиской.\n' +
'Решение: вызовите window.pushNotifications.resetEverything() и обновите страницу.'
);
}
return false;
}
}
@ -370,6 +351,79 @@ class PushNotificationsManager {
}
}
/**
* 🔄 ПОЛНАЯ ОЧИСТКА Service Worker и подписок
*
* Используйте ТОЛЬКО для recovery после критических ошибок:
* - AbortError при подписке
* - Конфликт VAPID ключей
* - Некорректное состояние Service Worker
*
* После вызова ОБЯЗАТЕЛЬНО обновите страницу: location.reload()
*
* @returns {Promise<boolean>}
*/
async resetEverything() {
try {
console.log('[PushNotifications] 🔄 ПОЛНАЯ ОЧИСТКА начата...');
console.log('[PushNotifications] Это действие удалит Service Worker и все подписки');
// 1. Удаляем подписку из браузера
if (this.swRegistration) {
try {
const subscription = await this.swRegistration.pushManager.getSubscription();
if (subscription) {
await subscription.unsubscribe();
console.log('[PushNotifications] ✅ Подписка удалена из браузера');
}
} catch (e) {
console.warn('[PushNotifications] ⚠️ Не удалось удалить подписку:', e.message);
}
// 2. Удаляем Service Worker
try {
const unregistered = await this.swRegistration.unregister();
if (unregistered) {
console.log('[PushNotifications] ✅ Service Worker удален');
}
} catch (e) {
console.warn('[PushNotifications] ⚠️ Не удалось удалить Service Worker:', e.message);
}
}
// 3. Очищаем localStorage
localStorage.removeItem('push_endpoint');
console.log('[PushNotifications] ✅ localStorage очищен');
// 4. Ждем 2 секунды для обработки Push Service
console.log('[PushNotifications] ⏳ Ожидание 2 сек для обработки Push Service...');
await new Promise(resolve => setTimeout(resolve, 2000));
// 5. Перерегистрируем Service Worker
try {
await this._registerServiceWorker();
console.log('[PushNotifications] ✅ Service Worker перерегистрирован');
} catch (e) {
console.error('[PushNotifications] ❌ Не удалось перерегистрировать Service Worker:', e);
throw e;
}
this.isSubscribed = false;
this.swRegistration = await navigator.serviceWorker.getRegistration('/');
console.log('[PushNotifications] ✅ Полная очистка завершена!');
console.log('[PushNotifications] 📍 ОБНОВИТЕ СТРАНИЦУ (F5) и попробуйте подписаться снова');
return true;
} catch (error) {
console.error('[PushNotifications] ❌ Критическая ошибка при сбросе:', error);
console.error('[PushNotifications] Попробуйте очистить данные сайта вручную:');
console.error('[PushNotifications] DevTools → Application → Clear storage → Clear site data');
return false;
}
}
/**
* Возвращает текущий статус разрешения
* @returns {string} 'granted', 'denied', 'default', или 'unsupported'