Skip to main content

Live Updates Service

A flexible, configuration-driven real-time update service that supports both Server-Sent Events (SSE) and WebSocket transports. The service is transport-agnostic and works with any authentication provider.

Overview

The Live Updates service provides:

  • Dual Transport Support: Automatic fallback from SSE to WebSocket
  • Pluggable Authentication: Works with any auth provider via the IAuthProvider interface
  • Flexible Event System: Define your own event types and action names per project
  • Configuration-Driven: No hardcoded business logic or event mappings
  • React Integration: Hooks and context provider for seamless React integration
  • Automatic Reconnection: Built-in reconnection logic with exponential backoff

Architecture

Core Services

LiveUpdateService

The main service that coordinates SSE and WebSocket services:

interface LiveUpdateConfig {
sseEnabled: boolean;
wsEnabled: boolean;
authProvider: IAuthProvider;
eventMappings?: Record<string, EventMapping>;
}

interface EventMapping {
sseEvent?: SSEEventType;
wsAction?: WS_ACTIONS;
}

SSEService

Server-Sent Events implementation with pluggable auth:

interface SSEConfig {
url: string;
authProvider: IAuthProvider;
headerName?: string; // Default: 'Authorization'
headerPrefix?: string; // Default: 'Bearer'
}

export type SSEEventType = string; // Flexible - define your own event types

WebsocketService

WebSocket implementation with pluggable auth:

interface WebsocketConfig {
url: string;
authProvider: IAuthProvider;
}

export type WS_ACTIONS = string; // Flexible - define your own action types

Installation

npm install @codella-software/utils

Setup

Core Service Usage

1. Initialize with Configuration

import { LiveUpdateService, type LiveUpdateConfig } from '@codella-software/utils';
import { firebaseAuthProvider } from './auth-provider'; // Your IAuthProvider

const config: LiveUpdateConfig = {
sseEnabled: true,
wsEnabled: true,
authProvider: firebaseAuthProvider,
eventMappings: {
notifications: {
sseEvent: 'GET_NOTIFICATIONS',
wsAction: 'NOTIFICATIONS_RESPONSE'
},
userUpdates: {
sseEvent: 'UPDATE_USER',
wsAction: 'UPDATE_USER'
},
invoices: {
sseEvent: 'INVOICE_FOR_VERIFICATION_RESPONSE',
wsAction: 'INVOICE_FOR_VERIFICATION_RESPONSE'
}
}
};

const liveUpdateService = new LiveUpdateService(config);

2. Connect to Server

await liveUpdateService.initialize();

3. Listen to Events

// Listen to specific event by name
liveUpdateService.on('notifications').subscribe((data) => {
console.log('Notification received:', data);
});

// Send WebSocket request
liveUpdateService.send('GET_NOTIFICATIONS', { filter: 'unread' });

// Check connection status
liveUpdateService.onConnectionStatusChange().subscribe((isConnected) => {
console.log('Connected:', isConnected);
});

// Cleanup
liveUpdateService.destroy();

React Integration

1. Wrap Application with Provider

import { LiveUpdateProvider } from '@codella-software/react';
import { firebaseAuthProvider } from './auth-provider';

const App = () => {
return (
<LiveUpdateProvider
config={{
sseEnabled: true,
wsEnabled: true,
authProvider: firebaseAuthProvider,
eventMappings: {
notifications: {
sseEvent: 'GET_NOTIFICATIONS',
wsAction: 'NOTIFICATIONS_RESPONSE'
},
userUpdates: {
sseEvent: 'UPDATE_USER',
wsAction: 'UPDATE_USER'
}
}
}}
>
<YourApp />
</LiveUpdateProvider>
);
};

2. Use React Hooks

useLiveUpdates

Main hook for accessing the service and connection status:

function NotificationCenter() {
const { service, isConnected, error } = useLiveUpdates();

return (
<div>
<status>{isConnected ? 'Connected' : 'Disconnected'}</status>
{error && <error>Connection error: {error.message}</error>}
<button onClick={() => service?.send('GET_NOTIFICATIONS')}>
Refresh
</button>
</div>
);
}
useLiveListener

Listen to specific events:

function NotificationsList() {
const [notifications, setNotifications] = useState([]);

useLiveListener('notifications', (data) => {
setNotifications(prev => [...prev, data]);
});

return (
<ul>
{notifications.map(n => <li key={n.id}>{n.message}</li>)}
</ul>
);
}
useLiveRequest

Request-response pattern with promise-based API:

function FetchInvoices() {
const [invoices, setInvoices] = useState(null);
const { request, loading, error } = useLiveRequest();

const handleFetch = async () => {
const data = await request('invoices', { status: 'pending' });
setInvoices(data);
};

return (
<div>
<button onClick={handleFetch} disabled={loading}>
{loading ? 'Loading...' : 'Fetch Invoices'}
</button>
{error && <error>{error.message}</error>}
{invoices && <InvoicesList data={invoices} />}
</div>
);
}
useLiveRefresh

Auto-refresh data on updates:

function UserProfile({ userId }) {
const { data, refresh } = useLiveRefresh('userUpdates', async () => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
});

return (
<div>
<h2>{data?.name}</h2>
<p>{data?.email}</p>
<button onClick={() => refresh()}>Manual Refresh</button>
</div>
);
}
useLiveUpdateListener

Listen to updates with callback:

function ReportMonitor() {
const [updates, setUpdates] = useState([]);

useLiveUpdateListener('reports', (update) => {
setUpdates(prev => [...prev, {
timestamp: new Date(),
...update
}]);
});

return (
<div>
<h3>Recent Updates</h3>
{updates.map((u, i) => (
<div key={i}>
{u.timestamp.toLocaleTimeString()} - {u.status}
</div>
))}
</div>
);
}

