iamqp
Version:
RabbitMQ wrapper for NodeJS with several produce(reply)/consume scenarios
570 lines (492 loc) • 20.9 kB
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>common.js - Documentation</title>
<script src="scripts/prettify/prettify.js"></script>
<script src="scripts/prettify/lang-css.js"></script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css">
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<input type="checkbox" id="nav-trigger" class="nav-trigger" />
<label for="nav-trigger" class="navicon-button x">
<div class="navicon"></div>
</label>
<label for="nav-trigger" class="overlay"></label>
<nav>
<li class="nav-link nav-home-link"><a href="index.html">Home</a></li><li class="nav-heading">Classes</li><li class="nav-heading"><span class="nav-item-type type-class">C</span><span class="nav-item-name"><a href="Common.html">Common</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Common.html#_commonPublishPreparations">_commonPublishPreparations</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Common.html#_emitAndDispatchError">_emitAndDispatchError</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Common.html#_logData">_logData</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Common.html#_messageDispatcherWrapper">_messageDispatcherWrapper</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Common.html#_parseConsumedMessage">_parseConsumedMessage</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Common.html#_preparePublishingMessage">_preparePublishingMessage</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Common.html#closeConnection">closeConnection</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Common.html#getEventer">getEventer</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Common.html#isReady">isReady</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Common.html#openConnection">openConnection</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Common.html#toString">toString</a></span></li><li class="nav-heading"><span class="nav-item-type type-class">C</span><span class="nav-item-name"><a href="ConsoleLogger.html">ConsoleLogger</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="ConsoleLogger.html#error">error</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="ConsoleLogger.html#info">info</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="ConsoleLogger.html#log">log</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="ConsoleLogger.html#warn">warn</a></span></li><li class="nav-heading"><span class="nav-item-type type-class">C</span><span class="nav-item-name"><a href="FanoutConsumer.html">FanoutConsumer</a></span></li><li class="nav-heading"><span class="nav-item-type type-class">C</span><span class="nav-item-name"><a href="FanoutProducer.html">FanoutProducer</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="FanoutProducer.html#fanout">fanout</a></span></li><li class="nav-heading"><span class="nav-item-type type-class">C</span><span class="nav-item-name"><a href="PlainConsumer.html">PlainConsumer</a></span></li><li class="nav-heading"><span class="nav-item-type type-class">C</span><span class="nav-item-name"><a href="PlainProducer.html">PlainProducer</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="PlainProducer.html#publish">publish</a></span></li><li class="nav-heading"><span class="nav-item-type type-class">C</span><span class="nav-item-name"><a href="RpcClient.html">RpcClient</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="RpcClient.html#callRemote">callRemote</a></span></li><li class="nav-heading"><span class="nav-item-type type-class">C</span><span class="nav-item-name"><a href="RpcServer.html">RpcServer</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="RpcServer.html#respond">respond</a></span></li><li class="nav-heading"><span class="nav-item-type type-class">C</span><span class="nav-item-name"><a href="UUIDv4.html">UUIDv4</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="UUIDv4.html#generate">generate</a></span></li><li class="nav-heading"><a href="global.html">Globals</a></li>
</nav>
<div id="main">
<h1 class="page-title">common.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* @typedef {string} AmqpURI
*/
/**
* @typedef {string} ChannelName
*/
/**
* Options applied when asserting Queues.
* @typedef {Object} QueueArguments
* @property {number} messageTtl `0 <= n < 2^32` expires messages arriving in the queue after n milliseconds
* @property {number} expires `0 < n < 2^32` the queue will be destroyed after n milliseconds of disuse,
* where use means having consumers, being declared
* @property {number} maxLength sets a maximum number of messages the queue will hold
* @property {number} maxPriority makes the queue a priority queue
*/
/**
* @typedef {Object} ConnectionOptions
* @property {number} heartbeat the period of the connection heartbeat, in seconds. Defaults to `60`
*/
/**
* Common configuration for instances.
* @typedef Configuration
* @property {boolean} printMessages if true, the messages will be printed if logging is enabled. Defaults to `true`.
* @property {number} maxMessageLengthToPrint if the messages are to printed, this is the max
* number of bytes that a message can be to be printed. Defaults to `257`.
* @property {QueueArguments} queueArguments
* @property {ConnectionOptions} connectionOptions
*/
const amqp = require('amqplib/callback_api')
const events = require('events')
const _get = require('lodash/get')
const _set = require('lodash/set')
const _isSafeInteger = require('lodash/isSafeInteger')
const _isString = require('lodash/isString')
const _isEmpty = require('lodash/isEmpty')
const _isPlainObject = require('lodash/isPlainObject')
const _isBoolean = require('lodash/isBoolean')
const _isBuffer = require('lodash/isBuffer')
const _isNull = require('lodash/isNull')
const ConsoleLogger = require('./console_logger')
const MESSAGES = require('./internal').MESSAGES
const QUEUE_ARGUMENTS_MAP = {
messageTtl: 'x-message-ttl',
expires: 'x-expires',
maxLength: 'x-max-length',
maxPriority: 'x-max-priority'
}
class Common {
/**
* @param {AmqpURI} amqpUri
* @param {ChannelName} channelName
* @param {Configuration} [configuration]
*/
constructor (amqpUri, channelName, configuration) {
/**
* Name associated with this instance - displayed in the logs.
* Each instance should override this with some relevant name.
* @type {string}
* @protected
*/
this._name = 'COMMON'
/**
* Internal instance to log messages.
* @type {ConsoleLogger}
* @protected
*/
this._consoleLogger = new ConsoleLogger('IAMQP')
/**
* Used to emit messages and statues.
* @type {*|EventEmitter}
* @protected
*/
this._E = new events.EventEmitter()
/**
* In case the user initiates the closing of the connection
* the reconnect is not preformed.
* @type {boolean}
* @private
*/
this._manualConnectionClose = false
/**
* Internal flag for signaling of the connection if ready.
* @type {boolean}
* @protected
*/
this._isReady = false
/**
* Is the connection process going on.
* This is a flag to use as not to perform multiple connection attempts.
* @type {boolean}
* @private
*/
this._isConnecting = false
this._amqp = {
reconnectMultiplier: 1,
connection: null,
channel: null,
queue: null
}
// Lose the reference
configuration = JSON.parse(JSON.stringify(configuration || {}))
/**
* @type {{configuration: Configuration, amqpUri: AmqpURI, channelName: ChannelName}}
* @protected
*/
this._configuration = {
amqpUri,
channelName,
configuration
}
this._processConfiguration()
}
/**
* Check the configuration of this instance.
* @private
*/
_processConfiguration () {
const ERR = (message) => {
this._messageDispatcherWrapper('error', message)
throw new Error(message)
}
const CHECK = {
amqpUri: {
default: undefined,
required: true,
check: (e) => _isString(e) && e.startsWith('amqp://')
},
channelName: {
default: undefined,
required: true,
check: (e) => _isString(e) && !_isEmpty(e.trim())
},
configuration: {
default: {},
required: false,
check: (e) => _isPlainObject(e)
},
'configuration.printMessages': {
default: true,
required: false,
check: (e) => _isBoolean(e)
},
'configuration.maxMessageLengthToPrint': {
default: 257,
required: false,
check: (e) => _isSafeInteger(e) && e > 0
},
'configuration.queueArguments': {
default: {},
required: false,
check: (e) => _isPlainObject(e)
},
'configuration.connectionOptions': {
default: {},
required: false,
check: (e) => _isPlainObject(e)
},
'configuration.queueArguments.messageTtl': {
default: undefined,
required: false,
check: (e) => _isSafeInteger(e) && e >= 0 && e < 4294967296
},
'configuration.queueArguments.expires': {
default: undefined,
required: false,
check: (e) => _isSafeInteger(e) && e > 0 && e < 4294967296
},
'configuration.queueArguments.maxLength': {
default: undefined,
required: false,
check: (e) => _isSafeInteger(e)
},
'configuration.queueArguments.maxPriority': {
default: undefined,
required: false,
check: (e) => _isSafeInteger(e)
},
'configuration.connectionOptions.heartbeat': {
default: 60,
required: false,
check: (e) => _isSafeInteger(e) && e >= 0
}
}
for (const conf in CHECK) {
const ref = CHECK[conf]
let val = _get(this._configuration, conf, undefined)
if (ref.required && val === undefined) {
ERR(`${MESSAGES.MISSING_CONFIGURATION} "${conf}" `)
}
if (ref.default !== undefined && val === undefined) {
_set(this._configuration, conf, ref.default)
val = _get(this._configuration, conf, undefined)
if (!ref.check(val)) {
ERR(`${MESSAGES.INVALID_CONFIGURATION} "${conf}" `)
}
}
if (val !== undefined && !ref.check(val)) {
ERR(`${MESSAGES.INVALID_CONFIGURATION} "${conf}" `)
}
}
// Replace the queue arguments names with the valid names that go into amqplib
const queueArguments = _get(this._configuration, 'configuration.queueArguments', {})
for (const argRef in queueArguments) {
if (!Object.prototype.hasOwnProperty.call(QUEUE_ARGUMENTS_MAP, argRef)) {
continue
}
const val = queueArguments[argRef]
delete queueArguments[argRef]
queueArguments[QUEUE_ARGUMENTS_MAP[argRef]] = val
}
}
/**
* Wrapper for common message handling.
* @param {'log'|'info'|'warn'|'error'} logType one of `log, info, warn, error`
* @param {string} message message to print
* @protected
*/
_messageDispatcherWrapper (logType, message) {
this._consoleLogger[logType] && this._consoleLogger[logType](`[${this._name}] ${message.toString()}`)
}
/**
* @param {string} errorMessage
* @protected
*/
_emitAndDispatchError (errorMessage) {
this._messageDispatcherWrapper('error', errorMessage)
this._E.emit('error', new Error(errorMessage))
}
/**
* Timeout the connection attempt.
* @private
*/
_reconnectHandler () {
this._messageDispatcherWrapper('info', MESSAGES.RECONNECTING)
if (this._amqp.reconnectMultiplier < 6) {
this._amqp.reconnectMultiplier += 0.125
}
// THAT IS RIGHT YOU SONS-OF-BITCHES; THAT IS THIS; BACK TO THE FUTURE MOTHERFUCKER
const that = this
setTimeout(() => {
that._isConnecting = false
that._connect()
}, 10000 * this._amqp.reconnectMultiplier)
}
/**
* Connect the AMQP and setup event listeners.
* @private
*/
_connect () {
if (this._isConnecting) {
return
}
this._isConnecting = true
this._manualConnectionClose = false
amqp.connect(
`${this._configuration.amqpUri}?heartbeat=${this._configuration.configuration.connectionOptions.heartbeat}`,
(amqpConnErr, amqpConn) => {
if (amqpConnErr) {
this._isReady = false
this._messageDispatcherWrapper('error', `${MESSAGES.CONNECTION_ERROR} -> ${amqpConnErr.message}`)
this._E.emit('error', amqpConnErr)
this._reconnectHandler()
} else {
this._amqp.connection = amqpConn
// event ERROR
this._amqp.connection.on('error', (amqpErr) => {
this._isReady = false
this._messageDispatcherWrapper('error', `${MESSAGES.CONNECTION_ERROR} -> ${amqpErr.message}`)
// Closing connection can be a valid action
if (amqpErr.message !== 'Connection closing') {
this._E.emit('error', amqpErr)
}
})
// event CLOSE
this._amqp.connection.on('close', () => {
this._isReady = false
this._E.emit('closed')
if (this._manualConnectionClose === false) {
this._messageDispatcherWrapper('error', MESSAGES.CONNECTION_ERROR)
this._reconnectHandler()
} else {
this._messageDispatcherWrapper('info', MESSAGES.CONNECTION_CLOSE)
}
})
this._amqp.reconnectMultiplier = 1
this._messageDispatcherWrapper('info', MESSAGES.CONNECTION_OPEN)
this._isReady = true
this._isConnecting = false
this._E.emit('connected')
}
})
}
/**
* Parse the message that was consumed.
* The parsing tries parsing via the <code>JSON.parse</code>.
* If this fails, it just tries to return a string representation.
* It can parse:
* <ul>
* <li>number</li>
* <li>string</li>
* <li>plain Object</li>
* <li>Array</li>
* <li>boolean</li>
* </ul>
* @param {Object} message as received by the consumer; it expects a property <code>content</code> of type Buffer
* @returns {number | string | Object | boolean | Array | null} null is returned if the message was not parsed
* @protected
*/
_parseConsumedMessage (message) {
if (
!message ||
!Object.prototype.hasOwnProperty.call(message, 'content') ||
!_isBuffer(message.content)
) {
return null
}
try {
return JSON.parse(message.content.toString())
} catch (parseError) {
if (message.content.toString) {
return message.content.toString()
}
return null
}
}
/**
* Prepare the data to be published.
* @param {string | Object | number | boolean | Array} message
* @returns {Buffer | null}
* @protected
*/
_preparePublishingMessage (message) {
let dataToPublish = null
if (_isString(message)) {
dataToPublish = Buffer.from(message)
} else {
try {
dataToPublish = Buffer.from(JSON.stringify(message))
// If reverse fails, the message could not be send either way, so we stop it here
JSON.parse(dataToPublish.toString())
} catch (parseErr) {
this._messageDispatcherWrapper('error', `${MESSAGES.DATA_PARSE_FAIL} -> ${parseErr.message}`)
dataToPublish = null
}
}
return dataToPublish
}
/**
* @param {number | string | boolean | Object | Array} contentToPublish
* @returns {* | null}
*/
_commonPublishPreparations (contentToPublish) {
if (this._isReady !== true) {
this._messageDispatcherWrapper('error', `${MESSAGES.PUBLISHING} -> ${MESSAGES.CONNECTION_ERROR}`)
return null
}
if (!this._amqp.channel) {
this._messageDispatcherWrapper('error', `${MESSAGES.PUBLISHING} -> ${MESSAGES.CHANNEL_NOT_READY}`)
return null
}
const data = this._preparePublishingMessage(contentToPublish)
if (_isNull(data)) {
this._messageDispatcherWrapper('error', `${MESSAGES.PUBLISHING} -> ${MESSAGES.INVALID_DATA}`)
return null
}
this._logData(data, MESSAGES.PUBLISHING)
return data
}
/**
* Log the data that was received or send in the channels.
* @param {Object | Buffer} data
* @param {string} description some additional information to be logged with the data
* @protected
*/
_logData (data, description) {
if (!this._configuration.configuration.printMessages) {
return
}
let actualDataRef = null
if (_isBuffer(data)) {
actualDataRef = data
} else if (Object.prototype.hasOwnProperty.call(data, 'content') && _isBuffer(data.content)
) {
actualDataRef = data.content
}
if (actualDataRef === null) {
return
}
let thatLength = null
try {
thatLength = Buffer.byteLength(actualDataRef)
} catch (someError) {
thatLength = this._configuration.configuration.maxMessageLengthToPrint * 2
}
if (thatLength < this._configuration.configuration.maxMessageLengthToPrint) {
this._messageDispatcherWrapper('log', `${description} message: ${actualDataRef.toString()}`)
} else {
this._messageDispatcherWrapper('log', `${description} message`)
}
}
/**
* Close the connection.
* The client will not preform an automatic reconnect when this is called.
* @returns {boolean} false if there was an error
*/
closeConnection () {
try {
this._amqp.connection && this._amqp.connection.close()
this._manualConnectionClose = true
return true
} catch (cer) {
this._messageDispatcherWrapper('error', `${MESSAGES.CONNECTION_CLOSE} -> ${cer.message}`)
}
return false
}
/**
* Open the connection.
*/
openConnection () {
if (this._isReady !== true) {
this._connect()
}
}
/**
* Check if the connection is operational.
* @returns {boolean}
*/
isReady () {
return this._isReady
}
/**
* Get the EventEmitter that emits messages and errors.
* @returns {EventEmitter}
*/
getEventer () {
return this._E
}
/**
* Get a string representation of this instance.
* If prints the keys and values of all non-private members that are primitives.
* @returns {string}
* @public
*/
toString () {
return `{${[
this._name,
this._configuration.amqpUri,
this._configuration.channelName
].join(', ')}}`
}
}
module.exports = Common
</code></pre>
</article>
</section>
</div>
<br class="clear">
<footer>
Generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.6.2</a> on Sun Aug 04 2019 22:10:47 GMT+0200 (Central European Summer Time) using the Minami theme.
</footer>
<script>prettyPrint();</script>
<script src="scripts/linenumber.js"></script>
</body>
</html>