rpc/rpc_client.js

const UUIDv4 = require('../uuid_v4')
const MESSAGES = require('../internal').MESSAGES
const _isNull = require('lodash/isNull')
const _get = require('lodash/get')

/**
 * RPC client. Init example:
 * <pre>
 *     const iamqp              = require('iamqp');
 *     const amqpUri            = 'amqp://localhost';
 *     const rpcChannel         = 'rpc-channel-a';
 *     let rpcClient            = new iamqp.RPCClient(amqpUri, rpcChannel);
 *
 *     // This needs to be done or the errors will bubble up.
 *     rpcClient.getEventer().on('error', (err) => {
 *       console.log('RPC client error:' + err.message);
 *     });
 *
 *     // For when the connection is established.
 *     rpcClient.getEventer().on('connected', () => {
 *       console.log('RPC client connected');
 *     });
 *
 *     rpcClient.getEventer().on('reply', (reply, correlationId) => {
 *       console.log('RPC client got a reply (' + correlationId + '): ' + reply);
 *     });
 *
 *     // Open the connection - this takes some time and is not sync
 *     rpcClient.openConnection();
 *
 *     // ...
 *
 *     // Call the RPC server with some data.
 *     // The correlation ID strings are to be used to distinguish replies to answers.
 *     let correlationIdA = rpcClient.callRemote({'a': 5});
 *     let correlationIdB = rpcClient.callRemote({'b': 5});
 *     if (correlationIdA === false) {
 *       console.log('call not performed on A');
 *     } else {
 *       console.log('correlationID on A: ' + correlationIdA);
 *     }
 *
 *     if (correlationIdB === false) {
 *       console.log('call not performed on B');
 *     } else {
 *       console.log('correlationID on B: ' + correlationIdB);
 *     }
 *
 *     // ...
 *
 *     // Close the connection
 *     if (rpcClient.closeConnection()) {
 *       console.log('RPC client connection closing ...');
 *     }
 * </pre>
 * ...
 */
class RpcClient extends require('../common') {
  /**
   * @param {AmqpURI} amqpUri
   * @param {ChannelName} channelName
   * @param {Configuration} [configuration]
   */
  constructor (amqpUri, channelName, configuration) {
    super(amqpUri, channelName, configuration)
    this._uidGenerator = new UUIDv4()
    this._correlationIDStorage = {}
    this._name = `RPC-CLIENT: ${this._configuration.channelName}`
    this._messageDispatcherWrapper('info', MESSAGES.INIT)

    this._E.on('connected', () => {
      this._createChannel()
    })
  }

  /**
   * Perform the "server" call with some data.
   * @param {number | string | boolean | Object | Array} contentToPublish
   * @returns {boolean | string} the correlation ID of the call.
   * The user can use this as to distinguish between multiple answers.
   * If something went wrong, false will be returned.
   */
  callRemote (contentToPublish) {
    const data = this._commonPublishPreparations(contentToPublish)
    if (!_isNull(data)) {
      const thisCorrelationId = this._uidGenerator.generate()
      this._correlationIDStorage[thisCorrelationId] = true
      this._amqp.channel.sendToQueue(this._configuration.channelName, data, {
        replyTo: this._amqp.queue,
        correlationId: thisCorrelationId
      })

      return thisCorrelationId
    }

    return false
  }

  /**
   * Create the channel.
   * @private
   */
  _createChannel () {
    this._amqp.connection.createChannel((channelCreatingErr, createdChannel) => {
      if (channelCreatingErr) {
        this._emitAndDispatchError(`${MESSAGES.CHANNEL_ERROR} -> ${channelCreatingErr.message}`)
        return
      }

      this._messageDispatcherWrapper('info', MESSAGES.CHANNEL_READY)
      this._amqp.channel = createdChannel
      this._assertQueue()
    })
  }

  /**
   * Assert the queue and bind the consumer function.
   * @private
   */
  _assertQueue () {
    this._amqp.channel.assertQueue('', {
      exclusive: true,
      arguments: _get(this._configuration, 'configuration.queueArguments', {})
    }, (channelAssertErr, asserted) => {
      if (channelAssertErr) {
        this._emitAndDispatchError(`${MESSAGES.CHANNEL_ERROR} -> ${channelAssertErr.message}`)
      } else {
        this._messageDispatcherWrapper('info', MESSAGES.CONSUMING)
        this._amqp.queue = asserted.queue
        this._amqp.channel.consume(this._amqp.queue, (data) => {
          if (
            !data ||
            !data.properties ||
            !data.properties.correlationId ||
            !Object.prototype.hasOwnProperty.call(this._correlationIDStorage, data.properties.correlationId)
          ) {
            this._emitAndDispatchError(MESSAGES.INVALID_DATA)
            return
          }

          this._logData(data.content, 'consume')
          const replied = this._parseConsumedMessage(data)
          delete this._correlationIDStorage[data.properties.correlationId]
          this._E.emit('reply', replied, data.properties.correlationId)
        }, {
          noAck: true
        })
      }
    })
  }
}

module.exports = RpcClient