UNPKG

@qbatch/sqs-consumer

Version:

Build SQS-based Node applications without the boilerplate

226 lines (225 loc) 7.38 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const debug = require('debug')('sqs-consumer'); const SQS = require("aws-sdk/clients/sqs"); const events_1 = require("events"); const bind_1 = require("./bind"); const errors_1 = require("./errors"); const requiredOptions = [ 'queueUrl', 'handleMessage' ]; function createTimeout(duration) { let timeout; const pending = new Promise((_, reject) => { timeout = setTimeout(() => { reject(new errors_1.TimeoutError()); }, duration); }); return [timeout, pending]; } function assertOptions(options) { requiredOptions.forEach((option) => { if (!options[option]) { throw new Error(`Missing SQS consumer option ['${option}'].`); } }); if (options.batchSize > 10 || options.batchSize < 1) { throw new Error('SQS batchSize option must be between 1 and 10.'); } } function isConnectionError(err) { if (err instanceof errors_1.SQSError) { return (err.statusCode === 403 || err.code === 'CredentialsError' || err.code === 'UnknownEndpoint'); } return false; } function toSQSError(err, message) { const sqsError = new errors_1.SQSError(message); sqsError.code = err.code; sqsError.statusCode = err.statusCode; sqsError.region = err.region; sqsError.retryable = err.retryable; sqsError.hostname = err.hostname; sqsError.time = err.time; return sqsError; } function hasMessages(response) { return response.Messages && response.Messages.length > 0; } class Consumer extends events_1.EventEmitter { constructor(options) { super(); assertOptions(options); this.queueUrl = options.queueUrl; this.handleMessage = options.handleMessage; this.handleMessageTimeout = options.handleMessageTimeout; this.attributeNames = options.attributeNames || []; this.messageAttributeNames = options.messageAttributeNames || []; this.stopped = true; this.batchSize = options.batchSize || 1; this.visibilityTimeout = options.visibilityTimeout; this.terminateVisibilityTimeout = options.terminateVisibilityTimeout || false; this.waitTimeSeconds = options.waitTimeSeconds || 20; this.authenticationErrorTimeout = options.authenticationErrorTimeout || 10000; this.sqs = options.sqs || new SQS({ region: options.region || process.env.AWS_REGION || 'eu-west-1' }); this.preProcessMessages = options.preProcessMessages; bind_1.autoBind(this); } get isRunning() { return !this.stopped; } static create(options) { return new Consumer(options); } start() { if (this.stopped) { debug('Starting consumer'); this.stopped = false; this.poll(); } } stop() { debug('Stopping consumer'); this.stopped = true; } async handleSqsResponse(response) { debug('Received SQS response'); debug(response); if (response) { if (hasMessages(response)) { const messages = this.preProcessMessages(response.Messages); await Promise.all(messages.map(this.processMessage)); this.emit('response_processed'); } else { this.emit('empty'); } } } async processMessage(message) { this.emit('message_received', message); try { await this.executeHandler(message); await this.deleteMessage(message); this.emit('message_processed', message); } catch (err) { this.emitError(err, message); if (this.terminateVisibilityTimeout) { try { await this.terminateVisabilityTimeout(message); } catch (err) { this.emit('error', err, message); } } } } async receiveMessage(params) { try { return await this.sqs .receiveMessage(params) .promise(); } catch (err) { throw toSQSError(err, `SQS receive message failed: ${err.message}`); } } async deleteMessage(message) { debug('Deleting message %s', message.MessageId); const deleteParams = { QueueUrl: this.queueUrl, ReceiptHandle: message.ReceiptHandle }; try { await this.sqs .deleteMessage(deleteParams) .promise(); } catch (err) { throw toSQSError(err, `SQS delete message failed: ${err.message}`); } } async executeHandler(message) { let timeout; let pending; try { if (this.handleMessageTimeout) { [timeout, pending] = createTimeout(this.handleMessageTimeout); await Promise.race([ this.handleMessage(message), pending ]); } else { await this.handleMessage(message); } } catch (err) { if (err instanceof errors_1.TimeoutError) { err.message = `Message handler timed out after ${this.handleMessageTimeout}ms: Operation timed out.`; } else { err.message = `Unexpected message handler failure: ${err.message}`; } throw err; } finally { clearTimeout(timeout); } } async terminateVisabilityTimeout(message) { return this.sqs .changeMessageVisibility({ QueueUrl: this.queueUrl, ReceiptHandle: message.ReceiptHandle, VisibilityTimeout: 0 }) .promise(); } emitError(err, message) { if (err.name === errors_1.SQSError.name) { this.emit('error', err, message); } else if (err instanceof errors_1.TimeoutError) { this.emit('timeout_error', err, message); } else { this.emit('processing_error', err, message); } } poll() { if (this.stopped) { this.emit('stopped'); return; } debug('Polling for messages'); const receiveParams = { QueueUrl: this.queueUrl, AttributeNames: this.attributeNames, MessageAttributeNames: this.messageAttributeNames, MaxNumberOfMessages: this.batchSize, WaitTimeSeconds: this.waitTimeSeconds, VisibilityTimeout: this.visibilityTimeout }; let pollingTimeout = 0; this.receiveMessage(receiveParams) .then(this.handleSqsResponse) .catch((err) => { this.emit('error', err); if (isConnectionError(err)) { debug('There was an authentication error. Pausing before retrying.'); pollingTimeout = this.authenticationErrorTimeout; } return; }).then(() => { setTimeout(this.poll, pollingTimeout); }).catch((err) => { this.emit('error', err); }); } } exports.Consumer = Consumer;