/**
* @license
* Copyright 2016 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('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.polyfill.register');
/**
* @summary A polyfill to implement Promises, primarily for IE.
* Only partially supports thenables, but otherwise passes the A+ conformance
* tests.
* Note that Promise.all() and Promise.race() are not tested by that suite.
*
* @constructor
* @struct
* @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)
* }}
*
* @summary A child promise, used for chaining.
* @description
* Only exists in the context of a then or catch chain.
* @property {!shaka.polyfill.Promise} promise
* The child promise.
* @property {(function(*)|undefined)} callback
* The then or catch callback to be invoked as part of this chain.
*/
shaka.polyfill.Promise.Child;
/**
* @enum {number}
*/
shaka.polyfill.Promise.State = {
PENDING: 0,
RESOLVED: 1,
REJECTED: 2
};
/**
* Install the polyfill if needed.
*/
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 setImmediate/clearImmediate convenience methods 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.setImmediate_ = function(callback) {
return window.setImmediate(callback);
};
shaka.polyfill.Promise.clearImmediate_ = function(id) {
return window.clearImmediate(id);
};
} else {
// For everyone else:
shaka.polyfill.Promise.setImmediate_ = function(callback) {
return window.setTimeout(callback, 0);
};
shaka.polyfill.Promise.clearImmediate_ = function(id) {
return window.clearTimeout(id);
};
}
};
/**
* @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) {
goog.asserts.assert(p.state_ != shaka.polyfill.Promise.State.RESOLVED,
'Invalid Promise state in Promise.all');
// 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] && 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] && 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) {
goog.asserts.assert(this.state_ != shaka.polyfill.Promise.State.PENDING,
'Invalid Promise state in Promise.schedule_');
var Promise = shaka.polyfill.Promise;
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;
}
// According to the spec, 'then' in a thenable may only be accessed once
// and any thrown exceptions in the getter must cause the Promise chain
// to fail.
var then;
try {
then = value && value.then;
} catch (exception) {
child.reject_(exception);
return;
}
if (value instanceof 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 (then) {
// If the returned value is thenable, chain it to the child.
Promise.handleThenable_(value, then, child);
} else {
// If the returned value is not a Promise, the child is resolved with
// that value.
child.resolve_(value);
}
} else if (this.state_ == 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_);
}
};
// Enqueue a call to the wrapper.
Promise.q_.push(wrapper.bind(this));
if (Promise.flushTimer_ == null) {
Promise.flushTimer_ = Promise.setImmediate_(Promise.flush_);
}
};
/**
* @param {!Object} thenable
* @param {Function} then
* @param {!shaka.polyfill.Promise} child
* @private
*/
shaka.polyfill.Promise.handleThenable_ = function(thenable, then, child) {
var Promise = shaka.polyfill.Promise;
try {
var sealed = false;
then.call(thenable, function(value) {
if (sealed) return;
sealed = true;
var nextThen;
try {
nextThen = value && value.then;
} catch (exception) {
child.reject_(exception);
return;
}
if (nextThen) {
Promise.handleThenable_(value, nextThen, child);
} else {
child.resolve_(value);
}
}, child.reject_.bind(child));
} catch (exception) {
child.reject_(exception);
}
};
/**
* Flush the queue of callbacks.
* @private
*/
shaka.polyfill.Promise.flush_ = function() {
var Promise = shaka.polyfill.Promise;
// Flush as long as we have callbacks. This means we can finish a chain more
// quickly, since we avoid the overhead of multiple calls to setTimeout, each
// of which has a minimum resolution of as much as 15ms on IE11.
// This helps to fix the out-of-order task bug on IE:
// https://github.com/google/shaka-player/issues/251#issuecomment-178146242
while (Promise.q_.length) {
// Callbacks may enqueue other callbacks, so clear the timer ID and swap the
// queue before we do anything else.
if (Promise.flushTimer_ != null) {
Promise.clearImmediate_(Promise.flushTimer_);
Promise.flushTimer_ = null;
}
var q = Promise.q_;
Promise.q_ = [];
for (var i = 0; i < q.length; ++i) {
q[i]();
}
}
};
/**
* @param {function()} callback
* @return {number}
* Schedule a callback as soon as possible.
* Bound in shaka.polyfill.Promise.install() to a specific implementation.
* @private
*/
shaka.polyfill.Promise.setImmediate_ = function(callback) { return 0; };
/**
* @param {number} id
* Clear a scheduled callback.
* Bound in shaka.polyfill.Promise.install() to a specific implementation.
* @private
*/
shaka.polyfill.Promise.clearImmediate_ = function(id) {};
/**
* A timer ID to flush the queue.
* @private {?number}
*/
shaka.polyfill.Promise.flushTimer_ = null;
/**
* A queue of callbacks to be invoked ASAP in the next frame.
* @private {!Array.<function()>}
*/
shaka.polyfill.Promise.q_ = [];
shaka.polyfill.register(shaka.polyfill.Promise.install);