Realtidsdata med Server-Sent Events 📡
Nu ska vi ersätta polling med äkta realtidsdata med Server-Sent Events!
Ditt uppdrag 🎯
Bygg professionella realtidsfunktioner med Server-Sent Events:
- Förstå varför polling är problematiskt för realtidsdata
- Implementera EventSource och ReadableStream
- Skapa live chat med instant meddelanden
- Hantera connections, disconnections och errors
- Bygga scalable real-time architecture
Mål: Ersätta din polling-baserade realtidsdata med äkta live streams som fungerar som Discord eller Slack!
Du har experimenterat med polling tidigare. Nu lär vi oss hur riktiga appar som Discord, Slack och Gmail bygger sina realtidsfunktioner!
Fas 1: Förstå problemen med polling 🧠
Steg 1: Analysera din nuvarande polling
Titta på din befintliga realtids-kod från tidigare modul:
// Din gamla polling-kod
setInterval(async () => {
const response = await fetch('/api/messages?since=' + lastMessageId);
const newMessages = await response.json();
// Uppdatera UI...
}, 1000); // Var sekund!
Kritiska problem att identifiera:
- Resursvärn: Hur många HTTP requests gör du per minut? Per timme?
- Latency: Vad är den maximala fördröjningen innan användaren ser nya data?
- Server complexity: Hur håller servern reda på vad varje klient redan sett?
- Skalbarhetsproblem: Vad händer med 1000 användare som pollar samtidigt?
Räkna ut:
- 10 användare × 60 requests/minut = 600 requests/minut
- 1000 användare × 60 requests/minut = 60,000 requests/minut
- De flesta requests returnerar "inga nya meddelanden"
Steg 2: Jämför med Server-Sent Events
Server-Sent Events (SSE) approach:
// SSE - öppna en enda långvarig connection
const eventSource = new EventSource('/chat-stream');
eventSource.onmessage = (event) => {
const newMessage = JSON.parse(event.data);
// Uppdatera UI direkt när data kommer!
};
Fördelar att fundera på:
- Hur många connections per användare? (1 istället för 3600/timme)
- Vad är latency för nya meddelanden? (Instant!)
- Behöver servern hålla reda på vad klienten sett? (Nej!)
- Vad händer med server load? (Drastiskt reducerad)
Steg 3: Förstå EventSource vs WebSockets
EventSource (SSE):
- Enkelriktad: Server → Client
- Baserat på HTTP
- Automatisk reconnection
- Enklare att implementera
WebSockets:
- Tvåriktad: Server ↔ Client
- Eget protokoll
- Manual reconnection
- Mer komplex setup
Fundera: För vilka use cases är SSE perfekt? När behöver du WebSockets?
Fas 2: Planera din realtids-arkitektur 📋
Steg 1: Designa data flow
Tänk igenom flödet:
- Client connection: Klient öppnar EventSource till
/chat-stream
- Server streaming: Server skapar ReadableStream för denna klient
- Broadcasting: När nytt data kommer, skicka till alla anslutna streams
- Client updates: Klient tar emot data och uppdaterar UI instantly
Arkitektur-frågor:
- Var lagrar du listan över aktiva connections?
- Hur hanterar du när klienter kopplar från?
- Vad händer om servern startas om?
- Hur filtrerar du data per användare/kanal?
Steg 2: Välja data format
SSE format:
data: {"type": "message", "content": "Hello!"}\n\n
data: {"type": "user_joined", "user": "Alice"}\n\n
data: {"type": "typing", "user": "Bob"}\n\n
Designbeslut:
- Ska du skicka bara nya data eller full state?
- Vilka event types behöver du?
- Hur hanterar du olika data structures?
Steg 3: Error handling strategi
Vad kan gå fel:
- Klient tappar internet connection
- Server kraschar eller startas om
- Browser tab stängs
- Network timeouts
Planeringsfrågor:
- Hur detekterar du broken connections?
- Ska du automatiskt reconnecta?
- Vad händer med meddelanden som skickas under disconnection?
- Hur informerar du användaren om connection status?
Fas 3: Implementera SSE server 🔧
Steg 1: Grundläggande SSE endpoint
Skapa src/routes/chat-stream/+server.ts
:
import type { RequestHandler } from './$types';
// Global array för att hålla aktiva streams
// I en riktig app skulle du använda Redis eller liknande
export const activeStreams: ReadableStreamDefaultController[] = [];
export const GET: RequestHandler = async () => {
// Din uppgift: Skapa en ReadableStream
const stream = new ReadableStream({
start(controller) {
// Vad ska hända när stream startar?
// Tips: Lägg till controller i activeStreams array
// Tips: Skicka initial data till ny klient
},
cancel() {
// Vad ska hända när klient kopplar från?
// Tips: Ta bort controller från activeStreams
}
});
// Returnera stream med rätt headers
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
// Eventuella CORS headers om nödvändigt
}
});
};
Frågor att lösa:
- Vad är SSE data format? (
data: ...\n\n
) - Hur konverterar du JavaScript object till SSE format?
- Vad är TextEncoder och varför behöver du den?
Steg 2: Broadcasting mechanism
Skapa broadcast-funktion:
// I samma fil eller separat utility
export function broadcastToAllClients(data: any) {
const encoder = new TextEncoder();
const formattedData = // Din kod: Formatera data som SSE
// Din uppgift: Loopa genom activeStreams och skicka data
// Tips: Använd controller.enqueue()
// Tips: Hantera fel om controller är stängd
}
// Alternativt: Broadcast till specifika användare
export function broadcastToUser(userId: string, data: any) {
// Hur skulle du filtrera streams per användare?
// Tips: Du behöver spara user info tillsammans med controller
}
Steg 3: Integrera med message creation
Uppdatera din message action:
// I din +page.server.ts eller message API
export const actions: Actions = {
sendMessage: async ({ request }) => {
const data = await request.formData();
const message = data.get('message')?.toString();
if (!message) {
return fail(400, { error: 'Message required' });
}
// Spara meddelandet i databas
const newMessage = await prisma.message.create({
data: {
content: message,
userId: /* current user */,
createdAt: new Date()
},
include: {
user: { select: { username: true } }
}
});
// NYTT: Broadcast till alla klienter
broadcastToAllClients({
type: 'new_message',
message: newMessage
});
return { success: true };
}
};
Fas 4: Implementera SSE klient 💻
Steg 1: Basic EventSource connection
I din +page.svelte
:
<script>
import { browser } from '$app/environment';
import { onDestroy } from 'svelte';
let messages = [];
let connectionStatus = 'connecting';
if (browser) {
// Din uppgift: Skapa EventSource connection
const eventSource = new EventSource('/chat-stream');
eventSource.onopen = () => {
// Vad ska hända när connection öppnas?
};
eventSource.onmessage = (event) => {
// Vad ska hända när meddelande tas emot?
// Tips: JSON.parse(event.data)
// Tips: Uppdatera messages array
};
eventSource.onerror = (error) => {
// Vad ska hända vid fel?
// Tips: Uppdatera connectionStatus
};
// Viktigt: Stäng connection när komponenten förstörs
onDestroy(() => {
// Din kod här
});
}
</script>
<div class="chat-container">
<div class="connection-status">
Status: {connectionStatus}
</div>
<div class="messages">
{#each messages as message}
<div class="message">
<strong>{message.user?.username}:</strong>
{message.content}
<small>{new Date(message.createdAt).toLocaleTimeString()}</small>
</div>
{/each}
</div>
<!-- Din befintliga message form -->
</div>
Steg 2: Hantera olika event types
Utöka din onmessage handler:
eventSource.onmessage = (event) => {
const eventData = JSON.parse(event.data);
switch (eventData.type) {
case 'new_message':
// Lägg till nytt meddelande
messages = [...messages, eventData.message];
break;
case 'user_joined':
// Visa notifikation att användare gick med
break;
case 'user_left':
// Visa notifikation att användare lämnade
break;
case 'initial_data':
// Första data-load när connection öppnas
messages = eventData.messages;
break;
default:
console.warn('Unknown event type:', eventData.type);
}
};
Steg 3: Connection status och UX
Förbättra användarupplevelsen:
<script>
let connectionStatus = 'connecting';
let reconnectAttempts = 0;
let maxReconnectAttempts = 5;
function setupEventSource() {
const eventSource = new EventSource('/chat-stream');
eventSource.onopen = () => {
connectionStatus = 'connected';
reconnectAttempts = 0;
};
eventSource.onerror = () => {
connectionStatus = 'disconnected';
if (reconnectAttempts < maxReconnectAttempts) {
connectionStatus = 'reconnecting';
reconnectAttempts++;
// Försök reconnecta efter delay
setTimeout(() => {
eventSource.close();
setupEventSource();
}, 1000 * reconnectAttempts); // Exponential backoff
} else {
connectionStatus = 'failed';
}
};
return eventSource;
}
</script>
<div class="connection-indicator" class:connected={connectionStatus === 'connected'}>
{#if connectionStatus === 'connecting'}
🟡 Connecting...
{:else if connectionStatus === 'connected'}
🟢 Live
{:else if connectionStatus === 'reconnecting'}
🟡 Reconnecting...
{:else}
🔴 Disconnected
{/if}
</div>
Fas 5: Avancerade features 🚀
Steg 1: Typing indicators
Server-side typing broadcast:
export const actions: Actions = {
startTyping: async ({ request }) => {
const data = await request.formData();
const userId = data.get('userId')?.toString();
broadcastToAllClients({
type: 'user_typing',
userId: userId,
timestamp: Date.now()
});
return { success: true };
},
stopTyping: async ({ request }) => {
// Liknande för när användare slutar skriva
}
};
Client-side typing detection:
<script>
let isTyping = false;
let typingTimeout;
function handleInput() {
if (!isTyping) {
isTyping = true;
// Skicka "start typing" event
fetch('/?/startTyping', { method: 'POST', body: new FormData() });
}
// Reset timeout
clearTimeout(typingTimeout);
typingTimeout = setTimeout(() => {
isTyping = false;
// Skicka "stop typing" event
fetch('/?/stopTyping', { method: 'POST', body: new FormData() });
}, 2000);
}
</script>
<input on:input={handleInput} placeholder="Type a message..." />
Steg 2: User presence
Spåra online användare:
// Utöka activeStreams med user info
interface ActiveStream {
controller: ReadableStreamDefaultController;
userId: string;
connectedAt: Date;
}
const activeStreams: ActiveStream[] = [];
export function broadcastUserList() {
const onlineUsers = activeStreams.map(stream => stream.userId);
broadcastToAllClients({
type: 'user_list',
users: onlineUsers
});
}
Steg 3: Channel/Room support
Separata streams för olika channels:
// Organisera streams per channel
const channelStreams = new Map<string, ActiveStream[]>();
export function broadcastToChannel(channelId: string, data: any) {
const streams = channelStreams.get(channelId) || [];
const encoder = new TextEncoder();
const formattedData = `data: ${JSON.stringify(data)}\n\n`;
const encoded = encoder.encode(formattedData);
streams.forEach(stream => {
try {
stream.controller.enqueue(encoded);
} catch (error) {
// Stream är stängd, ta bort den
removeStreamFromChannel(channelId, stream);
}
});
}
Fas 6: Performance och skalning 📈
Steg 1: Memory management
Hantera stream cleanup:
export const GET: RequestHandler = async () => {
const stream = new ReadableStream({
start(controller) {
const streamInfo = {
controller,
userId: /* get from auth */,
connectedAt: new Date()
};
activeStreams.push(streamInfo);
// Heartbeat för att detektera död connections
const heartbeat = setInterval(() => {
try {
controller.enqueue(encoder.encode('data: {"type":"ping"}\n\n'));
} catch {
clearInterval(heartbeat);
removeStream(streamInfo);
}
}, 30000); // 30 sekunder
},
cancel() {
// Cleanup vid disconnect
}
});
};
Steg 2: Rate limiting
Begränsa message frequency:
const userLastMessage = new Map<string, number>();
export const actions: Actions = {
sendMessage: async ({ request }) => {
const userId = /* get current user */;
const lastMessageTime = userLastMessage.get(userId) || 0;
// Rate limit: Max 1 meddelande per sekund
if (Date.now() - lastMessageTime < 1000) {
return fail(429, { error: 'Too many messages' });
}
userLastMessage.set(userId, Date.now());
// ... fortsätt med normal message logic
}
};
Utmaningar att lösa själv 🎯
Grundnivå:
- Basic SSE chat med instant meddelanden ✓
- Connection status indicators ✓
- Automatic reconnection logic ✓
Mellannivå:
- Typing indicators för bättre UX
- Online user list med presence
- Multiple chat rooms/channels
Expertnivå:
- Message history loading on scroll
- File upload med live progress
- Push notifications integration
Troubleshooting guide 🔧
❌ "Connection fails immediately"
Problem: CORS eller headers Lösning: Kontrollera response headers, lägg till CORS om nödvändigt
❌ "Memory leak - too many connections"
Problem: Glömmer cleanup vid disconnect Lösning: Implementera proper cleanup i cancel() och error handlers
❌ "Messages sent to wrong users"
Problem: Delar streams mellan användare Lösning: Associera streams med user IDs, filtrera broadcasts
❌ "Connection drops randomly"
Problem: Proxy/load balancer timeouts Lösning: Implementera heartbeat/ping för keep-alive
Reflektion och lärande 🤔
Efter implementation, reflektera:
Teknisk förståelse:
- Vad är praktiska skillnaderna mellan polling och streaming?
- Hur påverkas server-prestanda med många samtidiga streams?
- Vilka edge cases upptäckte du som du inte tänkt på?
Arkitektur-insikter:
- Hur skulle du skala SSE för 100,000 samtidiga användare?
- Vilka komponenter behöver förbättras för production?
- Vad är trade-offs mellan SSE och WebSockets för din use case?
Användarupplevelse:
- Känns appen mer "live" nu jämfört med polling?
- Vilka UX-förbättringar upptäckte du som möjliga?
- Hur kan du kommunicera connection status till användare?
Grattis! 🎉
Du har nu byggt:
- ✅ Professionell realtids-arkitektur med SSE streams
- ✅ Skalbar broadcasting system
- ✅ Robust error handling och reconnection
- ✅ Production-ready real-time features
Detta är samma teknik som används av:
- GitHub (notifications)
- Twitter (live feeds)
- Financial apps (live prices)
- Chat applications (Discord, Slack)
Steg 4: Lägg till live updates i din app ⚡
Välj lämplig app för realtidsdata
Inte alla appar behöver realtidsdata, men här är naturliga tillämpningar:
Forum-app:
- Live meddelanden i forum (nya meddelanden dyker upp automatiskt)
- "X skrev just nu..." indikator
- Online-användare räknare
Market-app:
- Live bid-uppdateringar (se nya bids direkt)
- "Nytt bud!" notifikationer
- Item status-changes
Character-app:
- Live match-results när andra spelar
- Leaderboard som uppdateras live
- "Senaste aktivitet" feed
Nästa steg 🚀
Nu när du behärskar realtidsdata är nästa (och sista!) modul om mobilappar med Capacitor.js - att ta din webbapp och göra den till en riktig mobilapp!
Med SSE-kunskaper kan du nu bygga live dashboards, realtids-chat, live notifications och andra features som gör appar kännas moderna och responsiva!
Resurser för fördjupning 📚
- MDN Server-Sent Events: developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
- EventSource API: developer.mozilla.org/en-US/docs/Web/API/EventSource
- ReadableStream: developer.mozilla.org/en-US/docs/Web/API/ReadableStream
- SSE vs WebSockets: stackoverflow.com/questions/5195452/websockets-vs-server-sent-events-eventsource