UNPKG

@logistically/events-nestjs

Version:

NestJS integration for @logistically/events v3 - Event-driven architecture with Redis Streams

322 lines (321 loc) 14.2 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 EventDiscoveryService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.EventDiscoveryService = void 0; const common_1 = require("@nestjs/common"); const core_1 = require("@nestjs/core"); const event_system_service_1 = require("./event-system.service"); const auto_event_handler_decorator_1 = require("../decorators/auto-event-handler.decorator"); const auto_events_decorator_1 = require("../decorators/auto-events.decorator"); let EventDiscoveryService = EventDiscoveryService_1 = class EventDiscoveryService { constructor(eventSystemService, reflector) { this.eventSystemService = eventSystemService; this.reflector = reflector; this.logger = new common_1.Logger(EventDiscoveryService_1.name); this.metadataScanner = new core_1.MetadataScanner(); this.consumer = null; this.autoRegisteredServices = new Set(); this.discoveryQueue = []; this.isInitialized = false; this.discoveryTimer = null; this.isProcessing = false; this.retryAttempts = new Map(); this.maxRetries = 5; this.baseRetryDelay = 100; // Start with 100ms } async onModuleInit() { this.logger.log('EventDiscoveryService onModuleInit called'); // Start the discovery loop only when needed this.startDiscoveryLoop(); } async onModuleDestroy() { this.cleanup(); } cleanup() { if (this.discoveryTimer) { clearInterval(this.discoveryTimer); this.discoveryTimer = null; } this.discoveryQueue = []; this.retryAttempts.clear(); } /** * Start discovery loop only when there are pending services * Uses event-driven approach instead of continuous polling */ startDiscoveryLoop() { // Only start timer if there are pending services if (this.discoveryQueue.length > 0 && !this.discoveryTimer) { this.discoveryTimer = setInterval(async () => { if (this.discoveryQueue.length > 0 && !this.isProcessing) { await this.processDiscoveryQueue(); } }, 1000); } } /** * Stop discovery loop when no services are pending */ stopDiscoveryLoop() { if (this.discoveryTimer && this.discoveryQueue.length === 0) { clearInterval(this.discoveryTimer); this.discoveryTimer = null; this.logger.debug('Discovery loop stopped - no pending services'); } } /** * Process the discovery queue with optimized batching and error handling */ async processDiscoveryQueue() { if (this.discoveryQueue.length === 0 || this.isProcessing) { return; } this.isProcessing = true; try { const consumer = await this.getConsumer(); if (!consumer) { return; // Consumer not ready yet } this.logger.log(`Processing discovery queue with ${this.discoveryQueue.length} services...`); // Process in batches for better performance const batchSize = 10; const batches = this.chunkArray(this.discoveryQueue, batchSize); let totalRegistered = 0; let failedServices = []; for (const batch of batches) { const batchPromises = batch.map(async ({ instance }) => { try { const registeredCount = await this.registerEventHandlers(instance); totalRegistered += registeredCount; this.autoRegisteredServices.add(instance); this.retryAttempts.delete(instance); return { success: true, instance }; } catch (error) { this.logger.error(`Failed to auto-register ${instance.constructor.name}:`, error); failedServices.push({ instance, error }); return { success: false, instance, error }; } }); // Wait for batch to complete before processing next batch await Promise.allSettled(batchPromises); } // Handle failed services with exponential backoff this.handleFailedServices(failedServices); // Clear successfully processed services this.discoveryQueue = this.discoveryQueue.filter(({ instance }) => !this.autoRegisteredServices.has(instance)); if (totalRegistered > 0) { this.logger.log(`Successfully registered ${totalRegistered} event handlers from ${batches.length} batches`); } // Stop discovery loop if no more pending services this.stopDiscoveryLoop(); } catch (error) { this.logger.debug('Consumer not ready yet, services will be processed later'); } finally { this.isProcessing = false; } } /** * Handle failed services with exponential backoff retry logic */ handleFailedServices(failedServices) { for (const { instance } of failedServices) { const attempts = this.retryAttempts.get(instance) || 0; if (attempts < this.maxRetries) { const delay = this.baseRetryDelay * Math.pow(2, attempts); this.retryAttempts.set(instance, attempts + 1); this.logger.debug(`Scheduling retry for ${instance.constructor.name} in ${delay}ms (attempt ${attempts + 1})`); setTimeout(() => { if (!this.autoRegisteredServices.has(instance)) { this.discoveryQueue.push({ instance, timestamp: Date.now() }); this.startDiscoveryLoop(); // Restart loop if needed } }, delay); } else { this.logger.error(`Max retries exceeded for ${instance.constructor.name}, removing from queue`); this.retryAttempts.delete(instance); } } } /** * Utility method to chunk array into batches */ chunkArray(array, size) { const chunks = []; for (let i = 0; i < array.length; i += size) { chunks.push(array.slice(i, i + size)); } return chunks; } /** * Add a service to be auto-registered when the event system is ready * This is the main entry point for automatic discovery */ addServiceForAutoRegistration(instance) { if (this.autoRegisteredServices.has(instance)) { return; // Already registered } // Check if this service has @AutoEvents decorator const autoRegisterMetadata = this.reflector.get(auto_events_decorator_1.AUTO_REGISTER_EVENTS_METADATA, instance.constructor); if (!autoRegisterMetadata?.enabled) { this.logger.debug(`Auto-registration not enabled for ${instance.constructor.name}`); return; } // Add to discovery queue this.discoveryQueue.push({ instance, timestamp: Date.now() }); this.logger.debug(`Added ${instance.constructor.name} to discovery queue (${this.discoveryQueue.length} pending)`); // Start discovery loop if not already running this.startDiscoveryLoop(); // Try to process immediately if consumer is ready this.processDiscoveryQueue(); } /** * Manually trigger registration of all pending services * This can be called by the EventModuleScanner */ async triggerRegistration() { this.logger.log('EventDiscoveryService: Manually triggering registration of pending services...'); await this.processDiscoveryQueue(); } /** * Get discovery statistics */ getDiscoveryStats() { return { pending: this.discoveryQueue.length, registered: this.autoRegisteredServices.size, queueLength: this.discoveryQueue.length, retryCount: this.retryAttempts.size }; } async getConsumer() { if (!this.consumer) { await this.initializeConsumer(); } return this.consumer; } async initializeConsumer(maxAttempts = 10) { let attempts = 0; while (attempts < maxAttempts) { try { const eventSystem = this.eventSystemService.getEventSystem(); this.consumer = eventSystem.consumer; this.isInitialized = true; this.logger.log('Event discovery service initialized successfully'); // Now try to process any pending services await this.processDiscoveryQueue(); return; } catch (error) { attempts++; if (attempts >= maxAttempts) { throw error; } // Use exponential backoff for retries const delay = this.baseRetryDelay * Math.pow(2, attempts - 1); await new Promise(resolve => setTimeout(resolve, delay)); } } } /** * Register a single event handler with explicit metadata * This method is used by the EventModuleScanner for automatic discovery */ async registerHandler(instance, methodKey, metadata) { const consumer = await this.getConsumer(); if (!consumer) { this.logger.warn('Consumer not initialized, skipping handler registration'); return; } const eventType = metadata.eventType; const handler = instance[methodKey].bind(instance); try { consumer.subscribe(eventType, async (event) => { try { await handler(event); } catch (error) { this.logger.error(`Error handling event ${eventType} in ${instance.constructor.name}.${methodKey}:`, error); } }); this.logger.log(`Registered auto event handler: ${instance.constructor.name}.${methodKey} for ${eventType}`); } catch (error) { this.logger.error(`Failed to register auto handler for ${eventType}:`, error); } } /** * Manually register event handlers from a service instance * This method can be called by services that want to register their handlers */ async registerEventHandlers(instance) { const consumer = await this.getConsumer(); if (!consumer) { this.logger.warn('Consumer not initialized, skipping handler registration'); return 0; } // Check if this service has @AutoEvents decorator const autoRegisterMetadata = this.reflector.get(auto_events_decorator_1.AUTO_REGISTER_EVENTS_METADATA, instance.constructor); if (!autoRegisterMetadata?.enabled) { this.logger.debug(`Auto-registration not enabled for ${instance.constructor.name}`); return 0; } // Prevent double registration if (this.autoRegisteredServices.has(instance)) { this.logger.debug(`Service ${instance.constructor.name} already registered, skipping`); return 0; } // Use MetadataScanner like @nestjs/microservices does const instancePrototype = Object.getPrototypeOf(instance); const allMethodNames = this.metadataScanner.getAllMethodNames(instancePrototype); this.logger.debug(`Scanning methods for ${instance.constructor.name}: ${allMethodNames.join(', ')}`); let registeredCount = 0; for (const methodName of allMethodNames) { const method = instance[methodName]; if (typeof method === 'function') { this.logger.debug(`Checking method: ${methodName}`); // Try to get metadata using the helper function let metadata = (0, auto_event_handler_decorator_1.getAutoEventHandlerMetadata)(instance, methodName); // If not found, try using reflector directly if (!metadata) { metadata = this.reflector.get(auto_event_handler_decorator_1.AUTO_EVENT_HANDLER_METADATA, method); } this.logger.debug(`Metadata for ${methodName}:`, metadata); if (metadata && metadata.eventType) { await this.registerHandler(instance, methodName, metadata); registeredCount++; } } } if (registeredCount > 0) { this.logger.log(`Registered ${registeredCount} event handlers from ${instance.constructor.name}`); this.autoRegisteredServices.add(instance); } return registeredCount; } /** * Auto-register a service if it has @AutoEvents decorator * This method is called automatically by the service lifecycle */ async autoRegisterService(instance) { return this.registerEventHandlers(instance); } }; exports.EventDiscoveryService = EventDiscoveryService; exports.EventDiscoveryService = EventDiscoveryService = EventDiscoveryService_1 = __decorate([ (0, common_1.Injectable)(), __metadata("design:paramtypes", [event_system_service_1.EventSystemService, core_1.Reflector]) ], EventDiscoveryService);