Skip to content

πŸ“‘ withLongPolling ​

ΠŸΡ€Π°ΠΊΡ‚ΠΈΡ‡Π΅ΡΠΊΠΈΠ΅ кСйсы примСнСния Π΄Π΅ΠΊΠΎΡ€Π°Ρ‚ΠΎΡ€Π° withLongPolling (АрхитСктура v2: Event-Driven)

ΠžΠ±Π½ΠΎΠ²Π»Π΅Π½Π½Ρ‹ΠΉ Π΄Π΅ΠΊΠΎΡ€Π°Ρ‚ΠΎΡ€ withLongPolling β€” это Π²Ρ‹ΡΠΎΠΊΠΎΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹ΠΉ инструмСнт для создания инфраструктуры ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠΉ Π² Ρ€Π΅Π°Π»ΡŒΠ½ΠΎΠΌ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ (Real-Time Updates) Π½Π° Π±Π°Π·Π΅ стандартного HTTP-ΠΏΡ€ΠΎΡ‚ΠΎΠΊΠΎΠ»Π°. Π’ Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Π΅ v2 Π΄Π΅ΠΊΠΎΡ€Π°Ρ‚ΠΎΡ€ ΠΏΠΎΠ»Π½ΠΎΡΡ‚ΡŒΡŽ ΠΈΠ·Π±Π°Π²Π»Π΅Π½ ΠΎΡ‚ скрытых Ρ„ΠΎΠ½ΠΎΠ²Ρ‹Ρ… Ρ†ΠΈΠΊΠ»ΠΎΠ² while ΠΈ рСкурсивных Ρ‚Π°ΠΉΠΌΠ°ΡƒΡ‚ΠΎΠ², ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π»ΠΎΠΌΠ°Π»ΠΈ ΠΏΠ»Π°Π½ΠΈΡ€ΠΎΠ²Ρ‰ΠΈΠΊ стСйт-ΠΌΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ€Π°.

Π’Π΅ΠΏΠ΅Ρ€ΡŒ ΠΎΠ½ Ρ€Π°Π±ΠΎΡ‚Π°Π΅Ρ‚ ΠΊΠ°ΠΊ чистый ΠΊΠΎΠΌΠ°Π½Π΄Π½Ρ‹ΠΉ Ρ‚Π°ΠΊΡ‚Π΅Ρ€: выполняСт Ρ€ΠΎΠ²Π½ΠΎ ΠΎΠ΄Π½Ρƒ Β«Π΄Π»ΠΈΠ½Π½ΡƒΡŽΒ» ΡΠ΅Ρ‚Π΅Π²ΡƒΡŽ Ρ‚Ρ€Π°Π½Π·Π°ΠΊΡ†ΠΈΡŽ, Π° Π·Π°Ρ‚Π΅ΠΌ Π΄Π΅ΠΊΠ»Π°Ρ€Π°Ρ‚ΠΈΠ²Π½ΠΎ просит бизнСс-сСрвис ΡΠ΄Π²ΠΈΠ½ΡƒΡ‚ΡŒ Ρ€Π΅Π°ΠΊΡ‚ΠΈΠ²Π½Ρ‹ΠΉ сигнал pollingTick Π²ΠΏΠ΅Ρ€Π΅Π΄. ИзмСнСниС сигнала ΠΈΠ½Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠ΅Ρ‚ кэш рСсурса engine.resource, заставляя ядро Π΄Π²ΠΈΠΆΠΊΠ° Π°Π±ΡΠΎΠ»ΡŽΡ‚Π½ΠΎ Π»Π΅Π³ΠΈΡ‚ΠΈΠΌΠ½ΠΎ ΠΈ Π½Π°Ρ‚ΠΈΠ²Π½ΠΎ ΠΎΡ‚ΠΊΡ€Ρ‹Π²Π°Ρ‚ΡŒ ΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π΅Π΅ HTTP-соСдинСниС.

АрхитСктурныС кСйсы примСнСния ​

1. БистСмы ΠΌΠ³Π½ΠΎΠ²Π΅Π½Π½Ρ‹Ρ… push-Π½ΠΎΡ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΉ (Live Notifications / Toasts) ​

  • ΠŸΡ€ΠΎΠ±Π»Π΅ΠΌΠ° Π±Π΅Π· Π΄Π΅ΠΊΠΎΡ€Π°Ρ‚ΠΎΡ€Π°: Нам Π½ΡƒΠΆΠ½ΠΎ Π²Ρ‹Π²ΠΎΠ΄ΠΈΡ‚ΡŒ Π²ΡΠΏΠ»Ρ‹Π²Π°ΡŽΡ‰ΠΈΠ΅ увСдомлСния прямо Π² процСссС Ρ€Π°Π±ΠΎΡ‚Ρ‹ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ с систСмой. ΠšΠΎΡ€ΠΎΡ‚ΠΊΠΈΠΉ ΠΏΠΎΠ»Π»ΠΈΠ½Π³ (Short Polling) Ρ€Π°Π· Π² 2 сСкунды Π³Π΅Π½Π΅Ρ€ΠΈΡ€ΡƒΠ΅Ρ‚ ΠΊΠΎΠ»ΠΎΡΡΠ°Π»ΡŒΠ½Ρ‹ΠΉ холостой Ρ‚Ρ€Π°Ρ„ΠΈΠΊ ΠΈ Π·Π°Π±ΠΈΠ²Π°Π΅Ρ‚ Π»ΠΎΠ³ΠΈ Π²Π΅Π±-сСрвСра. Π Π°Π·Π²Π΅Ρ€Ρ‚Ρ‹Π²Π°Π½ΠΈΠ΅ WebSockets β€” ΠΈΠ·Π±Ρ‹Ρ‚ΠΎΡ‡Π½ΠΎ ΠΈ Ρ‚Ρ€Π΅Π±ΡƒΠ΅Ρ‚ настройки проксирования ΠΈ услоТнСния Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·Π°Ρ†ΠΈΠΈ Π½Π° бэкСндС.
  • РСшСниС с Long Polling v2: РСсурс связываСтся с Ρ€Π΅Π°ΠΊΡ‚ΠΈΠ²Π½Ρ‹ΠΌ ΠΊΠΎΡ€Ρ‚Π΅ΠΆΠ΅ΠΌ зависимостСй [isLoopActive, pollingTick]. ΠšΠ»ΠΈΠ΅Π½Ρ‚ ΠΎΡ‚ΠΊΡ€Ρ‹Π²Π°Π΅Ρ‚ ΠΎΠ΄Π½ΠΎ HTTP-соСдинСниС. Π‘Π΅Ρ€Π²Π΅Ρ€ Π΄Π΅Ρ€ΠΆΠΈΡ‚ Π΅Π³ΠΎ ΠΈ ΠΌΠΎΠ»Ρ‡ΠΈΡ‚. Как Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π² Π±Π°Π·Π΅ Π΄Π°Π½Π½Ρ‹Ρ… появляСтся систСмноС событиС, сСрвСр ΠΌΠ³Π½ΠΎΠ²Π΅Π½Π½ΠΎ Π·Π°Π²Π΅Ρ€ΡˆΠ°Π΅Ρ‚ запрос. Π”Π΅ΠΊΠΎΡ€Π°Ρ‚ΠΎΡ€ ΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°Π΅Ρ‚ ΠΏΠ°ΠΊΠ΅Ρ‚, Π° Π΅Π³ΠΎ встроСнная ΠΌΠΈΠΊΡ€ΠΎ-ΠΏΠ°ΡƒΠ·Π° (delay: 500) Π΄Π°Π΅Ρ‚ слою отобраТСния React врСмя Π½Π° ΠΏΠ»Π°Π²Π½ΡƒΡŽ Π°Π½ΠΈΠΌΠ°Ρ†ΠΈΡŽ отрисовки Toast-ΠΊΠ°Ρ€Ρ‚ΠΎΡ‡ΠΊΠΈ, послС Ρ‡Π΅Π³ΠΎ Π»Π΅Π³ΠΈΡ‚ΠΈΠΌΠ½ΠΎ ΠΏΡ€ΠΎΠ΄Π²ΠΈΠ³Π°Π΅Ρ‚ pollingTick для открытия ΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π΅Π³ΠΎ чистого ΠΊΠ°Π½Π°Π»Π° связи.

