utils.js

/** @namespace utils */
// @flow

import { typeOf } from './types'

/**
 * Deferred is modeled after jQuery's deferred object. It inverts a promise
 * such that its resolve and reject methods can be invoked without wrapping
 * all of the related code within a Promise's function.
 *
 * @memberof utils
 * @class Deferred
 */
export class Deferred {
  /**
   * Creates an object with four properties of note; promise, resolve, reject
   * and a flag complete that will be set once either resolve or reject have
   * been called. A Deferred is considered to be pending while complete is set
   * to false.
   *
   * Once constructed, resolve and reject can be called later, at which point,
   * the promise is completed. The promise property is the promise resolved
   * or rejected by the associated properties and can be used with other
   * async/await or Promise based code.
   *
   * @instance
   * @memberof Deferred
   * @method ⎆⠀constructor
   *
   * @param {any} resolveWith a deferred resolved as Promise.resolve() might do
   * @param {any} rejectWith a deferred rejected as Promise.reject() might do
   */
  constructor(resolveWith: any, rejectWith: any) {
    this.promise = new Promise((resolve, reject) => {
      this.complete = false;

      this.resolve = (...args) => {
        this.complete = true;
        return resolve(...args);
      };

      this.reject = (...args) => {
        this.complete = true;
        return reject(...args);
      };

      if (resolveWith && !rejectWith) { this.resolve(resolveWith) }
      if (rejectWith && !resolveWith) { this.reject(rejectWith) }
    });
  }

  /**
   * Shorthand getter that denotes true if the deferred is not yet complete.
   *
   * @instance
   * @memberof Deferred
   * @method ⬇︎⠀pending
   *
   * @return {boolean} true if the promise is not yet complete; false otherwise
   */
  get pending(): boolean { return !this.complete }

  /**
   * Promises are great but if the code never resolves or rejects a deferred,
   * then things will become eternal; in a bad way. This makes that less likely
   * of an event.
   *
   * If the number of milliseconds elapses before a resolve or reject occur,
   * then the deferred is rejected.
   *
   * @static
   * @memberof Deferred
   * @method ⌾⠀TimedDeferred
   *
   * @param {Number} timeOut a number of milliseconds to wait before rejecting
   * the deferred.
   * @param {Promise} proxyPromise a promise to proxy then/catch through to the
   * deferreds resolve/reject.
   * @return {Deferred} an instance of deferred that will timeout after
   * `timeOut` milliseconds have elapsed. If `proxyPromise` is a `Promise`
   * then the deferred's reject and resolve will be tied to the Promise's
   * catch() and then() methods, respectively.
   */
  static TimedDeferred(timeOut: Number, proxyPromise: ?Promise): Deferred {
    const deferred = new Deferred();

    if (proxyPromise && typeOf(proxyPromise) === Promise.name) {
      proxyPromise.then((...args) => deferred.resolve(...args))
      proxyPromise.catch(reason => deferred.reject(reason))
    }

    setTimeout(() => deferred.reject(new Error('Deferred timed out'), timeOut))

    return deferred;
  }
}

/**
 * A small helper for multiline template strings that allows you to
 * not worry about new lines and indentations within your code from
 * breaking up the format of the string. 
 *
 * @memberof utils
 * @since 2.5
 * 
 * @param {Array} strings an array of Strings from the template, broken up by
 * where the substitions are to be inserted.
 * @param {Array} values an array of values to be inserted after each string
 * of a matching index.
 * @return {String} a template String without any prefixed or postfixed tabs
 * and other whitespaced characters. 
 */
export function joinLines(strings, ...values) {
  let result = [];
  for (let i = 0; i < strings.length; i++) {
    let string = strings[i];
    let value = values.length > i && `${values[i]} ` || '' 
    result.push(string
      .replace(/(^\s*)?(.*)(\s*$)?/g, '$2')
      .replace(/\r?\n/g, ' ')
    );
    result.push(value);
  }
  return result.join('');
}