managers/Transmitter.js

let Promise
try {
  Promise = require('bluebird')
} catch (err) {
  Promise = global.Promise
}

const crypto = require('crypto')

const Collection = require('../util/Collection')

/**
 * Manager class for inter-process communication
 * @prop {String} pid Process ID
 * @prop {Map} commands IPC commands
 */
class Transmitter extends Collection {
  /**
   * Creates a new Transmitter instance
   * @arg {Client} client Client instance
   */
  constructor (client) {
    super()

    this.pid = process.pid
    this._client = client

    process.on('message', this.onMessage.bind(this))
  }

  /**
   * Sends a message to the master IPC process
   * @arg {String} event Event name
   * @arg {*} data Attached data
   */
  send (event, data) {
    process.send({
      op: event,
      d: data
    })
  }

  onMessage (message) {
    if (!message.op) {
      if (!this._client.suppressWarnings) {
        this._logger.warn('Received IPC message with no op')
      }
      return
    }

    if (['resp', 'broadcast'].includes(message.op)) return

    if (this[message.op]) {
      return this[message.op](message)
    }

    const command = this.get(message.op)
    if (command) {
      return command(message, this._bot)
    }
  }

  /**
   * Awaits for a certain response
   * @arg {String} op op code
   * @arg {*} d Attached data
   * @returns {Promise<*>}
   */
  async awaitResponse (op, d) {
    const code = crypto.randomBytes(64).toString('hex')
    return new Promise((resolve, reject) => {
      const awaitListener = (msg) => {
        if (!['resp', 'error'].includes(msg.op)) return
        process.removeListener('message', awaitListener)
        if (msg.op === 'resp' && msg.code === code) return resolve(msg.d)
        if (msg.op === 'error') return reject(msg.d)
      }

      const payload = { op, code }
      if (d) payload.d = d

      process.on('message', awaitListener)
      process.send(payload)

      setTimeout(() => {
        process.removeListener('message', awaitListener)
        return reject('IPC timed out after 2000ms')
      }, 2000)
    })
  }

  /**
   * Registers an IPC command
   * @arg {function} command Command function
   * @arg {String} command.name Name of the IPC command
   * @returns {Transmitter}
   */
  register (command) {
    if (!command || !command.name) {
      this._client.throwOrEmit('ipc:error', new TypeError(`Invalid command - ${command}`))
      return
    }
    this.set(command.name, command)
    return this
  }

  /**
   * Unregisters an IPC command by name
   * @arg {String} name Name of the IPC command
   * @returns {Transmitter}
   */
  unregister (name) {
    this.delete(name)
    return this
  }

  /**
   * Fires when an error occurs in Transmitter
   * @event Client#ipc:error
   * @type {Error}
   */
}

module.exports = Transmitter