Коли сповіщення приходять через WebSocket, інтерфейс виглядає живим: нова подія відразу збільшує лічильник, додає запис у список або відкриває потрібну реакцію.
З цього легко зробити небезпечний висновок: якщо подія не прийшла через WebSocket, значить стан не змінився.
Насправді WebSocket - це канал доставки оперативного сигналу, а не гарантія, що клієнт бачив кожну зміну стану.
Для непрочитаних сповіщень джерелом правди має залишатися серверний стан, який клієнт уміє повторно отримати.
Подія може бути пропущена навіть у справній системі
Пропущена подія не обов’язково означає помилку сервера або несправний WebSocket.
Клієнт міг тимчасово перестати слухати канал. Браузер міг приспати вкладку. Ноутбук міг перейти в режим сну. Мережа могла зникнути на кілька секунд. З’єднання могло відновитися вже після того, як подія була відправлена.
У кожному з цих випадків серверний стан правильний, але локальний стан у браузері вже відстає.
Лічильник у браузері - це похідний стан
Якщо сервер зберігає сповіщення та їхній стан прочитання, то число "5 непрочитаних" у браузері не є самостійним джерелом даних.
Це лише локальне представлення результату, який можна знову обчислити або отримати із сервера.
Саме тому локальний лічильник можна оперативно змінювати після події, але періодично його потрібно звіряти з сервером.
Server state: unread = 7 Browser state: unread = 6 Reason: one WebSocket event was missed Recovery: GET /notifications/unread-count -> unread = 7WebSocket добре підходить для оперативного оновлення
Відмовлятися від WebSocket через можливість пропущених подій не потрібно.
Його сильна сторона саме в тому, що інтерфейс не мусить постійно опитувати сервер. Коли з’являється нове сповіщення, клієнт може відразу оновити лічильник і список.
Проблема виникає лише тоді, коли цей канал стає єдиним способом дізнатися про зміну.
echo.private(`notification.user.${userId}`) .listen('.notification.created', () => { unreadCount.value += 1; refreshNotificationList(); });HTTP потрібен не як запасний WebSocket, а як відновлення стану
HTTP-запит тут виконує іншу роль. Він не намагається повторити кожну подію.
Він просто запитує: "який стан правильний зараз?".
Це важливе спрощення. Клієнту не потрібно знати, яку саме подію він пропустив. Достатньо повторно отримати поточну кількість непрочитаних сповіщень або актуальний список.
async function refreshUnreadCount() { const response = await api.get( '/notifications/unread-count' ); unreadCount.value = response.data.unread;}Коли варто повторно синхронізувати стан
Оновлювати стан після кожної секунди без потреби не потрібно. Набагато корисніше вибрати моменти, коли ризик розходження локального і серверного стану справді високий.
- після повернення вкладки у видимий стан;
- після події
pageshow; - після відновлення мережі;
- після повторного підключення WebSocket;
- після входу або відновлення сесії;
- після дій, які змінюють стан прочитання.
document.addEventListener( 'visibilitychange', () => { if ( document.visibilityState === 'visible' ) { refreshUnreadCount(); } }); window.addEventListener( 'pageshow', refreshUnreadCount); Відновлення після повторного підключення важливіше за сам факт з’єднання
Повторне підключення WebSocket означає лише, що канал знову доступний.
Воно не доводить, що клієнт отримав усі події, які сталися під час розриву.
Тому після відновлення з’єднання корисно не просто продовжити слухати нові події, а ще раз отримати поточний стан через HTTP.
Не потрібно відновлювати пропущену історію, якщо потрібен лише поточний стан
Для лічильника непрочитаних сповіщень часто неважливо, чи клієнт пропустив одну, дві або п’ять подій.
Йому важливо знати правильну кількість зараз.
Тому просте повторне читання серверного стану може бути надійнішим і значно простішим за спробу відтворити кожну пропущену подію.
Оперативність і достовірність не суперечать одна одній
WebSocket event -> швидке локальне оновлення visibilitychange / pageshow / reconnect / online -> HTTP refresh -> звірка з сервером -> відновлення правильного стануЦі два механізми не дублюють один одного.
WebSocket дає швидку реакцію на нову подію. HTTP повертає клієнт до правильного стану, якщо між ними виник розрив.
Так система отримує і швидкість, і можливість самостійно відновитися після тимчасової втрати зв’язку.
Цей принцип стосується не лише сповіщень
Така сама модель корисна для інших даних, які оперативно оновлюються через події:
- лічильники повідомлень;
- стан задач;
- статус замовлення;
- прогрес довгої операції;
- наявність користувача в мережі;
- інші похідні показники інтерфейсу.
Якщо подія лише повідомляє про зміну, клієнт повинен мати спосіб повторно отримати правильний стан без залежності від повної історії подій.
Що варто перевірити тестами
1. Нова подія відразу оновлює лічильник.2. Пропущена подія не залишає лічильник неправильним назавжди.3. Повернення у вкладку запускає звірку стану.4. pageshow запускає звірку стану.5. Відновлення мережі запускає звірку стану.6. Повторне підключення WebSocket запускає звірку стану.7. HTTP-відповідь замінює застарілий локальний лічильник.8. Повторне оновлення не створює дублікати сповіщень.WebSocket має прискорювати інтерфейс, а не визначати правду
Якщо правильність лічильника залежить від того, що браузер ніколи не пропустить жодної події, система крихка.
Надійніша модель інша: WebSocket швидко повідомляє про зміну, а серверний стан у будь-який момент можна повторно отримати через HTTP.
Тоді тимчасовий розрив зв’язку перестає бути незворотною втратою інформації для клієнта.
Це доповнює проблему, яку я розбирала в окремій статті про застарілий лічильник після повернення у вкладку, але тут межа ширша: справа не лише у поверненні до вкладки, а в самому розподілі відповідальності між оперативною подією та достовірним станом.
WebSocket повідомляє, що щось змінило. Серверний стан відповідає на запитання, що є правильним зараз.



