UNPKG

whatsapp-crm-common

Version:

Componentes compartidos para servicios de WhatsApp CRM - Common utilities and types for WhatsApp CRM system

507 lines 19.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WhatsAppMessageQueue = void 0; const bullmq_1 = require("bullmq"); const event_types_1 = require("../../events/types/event-types"); const queue_config_1 = require("../../config/queue-config"); const logger_1 = __importDefault(require("../../utils/logger")); /** * Clase principal para manejo de message queues con BullMQ */ class WhatsAppMessageQueue { constructor(redisClient) { this.queues = new Map(); this.workers = new Map(); this.isInitialized = false; this.redisClient = redisClient; } /** * Inicializa todas las colas del sistema */ async initialize() { try { // Asegurar conexión Redis if (!this.redisClient.isConnected()) { await this.redisClient.connect(); } // Crear todas las colas await this.createQueues(); this.isInitialized = true; logger_1.default.info("WhatsApp message queue system initialized successfully"); } catch (error) { logger_1.default.error("Failed to initialize message queue system:", error); throw error; } } /** * Verifica si el sistema está inicializado */ getInitializedStatus() { return this.isInitialized; } /** * Asegura que el sistema esté inicializado, inicializando si es necesario */ async ensureInitialized() { if (!this.isInitialized) { logger_1.default.warn("Message queue not initialized, initializing now..."); await this.initialize(); } } /** * Publica un evento en la cola correspondiente */ async publishEvent(event, queueName) { // Auto-inicialización como fallback de seguridad await this.ensureInitialized(); const targetQueue = queueName || this.getDefaultQueue(event.eventType); const queue = this.queues.get(targetQueue); if (!queue) { throw new Error(`Queue ${targetQueue} not found. Available queues: ${Array.from(this.queues.keys()).join(', ')}`); } try { // Generar ID único si no existe if (!event.id) { event.id = this.generateEventId(event); } // Agregar timestamp si no existe if (!event.timestamp) { event.timestamp = Date.now(); } await queue.add(event.eventType, event, { priority: event.priority || 0, delay: 0, attempts: this.getAttemptsForEvent(event.eventType), jobId: event.id, }); logger_1.default.debug({ eventId: event.id, eventType: event.eventType, tenantId: event.tenantId, agentId: event.agentId, queue: targetQueue, msg: "Event published to queue", }); } catch (error) { logger_1.default.error({ eventId: event.id, eventType: event.eventType, tenantId: event.tenantId, agentId: event.agentId, queue: targetQueue, error: error instanceof Error ? error.message : String(error), msg: "Failed to publish event", }); throw error; } } /** * Publica múltiples eventos en batch */ async publishBatch(events, queueName) { // Auto-inicialización como fallback de seguridad await this.ensureInitialized(); const eventsByQueue = new Map(); // Agrupar eventos por cola for (const event of events) { const targetQueue = queueName || this.getDefaultQueue(event.eventType); if (!eventsByQueue.has(targetQueue)) { eventsByQueue.set(targetQueue, []); } eventsByQueue.get(targetQueue).push(event); } // Publicar cada grupo en su cola correspondiente const publishPromises = []; for (const [queue, queueEvents] of eventsByQueue) { const queueInstance = this.queues.get(queue); if (!queueInstance) { throw new Error(`Queue not found: ${queue}`); } const jobs = queueEvents.map((event) => ({ name: event.eventType, data: event, opts: { priority: event.priority || 0, jobId: event.id || this.generateEventId(event), attempts: this.getAttemptsForEvent(event.eventType), }, })); publishPromises.push(queueInstance.addBulk(jobs).then(() => { })); } try { await Promise.all(publishPromises); logger_1.default.info(`Successfully published ${events.length} events in batch`); } catch (error) { logger_1.default.error("Failed to publish events batch:", error); throw error; } } /** * Crea un worker para procesar eventos de una cola específica */ createWorker(queueName, processor) { const config = (0, queue_config_1.getQueueConfig)(queueName); const connection = this.redisClient.createBullMQConnection(); const workerOptions = { connection, concurrency: config.workerOptions.concurrency, limiter: config.workerOptions.limiter, }; const worker = new bullmq_1.Worker(queueName, async (job) => { const startTime = Date.now(); try { logger_1.default.debug({ jobId: job.id, eventType: job.data.eventType, tenantId: job.data.tenantId, agentId: job.data.agentId, attempt: job.attemptsMade + 1, msg: "Processing event", }); const result = await processor(job); const processingTime = Date.now() - startTime; logger_1.default.info({ jobId: job.id, eventType: job.data.eventType, tenantId: job.data.tenantId, agentId: job.data.agentId, success: result.success, processingTime, msg: "Event processed", }); return result; } catch (error) { const processingTime = Date.now() - startTime; logger_1.default.error({ jobId: job.id, eventType: job.data.eventType, tenantId: job.data.tenantId, agentId: job.data.agentId, attempt: job.attemptsMade + 1, processingTime, error: error instanceof Error ? error.message : String(error), msg: "Event processing failed", }); throw error; } }, workerOptions); // Configurar event listeners del worker this.setupWorkerEventListeners(worker, queueName); this.workers.set(queueName, worker); logger_1.default.info(`Worker created for queue: ${queueName}`); return worker; } /** * Obtiene estadísticas de una cola específica */ async getQueueStats(queueName) { const queue = this.queues.get(queueName); if (!queue) { throw new Error(`Queue not found: ${queueName}`); } const [waiting, active, completed, failed, delayed] = await Promise.all([ queue.getWaiting(), queue.getActive(), queue.getCompleted(), queue.getFailed(), queue.getDelayed(), ]); return { queueName, counts: { waiting: waiting.length, active: active.length, completed: completed.length, failed: failed.length, delayed: delayed.length, }, jobs: { waiting: waiting.map((job) => ({ id: job.id, data: job.data })), active: active.map((job) => ({ id: job.id, data: job.data })), failed: failed.map((job) => ({ id: job.id, data: job.data, error: job.failedReason, })), }, }; } /** * Cierra todas las colas y workers */ async close() { logger_1.default.info("Closing message queue system..."); // Cerrar workers const workerPromises = Array.from(this.workers.values()).map((worker) => worker.close()); await Promise.all(workerPromises); // Cerrar colas const queuePromises = Array.from(this.queues.values()).map((queue) => queue.close()); await Promise.all(queuePromises); this.workers.clear(); this.queues.clear(); this.isInitialized = false; logger_1.default.info("Message queue system closed"); } /** * Crea todas las colas del sistema */ async createQueues() { // Crear una conexión sin prefix para BullMQ const bullmqConnection = this.redisClient.createBullMQConnection(); for (const queueName of Object.values(event_types_1.QueueName)) { const config = (0, queue_config_1.getQueueConfig)(queueName); const queueOptions = { connection: bullmqConnection, defaultJobOptions: config.defaultJobOptions, prefix: queue_config_1.globalQueueConfig.connection.prefix, }; const queue = new bullmq_1.Queue(queueName, queueOptions); // Configurar limpieza automática await queue.clean(0, 0, "completed"); await queue.clean(0, 0, "failed"); this.queues.set(queueName, queue); logger_1.default.debug(`Queue created: ${queueName}`); } } /** * Obtiene la cola por defecto para un tipo de evento */ getDefaultQueue(eventType) { switch (eventType) { case event_types_1.WhatsAppEventType.MESSAGE_NEW: case event_types_1.WhatsAppEventType.MESSAGE_UPDATE: case event_types_1.WhatsAppEventType.MESSAGE_DELETE: case event_types_1.WhatsAppEventType.CONNECTION_STATUS: case event_types_1.WhatsAppEventType.QR_CODE_GENERATED: return event_types_1.QueueName.REALTIME; case event_types_1.WhatsAppEventType.HISTORY_SYNC: case event_types_1.WhatsAppEventType.HISTORY_PARTIAL: return event_types_1.QueueName.BULK; case event_types_1.WhatsAppEventType.WEBHOOK_NOTIFICATION: return event_types_1.QueueName.WEBHOOK; case event_types_1.WhatsAppEventType.CONTACT_UPDATE: case event_types_1.WhatsAppEventType.GROUP_UPSERT: case event_types_1.WhatsAppEventType.PRESENCE_UPDATE: return event_types_1.QueueName.NOTIFICATIONS; default: return event_types_1.QueueName.REALTIME; } } /** * Genera un ID único para el evento */ generateEventId(event) { const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 8); return `${event.tenantId}_${event.agentId}_${event.eventType}_${timestamp}_${random}`; } /** * Obtiene el número de intentos para un tipo de evento */ getAttemptsForEvent(eventType) { switch (eventType) { case event_types_1.WhatsAppEventType.QR_CODE_GENERATED: case event_types_1.WhatsAppEventType.CONNECTION_STATUS: return 1; // Eventos críticos, no reintentar case event_types_1.WhatsAppEventType.MESSAGE_NEW: case event_types_1.WhatsAppEventType.MESSAGE_UPDATE: return 3; // Mensajes importantes case event_types_1.WhatsAppEventType.WEBHOOK_NOTIFICATION: return 5; // Webhooks pueden fallar por red default: return 3; } } /** * Configura event listeners para un worker */ setupWorkerEventListeners(worker, queueName) { worker.on("ready", () => { logger_1.default.info(`Worker ready: ${queueName}`); }); worker.on("error", (error) => { logger_1.default.error({ queueName, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, name: error instanceof Error ? error.name : undefined, code: error?.code, syscall: error?.syscall, address: error?.address, port: error?.port, msg: `Worker error in ${queueName}`, fullError: error instanceof Error ? error : String(error) }); }); worker.on("failed", (job, error) => { logger_1.default.error({ queueName, jobId: job?.id, eventType: job?.data?.eventType, tenantId: job?.data?.tenantId, agentId: job?.data?.agentId, attempt: job?.attemptsMade, maxAttempts: job?.opts?.attempts, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, name: error instanceof Error ? error.name : undefined, code: error?.code, jobData: job?.data ? JSON.stringify(job.data, null, 2) : undefined, msg: "Job failed with detailed error info", }); }); worker.on("stalled", (jobId) => { logger_1.default.warn({ queueName, jobId, msg: "Job stalled - worker may be overwhelmed or stuck", }); }); worker.on("completed", (job) => { logger_1.default.debug({ queueName, jobId: job?.id, eventType: job?.data?.eventType, tenantId: job?.data?.tenantId, agentId: job?.data?.agentId, processingTime: job?.processedOn ? Date.now() - job.processedOn : undefined, msg: "Job completed successfully", }); }); worker.on("active", (job) => { logger_1.default.debug({ queueName, jobId: job?.id, eventType: job?.data?.eventType, tenantId: job?.data?.tenantId, agentId: job?.data?.agentId, attempt: job?.attemptsMade + 1, msg: "Job started processing", }); }); } /** * Check if the message queue system is ready */ async isReady() { try { // Check if Redis is connected if (!this.redisClient.isConnected()) { return false; } // Check if all queues are available const queueNames = [ event_types_1.QueueName.REALTIME, event_types_1.QueueName.BULK, event_types_1.QueueName.WEBHOOK, event_types_1.QueueName.NOTIFICATIONS, ]; const queueChecks = await Promise.all(queueNames.map((name) => { const queue = this.queues.get(name); return queue ? queue.getWaiting() : Promise.resolve([]); })); return queueChecks.every((jobs) => Array.isArray(jobs)); } catch (error) { logger_1.default.error("Queue readiness check failed:", error); return false; } } /** * Get statistics for all queues */ async getAllQueueStats() { const getQueueStat = async (queueName) => { try { const queue = this.queues.get(queueName); if (!queue) { return { waiting: 0, active: 0, completed: 0, failed: 0, delayed: 0, }; } const [waiting, active, completed, failed, delayed] = await Promise.all([ queue.getWaiting(), queue.getActive(), queue.getCompleted(), queue.getFailed(), queue.getDelayed(), ]); return { waiting: waiting.length, active: active.length, completed: completed.length, failed: failed.length, delayed: delayed.length, }; } catch (error) { logger_1.default.error(`Failed to get stats for queue ${queueName}:`, error); return { waiting: 0, active: 0, completed: 0, failed: 0, delayed: 0, }; } }; const [realtime, bulk, webhook, notifications] = await Promise.all([ getQueueStat(event_types_1.QueueName.REALTIME), getQueueStat(event_types_1.QueueName.BULK), getQueueStat(event_types_1.QueueName.WEBHOOK), getQueueStat(event_types_1.QueueName.NOTIFICATIONS), ]); return { realtime, bulk, webhook, notifications }; } /** * Clean up old jobs from all queues */ async cleanupQueues(maxAge = 24 * 60 * 60 * 1000) { const cleanQueue = async (queueName) => { try { const queue = this.queues.get(queueName); if (queue) { await queue.clean(maxAge, 0, "completed"); await queue.clean(maxAge, 0, "failed"); logger_1.default.info(`Cleaned up old jobs from ${queueName} queue`); } } catch (error) { logger_1.default.error(`Failed to cleanup ${queueName} queue:`, error); } }; const queueNames = [ event_types_1.QueueName.REALTIME, event_types_1.QueueName.BULK, event_types_1.QueueName.WEBHOOK, event_types_1.QueueName.NOTIFICATIONS, ]; await Promise.all(queueNames.map(cleanQueue)); } /** * Gracefully shutdown all queues */ async shutdown() { logger_1.default.info("Shutting down message queue system..."); try { const closePromises = Array.from(this.queues.values()).map((queue) => queue.close()); await Promise.all(closePromises); logger_1.default.info("Message queue system shutdown completed"); } catch (error) { logger_1.default.error("Error during message queue shutdown:", error); throw error; } } } exports.WhatsAppMessageQueue = WhatsAppMessageQueue; //# sourceMappingURL=redis-queue.js.map