Real-Time Updates Guide
Learn how to implement real-time updates in your application using GraphQL subscriptions and WebSocket connections. This guide covers setup, implementation patterns, and best practices for building reactive applications.
Prerequisites
- Unified Commerce API access with WebSocket permissions
- Understanding of GraphQL subscriptions
- Node.js environment or browser with WebSocket support
- Basic knowledge of event-driven programming
Table of Contents
- WebSocket Architecture
- Connection Setup
- GraphQL Subscriptions
- Event Types
- Client Implementation
- Server-Side Events
- Connection Management
- Scaling Considerations
- Security
- Troubleshooting
WebSocket Architecture
Overview
graph TD
A[Client App] -->|WebSocket| B[API Gateway]
B --> C[Subscription Manager]
C --> D[Event Bus]
E[Order Service] --> D
F[Inventory Service] --> D
G[Payment Service] --> D
H[Notification Service] --> D
D --> I[Event Processor]
I --> C
C -->|Push Updates| A
Connection Flow
- Client establishes WebSocket connection
- Client subscribes to specific events
- Services publish events to event bus
- Subscription manager filters and routes events
- Client receives real-time updates
Connection Setup
Step 1: Install Dependencies
# For Node.js/React
npm install @apollo/client graphql-ws ws subscriptions-transport-ws
# For vanilla JavaScript
npm install graphql-ws
Step 2: Configure WebSocket Client
// lib/websocket-client.ts
import { ApolloClient, InMemoryCache, split } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { getMainDefinition } from '@apollo/client/utilities';
import { HttpLink } from '@apollo/client/link/http';
// HTTP link for queries and mutations
const httpLink = new HttpLink({
uri: process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT,
headers: {
authorization: `Bearer ${localStorage.getItem('auth_token')}`,
},
});
// WebSocket link for subscriptions
const wsLink = new GraphQLWsLink(
createClient({
url: process.env.NEXT_PUBLIC_WS_ENDPOINT || 'wss://gateway.unifiedcommerce.app/graphql',
connectionParams: () => ({
authToken: localStorage.getItem('auth_token'),
merchantId: process.env.NEXT_PUBLIC_MERCHANT_ID,
}),
shouldRetry: (errOrCloseEvent) => {
// Retry on network errors
return true;
},
retryAttempts: 5,
retryWait: async (retryCount) => {
// Exponential backoff
await new Promise(resolve =>
setTimeout(resolve, Math.min(1000 * Math.pow(2, retryCount), 30000))
);
},
on: {
connected: () => console.log('WebSocket connected'),
error: (error) => console.error('WebSocket error:', error),
closed: (event) => console.log('WebSocket closed:', event),
},
})
);
// Split links based on operation type
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink
);
// Create Apollo Client
export const apolloClient = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
});
Step 3: Vanilla JavaScript Setup
// For non-React applications
import { createClient } from 'graphql-ws';
const client = createClient({
url: 'wss://gateway.unifiedcommerce.app/graphql',
connectionParams: {
authToken: localStorage.getItem('auth_token'),
},
});
// Subscribe to events
const unsubscribe = client.subscribe(
{
query: `
subscription OnOrderUpdate($orderId: ID!) {
orderUpdated(orderId: $orderId) {
id
status
updatedAt
}
}
`,
variables: {
orderId: 'order_123',
},
},
{
next: (data) => console.log('Order updated:', data),
error: (error) => console.error('Subscription error:', error),
complete: () => console.log('Subscription complete'),
}
);
// Cleanup
// unsubscribe();
GraphQL Subscriptions
Available Subscriptions
type Subscription {
# Order events
orderCreated(merchantId: ID!): Order!
orderUpdated(orderId: ID!): Order!
orderStatusChanged(orderId: ID!): OrderStatusUpdate!
# Inventory events
inventoryUpdated(productId: ID!): InventoryUpdate!
lowStockAlert(warehouseId: ID): LowStockAlert!
# Payment events
paymentProcessed(orderId: ID!): PaymentUpdate!
refundProcessed(orderId: ID!): RefundUpdate!
# Customer events
customerActivity(customerId: ID!): CustomerActivity!
cartUpdated(cartId: ID!): Cart!
# Real-time analytics
salesUpdate(channelId: ID): SalesMetric!
visitorActivity: VisitorMetric!
}
Order Status Updates
import { gql, useSubscription } from '@apollo/client';
const ORDER_STATUS_SUBSCRIPTION = gql`
subscription OnOrderStatusChange($orderId: ID!) {
orderStatusChanged(orderId: $orderId) {
orderId
previousStatus
newStatus
updatedAt
updatedBy
notes
}
}
`;
export function OrderTracker({ orderId }: { orderId: string }) {
const { data, loading, error } = useSubscription(
ORDER_STATUS_SUBSCRIPTION,
{
variables: { orderId },
onData: ({ data }) => {
// Handle real-time update
console.log('Order status changed:', data);
// Update UI
showNotification(`Order ${orderId} is now ${data.orderStatusChanged.newStatus}`);
},
}
);
if (loading) return <p>Connecting...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<h3>Order Status: {data?.orderStatusChanged?.newStatus}</h3>
<p>Last updated: {data?.orderStatusChanged?.updatedAt}</p>
</div>
);
}
Inventory Updates
const INVENTORY_SUBSCRIPTION = gql`
subscription OnInventoryUpdate($productIds: [ID!]!) {
inventoryUpdated(productIds: $productIds) {
productId
warehouseId
previousQuantity
newQuantity
adjustment
reason
timestamp
}
}
`;
export function useInventorySubscription(productIds: string[]) {
const [inventory, setInventory] = useState<Map<string, number>>(new Map());
useSubscription(INVENTORY_SUBSCRIPTION, {
variables: { productIds },
onData: ({ data }) => {
const update = data.data.inventoryUpdated;
setInventory(prev => {
const updated = new Map(prev);
updated.set(update.productId, update.newQuantity);
return updated;
});
// Check if out of stock
if (update.newQuantity === 0) {
showWarning(`Product ${update.productId} is now out of stock!`);
}
},
});
return inventory;
}
Cart Updates
const CART_SUBSCRIPTION = gql`
subscription OnCartUpdate($cartId: ID!) {
cartUpdated(cartId: $cartId) {
id
items {
id
product {
id
name
price
}
quantity
total
}
subtotal
tax
shipping
total
updatedAt
}
}
`;
export function LiveCart({ cartId }: { cartId: string }) {
const { data } = useSubscription(CART_SUBSCRIPTION, {
variables: { cartId },
});
const cart = data?.cartUpdated;
return (
<div className="live-cart">
<h3>Shopping Cart (Live)</h3>
{cart?.items.map(item => (
<div key={item.id}>
{item.product.name} x {item.quantity} = ${item.total / 100}
</div>
))}
<div>Total: ${cart?.total / 100}</div>
</div>
);
}
Event Types
Order Events
// Event type definitions
interface OrderEvent {
type: 'ORDER_CREATED' | 'ORDER_UPDATED' | 'ORDER_CANCELLED' | 'ORDER_COMPLETED';
orderId: string;
customerId: string;
timestamp: string;
data: Order;
}
// Subscribe to multiple order events
const ORDER_EVENTS_SUBSCRIPTION = gql`
subscription OnOrderEvents($merchantId: ID!) {
orderEvents(merchantId: $merchantId) {
type
orderId
customerId
timestamp
order {
id
orderNumber
status
total
items {
id
product {
name
}
quantity
}
}
}
}
`;
Payment Events
interface PaymentEvent {
type: 'PAYMENT_INITIATED' | 'PAYMENT_PROCESSING' | 'PAYMENT_SUCCEEDED' | 'PAYMENT_FAILED';
paymentId: string;
orderId: string;
amount: number;
currency: string;
timestamp: string;
}
const PAYMENT_SUBSCRIPTION = gql`
subscription OnPaymentUpdate($orderId: ID!) {
paymentProcessed(orderId: $orderId) {
type
paymentId
orderId
amount
currency
status
errorMessage
processedAt
}
}
`;
Analytics Events
const REAL_TIME_SALES = gql`
subscription OnSalesUpdate($channelId: ID, $interval: Int) {
salesUpdate(channelId: $channelId, interval: $interval) {
timestamp
channel {
id
name
}
metrics {
orders
revenue
averageOrderValue
conversionRate
}
comparison {
previousPeriod
percentageChange
}
}
}
`;
export function SalesDashboard() {
const [metrics, setMetrics] = useState<SalesMetric[]>([]);
useSubscription(REAL_TIME_SALES, {
variables: { interval: 60 }, // Update every minute
onData: ({ data }) => {
setMetrics(prev => [...prev.slice(-59), data.data.salesUpdate]);
},
});
return <SalesChart data={metrics} />;
}
Client Implementation
React Hook for Subscriptions
// hooks/use-subscription.ts
import { useEffect, useRef, useState } from 'react';
import { createClient, Client } from 'graphql-ws';
interface UseSubscriptionOptions<T> {
query: string;
variables?: Record<string, any>;
onData?: (data: T) => void;
onError?: (error: Error) => void;
enabled?: boolean;
}
export function useRealtimeSubscription<T>({
query,
variables = {},
onData,
onError,
enabled = true,
}: UseSubscriptionOptions<T>) {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(true);
const clientRef = useRef<Client | null>(null);
const unsubscribeRef = useRef<(() => void) | null>(null);
useEffect(() => {
if (!enabled) return;
// Create WebSocket client
clientRef.current = createClient({
url: process.env.NEXT_PUBLIC_WS_ENDPOINT!,
connectionParams: {
authToken: localStorage.getItem('auth_token'),
},
});
// Subscribe
unsubscribeRef.current = clientRef.current.subscribe(
{
query,
variables,
},
{
next: (result) => {
setData(result.data as T);
setLoading(false);
onData?.(result.data as T);
},
error: (err) => {
setError(err);
setLoading(false);
onError?.(err);
},
complete: () => {
console.log('Subscription complete');
},
}
);
// Cleanup
return () => {
unsubscribeRef.current?.();
clientRef.current?.dispose();
};
}, [query, JSON.stringify(variables), enabled]);
return { data, error, loading };
}
Vue.js Implementation
// composables/useSubscription.js
import { ref, onMounted, onUnmounted } from 'vue';
import { createClient } from 'graphql-ws';
export function useSubscription(query, variables = {}) {
const data = ref(null);
const error = ref(null);
const loading = ref(true);
let client = null;
let unsubscribe = null;
onMounted(() => {
client = createClient({
url: 'wss://gateway.unifiedcommerce.app/graphql',
connectionParams: {
authToken: localStorage.getItem('auth_token'),
},
});
unsubscribe = client.subscribe(
{ query, variables },
{
next: (result) => {
data.value = result.data;
loading.value = false;
},
error: (err) => {
error.value = err;
loading.value = false;
},
}
);
});
onUnmounted(() => {
unsubscribe?.();
client?.dispose();
});
return { data, error, loading };
}
Angular Implementation
// services/subscription.service.ts
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { createClient } from 'graphql-ws';
@Injectable({
providedIn: 'root',
})
export class SubscriptionService {
private client = createClient({
url: 'wss://gateway.unifiedcommerce.app/graphql',
connectionParams: () => ({
authToken: localStorage.getItem('auth_token'),
}),
});
subscribe<T>(query: string, variables?: any): Observable<T> {
return new Observable((observer) => {
const unsubscribe = this.client.subscribe(
{ query, variables },
{
next: (data) => observer.next(data.data as T),
error: (err) => observer.error(err),
complete: () => observer.complete(),
}
);
return () => unsubscribe();
});
}
}
Server-Side Events
Node.js WebSocket Server
// server/websocket-handler.ts
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { PubSub } from 'graphql-subscriptions';
// Create PubSub instance
export const pubsub = new PubSub();
// Define schema
const typeDefs = `
type Subscription {
orderUpdated(orderId: ID!): Order
inventoryChanged(productId: ID!): Inventory
}
`;
const resolvers = {
Subscription: {
orderUpdated: {
subscribe: (_, { orderId }) =>
pubsub.asyncIterator(`ORDER_UPDATED_${orderId}`),
},
inventoryChanged: {
subscribe: (_, { productId }) =>
pubsub.asyncIterator(`INVENTORY_CHANGED_${productId}`),
},
},
};
const schema = makeExecutableSchema({ typeDefs, resolvers });
// Create WebSocket server
const wsServer = new WebSocketServer({
port: 4000,
path: '/graphql',
});
// Set up GraphQL WebSocket server
useServer(
{
schema,
context: (ctx) => {
// Validate auth token
const token = ctx.connectionParams?.authToken;
if (!validateToken(token)) {
throw new Error('Unauthorized');
}
return { user: getUserFromToken(token) };
},
onConnect: (ctx) => {
console.log('Client connected');
},
onDisconnect: (ctx) => {
console.log('Client disconnected');
},
},
wsServer
);
// Publish events
export function publishOrderUpdate(orderId: string, order: Order) {
pubsub.publish(`ORDER_UPDATED_${orderId}`, {
orderUpdated: order,
});
}
export function publishInventoryChange(productId: string, inventory: Inventory) {
pubsub.publish(`INVENTORY_CHANGED_${productId}`, {
inventoryChanged: inventory,
});
}
Event Publishing
// services/event-publisher.ts
import { pubsub } from './websocket-handler';
export class EventPublisher {
// Publish order events
async publishOrderEvent(event: OrderEvent) {
const channels = [
`ORDER_${event.type}_${event.orderId}`,
`MERCHANT_ORDERS_${event.merchantId}`,
`CUSTOMER_ORDERS_${event.customerId}`,
];
channels.forEach(channel => {
pubsub.publish(channel, event);
});
// Store event for replay
await this.storeEvent(event);
}
// Batch publish for efficiency
async publishBatch(events: Event[]) {
const publishPromises = events.map(event =>
this.publishEvent(event)
);
await Promise.all(publishPromises);
}
// Scheduled events
scheduleEvent(event: Event, delay: number) {
setTimeout(() => {
this.publishEvent(event);
}, delay);
}
private async storeEvent(event: Event) {
// Store in database for event replay
await db.events.create({
data: {
type: event.type,
payload: event,
timestamp: new Date(),
},
});
}
}
Connection Management
Reconnection Strategy
// services/connection-manager.ts
export class ConnectionManager {
private client: Client | null = null;
private subscriptions: Map<string, () => void> = new Map();
private reconnectAttempts = 0;
private maxReconnectAttempts = 10;
private reconnectDelay = 1000;
async connect(): Promise<void> {
return new Promise((resolve, reject) => {
this.client = createClient({
url: this.wsUrl,
connectionParams: this.getConnectionParams,
on: {
connected: () => {
console.log('WebSocket connected');
this.reconnectAttempts = 0;
this.resubscribeAll();
resolve();
},
error: (error) => {
console.error('WebSocket error:', error);
this.handleConnectionError(error);
},
closed: (event) => {
console.log('WebSocket closed:', event);
this.handleDisconnection();
},
},
});
});
}
private handleConnectionError(error: Error) {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.scheduleReconnect();
} else {
this.notifyMaxRetriesReached();
}
}
private scheduleReconnect() {
this.reconnectAttempts++;
const delay = Math.min(
this.reconnectDelay * Math.pow(2, this.reconnectAttempts),
30000
);
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
setTimeout(() => {
this.connect();
}, delay);
}
private resubscribeAll() {
// Resubscribe to all active subscriptions after reconnection
this.subscriptions.forEach((unsubscribe, id) => {
console.log(`Resubscribing to ${id}`);
// Re-create subscription
});
}
subscribe(id: string, subscription: Subscription): () => void {
const unsubscribe = this.client!.subscribe(subscription, {
next: subscription.onData,
error: subscription.onError,
complete: subscription.onComplete,
});
this.subscriptions.set(id, unsubscribe);
return () => {
unsubscribe();
this.subscriptions.delete(id);
};
}
disconnect() {
this.subscriptions.forEach(unsubscribe => unsubscribe());
this.subscriptions.clear();
this.client?.dispose();
this.client = null;
}
}
Connection Health Monitoring
// services/connection-health.ts
export class ConnectionHealthMonitor {
private pingInterval: NodeJS.Timeout | null = null;
private lastPong: number = Date.now();
private healthCheckInterval = 30000; // 30 seconds
private unhealthyThreshold = 60000; // 1 minute
startMonitoring(client: Client) {
this.pingInterval = setInterval(() => {
this.performHealthCheck(client);
}, this.healthCheckInterval);
// Listen for pong messages
client.on('pong', () => {
this.lastPong = Date.now();
});
}
private performHealthCheck(client: Client) {
// Send ping
client.ping();
// Check if connection is healthy
const timeSinceLastPong = Date.now() - this.lastPong;
if (timeSinceLastPong > this.unhealthyThreshold) {
console.error('Connection unhealthy, no pong received');
this.handleUnhealthyConnection(client);
}
}
private handleUnhealthyConnection(client: Client) {
// Force reconnection
client.terminate();
client.connect();
}
stopMonitoring() {
if (this.pingInterval) {
clearInterval(this.pingInterval);
this.pingInterval = null;
}
}
}
Scaling Considerations
Horizontal Scaling with Redis
// services/redis-pubsub.ts
import { RedisPubSub } from 'graphql-redis-subscriptions';
import Redis from 'ioredis';
// Create Redis clients
const publishClient = new Redis({
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT!),
password: process.env.REDIS_PASSWORD,
});
const subscribeClient = new Redis({
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT!),
password: process.env.REDIS_PASSWORD,
});
// Create PubSub with Redis
export const pubsub = new RedisPubSub({
publisher: publishClient,
subscriber: subscribeClient,
});
// Subscription resolver with Redis
const resolvers = {
Subscription: {
orderUpdated: {
subscribe: withFilter(
() => pubsub.asyncIterator('ORDER_UPDATED'),
(payload, variables) => {
return payload.orderUpdated.id === variables.orderId;
}
),
},
},
};
Load Balancing WebSockets
# nginx.conf
upstream websocket_backend {
least_conn;
server ws1.example.com:4000;
server ws2.example.com:4000;
server ws3.example.com:4000;
}
server {
listen 443 ssl;
server_name api.example.com;
location /graphql {
proxy_pass http://websocket_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# WebSocket specific timeouts
proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;
}
}
Security
Authentication & Authorization
// middleware/websocket-auth.ts
export async function authenticateWebSocket(
connectionParams: any
): Promise<AuthContext> {
const token = connectionParams?.authToken;
if (!token) {
throw new Error('Missing auth token');
}
try {
// Verify JWT token
const decoded = jwt.verify(token, process.env.JWT_SECRET!);
// Get user permissions
const user = await getUserById(decoded.userId);
if (!user) {
throw new Error('User not found');
}
// Check subscription permissions
if (!user.permissions.includes('SUBSCRIPTION_ACCESS')) {
throw new Error('Subscription access denied');
}
return {
user,
permissions: user.permissions,
merchantId: user.merchantId,
};
} catch (error) {
throw new Error('Invalid auth token');
}
}
// Apply to subscription resolvers
const resolvers = {
Subscription: {
orderUpdated: {
subscribe: withFilter(
(_, __, context) => {
// Check permissions
if (!context.user.permissions.includes('ORDER_READ')) {
throw new Error('Unauthorized');
}
return pubsub.asyncIterator('ORDER_UPDATED');
},
(payload, variables, context) => {
// Filter by user's merchant
return payload.orderUpdated.merchantId === context.merchantId;
}
),
},
},
};
Rate Limiting
// middleware/rate-limiter.ts
export class WebSocketRateLimiter {
private connections: Map<string, ConnectionInfo> = new Map();
private maxConnectionsPerUser = 5;
private maxSubscriptionsPerConnection = 20;
private maxMessagesPerMinute = 100;
async onConnect(userId: string, connectionId: string): boolean {
const userConnections = this.getUserConnections(userId);
if (userConnections.length >= this.maxConnectionsPerUser) {
throw new Error('Maximum connections exceeded');
}
this.connections.set(connectionId, {
userId,
subscriptions: 0,
messages: [],
connectedAt: Date.now(),
});
return true;
}
async onSubscribe(connectionId: string): boolean {
const connection = this.connections.get(connectionId);
if (!connection) {
throw new Error('Connection not found');
}
if (connection.subscriptions >= this.maxSubscriptionsPerConnection) {
throw new Error('Maximum subscriptions exceeded');
}
connection.subscriptions++;
return true;
}
async onMessage(connectionId: string): boolean {
const connection = this.connections.get(connectionId);
if (!connection) {
throw new Error('Connection not found');
}
// Clean old messages
const oneMinuteAgo = Date.now() - 60000;
connection.messages = connection.messages.filter(
timestamp => timestamp > oneMinuteAgo
);
if (connection.messages.length >= this.maxMessagesPerMinute) {
throw new Error('Rate limit exceeded');
}
connection.messages.push(Date.now());
return true;
}
private getUserConnections(userId: string): ConnectionInfo[] {
return Array.from(this.connections.values()).filter(
conn => conn.userId === userId
);
}
}
Troubleshooting
Common Issues
1. Connection Drops Frequently
Problem: WebSocket connection drops every few minutes Solution:
// Implement keep-alive ping
setInterval(() => {
if (client.readyState === WebSocket.OPEN) {
client.ping();
}
}, 30000);
// Configure proxy timeouts
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
2. Missing Events
Problem: Not receiving all expected events Solution:
// Implement event replay
async function replayMissedEvents(lastEventId: string) {
const missedEvents = await getMissedEvents(lastEventId);
for (const event of missedEvents) {
handleEvent(event);
}
}
// Store last received event ID
let lastEventId = localStorage.getItem('lastEventId');
subscription.on('data', (event) => {
lastEventId = event.id;
localStorage.setItem('lastEventId', event.id);
});
3. Memory Leaks
Problem: Memory usage increases over time Solution:
// Properly clean up subscriptions
class SubscriptionManager {
private subscriptions = new Set();
add(subscription: any) {
this.subscriptions.add(subscription);
}
cleanup() {
this.subscriptions.forEach(sub => {
if (sub.unsubscribe) sub.unsubscribe();
});
this.subscriptions.clear();
}
}
// Use in component
useEffect(() => {
const manager = new SubscriptionManager();
// Add subscriptions
manager.add(subscription1);
manager.add(subscription2);
return () => manager.cleanup();
}, []);
4. Authentication Expires
Problem: Connection fails after token expires Solution:
// Implement token refresh
class TokenManager {
async getValidToken(): Promise<string> {
const token = localStorage.getItem('auth_token');
const expiresAt = localStorage.getItem('token_expires_at');
if (Date.now() > parseInt(expiresAt)) {
return await this.refreshToken();
}
return token;
}
async refreshToken(): Promise<string> {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
body: JSON.stringify({
refreshToken: localStorage.getItem('refresh_token'),
}),
});
const { accessToken, expiresIn } = await response.json();
localStorage.setItem('auth_token', accessToken);
localStorage.setItem('token_expires_at', Date.now() + expiresIn * 1000);
return accessToken;
}
}
Best Practices
- Always implement reconnection logic with exponential backoff
- Clean up subscriptions when components unmount
- Use connection pooling for multiple subscriptions
- Implement message deduplication to handle duplicate events
- Add health checks to monitor connection status
- Use compression for large payloads
- Implement circuit breakers for failing connections
- Store critical events for replay capability
- Monitor WebSocket metrics (connections, messages, errors)
- Test with network simulation tools
Next Steps
- Implement custom event channels
- Add support for presence indicators
- Build collaborative features
- Implement event sourcing
- Add support for offline mode
- Create custom subscription filters
- Implement event aggregation
- Add support for binary data