/**
* An extesion to the Promise utility that provides the ability to cancel
* nested promises passing an instance of the `A.CancellablePromise.Error`.
*
* @module aui-promise
*/
/**
* Adds a callback that will be invoked whether the Promise is fulfilled or
* rejected. The callback receives no argument, and a new child Promise is
* created. This is useful for ensuring that cleanup takes place after certain
* asynchronous operations. Callbacks added with `thenAlways` will be executed
* in the same order with other calls to `then`, `thenAlways`.
*
* @method thenAways
* @param {Function} callback A function that will be invoked when the Promise
* is resolved.
* @return {Promise} A new Promise that will receive the result of the callback.
*/
A.Promise.prototype.thenAlways = function(callback) {
var instance = this;
return this.then(function() {
callback.call(instance);
}, function() {
callback.call(instance);
});
};
/**
* Adds a callback that will be invoked only if the Promise is rejected. This is
* equivalent to `then(null, onRejected)`.
*
* @method thenCatch
* @param {Function} callback A function that will be invoked with the
* rejection reason if the Promise is rejected.
* @return {Promise} A new Promise that will receive the result of the callback.
*/
A.Promise.prototype.thenCatch = function(reject) {
return this.then(null, reject);
};
/**
* Cancellable promise.
*
* @class A.CancellablePromise
* @constructor
* @extends {Promise}
* @param {Function} fn A function where to insert the logic that resolves this
* promise. Receives `fulfill` and `reject` functions as parameters. This
* function is called synchronously.
* @param {Function} opt_errorCallback Optional error callback to be fired
* whenever a cancellable promise is cancelled.
*/
function CancellablePromise(fn, opt_errorCallback) {
this._errorCallback = opt_errorCallback;
CancellablePromise.superclass.constructor.apply(this, arguments);
}
A.extend(CancellablePromise, A.Promise, {
/**
* Holds the list of children promises.
*
* @property _childPromises
* @type {Array}
* @protected
*/
_childPromises: null,
/**
* Holds the Optional error callback to be fired whenever a cancellable
* promise is cancelled.
*
* @property _errorCallback
* @type {Function}
* @protected
*/
_errorCallback: null,
/**
* Cancels the Promise by rejecting it with a `A.CancellablePromise.Error`. No
* action is performed if the Promise is already resolved.
*
* @method cancel
* @param {String} opt_message An optional debugging message for describing
* the cancellation reason.
*/
cancel: function(opt_message) {
var instance = this,
resolver = this._resolver,
error = new A.CancellablePromise.Error(opt_message);
try {
instance._cancelChildPromises(opt_message);
if (instance._errorCallback && instance.getStatus() === 'pending') {
A.soon(A.bind(instance._errorCallback, instance, error));
}
}
catch (e) {
resolver.reject(e);
}
resolver.reject(error);
return this;
},
/**
* @override
*/
then: function(onFulfilled, onRejected) {
var instance = this,
child;
function onFulfilledInternal() {
var valueOrReason = onFulfilled.apply(instance, arguments);
if (!instance._childPromises) {
instance._childPromises = [];
}
instance._childPromises.push(valueOrReason);
return valueOrReason;
}
child = CancellablePromise.superclass.then.call(this, onFulfilledInternal, function(reason) {
try {
if (A.Lang.isFunction(onRejected)) {
onRejected(reason);
}
}
catch (e) {
throw e;
}
// Always throw rejection error when comes from a cancellation
if (A.instanceOf(reason, A.CancellablePromise.Error)) {
throw reason;
}
});
// TODO: Link cancel method from parent promise keeps child promises
// available for future cancellation. The same may be achieve linking
// the parent child reference on each child
child.cancel = A.bind(instance.cancel, instance);
return child;
},
/**
* Cancels children Promises. If the Promise has not already been resolved,
* reject it with a cancel error. If there are no other children in the list
* of callback entries, propagate the cancellation by canceling this Promise
* as well.
*
* @method _cancelChildPromises
* @param {CancellablePromise} childPromise The Promise to cancel.
* @param {String} opt_message An optional debugging message for describing
* the cancellation reason.
* @private
*/
_cancelChildPromises: function(opt_message) {
A.Array.invoke(this._childPromises, 'cancel', opt_message);
this._childPromises = null;
}
});
/*
* Ensures that a certain value is a cancellable promise. If it is not a
* promise, it wraps it in one.
*
* @method resolve
* @param {Any} Any object that may or may not be a promise
* @return {CancellablePromise}
* @static
*/
CancellablePromise.resolve = A.Promise.resolve;
/*
* A shorthand for creating a rejected cancellable promise.
*
* @method reject
* @param {Any} reason Reason for the rejection of this promise. Usually an
* Error Object
* @return {CancellablePromise} A rejected promise
* @static
*/
CancellablePromise.reject = A.Promise.reject;
/*
* Returns a cancellable promise that is resolved or rejected when all values
* are resolved or any is rejected. This is useful for waiting for the
* resolution of multiple promises, such as reading multiple files in Node.js or
* making multiple XHR requests in the browser.
*
* @method all
* @param {Any[]} values An array of any kind of values, promises or not. If a
* value is not
* @return {CancellablePromise} A promise for an array of all the fulfillment values
* @static
*/
CancellablePromise.all = function(values) {
var children = [];
return new A.CancellablePromise(
function(resolve, reject) {
if (!A.Lang.isArray(values)) {
reject(new TypeError('CancellablePromise.all expects an array of values or promises'));
return;
}
var instance = this,
results = [],
remaining = values.length;
function resolveInternal(childPos, val) {
results[childPos - 1] = val;
remaining--;
if (!remaining) {
resolve(results);
}
}
A.Array.each(values, function(maybePromise) {
var child = A.CancellablePromise.resolve(maybePromise);
child.then(
A.bind(resolveInternal, instance, children.push(child)),
A.bind(instance.cancel, instance));
});
},
function() {
A.Array.invoke(children, 'cancel');
children = null;
}
);
};
A.CancellablePromise = CancellablePromise;
/**
* Error used as a rejection reason for canceled Promises.
*
* @class A.CancellablePromise.Error
* @constructor
* @extends {Error}
* @param {String} opt_message An optional debugging message for describing the
* cancellation reason.
*/
function CancellablePromiseError(opt_message) {
CancellablePromiseError.superclass.constructor.apply(this, arguments);
this.message = opt_message;
}
A.extend(CancellablePromiseError, Error, {
name: 'cancel'
});
A.CancellablePromise.Error = CancellablePromiseError;