/** * Service Registry - Catálogo central de serviços * Arcadia Kernel - Módulo de Service Discovery */ import { EventEmitter } from 'events'; import { RegisteredService, ServiceFilter, RegistryEvent, RegistryConfig, DiscoveryProvider } from './types'; import { HealthStatus } from '../types'; export interface ServiceRegistryOptions { config?: Partial; discoveryProviders?: DiscoveryProvider[]; } const DEFAULT_CONFIG: RegistryConfig = { enableAutoDiscovery: true, discoveryInterval: 30000, // 30s healthCheckInterval: 15000, // 15s cleanupInterval: 60000, // 1min maxStaleAge: 300000, // 5min persistToDatabase: false, }; export class ServiceRegistry extends EventEmitter { private services: Map = new Map(); private config: RegistryConfig; private discoveryProviders: DiscoveryProvider[] = []; private discoveryTimer?: NodeJS.Timeout; private healthCheckTimer?: NodeJS.Timeout; private cleanupTimer?: NodeJS.Timeout; private isRunning = false; constructor(options: ServiceRegistryOptions = {}) { super(); this.config = { ...DEFAULT_CONFIG, ...options.config }; this.discoveryProviders = options.discoveryProviders || []; } /** * Inicia o registry e discovery */ async start(): Promise { if (this.isRunning) return; console.log('[Registry] Iniciando Service Registry...'); // Verifica quais providers estão disponíveis const availableProviders: DiscoveryProvider[] = []; for (const provider of this.discoveryProviders) { try { const available = await provider.isAvailable(); if (available) { availableProviders.push(provider); console.log(`[Registry] Provider disponível: ${provider.name}`); } else { console.log(`[Registry] Provider indisponível: ${provider.name}`); } } catch (error) { console.warn(`[Registry] Erro ao verificar provider ${provider.name}:`, error); } } this.discoveryProviders = availableProviders; // Executa discovery inicial await this.runDiscovery(); // Inicia timers if (this.config.enableAutoDiscovery) { this.discoveryTimer = setInterval(() => this.runDiscovery(), this.config.discoveryInterval); } this.healthCheckTimer = setInterval(() => this.runHealthChecks(), this.config.healthCheckInterval); this.cleanupTimer = setInterval(() => this.cleanupStaleServices(), this.config.cleanupInterval); this.isRunning = true; console.log(`[Registry] Registry iniciado com ${this.services.size} serviços`); } /** * Para o registry */ stop(): void { if (!this.isRunning) return; console.log('[Registry] Parando Service Registry...'); if (this.discoveryTimer) clearInterval(this.discoveryTimer); if (this.healthCheckTimer) clearInterval(this.healthCheckTimer); if (this.cleanupTimer) clearInterval(this.cleanupTimer); // Para os watchers dos providers for (const provider of this.discoveryProviders) { provider.stop?.(); } this.isRunning = false; console.log('[Registry] Registry parado'); } /** * Registra um serviço manualmente */ register(service: RegisteredService): void { const existing = this.services.get(service.id); if (existing) { // Atualiza serviço existente this.services.set(service.id, { ...service, metadata: { ...service.metadata, lastUpdated: new Date(), }, }); this.emitEvent('service:updated', service); } else { // Novo serviço this.services.set(service.id, service); this.emitEvent('service:registered', service); } } /** * Remove um serviço do registry */ unregister(serviceId: string): boolean { const service = this.services.get(serviceId); if (!service) return false; this.services.delete(serviceId); this.emitEvent('service:unregistered', undefined, serviceId); return true; } /** * Busca um serviço pelo ID */ getService(serviceId: string): RegisteredService | undefined { return this.services.get(serviceId); } /** * Lista todos os serviços (com filtros opcionais) */ getServices(filter?: ServiceFilter): RegisteredService[] { let services = Array.from(this.services.values()); if (filter) { if (filter.category) { services = services.filter(s => s.category === filter.category); } if (filter.type) { services = services.filter(s => s.type === filter.type); } if (filter.requiresIA !== undefined) { services = services.filter(s => s.requiresIA === filter.requiresIA); } if (filter.capability) { services = services.filter(s => s.capabilities.some(c => c.name === filter.capability) ); } if (filter.source) { services = services.filter(s => s.metadata.source === filter.source); } if (filter.healthyOnly) { services = services.filter(s => s.healthStatus === 'healthy'); } } return services; } /** * Busca serviços por categoria */ getByCategory(category: string): RegisteredService[] { return this.getServices({ category: category as any }); } /** * Busca serviço por capability */ getByCapability(capabilityName: string): RegisteredService[] { return this.getServices({ capability: capabilityName }); } /** * Atualiza status de saúde de um serviço */ updateHealth(serviceId: string, status: HealthStatus): void { const service = this.services.get(serviceId); if (!service) return; const previousHealth = service.healthStatus; if (previousHealth !== status) { service.healthStatus = status; service.lastHealthCheck = new Date(); this.emitEvent('service:health_changed', service, serviceId, previousHealth, status); } } /** * Retorna estatísticas do registry */ getStats() { const services = Array.from(this.services.values()); const byCategory: Record = {}; for (const service of services) { byCategory[service.category] = (byCategory[service.category] || 0) + 1; } return { total: services.length, healthy: services.filter(s => s.healthStatus === 'healthy').length, unhealthy: services.filter(s => s.healthStatus === 'unhealthy').length, unknown: services.filter(s => s.healthStatus === 'unknown').length, byCategory, bySource: this.groupBySource(services), }; } /** * Executa discovery em todos os providers */ private async runDiscovery(): Promise { for (const provider of this.discoveryProviders) { try { const discoveredServices = await provider.discover(); for (const service of discoveredServices) { const existing = this.services.get(service.id); if (!existing) { this.register(service); } else { // Atualiza metadata mas mantém health status this.register({ ...service, healthStatus: existing.healthStatus, lastHealthCheck: existing.lastHealthCheck, }); } } } catch (error) { console.error(`[Registry] Erro no discovery ${provider.name}:`, error); } } } /** * Executa health checks em todos os serviços */ private async runHealthChecks(): Promise { for (const service of this.services.values()) { // Só verifica serviços que não são externos ou que estão habilitados if (!service.enabled) continue; try { const isHealthy = await this.checkServiceHealth(service); const newStatus: HealthStatus = isHealthy ? 'healthy' : 'unhealthy'; this.updateHealth(service.id, newStatus); } catch (error) { this.updateHealth(service.id, 'unhealthy'); } } } /** * Verifica saúde de um serviço específico */ private async checkServiceHealth(service: RegisteredService): Promise { try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); const response = await fetch(`${service.endpoint}${service.healthCheckPath}`, { signal: controller.signal, }); clearTimeout(timeout); return response.ok; } catch { return false; } } /** * Remove serviços stale (sem update há muito tempo) */ private cleanupStaleServices(): void { const now = Date.now(); const toRemove: string[] = []; for (const [id, service] of this.services) { // Só remove serviços descobertos automaticamente (não manuais) if (service.metadata.source === 'manual') continue; const age = now - service.metadata.lastUpdated.getTime(); if (age > this.config.maxStaleAge) { toRemove.push(id); } } for (const id of toRemove) { console.log(`[Registry] Removendo serviço stale: ${id}`); this.unregister(id); } } /** * Agrupa serviços por source */ private groupBySource(services: RegisteredService[]): Record { const result: Record = {}; for (const service of services) { const source = service.metadata.source; result[source] = (result[source] || 0) + 1; } return result; } /** * Emite evento do registry */ private emitEvent( type: RegistryEvent['type'], service?: RegisteredService, serviceId?: string, previousHealth?: HealthStatus, newHealth?: HealthStatus ): void { const event: RegistryEvent = { type, timestamp: new Date(), serviceId: service?.id || serviceId || 'unknown', service, previousHealth, newHealth, }; this.emit('event', event); this.emit(type, event); } /** * Adiciona um provider de discovery dinamicamente */ addDiscoveryProvider(provider: DiscoveryProvider): void { this.discoveryProviders.push(provider); if (this.isRunning) { provider.isAvailable().then(available => { if (available) { this.runDiscovery(); } }); } } /** * Retorna se o registry está rodando */ running(): boolean { return this.isRunning; } }