Source: polyfill/promise.js

/**
 * @license
 * Copyright 2015 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

goog.provide('shaka.polyfill.Promise');

goog.require('shaka.asserts');
goog.require('shaka.log');


/**
 * @namespace shaka.polyfill.Promise
 * @export
 *
 * @summary A polyfill to implement Promises, primarily for IE.
 * Does not support thenables, but otherwise passes the A+ conformance tests.
 * Note that Promise.all() and Promise.race() are not tested by that suite.
 */



/**
 * @constructor
 * @param {function(function(*), function(*))=} opt_callback
 */
shaka.polyfill.Promise = function(opt_callback) {
  /** @private {!Array.<shaka.polyfill.Promise.Child>} */
  this.thens_ = [];

  /** @private {!Array.<shaka.polyfill.Promise.Child>} */
  this.catches_ = [];

  /** @private {shaka.polyfill.Promise.State} */
  this.state_ = shaka.polyfill.Promise.State.PENDING;

  /** @private {*} */
  this.value_;

  // External callers must supply the callback.  Internally, we may construct
  // child Promises without it, since we can directly access their resolve_ and
  // reject_ methods when convenient.
  if (opt_callback) {
    try {
      opt_callback(this.resolve_.bind(this), this.reject_.bind(this));
    } catch (e) {
      this.reject_(e);
    }
  }
};


/**
 * @typedef {{
 *   promise: !shaka.polyfill.Promise,
 *   callback: (function(*)|undefined)
 * }}
 */
shaka.polyfill.Promise.Child;


/**
 * @enum {number}
 */
shaka.polyfill.Promise.State = {
  PENDING: 0,
  RESOLVED: 1,
  REJECTED: 2
};


/**
 * Install the polyfill if needed.
 * @export
 */
shaka.polyfill.Promise.install = function() {
  if (window.Promise) {
    shaka.log.info('Using native Promises.');
    return;
  }

  shaka.log.info('Using Promises polyfill.');
  // Quoted to work around type-checking, since our then() signature doesn't
  // exactly match that of a native Promise.
  window['Promise'] = shaka.polyfill.Promise;

  // Explicitly installed because the compiler won't necessarily attach them
  // to the compiled constructor.  Exporting them will only attach them to
  // their original namespace, which isn't the same as attaching them to the
  // constructor unless you also export the constructor.
  window.Promise.resolve = shaka.polyfill.Promise.resolve;
  window.Promise.reject = shaka.polyfill.Promise.reject;
  window.Promise.all = shaka.polyfill.Promise.all;
  window.Promise.race = shaka.polyfill.Promise.race;

  // Decide on the best way to invoke a callback as soon as possible.
  // Precompute the Promise.soon_ convenience method to avoid the overhead
  // of this switch every time a callback has to be invoked.
  if (window.setImmediate) {
    // For IE and node.js:
    shaka.polyfill.Promise.soon_ = function(callback) {
      window.setImmediate(callback);
    };
  } else {
    shaka.polyfill.Promise.soon_ = function(callback) {
      window.setTimeout(callback, 0);
    };
  }
};


/**
 * @param {*} value
 * @return {!shaka.polyfill.Promise}
 */
shaka.polyfill.Promise.resolve = function(value) {
  var p = new shaka.polyfill.Promise();
  p.resolve_(value);
  return p;
};


/**
 * @param {*} reason
 * @return {!shaka.polyfill.Promise}
 */
shaka.polyfill.Promise.reject = function(reason) {
  var p = new shaka.polyfill.Promise();
  p.reject_(reason);
  return p;
};


/**
 * @param {!Array.<!shaka.polyfill.Promise>} others
 * @return {!shaka.polyfill.Promise}
 */
shaka.polyfill.Promise.all = function(others) {
  var p = new shaka.polyfill.Promise();
  if (!others.length) {
    p.resolve_([]);
    return p;
  }

  // The array of results must be in the same order as the array of Promises
  // passed to all().  So we pre-allocate the array and keep a count of how
  // many have resolved.  Only when all have resolved is the returned Promise
  // itself resolved.
  var count = 0;
  var values = new Array(others.length);
  var resolve = function(p, i, newValue) {
    shaka.asserts.assert(p.state_ != shaka.polyfill.Promise.State.RESOLVED);
    // If one of the Promises in the array was rejected, this Promise was
    // rejected and new values are ignored.  In such a case, the values array
    // and its contents continue to be alive in memory until all of the Promises
    // in the array have completed.
    if (p.state_ == shaka.polyfill.Promise.State.PENDING) {
      values[i] = newValue;
      count++;
      if (count == values.length) {
        p.resolve_(values);
      }
    }
  };

  var reject = p.reject_.bind(p);
  for (var i = 0; i < others.length; ++i) {
    if (others[i].then) {
      others[i].then(resolve.bind(null, p, i), reject);
    } else {
      resolve(p, i, others[i]);
    }
  }
  return p;
};