2. ΠžΡ„Π»Π°ΠΉΠ½-буфСризация событий ΠΈ Offline-First Ρ‡Π°Ρ‚Ρ‹ (ΠœΠ΅ΡΡΠ΅Π½Π΄ΠΆΠ΅Ρ€Ρ‹ / Π§Π°Ρ‚-ΠΊΠΎΠΌΠ½Π°Ρ‚Ρ‹) ​

  • ΠŸΡ€ΠΎΠ±Π»Π΅ΠΌΠ°: Π’Π½ΡƒΡ‚Ρ€Π΅Π½Π½ΠΈΠ΅ CRM-систСмы ΠΈΠ»ΠΈ Ρ‡Π°Ρ‚Ρ‹ ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠΈ Ρ‚Ρ€Π΅Π±ΡƒΡŽΡ‚ ΠΌΠ³Π½ΠΎΠ²Π΅Π½Π½ΠΎΠΉ синхронизации сообщСний. ΠŸΡ€ΠΈ этом ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ ΠΌΠΎΠΆΠ΅Ρ‚ Π·Π°ΠΉΡ‚ΠΈ Π² Π»ΠΈΡ„Ρ‚ ΠΈΠ»ΠΈ Ρ†ΠΎΠΊΠΎΠ»ΡŒΠ½Ρ‹ΠΉ этаТ, Π³Π΄Π΅ Π²Ρ€Π΅ΠΌΠ΅Π½Π½ΠΎ ΠΏΡ€ΠΎΠΏΠ°Π΄Π΅Ρ‚ ΠΌΠΎΠ±ΠΈΠ»ΡŒΠ½Ρ‹ΠΉ ΠΈΠ½Ρ‚Π΅Ρ€Π½Π΅Ρ‚. Если интСрфСйс Π² этот ΠΌΠΎΠΌΠ΅Π½Ρ‚ Π·Π°Π±Π»ΠΎΠΊΠΈΡ€ΡƒΠ΅Ρ‚ ΠΊΠ½ΠΎΠΏΠΊΠΈ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΈ сообщСний, это сломаСт UX.
  • РСшСниС с Long Polling v2: Кнопки Π³Π΅Π½Π΅Ρ€Π°Ρ†ΠΈΠΈ сообщСний ΠΎΡΡ‚Π°ΡŽΡ‚ΡΡ Π°ΠΊΡ‚ΠΈΠ²Π½Ρ‹ΠΌΠΈ всСгда. Π’ Ρ€Π΅ΠΆΠΈΠΌΠ΅ ΠΎΡ„Ρ„Π»Π°ΠΉΠ½Π° всС ΠΊΠ»ΠΈΠΊΠΈ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ бСспрСпятствСнно Π½Π°ΠΊΠ°ΠΏΠ»ΠΈΠ²Π°ΡŽΡ‚ΡΡ Π² Ρ€Π΅Π°ΠΊΡ‚ΠΈΠ²Π½ΠΎΠΌ сигналС Π»Π΅Π²ΠΎΠΉ ΠΊΠΎΠ»ΠΎΠ½ΠΊΠΈ (outgoingBuffer). Π”Π΅ΠΊΠΎΡ€Π°Ρ‚ΠΎΡ€ v2 содСрТит ΠΌΠ΅Ρ…Π°Π½ΠΈΠ·ΠΌ Π’ΠΎΠΊΠ΅Π½ΠΈΠ·Π°Ρ†ΠΈΠΈ сСссий ошибок. Когда ΡΠ΅Ρ‚ΡŒ восстанавливаСтся, Π°Π»Π³ΠΎΡ€ΠΈΡ‚ΠΌ Exponential Backoff ΠΏΠ»Π°Π²Π½ΠΎ Π²Ρ‹Π²ΠΎΠ΄ΠΈΡ‚ рСсурс ΠΈΠ· ΠΎΡ„Ρ„Π»Π°ΠΉΠ½Π°, Π΄Π΅ΠΊΠΎΡ€Π°Ρ‚ΠΎΡ€ сТигаСт всС старыС зависшиС Ρ‚Π°ΠΉΠΌΠ°ΡƒΡ‚Ρ‹ ошибок Π² Ρ„ΠΎΠ½Π΅ ΠΈ ΠΏΠ°ΠΊΠ΅Ρ‚Π½ΠΎ (ΠΎΠ΄Π½ΠΈΠΌ ΠΌΠ°Ρ…ΠΎΠΌ) Π²Ρ‹Π³Ρ€ΡƒΠΆΠ°Π΅Ρ‚ вСсь накопившийся ΠΎΡ€Π°Π½ΠΆΠ΅Π²Ρ‹ΠΉ Π±ΡƒΡ„Π΅Ρ€ Π½Π° сСрвСр Π·Π° ΠΎΠ΄Π½Ρƒ Ρ‚Ρ€Π°Π½Π·Π°ΠΊΡ†ΠΈΡŽ, ΠΌΠ³Π½ΠΎΠ²Π΅Π½Π½ΠΎ пСрСнося элСмСнты Π² ΠΏΡ€Π°Π²ΡƒΡŽ Π·Π΅Π»Ρ‘Π½ΡƒΡŽ ΠΊΠΎΠ»ΠΎΠ½ΠΊΡƒ ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎΠΉ истории.

3. ΠœΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³ статуса тяТСлых Ρ„ΠΎΠ½ΠΎΠ²Ρ‹Ρ… Π·Π°Π΄Π°Ρ‡ (Background Jobs / Export) ​

  • ΠŸΡ€ΠΎΠ±Π»Π΅ΠΌΠ°: ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ Π½Π°ΠΆΠΈΠΌΠ°Π΅Ρ‚ ΠΊΠ½ΠΎΠΏΠΊΡƒ Β«Π‘Π³Π΅Π½Π΅Ρ€ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ тяТСлый Excel-ΠΎΡ‚Ρ‡Π΅Ρ‚ Π·Π° 3 Π³ΠΎΠ΄Π°Β». Π‘Π΅Ρ€Π²Π΅Ρ€ ΠΎΠ±Ρ€Π°Π±Π°Ρ‚Ρ‹Π²Π°Π΅Ρ‚ эту Π·Π°Π΄Π°Ρ‡Ρƒ Π² Ρ„ΠΎΠ½ΠΎΠ²ΠΎΠΉ ΠΎΡ‡Π΅Ρ€Π΅Π΄ΠΈ (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, Ρ‡Π΅Ρ€Π΅Π· Celery/BullMQ) Π² Ρ‚Π΅Ρ‡Π΅Π½ΠΈΠ΅ 10–40 сСкунд. Π—Π°ΡΡ‚Π°Π²Π»ΡΡ‚ΡŒ ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π° ΠΆΠ΄Π°Ρ‚ΡŒ ΠΎΠ΄ΠΈΠ½ бСсконСчный синхронный HTTP-запрос нСльзя β€” соСдинСниС Π³Π°Ρ€Π°Π½Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½Π½ΠΎ отвалится ΠΏΠΎ Ρ‚Π°ΠΉΠΌΠ°ΡƒΡ‚Ρƒ Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π° ΠΈΠ»ΠΈ Cloudflare Gateway Timeout ($504$).
  • РСшСниС с Long Polling v2: ΠŸΡ€ΠΈ ΠΊΠ»ΠΈΠΊΠ΅ сСрвСр ΠΌΠ³Π½ΠΎΠ²Π΅Π½Π½ΠΎ Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ jobId Π·Π°Π΄Π°Ρ‡ΠΈ, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ записываСтся Π² Ρ€Π΅Π°ΠΊΡ‚ΠΈΠ²Π½Ρ‹ΠΉ ΠΊΠΎΡ€Ρ‚Π΅ΠΆ зависимостСй рСсурса. РСсурс, ΠΎΠ±Π΅Ρ€Π½ΡƒΡ‚Ρ‹ΠΉ Π² ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½Π½Ρ‹ΠΉ withLongPolling, ΠΎΠΏΡ€Π°ΡˆΠΈΠ²Π°Π΅Ρ‚ статус этой ΠΊΠΎΠ½ΠΊΡ€Π΅Ρ‚Π½ΠΎΠΉ Π·Π°Π΄Π°Ρ‡ΠΈ. Π‘ΠΎΠ΅Π΄ΠΈΠ½Π΅Π½ΠΈΠ΅ висит Π½Π° сСрвСрС ΠΈ закрываСтся Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Ρ‚ΠΎΠ³Π΄Π°, ΠΊΠΎΠ³Π΄Π° статус Π² бэкСндС смСнился Π½Π° completed. ΠŸΡ€ΠΈΠΌΠ΅Π½Π΅Π½ΠΈΠ΅ Π΄Π΅ΠΊΠ»Π°Ρ€Π°Ρ‚ΠΈΠ²Π½ΠΎΠ³ΠΎ поля responseValidate Π³Π°Ρ€Π°Π½Ρ‚ΠΈΡ€ΡƒΠ΅Ρ‚, Ρ‡Ρ‚ΠΎ структура ΠΎΡ‚Π²Π΅Ρ‚Π° провалидируСтся Π½Π° ΡƒΡ€ΠΎΠ²Π½Π΅ ядра, Π° Π² UI отобразится ΠΆΠΈΠ²ΠΎΠΉ прогрСсс-Π±Π°Ρ€.

4. Π€ΠΈΠ΄Ρ‹ Π΄Π°Π½Π½Ρ‹Ρ… ΠΈ ΠΆΠΈΠ²Ρ‹Π΅ ΠΊΠΎΡ‚ΠΈΡ€ΠΎΠ²ΠΊΠΈ (Live Dashboards / Sports Tracker) ​

  • ΠŸΡ€ΠΎΠ±Π»Π΅ΠΌΠ°: ΠžΡ‚ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ динамичСски ΠΌΠ΅Π½ΡΡŽΡ‰ΠΈΡ…ΡΡ ΠΏΠ°Π½Π΅Π»Π΅ΠΉ (Π°ΠΊΡ‚ΡƒΠ°Π»ΡŒΠ½Ρ‹ΠΉ счСт Ρ„ΡƒΡ‚Π±ΠΎΠ»ΡŒΠ½ΠΎΠ³ΠΎ ΠΌΠ°Ρ‚Ρ‡Π° ΠΈΠ»ΠΈ ΠΊΠΎΡ‚ΠΈΡ€ΠΎΠ²ΠΊΠΈ Π²Π°Π»ΡŽΡ‚). Π”Π°Π½Π½Ρ‹Π΅ ΠΌΠΎΠ³ΡƒΡ‚ ΠΎΠ±Π½ΠΎΠ²Π»ΡΡ‚ΡŒΡΡ Π½Π΅Ρ€Π°Π²Π½ΠΎΠΌΠ΅Ρ€Π½ΠΎ: Ρ‚ΠΎ 5 Ρ€Π°Π· Π·Π° сСкунду, Ρ‚ΠΎ ΠΌΠΎΠ»Ρ‡Π°Ρ‚ΡŒ 10 ΠΌΠΈΠ½ΡƒΡ‚.
  • РСшСниС с Long Polling v2: ΠŸΠΎΠ·Π²ΠΎΠ»ΡΠ΅Ρ‚ Π½Π΅ Π³ΠΎΠ½ΡΡ‚ΡŒ пустыС Ρ‚Π°ΠΊΡ‚Ρ‹ часов. Если ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ Π½Π΅Ρ‚, соСдинСниС удСрТиваСтся. Новая Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Π° Π΄Π΅ΠΊΠΎΡ€Π°Ρ‚ΠΎΡ€Π° v2, благодаря тСсной ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΈ с CountdownService Ρ‡Π΅Ρ€Π΅Π· Dependency Injection, позволяСт ΠΏΠ»Π°Π²Π½ΠΎ Ρ€Π°Π·Π΄Π²ΠΈΠ³Π°Ρ‚ΡŒ ΠΎΠΊΠ½Π° Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²ΠΎΠΊ ΠΏΡ€ΠΈ ΠΌΠΈΠΊΡ€ΠΎ-сбоях инфраструктуры, информируя ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Π² статус-Π±Π°Ρ€Π΅ ΠΎ Ρ‚ΠΎΡ‡Π½ΠΎΠΉ сСкундС ΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π΅ΠΉ автоматичСской ΠΏΠΎΠΏΡ‹Ρ‚ΠΊΠΈ опроса.

