UNPKG

digital-samba-mcp-server

Version:

Digital Samba MCP Server - Model Context Protocol server for Digital Samba's video conferencing API

312 lines 13 kB
/** * Digital Samba API Client with Circuit Breaker Integration * * This module extends the core DigitalSambaApiClient with circuit breaker pattern * integration for improved fault tolerance and resilience. * * Key features: * - Automatic protection of all API requests with circuit breakers * - Per-endpoint circuit isolation to prevent cascading failures * - Configurable fallback mechanisms for when circuits are open * - Metrics integration for circuit state and event monitoring * * @module digital-samba-api-circuit-breaker * @author Digital Samba Team * @version 0.1.0 */ // Local modules import { DigitalSambaApiClient } from './digital-samba-api.js'; import { ApiRequestError, ApiResponseError } from './errors.js'; import logger from './logger.js'; import { circuitBreakerRegistry } from './circuit-breaker.js'; /** * Default circuit breaker options */ const DEFAULT_CIRCUIT_OPTIONS = { failureThreshold: 3, resetTimeout: 30000, // 30 seconds successThreshold: 2, requestTimeout: 15000, // 15 seconds initialRequestTimeout: 60000, // 60 seconds for the initial request isFailure: (error) => { // Only count server errors (5xx) and network errors as circuit failures if (error instanceof ApiResponseError) { return error.statusCode >= 500; } if (error instanceof ApiRequestError) { return true; // Network errors break the circuit } return false; // Other errors (validation, not found, etc.) don't break the circuit } }; // We don't need a specific type for fallback function as we're using the generic type parameters /** * Circuit breaker wrapper for the Digital Samba API client */ export class CircuitBreakerApiClient { /** * Creates a circuit breaker wrapper for the Digital Samba API client * * @param apiClient The Digital Samba API client to wrap * @param circuitPrefix Prefix for circuit breaker names (default: 'api') * @param defaultOptions Default options for all circuit breakers */ constructor(apiClient, circuitPrefix = 'api', defaultOptions = {}) { this.apiClient = apiClient; this.circuitPrefix = circuitPrefix; this.defaultOptions = { ...DEFAULT_CIRCUIT_OPTIONS, ...defaultOptions }; logger.debug('Created Circuit Breaker API Client', { circuitPrefix, defaultOptions: this.defaultOptions }); } /** * Initialize the API connection * This is a special method that bypasses timeouts for the initial request * and implements retries for better resilience during startup */ async initializeConnection() { const maxRetries = 3; let retryCount = 0; const retryDelay = 1000; // 1 second between retries // Helper function to delay execution const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); // Debug flag for additional logging const debugInitialization = process.env.DEBUG_INITIALIZATION === 'true'; if (debugInitialization) { logger.info('Starting API connection initialization with enhanced debugging'); } // Retry loop while (retryCount < maxRetries) { try { // Create a special circuit breaker for initialization const circuit = this.createCircuitBreaker('initialize', { // Fail fast on initialization errors failureThreshold: 1, // Don't use timeouts for initialization requestTimeout: undefined, initialRequestTimeout: undefined }, true); // Force no timeout logger.info('Attempting API connection initialization', { attempt: retryCount + 1, maxRetries, apiBaseUrl: this.apiBaseUrl }); // Make a simple request to verify connectivity await circuit.exec(async () => { if (debugInitialization) { logger.info('Executing API initialization request', { apiBaseUrl: this.apiBaseUrl, retry: retryCount }); } // Check API key before making requests const apiKey = this.getApiKey(); if (!apiKey) { logger.warn('No API key available for initialization'); throw new Error('No API key available for initialization'); } try { // Try something simple like fetching default settings // or just checking if the API base URL is reachable if (debugInitialization) { logger.info(`Testing API connectivity to ${this.apiBaseUrl}`); } const response = await fetch(this.apiBaseUrl, { method: 'HEAD', headers: { 'Authorization': `Bearer ${apiKey}` } }); if (debugInitialization) { logger.info(`HEAD request status: ${response.status}`); } if (!response.ok) { throw new Error(`API HEAD request failed with status ${response.status}`); } return response; } catch (error) { // If we can't reach the base URL, try a specific endpoint if (debugInitialization) { logger.info(`HEAD request failed, trying GET /rooms endpoint`); } const response = await fetch(`${this.apiBaseUrl}/rooms`, { headers: { 'Authorization': `Bearer ${apiKey}` } }); if (debugInitialization) { logger.info(`GET /rooms request status: ${response.status}`); } if (!response.ok) { throw new Error(`API GET /rooms request failed with status ${response.status}`); } return response; } }, [], // No arguments needed true, // Force no timeout true // Mark as initialization request ); logger.info('API connection initialization successful'); return true; } catch (error) { retryCount++; logger.warn(`API connection initialization failed (attempt ${retryCount}/${maxRetries})`, { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, apiBaseUrl: this.apiBaseUrl }); if (retryCount < maxRetries) { logger.info(`Retrying API connection in ${retryDelay}ms...`); await delay(retryDelay); } } } logger.error(`Failed to initialize API connection after ${maxRetries} attempts`, { apiBaseUrl: this.apiBaseUrl }); return false; } /** * Create a circuit breaker for a specific API endpoint * * @param endpoint The API endpoint name * @param options Additional circuit breaker options * @param forceNoTimeout If true, disables timeout for this endpoint * @returns A circuit breaker instance */ createCircuitBreaker(endpoint, options = {}, forceNoTimeout = false) { const name = `${this.circuitPrefix}.${endpoint}`; return circuitBreakerRegistry.getOrCreate({ name, ...this.defaultOptions, ...options }); } /** * Get the API key from context or direct value * * This method retrieves the API key using a prioritized approach: * 1. First tries to get the API key from the ApiKeyContext (for session-based auth) * 2. If not found, falls back to using the direct API key if provided during construction * 3. If neither source provides an API key, throws an AuthenticationError * * @protected * @returns {string} The API key to use for authentication * @throws {AuthenticationError} If no API key is available from any source */ getApiKey() { return this.apiClient['getApiKey'](); } /** * Get base URL of the API */ get apiBaseUrl() { return this.apiClient.apiBaseUrl; } /** * Get default room settings */ async getDefaultRoomSettings() { const circuit = this.createCircuitBreaker('getDefaultRoomSettings'); return circuit.exec(() => this.apiClient.getDefaultRoomSettings(), []); } /** * Update default room settings */ async updateDefaultRoomSettings(settings) { const circuit = this.createCircuitBreaker('updateDefaultRoomSettings'); return circuit.exec(() => this.apiClient.updateDefaultRoomSettings(settings), [settings]); } // Rooms /** * List all rooms */ async listRooms(params) { const circuit = this.createCircuitBreaker('listRooms', { // Correctly type the fallback function to match the circuit breaker's exec method signature fallback: async (_args) => { const response = { data: [], total_count: 0, length: 0, map: function (callback) { return this.data.map(callback); } }; return response; } }); return circuit.exec(() => this.apiClient.listRooms(params), [params]); } /** * Get details for a specific room */ async getRoom(roomId) { const circuit = this.createCircuitBreaker('getRoom'); return circuit.exec(() => this.apiClient.getRoom(roomId), [roomId]); } /** * Create a new room */ async createRoom(settings) { const circuit = this.createCircuitBreaker('createRoom'); return circuit.exec(() => this.apiClient.createRoom(settings), [settings]); } /** * Update an existing room */ async updateRoom(roomId, settings) { const circuit = this.createCircuitBreaker('updateRoom'); return circuit.exec(() => this.apiClient.updateRoom(roomId, settings), [roomId, settings]); } /** * Delete a room */ async deleteRoom(roomId, options) { const circuit = this.createCircuitBreaker('deleteRoom'); return circuit.exec(() => this.apiClient.deleteRoom(roomId, options), [roomId, options]); } /** * Generate a token for joining a room */ async generateRoomToken(roomId, options) { const circuit = this.createCircuitBreaker('generateRoomToken'); return circuit.exec(() => this.apiClient.generateRoomToken(roomId, options), [roomId, options]); } /** * Delete all resources for a room */ async deleteRoomResources(roomId) { const circuit = this.createCircuitBreaker('deleteRoomResources'); return circuit.exec(() => this.apiClient.deleteRoomResources(roomId), [roomId]); } // Factory method to create a circuit breaker API client from a standard client /** * Create a circuit breaker API client from a standard Digital Samba API client * * @param apiClient The Digital Samba API client to wrap * @param options Circuit breaker options * @returns A circuit breaker wrapped API client */ static withCircuitBreaker(apiClient, options = {}) { return new CircuitBreakerApiClient(apiClient, options.circuitPrefix, options.defaultOptions); } /** * Create a new circuit breaker API client with the given API key * * @param apiKey API key for Digital Samba API * @param apiBaseUrl Base URL for Digital Samba API * @param cache Optional cache for API responses * @param options Circuit breaker options * @returns A circuit breaker API client */ static createWithApiKey(apiKey, apiBaseUrl, cache, options = {}) { const apiClient = new DigitalSambaApiClient(apiKey, apiBaseUrl, cache); return CircuitBreakerApiClient.withCircuitBreaker(apiClient, options); } } export default CircuitBreakerApiClient; //# sourceMappingURL=digital-samba-api-circuit-breaker.js.map