connection_manager.js

/*
 * Copyright 2017-2019 Tom Swindell
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
const EventEmitter = require('events')

/**
 * @private
 */
const sleep = (delay) => new Promise((resolve, reject) => setTimeout(resolve, delay))

/**
 * Emitted when a new connector has been attached to this instance
 * @event ConnectionManager#attached
 * 
 * @property {string} uid
 * 
 * @public
 */
/**
 * Emitted when a connector has been detached from this instance.
 * @event ConnectionManager#detached

 * @property {string} uid
 * 
 * @public
 */
/**
 * Emitted when a connector has began connecting to a remote device.
 * @event ConnectionManager#connecting
 * 
 * @property {string} uid
 * 
 * @public
 */
/**
 * Emitted when a device has connected.
 * @event ConnectionManager#connected
 * 
 * @property {string} uid
 * @property {Channel} channel
 * 
 * @public
 */
/**
 * Emitted when a device has disconnected.
 * @event ConnectionManager#disconnected
 * 
 * @property {string} uid
 * 
 * @public
 */
/**
 * Emitted when an error has occured
 * @event ConnectionManager#error
 * 
 * @property {Error} error
 * 
 * @public
 */


/**
 * Creates a new ARSDK ConnectionManager instance for managing connections
 * to ARSDK based protocol devices.
 * 
 * @class
 * @implements EventEmitter
 * @public
 * 
 * @hideconstructor
 * 
 * @param {string} [controllerType="js-arsdk"] Controller type to declare when connecting
 * @param {string} [controllerName=HOSTNAME] Controller name to declare when connecting
 * @param {Object} [opts]
 * @param {boolean} [opts.debug=false] Enable debugging output
 * 
 * @example
 * 
 * const manager = new ConnectionManager('js-arsdk', 'MyController')
 * 
 * manager.on('connected', (uid, device) => { ... })
 * manager.on('disconnected', (uid) => { ... })
 * manager.on('error', (error) => { ... })
 * 
 * manager.attach(new NetworkConnector('192.168.53.1', 44444))
 * 
 */
function ConnectionManager (controllerType, controllerName, __opts) {
  const __self = this

  const __connectors = {}
  const __connections = {}

  // Extend instance with EventEmitter class
  EventEmitter.call(this)

  /**
   * Attach a device protocol connector to this manager for managing
   * device connectivity.
   * 
   * @method ConnectionManager#attach
   * @public
   * 
   * @param {ConnectorInterface} connector
   * @param {Object} [opts]
   * @param {boolean} [opts.autoconnect=true] Automatically connect
   * @param {boolean} [opts.reconnect=true] Enable/Disable auto-reconnects
   * 
   * @returns {Promise}
   */
  const attach = (connector, opts) => {
    opts = Object.assign({
      autoconnect: true,
      reconnect: true
    }, opts || {})

    __connectors[connector.uid()] = [connector, opts]
    this.emit('attached', connector.uid())

    if (opts.autoconnect) return connect(connector.uid())
    return null
  }

  const __onClose = (uid) => {
    const [ _, opts ] = __connectors[uid]

    __connections[uid].off('close', __onClose)

    delete __connections[uid]
    __self.emit('disconnected', uid)

    if (opts.reconnect) setTimeout(() => connect(uid), 1000)
  }

  /**
   * 
   * @method ConnectionManager#connect
   * @public
   * 
   * @param {string} uid 
   * 
   * @returns {MessageChannel}
   */
  const connect = async (uid) => {
    if (uid in __connections) throw 'ALREADY_CONNECTED'
    if (!(uid in __connectors)) throw 'UKNOWN_UID'

    const [ connector, opts ] = __connectors[uid]

    this.emit('connecting', uid)
    try {
      const channel = await connector.connect()

      channel.on('close', __onClose)

      __connections[uid] = channel
      this.emit('connected', uid, channel)

      return channel
    } catch (error) {
      console.error(error.errno)
      if (opts.reconnect) setTimeout(() => connect(uid), 2000)
    }
  }

  /**
   * @method ConnectionManager#disconnect
   * @public
   * 
   * @param {string} uid
   * 
   * @returns {boolean}
   */
  const disconnect = async (uid) => {
    if (!(uid in __connections)) return false

    await __connections[uid].close()
    return true
  }

  /**
   * Detach a device protocol connector from this manager and disconnect from the
   * device if it's connected.
   * @method ConnectionManager#detach
   * @public
   * 
   * @param {string} uid 
   * @param {Object} [opts]
   * @param {boolean} [opts.disconnect=true]
   * 
   * @returns {Promise}
   */
  const detach = async (uid, opts) => {
    if (!(uid in __connectors)) return

    console.info('Detaching connection:', uid)
    if (uid in __connections) {
      console.info('Disconnecting from device:', uid)
      await disconnect(uid)
    }

    delete __connectors[uid]
    this.emit('detached', uid)
  }

  // Expose Public Interface
  this.attach = attach.bind(this)
  this.detach = detach.bind(this)

  this.connect = connect.bind(this)
  this.disconnect = disconnect.bind(this)
}

ConnectionManager.prototype = Object.create(EventEmitter.prototype)

module.exports = ConnectionManager