feat($q): add streamlined ES6-style interface for using $q

This potentially helps lead the way towards a more performant fly-weight implementation, as discussed
earlier in the year. Using a constructor means we can put things in the prototype chain, and essentially
treat $q as a Promise class, and reuse methods as appropriate.

Short of that, I feel this style is slightly more convenient and streamlined, compared with the older
API.

Closes #8311
Closes #6427 (I know it's not really the solution asked for in #6427, sorry!)
This commit is contained in:
Caitlin Potter
2014-07-03 14:09:31 -04:00
parent c54228fbe9
commit f3a763fd2e
2 changed files with 676 additions and 6 deletions

View File

@@ -8,6 +8,46 @@
* @description
* A promise/deferred implementation inspired by [Kris Kowal's Q](https://github.com/kriskowal/q).
*
* $q can be used in two fashions --- One, which is more similar to Kris Kowal's Q or jQuery's Deferred
* implementations, the other resembles ES6 promises to some degree.
*
* # $q constructor
*
* The streamlined ES6 style promise is essentially just using $q as a constructor which takes a `resolver`
* function as the first argument). This is similar to the native Promise implementation from ES6 Harmony,
* see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).
*
* While the constructor-style use is supported, not all of the supporting methods from Harmony promises are
* available yet.
*
* It can be used like so:
*
* ```js
* return $q(function(resolve, reject) {
* // perform some asynchronous operation, resolve or reject the promise when appropriate.
* setInterval(function() {
* if (pollStatus > 0) {
* resolve(polledValue);
* } else if (pollStatus < 0) {
* reject(polledValue);
* } else {
* pollStatus = pollAgain(function(value) {
* polledValue = value;
* });
* }
* }, 10000);
* }).
* then(function(value) {
* // handle success
* }, function(reason) {
* // handle failure
* });
* ```
*
* Note, progress/notify callbacks are not currently supported via the ES6-style interface.
*
* However, the more traditional CommonJS style usage is still available, and documented below.
*
* [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.
@@ -54,7 +94,6 @@
* 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()`.
@@ -163,6 +202,12 @@
* expect(resolvedValue).toEqual(123);
* }));
* ```
*
* @param {function(function, function)} resolver Function which is responsible for resolving or
* rejecting the newly created promise. The first parameteter is a function which resolves the
* promise, the second parameter is a function which rejects the promise.
*
* @returns {Promise} The newly created promise.
*/
function $QProvider() {
@@ -519,10 +564,36 @@ function qFactory(nextTick, exceptionHandler) {
return deferred.promise;
}
return {
defer: defer,
reject: reject,
when: when,
all: all
var $Q = function Q(resolver) {
if (!isFunction(resolver)) {
// TODO(@caitp): minErr this
throw new TypeError('Expected resolverFn');
}
if (!(this instanceof Q)) {
// More useful when $Q is the Promise itself.
return new Q(resolver);
}
var deferred = defer();
function resolveFn(value) {
deferred.resolve(value);
}
function rejectFn(reason) {
deferred.reject(reason);
}
resolver(resolveFn, rejectFn);
return deferred.promise;
};
$Q.defer = defer;
$Q.reject = reject;
$Q.when = when;
$Q.all = all;
return $Q;
}

View File

@@ -196,6 +196,605 @@ describe('q', function() {
});
describe('$Q', function() {
var resolve, reject, resolve2, reject2;
var createPromise = function() {
return q(function(resolveFn, rejectFn) {
if (resolve === null) {
resolve = resolveFn;
reject = rejectFn;
} else if (resolve2 === null) {
resolve2 = resolveFn;
reject2 = rejectFn;
}
});
};
afterEach(function() {
resolve = reject = resolve2 = reject2 = null;
});
it('should return a Promise', function() {
var promise = q(noop);
expect(typeof promise.then).toBe('function');
expect(typeof promise.catch).toBe('function');
expect(typeof promise.finally).toBe('function');
});
describe('resolve', function() {
it('should fulfill the promise and execute all success callbacks in the registration order',
function() {
var promise = createPromise();
promise.then(success(1), error());
promise.then(success(2), error());
expect(logStr()).toBe('');
resolve('foo');
mockNextTick.flush();
expect(logStr()).toBe('success1(foo)->foo; success2(foo)->foo');
});
it('should do nothing if a promise was previously resolved', function() {
var promise = createPromise();
promise.then(success(), error());
expect(logStr()).toBe('');
resolve('foo');
mockNextTick.flush();
expect(logStr()).toBe('success(foo)->foo');
log = [];
resolve('bar');
reject('baz');
expect(mockNextTick.queue.length).toBe(0);
expect(logStr()).toBe('');
});
it('should do nothing if a promise was previously rejected', function() {
var promise = createPromise();
promise.then(success(), error());
expect(logStr()).toBe('');
reject('foo');
mockNextTick.flush();
expect(logStr()).toBe('error(foo)->reject(foo)');
log = [];
resolve('bar');
reject('baz');
expect(mockNextTick.queue.length).toBe(0);
expect(logStr()).toBe('');
});
it('should allow deferred resolution with a new promise', function() {
var promise = createPromise();
promise.then(success(), error());
resolve(createPromise());
mockNextTick.flush();
expect(logStr()).toBe('');
resolve2('foo');
mockNextTick.flush();
expect(logStr()).toBe('success(foo)->foo');
});
it('should call the callback in the next turn', function() {
var promise = createPromise();
promise.then(success());
expect(logStr()).toBe('');
resolve('foo');
expect(logStr()).toBe('');
mockNextTick.flush();
expect(logStr()).toBe('success(foo)->foo');
});
it('should not break if a callbacks registers another callback', function() {
var promise = createPromise();
promise.then(function() {
log.push('outer');
promise.then(function() {
log.push('inner');
});
});
resolve('foo');
expect(logStr()).toBe('');
mockNextTick.flush();
expect(logStr()).toBe('outer; inner');
});
it('should not break if a callbacks tries to resolve the deferred again', function() {
var promise = createPromise();
promise.then(function(val) {
log.push('then1(' + val + ')->resolve(bar)');
deferred.resolve('bar'); // nop
});
promise.then(success(2));
resolve('foo');
expect(logStr()).toBe('');
mockNextTick.flush();
expect(logStr()).toBe('then1(foo)->resolve(bar); success2(foo)->foo');
});
});
describe('reject', function() {
it('should reject the promise and execute all error callbacks in the registration order',
function() {
var promise = createPromise();
promise.then(success(), error(1));
promise.then(success(), error(2));
expect(logStr()).toBe('');
reject('foo');
mockNextTick.flush();
expect(logStr()).toBe('error1(foo)->reject(foo); error2(foo)->reject(foo)');
});
it('should do nothing if a promise was previously resolved', function() {
var promise = createPromise();
promise.then(success(1), error(1));
expect(logStr()).toBe('');
resolve('foo');
mockNextTick.flush();
expect(logStr()).toBe('success1(foo)->foo');
log = [];
reject('bar');
resolve('baz');
expect(mockNextTick.queue.length).toBe(0);
expect(logStr()).toBe('');
promise.then(success(2), error(2));
expect(logStr()).toBe('');
mockNextTick.flush();
expect(logStr()).toBe('success2(foo)->foo');
});
it('should do nothing if a promise was previously rejected', function() {
var promise = createPromise();
promise.then(success(1), error(1));
expect(logStr()).toBe('');
reject('foo');
mockNextTick.flush();
expect(logStr()).toBe('error1(foo)->reject(foo)');
log = [];
reject('bar');
resolve('baz');
expect(mockNextTick.queue.length).toBe(0);
expect(logStr()).toBe('');
promise.then(success(2), error(2));
expect(logStr()).toBe('');
mockNextTick.flush();
expect(logStr()).toBe('error2(foo)->reject(foo)');
});
it('should not defer rejection with a new promise', function() {
var promise = createPromise();
promise.then(success(), error());
reject(createPromise());
mockNextTick.flush();
expect(logStr()).toBe('error({})->reject({})');
});
it('should call the error callback in the next turn', function() {
var promise = createPromise();
promise.then(success(), error());
expect(logStr()).toBe('');
reject('foo');
expect(logStr()).toBe('');
mockNextTick.flush();
expect(logStr()).toBe('error(foo)->reject(foo)');
});
it('should support non-bound execution', function() {
var promise = createPromise();
promise.then(success(), error());
reject('detached');
mockNextTick.flush();
expect(logStr()).toBe('error(detached)->reject(detached)');
});
});
describe('promise', function() {
describe('then', function() {
it('should allow registration of a success callback without an errback or progressback ' +
'and resolve', function() {
var promise = createPromise();
promise.then(success());
resolve('foo');
mockNextTick.flush();
expect(logStr()).toBe('success(foo)->foo');
});
it('should allow registration of a success callback without an errback and reject',
function() {
var promise = createPromise();
promise.then(success());
reject('foo');
mockNextTick.flush();
expect(logStr()).toBe('');
});
it('should allow registration of an errback without a success or progress callback and ' +
' reject', function() {
var promise = createPromise();
promise.then(null, error());
reject('oops!');
mockNextTick.flush();
expect(logStr()).toBe('error(oops!)->reject(oops!)');
});
it('should allow registration of an errback without a success callback and resolve',
function() {
var promise = createPromise();
promise.then(null, error());
resolve('done');
mockNextTick.flush();
expect(logStr()).toBe('');
});
it('should allow registration of an progressback without a success callback and resolve',
function() {
var promise = createPromise();
promise.then(null, null, progress());
resolve('done');
mockNextTick.flush();
expect(logStr()).toBe('');
});
it('should allow registration of an progressback without a error callback and reject',
function() {
var promise = createPromise();
promise.then(null, null, progress());
reject('oops!');
mockNextTick.flush();
expect(logStr()).toBe('');
});
it('should resolve all callbacks with the original value', function() {
var promise = createPromise();
promise.then(success('A', 'aVal'), error(), progress());
promise.then(success('B', 'bErr', true), error(), progress());
promise.then(success('C', q.reject('cReason')), error(), progress());
promise.then(success('D', q.reject('dReason'), true), error(), progress());
promise.then(success('E', 'eVal'), error(), progress());
expect(logStr()).toBe('');
resolve('yup');
mockNextTick.flush();
expect(log).toEqual(['successA(yup)->aVal',
'successB(yup)->throw(bErr)',
'successC(yup)->{}',
'successD(yup)->throw({})',
'successE(yup)->eVal']);
});
it('should reject all callbacks with the original reason', function() {
var promise = createPromise();
promise.then(success(), error('A', 'aVal'), progress());
promise.then(success(), error('B', 'bEr', true), progress());
promise.then(success(), error('C', q.reject('cReason')), progress());
promise.then(success(), error('D', 'dVal'), progress());
expect(logStr()).toBe('');
reject('noo!');
mockNextTick.flush();
expect(logStr()).toBe('errorA(noo!)->aVal; errorB(noo!)->throw(bEr); errorC(noo!)->{}; errorD(noo!)->dVal');
});
it('should propagate resolution and rejection between dependent promises', function() {
var promise = createPromise();
promise.then(success(1, 'x'), error('1')).
then(success(2, 'y', true), error('2')).
then(success(3), error(3, 'z', true)).
then(success(4), error(4, 'done')).
then(success(5), error(5));
expect(logStr()).toBe('');
resolve('sweet!');
mockNextTick.flush();
expect(log).toEqual(['success1(sweet!)->x',
'success2(x)->throw(y)',
'error3(y)->throw(z)',
'error4(z)->done',
'success5(done)->done']);
});
it('should reject a derived promise if an exception is thrown while resolving its parent',
function() {
var promise = createPromise();
promise.then(success(1, 'oops', true), error(1)).
then(success(2), error(2));
resolve('done!');
mockNextTick.flush();
expect(logStr()).toBe('success1(done!)->throw(oops); error2(oops)->reject(oops)');
});
it('should reject a derived promise if an exception is thrown while rejecting its parent',
function() {
var promise = createPromise();
promise.then(null, error(1, 'oops', true)).
then(success(2), error(2));
reject('timeout');
mockNextTick.flush();
expect(logStr()).toBe('error1(timeout)->throw(oops); error2(oops)->reject(oops)');
});
it('should call success callback in the next turn even if promise is already resolved',
function() {
var promise = createPromise();
resolve('done!');
promise.then(success());
expect(logStr()).toBe('');
mockNextTick.flush();
expect(log).toEqual(['success(done!)->done!']);
});
it('should call error callback in the next turn even if promise is already rejected',
function() {
var promise = createPromise();
reject('oops!');
promise.then(null, error());
expect(logStr()).toBe('');
mockNextTick.flush();
expect(log).toEqual(['error(oops!)->reject(oops!)']);
});
it('should forward success resolution when success callbacks are not functions', function() {
var promise = createPromise();
resolve('yay!');
promise.then(1).
then(null).
then({}).
then('gah!').
then([]).
then(success());
expect(logStr()).toBe('');
mockNextTick.flush();
expect(log).toEqual(['success(yay!)->yay!']);
});
it('should forward error resolution when error callbacks are not functions', function() {
var promise = createPromise();
reject('oops!');
promise.then(null, 1).
then(null, null).
then(null, {}).
then(null, 'gah!').
then(null, []).
then(null, error());
expect(logStr()).toBe('');
mockNextTick.flush();
expect(log).toEqual(['error(oops!)->reject(oops!)']);
});
});
describe('finally', function() {
it('should not take an argument',
function() {
var promise = createPromise();
promise['finally'](fin(1));
resolve('foo');
mockNextTick.flush();
expect(logStr()).toBe('finally1()');
});
describe("when the promise is fulfilled", function () {
it('should call the callback',
function() {
var promise = createPromise();
promise.then(success(1))['finally'](fin(1));
resolve('foo');
mockNextTick.flush();
expect(logStr()).toBe('success1(foo)->foo; finally1()');
});
it('should fulfill with the original value',
function() {
var promise = createPromise();
promise['finally'](fin('B', 'b'), error('B')).
then(success('BB', 'bb'), error('BB'));
resolve('RESOLVED_VAL');
mockNextTick.flush();
expect(log).toEqual(['finallyB()->b',
'successBB(RESOLVED_VAL)->bb']);
});
it('should fulfill with the original value (larger test)',
function() {
var promise = createPromise();
promise.then(success('A', 'a'), error('A'));
promise['finally'](fin('B', 'b'), error('B')).
then(success('BB', 'bb'), error('BB'));
promise.then(success('C', 'c'), error('C'))['finally'](fin('CC', 'IGNORED'))
.then(success('CCC', 'cc'), error('CCC'))
.then(success('CCCC', 'ccc'), error('CCCC'));
resolve('RESOLVED_VAL');
mockNextTick.flush();
expect(log).toEqual(['successA(RESOLVED_VAL)->a',
'finallyB()->b',
'successC(RESOLVED_VAL)->c',
'successBB(RESOLVED_VAL)->bb',
'finallyCC()->IGNORED',
'successCCC(c)->cc',
'successCCCC(cc)->ccc']);
});
describe("when the callback returns a promise", function() {
describe("that is fulfilled", function() {
it("should fulfill with the original reason after that promise resolves",
function () {
var promise = createPromise();
var promise2 = createPromise();
resolve2('bar');
promise['finally'](fin(1, promise))
.then(success(2));
resolve('foo');
mockNextTick.flush();
expect(logStr()).toBe('finally1()->{}; success2(foo)->foo');
});
});
describe("that is rejected", function() {
it("should reject with this new rejection reason",
function () {
var promise = createPromise();
var promise2 = createPromise();
reject2('bar');
promise['finally'](fin(1, promise2))
.then(success(2), error(1));
resolve('foo');
mockNextTick.flush();
expect(logStr()).toBe('finally1()->{}; error1(bar)->reject(bar)');
});
});
});
describe("when the callback throws an exception", function() {
it("should reject with this new exception", function() {
var promise = createPromise();
promise['finally'](fin(1, "exception", true))
.then(success(1), error(2));
resolve('foo');
mockNextTick.flush();
expect(logStr()).toBe('finally1()->throw(exception); error2(exception)->reject(exception)');
});
});
});
describe("when the promise is rejected", function () {
it("should call the callback", function () {
var promise = createPromise();
promise['finally'](fin(1))
.then(success(2), error(1));
reject('foo');
mockNextTick.flush();
expect(logStr()).toBe('finally1(); error1(foo)->reject(foo)');
});
it('should reject with the original reason', function() {
var promise = createPromise();
promise['finally'](fin(1), "hello")
.then(success(2), error(2));
reject('original');
mockNextTick.flush();
expect(logStr()).toBe('finally1(); error2(original)->reject(original)');
});
describe("when the callback returns a promise", function() {
describe("that is fulfilled", function() {
it("should reject with the original reason after that promise resolves", function () {
var promise = createPromise();
var promise2 = createPromise();
resolve2('bar');
promise['finally'](fin(1, promise2))
.then(success(2), error(2));
reject('original');
mockNextTick.flush();
expect(logStr()).toBe('finally1()->{}; error2(original)->reject(original)');
});
});
describe("that is rejected", function () {
it("should reject with the new reason", function() {
var promise = createPromise();
var promise2 = createPromise();
reject2('bar');
promise['finally'](fin(1, promise2))
.then(success(2), error(1));
resolve('foo');
mockNextTick.flush();
expect(logStr()).toBe('finally1()->{}; error1(bar)->reject(bar)');
});
});
});
describe("when the callback throws an exception", function() {
it("should reject with this new exception", function() {
var promise = createPromise();
promise['finally'](fin(1, "exception", true))
.then(success(1), error(2));
resolve('foo');
mockNextTick.flush();
expect(logStr()).toBe('finally1()->throw(exception); error2(exception)->reject(exception)');
});
});
});
});
describe('catch', function() {
it('should be a shorthand for defining promise error handlers', function() {
var promise = createPromise();
promise['catch'](error(1)).then(null, error(2));
reject('foo');
mockNextTick.flush();
expect(logStr()).toBe('error1(foo)->reject(foo); error2(foo)->reject(foo)');
});
});
});
});
describe('defer', function() {
it('should create a new deferred', function() {
expect(deferred.promise).toBeDefined();