-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This will help with porting to IE11. Chrome, Firefox, Safari, and Edge all have native Promises. This polyfill does not support thenables because Shaka does not use them. Other than tests related to thenables, this polyfill passes the A+ test suite. It is also worth noting that this polyfill is incompatible with native Promises, so it should not be used to replace a native implementation or mixed with browser APIs that may use a native implementation internally. To safely test in Chrome, force prefixed EME (to avoid native Promises), set window.Promise to null, then load some content in the test app. If using a verison of Chrome after prefixed EME was dropped, use unencrypted content. To run the A+ test suite, compile the library, install nodejs and the module 'promises-aplus-tests', then run ./test_promise_polyfill.js. Inspired by pull #176 Change-Id: I0d25049f162ff7f3b57bbc795403fcdedf927262
- Loading branch information
1 parent
5eb9310
commit c55978c
Showing
3 changed files
with
354 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,320 @@ | ||
/** | ||
* @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) { | ||
opt_callback(this.resolve_.bind(this), this.reject_.bind(this)); | ||
} | ||
}; | ||
|
||
|
||
/** @typedef {{ promise: !shaka.polyfill.Promise, callback: function(*) }} */ | ||
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.'); | ||
window['Promise'] = shaka.polyfill.Promise; | ||
|
||
// 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) { | ||
// We can't intermix with native Promises. | ||
shaka.asserts.assert(others[i] instanceof shaka.polyfill.Promise); | ||
others[i].then(resolve.bind(null, p, i), reject); | ||
} | ||
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) { | ||
// We can't intermix with native Promises. | ||
shaka.asserts.assert(others[i] instanceof shaka.polyfill.Promise); | ||
others[i].then(resolve, reject); | ||
} | ||
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) {}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
#!/usr/bin/env node | ||
// Tests Shaka's Promises polyfill using the A+ conformance tests. | ||
// Requires node.js, the 'promises-aplus-tests' module, and the compiled Shaka | ||
// Player library. | ||
|
||
// Load the compiled library. | ||
var shaka = require('./shaka-player.compiled'); | ||
shaka.polyfill.Promise.install(); | ||
|
||
// Build an adapter for the test suite. | ||
var adapter = { | ||
resolved: Promise.resolve, | ||
rejected: Promise.reject, | ||
deferred: function() { | ||
var resolveFn, rejectFn; | ||
var p = new Promise(function(resolve, reject) { | ||
resolveFn = resolve; | ||
rejectFn = reject; | ||
}); | ||
return { promise: p, resolve: resolveFn, reject: rejectFn }; | ||
} | ||
}; | ||
|
||
// Load the test suite and run conformance tests. | ||
// This implementation does not support thenables, which are not used by Shaka | ||
// Player. Tests related to thenables (2.3.3.*) are therefore ignored. | ||
var opts = { 'grep': /^2.3.3/, 'invert': true }; | ||
var promisesAplusTests = require('promises-aplus-tests'); | ||
promisesAplusTests(adapter, opts); |