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

  1. WebSocket Architecture
  2. Connection Setup
  3. GraphQL Subscriptions
  4. Event Types
  5. Client Implementation
  6. Server-Side Events
  7. Connection Management
  8. Scaling Considerations
  9. Security
  10. 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

  1. Client establishes WebSocket connection
  2. Client subscribes to specific events
  3. Services publish events to event bus
  4. Subscription manager filters and routes events
  5. 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

  1. Always implement reconnection logic with exponential backoff
  2. Clean up subscriptions when components unmount
  3. Use connection pooling for multiple subscriptions
  4. Implement message deduplication to handle duplicate events
  5. Add health checks to monitor connection status
  6. Use compression for large payloads
  7. Implement circuit breakers for failing connections
  8. Store critical events for replay capability
  9. Monitor WebSocket metrics (connections, messages, errors)
  10. 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

Resources

Was this page helpful?