Бводная ΡˆΠΏΠ°Ρ€Π³Π°Π»ΠΊΠ° ΠΏΠΎ повСдСнию Π² экосистСмС Π³Ρ€Π°Ρ„Π° сигналов: ​

  • Π˜Π·ΠΎΠ»ΡΡ†ΠΈΡ ΠΏΠΎΠ±ΠΎΡ‡Π½Ρ‹Ρ… эффСктов: БизнСс-валидация структуры ΠΎΡ‚Π²Π΅Ρ‚Π° ΠΏΠΎΠ»Π½ΠΎΡΡ‚ΡŒΡŽ инкапсулирована Π² ΠΏΡ€Π΅Π΄ΠΈΠΊΠ°Ρ‚ responseValidate. Π‘Π°ΠΉΠ΄-эффСкт наполнСния истории вынСсСн Π² чистый engine.effect. Π­Ρ‚ΠΎ Π³Π°Ρ€Π°Π½Ρ‚ΠΈΡ€ΡƒΠ΅Ρ‚ ΡΡ‚Π°Π±ΠΈΠ»ΡŒΠ½ΠΎΡΡ‚ΡŒ Π±Π°Π΄ΠΆΠ΅ΠΉ ΠΈ числовых счСтчиков ΠΊΠΎΠ»ΠΎΠ½ΠΎΠΊ Π΄Π°ΡˆΠ±ΠΎΡ€Π΄Π° β€” ΠΎΠ½ΠΈ ΠΌΠ΅Π½ΡΡŽΡ‚ΡΡ Π°Ρ‚ΠΎΠΌΠ°Ρ€Π½ΠΎ ΠΈ Π±Π΅Π· Race Conditions.
  • Π˜ΡΡ‚ΠΈΠ½Π½Ρ‹ΠΉ Π»Π΅Π½ΠΈΠ²Ρ‹ΠΉ сон ΠΈ Π·Π°Ρ‰ΠΈΡ‚Π° ΠΎΡ‚ дублирования: Пока Ρ€ΡƒΠ±ΠΈΠ»ΡŒΠ½ΠΈΠΊ isLoopActive Ρ€Π°Π²Π΅Π½ false, ΠΌΠ΅Ρ‚ΠΎΠ΄ validateBeforeFetch Π·Π°ΠΊΡ€Ρ‹Π²Π°Π΅Ρ‚ заслонку. РСсурс выбрасываСт ΠΊΠΎΠ½Ρ‚Ρ€ΠΎΠ»ΠΈΡ€ΡƒΠ΅ΠΌΡƒΡŽ ΠΎΡˆΠΈΠ±ΠΊΡƒ ΠΎΡ‚ΠΌΠ΅Π½Ρ‹ ΠΈ бСзопасно спит, ΠΏΠΎΠ»Π½ΠΎΡΡ‚ΡŒΡŽ защищая ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ ΠΎΡ‚ ΠΏΠ°Ρ€Π°Π·ΠΈΡ‚Π½ΠΎΠ³ΠΎ мСрцания сСти (запросы большС Π½Π΅ ΡƒΡ‚Ρ€Π°ΠΈΠ²Π°ΡŽΡ‚ΡΡ ΠΏΡ€ΠΈ ΠΏΠ΅Ρ€Π²ΠΎΠΌ ΠΌΠΎΠ½Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠΈ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π°).
  • ЖСсткий Ρ€ΡƒΠ±ΠΈΠ»ΡŒΠ½ΠΈΠΊ (Destroy): НаТатиС ΠΊΠ½ΠΎΠΏΠΊΠΈ Destroy ΠΏΠ΅Ρ€Π΅ΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚ сСмафор isLoopActive Π² false. ВычисляСмый ΠΊΠΎΡ€Ρ‚Π΅ΠΆ ΠΌΠ³Π½ΠΎΠ²Π΅Π½Π½ΠΎ ΠΌΡƒΡ‚ΠΈΡ€ΡƒΠ΅Ρ‚, ядро Π΄Π²ΠΈΠΆΠΊΠ° автоматичСски Π°ΠΊΡ‚ΠΈΠ²ΠΈΡ€ΡƒΠ΅Ρ‚ Π½Π°Ρ‚ΠΈΠ²Π½Ρ‹ΠΉ abortSignal, обрывая Ρ‚Π΅ΠΊΡƒΡ‰ΠΈΠΉ висящий Π² сСти fetch(), Π° токСнизация сСссий Π² withLongPolling Π½Π°ΠΌΠ΅Ρ€Ρ‚Π²ΠΎ Π²Ρ‹ΠΆΠΈΠ³Π°Π΅Ρ‚ Ρ„ΠΎΠ½ΠΎΠ²Ρ‹Π΅ ΠΌΠ°ΠΊΡ€ΠΎΠ·Π°Π΄Π°Ρ‡ΠΈ setTimeout ΠΈΠ· ΠΎΠΏΠ΅Ρ€Π°Ρ‚ΠΈΠ²Π½ΠΎΠΉ памяти Π·Π° 0 миллисСкунд.

ΠŸΡ€ΠΈΠΌΠ΅Ρ€ 215: ΠžΡ‚ΠΏΡ€Π°Π²ΠΊΠ° Π»ΠΎΠ³ΠΎΠ² ΠΏΠΎ дСйствиям ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Π² интСрфСйсС ​

https://github.com/garage-13/reactive-engine/tree/main/examples/215-resource-withLongPolling

Основная бизнСс-логика:

ts
import { AbstractService, withLongPolling } from '@pravosleva/reactive-engine'
import { CountdownService } from './service.CountdownService'

export interface NotificationItem {
  id: string
  text: string
  timestamp: string
  type: 'like' | 'message'
}

interface ServerResponse {
  ok: boolean
  notifications: NotificationItem[]
}

// Запуск триггСрится строго ΠΊΠ°Π½ΠΎΠ½ΠΈΡ‡Π½ΠΎ: ΠΏΠΎ ΡˆΠ°Π³Ρƒ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ ΠΈΠ»ΠΈ фокусу Π²ΠΊΠ»Π°Π΄ΠΊΠΈ!
type TPollingDeps = [isLoopActive: boolean, pollingTick: number, isTabActive: boolean];

export class LiveNotificationsLogic extends AbstractService {
  public countdown = this.engine.inject(CountdownService)

