UNPKG

redis-smq

Version:

A high-performance, reliable, and scalable message queue for Node.js.

359 lines 16.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConsumeMessage = void 0; const fs_1 = require("fs"); const path_1 = __importDefault(require("path")); const redis_smq_common_1 = require("redis-smq-common"); const redis_keys_js_1 = require("../../../common/redis/redis-keys/redis-keys.js"); const index_js_1 = require("./types/index.js"); const index_js_2 = require("../../../errors/index.js"); const message_unacknowledger_js_1 = require("./message-unacknowledger.js"); const event_publisher_js_1 = require("./event-publisher.js"); const connection_pool_js_1 = require("../../../common/redis/redis-connection-pool/types/connection-pool.js"); const redis_connection_pool_js_1 = require("../../../common/redis/redis-connection-pool/redis-connection-pool.js"); const message_acknowledger_js_1 = require("./message-acknowledger.js"); const isCallbackHandler = (handler) => { return handler.length === 2; }; const isPromiseHandler = (handler) => { return handler.length === 1; }; class ConsumeMessage extends redis_smq_common_1.Runnable { constructor(consumerContext, queue, messageHandlerId, messageHandler) { super(); this.redisClient = null; this.consumeMessageWorker = null; this.validateHandler = (cb) => { const messageHandler = this.messageHandler; if (typeof messageHandler === 'function') { if (![1, 2].includes(this.messageHandler.length)) { this.logger.debug('Function-based handler has invalid signature'); return cb(new index_js_2.InvalidMessageHandlerSignatureError({ metadata: { queue: this.queue, }, })); } return cb(); } if (typeof messageHandler === 'string') { const ext = path_1.default.extname(messageHandler); if (!['.js', '.cjs'].includes(ext)) { this.logger.error(`Invalid extension: ${ext}`); return cb(new index_js_2.MessageHandlerFilenameExtensionError()); } return (0, fs_1.stat)(messageHandler, (err) => { if (err) { this.logger.error(`Handler file not found: ${messageHandler}`); return cb(new index_js_2.MessageHandlerFileError()); } this.logger.debug(`Handler validated: ${messageHandler}`); cb(); }); } this.logger.debug('Invalid message handler type'); return cb(new index_js_2.InvalidMessageHandlerTypeError({ metadata: { queue: this.queue, }, })); }; this.consumerId = consumerContext.consumerId; this.logger = consumerContext.logger.createLogger(this.constructor.name); this.config = consumerContext.config; this.consumerOptions = consumerContext.consumerOptions; this.queue = queue; this.messageHandler = messageHandler; this.messageHandlerId = messageHandlerId; this.messageAcknowledger = this.initializeMessageAcknowledger(); this.messageUnacknowledger = this.initializeMessageUnacknowledger(); const { keyQueueProcessing } = redis_keys_js_1.redisKeys.getQueueConsumerKeys(this.queue.queueParams, this.consumerId); const { keyQueueProperties, keyQueueAcknowledged } = redis_keys_js_1.redisKeys.getQueueKeys(this.queue.queueParams.ns, this.queue.queueParams.name, this.queue.groupId); this.keyQueueProperties = keyQueueProperties; this.keyQueueAcknowledged = keyQueueAcknowledged; this.keyQueueProcessing = keyQueueProcessing; (0, event_publisher_js_1.eventPublisher)(this); this.logger.debug(`${this.constructor.name} initialized for queue ${queue.queueParams.name}@${queue.queueParams.ns}`); } initializeMessageAcknowledger() { const messageAcknowledger = new message_acknowledger_js_1.MessageAcknowledger(this.queue, this.consumerId, this.logger, this.consumerOptions); messageAcknowledger.on('messageAcknowledger.error', (err) => { this.handleError(err); }); messageAcknowledger.on('messageAcknowledger.messageAcknowledged', (message) => { this.onMessageAcknowledged(message); }); return messageAcknowledger; } initializeMessageUnacknowledger() { const messageUnacknowledger = new message_unacknowledger_js_1.MessageUnacknowledger(this.consumerId, this.queue, this.logger, this.consumerOptions); messageUnacknowledger.on('messageUnacknowledger.error', (err) => { this.handleError(err); }); messageUnacknowledger.on('messageUnacknowledger.messagesUnacknowledged', (status) => { this.handleUnacknowledgementResult(status); }); return messageUnacknowledger; } handleReceivedMessage(message) { const messageId = message.getId(); this.logger.debug(`Received message ${messageId}`); if (!this.isOperational()) { this.logger.warn(`Ignoring message ${messageId} - consumer not running`); return; } if (message.getSetExpired()) { this.logger.info(`Message ${messageId} expired, unacknowledging`); this.unacknowledgeMessage(message, index_js_1.EMessageUnacknowledgementCause.TTL_EXPIRED); } else { this.logger.debug(`Message ${messageId} valid, proceeding with consumption`); this.consumeMessage(message); } } acknowledgeMessage(message) { if (this.messageAcknowledger) { this.messageAcknowledger.add(message); } else { this.logger.error('MessageAcknowledger not initialized'); } this.emit('consumer.consumeMessage.next'); } unacknowledgeMessage(message, cause) { this.messageUnacknowledger.add(message, cause); this.emit('consumer.consumeMessage.next'); } onMessageAcknowledged(message) { const messageId = message.getId(); this.logger.info(`Message ${messageId} acknowledged successfully`); this.emit('consumer.consumeMessage.messageAcknowledged', messageId, this.queue, this.messageHandlerId, this.consumerId); } handleUnacknowledgementResult(result) { const messageCount = Object.keys(result).length; if (messageCount === 0) return; this.logger.debug(`Handling unacknowledgement result for ${messageCount} messages`); for (const [messageId, details] of Object.entries(result)) { this.logger.info(`Message ${messageId} unacknowledged: ${index_js_1.EMessageUnacknowledgementCause[details.cause]}`); this.emit('consumer.consumeMessage.messageUnacknowledged', messageId, this.queue, this.messageHandlerId, this.consumerId, details.cause); if (details.action === index_js_1.EMessageUnacknowledgementAction.DEAD_LETTER) { this.logger.info(`Message ${messageId} moved to dead letter queue`); this.emit('consumer.consumeMessage.messageDeadLettered', messageId, this.queue, this.messageHandlerId, this.consumerId, details.deadLetterCause); } else if (details.action === index_js_1.EMessageUnacknowledgementAction.DELAY) { this.logger.info(`Message ${messageId} delayed for retry`); this.emit('consumer.consumeMessage.messageDelayed', messageId, this.queue, this.messageHandlerId, this.consumerId); } else { this.logger.info(`Message ${messageId} re-queued for retry`); this.emit('consumer.consumeMessage.messageRequeued', messageId, this.queue, this.messageHandlerId, this.consumerId); } } } consumeMessage(message) { const messageId = message.getId(); this.logger.debug(`Consuming message ${messageId}`); let isCallbackHandled = false; try { let onConsumed = (err) => { if (isCallbackHandled) { this.logger.debug(`Callback for ${messageId} already handled`); return; } isCallbackHandled = true; if (!this.isOperational()) { this.logger.debug(`Consumer not running, ignoring callback for ${messageId}`); return; } if (err) { this.logger.error(`Error consuming ${messageId}: ${err.message}`); let reason = index_js_1.EMessageUnacknowledgementCause.UNACKNOWLEDGED; if (err instanceof redis_smq_common_1.AsyncCallbackTimeoutError) { reason = index_js_1.EMessageUnacknowledgementCause.TIMEOUT; } if (err instanceof index_js_2.InvalidMessageHandlerSignatureError) { reason = index_js_1.EMessageUnacknowledgementCause.INVALID_HANDLER_SIGNATURE; } this.unacknowledgeMessage(message, reason); return; } this.logger.debug(`Message ${messageId} consumed successfully`); this.acknowledgeMessage(message); }; const consumeTimeout = message.producibleMessage.getConsumeTimeout(); if (consumeTimeout) { this.logger.debug(`Setting timeout ${consumeTimeout}ms for ${messageId}`); onConsumed = redis_smq_common_1.async.withTimeout(onConsumed, consumeTimeout); } this.invokeMessageHandler(this.messageHandler, message.transfer(), onConsumed); } catch (error) { this.logger.error(`Exception consuming ${messageId}: ${error}`); isCallbackHandled = true; this.unacknowledgeMessage(message, index_js_1.EMessageUnacknowledgementCause.CONSUME_ERROR); this.emit('consumer.consumeMessage.next'); } } invokeMessageHandler(handler, msg, cb) { const messageId = msg.id; if (typeof handler === 'string') { this.getWorker(handler).call(msg, (err) => { if (err) { this.logger.error(`Worker handler failed for ${messageId}: ${err.message}`); } cb(err); }); return; } let isCompleted = false; const complete = (err) => { if (isCompleted) return; isCompleted = true; if (err) { this.logger.error(`Handler failed for ${messageId}: ${err.message}`); cb(err); } else { this.logger.debug(`Handler succeeded for ${messageId}`); cb(null); } }; try { if (isPromiseHandler(handler)) { const result = handler(msg); if (result && typeof result.then === 'function') { result .then(() => complete()) .catch((err) => { const error = err instanceof Error ? err : new Error(String(err !== null && err !== void 0 ? err : 'Promise rejected')); complete(error); }); } else { this.logger.warn(`Handler for ${messageId} didn't return a Promise`); complete(); } } else if (isCallbackHandler(handler)) { handler(msg, (err) => { complete(err !== null && err !== void 0 ? err : undefined); }); } else { this.logger.error(`Handler for ${messageId} has invalid signature`); complete(new index_js_2.InvalidMessageHandlerSignatureError({ metadata: { queue: this.queue, }, })); } } catch (err) { const error = err instanceof Error ? err : new Error(String(err || 'Handler threw error')); complete(error); } } getWorker(filename) { if (!this.consumeMessageWorker) { this.logger.debug(`Creating worker for ${filename}`); this.consumeMessageWorker = new redis_smq_common_1.CallableWorker(filename, this.logger); } return this.consumeMessageWorker; } goingUp() { return super.goingUp().concat([ (cb) => { this.logger.debug('Initializing Redis client'); redis_connection_pool_js_1.RedisConnectionPool.getInstance().acquire(connection_pool_js_1.ERedisConnectionAcquisitionMode.SHARED, (err, client) => { if (err) { this.logger.error(`Redis client failed: ${err.message}`); return cb(err); } if (!client) return cb(new redis_smq_common_1.CallbackEmptyReplyError()); this.redisClient = client; cb(); }); }, (cb) => { this.logger.debug('Starting MessageUnacknowledger'); this.messageUnacknowledger.run((err) => { if (err) { this.logger.error(`Failed to start MessageUnacknowledger: ${err.message}`); return cb(err); } this.logger.debug('MessageUnacknowledger started'); cb(); }); }, (cb) => { this.messageAcknowledger.run((err) => { if (err) { this.logger.error(`Failed to start MessageAcknowledger: ${err.message}`); return cb(err); } this.logger.debug('MessageAcknowledger started'); cb(); }); }, (cb) => { this.logger.debug('Validating message handler'); this.validateHandler(cb); }, ]); } goingDown() { return [ (cb) => { if (this.messageAcknowledger) { this.logger.info('Shutting down MessageAcknowledger'); this.messageAcknowledger.shutdown(cb); } else { cb(); } }, (cb) => { this.logger.debug('Shutting down MessageUnacknowledger'); this.messageUnacknowledger.shutdown(cb); }, (cb) => { if (this.consumeMessageWorker) { this.consumeMessageWorker.shutdown((err) => { if (err) this.logger.warn(`Worker shutdown error: ${err.message}`); cb(); }); } else { cb(); } }, (cb) => { if (this.redisClient) { redis_connection_pool_js_1.RedisConnectionPool.getInstance().release(this.redisClient); this.redisClient = null; } cb(); }, ].concat(super.goingDown()); } handleError(err) { if (!this.isOperational()) return; const error = err instanceof Error ? err : new Error(String(err)); this.emit('consumer.consumeMessage.error', error, this.consumerId, this.queue); super.handleError(err); } } exports.ConsumeMessage = ConsumeMessage; //# sourceMappingURL=consume-message.js.map