/** * @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.util.Task'); goog.require('shaka.asserts'); goog.require('shaka.util.PublicPromise'); /** * A utility to create abortable, multi-stage tasks based on Promises. * @constructor */ shaka.util.Task = function() { /** @private {!shaka.util.PublicPromise} */ this.taskPromise_ = new shaka.util.PublicPromise(); /** @private {boolean} */ this.started_ = false; /** @private {shaka.util.PublicPromise} */ this.abortedPromise_ = null; /** @private {!Array.} */ this.stages_ = []; /** @private {?function()} */ this.aborter_ = null; }; /** @typedef {function(?):(Array|undefined)} */ shaka.util.Task.StageFunction; /** * Adds a new stage to the task. Should only be used before starting the task. * * A stage function should return either nothing or an Array with two items in * it. * * If the stage function returns nothing, this stage is always successful and * completes right away. No data will be passed to the next stage. * * If the stage function returns an Array, the first item should be a Promise * which is resolved or rejected when the stage completes. If this promise is * rejected, the task has failed and the task's 'catch' functions are called. * * The second item in the Array should be a function which aborts this stage * of the operation. If this is omitted, then the stage cannot be terminated * early, and aborting the Task during this stage means waiting for the end of * the stage. * * @param {shaka.util.Task.StageFunction} fn The next stage of the task. * @throws {Error} if the task has been started. */ shaka.util.Task.prototype.append = function(fn) { if (this.started_) { throw new Error('Cannot append to a running task!'); } this.stages_.push(fn); }; /** * Starts the task. * @throws {Error} if the task has already been started. */ shaka.util.Task.prototype.start = function() { if (this.started_) { throw new Error('Task already started!'); } this.started_ = true; // The first 'stage' is an empty function, which ensures two things: // 1. All real stages execute asynchronously. // 2. It is always safe to call startNextStage_ at least once. this.stages_.unshift(function() {}); this.startNextStage_(undefined); }; /** * Abort the task and run all 'catch' handlers. * The caught error will have type 'aborted'. * @return {!Promise} resolved once the task is aborted. */ shaka.util.Task.prototype.abort = function() { if (this.abortedPromise_) { return this.abortedPromise_; } if (!this.started_) { this.started_ = true; return Promise.resolve(); } if (this.aborter_) { this.aborter_(); } this.abortedPromise_ = new shaka.util.PublicPromise(); return this.abortedPromise_; }; /** * End the running task. No more stages will be executed, and this will not * be considered an error. Should always be called from within a stage. */ shaka.util.Task.prototype.end = function() { // Forget all stages after this one. this.stages_.splice(1); }; /** * Get a promise which represents the entire task. * @return {!Promise} */ shaka.util.Task.prototype.getPromise = function() { return this.taskPromise_; }; /** * Start the next stage of the task. * @param {?} arg passed to the next stage. * @private */ shaka.util.Task.prototype.startNextStage_ = function(arg) { var retval = this.stages_[0](arg); var done; if (retval) { shaka.asserts.assert(retval.length == 1 || retval.length == 2); done = retval[0]; shaka.asserts.assert(done); this.aborter_ = retval[1]; } else { done = Promise.resolve(); this.aborter_ = null; } done.then(shaka.util.TypedBind(this, /** @param {?} arg */ function(arg) { if (this.abortedPromise_) { // Aborted in between stages or in a way that didn't fail the stage. // Clean up. this.stages_ = []; this.aborter_ = null; this.completeAbort_(); return; } // Throw away the stage we just completed. this.stages_.shift(); if (this.stages_.length) { // Start the next stage. this.startNextStage_(arg); } else { // All done. Clean up. this.taskPromise_.resolve(arg); this.aborter_ = null; } }) ).catch(shaka.util.TypedBind(this, /** @param {*} error */ function(error) { // Task failed. Clean up. this.stages_ = []; this.aborter_ = null; if (this.abortedPromise_) { // Aborted during a stage in a way that failed the stage. // Resolve the aborted promise. this.completeAbort_(); } else { this.taskPromise_.reject(error); } }) ); }; /** * Rejects the task promise and then resolves the abort promise. * * @private */ shaka.util.Task.prototype.completeAbort_ = function() { shaka.asserts.assert(this.taskPromise_); shaka.asserts.assert(this.abortedPromise_); var error = new Error('Task aborted.'); error.type = 'aborted'; this.taskPromise_.reject(error); // Ensure the abort promise is resolved after the task promise is rejected. // This allows callers to make some simplifying assumptions on the ordering // of async callbacks. window.setTimeout( function() { this.abortedPromise_.resolve(); this.abortedPromise_ = null; }.bind(this), 5); };