anon-identity
Version:
Decentralized identity framework with DIDs, Verifiable Credentials, and privacy-preserving selective disclosure
595 lines • 22.1 kB
JavaScript
"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