Home Reference Source Repository

es6/Services/Advertiser.js

import os         from 'os'
import Promise    from 'bluebird'
import genName    from 'project-name-generator'

import thinky     from '../thinky'
import Loop       from './Loop'
import Registry   from './Registry'

import Table      from '../utils/Table'
import Events     from '../utils/Events'
import fs         from '../utils/fs'


const {type, r} = thinky

/**
 * API that handles the lifecycle for updating and maintaing local copies of a Discovery
 */
export default class Discovery extends Table {
  /**
   * normalizes the `os.interfaces` Object
   *
   * @return     {Array}  os.interfaces() => Array
   */
  static get interfaces () {
    const ifaces = os.networkInterfaces()
    const allAddresses = {}
    
    Object.keys(ifaces).forEach(function (iface) {
        const addresses = {}
        let hasAddresses = false
        ifaces[iface].forEach(function (address) {
            if (!address.internal) {
                addresses[(address.family || "").toLowerCase()] = address.address
                hasAddresses = true
                if (address.mac) {
                    addresses.mac = address.mac
                }
            }
        })
        if (hasAddresses) {
            allAddresses[iface] = addresses
        }
    })

    return Object.keys(allAddresses).reduce( (addresses, address) => {
      allAddresses[address].name = address
      return addresses.concat([  allAddresses[address] ])
    }, [])

  }

  /**
   * initializes and resolves to an Advertiser instance
   *
   * @param      {Object}  settings  The settings
   */
  constructor (settings = {}) {
    super()

    return this.table.ready()
      .bind(this)
      .then(this.loadContext)
      .then(this.advertise)
      .return(this)
  }

  /**
   * returns the location where an Instance's id file should reside
   * 
   * @property file
   * @return   {String}
   */
  get file () {
    return Registry.appDir('id.json')
  }

  /**
   * returns a snapshot of the current Instance
   *
   * @attribute  attributes
   * @return     {Object}  the snapshot to use in a heartbeat
   * 
   */
  get attributes () {
    return {
        id         : this.id
      , heartbeat  : Date.now()
      , load       : os.loadavg()
      , interfaces : Discovery.interfaces
      , cpus       : os.cpus()
    }
  }

  /**
   * generates the Instance context
   * 
   * @return {Promise}
   */
  loadContext () {
    return Promise.props({
        loop       : Loop.create()
      , id         : this.loadId()
    }).then( props => Object.assign(this, props) )
  }

  /**
   * Determines if it has id file.
   *
   * @return     {Promise}  resolves to True if has id file, False otherwise.
   */
  hasIdFile () {
    return fs.stat$( this.file ).catch( err => {
      if ( err.code === "ENOENT" ) return false
      throw err
    })
  }

  /**
   * fetches or creates a new ID for the Instance
   *
   * @return     {Promise} Resolve to the id
   */
  loadId () {
    return Registry.ensureAppDir()
      .bind(this)
      .then(this.hasIdFile)
      .then( exists =>  {
        return exists
          ? true
          : r.uuid().run().bind(this).then(this.saveId)
      })
      .then( _=> require(this.file).id )
  }

  /**
   * saves the id to the local filesystem to survive Instance restarts
   *
   * @param      {String}  id      the id to be persisted
   * @return     {Promise}  
   */
  saveId (id) {
    return fs.writeJson$( this.file, { id: id } ).then( _ => {
      return Discovery.row(this.attributes).save()
    })
  }

  /**
   * heartbeat to let other Services know that this one is alive
   *
   * @return     {Promise}
   */
  heartbeat () {
    return this.table.insert(this.attributes, { conflict : "update" }).run().then( instance =>  {
      return this.emit(Discovery.Event.hb, instance)
    })
  }

  /**
   * main loop that generates heartbeats
   *
   * @return     {null}
   */
  advertise () {
    this.advertising = true
    this.loop.on( Loop.Event.tick, done => {
      return this.heartbeat()
        .then(done)
    })

    this.loop.start()
  }

  /**
   * kills the Advertiser instance and makes sure we don't spawn a ZOMBIEEEEE
   */
  kill () {
    this.removeAllListeners()
    this.loop.kill()
  }
}

Discovery.Event = {
    join    : "discovery:join"
  , sibling : "discovery:sibling"
  , error   : "discovery:error"
  , hb      : "discovery:heartbeat"
}

Events.methodize( Discovery, Discovery.Event )

Discovery.createModel( 'Services', {
    name        : type.string().default( function () { return genName({ words: 4, number: true }).dashed })
  , service     : type.string().default("wyst:queue")
  , host        : type.string().default(os.hostname)
  , os          : type.string().default(os.type)
  , cpus        : type.array().default(os.cpus)
  , load        : type.array().default(os.loadavg)
  , interfaces  : type.array().default( function () { return Discovery.interfaces })
  , created_at  : type.date().default(Date.now)
  , heartbeat   : type.date().default(Date.now)
})