Przejdź do treś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": 5,
  "tags": ["critical", "server"],
  "created_at": "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 - created_at - 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

Kod Znaczenie Akcja
200 Sukces Połączenie ustanowione
401 Brak autoryzacji Sprawdź token uwierzytelniający
404 Temat nie znaleziony Zweryfikuj nazwę tematu
403 Zabronione Sprawdź uprawnienia dostępu do tematu
429 Ograniczenie 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 >= 4) {
      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.created_at).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['created_at'].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=4)

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

Funkcja SSE WebSocket
Kierunek Tylko serwer → klient Dwukierunkowe
Protokół HTTP WS/WSS
Ponowne łączenie Automatyczne Ręczne
Przypadek użycia Aktualizacje w czasie rzeczywistym Czat, gry
Złożoność Proste Bardziej złożone
Wsparcie przeglądarek Wszystkie nowoczesne przeglądarki Wszystkie 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.