Connection Management

WebSocket connection lifecycle, state monitoring, error handling, and best practices

Connection Management

Complete guide to managing WebSocket connections, monitoring states, handling errors, and implementing best practices for robust real-time applications.

Core Connection APIs

joinChannel(channel: string): void

Sets the channel for this client instance. Must be called before connect().

client.joinChannel("chat-room");

Important: A channel groups related topics together and defines your authorization scope.

connect(timeout?: number): Promise<void>

Establishes WebSocket connection. Automatically fetches grant tokens from authBaseUrl.

Parameters:

  • timeout?: number - Connection timeout in milliseconds (default: 30000)
await client.connect();
// or with custom timeout
await client.connect(10000);

close(): void

Closes the WebSocket connection and cleans up resources.

client.close();

Connection State Monitoring

Connection State Properties

Monitor your connection status in real-time:

isConnected: boolean

Check if WebSocket is connected.

if (client.isConnected) {
  console.log("Ready to send messages");
}

isReadable: boolean

Check if client can receive messages.

if (client.isReadable) {
  console.log("Receiving messages");
}

isWritable: boolean

Check if client can send messages.

if (client.isWritable) {
  await client.publish("topic", "message");
}

Real-time State Monitoring

// Check connection state periodically
setInterval(() => {
  console.log(`Connection: ${client.isConnected ? "✅" : "❌"}`);
  console.log(`Readable: ${client.isReadable ? "✅" : "❌"}`);  
  console.log(`Writable: ${client.isWritable ? "✅" : "❌"}`);
}, 5000);

Connection Lifecycle Management

Connection States

The client maintains connection state that you can monitor:

console.log(client.isConnected);  // boolean - WebSocket connected
console.log(client.isReadable);   // boolean - Can receive messages
console.log(client.isWritable);   // boolean - Can send messages

Automatic Reconnection

The SDK handles reconnection automatically with exponential backoff. Connection errors are thrown from async methods:

// Monitor connection state
setInterval(() => {
  if (!client.isConnected) {
    console.log("Connection lost - SDK will reconnect automatically");
  }
}, 5000);

// Errors are thrown from operations
try {
  await client.connect();
} catch (error) {
  console.log("Connection error:", error);
  // SDK will attempt to reconnect automatically
}

Manual Connection Management

// Graceful shutdown
process.on('SIGINT', () => {
  client.close();
  process.exit(0);
});

// Connection with custom timeout
try {
  await client.connect(10000); // 10 second timeout
} catch (error) {
  console.error("Connection failed:", error);
}

Error Handling

Common Error Patterns

All async methods can throw errors. Always use try-catch:

try {
  await client.connect();
  await client.subscribe("topic", handler);
} catch (error) {
  console.error("Operation failed:", error);
}

Error Types and Handling

Connection Errors

try {
  await client.connect();
} catch (error) {
  if (error.message.includes("Channel must be set")) {
    console.error("Call joinChannel() first");
  } else if (error.message.includes("timeout")) {
    console.error("Connection timeout - check server");
  } else if (error.message.includes("Invalid WebSocket URL")) {
    console.error("Check wsBaseUrl configuration");
  }
}

Publishing Errors

try {
  await client.publish("topic", "message");
} catch (error) {
  if (error.message.includes("Not connected")) {
    console.error("Connection lost - will reconnect automatically");
  } else if (error.message.includes("Invalid topic")) {
    console.error("Topic name must be non-empty string");
  }
}

History API Errors

try {
  const history = await client.getHistory("topic");
} catch (error) {
  if (error.message.includes("HTTP base URL required")) {
    console.error("Configure httpBaseUrl in client options");
  } else if (error.message.includes("History API failed")) {
    console.error("Server error - check API status");
  }
}

ACK and Subscription Errors

// Handle ACK errors
client.publishWithAck("topic", "message", (ack) => {
  if (!ack.success) {
    console.error("Publish failed:", ack.error?.code, ack.error?.message);
  }
});

// Handle subscription errors
client.subscribeWithCallback("topic", handler, (response) => {
  if (!response.success) {
    console.error("Subscription failed:", response.error?.message);
  }
});

Error Recovery Patterns

Retry with Backoff

async function connectWithRetry(maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      await client.connect();
      console.log("Connected successfully");
      return;
    } catch (error) {
      console.log(`Retry ${i + 1}/${maxRetries}`);
      if (i === maxRetries - 1) throw error;
      
      // Exponential backoff
      await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)));
    }
  }
}

