Przejdź do głównej zawartości

Server-Sent Events (SSE)

Subskrybuj tematy Notifer w czasie rzeczywistym używając Server-Sent Events (SSE).

Czym jest SSE?

Server-Sent Events (SSE) to technologia pozwalająca serwerowi na wysyłanie danych do klienta przez HTTP. Jest idealna dla powiadomień w czasie rzeczywistym, ponieważ:

  • Proste: Działa przez standardowy HTTP (bez specjalnych protokołów)
  • Niezawodne: Automatyczne ponowne łączenie przy utracie połączenia
  • Wydajne: Długotrwałe połączenie z niskim narzutem
  • Uniwersalne: Działa we wszystkich nowoczesnych przeglądarkach i curl
  • Przyjazne dla zapory: Używa standardowych portów HTTP/HTTPS

Przykładowy przepływ:

Klient → Serwer: GET /my-topic/sse
Serwer → Klient: (połączenie pozostaje otwarte)
Serwer → Klient: event: message\ndata: {"message": "Hello"}\n\n
Serwer → Klient: event: message\ndata: {"message": "World"}\n\n

Szybki start

Używanie cURL

curl -N https://app.notifer.io/my-topic/sse

Wynik:

event: message
data: {"id": "uuid", "message": "Pierwsza wiadomość", "priority": 3, "timestamp": "2025-11-22T10:00:00Z"}

event: message
data: {"id": "uuid", "message": "Druga wiadomość", "priority": 5, "timestamp": "2025-11-22T10:01:00Z"}

Używanie JavaScript (przeglądarka)

const eventSource = new EventSource('https://app.notifer.io/my-topic/sse');

eventSource.onmessage = (event) => {
const message = JSON.parse(event.data);
console.log('Nowa wiadomość:', message);
};

eventSource.onerror = (error) => {
console.error('Błąd SSE:', error);
};

// Zamknij połączenie gdy skończysz
// eventSource.close();

Używanie Python

import requests

url = 'https://app.notifer.io/my-topic/sse'

with requests.get(url, stream=True) as response:
for line in response.iter_lines():
if line:
decoded_line = line.decode('utf-8')
if decoded_line.startswith('data:'):
message_json = decoded_line[5:].strip()
print(message_json)

Subskrybowanie prywatnych tematów

Używanie tokena uwierzytelniającego

Dla prywatnych tematów dołącz swój token JWT lub token dostępu do tematu:

# Metoda 1: Parametr zapytania
curl -N "https://app.notifer.io/my-private-topic/sse?token=YOUR_JWT_TOKEN"

# Metoda 2: Nagłówek Authorization
curl -N https://app.notifer.io/my-private-topic/sse \
-H "Authorization: Bearer YOUR_JWT_TOKEN"

JavaScript:

// Dołącz token w URL
const eventSource = new EventSource(
'https://app.notifer.io/my-private-topic/sse?token=' + encodeURIComponent(token)
);

Python:

headers = {'Authorization': f'Bearer {token}'}
with requests.get(url, headers=headers, stream=True) as response:
# ... przetwarzaj zdarzenia

Parametry zapytania

since_id - Wznów od ostatniej wiadomości

Pobierz wszystkie wiadomości od konkretnego ID wiadomości:

curl -N "https://app.notifer.io/my-topic/sse?since_id=LAST_MESSAGE_ID"

Przypadek użycia: Połącz ponownie i nadgon pominięte wiadomości.

Przykład:

let lastMessageId = null;

function connect() {
const url = lastMessageId
? `https://app.notifer.io/my-topic/sse?since_id=${lastMessageId}`
: 'https://app.notifer.io/my-topic/sse';

const eventSource = new EventSource(url);

eventSource.onmessage = (event) => {
const message = JSON.parse(event.data);
lastMessageId = message.id; // Zapisz dla ponownego połączenia
displayMessage(message);
};

eventSource.onerror = () => {
eventSource.close();
setTimeout(connect, 5000); // Połącz ponownie po 5s
};
}