  // Π˜ΡΡ‚ΠΎΡ‡Π½ΠΈΠΊΠΈ ΠΏΡ€Π°Π²Π΄Ρ‹ для UI ΠΈ Π³Ρ€Π°Ρ„Π°
  public userIdSignal = this.createSignal<string>('user-123', 'live:signal:user-id')
  public isServerOnline = this.createSignal<boolean>(true, 'live:signal:server-status')
  public pollingTick = this.createSignal<number>(0, 'live:signal:tick')
  public isLoopActive = this.createSignal<boolean>(false, 'live:signal:loop-active')
  public isTabActive = this.createSignal<boolean>(true, 'live:signal:tab-active')

  public receivedNotifications = this.createSignal<NotificationItem[]>([], 'live:signal:feed-list')
  public outgoingBuffer = this.createSignal<NotificationItem[]>([], 'live:signal:outgoing-buffer')

  private feedSessionId = this.createSignal<string>(Math.random().toString(), 'live:signal:session-id')
  private resourceSessionMap = new Map<any, string>()

  // ΠœΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ
  public requestCount = this.createSignal<number>(0, 'live:signal:request-count')
  public currentStatus = this.createSignal<string>('πŸ›‘ Поллинг остановлСн. НаТмитС Init для запуска.', 'live:signal:status')

  // ΠžΡ‡ΠΈΡ‰Π΅Π½Π½Ρ‹ΠΉ Ρ€Π΅Π°ΠΊΡ‚ΠΈΠ²Π½Ρ‹ΠΉ ΠΊΠΎΡ€Ρ‚Π΅ΠΆ зависимостСй
  private apiDeps = this.engine.computed<TPollingDeps>(() => [
    this.isLoopActive.value,
    this.pollingTick.value,
    this.isTabActive.value
  ], 'live:computed:apiDeps')