Graceful Degradation

// Continue without history if HTTP API unavailable
let historySupported = true;
try {
  await client.getHistory("topic", { limit: 1 });
} catch (error) {
  if (error.message.includes("HTTP base URL required")) {
    historySupported = false;
    console.warn("History API not available");
  }
}

// Adapt UI based on capabilities
if (!historySupported) {
  hideHistoryButton();
}

Common Error Messages

Error MessageCauseSolution
"Channel must be set before connecting"No channel setCall joinChannel() first
"HTTP base URL required for history API"Missing HTTP configAdd httpBaseUrl to client options
"Connection timeout"Server unreachableCheck wsBaseUrl and network
"Not connected"Publishing without connectionWait for connection or handle gracefully
"Invalid topic"Empty/invalid topic nameUse non-empty string topics
"ACK callback required"Missing callback in publishWithAckProvide ACK callback function

Best Practices

1. Always set channel before connecting

client.joinChannel("my-channel");
await client.connect();

2. Handle reconnection

async function connectWithRetry(maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      await client.connect();
      return;
    } catch (error) {
      console.log(`Retry ${i + 1}/${maxRetries}`);
      await new Promise(r => setTimeout(r, 1000 * (i + 1)));
    }
  }
  throw new Error("Failed to connect after retries");
}

3. Clean up on exit

process.on("SIGINT", () => {
  client.close();
  process.exit(0);
});

4. Connection Health Monitoring

class ConnectionMonitor {
  private client: ErebusPubSubClient;
  private healthCheckInterval?: NodeJS.Timeout;
  
  constructor(client: ErebusPubSubClient) {
    this.client = client;
  }
  
  startMonitoring() {
    this.healthCheckInterval = setInterval(() => {
      if (!this.client.isConnected) {
        console.warn("Connection unhealthy");
        this.onConnectionLost();
      }
    }, 5000);
  }
  
  stopMonitoring() {
    if (this.healthCheckInterval) {
      clearInterval(this.healthCheckInterval);
    }
  }
  
  private onConnectionLost() {
    // Implement custom reconnection logic
    // Show user notification
    // Pause outgoing messages
  }
}

5. Production Configuration

const client = ErebusClient.createClient({
  client: ErebusClientState.PubSub,
  authBaseUrl: process.env.EREBUS_AUTH_URL!,
  wsBaseUrl: process.env.EREBUS_WS_URL,
  heartbeatMs: 30000,      // Longer heartbeat for production
  connectionTimeoutMs: 10000,  // Connection timeout
  debug: process.env.NODE_ENV === 'development',
  log: (level, msg, meta) => {
    // Use your logging framework
    logger[level](msg, meta);
  }
});

Advanced Patterns

Connection Pooling for Multiple Channels

class ErebusConnectionPool {
  private connections = new Map<string, ErebusPubSubClient>();
  
  async getConnection(channel: string): Promise<ErebusPubSubClient> {
    if (!this.connections.has(channel)) {
      const client = ErebusClient.createClient({
        client: ErebusClientState.PubSub,
        authBaseUrl: process.env.EREBUS_AUTH_URL!,
      });
      
      client.joinChannel(channel);
      await client.connect();
      
      this.connections.set(channel, client);
    }
    
    return this.connections.get(channel)!;
  }
  
  async closeAll() {
    for (const client of this.connections.values()) {
      client.close();
    }
    this.connections.clear();
  }
}

Circuit Breaker Pattern

class ErebusCircuitBreaker {
  private failures = 0;
  private lastFailure = 0;
  private state: 'closed' | 'open' | 'half-open' = 'closed';
  
  async execute<T>(operation: () => Promise<T>): Promise<T> {
    if (this.state === 'open') {
      if (Date.now() - this.lastFailure > 60000) { // 1 minute
        this.state = 'half-open';
      } else {
        throw new Error('Circuit breaker is OPEN');
      }
    }
    
    try {
      const result = await operation();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }
  
  private onSuccess() {
    this.failures = 0;
    this.state = 'closed';
  }
  
  private onFailure() {
    this.failures++;
    this.lastFailure = Date.now();
    
    if (this.failures >= 3) {
      this.state = 'open';
    }
  }
}

Next Steps