Permalink
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
655 lines (580 sloc) 20.1 KB
'use strict';
/**
* @ngdoc service
* @name $q
* @requires $rootScope
*
* @description
* A promise/deferred implementation inspired by [Kris Kowal's Q](https://github.com/kriskowal/q).
*
* [The CommonJS Promise proposal](http://wiki.commonjs.org/wiki/Promises) describes a promise as an
* interface for interacting with an object that represents the result of an action that is
* performed asynchronously, and may or may not be finished at any given point in time.
*
* From the perspective of dealing with error handling, deferred and promise APIs are to
* asynchronous programming what `try`, `catch` and `throw` keywords are to synchronous programming.
*
* ```js
* // for the purpose of this example let's assume that variables `$q`, `scope` and `okToGreet`
* // are available in the current lexical scope (they could have been injected or passed in).
*
* function asyncGreet(name) {
* var deferred = $q.defer();
*
* setTimeout(function() {
* // since this fn executes async in a future turn of the event loop, we need to wrap
* // our code into an $apply call so that the model changes are properly observed.
* scope.$apply(function() {
* deferred.notify('About to greet ' + name + '.');
*
* if (okToGreet(name)) {
* deferred.resolve('Hello, ' + name + '!');
* } else {
* deferred.reject('Greeting ' + name + ' is not allowed.');
* }
* });
* }, 1000);
*
* return deferred.promise;
* }
*
* var promise = asyncGreet('Robin Hood');
* promise.then(function(greeting) {
* alert('Success: ' + greeting);
* }, function(reason) {
* alert('Failed: ' + reason);
* }, function(update) {
* alert('Got notification: ' + update);
* });
* ```
*
* At first it might not be obvious why this extra complexity is worth the trouble. The payoff
* comes in the way of guarantees that promise and deferred APIs make, see
* https://github.com/kriskowal/uncommonjs/blob/master/promises/specification.md.
*
* Additionally the promise api allows for composition that is very hard to do with the
* traditional callback ([CPS](http://en.wikipedia.org/wiki/Continuation-passing_style)) approach.
* For more on this please see the [Q documentation](https://github.com/kriskowal/q) especially the
* section on serial or parallel joining of promises.
*
*
* # The Deferred API
*
* A new instance of deferred is constructed by calling `$q.defer()`.
*
* The purpose of the deferred object is to expose the associated Promise instance as well as APIs
* that can be used for signaling the successful or unsuccessful completion, as well as the status
* of the task.
*
* **Methods**
*
* - `resolve(value)` – resolves the derived promise with the `value`. If the value is a rejection
* constructed via `$q.reject`, the promise will be rejected instead.
* - `reject(reason)` – rejects the derived promise with the `reason`. This is equivalent to
* resolving it with a rejection constructed via `$q.reject`.
* - `notify(value)` - provides updates on the status of the promise's execution. This may be called
* multiple times before the promise is either resolved or rejected.
*
* **Properties**
*
* - promise – `{Promise}` – promise object associated with this deferred.
*
*
* # The Promise API
*
* A new promise instance is created when a deferred instance is created and can be retrieved by
* calling `deferred.promise`.
*
* The purpose of the promise object is to allow for interested parties to get access to the result
* of the deferred task when it completes.
*
* **Methods**
*
* - `then(successCallback, errorCallback, notifyCallback)` – regardless of when the promise was or
* will be resolved or rejected, `then` calls one of the success or error callbacks asynchronously
* as soon as the result is available. The callbacks are called with a single argument: the result
* or rejection reason. Additionally, the notify callback may be called zero or more times to
* provide a progress indication, before the promise is resolved or rejected.
*
* This method *returns a new promise* which is resolved or rejected via the return value of the
* `successCallback`, `errorCallback`. It also notifies via the return value of the
* `notifyCallback` method. The promise can not be resolved or rejected from the notifyCallback
* method.
*
* - `catch(errorCallback)` – shorthand for `promise.then(null, errorCallback)`
*
* - `finally(callback)` – allows you to observe either the fulfillment or rejection of a promise,
* but to do so without modifying the final value. This is useful to release resources or do some
* clean-up that needs to be done whether the promise was rejected or resolved. See the [full
* specification](https://github.com/kriskowal/q/wiki/API-Reference#promisefinallycallback) for
* more information.
*
* Because `finally` is a reserved word in JavaScript and reserved keywords are not supported as
* property names by ES3, you'll need to invoke the method like `promise['finally'](callback)` to
* make your code IE8 and Android 2.x compatible.
*
* # Chaining promises
*
* Because calling the `then` method of a promise returns a new derived promise, it is easily
* possible to create a chain of promises:
*
* ```js
* promiseB = promiseA.then(function(result) {
* return result + 1;
* });
*
* // promiseB will be resolved immediately after promiseA is resolved and its value
* // will be the result of promiseA incremented by 1
* ```
*
* It is possible to create chains of any length and since a promise can be resolved with another
* promise (which will defer its resolution further), it is possible to pause/defer resolution of
* the promises at any point in the chain. This makes it possible to implement powerful APIs like
* $http's response interceptors.
*
*
* # Differences between Kris Kowal's Q and $q
*
* There are two main differences:
*
* - $q is integrated with the {@link ng.$rootScope.Scope} Scope model observation
* mechanism in angular, which means faster propagation of resolution or rejection into your
* models and avoiding unnecessary browser repaints, which would result in flickering UI.
* - Q has many more features than $q, but that comes at a cost of bytes. $q is tiny, but contains
* all the important functionality needed for common async tasks.
*
* # Testing
*
* ```js
* it('should simulate promise', inject(function($q, $rootScope) {
* var deferred = $q.defer();
* var promise = deferred.promise;
* var resolvedValue;
*
* promise.then(function(value) { resolvedValue = value; });
* expect(resolvedValue).toBeUndefined();
*
* // Simulate resolving of promise
* deferred.resolve(123);
* // Note that the 'then' function does not get called synchronously.
* // This is because we want the promise API to always be async, whether or not
* // it got called synchronously or asynchronously.
* expect(resolvedValue).toBeUndefined();
*
* // Propagate promise resolution to 'then' functions using $apply().
* $rootScope.$apply();
* expect(resolvedValue).toEqual(123);
* }));
* ```
*/
function $QProvider() {
this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) {
return qFactory(function(callback) {
$rootScope.$evalAsync(callback);
}, $exceptionHandler);
}];
}
/**
* Constructs a promise manager.
*
* @param {function(Function)} nextTick Function for executing functions in the next turn.
* @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for
* debugging purposes.
* @returns {object} Promise manager.
*/
function qFactory(nextTick, exceptionHandler) {
/**
* @ngdoc method
* @name $q#defer
* @function
*
* @description
* Creates a `Deferred` object which represents a task which will finish in the future.
*
* @returns {Deferred} Returns a new instance of deferred.
*/
var defer = function() {
var pending = [],
value, deferred;
deferred = {
resolve: function(val) {
if (pending) {
var callbacks = pending;
pending = undefined;
value = ref(val);
if (callbacks.length) {
nextTick(function() {
var callback;
for (var i = 0, ii = callbacks.length; i < ii; i++) {
callback = callbacks[i];
value.then(callback[0], callback[1], callback[2]);
}
});
}
}
},
reject: function(reason) {
deferred.resolve(createInternalRejectedPromise(reason));
},
notify: function(progress) {
if (pending) {
var callbacks = pending;
if (pending.length) {
nextTick(function() {
var callback;
for (var i = 0, ii = callbacks.length; i < ii; i++) {
callback = callbacks[i];
callback[2](progress);
}
});
}
}
},
promise: {
then: function(callback, errback, progressback) {
var result = defer();
var wrappedCallback = function(value) {
try {
result.resolve((isFunction(callback) ? callback : defaultCallback)(value));
} catch(e) {
result.reject(e);
exceptionHandler(e);
}
};
var wrappedErrback = function(reason) {
try {
result.resolve((isFunction(errback) ? errback : defaultErrback)(reason));
} catch(e) {
result.reject(e);
exceptionHandler(e);
}
};
var wrappedProgressback = function(progress) {
try {
result.notify((isFunction(progressback) ? progressback : defaultCallback)(progress));
} catch(e) {
exceptionHandler(e);
}
};
if (pending) {
pending.push([wrappedCallback, wrappedErrback, wrappedProgressback]);
} else {
value.then(wrappedCallback, wrappedErrback, wrappedProgressback);
}
return result.promise;
},
"catch": function(callback) {
return this.then(null, callback);
},
"finally": function(callback) {
function makePromise(value, resolved) {
var result = defer();
if (resolved) {
result.resolve(value);
} else {
result.reject(value);
}
return result.promise;
}
function handleCallback(value, isResolved) {
var callbackOutput = null;
try {
callbackOutput = (callback ||defaultCallback)();
} catch(e) {
return makePromise(e, false);
}
if (callbackOutput && isFunction(callbackOutput.then)) {
return callbackOutput.then(function() {
return makePromise(value, isResolved);
}, function(error) {
return makePromise(error, false);
});
} else {
return makePromise(value, isResolved);
}
}
return this.then(function(value) {
return handleCallback(value, true);
}, function(error) {
return handleCallback(error, false);
});
},
thenIf: function(condition, thenBlock, elseBlock)
{
return this.then(
function(value)
{
var test = condition && condition(value);
return !test ? next(elseBlock) :
!isFunction(test.then) ? next(thenBlock) :
test.then(
function(value) { return next(value ? thenBlock : elseBlock); });
function next(block)
{
return block && block(value);
}
});
},
thenWhile: function(condition, body)
{
var self = this;
var deferred = defer();
next();
return deferred.promise;
function next()
{
self.
thenIf(condition, block, deferred.resolve)
["catch"](deferred.reject);
}
function block(value)
{
var result = body && body(value);
result && isFunction(result.then) ?
result.then(next, deferred.reject) : next();
}
},
thenDoWhile: function(condition, body)
{
var self = this;
return self.then(body).
then(function() { return self.thenWhile(condition, body) });
},
thenForEach: function(items, body)
{
var self = this;
return self.then(
function(value)
{
var values = items(value);
return values && isFunction(values.then) ?
values.then(next) : next(values);
function next(values)
{
var i;
if (isArrayLike(values))
{
return self.thenFor(
function() { i = 0; },
function() { return i < values.length; },
function() { ++i; },
function() { return body && body(values[i], i, value); });
}
else
{
var keys = map(values, function(value, key) { return key; });
return self.thenFor(
function() { i = 0; },
function() { return i < keys.length; },
function() { ++i; },
function()
{
var key = keys[i];
return body && body(values[key], key, value);
});
}
}
});
},
thenFor: function(init, condition, increment, body)
{
var self = this;
var fn = init;
return self.thenWhile(
function(value)
{
var result = fn && fn(value);
fn = increment;
return result && isFunction(result.then) ?
result.then(next) : next();
function next()
{
return condition && condition(value);
}
},
body);
}
}
};
return deferred;
};
var ref = function(value) {
if (value && isFunction(value.then)) return value;
return {
then: function(callback) {
var result = defer();
nextTick(function() {
result.resolve(callback(value));
});
return result.promise;
}
};
};
/**
* @ngdoc method
* @name $q#reject
* @function
*
* @description
* Creates a promise that is resolved as rejected with the specified `reason`. This api should be
* used to forward rejection in a chain of promises. If you are dealing with the last promise in
* a promise chain, you don't need to worry about it.
*
* When comparing deferreds/promises to the familiar behavior of try/catch/throw, think of
* `reject` as the `throw` keyword in JavaScript. This also means that if you "catch" an error via
* a promise error callback and you want to forward the error to the promise derived from the
* current promise, you have to "rethrow" the error by returning a rejection constructed via
* `reject`.
*
* ```js
* promiseB = promiseA.then(function(result) {
* // success: do something and resolve promiseB
* // with the old or a new result
* return result;
* }, function(reason) {
* // error: handle the error if possible and
* // resolve promiseB with newPromiseOrValue,
* // otherwise forward the rejection to promiseB
* if (canHandle(reason)) {
* // handle the error and recover
* return newPromiseOrValue;
* }
* return $q.reject(reason);
* });
* ```
*
* @param {*} reason Constant, message, exception or an object representing the rejection reason.
* @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`.
*/
var reject = function(reason) {
var result = defer();
result.reject(reason);
return result.promise;
};
var createInternalRejectedPromise = function(reason) {
return {
then: function(callback, errback) {
var result = defer();
nextTick(function() {
try {
result.resolve((isFunction(errback) ? errback : defaultErrback)(reason));
} catch(e) {
result.reject(e);
exceptionHandler(e);
}
});
return result.promise;
}
};
};
/**
* @ngdoc method
* @name $q#when
* @function
*
* @description
* Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise.
* This is useful when you are dealing with an object that might or might not be a promise, or if
* the promise comes from a source that can't be trusted.
*
* @param {*} value Value or a promise
* @returns {Promise} Returns a promise of the passed value or promise
*/
var when = function(value, callback, errback, progressback) {
var result = defer(),
done;
var wrappedCallback = function(value) {
try {
return (isFunction(callback) ? callback : defaultCallback)(value);
} catch (e) {
exceptionHandler(e);
return reject(e);
}
};
var wrappedErrback = function(reason) {
try {
return (isFunction(errback) ? errback : defaultErrback)(reason);
} catch (e) {
exceptionHandler(e);
return reject(e);
}
};
var wrappedProgressback = function(progress) {
try {
return (isFunction(progressback) ? progressback : defaultCallback)(progress);
} catch (e) {
exceptionHandler(e);
}
};
nextTick(function() {
ref(value).then(function(value) {
if (done) return;
done = true;
result.resolve(ref(value).then(wrappedCallback, wrappedErrback, wrappedProgressback));
}, function(reason) {
if (done) return;
done = true;
result.resolve(wrappedErrback(reason));
}, function(progress) {
if (done) return;
result.notify(wrappedProgressback(progress));
});
});
return result.promise;
};
function defaultCallback(value) {
return value;
}
function defaultErrback(reason) {
return reject(reason);
}
/**
* @ngdoc method
* @name $q#all
* @function
*
* @description
* Combines multiple promises into a single promise that is resolved when all of the input
* promises are resolved.
*
* @param {Array.<Promise>|Object.<Promise>} promises An array or hash of promises.
* @returns {Promise} Returns a single promise that will be resolved with an array/hash of values,
* each value corresponding to the promise at the same index/key in the `promises` array/hash.
* If any of the promises is resolved with a rejection, this resulting promise will be rejected
* with the same rejection value.
*/
function all(promises) {
var deferred = defer(),
counter = 0,
results = isArray(promises) ? [] : {};
forEach(promises, function(promise, key) {
counter++;
ref(promise).then(function(value) {
if (results.hasOwnProperty(key)) return;
results[key] = value;
if (!(--counter)) deferred.resolve(results);
}, function(reason) {
if (results.hasOwnProperty(key)) return;
deferred.reject(reason);
});
});
if (counter === 0) {
deferred.resolve(results);
}
return deferred.promise;
}
function run(callback)
{
var deferred = defer();
var result = callback ? callback(deferred.promise) : nextTick();
deferred.resolve();
return result && isFunction(result.then) ? result : deferred.promise;
}
return {
defer: defer,
reject: reject,
when: when,
all: all,
run: run
};
}