connect();

token - Uwierzytelnianie

curl -N "https://app.notifer.io/my-topic/sse?token=YOUR_TOKEN"

Format wiadomości

Struktura zdarzenia

event: message
data: {...json...}

Pola:

  • event: - Typ zdarzenia (zawsze "message" dla powiadomień)
  • data: - Dane wiadomości JSON
  • Pusta linia - Oznacza koniec zdarzenia

Dane wiadomości

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"topic": "my-topic",
"message": "Serwer nie działa!",
"title": "Alert krytyczny",
"priority": 1,
"tags": ["critical", "server"],
"timestamp": "2025-11-22T10:30:00Z"
}

Pola:

  • id - Unikalny ID wiadomości (UUID)
  • topic - Nazwa tematu
  • message - Treść wiadomości (zwykły tekst lub Markdown)
  • title - Opcjonalny tytuł wiadomości
  • priority - Poziom priorytetu (1-5)
  • tags - Tablica tagów
  • timestamp - Znacznik czasu (ISO 8601)

Obsługa ponownego łączenia

Połączenia SSE automatycznie łączą się ponownie przy zerwaniu, ale powinieneś obsługiwać ponowne łączenia z gracją:

Przeglądarka (EventSource)

function createConnection() {
const eventSource = new EventSource('https://app.notifer.io/my-topic/sse');

eventSource.onopen = () => {
console.log('✅ Połączono');
};

eventSource.onerror = (error) => {
console.error('❌ Błąd połączenia:', error);
// Przeglądarka automatycznie połączy ponownie
// EventSource ma wbudowaną logikę ponawiania
};

eventSource.onmessage = (event) => {
const message = JSON.parse(event.data);
handleMessage(message);
};

return eventSource;
}

let connection = createConnection();

// Ręczne ponowne połączenie jeśli potrzebne
function reconnect() {
connection.close();
connection = createConnection();
}

Python (ręczne ponowne łączenie)

import requests
import time
import json

def subscribe(topic, token=None):
url = f'https://app.notifer.io/{topic}/sse'
headers = {}
if token:
headers['Authorization'] = f'Bearer {token}'

while True:
try:
print('🔌 Łączenie...')
with requests.get(url, headers=headers, stream=True) as response:
response.raise_for_status()
print('✅ Połączono')

for line in response.iter_lines():
if line:
decoded = line.decode('utf-8')
if decoded.startswith('data:'):
message_json = decoded[5:].strip()
message = json.loads(message_json)
handle_message(message)

except requests.exceptions.RequestException as e:
print(f'❌ Błąd połączenia: {e}')
print('🔄 Ponowne łączenie za 5 sekund...')
time.sleep(5)

def handle_message(message):
print(f"📬 [{message['priority']}] {message['title']}: {message['message']}")

subscribe('my-topic')

Obsługa błędów

Kody statusu HTTP

KodZnaczenieAkcja
200SukcesPołączenie ustanowione
401Brak autoryzacjiSprawdź token uwierzytelniający
404Temat nie znalezionyZweryfikuj nazwę tematu
403ZabronioneSprawdź uprawnienia dostępu do tematu
429Ograniczenie liczby żądańPoczekaj i spróbuj ponownie z backoff

Obsługa błędów w JavaScript

const eventSource = new EventSource('https://app.notifer.io/my-topic/sse');

eventSource.addEventListener('error', (event) => {
if (event.target.readyState === EventSource.CLOSED) {
console.error('Połączenie zamknięte przez serwer');
} else if (event.target.readyState === EventSource.CONNECTING) {
console.log('Ponowne łączenie...');
}
});

Przykłady z rzeczywistości

Pulpit monitorowania serwera

const topics = ['prod-web', 'prod-api', 'prod-db'];
const connections = {};

