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