whatsapp-crm-common
Version:
Componentes compartidos para servicios de WhatsApp CRM - Common utilities and types for WhatsApp CRM system
507 lines • 19.7 kB
JavaScript
;
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