  /**
   * Π”Π•ΠšΠ›ΠΠ ΠΠ’Π˜Π’ΠΠ«Π™ Π Π•Π‘Π£Π Π‘ Π›ΠžΠΠ“-ΠŸΠžΠ›Π›Π˜ΠΠ“Π
   * ΠŸΠΎΠ»Π½ΠΎΡΡ‚ΡŒΡŽ ΠΏΠ΅Ρ€Π΅Π²Π΅Π΄Π΅Π½ Π½Π° v2 Options API Π΄Π΅ΠΊΠΎΡ€Π°Ρ‚ΠΎΡ€Π° withLongPolling с ΠΌΠ΅Ρ‚ΠΎΠ΄ΠΎΠΌ onError.
   */
  public notificationsResource = this.engine.resource<ServerResponse | null, TPollingDeps>(
    withLongPolling(
      async (deps, abortSignal) => {
        const [isLoopActive, pollingTick, isTabActive] = deps

        if (!this.isServerOnline.value) {
          this.currentStatus.value = `🚨 Ошибка 502: НСт связи. Π‘ΡƒΡ„Π΅Ρ€ΠΈΠ·ΠΈΡ€ΡƒΠ΅ΠΌ...`
          throw new Error('502 Bad Gateway')
        }

        this.requestCount.value += 1
        this.currentStatus.value = `πŸ“‘ [Π’Π°ΠΊΡ‚: ${pollingTick}] Π£Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅ΠΌ Π΄Π»ΠΈΠ½Π½ΠΎΠ΅ соСдинСниС...`

        const notificationsPayload = [...this.outgoingBuffer.value]
        this.outgoingBuffer.value = []

        const mockPayload: ServerResponse = { ok: true, notifications: notificationsPayload }
        const queryParams = new URLSearchParams({
          userId: String(this.userIdSignal.value),
          tick: String(pollingTick),
          _addData: JSON.stringify(mockPayload)
        }).toString()

        try {
          const response = await fetch(`/fake-feed-vite-proxy/?${queryParams}`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ userId: this.userIdSignal.value, notifications: notificationsPayload }),
            signal: abortSignal
          })

          if (!response.ok) {
            this.outgoingBuffer.value = [...notificationsPayload, ...this.outgoingBuffer.value]
            throw new Error(`Ошибка HTTP: ${response.status}`)
          }

          const jsonResult = await response.json()
          this.resourceSessionMap.set(jsonResult, this.feedSessionId.value)
          return jsonResult
        } catch (error) {
          if (error instanceof DOMException && error.name === 'AbortError') throw error
          this.resourceSessionMap.set(mockPayload, this.feedSessionId.value)
          return mockPayload
        }
      },
      {
        onNextTick: () => this.nextTick(),

        /**
         * Π Π΅Π°Π»ΠΈΠ·ΡƒΠ΅Ρ‚ мост ΠΌΠ΅ΠΆΠ΄Ρƒ ошибкой Π΄Π΅ΠΊΠΎΡ€Π°Ρ‚ΠΎΡ€Π° ΠΈ инфраструктурным UI-Ρ‚Π°ΠΉΠΌΠ΅Ρ€ΠΎΠΌ.
         *
         * @param {number} ms - ВрСмя оТидания Π² мс Π΄ΠΎ ΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π΅ΠΉ ΠΏΠΎΠΏΡ‹Ρ‚ΠΊΠΈ (Backoff).
         * @param {() => void} onRetryScheduled - Π‘ΠΈΠ³Π½Π°Π»ΡŒΠ½Ρ‹ΠΉ Ρ‚Ρ€ΠΈΠ³Π³Π΅Ρ€ для фиксации шага экспонСнты.
         */
        onError: (ms: number, onRetryScheduled: () => void) => this.countdown.start(ms, onRetryScheduled),

        delay: 3000,
        errorInitialDelay: 2000,
        errorMaxDelay: 8000
      }
    ),
    this.apiDeps,
    {
      name: 'live:resource:notifications',
      resetDataOnSourceChange: true,
      validateBeforeFetch: () => this.isLoopActive.value && this.isTabActive.value,

      responseValidate: (res: any) => {
        const payload = res as ServerResponse | null
        if (!payload || payload.ok !== true) {
          this.currentStatus.value = '🚨 Ошибка Π²Π°Π»ΠΈΠ΄Π°Ρ†ΠΈΠΈ структуры ΠΎΡ‚Π²Π΅Ρ‚Π°'
          return 'НСкоррСктная структура JSON ΠΎΡ‚Π²Π΅Ρ‚Π°'
        }

        const activeSession = this.feedSessionId.value

        // Π‘Π½Π°Ρ‡Π°Π»Π° ТСстко провСряСм Π½Π°Π»ΠΈΡ‡ΠΈΠ΅ элСмСнтов Π² ΠΏΡ€ΠΈΠ»Π΅Ρ‚Π΅Π²ΡˆΠ΅ΠΉ ΠΏΠ°Ρ‡ΠΊΠ΅ бэкСнда.
        // Если ΠΏΠ°Ρ‡ΠΊΠ° Π½Π΅ пустая β€” ΠΌΡ‹ ВБЕГДА Π²Ρ‹Π²ΠΎΠ΄ΠΈΠΌ статус ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎΠΉ доставки, убирая Π΄Π΅Π΄Π»ΠΎΠΊ "Канал чист"!
        if (payload.notifications && payload.notifications.length > 0) {
          const incomingItems = payload.notifications
          const currentList = this.receivedNotifications.value

          // Π€ΠΈΠ»ΡŒΡ‚Ρ€ΡƒΠ΅ΠΌ Π΄ΡƒΠ±Π»ΠΈΠΊΠ°Ρ‚Ρ‹
          const filteredItems = incomingItems.filter(item => !currentList.some(exist => exist.id === item.id))

          if (filteredItems.length > 0) {
            this.receivedNotifications.value = [...filteredItems, ...currentList]
            this.currentStatus.value = `⚑ УспСшно доставлСн ΠΏΠ°ΠΊΠ΅Ρ‚ ΠΈΠ· ${filteredItems.length} событий!`
          } else {
            // Если всС элСмСнты ΠΈΠ· ΠΏΠ°Ρ‡ΠΊΠΈ ΡƒΠΆΠ΅ Π±Ρ‹Π»ΠΈ Π² Π»Π΅Π½Ρ‚Π΅ Ρ€Π°Π½Π΅Π΅
            this.currentStatus.value = '⏳ Канал ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½. НовыС события ΡƒΠΆΠ΅ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Π°Π½Ρ‹.'
          }
        } else {
          // Π‘Ρ‚Ρ€ΠΎΠ³ΠΎ Ссли бэкСнд прислал Π°Π±ΡΠΎΠ»ΡŽΡ‚Π½ΠΎ пустой массив (Π½ΠΎΡ€ΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ Ρ‚Π°ΠΉΠΌΠ°ΡƒΡ‚ удСрТания)
          this.currentStatus.value = '⏳ Канал чист. Новых событий Π½Π° бэкСндС Π½Π΅Ρ‚.'
        }

        return true
      }
    }
  )

  public setTabVisibility(isActive: boolean) {
    this.isTabActive.value = isActive

    if (isActive) {
      if (this.isLoopActive.value) {
        this.currentStatus.value = 'πŸ”„ Π’ΠΊΠ»Π°Π΄ΠΊΠ° снова Π°ΠΊΡ‚ΠΈΠ²Π½Π°. ВозобновляСм Π»ΠΎΠ½Π³-ΠΏΠΎΠ»Π»ΠΈΠ½Π³...'
        this.nextTick()
      }
    } else {
      this.countdown.stop()
      this.currentStatus.value = '⏸️ Π’ΠΊΠ»Π°Π΄ΠΊΠ° Π½Π΅Π°ΠΊΡ‚ΠΈΠ²Π½Π°. Π›ΠΎΠ½Π³-ΠΏΠΎΠ»Π»ΠΈΠ½Π³ Π²Ρ€Π΅ΠΌΠ΅Π½Π½ΠΎ Π·Π°ΠΌΠΎΡ€ΠΎΠΆΠ΅Π½.'
    }
  }

  /**
   * Π˜ΠœΠŸΠ•Π ΠΠ’Π˜Π’ΠΠ«Π™ БВАРВЕР (Init)
   * Достаточно, Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π΄Π²ΠΈΠΆΠΎΠΊ сам ΠΎΡ‚ΠΊΡ€Ρ‹Π» соСдинСниС.
   */
  public startLongPolling = () => {
    if (this.isLoopActive.value) return
    this.isLoopActive.value = true
  }

  public nextTick() {
    if (this.isLoopActive.value && this.isTabActive.value) {
      this.pollingTick.value += 1
    }
  }

  public async triggerEventOnServer(text: string, type: 'like' | 'message') {
    const newItem: NotificationItem = {
      id: Math.random().toString(36).substring(2, 11),
      text,
      timestamp: new Date().toLocaleTimeString(),
      type
    }
    this.outgoingBuffer.value = [...this.outgoingBuffer.value, newItem]
    this.currentStatus.value = `⏳ Π‘ΠΎΠ±Ρ‹Ρ‚ΠΈΠ΅ зафиксировано ΠΈ ΠΆΠ΄Π΅Ρ‚ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΈ.`

    // Если ΠΏΠΎΠ»Π»ΠΈΠ½Π³ спит (Destroy) β€” ΠΌΡ‹ НЕ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΠΏΠΈΠ½Π°Ρ‚ΡŒ refetch рСсурса!
    // Бобытия Π±ΡƒΠ΄ΡƒΡ‚ ΠΊΠΎΠΏΠΈΡ‚ΡŒΡΡ Π² ΠΎΡ‡Π΅Ρ€Π΅Π΄ΠΈ, ΠΏΠΎΠΊΠ° ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ сам Π½Π΅ Π½Π°ΠΆΠΌΠ΅Ρ‚ Init.
    if (this.isLoopActive.value && this.isServerOnline.value && this.isTabActive.value) {
      this.notificationsResource.refetch()
    }
  }

  public toggleServerStatus() {
    this.isServerOnline.value = !this.isServerOnline.value

    if (this.isServerOnline.value) {
      this.countdown.stop()
      this.currentStatus.value = 'πŸ”„ Π‘Π΅Ρ‚ΡŒ Π½Π°ΠΉΠ΄Π΅Π½Π°. ВозобновляСм Π»ΠΎΠ½Π³-ΠΏΠΎΠ»Π»ΠΈΠ½Π³...'

      // ПинаСм пСрСзапуск Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Ссли сам ΠΏΠΎΠ»Π»ΠΈΠ½Π³ сСйчас Π°ΠΊΡ‚ΠΈΠ²Π΅Π½
      if (this.isLoopActive.value) {
        this.notificationsResource.refetch()
      }
    } else {
      this.currentStatus.value = '🚨 Бвязь потСряна. НакапливаСм Π±ΡƒΡ„Π΅Ρ€.'
      if (this.isLoopActive.value) {
        this.nextTick()
      }
    }
  }

  public clearFeed() {
    this.feedSessionId.value = Math.random().toString()
    this.receivedNotifications.value = []
    this.currentStatus.value = 'πŸ—‘οΈ Π›Π΅Π½Ρ‚Π° ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎ ΠΎΡ‡ΠΈΡ‰Π΅Π½Π°.'
  }

  public destroy() {
    this.countdown.stop()
    this.isLoopActive.value = false
    this.pollingTick.value = 0
    this.resourceSessionMap.clear()
    this.outgoingBuffer.value = []
    this.receivedNotifications.value = []
    this.requestCount.value = 0
    this.isServerOnline.value = true
    this.currentStatus.value = 'πŸ›‘ Поллинг остановлСн. НаТмитС Init для запуска.'
  }
}

React ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚:

tsx
import { useEffect } from 'react'
import baseClasses from '~/ui.common.module.scss'
import btnClasses from '~/shared/Button/ui.button.module.scss'
import { ReactiveEngine } from '@pravosleva/reactive-engine/react'
import { LiveNotificationsLogic } from './service.LiveNotificationsLogic'
import clsx from 'clsx'

const engine = new ReactiveEngine()

export const LiveNotificationsExample = () => {
  const logic = engine.inject(LiveNotificationsLogic)

  const userId = engine.use(logic.userIdSignal)
  const isServerOnline = engine.use(logic.isServerOnline)
  const isLoopActive = engine.use(logic.isLoopActive)
  const isTabActive = engine.use(logic.isTabActive) // ΠŸΠΎΠ΄ΠΏΠΈΡΡ‹Π²Π°Π΅ΠΌΡΡ Π½Π° Ρ„Π»Π°Π³ Π²ΠΊΠ»Π°Π΄ΠΊΠΈ
  const requestCount = engine.use(logic.requestCount)
  const currentStatus = engine.use(logic.currentStatus)

  const secondsToRetry = engine.use(logic.countdown.secondsLeft)
  const receivedNotifications = engine.use(logic.receivedNotifications)
  const outgoingBuffer = engine.use(logic.outgoingBuffer)

  // ΠšΠΎΠΌΠΏΠ»Π΅ΠΊΡΠ½Ρ‹ΠΉ эффСкт ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ ΠΈ отслСТивания активности Π²ΠΊΠ»Π°Π΄ΠΊΠΈ
  useEffect(() => {
    logic.startLongPolling()

    const handleVisibilityChange = () => {
      // ΠŸΠ΅Ρ€Π΅Π΄Π°Π΅ΠΌ статус видимости Π²ΠΊΠ»Π°Π΄ΠΊΠΈ (true/false) Π² бизнСс-сСрвис
      logic.setTabVisibility(document.visibilityState === 'visible')
    }

    document.addEventListener('visibilitychange', handleVisibilityChange)

    return () => {
      document.removeEventListener('visibilitychange', handleVisibilityChange)
      logic.destroy()
    }
  }, [logic])

  return (
    <div
      className={clsx(baseClasses.unit, baseClasses.stack2)}
      style={{ padding: '24px', fontFamily: 'system-ui', width: '600px' }}
    >
      <div className={baseClasses.absoluteUnitLabel}>
        Long Polling Live Stream Demo {!isTabActive && '⏸️'}
      </div>

      {/* ОписаниС */}
      <div style={{ fontSize: 'small' }}>
        βš™οΈ Π‘ ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ ΠΊΠ½ΠΎΠΏΠΎΠΊ <strong>Init</strong> ΠΈ <strong>Destroy</strong> Π²Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΠΏΠΎΠ»Π½ΠΎΡΡ‚ΡŒΡŽ
        Π·Π°ΠΏΡƒΡΠΊΠ°Ρ‚ΡŒ ΠΈ Π³Π°ΡΠΈΡ‚ΡŒ Ρ„ΠΎΠ½ΠΎΠ²Ρ‹Π΅ сСтСвыС ΠΏΠΎΡ‚ΠΎΠΊΠΈ. ΠŸΠ΅Ρ€Π΅ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ ΠΌΠ΅ΠΆΠ΄Ρƒ Π½ΠΈΠΌΠΈ Π°Π±ΡΠΎΠ»ΡŽΡ‚Π½ΠΎ Π²Π·Π°ΠΈΠΌΠΎΠΈΡΠΊΠ»ΡŽΡ‡Π°ΡŽΡ‰Π΅Π΅.
      </div>

      {/* Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ ΠΆΠΈΠ·Π½Π΅Π½Π½Ρ‹ΠΌ Ρ†ΠΈΠΊΠ»ΠΎΠΌ */}
      <div style={{ display: 'flex', gap: '16px', width: '100%' }}>
        <button
          disabled={isLoopActive}
          onClick={() => logic.startLongPolling()}
          className={clsx(btnClasses.btn, btnClasses.neonBtn, btnClasses['neonBtn--primary'], {
            [btnClasses['neonBtn--outlined']]: isLoopActive,
            [btnClasses['neonBtn--contained']]: !isLoopActive,
          })}
          style={{ flex: 1, opacity: isLoopActive ? 0.3 : 1 }}
        >
          Init
        </button>
        <button
          disabled={!isLoopActive}
          onClick={() => logic.destroy()}
          className={clsx(btnClasses.btn, btnClasses.neonBtn, btnClasses['neonBtn--danger'], btnClasses['neonBtn--outlined'])}
          style={{ flex: 1, opacity: !isLoopActive ? 0.3 : 1 }}
        >
          Destroy
        </button>
      </div>

      {/* Π˜Π½Π΄ΠΈΠΊΠ°Ρ‚ΠΎΡ€Ρ‹ сСти */}
      <div style={{ display: 'flex', justifyContent: 'space-between', background: '#15151a', color: '#fff', padding: '16px', borderRadius: '8px', fontSize: '13px' }}>
        <div>
          БСссия: <strong style={{ color: '#00b4d8' }}>{userId}</strong>
          {!isTabActive && <span style={{ color: '#ffb74d', marginLeft: '8px' }}>(Π’ΠΊΠ»Π°Π΄ΠΊΠ° Π² Ρ„ΠΎΠ½Π΅)</span>}
        </div>
        <div>Π‘ΠΎΠ²Π΅Ρ€ΡˆΠ΅Π½ΠΎ HTTP-запросов: <strong style={{ color: '#e6af2e' }}>{requestCount}</strong></div>
      </div>

      {/* ΠŸΠ΅Ρ€Π΅ΠΊΠ»ΡŽΡ‡Π°Ρ‚Π΅Π»ΡŒ сСрвСра */}
      <div style={{ display: 'flex', justifyContent: 'space-between', gap: '16px', alignItems: 'center' }}>
        <div style={{ display: 'flex', gap: '8px', alignItems: 'center', fontSize: '14px' }}>
          <span style={{
            display: 'inline-block', width: '10px', height: '10px', borderRadius: '50%',
            background: isServerOnline ? '#4caf50' : '#ef5350',
            boxShadow: isServerOnline ? '0 0 8px #4caf50' : '0 0 8px #ef5350'
          }} />
          <span>БэкСнд: <strong>{isServerOnline ? 'ONLINE' : 'OFFLINE (502)'}</strong></span>
        </div>

        <button
          disabled={!isLoopActive}
          onClick={() => logic.toggleServerStatus()}
          className={clsx(
            btnClasses.btn,
            btnClasses.neonBtn,
            {
              [btnClasses['neonBtn--primary']]: !isServerOnline,
              [btnClasses['neonBtn--danger']]: isServerOnline,
            },
            btnClasses['neonBtn--outlined']
          )}
        >
          {isServerOnline ? 'Π’Ρ‹ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ сСрвСр' : 'Π’ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ сСрвСр'}
        </button>
      </div>

      {/* Бтатус-Π±Π°Ρ€ */}
      <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', fontSize: '12px', minHeight: '38px', background: '#111', borderRadius: '8px', padding: '6px 12px', textAlign: 'center' }}>
        {!isServerOnline && isLoopActive ? (
          <span style={{ color: '#ef5350' }}>
            🚨 Π‘Π΅Ρ‚ΡŒ Ρ€Π°Π·ΠΎΡ€Π²Π°Π½Π°. НакапливаСм Π±ΡƒΡ„Π΅Ρ€. ΠŸΠ΅Ρ€Π΅ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ Ρ‡Π΅Ρ€Π΅Π·: <strong style={{ fontSize: '14px', color: '#fff', background: '#3a1a1a', padding: '2px 6px', borderRadius: '4px', marginLeft: '4px' }}>{secondsToRetry} сСк</strong>
          </span>
        ) : (
          <span style={{ color: !isLoopActive ? '#aaa' : currentStatus.includes('⚑') ? '#4caf50' : currentStatus.includes('⏳') ? '#e6af2e' : '#00b4d8', fontWeight: currentStatus.includes('⚑') ? 'bold' : 'normal' }}>
            {currentStatus}
          </span>
        )}
      </div>

      {/* Кнопки ΠΈΠΌΠΈΡ‚Π°Ρ†ΠΈΠΈ дСйствий */}
      <div style={{ display: 'flex', gap: '16px', width: '100%' }}>
        <button
          onClick={() => logic.triggerEventOnServer('Новый Π»Π°ΠΉΠΊ ΠΊ ΡΡ‚Π°Ρ‚ΡŒΠ΅', 'like')}
          className={clsx(btnClasses.btn, btnClasses.neonBtn, btnClasses['neonBtn--primary'], btnClasses['neonBtn--outlined'])}
          style={{ flex: 1 }}
        >
          πŸ‘ Π˜ΠΌΠΈΡ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ Π›Π°ΠΉΠΊ
        </button>
        <button
          onClick={() => logic.triggerEventOnServer('НовоС сообщСниС Ρ‡Π°Ρ‚Π°', 'message')}
          className={clsx(btnClasses.btn, btnClasses.neonBtn, btnClasses['neonBtn--primary'], btnClasses['neonBtn--outlined'])}
          style={{ flex: 1 }}
        >
          πŸ’¬ Π˜ΠΌΠΈΡ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ Π‘ΠΎΠΎΠ±Ρ‰Π΅Π½ΠΈΠ΅
        </button>
      </div>

      {/* Π’Π°Π±Π»ΠΈΡ†Π° ΠΊΠΎΠ»ΠΎΠ½ΠΎΠΊ */}
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', width: '100%', marginTop: '8px' }}>

        <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
          <div style={{ fontSize: '12px', fontWeight: 'bold', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
            <span>🟑 Π’Π΅, Ρ‡Ρ‚ΠΎ Π² ΠΎΡ‡Π΅Ρ€Π΅Π΄ΠΈ:</span>
            <span style={{ background: '#3a2510', color: '#ffb74d', padding: '2px 8px', borderRadius: '10px', fontSize: '11px', border: '1px solid #ffb74d' }}>{outgoingBuffer.length}</span>
          </div>
          <div
            style={{
              background: '#000',
              height: '180px',
              borderRadius: '8px',
              padding: '12px',
              overflowY: 'auto',
              display: 'flex',
              flexDirection: 'column',
              gap: '6px',
            }}
          >
            {outgoingBuffer.length === 0 ? (
              <span style={{ color: '#aaa', fontStyle: 'italic', fontSize: '13px', textAlign: 'center' }}>ΠžΡ‡Π΅Ρ€Π΅Π΄ΡŒ пуста.<br />Π’Ρ‹ΠΊΠ»ΡŽΡ‡ΠΈΡ‚Π΅ сСрвСр для накоплСния.</span>
            ) : (
              outgoingBuffer.map((item) => (
                <div key={item.id} style={{ color: '#ffb74d', fontSize: '13px', fontFamily: 'monospace', display: 'flex', justifyContent: 'space-between' }}>
                  <span>{item.type === 'like' ? '❀️' : 'πŸ’¬'} {item.text}</span>
                  <span style={{ color: '#666', fontSize: '10px' }}>{item.timestamp}</span>
                </div>
              ))
            )}
          </div>
        </div>

        <div className={baseClasses.stack1}>
          <div style={{ fontSize: '12px', fontWeight: 'bold', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
            <span style={{ display: 'flex', gap: '6px', alignItems: 'center' }}>
              <span>🟒 УспСшно ΠΎΡ‚ΠΏΡ€Π°Π²Π»Π΅Π½Π½Ρ‹Π΅:</span>
              <span style={{ background: '#122b15', color: '#4caf50', padding: '2px 8px', borderRadius: '10px', fontSize: '11px', border: '1px solid #4caf50' }}>{receivedNotifications.length}</span>
            </span>
            {receivedNotifications.length > 0 && (
              <button onClick={() => logic.clearFeed()} style={{ background: 'transparent', color: '#666', border: 'none', cursor: 'pointer', textDecoration: 'underline', fontSize: '11px', padding: 0 }}>
                ΠΎΡ‡ΠΈΡΡ‚ΠΈΡ‚ΡŒ
              </button>
            )}
          </div>
          <div
            style={{
              background: '#000',
              height: '180px',
              borderRadius: '8px',
              padding: '12px',
              overflowY: 'auto',
              display: 'flex',
              flexDirection: 'column',
              gap: '6px',
            }}
          >
            {receivedNotifications.length === 0 ? (
              <span style={{ color: '#aaa', fontStyle: 'italic', fontSize: '13px', textAlign: 'center' }}>НСт доставлСнных событий.<br />ΠšΠ»ΠΈΠΊΠ½ΠΈΡ‚Π΅ ΠΏΠΎ ΠΊΠ½ΠΎΠΏΠΊΠ°ΠΌ Π²Ρ‹ΡˆΠ΅.</span>
            ) : (
              receivedNotifications.map((item) => (
                <div key={item.id} style={{ color: item.type === 'like' ? '#ff6b6b' : '#4caf50', fontSize: '12px', fontFamily: 'monospace', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
                  <span style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
                    <span>{item.type === 'like' ? '❀️' : 'πŸ’¬'}</span>
                    <span>{item.text}</span>
                  </span>
                  <span style={{ color: '#555', fontSize: '11px' }}>{item.timestamp}</span>
                </div>
              ))
            )}
          </div>
        </div>

      </div>
    </div>
  )
}