UNPKG

@nam088/nestjs-rabbitmq

Version:

A comprehensive RabbitMQ module for NestJS with decorator-based message handling

286 lines 11.6 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var ServiceDiscoveryService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.ServiceDiscoveryService = void 0; const crypto_1 = require("crypto"); const os_1 = require("os"); const common_1 = require("@nestjs/common"); const service_discovery_interface_1 = require("../interfaces/service-discovery.interface"); const rabbitmq_service_1 = require("./rabbitmq.service"); let ServiceDiscoveryService = ServiceDiscoveryService_1 = class ServiceDiscoveryService { constructor(rabbitMQService, options) { this.rabbitMQService = rabbitMQService; this.options = options; this.isRegistered = false; this.logger = new common_1.Logger(ServiceDiscoveryService_1.name); this.services = new Map(); this.serviceId = (0, crypto_1.randomUUID)(); this.logLevel = options.logLevel ?? 'error'; } async onModuleDestroy() { if (!this.options.enabled || !this.isRegistered) { return; } try { await this.deregisterService(); this.stopHeartbeat(); this.stopCleanup(); } catch (error) { this.logger.error('Failed to cleanup service discovery', error); } } async onModuleInit() { if (!this.options.enabled) { return; } try { await this.setupDiscovery(); await this.registerService(); this.startHeartbeat(); this.startCleanup(); } catch (error) { this.logger.error('Failed to initialize service discovery', error); } } getAllServices() { return Array.from(this.services.values()); } getCurrentServiceInfo() { return { status: 'healthy', healthCheckEndpoint: this.options.healthCheckEndpoint, host: this.options.host || (0, os_1.hostname)(), lastHeartbeat: new Date(), metadata: this.options.metadata || {}, port: this.options.port, registeredAt: new Date(), serviceName: this.options.serviceName || 'unknown', tags: this.options.tags || [], version: this.options.version || '1.0.0', serviceId: this.serviceId, }; } getExchangeName() { return this.options.discoveryExchange || 'service.discovery'; } getHealthyServices(serviceName) { return this.getServices({ status: 'healthy', serviceName, }); } getRandomHealthyService(serviceName) { const services = this.getHealthyServices(serviceName); if (services.length === 0) { return undefined; } return services[Math.floor(Math.random() * services.length)]; } getRoutingKey(type) { switch (type) { case service_discovery_interface_1.ServiceDiscoveryEventType.SERVICE_DEREGISTERED: return this.options.deregistrationRoutingKey || 'service.deregister'; case service_discovery_interface_1.ServiceDiscoveryEventType.SERVICE_HEARTBEAT: return this.options.heartbeatRoutingKey || 'service.heartbeat'; case service_discovery_interface_1.ServiceDiscoveryEventType.SERVICE_REGISTERED: return this.options.registrationRoutingKey || 'service.register'; default: return 'service.unknown'; } } getServiceById(serviceId) { return this.services.get(serviceId); } getServiceCount(serviceName) { if (serviceName) { return this.getServices({ serviceName }).length; } return this.services.size; } getServices(filter) { let services = this.getAllServices(); if (!filter) { return services; } if (filter.serviceName) { services = services.filter((s) => s.serviceName === filter.serviceName); } if (filter.version) { services = services.filter((s) => s.version === filter.version); } if (filter.status) { services = services.filter((s) => s.status === filter.status); } if (filter.tags && filter.tags.length > 0) { services = services.filter((s) => filter.tags.some((tag) => s.tags?.includes(tag))); } if (filter.metadata) { services = services.filter((s) => { if (!s.metadata) return false; return Object.entries(filter.metadata).every(([key, value]) => s.metadata[key] === value); }); } return services; } async setupDiscovery() { const exchange = this.getExchangeName(); await this.rabbitMQService.assertExchange(exchange, 'topic', { durable: true, }); const queue = `service.discovery.${this.serviceId}`; await this.rabbitMQService.assertQueue(queue, { autoDelete: true, exclusive: true }); await this.rabbitMQService.bindQueue(queue, exchange, '*'); await this.rabbitMQService.consume(queue, this.handleServiceEvent.bind(this)); this.info('Service discovery setup completed'); } async publishServiceEvent(type, service) { const exchange = this.getExchangeName(); const routingKey = this.getRoutingKey(type); const event = { type, service, timestamp: new Date(), }; await this.rabbitMQService.publish(exchange, routingKey, event); } hasService(serviceName) { return this.getServices({ serviceName }).length > 0; } cleanupDeadServices() { const timeout = this.options.serviceTimeout || 90000; const now = new Date(); for (const [serviceId, service] of this.services.entries()) { const timeSinceLastHeartbeat = now.getTime() - service.lastHeartbeat.getTime(); if (timeSinceLastHeartbeat > timeout) { this.warn(`Service ${service.serviceName} (${serviceId}) is considered dead, removing...`); this.services.delete(serviceId); } else if (timeSinceLastHeartbeat > timeout / 2 && service.status === 'healthy') { service.status = 'unhealthy'; this.warn(`Service ${service.serviceName} (${serviceId}) marked as unhealthy`); } } } debug(message) { if (this.shouldLog('debug')) this.logger.debug(message); } async deregisterService() { const serviceInfo = this.services.get(this.serviceId); if (!serviceInfo) { return; } await this.publishServiceEvent(service_discovery_interface_1.ServiceDiscoveryEventType.SERVICE_DEREGISTERED, serviceInfo); this.services.delete(this.serviceId); this.isRegistered = false; this.info(`Service deregistered: ${serviceInfo.serviceName} (${this.serviceId})`); } handleServiceEvent(event) { const { type, service } = event; if (service.serviceId === this.serviceId) { return; } switch (type) { case service_discovery_interface_1.ServiceDiscoveryEventType.SERVICE_DEREGISTERED: { this.services.delete(service.serviceId); this.info(`Service removed: ${service.serviceName} (${service.serviceId})`); break; } case service_discovery_interface_1.ServiceDiscoveryEventType.SERVICE_HEARTBEAT: { const existingService = this.services.get(service.serviceId); if (existingService) { existingService.lastHeartbeat = service.lastHeartbeat; existingService.status = service.status; } else { this.services.set(service.serviceId, service); } break; } case service_discovery_interface_1.ServiceDiscoveryEventType.SERVICE_REGISTERED: this.services.set(service.serviceId, service); this.info(`Service discovered: ${service.serviceName} (${service.serviceId})`); break; } } info(message) { if (this.shouldLog('log')) this.logger.log(message); } async registerService() { const serviceInfo = this.getCurrentServiceInfo(); await this.publishServiceEvent(service_discovery_interface_1.ServiceDiscoveryEventType.SERVICE_REGISTERED, serviceInfo); this.services.set(this.serviceId, serviceInfo); this.isRegistered = true; this.info(`Service registered: ${serviceInfo.serviceName} (${this.serviceId})`); } async sendHeartbeat() { const serviceInfo = this.services.get(this.serviceId); if (!serviceInfo) { return; } serviceInfo.lastHeartbeat = new Date(); serviceInfo.status = 'healthy'; await this.publishServiceEvent(service_discovery_interface_1.ServiceDiscoveryEventType.SERVICE_HEARTBEAT, serviceInfo); } shouldLog(level) { const order = { debug: 3, error: 0, log: 2, none: -1, warn: 1, }; return order[level] <= order[this.logLevel]; } startCleanup() { const interval = (this.options.heartbeatInterval || 30000) * 2; this.cleanupInterval = setInterval(() => { this.cleanupDeadServices(); }, interval); this.debug(`Cleanup started with interval: ${interval}ms`); } startHeartbeat() { const interval = this.options.heartbeatInterval || 30000; this.heartbeatInterval = setInterval(() => { this.sendHeartbeat().catch((error) => { this.logger.error('Failed to send heartbeat', error); }); }, interval); this.debug(`Heartbeat started with interval: ${interval}ms`); } stopCleanup() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = undefined; } } stopHeartbeat() { if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.heartbeatInterval = undefined; } } warn(message) { if (this.shouldLog('warn')) this.logger.warn(message); } }; exports.ServiceDiscoveryService = ServiceDiscoveryService; exports.ServiceDiscoveryService = ServiceDiscoveryService = ServiceDiscoveryService_1 = __decorate([ (0, common_1.Injectable)(), __metadata("design:paramtypes", [rabbitmq_service_1.RabbitMQService, Object]) ], ServiceDiscoveryService); //# sourceMappingURL=service-discovery.service.js.map