topics.forEach(topic => {
const eventSource = new EventSource(`https://app.notifer.io/${topic}/sse`);

eventSource.onmessage = (event) => {
const message = JSON.parse(event.data);
updateDashboard(topic, message);

// Pokaż alert dla krytycznych wiadomości
if (message.priority <= 2) {
showAlert(message);
playAlertSound();
}
};

connections[topic] = eventSource;
});

function updateDashboard(topic, message) {
const container = document.getElementById(`topic-${topic}`);
const messageEl = document.createElement('div');
messageEl.className = `message priority-${message.priority}`;
messageEl.innerHTML = `
<strong>${message.title}</strong>
<p>${message.message}</p>
<small>${new Date(message.timestamp).toLocaleString()}</small>
`;
container.prepend(messageEl);
}

Monitor alertów Python

import requests
import json
from datetime import datetime

def monitor_alerts(topic, priority_threshold=3):
"""Monitoruj temat i wyświetlaj tylko ważne alerty"""
url = f'https://app.notifer.io/{topic}/sse'

print(f'📡 Monitorowanie {topic} (priorytet <= {priority_threshold})')

with requests.get(url, stream=True) as response:
for line in response.iter_lines():
if line and line.startswith(b'data:'):
data = line[5:].strip()
message = json.loads(data)

if message['priority'] <= priority_threshold:
timestamp = datetime.fromisoformat(message['timestamp'].replace('Z', '+00:00'))
print(f"\n🚨 [{message['priority']}] {message['title']}")
print(f" {message['message']}")
print(f" Tagi: {', '.join(message['tags'])}")
print(f" Czas: {timestamp.strftime('%Y-%m-%d %H:%M:%S')}")

monitor_alerts('production-alerts', priority_threshold=2)

Subskrypcja wielotematowa (Python)

import requests
import json
from threading import Thread

def subscribe_topic(topic, callback):
"""Subskrybuj temat w oddzielnym wątku"""
url = f'https://app.notifer.io/{topic}/sse'

while True:
try:
with requests.get(url, stream=True) as response:
for line in response.iter_lines():
if line and line.startswith(b'data:'):
message = json.loads(line[5:].strip())
callback(topic, message)
except Exception as e:
print(f'Błąd w {topic}: {e}')
time.sleep(5)

def handle_message(topic, message):
print(f"[{topic}] {message['title']}: {message['message']}")

# Subskrybuj wiele tematów równolegle
topics = ['deployments', 'errors', 'monitoring']

threads = []
for topic in topics:
thread = Thread(target=subscribe_topic, args=(topic, handle_message))
thread.daemon = True
thread.start()
threads.append(thread)

# Utrzymuj główny wątek żywym
for thread in threads:
thread.join()

SSE vs WebSocket

FunkcjaSSEWebSocket
KierunekTylko serwer → klientDwukierunkowe
ProtokółHTTPWS/WSS
Ponowne łączenieAutomatyczneRęczne
Przypadek użyciaAktualizacje w czasie rzeczywistymCzat, gry
ZłożonośćProsteBardziej złożone
Wsparcie przeglądarekWszystkie nowoczesne przeglądarkiWszystkie nowoczesne przeglądarki

Kiedy używać SSE:

  • ✅ Jednokierunkowe powiadomienia (serwer → klient)
  • ✅ Prosta konfiguracja i debugowanie
  • ✅ Potrzebne automatyczne ponowne łączenie
  • ✅ Środowiska tylko HTTP

Kiedy używać WebSocket:

  • ✅ Potrzebna dwukierunkowa komunikacja
  • ✅ Bardzo częste aktualizacje
  • ✅ Aplikacje do gier lub czatu

Dla Notifer zalecane jest SSE, ponieważ:

  • Powiadomienia są jednokierunkowe (serwer → klient)
  • Automatyczne ponowne łączenie upraszcza kod klienta
  • Działa ze standardową infrastrukturą HTTP

Wsparcie przeglądarek

SSE jest wspierane we wszystkich nowoczesnych przeglądarkach:

  • ✅ Chrome/Edge 6+
  • ✅ Firefox 6+
  • ✅ Safari 5+
  • ✅ Opera 11+
  • ❌ Internet Explorer (nie wspierane)

