Skip to content

Commit

Permalink
Add a Promise polyfill
Browse files Browse the repository at this point in the history
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
joeyparrish committed Oct 21, 2015
1 parent 5eb9310 commit c55978c
Show file tree
Hide file tree
Showing 3 changed files with 354 additions and 1 deletion.
6 changes: 5 additions & 1 deletion lib/polyfill/all.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ goog.provide('shaka.polyfill.installAll');
goog.require('shaka.polyfill.CustomEvent');
goog.require('shaka.polyfill.Fullscreen');
goog.require('shaka.polyfill.MediaKeys');
goog.require('shaka.polyfill.Promise');
goog.require('shaka.polyfill.VideoPlaybackQuality');


Expand All @@ -38,7 +39,10 @@ goog.require('shaka.polyfill.VideoPlaybackQuality');
shaka.polyfill.installAll = function() {
shaka.polyfill.CustomEvent.install();
shaka.polyfill.Fullscreen.install();
shaka.polyfill.MediaKeys.install();
shaka.polyfill.VideoPlaybackQuality.install();

// Promise must come before other polyfills that use Promises.
shaka.polyfill.Promise.install();
shaka.polyfill.MediaKeys.install();
};

320 changes: 320 additions & 0 deletions lib/polyfill/promise.js
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) {};
29 changes: 29 additions & 0 deletions test_promise_polyfill.js
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);

0 comments on commit c55978c

Please sign in to comment.