Chapter 14: JavaScript Server-Sent Events - Complete Guide to Real-time Data Push
Server-Sent Events (SSE) is a web standard that enables servers to push data to web browsers in real-time over a single HTTP connection. Unlike WebSocket, SSE is unidirectional (server-to-client only) and is perfect for live updates, notifications, and real-time data streaming.
Why Server-Sent Events is Essential in JavaScript
Server-Sent Events in JavaScript is crucial because it:
- Enables Real-time Updates: Provides instant server-to-client data push
- Simplifies Implementation: Easier to implement than WebSocket for one-way communication
- Supports Automatic Reconnection: Built-in reconnection mechanism
- Works with HTTP: Uses standard HTTP connections, no special protocols
- Enables Live Applications: Perfect for live feeds, notifications, and real-time dashboards
- Reduces Server Load: More efficient than polling for real-time data
Learning Objectives
Through this chapter, you will master:
- EventSource API and basic SSE implementation
- Real-time data streaming and event handling
- Error handling and reconnection strategies
- Advanced SSE patterns and best practices
- Building live applications with SSE
- Performance optimization and security considerations
Basic Server-Sent Events
EventSource API
// Basic EventSource connection
const eventSource = new EventSource('/api/events');
// Handle connection open
eventSource.onopen = function(event) {
console.log('SSE connection opened');
console.log('Ready state:', eventSource.readyState);
};
// Handle incoming messages
eventSource.onmessage = function(event) {
console.log('Received message:', event.data);
console.log('Event type:', event.type);
console.log('Last event ID:', event.lastEventId);
// Parse JSON data if needed
try {
const data = JSON.parse(event.data);
handleMessage(data);
} catch (error) {
console.log('Received text message:', event.data);
}
};
// Handle connection errors
eventSource.onerror = function(event) {
console.error('SSE connection error:', event);
switch (eventSource.readyState) {
case EventSource.CONNECTING:
console.log('Reconnecting...');
break;
case EventSource.CLOSED:
console.log('Connection closed');
break;
}
};
// Close connection
function closeConnection() {
eventSource.close();
}
// Helper function to handle different message types
function handleMessage(data) {
switch (data.type) {
case 'notification':
showNotification(data.message);
break;
case 'update':
updateData(data.payload);
break;
case 'status':
updateStatus(data.status);
break;
default:
console.log('Unknown message type:', data.type);
}
}
Event Types and Custom Events
// EventSource with custom event types
const eventSource = new EventSource('/api/events');
// Handle specific event types
eventSource.addEventListener('user-notification', function(event) {
console.log('User notification:', event.data);
const notification = JSON.parse(event.data);
displayNotification(notification);
});
eventSource.addEventListener('system-update', function(event) {
console.log('System update:', event.data);
const update = JSON.parse(event.data);
applySystemUpdate(update);
});
eventSource.addEventListener('data-stream', function(event) {
console.log('Data stream:', event.data);
const data = JSON.parse(event.data);
updateDataStream(data);
});
eventSource.addEventListener('heartbeat', function(event) {
console.log('Heartbeat received');
updateLastHeartbeat();
});
// Handle all messages (fallback)
eventSource.onmessage = function(event) {
console.log('Default message handler:', event.data);
};
// Display notification
function displayNotification(notification) {
const notificationElement = document.createElement('div');
notificationElement.className = 'notification';
notificationElement.innerHTML = `
<h4>${notification.title}</h4>
<p>${notification.message}</p>
<span class="timestamp">${new Date().toLocaleTimeString()}</span>
`;
document.getElementById('notifications').appendChild(notificationElement);
// Auto-remove after 5 seconds
setTimeout(() => {
notificationElement.remove();
}, 5000);
}
// Update data stream
function updateDataStream(data) {
const dataContainer = document.getElementById('dataStream');
dataContainer.innerHTML = `
<div class="data-item">
<span class="label">Value:</span>
<span class="value">${data.value}</span>
</div>
<div class="data-item">
<span class="label">Timestamp:</span>
<span class="value">${new Date(data.timestamp).toLocaleString()}</span>
</div>
`;
}
Advanced SSE Patterns
SSE Manager Class
class SSEManager {
constructor(url, options = {}) {
this.url = url;
this.options = {
withCredentials: false,
reconnectInterval: 3000,
maxReconnectAttempts: 5,
heartbeatInterval: 30000,
...options
};
this.eventSource = null;
this.reconnectAttempts = 0;
this.heartbeatTimer = null;
this.eventListeners = new Map();
this.isConnected = false;
this.lastEventId = null;
this.connect();
}
connect() {
try {
const url = this.lastEventId ?
`${this.url}?lastEventId=${this.lastEventId}` :
this.url;
this.eventSource = new EventSource(url, {
withCredentials: this.options.withCredentials
});
this.setupEventHandlers();
} catch (error) {
console.error('Failed to create EventSource:', error);
this.handleReconnect();
}
}
setupEventHandlers() {
this.eventSource.onopen = (event) => {
console.log('SSE connection opened');
this.isConnected = true;
this.reconnectAttempts = 0;
this.startHeartbeat();
this.emit('open', event);
};
this.eventSource.onmessage = (event) => {
this.lastEventId = event.lastEventId;
this.emit('message', event);
};
this.eventSource.onerror = (event) => {
console.error('SSE connection error:', event);
this.isConnected = false;
this.stopHeartbeat();
this.emit('error', event);
if (this.eventSource.readyState === EventSource.CLOSED) {
this.handleReconnect();
}
};
}
handleReconnect() {
if (this.reconnectAttempts < this.options.maxReconnectAttempts) {
this.reconnectAttempts++;
console.log(`Reconnecting... (${this.reconnectAttempts}/${this.options.maxReconnectAttempts})`);
setTimeout(() => {
this.connect();
}, this.options.reconnectInterval);
} else {
console.error('Max reconnection attempts reached');
this.emit('maxReconnectAttemptsReached');
}
}
startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
if (!this.isConnected) {
console.log('No heartbeat received, reconnecting...');
this.handleReconnect();
}
}, this.options.heartbeatInterval);
}
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
// Event system
on(event, callback) {
if (!this.eventListeners.has(event)) {
this.eventListeners.set(event, []);
}
this.eventListeners.get(event).push(callback);
}
off(event, callback) {
if (this.eventListeners.has(event)) {
const listeners = this.eventListeners.get(event);
const index = listeners.indexOf(callback);
if (index > -1) {
listeners.splice(index, 1);
}
}
}
emit(event, data) {
if (this.eventListeners.has(event)) {
this.eventListeners.get(event).forEach(callback => {
callback(data);
});
}
}
// Add custom event listener
addEventListener(eventType, callback) {
if (this.eventSource) {
this.eventSource.addEventListener(eventType, callback);
}
}
close() {
this.stopHeartbeat();
if (this.eventSource) {
this.eventSource.close();
}
}
}
// Usage
const sseManager = new SSEManager('/api/events');
sseManager.on('open', () => {
console.log('Connected to SSE stream');
});
sseManager.on('message', (event) => {
console.log('Received message:', event.data);
});
sseManager.on('error', (error) => {
console.error('SSE error:', error);
});
sseManager.addEventListener('notification', (event) => {
const data = JSON.parse(event.data);
showNotification(data);
});
Real-time Dashboard
class RealTimeDashboard {
constructor() {
this.sseManager = null;
this.data = {
users: 0,
orders: 0,
revenue: 0,
alerts: []
};
this.initializeDashboard();
}
async initializeDashboard() {
await this.setupSSE();
this.setupUI();
this.startPeriodicUpdates();
}
async setupSSE() {
this.sseManager = new SSEManager('/api/dashboard/events');
this.sseManager.on('open', () => {
this.updateConnectionStatus('connected');
});
this.sseManager.on('error', () => {
this.updateConnectionStatus('disconnected');
});
this.sseManager.on('message', (event) => {
this.handleDashboardUpdate(event);
});
// Handle specific event types
this.sseManager.addEventListener('user-count', (event) => {
const data = JSON.parse(event.data);
this.updateUserCount(data.count);
});
this.sseManager.addEventListener('order-update', (event) => {
const data = JSON.parse(event.data);
this.updateOrderCount(data.orders);
});
this.sseManager.addEventListener('revenue-update', (event) => {
const data = JSON.parse(event.data);
this.updateRevenue(data.revenue);
});
this.sseManager.addEventListener('alert', (event) => {
const data = JSON.parse(event.data);
this.addAlert(data);
});
}
setupUI() {
// Setup UI elements
this.userCountElement = document.getElementById('userCount');
this.orderCountElement = document.getElementById('orderCount');
this.revenueElement = document.getElementById('revenue');
this.alertsContainer = document.getElementById('alerts');
this.connectionStatus = document.getElementById('connectionStatus');
}
handleDashboardUpdate(event) {
try {
const data = JSON.parse(event.data);
switch (data.type) {
case 'dashboard-update':
this.updateDashboard(data.payload);
break;
case 'metric-update':
this.updateMetric(data.metric, data.value);
break;
case 'alert':
this.addAlert(data.alert);
break;
}
} catch (error) {
console.error('Error parsing dashboard update:', error);
}
}
updateDashboard(payload) {
this.data = { ...this.data, ...payload };
this.renderDashboard();
}
updateMetric(metric, value) {
this.data[metric] = value;
this.updateMetricDisplay(metric, value);
}
updateUserCount(count) {
this.data.users = count;
this.userCountElement.textContent = count.toLocaleString();
this.animateValueChange(this.userCountElement);
}
updateOrderCount(orders) {
this.data.orders = orders;
this.orderCountElement.textContent = orders.toLocaleString();
this.animateValueChange(this.orderCountElement);
}
updateRevenue(revenue) {
this.data.revenue = revenue;
this.revenueElement.textContent = `$${revenue.toLocaleString()}`;
this.animateValueChange(this.revenueElement);
}
addAlert(alert) {
this.data.alerts.unshift(alert);
// Keep only last 10 alerts
if (this.data.alerts.length > 10) {
this.data.alerts = this.data.alerts.slice(0, 10);
}
this.renderAlerts();
}
renderAlerts() {
this.alertsContainer.innerHTML = '';
this.data.alerts.forEach(alert => {
const alertElement = document.createElement('div');
alertElement.className = `alert alert-${alert.level}`;
alertElement.innerHTML = `
<div class="alert-content">
<h4>${alert.title}</h4>
<p>${alert.message}</p>
<span class="alert-time">${new Date(alert.timestamp).toLocaleTimeString()}</span>
</div>
<button class="alert-close" onclick="this.parentElement.remove()">×</button>
`;
this.alertsContainer.appendChild(alertElement);
});
}
updateConnectionStatus(status) {
this.connectionStatus.textContent = status;
this.connectionStatus.className = `status status-${status}`;
}
animateValueChange(element) {
element.classList.add('value-updated');
setTimeout(() => {
element.classList.remove('value-updated');
}, 1000);
}
startPeriodicUpdates() {
// Update timestamps every minute
setInterval(() => {
this.updateTimestamps();
}, 60000);
}
updateTimestamps() {
const timestampElements = document.querySelectorAll('.timestamp');
timestampElements.forEach(element => {
const timestamp = new Date(element.dataset.timestamp);
element.textContent = timestamp.toLocaleTimeString();
});
}
}
// Initialize dashboard
const dashboard = new RealTimeDashboard();
Live Notifications System
Real-time Notification Manager
class NotificationManager {
constructor() {
this.sseManager = null;
this.notifications = [];
this.maxNotifications = 50;
this.notificationContainer = null;
this.initialize();
}
async initialize() {
this.setupUI();
await this.setupSSE();
this.setupNotificationHandlers();
}
setupUI() {
this.notificationContainer = document.getElementById('notificationContainer');
// Create notification bell icon
const bellIcon = document.createElement('div');
bellIcon.className = 'notification-bell';
bellIcon.innerHTML = '🔔';
bellIcon.addEventListener('click', () => {
this.toggleNotificationPanel();
});
document.body.appendChild(bellIcon);
// Create notification panel
const panel = document.createElement('div');
panel.id = 'notificationPanel';
panel.className = 'notification-panel hidden';
panel.innerHTML = `
<div class="notification-header">
<h3>Notifications</h3>
<button class="clear-all" onclick="notificationManager.clearAll()">Clear All</button>
</div>
<div class="notification-list" id="notificationList"></div>
`;
document.body.appendChild(panel);
}
async setupSSE() {
this.sseManager = new SSEManager('/api/notifications/stream');
this.sseManager.on('open', () => {
console.log('Notification stream connected');
});
this.sseManager.on('error', (error) => {
console.error('Notification stream error:', error);
});
this.sseManager.on('message', (event) => {
this.handleNotification(event);
});
// Handle specific notification types
this.sseManager.addEventListener('user-notification', (event) => {
const notification = JSON.parse(event.data);
this.addNotification(notification);
});
this.sseManager.addEventListener('system-notification', (event) => {
const notification = JSON.parse(event.data);
this.addNotification(notification);
});
this.sseManager.addEventListener('alert-notification', (event) => {
const notification = JSON.parse(event.data);
this.addNotification(notification);
});
}
setupNotificationHandlers() {
// Handle browser notifications
if ('Notification' in window) {
Notification.requestPermission();
}
// Handle page visibility changes
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.pauseNotifications();
} else {
this.resumeNotifications();
}
});
}
handleNotification(event) {
try {
const notification = JSON.parse(event.data);
this.addNotification(notification);
} catch (error) {
console.error('Error parsing notification:', error);
}
}
addNotification(notification) {
// Add to notifications array
this.notifications.unshift({
...notification,
id: Date.now(),
timestamp: new Date(),
read: false
});
// Keep only max notifications
if (this.notifications.length > this.maxNotifications) {
this.notifications = this.notifications.slice(0, this.maxNotifications);
}
// Display notification
this.displayNotification(notification);
// Show browser notification if permission granted
this.showBrowserNotification(notification);
// Update notification count
this.updateNotificationCount();
}
displayNotification(notification) {
const notificationElement = document.createElement('div');
notificationElement.className = `notification-item ${notification.type}`;
notificationElement.innerHTML = `
<div class="notification-content">
<div class="notification-icon">${this.getNotificationIcon(notification.type)}</div>
<div class="notification-text">
<h4>${notification.title}</h4>
<p>${notification.message}</p>
<span class="notification-time">${new Date().toLocaleTimeString()}</span>
</div>
</div>
<button class="notification-close" onclick="this.parentElement.remove()">×</button>
`;
// Add click handler
notificationElement.addEventListener('click', () => {
this.handleNotificationClick(notification);
});
// Insert at the beginning of the list
const notificationList = document.getElementById('notificationList');
notificationList.insertBefore(notificationElement, notificationList.firstChild);
// Auto-remove after 10 seconds
setTimeout(() => {
if (notificationElement.parentNode) {
notificationElement.remove();
}
}, 10000);
}
showBrowserNotification(notification) {
if ('Notification' in window && Notification.permission === 'granted') {
const browserNotification = new Notification(notification.title, {
body: notification.message,
icon: '/icons/notification-icon.png',
tag: notification.id
});
browserNotification.onclick = () => {
this.handleNotificationClick(notification);
browserNotification.close();
};
// Auto-close after 5 seconds
setTimeout(() => {
browserNotification.close();
}, 5000);
}
}
handleNotificationClick(notification) {
// Mark as read
const notificationItem = this.notifications.find(n => n.id === notification.id);
if (notificationItem) {
notificationItem.read = true;
}
// Handle notification action
if (notification.action) {
switch (notification.action.type) {
case 'navigate':
window.location.href = notification.action.url;
break;
case 'open-modal':
this.openModal(notification.action.modalId);
break;
case 'execute-function':
if (window[notification.action.functionName]) {
window[notification.action.functionName](notification.action.params);
}
break;
}
}
this.updateNotificationCount();
}
getNotificationIcon(type) {
const icons = {
'info': 'ℹ️',
'success': '✅',
'warning': '⚠️',
'error': '❌',
'user': '👤',
'system': '⚙️',
'alert': '🚨'
};
return icons[type] || '📢';
}
updateNotificationCount() {
const unreadCount = this.notifications.filter(n => !n.read).length;
const bellIcon = document.querySelector('.notification-bell');
if (unreadCount > 0) {
bellIcon.classList.add('has-notifications');
bellIcon.setAttribute('data-count', unreadCount);
} else {
bellIcon.classList.remove('has-notifications');
bellIcon.removeAttribute('data-count');
}
}
toggleNotificationPanel() {
const panel = document.getElementById('notificationPanel');
panel.classList.toggle('hidden');
if (!panel.classList.contains('hidden')) {
this.renderNotificationList();
}
}
renderNotificationList() {
const notificationList = document.getElementById('notificationList');
notificationList.innerHTML = '';
this.notifications.forEach(notification => {
const notificationElement = document.createElement('div');
notificationElement.className = `notification-item ${notification.type} ${notification.read ? 'read' : 'unread'}`;
notificationElement.innerHTML = `
<div class="notification-content">
<div class="notification-icon">${this.getNotificationIcon(notification.type)}</div>
<div class="notification-text">
<h4>${notification.title}</h4>
<p>${notification.message}</p>
<span class="notification-time">${notification.timestamp.toLocaleTimeString()}</span>
</div>
</div>
<button class="notification-close" onclick="this.parentElement.remove()">×</button>
`;
notificationElement.addEventListener('click', () => {
this.handleNotificationClick(notification);
});
notificationList.appendChild(notificationElement);
});
}
clearAll() {
this.notifications = [];
this.renderNotificationList();
this.updateNotificationCount();
}
pauseNotifications() {
// Pause SSE connection when page is hidden
if (this.sseManager) {
this.sseManager.close();
}
}
resumeNotifications() {
// Resume SSE connection when page becomes visible
if (this.sseManager) {
this.sseManager.connect();
}
}
}
// Initialize notification manager
const notificationManager = new NotificationManager();
Performance Optimization
Efficient SSE Handling
class OptimizedSSE {
constructor(url, options = {}) {
this.url = url;
this.options = {
bufferSize: 100,
flushInterval: 1000,
...options
};
this.eventSource = null;
this.messageBuffer = [];
this.flushTimer = null;
this.messageHandlers = new Map();
this.connect();
}
connect() {
this.eventSource = new EventSource(this.url);
this.setupEventHandlers();
}
setupEventHandlers() {
this.eventSource.onmessage = (event) => {
this.bufferMessage(event);
};
this.eventSource.onerror = (event) => {
console.error('SSE error:', event);
};
}
bufferMessage(event) {
this.messageBuffer.push(event);
// Flush buffer if it's full
if (this.messageBuffer.length >= this.options.bufferSize) {
this.flushBuffer();
}
// Schedule flush if not already scheduled
if (!this.flushTimer) {
this.flushTimer = setTimeout(() => {
this.flushBuffer();
}, this.options.flushInterval);
}
}
flushBuffer() {
if (this.messageBuffer.length === 0) {
return;
}
// Process all buffered messages
this.messageBuffer.forEach(event => {
this.processMessage(event);
});
// Clear buffer
this.messageBuffer = [];
// Clear timer
if (this.flushTimer) {
clearTimeout(this.flushTimer);
this.flushTimer = null;
}
}
processMessage(event) {
try {
const data = JSON.parse(event.data);
// Route to appropriate handler
if (this.messageHandlers.has(data.type)) {
const handler = this.messageHandlers.get(data.type);
handler(data);
} else {
// Default handler
console.log('Unhandled message type:', data.type);
}
} catch (error) {
console.error('Error processing message:', error);
}
}
onMessageType(type, handler) {
this.messageHandlers.set(type, handler);
}
close() {
if (this.flushTimer) {
clearTimeout(this.flushTimer);
}
if (this.eventSource) {
this.eventSource.close();
}
}
}
// Usage
const optimizedSSE = new OptimizedSSE('/api/events');
optimizedSSE.onMessageType('update', (data) => {
updateUI(data);
});
optimizedSSE.onMessageType('notification', (data) => {
showNotification(data);
});
Best Practices
1. Error Handling and Reconnection
// Good: Robust error handling
class RobustSSE {
constructor(url) {
this.url = url;
this.eventSource = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectInterval = 3000;
}
connect() {
try {
this.eventSource = new EventSource(this.url);
this.setupEventHandlers();
} catch (error) {
console.error('Failed to create EventSource:', error);
this.handleReconnect();
}
}
handleReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
setTimeout(() => {
this.connect();
}, this.reconnectInterval);
} else {
console.error('Max reconnection attempts reached');
}
}
setupEventHandlers() {
this.eventSource.onopen = () => {
this.reconnectAttempts = 0;
};
this.eventSource.onerror = (event) => {
if (this.eventSource.readyState === EventSource.CLOSED) {
this.handleReconnect();
}
};
}
}
// Avoid: No error handling
function badSSE(url) {
const eventSource = new EventSource(url);
// No error handling - bad practice
return eventSource;
}
2. Resource Management
// Good: Proper resource cleanup
class SSEResourceManager {
constructor() {
this.connections = [];
}
createConnection(url) {
const eventSource = new EventSource(url);
this.connections.push(eventSource);
return eventSource;
}
closeAll() {
this.connections.forEach(connection => {
connection.close();
});
this.connections = [];
}
closeConnection(eventSource) {
const index = this.connections.indexOf(eventSource);
if (index > -1) {
this.connections.splice(index, 1);
}
eventSource.close();
}
}
// Avoid: Memory leaks
function badResourceManagement() {
const eventSource = new EventSource('/api/events');
// No cleanup - memory leak
}
3. Security Considerations
// Good: Secure SSE implementation
class SecureSSE {
constructor(url, options = {}) {
this.url = url;
this.options = {
withCredentials: false,
...options
};
this.validateUrl();
this.createSecureConnection();
}
validateUrl() {
const url = new URL(this.url);
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
throw new Error('Invalid protocol');
}
}
createSecureConnection() {
this.eventSource = new EventSource(this.url, {
withCredentials: this.options.withCredentials
});
}
}
Summary
Server-Sent Events are essential for real-time server-to-client communication:
- EventSource API: Use for establishing SSE connections
- Event Handling: Handle different event types and messages
- Error Handling: Implement robust error handling and reconnection
- Performance: Use buffering and optimization techniques
- Security: Follow security best practices for SSE
- Resource Management: Properly manage connections and cleanup
- Real-time Applications: Build live dashboards, notifications, and updates
Mastering Server-Sent Events enables you to build responsive, real-time applications that provide excellent user experiences through instant server-to-client data push.
This tutorial is part of the JavaScript Mastery series by syscook.dev