Source: lib/travis-status-checker.js

/**
 * @copyright Copyright 2016 Kevin Locke <kevin@kevinlocke.name>
 * @license MIT
 */

'use strict';

const Travis = require('travis-ci');

const TravisStatusHttp = require('./travis-status-http');
const constants = require('./constants');
const stateInfo = require('./state-info');
const trimSlash = require('./trim-slash');

/** Time to wait before polling status on first retry (subsequent retries use
 * truncated exponential backoff) in milliseconds.
 * @const
 * @private
 */
const POLL_TIME_START_MS = 4000;

/** Maximum amount of time to wait between each status request when polling.
 * @const
 * @private
 */
const POLL_TIME_MAX_MS = 60000;

/** Options for {@link TravisStatusChecker}.
 *
 * @typedef {{
 *   apiEndpoint: string|undefined,
 *   requestOpts: Object|undefined,
 *   token: string|undefined
 * }} TravisStatusCheckerOptions
 * @property {string=} apiEndpoint Travis API server to talk to
 * @property {Object=} requestOpts Options for Travis CI API requests (suitable
 * for the {@link https://www.npmjs.com/package/request request module}).
 * Callers are encouraged to pass the <code>agent</code> or
 * <code>forever</code> options to leverage TCP keep-alive across requests.
 * @property {string=} token access token to use for Travis CI API requests
 */
// var TravisStatusCheckerOptions;

/** Creates a status checker for Travis CI.
 *
 * @constructor
 * @param {TravisStatusCheckerOptions=} options Options.
 */
function TravisStatusChecker(options) {
  if (options && typeof options !== 'object') {
    throw new TypeError('options must be an object');
  }
  options = options || {};

  const apiEndpoint = options.apiEndpoint && trimSlash(options.apiEndpoint);
  this._travis = new Travis({
    pro: apiEndpoint === constants.PRO_URI,
    version: '2.0.0',
  });
  this._travis.agent = new TravisStatusHttp(apiEndpoint, options.requestOpts);
  if (options.token) {
    this._travis.agent.setAccessToken(options.token);
  }
}

function branchIsPending(branch) {
  return stateInfo.isPending[branch.branch.state];
}

function buildIsPending(build) {
  return stateInfo.isPending[build.build.state];
}

function repoIsPending(repo) {
  return stateInfo.isPending[repo.repo.last_build_state];
}

/** Options for {@link TravisStatusQueryOptions}.
 *
 * @typedef {{
 *   wait: number|undefined
 * }} TravisStatusQueryOptions
 * @property {number=} wait The maximum amount of time during which to retry
 * while the response is pending (in milliseconds). (default: no retry)
 */
// var TravisStatusQueryOptions;

/** Performs a travis-ci query, retrying while pending, up to a given wait
 * time.
 * @param {!{get: callback}} query <code>travis-ci</code> resource to query.
 * @param {(function(!Object): boolean)=} valueIsPending Function which
 * determines if an API value is considered to be pending for retry purposes.
 * @param {TravisStatusQueryOptions=} options Query options.
 * @return {Promise<!Object>} Promise with the response content or Error.
 * @private
 */
function queryWithWait(query, valueIsPending, options) {
  const maxWaitMs = options && options.wait ? Number(options.wait) : 0;
  if (Number.isNaN(maxWaitMs)) {
    return Promise.reject(new TypeError('wait must be a number'));
  }
  if (maxWaitMs < 0) {
    return Promise.reject(new RangeError('wait must be non-negative'));
  }

  const startMs = Date.now();
  // Note:  Divide by 2 so we can double unconditionally below
  let nextWaitMs = POLL_TIME_START_MS / 2;

  function doQuery(cb) {
    query.get(cb);
  }

  return new Promise((resolve, reject) => {
    function checkBuild(err, result) {
      if (err) {
        reject(err);
        return;
      }

      if (maxWaitMs) {
        let isPending;
        try {
          isPending = valueIsPending(result);
        } catch (errPending) {
          reject(errPending);
          return;
        }

        if (isPending) {
          const nowMs = Date.now();
          const totalWaitMs = nowMs - startMs;
          if (totalWaitMs < maxWaitMs) {
            nextWaitMs = Math.min(
              nextWaitMs * 2,
              POLL_TIME_MAX_MS,
              maxWaitMs - totalWaitMs,
            );
            setTimeout(doQuery, nextWaitMs, checkBuild);
            return;
          }
        }
      }

      resolve(result);
    }

    doQuery(checkBuild);
  });
}

/** Gets the Travis CI branch information for a given branch and repo name.
 * @param {string} repoName Travis repository name to query (e.g. owner/repo).
 * @param {string} branchName Branch name to query.
 * @param {TravisStatusQueryOptions=} options Query options.
 * @return {Promise<!Object>} Promise with the response content or Error.
 */
TravisStatusChecker.prototype.getBranch = function getBranch(repoName,
  branchName, options) {
  const queryBranch = this._travis.repos(repoName).branches(branchName);
  return queryWithWait(queryBranch, branchIsPending, options);
};

/** Gets the Travis CI build information for a given build.
 * @param {string} repoName Travis repository name to query (e.g. owner/repo).
 * @param {string} buildId ID of the build to query.
 * @param {TravisStatusQueryOptions=} options Query options.
 * @return {Promise<!Object>} Promise with the response content or Error.
 */
TravisStatusChecker.prototype.getBuild = function getBuild(repoName, buildId,
  options) {
  // Note:  travis-ci doesn't expose /repos/{repository.id}/builds/{build.id}
  // Use /builds/{build.id} instead
  const queryBuild = this._travis.builds(buildId);
  return queryWithWait(queryBuild, buildIsPending, options);
};

/** Gets the Travis CI repository information for a given name.
 * @param {string} repoName Travis repository name to query (e.g. owner/repo).
 * @param {TravisStatusQueryOptions=} options Query options.
 * @return {Promise<!Object>} Promise with the response content or Error.
 */
TravisStatusChecker.prototype.getRepo = function getRepo(repoName, options) {
  const queryRepo = this._travis.repos(repoName);
  return queryWithWait(queryRepo, repoIsPending, options);
};

/** Travis CI for open source API endpoint.
 * Same name/value as lib/travis/client.rb#ORG_URI.
 * @const
 */
TravisStatusChecker.ORG_URI = `${constants.ORG_URI}/`;

/** Travis Pro API endpoint.
 * Same name/value as lib/travis/client.rb#PRO_URI.
 * @const
 */
TravisStatusChecker.PRO_URI = `${constants.PRO_URI}/`;

module.exports = TravisStatusChecker;