rpc/rpc_server.js

const MESSAGES = require('../internal').MESSAGES
const _get = require('lodash/get')
const _isString = require('lodash/isString')

/**
 * RPC server. Init example:
 * <pre>
 *     const iamqp              = require('iamqp');
 *     const amqpUri            = 'amqp://localhost';
 *     const rpcChannel         = 'rpc-channel-a';
 *     let rpcServer            = new iamqp.RPCServer(amqpUri, rpcChannel);
 *
 *     // This needs to be done or the errors will bubble up.
 *     rpcServer.getEventer().on('error', (err) => {
 *       console.log('RPC server error:' + err.message);
 *     });
 *
 *     // For when the connection is established.
 *     rpcServer.getEventer().on('connected', () => {
 *       console.log('RPC server connected');
 *     });
 *
 *     // An event gets emitted on each call.
 *     // Handling of the pCorrelationId is optional and should be done only if you expect a single RPC server instance
 *     // to handle multiple RPC clients.
 *     // For example if the RPC server interacts with a DB on the behalf of the client and the calls are just
 *     // coming in.
 *     // This means that you can parameters pCorrelationId in both receiving the call and responding.
 *     rpcServer.getEventer().on('call', (callData, pCorrelationId) => {
 *       console.log('RPC server got a call: ' + callData);
 *
 *       // ...
 *
 *       let isServerResponding = rpcServer.respond({
 *         'a': 3
 *       }, pCorrelationId);
 *     });
 *
 *     // Open the connection - this takes some time and is not sync
 *     rpcServer.openConnection();
 *
 *     // ...
 *
 *     // Close the connection
 *     if (rpcServer.closeConnection()) {
 *       console.log('RPC server connection closing ...');
 *     }
 * </pre>
 * ...
 */
class RpcServer extends require('../common') {
  /**
   * @param {AmqpURI} amqpUri
   * @param {ChannelName} channelName
   * @param {Configuration} [configuration]
   */
  constructor (amqpUri, channelName, configuration) {
    super(amqpUri, channelName, configuration)
    this._name = `RPC-SERVER: ${this._configuration.channelName}`
    this._messageDispatcherWrapper('info', MESSAGES.INIT)
    this._lastMessage = null
    this._calls = {}

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

  /**
   * 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} -> ${MESSAGES.QUEUE_ASSERTING} -> ${MESSAGES.BINDING_CONSUMER_FUNCTION}`)
      this._lastMessage = null
      this._amqp.channel = createdChannel
      this._amqp.channel.assertQueue(this._configuration.channelName, {
        durable: false,
        arguments: _get(this._configuration, 'configuration.queueArguments', {})
      })

      // this._amqp.channel.prefetch(1);
      this._amqp.channel.consume(this._configuration.channelName, (data) => {
        if (
          data &&
          _get(data, 'properties.replyTo', null) &&
          _get(data, 'properties.correlationId', null)
        ) {
          this._calls[data.properties.correlationId] = data
          this._lastMessage = data
          this._E.emit('call', this._parseConsumedMessage(data), data.properties.correlationId)
        } else {
          this._lastMessage = null
          this._messageDispatcherWrapper('error', `${MESSAGES.GOT_MESSAGE} -> ${MESSAGES.INVALID_PROPERTY}`)
        }
      }, {
        noAck: false
      })
    })
  }

  /**
   * Respond to the last call/message.
   * @param {number | string | boolean | Object | Array} contentToPublish
   * @param {string} correlationId correlation ID (as gotten from the call) parameter to use when the server
   * is to process multiple calls at once
   * @returns {boolean}
   */
  respond (contentToPublish, correlationId) {
    if (this._isReady === true) {
      let lastMessage = null
      let isFromCallsStorage = false

      if (correlationId) {
        if (
          _isString(correlationId) &&
          Object.prototype.hasOwnProperty.call(this._calls, correlationId)
        ) {
          lastMessage = this._calls[correlationId]
          isFromCallsStorage = true
        } else {
          this._messageDispatcherWrapper('error', 'attempting to respond with an invalid correlation ID')
          return false
        }
      } else if (this._lastMessage) {
        lastMessage = this._lastMessage
      }

      if (
        !lastMessage ||
        !_get(lastMessage, 'properties.replyTo', null) ||
        !_get(lastMessage, 'properties.correlationId', null)
      ) {
        this._messageDispatcherWrapper('error', 'attempting to respond to an invalid message')
        return false
      }

      this._amqp.channel.sendToQueue(
        lastMessage.properties.replyTo,
        this._preparePublishingMessage(contentToPublish),
        {
          correlationId: lastMessage.properties.correlationId
        }
      )

      this._amqp.channel.ack(lastMessage)
      process.nextTick(() => {
        delete this._calls[lastMessage.properties.correlationId]
        if (!isFromCallsStorage) {
          this._lastMessage = null
        }
      })

      return true
    } else {
      this._messageDispatcherWrapper('error', `${MESSAGES.RESPOND} -> ${MESSAGES.CONNECTION_ERROR}`)
      return false
    }
  }
}

module.exports = RpcServer