Real-time Features
Quick reference for real-time communication patterns in LARC applications. For detailed tutorials, see Learning LARC Chapter 11.
Overview
Real-time features enable live data updates without page reloads using WebSockets, Server-Sent Events (SSE), BroadcastChannel for cross-tab sync, and Web Workers for background processing.
Key Concepts:- WebSockets: Full-duplex bidirectional communication
- SSE: One-way server-to-client streaming
- BroadcastChannel: Cross-tab/window messaging
- Web Workers: Background thread processing
- Heartbeat: Keep-alive mechanism to detect disconnections
- Reconnection: Automatic recovery from connection failures
Quick Example
// WebSocket connection
import { wsClient } from './services/websocket-client.js';
await wsClient.connect();
// Subscribe to events
wsClient.on('notification', (data) => {
console.log('New notification:', data);
});
// Send message
wsClient.send('chat.message', { text: 'Hello!' });
// Check connection
if (wsClient.isConnected()) {
// Connected
}
WebSocket Client API
| Method | Parameters | Returns | Description |
|--------|-----------|---------|-------------|
| connect() | - | Promise\disconnect() | - | void | Close connection |
| send(type, payload) | type: string, payload: any | void | Send message to server |
| on(type, handler) | type: string, handler: Function | Function | Subscribe to message type (returns unsubscribe) |
| isConnected() | - | boolean | Check connection status |
WebSocket Configuration
class WebSocketClient {
constructor(config) {
this.config = {
url: 'ws://localhost:3000/ws',
reconnectInterval: 5000, // Delay between reconnect attempts
maxReconnectAttempts: 10, // Max reconnection attempts
heartbeatInterval: 30000, // Heartbeat ping interval
...config
};
}
}
WebSocket Connection Pattern
const wsClient = new WebSocketClient({
url: 'ws://localhost:3000/ws'
});
// Connection lifecycle
wsClient.on('connected', () => {
console.log('Connected');
});
wsClient.on('disconnected', ({ code, reason }) => {
console.log('Disconnected:', code, reason);
});
wsClient.on('error', ({ error }) => {
console.error('WebSocket error:', error);
});
wsClient.on('reconnect-failed', () => {
console.error('Reconnection failed');
});
await wsClient.connect();
Server-Sent Events (SSE)
SSE provides one-way server-to-client streaming over HTTP. Simpler than WebSockets, automatic reconnection, works with existing HTTP infrastructure.
SSE Client API
| Method | Parameters | Returns | Description |
|--------|-----------|---------|-------------|
| connect() | - | void | Connect to SSE endpoint |
| disconnect() | - | void | Close connection |
| on(eventName, handler) | eventName: string, handler: Function | Function | Subscribe to named events |
| isConnected() | - | boolean | Check connection status |
SSE Usage Pattern
import { SSEClient } from './services/sse-client.js';
const sseClient = new SSEClient({
url: '/api/events',
withCredentials: true,
reconnectDelay: 3000
});
sseClient.connect();
// Subscribe to named events
sseClient.on('activity', (data) => {
console.log('Activity:', data);
});
sseClient.on('notification', (data) => {
console.log('Notification:', data);
});
// Connection events
sseClient.on('connected', () => console.log('SSE connected'));
sseClient.on('disconnected', () => console.log('SSE disconnected'));
BroadcastChannel: Cross-Tab Communication
Synchronize state across browser tabs and windows.
BroadcastChannel API
class TabSyncService {
constructor(channelName = 'app-sync') {
this.channel = new BroadcastChannel(channelName);
this.tabId = this.generateTabId();
this.channel.onmessage = (event) => {
this.handleMessage(event.data);
};
}
broadcast(type, payload) {
this.channel.postMessage({
type,
payload,
timestamp: Date.now(),
tabId: this.tabId
});
}
on(type, handler) {
// Subscribe to message type
}
}
const tabSync = new TabSyncService();
Cross-Tab Sync Patterns
// Sync authentication state
tabSync.on('auth-logout', () => {
authService.logout();
window.location.href = '/login';
});
tabSync.on('auth-login', (user) => {
window.location.reload();
});
// Broadcast logout to other tabs
tabSync.broadcast('auth-logout', {});
// Sync data changes
tabSync.on('data-updated', ({ resource, id }) => {
// Refresh data in this tab
reloadResource(resource, id);
});
Web Workers: Background Processing
Run JavaScript in background threads without blocking UI.
Worker Creation
// worker.js
self.onmessage = (event) => {
const { id, type, data, options } = event.data;
try {
let result;
switch (type) {
case 'sort':
result = sortData(data, options);
break;
case 'filter':
result = filterData(data, options);
break;
case 'aggregate':
result = aggregateData(data, options);
break;
}
self.postMessage({ id, result });
} catch (error) {
self.postMessage({ id, result: null, error: error.message });
}
};
function sortData(data, options) {
const { field, order = 'asc' } = options;
return [...data].sort((a, b) => {
const comparison = a[field] < b[field] ? -1 : a[field] > b[field] ? 1 : 0;
return order === 'asc' ? comparison : -comparison;
});
}
Worker Manager
class WorkerManager {
constructor(workerUrl) {
this.worker = new Worker(workerUrl);
this.requestId = 0;
this.pendingRequests = new Map();
this.worker.onmessage = (event) => {
const { id, result, error } = event.data;
const pending = this.pendingRequests.get(id);
if (pending) {
this.pendingRequests.delete(id);
error ? pending.reject(new Error(error)) : pending.resolve(result);
}
};
}
async process(type, data, options) {
const id = `req-${++this.requestId}`;
return new Promise((resolve, reject) => {
this.pendingRequests.set(id, { resolve, reject });
this.worker.postMessage({ id, type, data, options });
setTimeout(() => {
if (this.pendingRequests.has(id)) {
this.pendingRequests.delete(id);
reject(new Error('Request timeout'));
}
}, 30000);
});
}
terminate() {
this.worker.terminate();
this.pendingRequests.forEach(({ reject }) => {
reject(new Error('Worker terminated'));
});
this.pendingRequests.clear();
}
}
const dataWorker = new WorkerManager('/workers/data-processor.js');
// Use worker
const sorted = await dataWorker.process('sort', data, {
field: 'name',
order: 'asc'
});
Real-time Component Patterns
Notification Feed
class NotificationFeed extends HTMLElement {
connectedCallback() {
this.notifications = [];
wsClient.on('notification', (notification) => {
this.notifications.unshift(notification);
this.render();
// Auto-dismiss after 5s
setTimeout(() => {
this.removeNotification(notification.id);
}, 5000);
});
this.render();
}
render() {
this.innerHTML = `
<div class="notifications">
${this.notifications.map(n => `
<div class="notification ${n.type}">
<strong>${n.title}</strong>
<p>${n.message}</p>
</div>
`).join('')}
</div>
`;
}
}
customElements.define('notification-feed', NotificationFeed);
Live Activity Feed
class ActivityFeed extends HTMLElement {
async connectedCallback() {
this.activities = [];
// Load initial data
const response = await fetch('/api/activities');
this.activities = await response.json();
// Subscribe to live updates
sseClient.on('activity', (activity) => {
this.activities.unshift(activity);
this.activities = this.activities.slice(0, 100); // Limit
this.render();
});
this.render();
}
render() {
this.innerHTML = `
<div class="activities">
${this.activities.map(a => `
<div class="activity">
<strong>${a.userName}</strong> ${a.action} <em>${a.target}</em>
<span class="time">${this.formatTime(a.timestamp)}</span>
</div>
`).join('')}
</div>
`;
}
formatTime(timestamp) {
const diff = Date.now() - timestamp;
if (diff < 60000) return 'Just now';
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
return new Date(timestamp).toLocaleString();
}
}
customElements.define('activity-feed', ActivityFeed);
Collaborative Editor
class CollaborativeEditor extends HTMLElement {
connectedCallback() {
this.docId = this.getAttribute('doc-id');
this.content = '';
this.collaborators = new Map();
// Join document
wsClient.send('join-document', { docId: this.docId });
// Subscribe to updates
wsClient.on('document-update', (update) => {
if (!this.isLocalUpdate) {
this.content = update.content;
this.render();
}
});
wsClient.on('collaborator-joined', (collab) => {
this.collaborators.set(collab.userId, collab);
this.render();
});
wsClient.on('collaborator-left', ({ userId }) => {
this.collaborators.delete(userId);
this.render();
});
this.render();
}
handleInput(e) {
this.content = e.target.value;
this.isLocalUpdate = true;
wsClient.send('document-update', {
docId: this.docId,
content: this.content
});
setTimeout(() => { this.isLocalUpdate = false; }, 100);
}
render() {
this.innerHTML = `
<div class="editor">
<div class="collaborators">
${Array.from(this.collaborators.values()).map(c => `
<div class="badge">${c.name[0]}</div>
`).join('')}
</div>
<textarea>${this.content}</textarea>
</div>
`;
this.querySelector('textarea').addEventListener('input', (e) => this.handleInput(e));
}
}
customElements.define('collaborative-editor', CollaborativeEditor);
Component Reference
See Chapter 20 for real-time components:
- pan-websocket: WebSocket connection management
- pan-sse: Server-Sent Events integration
- pan-live-data: Auto-refreshing data display
Advanced Patterns
Optimistic Updates
class OptimisticList extends HTMLElement {
async addItem(item) {
// Add to UI immediately (optimistic)
this.items.push({ ...item, pending: true });
this.render();
try {
// Send to server
const response = await fetch('/api/items', {
method: 'POST',
body: JSON.stringify(item)
});
const serverItem = await response.json();
// Replace pending with server version
const index = this.items.findIndex(i => i.pending);
this.items[index] = serverItem;
this.render();
} catch (err) {
// Rollback on error
this.items = this.items.filter(i => !i.pending);
this.render();
alert('Failed to add item');
}
}
}
Presence Tracking
class PresenceTracker {
constructor() {
this.users = new Map();
wsClient.on('presence-update', ({ userId, status }) => {
this.users.set(userId, { userId, status, lastSeen: Date.now() });
this.notifySubscribers();
});
wsClient.on('presence-offline', ({ userId }) => {
this.users.delete(userId);
this.notifySubscribers();
});
// Send heartbeat
setInterval(() => {
wsClient.send('presence-heartbeat', {});
}, 30000);
}
getOnlineUsers() {
return Array.from(this.users.values()).filter(u => u.status === 'online');
}
}
Conflict Resolution
class ConflictResolver {
async handleUpdate(localVersion, remoteVersion) {
if (localVersion.timestamp > remoteVersion.timestamp) {
// Local wins - send to server
await this.sendUpdate(localVersion);
} else if (localVersion.timestamp < remoteVersion.timestamp) {
// Remote wins - apply locally
this.applyUpdate(remoteVersion);
} else {
// Timestamps equal - merge
const merged = this.merge(localVersion, remoteVersion);
this.applyUpdate(merged);
}
}
merge(local, remote) {
// Last-write-wins per field
return {
...remote,
...Object.entries(local).reduce((acc, [key, value]) => {
if (local[`${key}_timestamp`] > remote[`${key}_timestamp`]) {
acc[key] = value;
}
return acc;
}, {})
};
}
}
Performance Considerations
| Strategy | Use Case | Implementation | |----------|----------|----------------| | Throttling | High-frequency events (scroll, mousemove) | Send update max once per 100-500ms | | Debouncing | Text input | Send update after 300ms of inactivity | | Batching | Multiple updates | Accumulate and send in single message | | Compression | Large payloads | Use MessagePack or gzip compression | | Selective sync | Large datasets | Only sync visible/relevant data |
Throttle Example
class ThrottledInput extends HTMLElement {
connectedCallback() {
let lastSent = 0;
const throttleMs = 300;
this.querySelector('input').addEventListener('input', (e) => {
const now = Date.now();
if (now - lastSent >= throttleMs) {
wsClient.send('input-update', { value: e.target.value });
lastSent = now;
}
});
}
}
Cross-References
- Tutorial: Learning LARC Chapter 11 (Real-time Features)
- Components: Chapter 20 (pan-websocket, pan-sse, pan-live-data)
- Patterns: Appendix E (Real-time Patterns)
- Related: Chapter 7 (API Integration), Chapter 12 (Performance)
Common Issues
Issue: Connection drops on mobile/sleep
Problem: WebSocket closes when device sleeps Solution: Implement heartbeat + reconnection; use SSE for mobileIssue: Message order not guaranteed
Problem: Messages arrive out of sequence Solution: Add sequence numbers; buffer and reorder on clientIssue: Memory leaks from event listeners
Problem: Component unmounts but listeners remain Solution: Always unsubscribe indisconnectedCallback()
Issue: Duplicate messages on reconnect
Problem: Server resends messages after reconnection Solution: Track message IDs; deduplicate on clientIssue: High bandwidth usage
Problem: Too many messages or large payloads Solution: Throttle updates; compress payloads; batch messagesSee Learning LARC Chapter 11 for complete real-time patterns, WebSocket authentication, and scaling strategies.