/**
* @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.<shaka.util.Task.StageFunction>} */
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);
};