All files / copacetic/lib dependency.js

100% Statements 42/42
90.47% Branches 19/21
100% Functions 11/11
100% Lines 42/42

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 1641x 1x   1x 1x 1x                                   50x       50x       50x       50x   50x 49x     50x 50x 50x 50x 50x 50x                               54x           54x 8x   54x             20x 20x             12x 12x             1x                     29x     30x         9x 9x   9x     20x 20x 3x 2x   2x     18x   18x         1x 12x           12x                         50x                  
const isObject = require('lodash.isobject')
const precond = require('precond')
 
const dependencyLevel = require('./dependency-level')
const HealthFactoryProvider = require('./health-strategies').HealthFactoryProvider
const ExponentialBackoffStrategy = require('./backoff-strategies').ExponentialBackoffStrategy
 
/**
 * @class
 * @classdesc Holds information about the health of a dependency, and executes strategies for
 * checking a dependency's health
 */
class Dependency {
  /**
   * @param {Function} [backoffStrategy=ExponentialBackoffStrategy] The behaviour of the delay
   * when re-checking a dependencys
   * @param {Function} [healthStrategy=HttpStrategy] The strategy used to check a dependency's
   * health
   * @param {Enum} [level] A description of the reliance on this dependency
   * @param {String} name The identifier to be used for a dependency
   * @param {String} [url] The location where the dependency can be reached
   */
  constructor ({ backoffStrategy, healthStrategy, level, name, url } = {}) {
    precond.checkArgument(
      isObject(backoffStrategy) && backoffStrategy.execute,
      'backoffStrategy must be of type object and have a "execute" method'
    )
    precond.checkArgument(
      isObject(healthStrategy) && healthStrategy.check,
      'healthStrategy must be of type object and have a "check" method'
    )
    precond.checkArgument(
      level === 'HARD' || level === 'SOFT',
      'a dependency must either have a dependency level of HARD or soft'
    )
    precond.checkIsString(name, 'The dependency name must be a string')
 
    if (url) {
      precond.checkIsString(url, 'The dependency url must be a string')
    }
 
    this.backoffStrategy = backoffStrategy
    this.healthStrategy = healthStrategy
    this.level = level
    this.healthy = true
    this.name = name
    this.url = url
  }
 
  /**
   * @return {Object} A description of the dependency's health
   */
  get healthSummary () {
    /**
     * Health information about a dependency
     * Strategies may alter this summary to provide additional information
     * @typedef {Object} Dependency~HealthInfo
     * @property {String} name
     * @property {Boolean} healthy
     * @property {String} level
     * @property {Date} lastChecked
     */
    const summary = {
      name: this.name,
      healthy: this.healthy,
      level: this.level,
      lastChecked: this.lastChecked
    }
    if (this.healthStrategy.improveSummary) {
      this.healthStrategy.improveSummary(summary, this.lastCheckResult)
    }
    return summary
  }
 
  /**
   * Sets a dependency's status to healthy
   */
  onHealthy () {
    this.healthy = true
    this.lastChecked = new Date()
  }
 
  /**
   * Sets a dependency's status to unhealthy
   */
  onUnhealthy () {
    this.healthy = false
    this.lastChecked = new Date()
  }
 
  /**
   * Performs any cleanup needed
   */
  cleanup () {
    this.healthStrategy.cleanup()
  }
 
  /**
   * @param {Integer} failAfter The number of times a dependency's health
   * will be checked, before giving up
   * @param {Integer} maxDelay The maximum interval of time to wait when retrying
   * @return {Promise}
   */
  check (retries = 1, maxDelay = 30000) {
    // console.log('here', retries, maxDelay)
    return this
      .backoffStrategy
      .execute({
        func: () => this.healthStrategy.check(this.url),
        retries,
        maxDelay
      })
      .catch((e) => {
        this.onUnhealthy()
        this.lastCheckResult = undefined
 
        return Promise.reject(this.healthSummary)
      })
      .then((result) => {
        this.lastCheckResult = result
        if (this.healthStrategy.areYouOk) {
          if (!this.healthStrategy.areYouOk(result)) {
            this.onUnhealthy()
 
            return Promise.reject(this.healthSummary)
          }
        }
        this.onHealthy()
 
        return Promise.resolve(this.healthSummary)
      })
  }
}
 
module.exports = (injector) => {
  const HealthStrategy = HealthFactoryProvider(injector)
 
  /**
   * @constructs Dependency
   * @param {Object} opts Args for creating an instance of Dependency
   */
  return ({
    backoffStrategy = ExponentialBackoffStrategy({
      retries: 3,
      maxDelay: 0,
      multiplier: 1000,
      onSuccessEvt: 'healthy',
      onFailureEvt: 'unhealthy'
    }),
    strategy = { type: 'http' },
    level = dependencyLevel.SOFT,
    name,
    url
  } = {}) => {
    return new Dependency({
      backoffStrategy,
      healthStrategy: HealthStrategy(strategy),
      level,
      name,
      url
    })
  }
}