Hoppa till huvudinnehåll

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!

Från polling till streaming! 🚀

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:

  1. Resursvärn: Hur många HTTP requests gör du per minut? Per timme?
  2. Latency: Vad är den maximala fördröjningen innan användaren ser nya data?
  3. Server complexity: Hur håller servern reda på vad varje klient redan sett?
  4. 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:

  1. Client connection: Klient öppnar EventSource till /chat-stream
  2. Server streaming: Server skapar ReadableStream för denna klient
  3. Broadcasting: När nytt data kommer, skicka till alla anslutna streams
  4. 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å:

  1. Basic SSE chat med instant meddelanden ✓
  2. Connection status indicators ✓
  3. Automatic reconnection logic ✓

Mellannivå:

  1. Typing indicators för bättre UX
  2. Online user list med presence
  3. Multiple chat rooms/channels

Expertnivå:

  1. Message history loading on scroll
  2. File upload med live progress
  3. 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!

Du kan bygga live applications! 📱

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 📚