/**
 * @param {!Array.<!shaka.polyfill.Promise>} others
 * @return {!shaka.polyfill.Promise}
 */
shaka.polyfill.Promise.race = function(others) {
  var p = new shaka.polyfill.Promise();

  // The returned Promise is resolved or rejected as soon as one of the others
  // is.
  var resolve = p.resolve_.bind(p);
  var reject = p.reject_.bind(p);
  for (var i = 0; i < others.length; ++i) {
    if (others[i].then) {
      others[i].then(resolve, reject);
    } else {
      resolve(others[i]);
    }
  }
  return p;
};


/**
 * @param {function(*)=} opt_successCallback
 * @param {function(*)=} opt_failCallback
 * @return {!shaka.polyfill.Promise}
 * @export
 */
shaka.polyfill.Promise.prototype.then = function(opt_successCallback,
                                                 opt_failCallback) {
  // then() returns a child Promise which is chained onto this one.
  var child = new shaka.polyfill.Promise();
  switch (this.state_) {
    case shaka.polyfill.Promise.State.RESOLVED:
      // This is already resolved, so we can chain to the child ASAP.
      this.schedule_(child, opt_successCallback);
      break;
    case shaka.polyfill.Promise.State.REJECTED:
      // This is already rejected, so we can chain to the child ASAP.
      this.schedule_(child, opt_failCallback);
      break;
    case shaka.polyfill.Promise.State.PENDING:
      // This is pending, so we have to track both callbacks and the child
      // in order to chain later.
      this.thens_.push({ promise: child, callback: opt_successCallback});
      this.catches_.push({ promise: child, callback: opt_failCallback});
      break;
  }

  return child;
};


/**
 * @param {function(*)} callback
 * @return {!shaka.polyfill.Promise}
 * @export
 */
shaka.polyfill.Promise.prototype.catch = function(callback) {
  // Devolves into a two-argument call to 'then'.
  return this.then(undefined, callback);
};


/**
 * @param {*} value
 * @private
 */
shaka.polyfill.Promise.prototype.resolve_ = function(value) {
  // Ignore resolve calls if we aren't still pending.
  if (this.state_ == shaka.polyfill.Promise.State.PENDING) {
    this.value_ = value;
    this.state_ = shaka.polyfill.Promise.State.RESOLVED;
    // Schedule calls to all of the chained callbacks.
    for (var i = 0; i < this.thens_.length; ++i) {
      this.schedule_(this.thens_[i].promise, this.thens_[i].callback);
    }
    this.thens_ = [];
    this.catches_ = [];
  }
};


/**
 * @param {*} reason
 * @private
 */
shaka.polyfill.Promise.prototype.reject_ = function(reason) {
  // Ignore reject calls if we aren't still pending.
  if (this.state_ == shaka.polyfill.Promise.State.PENDING) {
    this.value_ = reason;
    this.state_ = shaka.polyfill.Promise.State.REJECTED;
    // Schedule calls to all of the chained callbacks.
    for (var i = 0; i < this.catches_.length; ++i) {
      this.schedule_(this.catches_[i].promise, this.catches_[i].callback);
    }
    this.thens_ = [];
    this.catches_ = [];
  }
};


/**
 * @param {!shaka.polyfill.Promise} child
 * @param {function(*)|undefined} callback
 * @private
 */
shaka.polyfill.Promise.prototype.schedule_ = function(child, callback) {
  shaka.asserts.assert(this.state_ != shaka.polyfill.Promise.State.PENDING);

  var wrapper = function() {
    if (callback && typeof callback == 'function') {
      // Wrap around the callback.  Exceptions thrown by the callback are
      // converted to failures.
      try {
        var value = callback(this.value_);
      } catch (exception) {
        child.reject_(exception);
        return;
      }

      if (value instanceof shaka.polyfill.Promise) {
        // If the returned value is a Promise, we bind it's state to the child.
        if (value == child) {
          // Without this, a bad calling pattern can cause an infinite loop.
          child.reject_(new TypeError('Chaining cycle detected'));
        } else {
          value.then(child.resolve_.bind(child), child.reject_.bind(child));
        }
      } else {
        // If the returned value is not a Promise, the child is resolved with
        // that value.
        child.resolve_(value);
      }
    } else if (this.state_ == shaka.polyfill.Promise.State.RESOLVED) {
      // No callback for this state, so just chain on down the line.
      child.resolve_(this.value_);
    } else {
      // No callback for this state, so just chain on down the line.
      child.reject_(this.value_);
    }
  };

  // Call the wrapper ASAP.
  shaka.polyfill.Promise.soon_(wrapper.bind(this));
};


/**
 * @param {function()} callback
 * Schedule a callback as soon as possible.
 * Bound in shaka.polyfill.Promise.install() to a specific implementation.
 * @private
 */
shaka.polyfill.Promise.soon_ = function(callback) {};