Polyfills: Dla wsparcia IE użyj EventSource polyfill

Dobre praktyki

1. Zapisuj ID ostatniej wiadomości

Zawsze śledź ID ostatniej wiadomości dla ponownego połączenia:

let lastId = localStorage.getItem('lastMessageId');

const url = lastId
? `https://app.notifer.io/topic/sse?since_id=${lastId}`
: 'https://app.notifer.io/topic/sse';

const eventSource = new EventSource(url);

eventSource.onmessage = (event) => {
const message = JSON.parse(event.data);
lastId = message.id;
localStorage.setItem('lastMessageId', lastId);
};

2. Obsługuj zmiany widoczności

Wstrzymaj/wznów połączenie gdy zakładka jest ukryta:

document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// Opcjonalnie zamknij połączenie gdy zakładka jest ukryta
// eventSource.close();
} else {
// Połącz ponownie gdy zakładka jest widoczna
// eventSource = createConnection();
}
});

3. Zaimplementuj wykrywanie heartbeat

Wykrywaj nieaktywne połączenia:

let heartbeatTimeout;

eventSource.onmessage = (event) => {
clearTimeout(heartbeatTimeout);

// Oczekuj wiadomości w ciągu 60 sekund
heartbeatTimeout = setTimeout(() => {
console.warn('Nie otrzymano heartbeat, ponowne łączenie...');
eventSource.close();
eventSource = createConnection();
}, 60000);
};

4. Używaj wykładniczego backoff

Dla ręcznych ponownych połączeń:

import time

def exponential_backoff(attempt, max_wait=60):
"""Oblicz opóźnienie backoff z wykładniczym wzrostem"""
wait = min(2 ** attempt, max_wait)
return wait

attempt = 0
while True:
try:
subscribe(topic)
attempt = 0 # Zresetuj po sukcesie
except Exception as e:
wait = exponential_backoff(attempt)
print(f'Ponowne łączenie za {wait}s...')
time.sleep(wait)
attempt += 1

Ograniczenia

  • Rozmiar wiadomości: Maks. 4 000 znaków na wiadomość
  • Limit połączeń: 100 równoczesnych połączeń SSE na IP (plan darmowy)
  • Historia wiadomości: Ostatnie 100 wiadomości w pamięci podręcznej (12 godzin TTL)
  • Limit przeglądarki: 6 równoczesnych połączeń SSE na domenę (ograniczenie przeglądarki)

Rozwiązywanie problemów

Połączenie natychmiast się zamyka

Problem: Połączenie SSE otwiera się i natychmiast zamyka.

Rozwiązania:

  1. Sprawdź token uwierzytelniający (dla prywatnych tematów)
  2. Zweryfikuj, czy temat istnieje
  3. Sprawdź ustawienia sieci/zapory
  4. Przejrzyj konsolę przeglądarki pod kątem błędów

Brak otrzymywanych wiadomości

Problem: Połączono, ale nie otrzymuję wiadomości.

Rozwiązania:

  1. Opublikuj wiadomość testową, aby zweryfikować temat
  2. Sprawdź parametr since_id (może być zbyt niedawny)
  3. Zweryfikuj, że sieć nie buforuje odpowiedzi
  4. Użyj flagi -N z curl (wyłącz buforowanie)

Wysokie zużycie pamięci

Problem: Zakładka przeglądarki zużywa zbyt dużo pamięci.

Rozwiązania:

  1. Ogranicz wyświetlane wiadomości (zachowaj ostatnie 50-100)
  2. Zamknij połączenie gdy zakładka jest ukryta
  3. Okresowo czyszcz stare wiadomości z DOM

Następne kroki


Wskazówka: Używaj SSE dla większości subskrypcji w czasie rzeczywistym - jest proste, niezawodne i działa wszędzie! Używaj WebSocket tylko jeśli potrzebujesz dwukierunkowej komunikacji.