service-bus
Version:
Just another service bus based on Amazon SQS - UNDER HEAVY DEVELOPMENT. Expected to be stable and documented with 0.11 release.
292 lines (218 loc) • 7.79 kB
JavaScript
;
var crypto = require("crypto");
var zlib = require("zlib");
var Promise = require("bluebird");
var AWS = require("aws-sdk");
var createError = require("custom-error-generator");
var _ = require("lodash");
var MessageError = createError('MessageError');
var InvalidArgumentError = createError('InvalidArgumentError');
function serviceBus(queueUrl, options) {
validate(queueUrl, options);
var _delegate;
var _isPolling = false;
var _queueUrl = queueUrl;
var _sqs = new AWS.SQS({
accessKeyId: options.accessKeyId,
secretAccessKey: options.secretAccessKey,
region: options.region
});
var receiveMessagesAsync = Promise.promisify(_sqs.receiveMessage, _sqs);
var deleteMessageAsync = Promise.promisify(_sqs.deleteMessage, _sqs);
var sendMessageAsync = Promise.promisify(_sqs.sendMessage, _sqs);
function acknowledge(message, callback) {
return deleteMessageAsync({
QueueUrl: _queueUrl,
ReceiptHandle: message.receiptHandle
}).nodeify(callback);
}
function publish(data, callback) {
var bufferHashHex;
return toBuffer(data)
.then(function(buffer) {
bufferHashHex = md5(buffer);
return buffer;
})
.then(zipBuffer)
.then(function (zippedBuffer) {
return sendMessageAsync({
QueueUrl: _queueUrl,
MessageBody: bufferHashHex,
DelaySeconds: 0,
MessageAttributes: {
data: {
DataType: "Binary",
BinaryValue: zippedBuffer
}
}
});
})
.then(function (message) {
if (md5(bufferHashHex) !== message.MD5OfMessageBody) {
throw MessageError("Message body MD5 mismatch")
}
return bufferHashHex;
})
.nodeify(callback);
}
function subscribe(delegate, callback) {
return new Promise(function (resolve, reject) {
if (!delegate || typeof delegate !== "function") {
return reject(new Error("No delegate function provided"));
}
_delegate = delegate;
next();
return resolve();
}).nodeify(callback);
}
function poll() {
_isPolling = true;
var params = {
QueueUrl: _queueUrl,
MaxNumberOfMessages: 10,
VisibilityTimeout: 60,
WaitTimeSeconds: 10,
MessageAttributeNames: ["data"]
};
receiveMessagesAsync(params)
.then(function (data) {
_isPolling = false;
if (data && data.Messages) {
return parseMessages(data.Messages)
.then(function(messages) {
_delegate(messages, next);
});
}
else {
next();
}
})
.catch(function (err) {
_isPolling = false;
throw err;
});
}
function next() {
if (_isPolling) return;
poll();
}
function parseMessages(messages, callback) {
return Promise.resolve(messages)
.map(function(message) {
return Promise.resolve(message)
.then(function(message) {
if (!message.MessageAttributes ||
!message.MessageAttributes.data ||
!message.MessageAttributes.data.BinaryValue) {
throw MessageError("Message has invalid payload");
}
if (message.MD5OfBody !== md5(message.Body)) {
throw MessageError("Message body MD5 mismatch")
}
return unzipBuffer(message.MessageAttributes.data.BinaryValue)
.then(function(buffer) {
var bufferHashHex = md5(buffer);
if (bufferHashHex !== message.Body) {
throw MessageError("Message body MD5 mismatch")
}
return buffer;
})
.then(toObject);
})
.then(function(body) {
return {
messageId: message.MessageId,
receiptHandle: message.ReceiptHandle,
body: body
}
});
}).nodeify(callback);
}
function toBuffer(data, callback) {
return new Promise(function (resolve, reject) {
if (!_.isPlainObject(data)) {
return reject(new InvalidArgumentError("Input data has to be plain object"));
}
try {
var dataString = JSON.stringify(data);
var dataBuffer = new Buffer(dataString);
return resolve(dataBuffer);
}
catch (err) {
return reject(err);
}
}).nodeify(callback);
}
function toObject(buffer, callback) {
return new Promise(function (resolve, reject) {
try {
var data = JSON.parse(buffer);
return resolve(data);
}
catch (err) {
return reject(err);
}
}).nodeify(callback);
}
function zipBuffer(buffer, callback) {
return new Promise(function (resolve, reject) {
try {
zlib.gzip(buffer, function (err, res) {
if (err) {
return reject(err);
}
return resolve(res);
});
}
catch (err) {
return reject(err);
}
}).nodeify(callback);
}
function unzipBuffer(zippedBuffer, callback) {
return new Promise(function (resolve, reject) {
zlib.gunzip(zippedBuffer, function (err, res) {
if (err) {
return reject(err);
}
return resolve(res);
});
}).nodeify(callback);
}
function md5(data) {
var hash = crypto.createHash('md5');
hash.update(data);
return hash.digest('hex');
}
function hash(data, callback) {
return toBuffer(data)
.then(md5)
.nodeify(callback);
}
function validate(queueUrl, options) {
if (!_.isString(queueUrl)) {
throw InvalidArgumentError("No AWS SQS queue URL provided");
}
if (!options) {
throw InvalidArgumentError("No options provided");
}
if (!options.accessKeyId) {
throw InvalidArgumentError("No AWS 'accessKeyId' provided");
}
if (!options.secretAccessKey) {
throw InvalidArgumentError("No AWS 'secretAccessKey' provided");
}
if (!options.region) {
throw InvalidArgumentError("No AWS 'region' provided");
}
}
return {
hash: hash,
publish: publish,
subscribe: subscribe,
acknowledge: acknowledge,
InvalidArgumentError: InvalidArgumentError,
MessageError: MessageError
};
}
module.exports = serviceBus;