Source: lib/polyfill/promise.js

  1. /**
  2. * @license
  3. * Copyright 2016 Google Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. goog.provide('shaka.polyfill.Promise');
  18. goog.require('goog.asserts');
  19. goog.require('shaka.log');
  20. goog.require('shaka.polyfill.register');
  21. /**
  22. * @summary A polyfill to implement Promises, primarily for IE.
  23. * Only partially supports thenables, but otherwise passes the A+ conformance
  24. * tests.
  25. * Note that Promise.all() and Promise.race() are not tested by that suite.
  26. *
  27. * @constructor
  28. * @struct
  29. * @param {function(function(*=), function(*=))=} opt_callback
  30. * @template T
  31. */
  32. shaka.polyfill.Promise = function(opt_callback) {
  33. /** @private {!Array.<shaka.polyfill.Promise.Child>} */
  34. this.thens_ = [];
  35. /** @private {!Array.<shaka.polyfill.Promise.Child>} */
  36. this.catches_ = [];
  37. /** @private {shaka.polyfill.Promise.State} */
  38. this.state_ = shaka.polyfill.Promise.State.PENDING;
  39. /** @private {*} */
  40. this.value_;
  41. // External callers must supply the callback. Internally, we may construct
  42. // child Promises without it, since we can directly access their resolve_ and
  43. // reject_ methods when convenient.
  44. if (opt_callback) {
  45. try {
  46. opt_callback(this.resolve_.bind(this), this.reject_.bind(this));
  47. } catch (e) {
  48. this.reject_(e);
  49. }
  50. }
  51. };
  52. /**
  53. * @typedef {{
  54. * promise: !shaka.polyfill.Promise,
  55. * callback: (function(*)|undefined)
  56. * }}
  57. *
  58. * @summary A child promise, used for chaining.
  59. * @description
  60. * Only exists in the context of a then or catch chain.
  61. * @property {!shaka.polyfill.Promise} promise
  62. * The child promise.
  63. * @property {(function(*)|undefined)} callback
  64. * The then or catch callback to be invoked as part of this chain.
  65. */
  66. shaka.polyfill.Promise.Child;
  67. /**
  68. * @enum {number}
  69. */
  70. shaka.polyfill.Promise.State = {
  71. PENDING: 0,
  72. RESOLVED: 1,
  73. REJECTED: 2
  74. };
  75. /**
  76. * Install the polyfill if needed.
  77. * @param {boolean=} opt_force If true, force the polyfill to be installed.
  78. * Used in some unit tests.
  79. */
  80. shaka.polyfill.Promise.install = function(opt_force) {
  81. // Decide on the best way to invoke a callback as soon as possible.
  82. // Precompute the setImmediate/clearImmediate convenience methods to avoid the
  83. // overhead of this switch every time a callback has to be invoked.
  84. if (window.setImmediate) {
  85. // For IE and node.js:
  86. shaka.polyfill.Promise.setImmediate_ = function(callback) {
  87. return window.setImmediate(callback);
  88. };
  89. shaka.polyfill.Promise.clearImmediate_ = function(id) {
  90. return window.clearImmediate(id);
  91. };
  92. } else {
  93. // For everyone else:
  94. shaka.polyfill.Promise.setImmediate_ = function(callback) {
  95. return window.setTimeout(callback, 0);
  96. };
  97. shaka.polyfill.Promise.clearImmediate_ = function(id) {
  98. return window.clearTimeout(id);
  99. };
  100. }
  101. if (window.Promise && !opt_force) {
  102. shaka.log.info('Using native Promises.');
  103. return;
  104. }
  105. shaka.log.info('Using Promises polyfill.');
  106. // Quoted to work around type-checking, since our then() signature doesn't
  107. // exactly match that of a native Promise.
  108. window['Promise'] = shaka.polyfill.Promise;
  109. // Explicitly installed because the compiler won't necessarily attach them
  110. // to the compiled constructor. Exporting them will only attach them to
  111. // their original namespace, which isn't the same as attaching them to the
  112. // constructor unless you also export the constructor.
  113. window['Promise'].resolve = shaka.polyfill.Promise.resolve;
  114. window['Promise'].reject = shaka.polyfill.Promise.reject;
  115. window['Promise'].all = shaka.polyfill.Promise.all;
  116. window['Promise'].race = shaka.polyfill.Promise.race;
  117. // These are manually exported as well, because allowing the compiler to
  118. // export them for us will cause the polyfill to end up in our generated
  119. // externs. Since nobody should be accessing this directly using the
  120. // shaka.polyfill namespace, it is okay not to @export these methods.
  121. window['Promise']['prototype']['then'] =
  122. shaka.polyfill.Promise.prototype.then;
  123. window['Promise']['prototype']['catch'] =
  124. shaka.polyfill.Promise.prototype.catch;
  125. };
  126. /**
  127. * Uninstall the polyfill. Used in some unit tests.
  128. */
  129. shaka.polyfill.Promise.uninstall = function() {
  130. // Do nothing if there is no native implementation.
  131. if (shaka.polyfill.Promise.nativePromise_) {
  132. shaka.log.info('Removing Promise polyfill.');
  133. window['Promise'] = shaka.polyfill.Promise.nativePromise_;
  134. shaka.polyfill.Promise.q_ = [];
  135. }
  136. };
  137. /**
  138. * @param {*=} opt_value
  139. * @return {!shaka.polyfill.Promise}
  140. */
  141. shaka.polyfill.Promise.resolve = function(opt_value) {
  142. var p = new shaka.polyfill.Promise();
  143. p.resolve_(undefined);
  144. return p.then(function() {
  145. return opt_value;
  146. });
  147. };
  148. /**
  149. * @param {*=} opt_reason
  150. * @return {!shaka.polyfill.Promise}
  151. */
  152. shaka.polyfill.Promise.reject = function(opt_reason) {
  153. var p = new shaka.polyfill.Promise();
  154. p.reject_(opt_reason);
  155. return p;
  156. };
  157. /**
  158. * @param {!Array.<!shaka.polyfill.Promise>} others
  159. * @return {!shaka.polyfill.Promise}
  160. */
  161. shaka.polyfill.Promise.all = function(others) {
  162. var p = new shaka.polyfill.Promise();
  163. if (!others.length) {
  164. p.resolve_([]);
  165. return p;
  166. }
  167. // The array of results must be in the same order as the array of Promises
  168. // passed to all(). So we pre-allocate the array and keep a count of how
  169. // many have resolved. Only when all have resolved is the returned Promise
  170. // itself resolved.
  171. var count = 0;
  172. var values = new Array(others.length);
  173. var resolve = function(p, i, newValue) {
  174. goog.asserts.assert(p.state_ != shaka.polyfill.Promise.State.RESOLVED,
  175. 'Invalid Promise state in Promise.all');
  176. // If one of the Promises in the array was rejected, this Promise was
  177. // rejected and new values are ignored. In such a case, the values array
  178. // and its contents continue to be alive in memory until all of the Promises
  179. // in the array have completed.
  180. if (p.state_ == shaka.polyfill.Promise.State.PENDING) {
  181. values[i] = newValue;
  182. count++;
  183. if (count == values.length) {
  184. p.resolve_(values);
  185. }
  186. }
  187. };
  188. var reject = p.reject_.bind(p);
  189. for (var i = 0; i < others.length; ++i) {
  190. if (others[i] && others[i].then) {
  191. others[i].then(resolve.bind(null, p, i), reject);
  192. } else {
  193. resolve(p, i, others[i]);
  194. }
  195. }
  196. return p;
  197. };
  198. /**
  199. * @param {!Array.<!shaka.polyfill.Promise>} others
  200. * @return {!shaka.polyfill.Promise}
  201. */
  202. shaka.polyfill.Promise.race = function(others) {
  203. var p = new shaka.polyfill.Promise();
  204. // The returned Promise is resolved or rejected as soon as one of the others
  205. // is.
  206. var resolve = p.resolve_.bind(p);
  207. var reject = p.reject_.bind(p);
  208. for (var i = 0; i < others.length; ++i) {
  209. if (others[i] && others[i].then) {
  210. others[i].then(resolve, reject);
  211. } else {
  212. resolve(others[i]);
  213. }
  214. }
  215. return p;
  216. };
  217. /**
  218. * @param {function(*)=} opt_successCallback
  219. * @param {function(*)=} opt_failCallback
  220. * @return {!shaka.polyfill.Promise}
  221. */
  222. shaka.polyfill.Promise.prototype.then = function(opt_successCallback,
  223. opt_failCallback) {
  224. // then() returns a child Promise which is chained onto this one.
  225. var child = new shaka.polyfill.Promise();
  226. switch (this.state_) {
  227. case shaka.polyfill.Promise.State.RESOLVED:
  228. // This is already resolved, so we can chain to the child ASAP.
  229. this.schedule_(child, opt_successCallback);
  230. break;
  231. case shaka.polyfill.Promise.State.REJECTED:
  232. // This is already rejected, so we can chain to the child ASAP.
  233. this.schedule_(child, opt_failCallback);
  234. break;
  235. case shaka.polyfill.Promise.State.PENDING:
  236. // This is pending, so we have to track both callbacks and the child
  237. // in order to chain later.
  238. this.thens_.push({ promise: child, callback: opt_successCallback});
  239. this.catches_.push({ promise: child, callback: opt_failCallback});
  240. break;
  241. }
  242. return child;
  243. };
  244. /**
  245. * @param {function(*)=} opt_callback
  246. * @return {!shaka.polyfill.Promise}
  247. */
  248. shaka.polyfill.Promise.prototype.catch = function(opt_callback) {
  249. // Devolves into a two-argument call to 'then'.
  250. return this.then(undefined, opt_callback);
  251. };
  252. /**
  253. * @param {*=} opt_value
  254. * @private
  255. */
  256. shaka.polyfill.Promise.prototype.resolve_ = function(opt_value) {
  257. // Ignore resolve calls if we aren't still pending.
  258. if (this.state_ == shaka.polyfill.Promise.State.PENDING) {
  259. this.value_ = opt_value;
  260. this.state_ = shaka.polyfill.Promise.State.RESOLVED;
  261. // Schedule calls to all of the chained callbacks.
  262. for (var i = 0; i < this.thens_.length; ++i) {
  263. this.schedule_(this.thens_[i].promise, this.thens_[i].callback);
  264. }
  265. this.thens_ = [];
  266. this.catches_ = [];
  267. }
  268. };
  269. /**
  270. * @param {*=} opt_reason
  271. * @private
  272. */
  273. shaka.polyfill.Promise.prototype.reject_ = function(opt_reason) {
  274. // Ignore reject calls if we aren't still pending.
  275. if (this.state_ == shaka.polyfill.Promise.State.PENDING) {
  276. this.value_ = opt_reason;
  277. this.state_ = shaka.polyfill.Promise.State.REJECTED;
  278. // Schedule calls to all of the chained callbacks.
  279. for (var i = 0; i < this.catches_.length; ++i) {
  280. this.schedule_(this.catches_[i].promise, this.catches_[i].callback);
  281. }
  282. this.thens_ = [];
  283. this.catches_ = [];
  284. }
  285. };
  286. /**
  287. * @param {!shaka.polyfill.Promise} child
  288. * @param {function(*)|undefined} callback
  289. * @private
  290. */
  291. shaka.polyfill.Promise.prototype.schedule_ = function(child, callback) {
  292. goog.asserts.assert(this.state_ != shaka.polyfill.Promise.State.PENDING,
  293. 'Invalid Promise state in Promise.schedule_');
  294. var Promise = shaka.polyfill.Promise;
  295. var wrapper = function() {
  296. if (callback && typeof callback == 'function') {
  297. // Wrap around the callback. Exceptions thrown by the callback are
  298. // converted to failures.
  299. try {
  300. var value = callback(this.value_);
  301. } catch (exception) {
  302. child.reject_(exception);
  303. return;
  304. }
  305. // According to the spec, 'then' in a thenable may only be accessed once
  306. // and any thrown exceptions in the getter must cause the Promise chain
  307. // to fail.
  308. var then;
  309. try {
  310. then = value && value.then;
  311. } catch (exception) {
  312. child.reject_(exception);
  313. return;
  314. }
  315. if (value instanceof Promise) {
  316. // If the returned value is a Promise, we bind it's state to the child.
  317. if (value == child) {
  318. // Without this, a bad calling pattern can cause an infinite loop.
  319. child.reject_(new TypeError('Chaining cycle detected'));
  320. } else {
  321. value.then(child.resolve_.bind(child), child.reject_.bind(child));
  322. }
  323. } else if (then) {
  324. // If the returned value is thenable, chain it to the child.
  325. Promise.handleThenable_(value, then, child);
  326. } else {
  327. // If the returned value is not a Promise, the child is resolved with
  328. // that value.
  329. child.resolve_(value);
  330. }
  331. } else if (this.state_ == Promise.State.RESOLVED) {
  332. // No callback for this state, so just chain on down the line.
  333. child.resolve_(this.value_);
  334. } else {
  335. // No callback for this state, so just chain on down the line.
  336. child.reject_(this.value_);
  337. }
  338. };
  339. // Enqueue a call to the wrapper.
  340. Promise.q_.push(wrapper.bind(this));
  341. if (Promise.flushTimer_ == null) {
  342. Promise.flushTimer_ = Promise.setImmediate_(Promise.flush);
  343. }
  344. };
  345. /**
  346. * @param {!Object} thenable
  347. * @param {Function} then
  348. * @param {!shaka.polyfill.Promise} child
  349. * @private
  350. */
  351. shaka.polyfill.Promise.handleThenable_ = function(thenable, then, child) {
  352. var Promise = shaka.polyfill.Promise;
  353. try {
  354. var sealed = false;
  355. then.call(thenable, function(value) {
  356. if (sealed) return;
  357. sealed = true;
  358. var nextThen;
  359. try {
  360. nextThen = value && value.then;
  361. } catch (exception) {
  362. child.reject_(exception);
  363. return;
  364. }
  365. if (nextThen) {
  366. Promise.handleThenable_(value, nextThen, child);
  367. } else {
  368. child.resolve_(value);
  369. }
  370. }, child.reject_.bind(child));
  371. } catch (exception) {
  372. child.reject_(exception);
  373. }
  374. };
  375. /**
  376. * Flush the queue of callbacks.
  377. * Used directly by some unit tests.
  378. */
  379. shaka.polyfill.Promise.flush = function() {
  380. var Promise = shaka.polyfill.Promise;
  381. // Flush as long as we have callbacks. This means we can finish a chain more
  382. // quickly, since we avoid the overhead of multiple calls to setTimeout, each
  383. // of which has a minimum resolution of as much as 15ms on IE11.
  384. // This helps to fix the out-of-order task bug on IE:
  385. // https://github.com/google/shaka-player/issues/251#issuecomment-178146242
  386. while (Promise.q_.length) {
  387. // Callbacks may enqueue other callbacks, so clear the timer ID and swap the
  388. // queue before we do anything else.
  389. if (Promise.flushTimer_ != null) {
  390. Promise.clearImmediate_(Promise.flushTimer_);
  391. Promise.flushTimer_ = null;
  392. }
  393. var q = Promise.q_;
  394. Promise.q_ = [];
  395. for (var i = 0; i < q.length; ++i) {
  396. q[i]();
  397. }
  398. }
  399. };
  400. /**
  401. * @param {function()} callback
  402. * @return {number}
  403. * Schedule a callback as soon as possible.
  404. * Bound in shaka.polyfill.Promise.install() to a specific implementation.
  405. * @private
  406. */
  407. shaka.polyfill.Promise.setImmediate_ = function(callback) { return 0; };
  408. /**
  409. * @param {number} id
  410. * Clear a scheduled callback.
  411. * Bound in shaka.polyfill.Promise.install() to a specific implementation.
  412. * @private
  413. */
  414. shaka.polyfill.Promise.clearImmediate_ = function(id) {};
  415. /**
  416. * A timer ID to flush the queue.
  417. * @private {?number}
  418. */
  419. shaka.polyfill.Promise.flushTimer_ = null;
  420. /**
  421. * A queue of callbacks to be invoked ASAP in the next frame.
  422. * @private {!Array.<function()>}
  423. */
  424. shaka.polyfill.Promise.q_ = [];
  425. /** @private {?} */
  426. shaka.polyfill.Promise.nativePromise_ = window.Promise;
  427. shaka.polyfill.register(shaka.polyfill.Promise.install);