@nam088/nestjs-rabbitmq
Version:
A comprehensive RabbitMQ module for NestJS with decorator-based message handling
286 lines • 11.6 kB
JavaScript
"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