Creating Custom Auth Provider

Implement the IAuthProvider interface:

import type { IAuthProvider } from '@codella-software/utils';

export class CustomAuthProvider implements IAuthProvider {
private authStateCallbacks: ((isAuthenticated: boolean) => void)[] = [];

async getIdToken(forceRefresh?: boolean): Promise<string | null> {
// Return your authentication token
if (forceRefresh) {
return this.refreshToken();
}
return localStorage.getItem('auth_token');
}

isAuthenticated(): boolean {
// Check if user is authenticated
return !!localStorage.getItem('auth_token');
}

onAuthStateChanged(callback: (isAuthenticated: boolean) => void): () => void {
// Subscribe to auth state changes
this.authStateCallbacks.push(callback);

// Return unsubscribe function
return () => {
this.authStateCallbacks = this.authStateCallbacks.filter(cb => cb !== callback);
};
}

private async refreshToken(): Promise<string | null> {
// Refresh token if needed
const response = await fetch('/api/auth/refresh', {
method: 'POST'
});
const { token } = await response.json();
localStorage.setItem('auth_token', token);
this.notifyAuthStateChanged();
return token;
}

private notifyAuthStateChanged(): void {
const isAuthenticated = this.isAuthenticated();
this.authStateCallbacks.forEach(callback => callback(isAuthenticated));
}
}

export const customAuthProvider = new CustomAuthProvider();

Event Mapping Examples

Minimal Setup

const config: LiveUpdateConfig = {
sseEnabled: true,
wsEnabled: true,
authProvider: myAuthProvider
// No eventMappings - use generic 'on()' calls
};

service.on('any-event-name').subscribe(data => {
console.log(data);
});

Full Configuration

const config: LiveUpdateConfig = {
sseEnabled: true,
wsEnabled: true,
authProvider: myAuthProvider,
eventMappings: {
notifications: {
sseEvent: 'GET_NOTIFICATIONS',
wsAction: 'NOTIFICATIONS_RESPONSE'
},
invoices: {
sseEvent: 'INVOICE_FOR_VERIFICATION_RESPONSE',
wsAction: 'INVOICE_FOR_VERIFICATION_RESPONSE'
},
userProfile: {
sseEvent: 'UPDATE_USER',
wsAction: 'UPDATE_USER'
},
reports: {
sseEvent: 'UPDATE_REPORTS',
wsAction: 'UPDATE_REPORTS'
},
fileUpload: {
sseEvent: 'FILE_UPLOADED',
wsAction: 'FILE_UPLOADED'
}
}
};

Configuration Options

SSEConfig

  • url (required): SSE endpoint URL
  • authProvider (required): IAuthProvider implementation
  • headerName (optional): Auth header name (default: 'Authorization')
  • headerPrefix (optional): Auth header prefix (default: 'Bearer')

WebsocketConfig

  • url (required): WebSocket endpoint URL (ws:// or wss://)
  • authProvider (required): IAuthProvider implementation

LiveUpdateConfig

  • sseEnabled (required): Enable SSE transport
  • wsEnabled (required): Enable WebSocket transport
  • authProvider (required): IAuthProvider implementation
  • eventMappings (optional): Map event names to SSE events/WS actions

Best Practices

  1. Define Event Mappings: Document your event names and their SSE/WS counterparts
  2. Error Handling: Always subscribe to error events and handle disconnections
  3. Cleanup: Call destroy() on service when no longer needed (in React, the provider handles this)
  4. Authentication: Ensure auth provider properly handles token refresh
  5. Connection Status: Check isConnected before sending messages
  6. Backoff Strategy: The service automatically handles reconnection with exponential backoff

Connection Lifecycle

[Initialization]

[Connect to SSE]

[Success] → [Listen to Events] → [Reconnect on Error]

[SSE Fails] → [Fallback to WebSocket]

[WebSocket Connected] → [Listen to Events]

Error Handling

function MyComponent() {
const { service, error, isConnected } = useLiveUpdates();

useEffect(() => {
if (error) {
console.error('Live update error:', error);
// Show user-facing error
}
}, [error]);

if (!isConnected) {
return <div>Connecting...</div>;
}

return <div>Your content</div>;
}

Migration from Hardcoded Services

Before (Old Approach)

// Hardcoded Firebase, hardcoded events
import { SSEService } from '@codella-software/utils';

const service = new SSEService();
service.on(SSEEventType.GetNotifications).subscribe(...);

After (New Approach)

// Configurable auth, flexible events
import { LiveUpdateService } from '@codella-software/utils';
import { myAuthProvider } from './auth';

const service = new LiveUpdateService({
sseEnabled: true,
wsEnabled: true,
authProvider: myAuthProvider,
eventMappings: {
notifications: { sseEvent: 'GET_NOTIFICATIONS' }
}
});

service.on('notifications').subscribe(...);

Performance Considerations

  • Connection Pool: Each app should typically have one LiveUpdateService instance (use React Provider)
  • Memory: Services automatically clean up subscriptions on destroy
  • Network: WebSocket preferred over SSE for high-frequency updates
  • Battery: SSE more battery-efficient for mobile applications

Troubleshooting

Service Won't Connect

  • Check auth provider returns valid token
  • Verify SSE/WebSocket URLs are correct
  • Check server is running and accepting connections
  • Review browser console for CORS errors

Events Not Received

  • Verify event mapping matches server-side event names
  • Check connection status is true
  • Ensure server is sending events on configured endpoints
  • Review network tab for dropped connections

Memory Leaks

  • Always call destroy() or unmount React Provider
  • Unsubscribe from manual subscriptions
  • Check for circular references in event data