Comprehensive error handling, recovery mechanisms, and debugging strategies for robust HAIP SDK applications
// Base error class
class HAIPError extends Error {
constructor(message: string, code?: string, details?: any);
code: string;
details: any;
}
// Connection-related errors
class HAIPConnectionError extends HAIPError {
constructor(message: string, details?: any);
}
// Authentication errors
class HAIPAuthenticationError extends HAIPError {
constructor(message: string, details?: any);
}
// Flow control errors
class HAIPFlowControlError extends HAIPError {
constructor(message: string, details?: any);
}
// Protocol errors
class HAIProtocolError extends HAIPError {
constructor(message: string, details?: any);
}
Error Code | Description | Recovery Action |
---|---|---|
CONNECTION_FAILED | Connection to server failed | Retry with exponential backoff |
AUTHENTICATION_FAILED | JWT token invalid or expired | Refresh token and reconnect |
FLOW_CONTROL | No credits available for sending | Wait for flow update or request credits |
PROTOCOL_ERROR | Invalid message format or sequence | Reset connection |
TIMEOUT | Operation timed out | Retry with longer timeout |
RATE_LIMITED | Too many requests | Implement rate limiting |
SERVER_ERROR | Server-side error | Retry with exponential backoff |
import { createHAIPClient } from "haip-sdk";
const client = createHAIPClient({
url: "ws://localhost:8080",
token: "your-jwt-token",
transport: "websocket",
});
// Handle connection errors
client.on("error", (error: Error) => {
console.error("Client error:", error);
if (error.message.includes("AUTHENTICATION_FAILED")) {
console.log("Authentication failed - check your token");
} else if (error.message.includes("CONNECTION_FAILED")) {
console.log("Connection failed - will attempt to reconnect");
} else if (error.message.includes("FLOW_CONTROL")) {
console.log("Flow control error - message was dropped");
}
});
// Handle connection state changes
client.on("disconnect", (reason: string) => {
console.log("Disconnected:", reason);
if (reason === "AUTHENTICATION_FAILED") {
// Handle authentication failure
handleAuthenticationFailure();
} else if (reason === "NETWORK_ERROR") {
// Handle network issues
handleNetworkError();
}
});
async function sendMessageSafely(text: string, runId: string) {
try {
const messageId = await client.sendTextMessage("USER", text, "user", runId);
console.log("Message sent successfully:", messageId);
return messageId;
} catch (error) {
console.error("Failed to send message:", error);
if (error instanceof HAIPFlowControlError) {
// Queue message for later
await queueMessage(text, runId);
} else if (error instanceof HAIPConnectionError) {
// Attempt to reconnect
await reconnect();
} else {
// Handle other errors
throw error;
}
}
}
class ErrorRecoveryManager {
private retryAttempts = new Map<string, number>();
private maxRetries = 3;
private backoffMultiplier = 2;
constructor(private client: any) {
this.setupErrorHandling();
}
private setupErrorHandling() {
this.client.on("error", async (error: Error) => {
await this.handleError(error);
});
}
private async handleError(error: Error) {
const errorType = this.getErrorType(error);
const retryCount = this.retryAttempts.get(errorType) || 0;
if (retryCount < this.maxRetries) {
console.log(
`Retrying ${errorType} (attempt ${retryCount + 1}/${this.maxRetries})`
);
this.retryAttempts.set(errorType, retryCount + 1);
const delay = this.calculateBackoff(retryCount);
await this.delay(delay);
await this.retryOperation(errorType);
} else {
console.error(`Max retries exceeded for ${errorType}`);
this.handleMaxRetriesExceeded(errorType);
}
}
private getErrorType(error: Error): string {
if (error.message.includes("AUTHENTICATION_FAILED")) return "AUTH";
if (error.message.includes("CONNECTION_FAILED")) return "CONNECTION";
if (error.message.includes("FLOW_CONTROL")) return "FLOW_CONTROL";
if (error.message.includes("TIMEOUT")) return "TIMEOUT";
return "UNKNOWN";
}
private calculateBackoff(attempt: number): number {
return Math.min(1000 * Math.pow(this.backoffMultiplier, attempt), 30000);
}
private async retryOperation(errorType: string) {
switch (errorType) {
case "AUTH":
await this.refreshToken();
break;
case "CONNECTION":
await this.reconnect();
break;
case "FLOW_CONTROL":
await this.requestCredits();
break;
case "TIMEOUT":
await this.retryWithLongerTimeout();
break;
}
}
private async refreshToken() {
// Implement token refresh logic
console.log("Refreshing authentication token...");
}
private async reconnect() {
try {
await this.client.disconnect();
await this.client.connect();
console.log("Reconnected successfully");
} catch (error) {
console.error("Reconnection failed:", error);
}
}
private async requestCredits() {
try {
await this.client.sendFlowUpdate("USER", 10, 1024 * 1024);
console.log("Requested more credits");
} catch (error) {
console.error("Failed to request credits:", error);
}
}
private async retryWithLongerTimeout() {
// Implement retry with longer timeout
console.log("Retrying with longer timeout...");
}
private handleMaxRetriesExceeded(errorType: string) {
// Implement final error handling
console.error(`Max retries exceeded for ${errorType}`);
// Could trigger user notification, fallback mode, etc.
this.notifyUserOfError(errorType);
}
private notifyUserOfError(errorType: string) {
// Implement user notification
console.log(`Notifying user of ${errorType} error`);
}
private delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
resetRetryCount(errorType: string) {
this.retryAttempts.delete(errorType);
}
}
// Usage
const recoveryManager = new ErrorRecoveryManager(client);
class CircuitBreaker {
private state: "CLOSED" | "OPEN" | "HALF_OPEN" = "CLOSED";
private failureCount = 0;
private lastFailureTime = 0;
private threshold = 5;
private timeout = 60000; // 1 minute
async execute<T>(operation: () => Promise<T>): Promise<T> {
if (this.state === "OPEN") {
if (Date.now() - this.lastFailureTime > this.timeout) {
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.failureCount = 0;
this.state = "CLOSED";
}
private onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.threshold) {
this.state = "OPEN";
}
}
getState(): string {
return this.state;
}
}
// Usage with HAIP client
const circuitBreaker = new CircuitBreaker();
async function sendMessageWithCircuitBreaker(text: string, runId: string) {
return circuitBreaker.execute(async () => {
return await client.sendTextMessage("USER", text, "user", runId);
});
}
class ReconnectionManager {
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private reconnectDelay = 1000;
private isReconnecting = false;
constructor(private client: any) {
this.setupReconnectionHandling();
}
private setupReconnectionHandling() {
this.client.on("disconnect", async (reason: string) => {
console.log("Disconnected:", reason);
if (reason !== "USER_DISCONNECT" && !this.isReconnecting) {
await this.attemptReconnection();
}
});
this.client.on("connect", () => {
console.log("Reconnected successfully");
this.reconnectAttempts = 0;
this.isReconnecting = false;
});
}
private async attemptReconnection() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error("Max reconnection attempts reached");
return;
}
this.isReconnecting = true;
this.reconnectAttempts++;
const delay = this.calculateReconnectDelay();
console.log(
`Attempting reconnection in ${delay}ms (attempt ${this.reconnectAttempts})`
);
setTimeout(async () => {
try {
await this.client.connect();
} catch (error) {
console.error("Reconnection failed:", error);
await this.attemptReconnection();
}
}, delay);
}
private calculateReconnectDelay(): number {
return Math.min(
this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1),
30000
);
}
}
// Usage
const reconnectionManager = new ReconnectionManager(client);
class ConnectionHealthMonitor {
private healthChecks = 0;
private failedHealthChecks = 0;
private maxFailedChecks = 3;
constructor(private client: any) {
this.startHealthMonitoring();
}
private startHealthMonitoring() {
// Periodic health check
setInterval(() => {
this.performHealthCheck();
}, 30000); // Every 30 seconds
// Monitor connection state
this.client.on("connect", () => {
this.resetHealthChecks();
});
this.client.on("disconnect", () => {
this.failedHealthChecks++;
});
}
private async performHealthCheck() {
this.healthChecks++;
try {
if (!this.client.isConnected()) {
throw new Error("Client not connected");
}
// Send a ping to check connection health
const startTime = Date.now();
await this.sendPing();
const responseTime = Date.now() - startTime;
console.log(`Health check passed (${responseTime}ms)`);
this.resetHealthChecks();
} catch (error) {
console.error("Health check failed:", error);
this.failedHealthChecks++;
if (this.failedHealthChecks >= this.maxFailedChecks) {
console.error("Too many failed health checks, triggering reconnection");
await this.triggerReconnection();
}
}
}
private async sendPing() {
// Send a lightweight ping message
await this.client.sendMessage({
type: "PING",
payload: { timestamp: Date.now() },
});
}
private async triggerReconnection() {
try {
await this.client.disconnect();
await this.client.connect();
} catch (error) {
console.error("Reconnection triggered by health monitor failed:", error);
}
}
private resetHealthChecks() {
this.failedHealthChecks = 0;
}
getHealthStatus() {
return {
healthChecks: this.healthChecks,
failedHealthChecks: this.failedHealthChecks,
isHealthy: this.failedHealthChecks < this.maxFailedChecks,
};
}
}
// Usage
const healthMonitor = new ConnectionHealthMonitor(client);
class MessageRetryManager {
private retryQueue = new Map<
string,
{
message: any;
attempts: number;
maxAttempts: number;
delay: number;
}
>();
constructor(private client: any) {
this.setupRetryHandling();
}
private setupRetryHandling() {
this.client.on("error", (error: Error) => {
if (
error.message.includes("FLOW_CONTROL") ||
error.message.includes("TIMEOUT")
) {
// These errors might be retryable
this.handleRetryableError(error);
}
});
}
async sendMessageWithRetry(
message: any,
maxAttempts: number = 3
): Promise<string> {
const messageId = HAIPUtils.generateUUID();
try {
return await this.client.sendMessage(message);
} catch (error) {
if (this.isRetryableError(error)) {
return this.queueForRetry(messageId, message, maxAttempts);
}
throw error;
}
}
private isRetryableError(error: Error): boolean {
return (
error.message.includes("FLOW_CONTROL") ||
error.message.includes("TIMEOUT") ||
error.message.includes("NETWORK_ERROR")
);
}
private queueForRetry(messageId: string, message: any, maxAttempts: number) {
this.retryQueue.set(messageId, {
message,
attempts: 0,
maxAttempts,
delay: 1000,
});
this.scheduleRetry(messageId);
return messageId;
}
private scheduleRetry(messageId: string) {
const retryInfo = this.retryQueue.get(messageId);
if (!retryInfo) return;
setTimeout(async () => {
await this.attemptRetry(messageId);
}, retryInfo.delay);
}
private async attemptRetry(messageId: string) {
const retryInfo = this.retryQueue.get(messageId);
if (!retryInfo) return;
retryInfo.attempts++;
try {
await this.client.sendMessage(retryInfo.message);
this.retryQueue.delete(messageId);
console.log(`Message ${messageId} retry successful`);
} catch (error) {
if (retryInfo.attempts < retryInfo.maxAttempts) {
retryInfo.delay *= 2; // Exponential backoff
this.scheduleRetry(messageId);
console.log(
`Message ${messageId} retry failed, scheduling next attempt`
);
} else {
this.retryQueue.delete(messageId);
console.error(`Message ${messageId} max retries exceeded`);
// Could trigger user notification or fallback handling
}
}
}
private handleRetryableError(error: Error) {
// Process any queued retries
for (const [messageId] of this.retryQueue) {
this.scheduleRetry(messageId);
}
}
getRetryQueueStatus() {
return {
queuedMessages: this.retryQueue.size,
messageIds: Array.from(this.retryQueue.keys()),
};
}
}
// Usage
const retryManager = new MessageRetryManager(client);
// Send message with retry
const messageId = await retryManager.sendMessageWithRetry(
{
type: "MESSAGE_START",
payload: {
channel: "USER",
text: "Hello",
author: "user",
},
},
3
);
class ErrorLogger {
private logLevel: "debug" | "info" | "warn" | "error" = "info";
constructor(private client: any) {
this.setupErrorLogging();
}
private setupErrorLogging() {
this.client.on("error", (error: Error) => {
this.logError("CLIENT_ERROR", error);
});
this.client.on("disconnect", (reason: string) => {
this.logError("DISCONNECT", new Error(reason));
});
}
private logError(type: string, error: Error) {
const logEntry = {
timestamp: new Date().toISOString(),
type,
message: error.message,
stack: error.stack,
clientState: this.client.getConnectionState(),
performanceMetrics: this.client.getPerformanceMetrics(),
};
switch (this.logLevel) {
case "debug":
console.debug("HAIP Error:", logEntry);
break;
case "info":
console.info("HAIP Error:", logEntry);
break;
case "warn":
console.warn("HAIP Error:", logEntry);
break;
case "error":
console.error("HAIP Error:", logEntry);
break;
}
// Could also send to external logging service
this.sendToLoggingService(logEntry);
}
private sendToLoggingService(logEntry: any) {
// Implementation for sending to external logging service
// e.g., Sentry, LogRocket, etc.
}
setLogLevel(level: "debug" | "info" | "warn" | "error") {
this.logLevel = level;
}
}
// Usage
const errorLogger = new ErrorLogger(client);
errorLogger.setLogLevel("error");
class ErrorMetrics {
private metrics = {
totalErrors: 0,
errorTypes: new Map<string, number>(),
errorTimeline: [] as Array<{
timestamp: number;
type: string;
message: string;
}>,
recoveryAttempts: 0,
successfulRecoveries: 0,
};
recordError(type: string, message: string) {
this.metrics.totalErrors++;
const currentCount = this.metrics.errorTypes.get(type) || 0;
this.metrics.errorTypes.set(type, currentCount + 1);
this.metrics.errorTimeline.push({
timestamp: Date.now(),
type,
message,
});
// Keep only last 100 errors
if (this.metrics.errorTimeline.length > 100) {
this.metrics.errorTimeline.shift();
}
}
recordRecoveryAttempt() {
this.metrics.recoveryAttempts++;
}
recordSuccessfulRecovery() {
this.metrics.successfulRecoveries++;
}
getMetrics() {
return {
...this.metrics,
errorTypes: Object.fromEntries(this.metrics.errorTypes),
recoveryRate:
this.metrics.recoveryAttempts > 0
? (this.metrics.successfulRecoveries /
this.metrics.recoveryAttempts) *
100
: 0,
};
}
reset() {
this.metrics = {
totalErrors: 0,
errorTypes: new Map(),
errorTimeline: [],
recoveryAttempts: 0,
successfulRecoveries: 0,
};
}
}
// Usage
const errorMetrics = new ErrorMetrics();
// Monitor error metrics
setInterval(() => {
const metrics = errorMetrics.getMetrics();
console.log("Error metrics:", metrics);
}, 60000); // Every minute
class ErrorSimulator {
constructor(private client: any) {}
simulateConnectionError() {
// Simulate network disconnection
this.client.emit("disconnect", "NETWORK_ERROR");
}
simulateAuthenticationError() {
// Simulate authentication failure
this.client.emit("error", new HAIPAuthenticationError("Token expired"));
}
simulateFlowControlError() {
// Simulate flow control error
this.client.emit("error", new HAIPFlowControlError("No credits available"));
}
simulateTimeout() {
// Simulate operation timeout
this.client.emit("error", new Error("Operation timed out"));
}
}
// Usage in tests
const errorSimulator = new ErrorSimulator(client);
describe("Error Handling", () => {
test("should handle connection errors", async () => {
const reconnectionSpy = jest.spyOn(client, "connect");
errorSimulator.simulateConnectionError();
// Wait for reconnection attempt
await new Promise((resolve) => setTimeout(resolve, 1000));
expect(reconnectionSpy).toHaveBeenCalled();
});
test("should handle authentication errors", async () => {
const tokenRefreshSpy = jest.spyOn(client, "refreshToken");
errorSimulator.simulateAuthenticationError();
expect(tokenRefreshSpy).toHaveBeenCalled();
});
});
client.on("error", (error) => {
// Log error and implement recovery strategy
});
// Use exponential backoff for retries
const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
// Track error metrics and alert on high error rates
if (errorRate > 0.1) {
alert("High error rate detected");
}
// Show user-friendly error messages
showUserNotification("Connection lost. Reconnecting...");
// Fall back to offline mode when connection fails
if (connectionFailed) {
enableOfflineMode();
}
// Pattern 1: Retry with exponential backoff
async function retryWithBackoff<T>(
operation: () => Promise<T>,
maxAttempts: number = 3
): Promise<T> {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await operation();
} catch (error) {
if (attempt === maxAttempts) throw error;
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 30000);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw new Error("Max attempts exceeded");
}
// Pattern 2: Circuit breaker
const circuitBreaker = new CircuitBreaker();
// Pattern 3: Graceful degradation
class GracefulDegradation {
private fallbackMode = false;
async sendMessage(text: string) {
try {
return await client.sendTextMessage("USER", text, "user", runId);
} catch (error) {
if (this.shouldUseFallback(error)) {
return this.sendMessageFallback(text);
}
throw error;
}
}
private shouldUseFallback(error: Error): boolean {
return (
error.message.includes("CONNECTION_FAILED") ||
error.message.includes("AUTHENTICATION_FAILED")
);
}
private sendMessageFallback(text: string) {
// Store message locally, sync when connection restored
this.storeMessageLocally(text);
return "local-message-id";
}
private storeMessageLocally(text: string) {
// Implementation for local storage
}
}
Was this page helpful?