@qbatch/sqs-consumer
Version:
Build SQS-based Node applications without the boilerplate
226 lines (225 loc) • 7.38 kB
JavaScript
"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;