resilient-http/resilient.js

View on GitHub
resilient.js

Summary

Maintainability
F
2 mos
Test Coverage
/*! resilient - v0.4.0 - MIT License - https://github.com/resilient-http/resilient.js */
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.resilient = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
var _ = require('./utils')

module.exports = Cache

function Cache () {
  this.store = Object.create(null)
}

Cache.prototype.flush = function (key) {
  if (key) {
    this.store[key] = null
  } else {
    this.store = _.emptyObject()
  }
}

Cache.prototype.get = function (key) {
  return key ? this.store[key] : _.clone(this.store)
}

Cache.prototype.set = function (key, data) {
  if (key) {
    this.store[key] = { data: data, time: _.now() }
  }
}

Cache.prototype.time = function (key) {
  var value = this.store[key]
  return value ? value.time : null
}

Cache.prototype.exists = function (key) {
  var value = this.store[key]
  return _.isObj(value) && (
    (_.isArr(value.data) && value.data.length > 0) ||
    _.isObj(value.data)) ||
    false
}

},{"./utils":20}],2:[function(require,module,exports){
var _ = require('./utils')
var resolver = require('./resolver')
var http = require('./http')

module.exports = Client

function Client (resilient) {
  this._resilient = resilient
}

Client.prototype.get = function (path, options, cb) {
  return this.send(path, options, cb, 'GET')
}

Client.prototype.post = function (path, options, cb) {
  return this.send(path, options, cb, 'POST')
}

Client.prototype.put = function (path, options, cb) {
  return this.send(path, options, cb, 'PUT')
}

Client.prototype.del =
Client.prototype.delete = function (path, options, cb) {
  return this.send(path, options, cb, 'DELETE')
}

Client.prototype.patch = function (path, options, cb) {
  return this.send(path, options, cb, 'PATCH')
}

Client.prototype.head = function (path, options, cb) {
  return this.send(path, options, cb, 'HEAD')
}

Client.prototype.send = function (path, options, cb, method) {
  var args = normalizeArgs.call(this, path, options, cb, method)
  var opts = args[0]
  var callback = args[1]

  var promise = new Promise(function (resolve, reject) {
    this._resilient.emit('request:start', opts, this._resilient)

    requester.apply(this, [opts, function (err, res) {
      if (err) {
        reject(err)
      } else {
        resolve(res)
      }
      // Always resolve the callback
      callback(err, res)
    }])
  }.bind(this))

  // If a callback is passed, subscribe to then/catch to avoid node +6.6 stdout warnings
  if (callback !== _.noop) {
    promise.then(callback).catch(callback)
  }

  return promise
}

function requester (options, cb) {
  if (isFullUrlSchema(options)) {
    return plainHttpRequest(this._resilient, options, cb)
  } else {
    return resolver(this._resilient, options, cb)
  }
}

function normalizeArgs (path, options, cb, method) {
  if (typeof options === 'function') {
    cb = options
    options = arguments[0]
  }

  options = mergeHttpOptions(this._resilient, _.isObj(options) ? options : _.emptyObject())

  if (typeof path === 'string') options.path = path
  if (typeof method === 'string') options.method = method
  if (typeof cb !== 'function') cb = _.noop

  return [ options, wrapCallback(this._resilient, cb) ]
}

function wrapCallback (resilient, cb) {
  return once(function finalRequestHandler (err, res) {
    resilient.emit('request:finish', err, res, resilient)
    cb(err, res)
  })
}

function mergeHttpOptions (resilient, options) {
  var defaults = resilient.options('service').get()

  if (options.timeout) {
    options.$timeout = options.timeout
  }

  return _.merge(defaults, options)
}

function isFullUrlSchema (options) {
  return options && ((_.isURI(options.path) || _.isURI(options.url))) || false
}

function plainHttpRequest (resilient, options, cb) {
  if (options.path) {
    options.url = options.path
    options.path = null
  }

  return (resilient._httpClient || http)(options, cb)
}

function once (fn) {
  var called = false
  return function () {
    if (called === false) {
      called = true
      fn.apply(null, arguments)
    }
  }
}

},{"./http":7,"./resolver":13,"./utils":20}],3:[function(require,module,exports){
var defaults = module.exports = Object.create(null)

defaults.service = {
  method: 'GET',
  timeout: 10 * 1000,
  timeouts: null,
  servers: null,
  retry: 0,
  waitBeforeRetry: 50,
  discoverBeforeRetry: true,
  promiscuousErrors: false,
  omitRetryWhen: null,
  omitFallbackWhen: null,
  omitRetryOnMethods: null,
  omitFallbackOnMethods: null,
  omitRetryOnErrorCodes: null,
  omitFallbackOnErrorCodes: null
}

defaults.balancer = {
  enable: true,
  random: false,
  roundRobin: false,
  roundRobinSize: 3,
  balanceStrategy: null,
  disableWeight: false,
  weight: {
    success: 15,
    error: 50,
    latency: 35
  }
}

defaults.discovery = {
  servers: null,
  method: 'GET',
  retry: 3,
  parallel: true,
  waitBeforeRetry: 1000,
  timeout: 2 * 1000,
  cacheEnabled: true,
  cacheExpiration: 60 * 15 * 1000,
  promiscuousErrors: true,
  refreshInterval: 60 * 2 * 1000,
  enableRefreshServers: true,
  enableSelfRefresh: false,
  forceRefreshOnStart: true,
  refreshServersInterval: 60 * 5 * 1000,
  refreshServers: null,
  refreshOptions: null,
  refreshPath: null,
  omitRetryWhen: null,
  omitFallbackWhen: null,
  omitRetryOnMethods: null,
  omitFallbackOnMethods: null,
  omitRetryOnErrorCodes: null,
  omitFallbackOnErrorCodes: null
}

defaults.resilientOptions = [
  'servers',
  'retry',
  'timeouts',
  '$timeout',
  'parallel',
  'cacheEnabled',
  'cacheExpiration',
  'refreshInterval',
  'refreshServers',
  'refreshOptions',
  'refreshPath',
  'waitBeforeRetry',
  'promiscuousErrors',
  'omitRetryWhen',
  'omitFallbackWhen',
  'omitRetryOnMethods',
  'omitFallbackOnMethods',
  'omitRetryOnErrorCodes',
  'omitFallbackOnErrorCodes',
  'enableRefreshServers',
  'refreshServersInterval',
  'discoverBeforeRetry',
  'enableSelfRefresh',
  'forceRefreshOnStart'
]

},{}],4:[function(require,module,exports){
var _ = require('./utils')
var Servers = require('./servers')
var Requester = require('./requester')
var ResilientError = require('./error')

var CONCURRENCY = 3

module.exports = ServersDiscovery

function ServersDiscovery (resilient, options, servers) {
  var requester = Requester(resilient)
  var middleware = resilient._middleware

  function getOptions () {
    return _.merge(resilient.options('discovery').get(), options)
  }

  function getServers () {
    return servers || resilient.servers('discovery')
  }

  function hasDiscoveryServers () {
    var servers = getServers()
    return servers && servers.exists() || false
  }

  function updateServersInParallel (options, cb) {
    var buf = []
    var servers = getServers().sort('read', resilient.balancer())
    var pending = counter(servers.length)

    var onServersUpdateFn = onUpdateServers(cb, buf)
    var onUpdateInParallelFn = onUpdateInParallel(servers, pending, onServersUpdateFn)

    servers.slice(0, CONCURRENCY).forEach(function (server, index) {
      var pool = [ server ]
      if (index === 2 && servers.length > CONCURRENCY) {
        pool = pool.concat(servers.slice(CONCURRENCY))
      }
      requester(new Servers(pool), options, onUpdateInParallelFn, buf)(null)
    })
  }

  function updateServers (options, next) {
    if (options.parallel) {
      updateServersInParallel(options, next)
    } else {
      requester(getServers(), options, onUpdateServers(next))(null)
    }
  }

  function fetchServers (next) {
    var options = getOptions()
    options.params = addTimeStamp(options)

    middleware.run('discovery', 'out')(options, function (err) {
      if (err) {
        next(new ResilientError(1007, err))
      } else {
        updateServers(options, next)
      }
    })
  }

  return function resolver (next) {
    if (hasDiscoveryServers() === false) {
      next(new ResilientError(1002))
    } else {
      fetchServers(next)
    }
  }
}

function onUpdateInParallel (servers, counter, next) {
  return function handler (err, res) {
    var pending = counter()
    if (err == null || pending === 0) {
      counter(true)
      next(err, res)
    }
  }
}

function counter (total) {
  total = total > 2 ? CONCURRENCY : total
  return function decrement (reset) {
    return (total = reset ? -1 : (total - 1))
  }
}

function addTimeStamp (options) {
  return _.extend(options.params || options.qs || {}, { _time: _.now() })
}

function onUpdateServers (cb, buf) {
  return function (err, res) {
    closeAndEmptyPendingRequests(buf)
    cb(err || null, res)
  }
}

function closeAndEmptyPendingRequests (buf) {
  if (buf) {
    if (!isEmptyBuffer(buf)) {
      buf.forEach(closePendingRequest)
    }
    buf.splice(0)
  }
}

function closePendingRequest (client) {
  if (client) {
    if (client.xhr) {
      if (client.xhr.readyState !== 4) {
        closeClient(client.xhr)
      }
    } else {
      closeClient(client)
    }
  }
}

function closeClient (client) {
  if (typeof client.abort === 'function') {
    try { client.abort() } catch (e) {}
  }
}

function isEmptyBuffer (buf) {
  return buf.filter(function (request) { return _.isObj(request) }).length === 0
}

},{"./error":5,"./requester":11,"./servers":18,"./utils":20}],5:[function(require,module,exports){
module.exports = ResilientError

var MESSAGES = {
  1000: 'All requests failed. No servers available',
  1001: 'Cannot update discovery servers. Empty or invalid response body',
  1002: 'Missing discovery servers. Cannot resolve the server',
  1003: 'Cannot resolve servers. Missing data',
  1004: 'Discovery server response is invalid or empty',
  1005: 'Missing servers during retry process',
  1006: 'Internal unexpected error',
  1007: 'Middleware error'
}

function ResilientError (status, error) {
  if (error instanceof Error) {
    Error.call(this)
    this.error = error
    if (error.code) this.code = error.code
    if (error.stack) this.stack = error.stack
  } else if (error) {
    this.request = error
  }
  this.status = status
  this.message = MESSAGES[this.status]

  if (status === 1007 && error) {
    this.message += ': ' + (error.message || error)
  }
}

ResilientError.prototype = Object.create(Error.prototype)

ResilientError.MESSAGES = MESSAGES

},{}],6:[function(require,module,exports){
module.exports = Evaluator

function Evaluator () {
  this.strategies = []
}

Evaluator.prototype.add = function (strategy) {
  if (typeof strategy === 'function') {
    this.strategies.push(strategy)
  }
}

Evaluator.prototype.eval = function (err, res) {
  return this.strategies.some(function (strategy) {
    return strategy(err, res)
  })
}

},{}],7:[function(require,module,exports){
var _ = require('./utils')

var IS_BROWSER = typeof window === 'object' && window
var JSON_MIME = /application\/json/i
var http = resolveModule()

module.exports = HttpClient

function HttpClient () {
  return http.apply(null, arguments)
}

HttpClient.VERSION = http.VERSION
HttpClient.mapResponse = mapResponse

function mapResponse (cb) {
  return function (err, res, body) {
    if (res && res.statusCode) {
      res.status = res.statusCode
    }
    if (body) {
      try {
        (err || res).data = isJSONContent(err || res) ? JSON.parse(body) : body
      } catch (e) {
        err = e
      }
    }
    cb(err, res)
  }
}

function isJSONContent (res) {
  return typeof res.body === 'string' &&
    JSON_MIME.test(res.headers['content-type'])
}

function mapOptions (options) {
  if (typeof options === 'string') {
    options = { url: options }
  } else {
    options = options || {}
  }

  if (options.params) options.qs = options.params
  if (options.data) mapRequestBody(options)
  if (!IS_BROWSER) defineUserAgent(options)

  return options
}

function defineUserAgent (options) {
  options.headers = options.headers || {}

  if (!options.headers['User-Agent']) {
    options.headers['User-Agent'] = getUserAgent()
  }
}

function getUserAgent () {
  return 'resilient-http ' + HttpClient.LIBRARY_VERSION + ' (node)'
}

function mapRequestBody (options) {
  var body = options.data || options.body

  if (body && _.isObj(body) || _.isArr(body)) {
    options.json = true
    options.data = null
  }

  options.body = body
}

function resolveModule () {
  if (IS_BROWSER) {
    return require('lil-http')
  } else {
    return requestWrapper(require('request'))
  }
}

function requestWrapper (request) {
  return function requester (options, cb) {
    return request(mapOptions(options), mapResponse(cb))
  }
}

},{"./utils":20,"lil-http":23,"request":21}],8:[function(require,module,exports){
var http = require('./http')
var Client = require('./client')
var Options = require('./options')
var defaults = require('./defaults')
var Resilient = require('./resilient')

module.exports = Resilient

Resilient.VERSION = '0.4.0'
Resilient.CLIENT_VERSION = http.VERSION
Resilient.defaults = defaults
Resilient.Options = Options
Resilient.Client = Client
Resilient.request = http
http.LIBRARY_VERSION = Resilient.VERSION

// Force globalization in browsers
if (typeof window !== 'undefined' && typeof require === 'function') {
  window.resilient = Resilient
}

},{"./client":2,"./defaults":3,"./http":7,"./options":10,"./resilient":12}],9:[function(require,module,exports){
var _ = require('./utils')
var midware = require('midware')

var hooks = ['in', 'out']

module.exports = Middleware

function Middleware () {
  this.pool = createPool()
}

Middleware.prototype.use = function (resilient, args) {
  var pool = this.pool
  var middlewares = _.toArr(args)

  middlewares
    .filter(_.isFn)
    .map(passArgs(resilient))
    .forEach(register(pool))
}

Middleware.prototype.run = function (type, hook) {
  return this.pool[type][hook].run
}

function createPool () {
  return {
    discovery: hooksMiddleware(),
    service: hooksMiddleware()
  }
}

function hooksMiddleware () {
  return { 'in': midware(), 'out': midware() }
}

function passArgs (resilient) {
  return function (mw) {
    var type = mw.type || 'service'
    var opts = resilient.options(type)
    return { type: type, handler: mw(opts, resilient) }
  }
}

function register (pool) {
  return function (mw) {
    var hook = 'in'
    var handler = mw.handler

    if (_.isFn(handler)) {
      if (handler.hook === 'out') {
        hook = 'out'
      }
      pool[mw.type][hook](handler)
    }

    if (_.isObj(handler)) {
      hooks
        .filter(function (key) {
          return _.isFn(handler[key])
        })
        .forEach(function (key) {
          pool[mw.type][key](handler[key])
        })
    }
  }
}

},{"./utils":20,"midware":24}],10:[function(require,module,exports){
var _ = require('./utils')
var defaults = require('./defaults')
var Servers = require('./servers')

module.exports = Options

function Options (options, type) {
  this.store = type ? _.clone(defaults[type]) : Object.create(null)
  this.set(options)
}

Options.prototype.get = function (key) {
  return key ? this.store[key] : _.clone(this.store)
}

Options.prototype.http = function () {
  return _.omit(this.store, defaults.resilientOptions)
}

Options.prototype.clone = function () {
  return Options.define(getRaw(this.store))
}

Options.prototype.servers = function (servers) {
  return servers ? this.setServers(servers) : this.store.servers
}

Options.prototype.setServers = function (servers) {
  if (this.store.servers) {
    this.store.servers.set(servers)
  } else {
    this.store.servers = new Servers(servers)
  }
}

Options.prototype.set = function (key, value) {
  if (_.isObj(key)) {
    _.each(key, _.bind(this, this.set))
  } else if (value !== undefined) {
    if (key === 'servers') {
      this.setServers(value)
    } else {
      if (key === 'refreshServers') {
        value = new Servers(value)
      }
      this.store[key] = value
    }
  }
}

Options.define = function (options, defaultOptions) {
  var store = new Options()
  var opts = options || {}

  Object.keys(defaultOptions || defaults)
    .filter(function (key) {
      return key !== 'resilientOptions'
    })
    .forEach(function (type) {
      store.set(type, new Options(opts[type], type))
    })

  return store
}

function getRaw (options) {
  var buf = {}
  _.each(options, function (key, value) {
    if (value instanceof Options) {
      buf[key] = value.get()
    }
  })
  return buf
}

},{"./defaults":3,"./servers":18,"./utils":20}],11:[function(require,module,exports){
var _ = require('./utils')
var http = require('./http')
var ResilientError = require('./error')
var resilientOptions = require('./defaults').resilientOptions

module.exports = Requester

function Requester (resilient) {
  return function request (servers, options, cb, buf) {
    var operation = getOperation(options.method)
    var serversList = getServersList(resilient, servers, operation)
    options = _.clone(options)

    return function requestNextServer (previousError) {
      var handler = null
      var server = serversList.shift()

      if (server) {
        options = defineRequestOptions(server, options)
        handler = requestHandler(server, operation, options, resolveRequest(cb), requestNextServer, resilient)
        sendRequest(resilient, options, handler, buf)
      } else {
        handleMissingServers(resilient, servers, options, previousError, cb)
      }
    }
  }
}

function getServersList (resilient, servers, operation) {
  if (resilient.balancer().get('enable')) {
    return servers.sort(operation, resilient.balancer())
  } else {
    return servers.get()
  }
}

function handleMissingServers (resilient, servers, options, getPreviousResponse, cb) {
  var cachedPreviousResponse = getPreviousResponse()
  var responseObj = getFirstValue(cachedPreviousResponse)

  if (shouldOmitRetryCycle(options, responseObj)) {
    cb.apply(null, cachedPreviousResponse)
  } else if (options.retry) {
    retryRequest(resilient, servers, options, cb)
  } else {
    cb(new ResilientError(1000, responseObj))
  }
}

function retryRequest (resilient, servers, options, cb) {
  var onRetry = retrier(resilient, servers, options, cb)
  var retry = waitBeforeRetry(onRetry, options)

  if (options.discoverBeforeRetry) {
    updateDiscoveryServersAndRetry(resilient, retry)
  } else {
    retry()
  }
}

function waitBeforeRetry (retrier, options) {
  return function () {
    _.delay(retrier, options.waitBeforeRetry)
  }
}

function retrier (resilient, servers, options, cb) {
  return function () {
    if (servers.exists()) {
      options.retry -= 1
      resilient.emit('request:retry', options, servers)
      Requester(resilient)(servers, options, cb)()
    } else {
      cb(new ResilientError(1005))
    }
  }
}

function requestHandler (server, operation, options, resolve, nextServer, resilient) {
  var start = _.now()
  return function requestReporter (err, res) {
    var latency = _.now() - start
    resilient.emit('request:incoming', err, res, options, resilient)

    if (shouldOmitFallback(options, err || res)) {
      server.reportError(operation, latency)
      resolve(err, res)
    } else if (isErrorResponse(resilient, options, err, res)) {
      server.reportError(operation, latency)
      resilient.emit('request:fallback', options, err || res)
      nextServer(memoizeResponse(err, res))
    } else {
      server.report(operation, latency)
      resolve(err, res)
    }
  }
}

function memoizeResponse (err, res) {
  return function (cb) {
    return [ err, res ]
  }
}

function sendRequest (resilient, options, handler, buf) {
  resilient.emit('request:outgoing', options, resilient)

  try {
    var request = getHttpClient(resilient)(_.omit(options, resilientOptions), handler)
    if (buf) buf.push(request)
  } catch (err) {
    handler(err)
  }
}

function resolveRequest (cb) {
  var mapAndResolve = http.mapResponse(cb)
  return function (err, res) {
    var resolution = err || res
    var body = resolution ? resolution.body || resolution.data : null
    mapAndResolve(err, res, body)
  }
}

function defineRequestOptions (server, options) {
  options.url = _.join(server.url, options.basePath, options.path)
  options.timeout = getTimeout(options)
  return options
}

function getTimeout (options) {
  var timeout = options.$timeout || options.timeout
  var timeouts = options.timeouts
  var method = options.method

  if (!options.$timeout && _.isObj(timeouts)) {
    timeout = timeouts[method] || timeouts[method.toLowerCase()] || timeout
  }

  return timeout
}

function getFirstValue (arr) {
  return arr.filter(function (v) { return v != null }).pop()
}

function updateDiscoveryServersAndRetry (resilient, onRetry) {
  if (resilient.hasDiscoveryServers()) {
    resilient._sync.unlock('updating')
    Requester.DiscoveryResolver.update(resilient, null, onRetry)
  } else {
    onRetry()
  }
}

function shouldOmitRetryCycle (options, err) {
  return shouldOmitOnErrorCode(options.omitRetryOnErrorCodes, err ? err.status : null) ||
    shouldOmitOnMethod(options.omitRetryOnMethods, options.method) ||
    shouldOmitWhenRules(options.omitRetryWhen, options.method, err)
}

function shouldOmitFallback (options, err) {
  return shouldOmitFallbackOnErrorCode(options, err) ||
    shouldOmitFallbackOnMethod(options) ||
    shouldOmitWhenRules(options.omitFallbackWhen, options.method, err)
}

function shouldOmitFallbackOnMethod (options) {
  var methods = options.omitFallbackOnMethods
  return shouldOmitOnMethod(methods, options.method)
}

function shouldOmitWhenRules (rules, method, res) {
  return _.isArr(rules) &&
    res && res.status &&
    rules.some(ruleFilter(method, res))
}

function ruleFilter (method, err) {
  return function (rule) {
    return _.isObj(rule) &&
      matchRuleMethod(rule, method) &&
      matchRuleStatusCode(rule, err.status)
  }
}

function matchRuleMethod (rule, method) {
  return ((rule.method && rule.method === method) ||
    _.isArr(rule.methods) && matchHttpMethod(rule.methods, method))
}

function matchRuleStatusCode (rule, status) {
  return status && ((rule.code && rule.code === status) ||
    (_.isArr(rule.codes) && shouldOmitOnErrorCode(rule.codes, status)))
}

function shouldOmitFallbackOnErrorCode (options, err) {
  var codes = options.omitFallbackOnErrorCodes
  return err && shouldOmitOnErrorCode(codes, err.status)
}

function shouldOmitOnMethod (methods, method) {
  return _.isArr(methods) && methods.length && matchHttpMethod(methods, method)
}

function shouldOmitOnErrorCode (codes, status) {
  return status && _.isArr(codes) && codes.length &&
    ~codes.indexOf(status) &&
    status >= 300
}

function matchHttpMethod (methods, method) {
  method = (method || 'GET').toUpperCase()
  return methods
    .filter(function (m) { return typeof m === 'string' })
    .map(function (m) { return m.toUpperCase().trim() })
    .some(function (m) { return m === method })
}

function isErrorResponse (resilient, options, err, res) {
  return isUnavailableStatus(err, res) ||
    resilient._failStrategies.eval(err, res) ||
    (options.promiscuousErrors && (isErrorStatus(err || res)))
}

function isUnavailableStatus (err, res) {
  return (err && (err.code !== undefined ||
    (err.status === undefined && res === undefined))) ||
    isInvalidStatus(err || res) ||
    false
}

function isInvalidStatus (res) {
  return checkResponseStatus(429, res)
}

function isErrorStatus (res) {
  return checkResponseStatus(400, res)
}

function checkResponseStatus (code, res) {
  return (res && !res.code && (res.status >= code || res.status === 0)) || false
}

function getOperation (method) {
  return method.toUpperCase() === 'GET' ? 'read' : 'write'
}

function getHttpClient (resilient) {
  return typeof resilient._httpClient === 'function' ? resilient._httpClient : http
}

},{"./defaults":3,"./error":5,"./http":7,"./utils":20}],12:[function(require,module,exports){
var EventBus = require('lil-event')
var _ = require('./utils')
var Sync = require('./sync')
var Cache = require('./cache')
var Client = require('./client')
var Options = require('./options')
var Middleware = require('./middleware')
var Evaluator = require('./evaluator')
var DiscoveryResolver = require('./resolvers/discovery')

module.exports = Resilient

function Resilient (options) {
  if (!(this instanceof Resilient)) return new Resilient(options)
  this.cache = new Cache()
  this._sync = new Sync()
  this._client = new Client(this)
  this._middleware = new Middleware()
  this._failStrategies = new Evaluator()
  this._options = Options.define(options)
}

Resilient.prototype = Object.create(EventBus.prototype)

Resilient.prototype.servers = function (type) {
  var options = this.options(type || 'service')
  return options ? options.servers() : null
}

Resilient.prototype.setServers = function (list, type) {
  this.options(type || 'service').servers(list)
  return this
}

Resilient.prototype.serversURL = function (type) {
  var servers = this.servers(type)
  return servers ? servers.urls() : null
}

Resilient.prototype.discoveryServers = function (list) {
  if (_.isArr(list)) {
    this.options('discovery').servers(list)
  } else {
    return this.servers('discovery')
  }
}

Resilient.prototype.hasDiscoveryServers = function () {
  var servers = this.discoveryServers()
  return _.isObj(servers) && servers.exists() || false
}

Resilient.prototype.resetScore =
Resilient.prototype.resetStats = function (type) {
  var servers = this.options(type || 'service').servers()
  if (servers) {
    servers.resetStats()
  }
  return this
}

Resilient.prototype.latestServers =
Resilient.prototype.getUpdatedServers = function (options, cb) {
  cb = typeof options === 'function' ? options : cb

  if (this.discoveryServers()) {
    this.discoverServers(options, cb)
  } else if (this.servers('service')) {
    cb(null, this.servers('service').urls())
  } else {
    cb(new Error('Missing servers'))
  }

  return this
}

Resilient.prototype.discoverServers = function (options, cb) {
  return updateServers(this, 'fetch', options, cb)
}

Resilient.prototype.updateServers = function (options, cb) {
  return updateServers(this, 'update', options, cb)
}

function updateServers (resilient, method, options, cb) {
  if (typeof options === 'function') { cb = options; options = null }
  DiscoveryResolver[method](resilient, options, cb || _.noop)
  return resilient
}

Resilient.prototype.options = function (type, options) {
  if (type && _.isObj(options)) {
    var store = this._options.get(type)
    if (store instanceof Options) {
      store.set(options)
    }
    return
  }

  if (_.isObj(type)) {
    this._options = Options.define(type)
    return
  }

  return this._options.get(type)
}

Resilient.prototype.discoveryOptions = function (options) {
  if (options) {
    this.options('discovery', options)
  } else {
    return this.options('discovery').get()
  }
}

Resilient.prototype.serviceOptions = function (options) {
  if (options) {
    this.options('service', options)
  } else {
    return this.options('service').get()
  }
}

Resilient.prototype.httpOptions = function (type) {
  var options = this.options(type || 'service')
  if (options) return options.http()
}

Resilient.prototype.useHttpClient = function (client) {
  if (typeof client === 'function') {
    this._httpClient = client
  }
  return this
}

Resilient.prototype.restoreHttpClient = function () {
  this._httpClient = null
  return this
}

Resilient.prototype.serversUpdated = function () {
  var updated = this.servers().lastUpdate()
  var refresh = this.options('discovery').get('refreshInterval') || 0
  return updated < refresh
}

Resilient.prototype.balancer = function (options) {
  if (options) {
    this.options('balancer', options)
  } else {
    return this.options('balancer')
  }
}

Resilient.prototype.send =
Resilient.prototype.request = function (path, options, cb) {
  return this._client.send(path, options, cb)
}

Resilient.prototype.mock = function (mockFn) {
  this.useHttpClient(function (options, cb) {
    _.delay(function () { mockFn(options, cb) })
  })
  return this
}

Resilient.prototype.unmock = function () {
  this.restoreHttpClient()
  return this
}

Resilient.prototype.use = function () {
  this._middleware.use(this, arguments)
  return this
}

Resilient.prototype.http =
Resilient.prototype.client = function () {
  return this._client
}

Resilient.prototype.failStrategy =
Resilient.prototype.addFailStrategy = function (strategy) {
  this._failStrategies.add(strategy)
  return this
}

;['get', 'post', 'put', 'del', 'delete', 'head', 'patch']
  .forEach(function defineMethodProxy (verb) {
    Resilient.prototype[verb] = function (path, options, cb) {
      return this._client[verb](path, options, cb)
    }
  })

},{"./cache":1,"./client":2,"./evaluator":6,"./middleware":9,"./options":10,"./resolvers/discovery":14,"./sync":19,"./utils":20,"lil-event":22}],13:[function(require,module,exports){
var _ = require('./utils')
var Servers = require('./servers')
var Requester = require('./requester')
var ResilientError = require('./error')
var ServersDiscovery = require('./discovery')
var DiscoveryResolver = require('./resolvers/discovery')

module.exports = Resolver

function Resolver (resilient, options, cb) {
  cb = wrapHandler(cb)

  var sync = resilient._sync
  var middleware = resilient._middleware

  try {
    resolve(resolver)
  } catch (err) {
    cb(new ResilientError(1006, err))
  }

  function resolve (next) {
    if (hasDiscoveryServersOutdated()) {
      updateDiscoveryServers(next)
    } else if (hasValidServiceServers()) {
      next()
    } else if (hasDiscoveryServers()) {
      updateServiceServers(next)
    } else {
      next(new ResilientError(1002))
    }
  }

  function updateDiscoveryServers (next) {
    var options = discoveryOptions()
    var servers = getRefreshServers(options)
    var refreshOptions = getRefreshOptions(options)

    if (sync.locked('discovering')) {
      sync.enqueue('discovering', onRefreshServers(options, next))
    } else {
      sync.lock('discovering')
      ServersDiscovery(resilient, refreshOptions, servers)(onRefreshServers(options, next))
    }
  }

  function onRefreshServers (options, next) {
    return function (err, res) {
      sync.unlock('discovering')
      sync.dequeue('discovering').forEach(function (cb) { cb(err, res) })

      middleware.run('discovery', 'in')(err, res, function (mErr) {
        if (err || mErr) {
          next(new ResilientError(err ? 1001 : 1007, err || mErr))
        } else if (res && res.data) {
          refreshDiscoveryServers(res.data, options)
          updateServiceServers(next)
        } else {
          next(new ResilientError(1004, err))
        }
      })
    }
  }

  function discoveryOptions () {
    return resilient.options('discovery')
  }

  function refreshDiscoveryServers (data, options) {
    resilient.emit('discovery:refresh', data, resilient)
    options.servers(data)
  }

  function updateServiceServers (next) {
    DiscoveryResolver.update(resilient, null, next)
  }

  function hasDiscoveryServersOutdated () {
    var outdated = false
    var options = discoveryOptions()
    var servers = options.get('servers')

    if (canUpdateDiscoveryServers(options)) {
      if (servers && servers.exists()) {
        if (options.get('forceRefreshOnStart')) {
          outdated = servers.updated === 0
        }
        if (!outdated) {
          outdated = servers.lastUpdate() > options.get('refreshServersInterval')
        }
      } else {
        outdated = true
      }
    }

    return outdated
  }

  function hasDiscoveryServers () {
    return resilient.hasDiscoveryServers()
  }

  function hasValidServiceServers () {
    var servers = resilient.servers()
    return servers && servers.exists() && serversAreUpdated(servers) || false
  }

  function serversAreUpdated (servers) {
    var interval = discoveryOptions().get('refreshInterval')
    if (hasDiscoveryServers()) {
      return servers.lastUpdate() < interval
    }
    return true
  }

  function resolver (err, res) {
    err ? cb(err) : handleResolution(res)
  }

  function handleResolution (res) {
    var servers = resilient.servers()
    var requester = Requester(resilient)

    if (res && res._cache) {
      servers = new Servers(res.data)
    } else if (!hasValidServiceServers()) {
      return cb(new ResilientError(1003))
    }

    middleware.run('service', 'out')(servers, options, function (err) {
      if (err) {
        cb(new ResilientError(1007, err))
      } else {
        requester(servers, options, cb)(null)
      }
    })
  }

  function wrapHandler (cb) {
    return function (err, res) {
      middleware.run('service', 'in')(err, res, function (mErr) {
        if (err || mErr) {
          cb(err || new ResilientError(1007, mErr))
        } else {
          cb(null, res)
        }
      })
    }
  }
}

function canUpdateDiscoveryServers (options) {
  var refreshServers = options.get('refreshServers')
  return options.get('enableRefreshServers') &&
    (options.get('enableSelfRefresh') ||
    (refreshServers && refreshServers.exists()))
}

function getRefreshServers (options) {
  var type = options.get('enableSelfRefresh')
    ? 'servers'
    : 'refreshServers'
  return options.get(type)
}

function getRefreshOptions (options) {
  var defaultOptions = _.omit(options.get(), ['servers', 'refreshOptions'])
  var refreshOptions = _.merge(defaultOptions, options.get('refreshOptions'), { discoverBeforeRetry: false })
  var basePath = getRefreshBasePath(options.get())
  if (basePath) refreshOptions.basePath = basePath
  return refreshOptions
}

function getRefreshBasePath (options) {
  return options && (options.refreshPath ||
    (_.isObj(options.refreshOptions) &&
    options.refreshOptions.basePath)) ||
    false
}

},{"./discovery":4,"./error":5,"./requester":11,"./resolvers/discovery":14,"./servers":18,"./utils":20}],14:[function(require,module,exports){
var ResilientError = require('../error')
var Requester = require('../requester')
var ServersDiscovery = require('../discovery')
var ServiceResolve = require('./service')

module.exports = DiscoveryResolver

Requester.DiscoveryResolver = DiscoveryResolver

function DiscoveryResolver (resilient, options, servers) {
  var sync = resilient._sync

  function resolver (resolve) {
    return function (err, res) {
      sync.unlock('updating')
      sync.dequeue('updating').forEach(function (cb) { cb(err, res) })
      resolve(err, res)
    }
  }

  function updateServers (cb) {
    sync.lock('updating')
    ServersDiscovery(resilient, options, servers)(resolver(cb))
  }

  return function resolve (cb) {
    if (sync.locked('updating')) {
      sync.enqueue('updating', cb)
    } else {
      updateServers(cb)
    }
  }
}

DiscoveryResolver.update = function (resilient, options, cb) {
  DiscoveryResolver(resilient, options)(ServiceResolve(resilient)(cb))
}

DiscoveryResolver.fetch = function (resilient, options, cb) {
  DiscoveryResolver(resilient, options)(fetchHandler(cb))
}

function fetchHandler (cb) {
  return function (err, res) {
    if (err) {
      cb(err)
    } else if (res && res.data) {
      cb(null, res.data)
    } else {
      cb(new ResilientError(1001, res))
    }
  }
}

},{"../discovery":4,"../error":5,"../requester":11,"./service":15}],15:[function(require,module,exports){
var _ = require('../utils')
var ResilientError = require('../error')

module.exports = ServiceResolve

function ServiceResolve (resilient) {
  var middleware = resilient._middleware
  var options = resilient.options('discovery')

  function isCacheEnabled () {
    return options.get('cacheEnabled')
  }

  function getCache () {
    return resilient.cache.get('servers')
  }

  function emit (name, data) {
    resilient.emit('servers:' + name, data, resilient)
  }

  function saveServers (res, next) {
    var data = res.data
    emit('refresh', data)
    resilient.setServers(data)
    refreshCache(data)
    next(null, res)
  }

  function errorHandler (err, next) {
    if (isCacheEnabled()) {
      resolveFromCache(err, next)
    } else {
      resolveWithError(err, next)
    }
  }

  function resolveFromCache (err, next) {
    var cache = getCache()
    if (hasValidCache(cache)) {
      next(null, { status: 200, _cache: true, data: cache.data })
    } else {
      resolveWithError(err, next)
    }
  }

  function hasValidCache (cache) {
    var expires = options.get('cacheExpiration')
    return cache && _.isArr(cache.data) && (_.now() - cache.time) > expires || false
  }

  function refreshCache (data) {
    if (isCacheEnabled()) {
      emit('cache', data)
      resilient.cache.set('servers', data)
    }
  }

  function handleResponse (err, res, done) {
    if (err) {
      errorHandler(err, done)
    } else if (isValidResponse(res)) {
      saveServers(res, done)
    } else {
      done(new ResilientError(1004, res))
    }
  }

  return function defineServers (done) {
    return function handler (err, res) {
      middleware.run('discovery', 'in')(err, res, function (mErr) {
        handleResponse(err || mErr, res, done)
      })
    }
  }
}

function isValidResponse (res) {
  var valid = false
  if (res) {
    if (_.isArr(res.data) && res.data.length) {
      valid = true
    } else if (typeof res.data === 'string') {
      valid = parseAsJSON(res)
    }
  }
  return valid
}

function parseAsJSON (res) {
  try {
    res.data = JSON.parse(res.data)
    return true
  } catch (e) {
    return false
  }
}

function resolveWithError (err, next) {
  var error = new ResilientError(err.status, err || { status: 1000 })
  return next(error)
}

},{"../error":5,"../utils":20}],16:[function(require,module,exports){
module.exports = roundRobinSerie

function roundRobinSerie (arr, size) {
  var max = +size < 2 ? 2 : size
  var rounds = roundRobin(max, arr).shift()
  return [].concat.apply([], rounds)
}

function roundRobin (n, ps) {
  var k
  var j
  var i
  var rs = [] // rs = round array

  if (!ps) {
    ps = []
    for (k = 1; k <= n; k += 1) ps.push(k)
  } else {
    ps = ps.slice()
  }
  if (n % 2 === 1) {
    ps.push(-1) // so we can match algorithm for even numbers
    n += 1
  }
  for (j = 0; j < n - 1; j += 1) {
    rs[j] = [] // create inner match array for round j
    for (i = 0; i < n / 2; i += 1) {
      if (ps[i] !== -1 && ps[n - 1 - i] !== -1) {
        rs[j].push([ps[i], ps[n - 1 - i]]) // insert pair as a match
      }
    }
    ps.splice(1, 0, ps.pop()) // permutate for next round
  }
  return rs
}

},{}],17:[function(require,module,exports){
var balancerOptions = require('./defaults').balancer

module.exports = Server

function Server (url) {
  this.url = url
  this.resetStats()
}

Server.prototype.report = function (operation, latency, type) {
  var stats = this.stats(operation)
  if (stats) {
    stats[type || 'request'] += 1
    stats.latency = calculateAvgLatency(latency || 0, stats)
  }
}

Server.prototype.reportError = function (operation, latency) {
  this.report(operation, latency, 'error')
}

Server.prototype.balance = function (operation, options) {
  var stats = this.stats(operation)
  var weight = balancerOptions.weight
  var total = stats.request + stats.error
  return total ? calculateStatsBalance(stats, weight, total) : 0
}

Server.prototype.stats = function (operation, field) {
  var stats = this.statsStore[operation || 'read']
  if (stats && field) stats = stats[field]
  return stats
}

Server.prototype.resetStats = function () {
  this.statsStore = createServerStats()
}

function createServerStats () {
  return {
    read: createStats(),
    write: createStats()
  }
}

function createStats () {
  return {
    latency: 0,
    error: 0,
    request: 0
  }
}

function calculateStatsBalance (stats, weight, total) {
  return round(
    ((((stats.request * 100 / total) * weight.success) +
    ((stats.error * 100 / total) * weight.error)) +
    (stats.latency * weight.latency)) / 100)
}

function calculateAvgLatency (latency, stats) {
  return round((latency + stats.latency) / (stats.request + stats.error))
}

function round (number) {
  return +((number * 100) / 100).toFixed(2)
}

},{"./defaults":3}],18:[function(require,module,exports){
var _ = require('./utils')
var Server = require('./server')
var RoundRobin = require('./roundrobin')

module.exports = Servers

function Servers (servers) {
  this.servers = []
  this.updated = 0
  this.set(servers)
}

Servers.prototype.get = function () {
  return this.servers.slice(0)
}

Servers.prototype.set = function (servers) {
  if (_.isArr(servers)) {
    this.updated = _.now()
    this.servers = mapServers.call(this, servers)
  }
}

Servers.prototype.lastUpdate = function () {
  return _.now() - this.updated
}

Servers.prototype.empty = function () {
  return this.servers.length === 0
}

Servers.prototype.exists = function () {
  return this.size() > 0
}

Servers.prototype.size = function () {
  return this.servers.length
}

Servers.prototype.urls = function () {
  return this.servers.map(function (server) {
    return server.url
  })
}

Servers.prototype.resetStats = function () {
  this.servers.forEach(function (server) {
    server.resetStats()
  })
}

Servers.prototype.sort = function (operation, options) {
  var servers = this.servers.slice(0)
  if (servers.length) {
    if (!options.get('disableWeight')) {
      servers.sort(function (x, y) {
        return x.balance(operation, options) - y.balance(operation, options)
      })
    }
    if (typeof options.get('balanceStrategy') === 'function') {
      servers = options.get('balanceStrategy')(servers)
    } else if (options.get('random')) {
      servers = shuffle(servers)
    } else if (options.get('roundRobin')) {
      servers = roundRobinSort(servers, options)
    }
  }
  return servers
}

Servers.prototype.find = function (url) {
  var server = null
  for (var i = 0, l = this.servers.length; i < l; i += 1) {
    if (this.servers[i].url === url) {
      server = this.servers[i]
      break
    }
  }
  return server
}

function isValidURI (uri) {
  if (_.isObj(uri)) uri = uri.url || uri.uri
  return _.isURI(uri)
}

function mapServers (servers) {
  return servers
    .filter(isValidURI)
    .map(_.bind(this, mapServer))
}

function mapServer (data) {
  var server
  if (data instanceof Server) {
    server = data
  } else {
    server = this.find(_.isObj(data) ? data.url : data)
    if (!server) server = new Server(data)
  }
  return server
}

function shuffle (arr) {
  var array = arr.slice(0)
  for (var i = array.length - 1; i > 0; i -= 1) {
    var j = Math.random() * (i + 1) | 0
    var temp = array[i]
    array[i] = array[j]
    array[j] = temp
  }
  return array
}

function roundRobinSort (servers, options) {
  var size = 0
  if (options && options.get('roundRobin')) {
    var configSize = +options.get('roundRobinSize')
    size = configSize > servers.length ? servers.length : configSize
    if (size > 1) {
      return RoundRobin(servers, size)
    }
  }
  return servers
}

},{"./roundrobin":16,"./server":17,"./utils":20}],19:[function(require,module,exports){
var isArr = require('./utils').isArr

module.exports = Sync

function Sync () {
  this.locks = {}
  this.queues = {}
}

Sync.prototype.locked = function (state) {
  return getSync(this.locks, state)
}

Sync.prototype.lock = function (state) {
  this.locks[state] = true
  return true
}

Sync.prototype.unlock = function (state) {
  this.locks[state] = false
  return false
}

Sync.prototype.enqueue = function (state, task) {
  if (getSync(this.locks, state)) {
    this.push(state, task)
  }
}

Sync.prototype.dequeue = function (state) {
  return (this.queues[state] || []).splice(0)
}

Sync.prototype.push = function (state, task) {
  var queue = this.queues[state]
  if (isArr(queue) === false) {
    queue = this.queues[state] = []
  }
  queue.push(task)
}

function getSync (locks, state) {
  var lock = locks[state]
  if (lock === undefined) {
    lock = locks[state] = false
  }
  return lock
}

},{"./utils":20}],20:[function(require,module,exports){
var _ = exports
var toStr = Object.prototype.toString
var slice = Array.prototype.slice
var hasOwn = Object.prototype.hasOwnProperty
var bind = Function.prototype.bind
var isArrayNative = Array.isArray
var uriRegex = /^http[s]?\:\/\/(.+)/i

exports.noop = function () {}

exports.now = function () {
  return new Date().getTime()
}

exports.emptyObject = function () {
  return Object.create(null)
}

exports.isObj = function (o) {
  return o && toStr.call(o) === '[object Object]' || false
}

exports.isArr = function (o) {
  return o && isArrayNative
    ? isArrayNative(o)
    : toStr.call(o) === '[object Array]'
}

exports.isFn = function (o) {
  return typeof o === 'function'
}

exports.bind = function (ctx, fn) {
  return bind ? fn.bind(ctx) : function () {
    return fn.apply(ctx, arguments)
  }
}

exports.each = function (obj, fn) {
  var i = null
  var l = 0
  if (_.isArr(obj)) {
    for (i = 0, l = obj.length; i < l; i += 1) fn(obj[i], i)
  } else if (_.isObj(obj)) {
    for (i in obj) if (hasOwn.call(obj, i)) fn(i, obj[i])
  }
}

exports.clone = function (obj) {
  return _.extend({}, obj)
}

exports.omit = function (obj, keys) {
  var key = null
  var buf = {}
  if (_.isObj(obj)) {
    for (key in obj) if (hasOwn.call(obj, key)) {
      if (keys.indexOf(key) === -1) buf[key] = obj[key]
    }
  }
  return buf
}

exports.delay = function (fn, ms) {
  return setTimeout(fn, ms || 1)
}

exports.isURI = function (str) {
  return typeof str === 'string' && uriRegex.test(str)
}

exports.join = function (base) {
  return (base || '') +
  (_.toArr(arguments, 1)
    .filter(notEmpty)
    .join(''))
}

function notEmpty (str) {
  return typeof str === 'string' && str.length > 0
}

exports.toArr = function (args, index) {
  return slice.call(args, +index || 0)
}

exports.extend = objIterator(extender)

exports.merge = objIterator(merger)

function extender (target, key, value) {
  target[key] = value
}

function objIterator (iterator) {
  return function (target) {
    var args = _.toArr(arguments, 1)
    _.each(args, eachArgument(target, iterator))
    return target
  }
}

function eachArgument (target, iterator) {
  return function (obj) {
    if (_.isObj(obj)) {
      _.each(obj, function (key, value) {
        iterator(target, key, value)
      })
    }
  }
}

function merger (target, key, value) {
  if (_.isObj(value) && _.isObj(target[key])) {
    _.merge(target[key], value)
  } else {
    extender(target, key, value)
  }
}

},{}],21:[function(require,module,exports){

},{}],22:[function(require,module,exports){
/*! lil-event - v0.1 - MIT License - https://github.com/lil-js/event */
(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    define(['exports'], factory)
  } else if (typeof exports === 'object') {
    factory(exports)
    if (typeof module === 'object' && module !== null) {
      module.exports = exports.Event
    }
  } else {
    factory((root.lil = root.lil || {}))
  }
}(this, function (exports) {
  'use strict'
  var VERSION = '0.1.3'
  var slice = Array.prototype.slice
  var hasOwn = Object.prototype.hasOwnProperty

  function Event() {}

  Event.prototype.constructor = Event

  Event.prototype.addListener = Event.prototype.on = function (event, fn, once) {
    if (typeof event !== 'string') throw new TypeError('First argument must be a string')
    if (typeof fn !== 'function') throw new TypeError('Second argument must be a function')
    if (!findListener.call(this, event, fn)) {
      getListeners.call(this, event).push({ fn: fn, once: once || false })
    }
    return this
  }

  Event.prototype.removeListener = Event.prototype.off = function (event, fn) {
    var index
    var listeners = getListeners.call(this, event)
    var listener = findListener.call(this, event, fn)
    if (listener) {
      index = listeners.indexOf(listener)
      if (index >= 0) listeners.splice(index, 1)
    }
    return this
  }

  Event.prototype.addOnceListener = Event.prototype.once = function (event, fn, once) {
    this.addListener(event, fn, true)
    return this
  }

  Event.prototype.emit = Event.prototype.fire = function (event) {
    var i, l, listener, args = slice.call(arguments).slice(1)
    var listeners = getListeners.call(this, event)
    if (event) {
      for (i = 0, l = listeners.length; i < l; i += 1) {
        listener = listeners[i]
        if (listener.once) listeners.splice(i, 1)
        listener.fn.apply(null, args)
      }
    }
    return this
  }

  Event.prototype.removeAllListeners = Event.prototype.offAll = function (event) {
    if (event && hasOwn.call(this._events, event)) {
      this._events[event].splice(0)
    }
    return this
  }

  function findListener(event, fn) {
    var i, l, listener, listeners = getListeners.call(this, event)
    for (i = 0, l = listeners.length; i < l; i += 1) {
      listener = listeners[i]
      if (listener.fn === fn) return listener
    }
  }

  function getListeners(event, fn) {
    var events = getEvents.call(this)
    return hasOwn.call(events, event) ? events[event] : (events[event] = [])
  }

  function getEvents() {
    return this._events || (this._events = {})
  }

  Event.VERSION = VERSION
  exports.Event = Event
}))

},{}],23:[function(require,module,exports){
/*! lil-http - v0.1.17 - MIT License - https://github.com/lil-js/http */
;(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    define(['exports'], factory)
  } else if (typeof exports === 'object') {
    factory(exports)
    if (typeof module === 'object' && module !== null) {
      module.exports = exports = exports.http
    }
  } else {
    factory((root.lil = root.lil || {}))
  }
}(this, function (exports) {
  'use strict'

  var VERSION = '0.1.17'
  var toStr = Object.prototype.toString
  var slicer = Array.prototype.slice
  var hasOwn = Object.prototype.hasOwnProperty
  var hasBind = typeof Function.prototype.bind === 'function'
  var origin = location.origin
  var originRegex = /^(http[s]?:\/\/[a-z0-9\-\.\:]+)[\/]?/i
  var jsonMimeRegex = /application\/json/
  var hasDomainRequest = typeof XDomainRequest !== 'undefined'
  var noop = function () {}

  var defaults = {
    method: 'GET',
    timeout: 30 * 1000,
    auth: null,
    data: null,
    headers: null,
    withCredentials: false,
    responseType: 'text'
  }

  function isObj (o) {
    return o && toStr.call(o) === '[object Object]' || false
  }

  function assign (target) {
    var i, l, x, cur, args = slicer.call(arguments).slice(1)
    for (i = 0, l = args.length; i < l; i += 1) {
      cur = args[i]
      for (x in cur) if (hasOwn.call(cur, x)) target[x] = cur[x]
    }
    return target
  }

  function once (fn) {
    var called = false
    return function () {
      if (called === false) {
        called = true
        fn.apply(null, arguments)
      }
    }
  }

  function setHeaders (xhr, headers) {
    if (!isObj(headers)) return

    // Set default content type
    headers['Content-Type'] = headers['Content-Type'] ||
      headers['content-type'] ||
      http.defaultContent

    var buf = Object.keys(headers).reduce(function (buf, field) {
      var lowerField = field.toLowerCase()

      // Remove duplicated headers
      if (lowerField !== field) {
        if (hasOwn.call(headers, lowerField)) {
          delete headers[lowerField]
          delete buf[lowerField]
        }
      }

      buf[field] = headers[field]
      return buf
    }, {})

    Object.keys(buf).forEach(function (field) {
      xhr.setRequestHeader(field, buf[field])
    })
  }

  function getHeaders (xhr) {
    var headers = {}, rawHeaders = xhr.getAllResponseHeaders().trim().split('\n')
    rawHeaders.forEach(function (header) {
      var split = header.trim().split(':')
      var key = split.shift().trim()
      var value = split.join(':').trim()
      headers[key] = value
    })
    return headers
  }

  function isJSONResponse (xhr) {
    return jsonMimeRegex.test(xhr.getResponseHeader('Content-Type'))
  }

  function encodeParams (params) {
    return Object.getOwnPropertyNames(params).filter(function (name) {
      return params[name] !== undefined
    }).map(function (name) {
      var value = (params[name] === null) ? '' : params[name]
      return encodeURIComponent(name) + (value ? '=' + encodeURIComponent(value) : '')
    }).join('&').replace(/%20/g, '+')
  }

  function parseData (xhr) {
    var data = null
    if (xhr.responseType === 'text') {
      data = xhr.responseText
      if (isJSONResponse(xhr) && data) data = JSON.parse(data)
    } else {
      data = xhr.response
    }
    return data
  }

  function getStatus (status) {
    return status === 1223 ? 204 : status // IE9 fix
  }

  function buildResponse (xhr) {
    var response = {
      xhr: xhr,
      status: getStatus(xhr.status),
      statusText: xhr.statusText,
      data: null,
      headers: {}
    }
    if (xhr.readyState === 4) {
      response.data = parseData(xhr)
      response.headers = getHeaders(xhr)
    }
    return response
  }

  function buildErrorResponse (xhr, error) {
    var response = buildResponse(xhr)
    response.error = error
    if (error.stack) response.stack = error.stack
    return response
  }

  function cleanReferences (xhr) {
    xhr.onreadystatechange = xhr.onerror = xhr.ontimeout = null
  }

  function isValidResponseStatus (xhr) {
    var status = getStatus(xhr.status)
    return status >= 200 && status < 300 || status === 304
  }

  function onError (xhr, cb) {
    return once(function (err) {
      cb(buildErrorResponse(xhr, err), null)
    })
  }

  function onLoad (config, xhr, cb) {
    return function (ev) {
      if (xhr.readyState === 4) {
        cleanReferences(xhr)
        if (isValidResponseStatus(xhr)) {
          cb(null, buildResponse(xhr))
        } else {
          onError(xhr, cb)(ev)
        }
      }
    }
  }

  function isCrossOrigin (url) {
    var match = url.match(originRegex)
    return match && match[1] === origin
  }

  function getURL (config) {
    var url = config.url
    if (isObj(config.params)) {
      url += (url.indexOf('?') === -1 ? '?' : '&') + encodeParams(config.params)
    }
    return url
  }

  function XHRFactory (url) {
    if (hasDomainRequest && isCrossOrigin(url)) {
      return new XDomainRequest()
    } else {
      return new XMLHttpRequest()
    }
  }

  function createClient (config) {
    var method = (config.method || 'GET').toUpperCase()
    var auth = config.auth
    var url = getURL(config)

    if (!url || typeof url !== 'string') {
      throw new TypeError('Missing required request URL')
    }

    var xhr = XHRFactory(url)
    if (auth) {
      xhr.open(method, url, true, auth.user, auth.password)
    } else {
      xhr.open(method, url)
    }
    xhr.withCredentials = config.withCredentials
    xhr.responseType = config.responseType
    xhr.timeout = config.timeout
    setHeaders(xhr, config.headers)
    return xhr
  }

  function updateProgress (xhr, cb) {
    return function (ev) {
      if (ev.lengthComputable) {
        cb(ev, ev.loaded / ev.total)
      } else {
        cb(ev)
      }
    }
  }

  function hasContentTypeHeader (config) {
    return config && isObj(config.headers)
    && (config.headers['content-type'] || config.headers['Content-Type'])
    || false
  }

  function buildPayload (xhr, config) {
    var data = config.data
    if (isObj(config.data) || Array.isArray(config.data)) {
      if (hasContentTypeHeader(config) === false) {
        xhr.setRequestHeader('Content-Type', 'application/json')
      }
      data = JSON.stringify(config.data)
    }
    return data
  }

  function timeoutResolver (cb, timeoutId) {
    return function () {
      clearTimeout(timeoutId)
      cb.apply(null, arguments)
    }
  }

  function request (config, cb, progress) {
    var xhr = createClient(config)
    var data = buildPayload(xhr, config)
    var errorHandler = onError(xhr, cb)

    if (hasBind) {
      xhr.ontimeout = errorHandler
    } else {
      var timeoutId = setTimeout(function abort () {
        if (xhr.readyState !== 4) {
          xhr.abort()
        }
      }, config.timeout)
      cb = timeoutResolver(cb, timeoutId)
      errorHandler = onError(xhr, cb)
    }

    xhr.onreadystatechange = onLoad(config, xhr, cb)
    xhr.onerror = errorHandler
    if (typeof progress === 'function') {
      xhr.onprogress = updateProgress(xhr, progress)
    }

    try {
      xhr.send(data || null)
    } catch (e) {
      errorHandler(e)
    }

    return { xhr: xhr, config: config }
  }

  function requestFactory (method) {
    return function (url, options, cb, progress) {
      var i, l, cur = null
      var config = assign({}, defaults, { method: method })
      var args = slicer.call(arguments)

      for (i = 0, l = args.length; i < l; i += 1) {
        cur = args[i]
        if (typeof cur === 'function') {
          if (args.length === (i + 1) && typeof args[i - 1] === 'function') {
            progress = cur
          } else {
            cb = cur
          }
        } else if (isObj(cur)) {
          assign(config, cur)
        } else if (typeof cur === 'string' && !config.url) {
          config.url = cur
        }
      }

      return request(config, cb || noop, progress)
    }
  }

  function http (config, data, cb, progress) {
    return requestFactory('GET').apply(null, arguments)
  }

  http.VERSION = VERSION
  http.defaults = defaults
  http.defaultContent = 'text/plain'
  http.get = requestFactory('GET')
  http.post = requestFactory('POST')
  http.put = requestFactory('PUT')
  http.patch = requestFactory('PATCH')
  http.head = requestFactory('HEAD')
  http.delete = http.del = requestFactory('DELETE')

  return exports.http = http
}))

},{}],24:[function(require,module,exports){
(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    define(['exports'], factory)
  } else if (typeof exports === 'object') {
    factory(exports)
    if (typeof module === 'object' && module !== null) {
      module.exports = exports = exports.midware
    }
  } else {
    factory(root)
  }
}(this, function (exports) {
  'use strict'

  function midware(ctx) {
    var calls = use.stack = []
    ctx = ctx || null
       
    function use() {
      toArray(arguments)
      .filter(function (fn) {
        return typeof fn === 'function'
      })
      .forEach(function (fn) {
        calls.push(fn)
      })
      return ctx
    }

    use.run = function run() {
      var done, args = toArray(arguments)
      
      if (typeof args[args.length - 1] === 'function') {
        done = args.pop()
      }
      
      if (!calls.length) {
        if (done) done.call(ctx)
        return
      }
      
      var stack = calls.slice()
      args.push(next)
      
      function runNext() {
        var fn = stack.shift()
        fn.apply(ctx, args)
      }

      function next(err, end) {
        if (err || end || !stack.length) {
          stack = null
          if (done) done.call(ctx, err, end)
        } else {
          runNext()
        }
      }

      runNext()
    }

    use.remove = function (name) {
      for (var i = 0, l = calls.length; i < l; i += 1) {
        var fn = calls[i]
        if (fn === name || fn.name === name) {
          calls.splice(i, 1)
          break
        }
      }
    }

    use.flush = function () {
      calls.splice(0)
    }

    return use
  }

  function toArray(nargs) {
    var args = new Array(nargs.length)
    for (var i = 0, l = args.length; i < l; i += 1) {
      args[i] = nargs[i]
    }
    return args
  }
  
  midware.VERSION = '0.1.7'
  exports.midware = midware
}))

},{}]},{},[8])(8)
});