@logistically/events-nestjs
Version:
NestJS integration for @logistically/events v3 - Event-driven architecture with Redis Streams
322 lines (321 loc) • 14.2 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 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);