π‘ 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
ΠΡΠ½ΠΎΠ²Π½Π°Ρ Π±ΠΈΠ·Π½Π΅Ρ-Π»ΠΎΠ³ΠΈΠΊΠ°:
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 ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ:
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>
)
}