UNPKG

anon-identity

Version:

Decentralized identity framework with DIDs, Verifiable Credentials, and privacy-preserving selective disclosure

595 lines 22.1 kB
"use strict"; /** * Connection Management System for MCP * * Handles provider connections, health monitoring, failover, and load balancing */ Object.defineProperty(exports, "__esModule", { value: true }); exports.FailoverManager = exports.RateLimiter = exports.LoadBalancer = exports.ProviderHealthMonitor = exports.MCPConnectionManager = void 0; const events_1 = require("events"); const types_1 = require("./types"); /** * Provider health monitor */ class ProviderHealthMonitor extends events_1.EventEmitter { constructor(config) { super(); this.config = config; this.healthChecks = new Map(); this.healthHistory = new Map(); this.maxHealthHistory = 10; } /** * Start monitoring provider health */ startMonitoring(providerId, connection) { if (this.healthChecks.has(providerId)) { this.stopMonitoring(providerId); } const interval = setInterval(async () => { try { const healthResult = await Promise.race([ connection.health(), this.createTimeout(this.config.timeout) ]); const isHealthy = healthResult.status === 'healthy'; this.recordHealthCheck(providerId, isHealthy); if (!isHealthy) { this.emit('unhealthy', providerId, healthResult); } } catch (error) { this.recordHealthCheck(providerId, false); this.emit('unhealthy', providerId, { status: 'unhealthy', error }); } }, this.config.interval); this.healthChecks.set(providerId, interval); } /** * Stop monitoring provider health */ stopMonitoring(providerId) { const interval = this.healthChecks.get(providerId); if (interval) { clearInterval(interval); this.healthChecks.delete(providerId); } } /** * Get provider health score (0-1) */ getHealthScore(providerId) { const history = this.healthHistory.get(providerId) || []; if (history.length === 0) return 1; // No history, assume healthy const healthyCount = history.filter(Boolean).length; return healthyCount / history.length; } /** * Record health check result */ recordHealthCheck(providerId, isHealthy) { const history = this.healthHistory.get(providerId) || []; history.push(isHealthy); if (history.length > this.maxHealthHistory) { history.shift(); } this.healthHistory.set(providerId, history); } /** * Create timeout promise */ createTimeout(ms) { return new Promise((_, reject) => { setTimeout(() => reject(new Error('Health check timeout')), ms); }); } /** * Stop all monitoring */ stopAll() { for (const providerId of this.healthChecks.keys()) { this.stopMonitoring(providerId); } } } exports.ProviderHealthMonitor = ProviderHealthMonitor; /** * Load balancer for distributing requests across providers */ class LoadBalancer { constructor() { this.requestCounts = new Map(); this.lastUsed = new Map(); } /** * Select best provider using round-robin with health weighting */ selectProvider(providers, healthMonitor, strategy = 'health-weighted') { if (providers.length === 0) return undefined; if (providers.length === 1) return providers[0]; switch (strategy) { case 'round-robin': return this.roundRobinSelection(providers); case 'least-connections': return this.leastConnectionsSelection(providers); case 'health-weighted': return this.healthWeightedSelection(providers, healthMonitor); default: return providers[0]; } } /** * Record request for load balancing metrics */ recordRequest(providerId) { const count = this.requestCounts.get(providerId) || 0; this.requestCounts.set(providerId, count + 1); this.lastUsed.set(providerId, Date.now()); } /** * Round-robin selection */ roundRobinSelection(providers) { // Find provider with oldest last used time let selected = providers[0]; let oldestTime = this.lastUsed.get(selected) || 0; for (const provider of providers) { const lastTime = this.lastUsed.get(provider) || 0; if (lastTime < oldestTime) { selected = provider; oldestTime = lastTime; } } return selected; } /** * Least connections selection */ leastConnectionsSelection(providers) { let selected = providers[0]; let minConnections = this.requestCounts.get(selected) || 0; for (const provider of providers) { const connections = this.requestCounts.get(provider) || 0; if (connections < minConnections) { selected = provider; minConnections = connections; } } return selected; } /** * Health-weighted selection */ healthWeightedSelection(providers, healthMonitor) { // Calculate weighted scores const scores = providers.map(provider => ({ provider, score: this.calculateProviderScore(provider, healthMonitor) })); // Sort by score (highest first) scores.sort((a, b) => b.score - a.score); // Return best provider return scores[0].provider; } /** * Calculate provider score for selection */ calculateProviderScore(provider, healthMonitor) { const healthScore = healthMonitor.getHealthScore(provider); const requestCount = this.requestCounts.get(provider) || 0; const lastUsed = this.lastUsed.get(provider) || 0; const timeSinceUsed = Date.now() - lastUsed; // Weight factors const healthWeight = 0.5; const loadWeight = 0.3; const freshnessWeight = 0.2; // Calculate component scores const normalizedLoad = Math.max(0, 1 - (requestCount / 100)); // Assume 100 as high load const normalizedFreshness = Math.min(1, timeSinceUsed / (5 * 60 * 1000)); // 5 minutes return (healthScore * healthWeight + normalizedLoad * loadWeight + normalizedFreshness * freshnessWeight); } } exports.LoadBalancer = LoadBalancer; /** * Rate limiter for provider requests */ class RateLimiter { constructor() { this.requestCounts = new Map(); this.tokenCounts = new Map(); } /** * Check if request is allowed under rate limits */ isAllowed(providerId, rateLimits, tokens = 0) { const now = Date.now(); const minuteAgo = now - 60 * 1000; const dayAgo = now - 24 * 60 * 60 * 1000; // Clean old entries this.cleanOldEntries(providerId, minuteAgo, dayAgo); const recentRequests = this.requestCounts.get(providerId) || []; const recentTokens = this.tokenCounts.get(providerId) || []; // Check request limits const requestsThisMinute = recentRequests.filter(time => time > minuteAgo).length; const requestsToday = recentRequests.filter(time => time > dayAgo).length; if (requestsThisMinute >= rateLimits.requestsPerMinute) return false; if (requestsToday >= rateLimits.requestsPerDay) return false; // Check token limits const tokensThisMinute = recentTokens .filter(entry => entry > minuteAgo) .reduce((sum, entry) => sum + entry.tokens, 0); const tokensToday = recentTokens .filter(entry => entry > dayAgo) .reduce((sum, entry) => sum + entry.tokens, 0); if (tokensThisMinute + tokens > rateLimits.tokensPerMinute) return false; if (tokensToday + tokens > rateLimits.tokensPerDay) return false; return true; } /** * Record request for rate limiting */ recordRequest(providerId, tokens = 0) { const now = Date.now(); // Record request const requests = this.requestCounts.get(providerId) || []; requests.push(now); this.requestCounts.set(providerId, requests); // Record tokens if (tokens > 0) { const tokenEntries = this.tokenCounts.get(providerId) || []; tokenEntries.push({ time: now, tokens }); this.tokenCounts.set(providerId, tokenEntries); } } /** * Clean old entries */ cleanOldEntries(providerId, minuteAgo, dayAgo) { // Clean request counts const requests = this.requestCounts.get(providerId) || []; const recentRequests = requests.filter(time => time > dayAgo); this.requestCounts.set(providerId, recentRequests); // Clean token counts const tokens = this.tokenCounts.get(providerId) || []; const recentTokens = tokens.filter((entry) => entry.time > dayAgo); this.tokenCounts.set(providerId, recentTokens); } /** * Get current usage stats */ getUsageStats(providerId, rateLimits) { const now = Date.now(); const minuteAgo = now - 60 * 1000; const dayAgo = now - 24 * 60 * 60 * 1000; const requests = this.requestCounts.get(providerId) || []; const tokens = this.tokenCounts.get(providerId) || []; const requestsThisMinute = requests.filter(time => time > minuteAgo).length; const requestsToday = requests.filter(time => time > dayAgo).length; const tokensThisMinute = tokens .filter((entry) => entry.time > minuteAgo) .reduce((sum, entry) => sum + entry.tokens, 0); const tokensToday = tokens .filter((entry) => entry.time > dayAgo) .reduce((sum, entry) => sum + entry.tokens, 0); return { requestsPerMinute: { used: requestsThisMinute, limit: rateLimits.requestsPerMinute }, tokensPerMinute: { used: tokensThisMinute, limit: rateLimits.tokensPerMinute }, requestsPerDay: { used: requestsToday, limit: rateLimits.requestsPerDay }, tokensPerDay: { used: tokensToday, limit: rateLimits.tokensPerDay } }; } } exports.RateLimiter = RateLimiter; /** * Failover manager for handling provider failures */ class FailoverManager extends events_1.EventEmitter { constructor() { super(...arguments); this.failoverHistory = new Map(); this.maxFailoverAttempts = 3; this.failoverWindow = 5 * 60 * 1000; // 5 minutes } /** * Handle provider failure and attempt failover */ async handleFailure(failedProvider, availableProviders, request, error) { this.recordFailure(failedProvider); // Check if we should attempt failover if (!this.shouldAttemptFailover(failedProvider) || availableProviders.length === 0) { return { provider: failedProvider, shouldRetry: false }; } // Find alternative provider const alternativeProviders = availableProviders.filter(p => p !== failedProvider); if (alternativeProviders.length === 0) { return { provider: failedProvider, shouldRetry: false }; } // Select best alternative based on error type and request priority const selectedProvider = this.selectFailoverProvider(alternativeProviders, request, error); this.emit('failover', { from: failedProvider, to: selectedProvider, reason: error.code, request: request.id }); return { provider: selectedProvider, shouldRetry: true }; } /** * Record provider failure */ recordFailure(providerId) { const failures = this.failoverHistory.get(providerId) || []; failures.push(new Date()); // Keep only recent failures const cutoff = new Date(Date.now() - this.failoverWindow); const recentFailures = failures.filter(date => date > cutoff); this.failoverHistory.set(providerId, recentFailures); } /** * Check if failover should be attempted */ shouldAttemptFailover(providerId) { const failures = this.failoverHistory.get(providerId) || []; return failures.length < this.maxFailoverAttempts; } /** * Select best provider for failover */ selectFailoverProvider(providers, request, error) { // For now, select first available provider // In production, this would consider: // - Provider capabilities matching request type // - Historical reliability // - Current load // - Geographic proximity return providers[0]; } /** * Reset failure history for provider */ resetFailureHistory(providerId) { this.failoverHistory.delete(providerId); } /** * Get failure stats */ getFailureStats(providerId) { const failures = this.failoverHistory.get(providerId) || []; const recentFailures = failures.length; const lastFailure = failures.length > 0 ? failures[failures.length - 1] : undefined; const failoverAvailable = recentFailures < this.maxFailoverAttempts; return { recentFailures, lastFailure, failoverAvailable }; } } exports.FailoverManager = FailoverManager; /** * Main connection manager */ class MCPConnectionManager extends events_1.EventEmitter { constructor(config) { super(); this.config = config; this.connections = new Map(); this.providers = new Map(); this.healthMonitor = new ProviderHealthMonitor({ interval: 30000, // 30 seconds timeout: 5000 // 5 seconds }); this.loadBalancer = new LoadBalancer(); this.rateLimiter = new RateLimiter(); this.failoverManager = new FailoverManager(); this.setupEventHandlers(); } /** * Add provider and establish connection */ async addProvider(provider, connection) { this.providers.set(provider.id, provider); this.connections.set(provider.id, connection); // Start health monitoring this.healthMonitor.startMonitoring(provider.id, connection); // Setup connection event handlers connection.on('disconnect', () => this.handleDisconnection(provider.id)); connection.on('error', (error) => this.handleConnectionError(provider.id, error)); this.emit('provider_added', provider.id); } /** * Remove provider and close connection */ async removeProvider(providerId) { const connection = this.connections.get(providerId); if (connection) { await connection.disconnect(); } this.connections.delete(providerId); this.providers.delete(providerId); this.healthMonitor.stopMonitoring(providerId); this.emit('provider_removed', providerId); } /** * Send request with load balancing and failover */ async sendRequest(request, preferredProvider) { const availableProviders = this.getAvailableProviders(); if (availableProviders.length === 0) { throw new types_1.MCPError({ code: types_1.MCPErrorCode.PROVIDER_UNAVAILABLE, message: 'No providers available', timestamp: new Date(), retryable: false }); } let selectedProvider = preferredProvider; // If no preferred provider or preferred is not available, use load balancer if (!selectedProvider || !availableProviders.includes(selectedProvider)) { selectedProvider = this.loadBalancer.selectProvider(availableProviders, this.healthMonitor) || undefined; } if (!selectedProvider) { throw new types_1.MCPError({ code: types_1.MCPErrorCode.PROVIDER_UNAVAILABLE, message: 'No suitable provider found', timestamp: new Date(), retryable: false }); } return this.sendRequestToProvider(request, selectedProvider); } /** * Send request to specific provider with rate limiting and failover */ async sendRequestToProvider(request, providerId) { const provider = this.providers.get(providerId); const connection = this.connections.get(providerId); if (!provider || !connection) { throw new types_1.MCPError({ code: types_1.MCPErrorCode.PROVIDER_UNAVAILABLE, message: `Provider ${providerId} not available`, timestamp: new Date(), provider: providerId, retryable: true }); } // Check rate limits const estimatedTokens = Math.ceil((request.prompt?.length || 0) / 4); // Rough estimate if (!this.rateLimiter.isAllowed(providerId, provider.rateLimits, estimatedTokens)) { throw new types_1.MCPError({ code: types_1.MCPErrorCode.RATE_LIMITED, message: `Rate limit exceeded for provider ${providerId}`, timestamp: new Date(), provider: providerId, retryable: true }); } try { // Record request for load balancing this.loadBalancer.recordRequest(providerId); this.rateLimiter.recordRequest(providerId, estimatedTokens); // Send request const response = await connection.sendRequest(request); // Record actual token usage if available if (response.usage) { this.rateLimiter.recordRequest(providerId, response.usage.totalTokens); } return response; } catch (error) { const mcpError = error instanceof types_1.MCPError ? error : new types_1.MCPError({ code: types_1.MCPErrorCode.PROVIDER_ERROR, message: error.message, timestamp: new Date(), provider: providerId, retryable: true }); // Attempt failover if appropriate const failoverResult = await this.failoverManager.handleFailure(providerId, this.getAvailableProviders(), request, mcpError); if (failoverResult.shouldRetry && failoverResult.provider !== providerId) { return this.sendRequestToProvider(request, failoverResult.provider); } throw mcpError; } } /** * Get list of available (connected and healthy) providers */ getAvailableProviders() { return Array.from(this.providers.keys()).filter(providerId => { const connection = this.connections.get(providerId); const healthScore = this.healthMonitor.getHealthScore(providerId); return connection && healthScore > 0.5; // Require minimum health score }); } /** * Get connection for provider */ getConnection(providerId) { return this.connections.get(providerId); } /** * Get provider information */ getProvider(providerId) { return this.providers.get(providerId); } /** * Get comprehensive health status */ async getHealthStatus() { const status = new Map(); for (const [providerId, provider] of this.providers.entries()) { const connection = this.connections.get(providerId); const healthScore = this.healthMonitor.getHealthScore(providerId); const failureStats = this.failoverManager.getFailureStats(providerId); const usageStats = this.rateLimiter.getUsageStats(providerId, provider.rateLimits); let connectionHealth = { status: 'disconnected' }; if (connection) { try { connectionHealth = await connection.health(); } catch (error) { connectionHealth = { status: 'unhealthy', error: error.message }; } } status.set(providerId, { provider: provider.name, connection: connectionHealth, healthScore, failureStats, usageStats, available: this.getAvailableProviders().includes(providerId) }); } return status; } /** * Handle connection disconnection */ handleDisconnection(providerId) { this.emit('provider_disconnected', providerId); } /** * Handle connection errors */ handleConnectionError(providerId, error) { this.emit('provider_error', providerId, error); } /** * Setup event handlers */ setupEventHandlers() { this.healthMonitor.on('unhealthy', (providerId, healthResult) => { this.emit('provider_unhealthy', providerId, healthResult); }); this.failoverManager.on('failover', (event) => { this.emit('failover', event); }); } /** * Shutdown connection manager */ async shutdown() { // Stop health monitoring this.healthMonitor.stopAll(); // Close all connections const disconnectPromises = Array.from(this.connections.values()).map(connection => connection.disconnect()); await Promise.all(disconnectPromises); // Clear all maps this.connections.clear(); this.providers.clear(); this.removeAllListeners(); } } exports.MCPConnectionManager = MCPConnectionManager; //# sourceMappingURL=connection-manager.js.map