Source: lib/util/abortable_operation.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.util.AbortableOperation');
  7. goog.require('shaka.util.Error');
  8. goog.require('shaka.util.PublicPromise');
  9. /**
  10. * A utility to wrap abortable operations. Note that these are not cancelable.
  11. * Cancelation implies undoing what has been done so far, whereas aborting only
  12. * means that further work is stopped.
  13. *
  14. * @implements {shaka.extern.IAbortableOperation.<T>}
  15. * @template T
  16. * @export
  17. */
  18. shaka.util.AbortableOperation = class {
  19. /**
  20. * @param {!Promise.<T>} promise
  21. * A Promise which represents the underlying operation. It is resolved when
  22. * the operation is complete, and rejected if the operation fails or is
  23. * aborted. Aborted operations should be rejected with a shaka.util.Error
  24. * object using the error code OPERATION_ABORTED.
  25. * @param {function():!Promise} onAbort
  26. * Will be called by this object to abort the underlying operation.
  27. * This is not cancelation, and will not necessarily result in any work
  28. * being undone. abort() should return a Promise which is resolved when the
  29. * underlying operation has been aborted. The returned Promise should never
  30. * be rejected.
  31. */
  32. constructor(promise, onAbort) {
  33. /** @const {!Promise.<T>} */
  34. this.promise = promise;
  35. /** @private {function():!Promise} */
  36. this.onAbort_ = onAbort;
  37. /** @private {boolean} */
  38. this.aborted_ = false;
  39. }
  40. /**
  41. * @param {!shaka.util.Error} error
  42. * @return {!shaka.util.AbortableOperation} An operation which has already
  43. * failed with the error given by the caller.
  44. * @export
  45. */
  46. static failed(error) {
  47. return new shaka.util.AbortableOperation(
  48. Promise.reject(error),
  49. () => Promise.resolve());
  50. }
  51. /**
  52. * @return {!shaka.util.AbortableOperation} An operation which has already
  53. * failed with the error OPERATION_ABORTED.
  54. * @export
  55. */
  56. static aborted() {
  57. const p = Promise.reject(shaka.util.AbortableOperation.abortError());
  58. // Silence uncaught rejection errors, which may otherwise occur any place
  59. // we don't explicitly handle aborted operations.
  60. p.catch(() => {});
  61. return new shaka.util.AbortableOperation(p, () => Promise.resolve());
  62. }
  63. /** @return {!shaka.util.Error} */
  64. static abortError() {
  65. return new shaka.util.Error(
  66. shaka.util.Error.Severity.CRITICAL,
  67. shaka.util.Error.Category.PLAYER,
  68. shaka.util.Error.Code.OPERATION_ABORTED);
  69. }
  70. /**
  71. * @param {U} value
  72. * @return {!shaka.util.AbortableOperation.<U>} An operation which has already
  73. * completed with the given value.
  74. * @template U
  75. * @export
  76. */
  77. static completed(value) {
  78. return new shaka.util.AbortableOperation(
  79. Promise.resolve(value),
  80. () => Promise.resolve());
  81. }
  82. /**
  83. * @param {!Promise.<U>} promise
  84. * @return {!shaka.util.AbortableOperation.<U>} An operation which cannot be
  85. * aborted. It will be completed when the given Promise is resolved, or
  86. * will be failed when the given Promise is rejected.
  87. * @template U
  88. * @export
  89. */
  90. static notAbortable(promise) {
  91. return new shaka.util.AbortableOperation(
  92. promise,
  93. // abort() here will return a Promise which is resolved when the input
  94. // promise either resolves or fails.
  95. () => promise.catch(() => {}));
  96. }
  97. /**
  98. * @override
  99. * @export
  100. */
  101. abort() {
  102. this.aborted_ = true;
  103. return this.onAbort_();
  104. }
  105. /**
  106. * @param {!Array.<!shaka.util.AbortableOperation>} operations
  107. * @return {!shaka.util.AbortableOperation} An operation which is resolved
  108. * when all operations are successful and fails when any operation fails.
  109. * For this operation, abort() aborts all given operations.
  110. * @export
  111. */
  112. static all(operations) {
  113. return new shaka.util.AbortableOperation(
  114. Promise.all(operations.map((op) => op.promise)),
  115. () => Promise.all(operations.map((op) => op.abort())));
  116. }
  117. /**
  118. * @override
  119. * @export
  120. */
  121. finally(onFinal) {
  122. this.promise.then((value) => onFinal(true), (e) => onFinal(false));
  123. return this;
  124. }
  125. /**
  126. * @param {(undefined|
  127. * function(T):U|
  128. * function(T):!Promise.<U>|
  129. * function(T):!shaka.util.AbortableOperation.<U>)} onSuccess
  130. * A callback to be invoked after this operation is complete, to chain to
  131. * another operation. The callback can return a plain value, a Promise to
  132. * an asynchronous value, or another AbortableOperation.
  133. * @param {function(*)=} onError
  134. * An optional callback to be invoked if this operation fails, to perform
  135. * some cleanup or error handling. Analogous to the second parameter of
  136. * Promise.prototype.then.
  137. * @return {!shaka.util.AbortableOperation.<U>} An operation which is resolved
  138. * when this operation and the operation started by the callback are both
  139. * complete.
  140. * @template U
  141. * @export
  142. */
  143. chain(onSuccess, onError) {
  144. const newPromise = new shaka.util.PublicPromise();
  145. const abortError = shaka.util.AbortableOperation.abortError();
  146. // If called before "this" completes, just abort "this".
  147. let abort = () => {
  148. newPromise.reject(abortError);
  149. return this.abort();
  150. };
  151. const makeCallback = (isSuccess) => {
  152. return (value) => {
  153. if (this.aborted_ && isSuccess) {
  154. // If "this" is not abortable(), or if abort() is called after "this"
  155. // is complete but before the next stage in the chain begins, we
  156. // should stop right away.
  157. newPromise.reject(abortError);
  158. return;
  159. }
  160. const cb = isSuccess ? onSuccess : onError;
  161. if (!cb) {
  162. // No callback? Pass it along.
  163. const next = isSuccess ? newPromise.resolve : newPromise.reject;
  164. next(value);
  165. return;
  166. }
  167. // Call the callback, interpret the return value, set the Promise state,
  168. // and get the next abort function.
  169. abort = shaka.util.AbortableOperation.wrapChainCallback_(
  170. cb, value, newPromise);
  171. };
  172. };
  173. this.promise.then(makeCallback(true), makeCallback(false));
  174. return new shaka.util.AbortableOperation(
  175. newPromise,
  176. // By creating a closure around abort(), we can update the value of
  177. // abort() at various stages.
  178. () => abort());
  179. }
  180. /**
  181. * @param {(function(T):U|
  182. * function(T):!Promise.<U>|
  183. * function(T):!shaka.util.AbortableOperation.<U>|
  184. * function(*))} callback
  185. * A callback to be invoked with the given value.
  186. * @param {T} value
  187. * @param {!shaka.util.PublicPromise} newPromise The promise for the next
  188. * stage in the chain.
  189. * @return {function():!Promise} The next abort() function for the chain.
  190. * @private
  191. * @template T, U
  192. */
  193. static wrapChainCallback_(callback, value, newPromise) {
  194. try {
  195. const ret = callback(value);
  196. if (ret && ret.promise && ret.abort) {
  197. // This is an abortable operation, with its own abort() method.
  198. // After this point, abort() should abort the operation from the
  199. // callback, and the new promise should be tied to the promise
  200. // from the callback's operation.
  201. newPromise.resolve(ret.promise);
  202. // This used to say "return ret.abort;", but it caused subtle issues by
  203. // unbinding part of the abort chain. There is now a test to ensure
  204. // that we don't call abort with the wrong "this".
  205. return () => ret.abort();
  206. } else {
  207. // This is a Promise or a plain value, and this step cannot be aborted.
  208. newPromise.resolve(ret);
  209. // Abort is complete when the returned value/Promise is resolved or
  210. // fails, but never fails itself nor returns a value.
  211. return () => Promise.resolve(ret).then(() => {}, () => {});
  212. }
  213. } catch (exception) {
  214. // The callback threw an exception or error. Reject the new Promise and
  215. // resolve any future abort call right away.
  216. newPromise.reject(exception);
  217. return () => Promise.resolve();
  218. }
  219. }
  220. };
  221. /**
  222. * @const {!Promise.<T>}
  223. * @exportInterface
  224. */
  225. // eslint-disable-next-line no-restricted-syntax
  226. shaka.util.AbortableOperation.prototype.promise;