aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCaio Cunha2013-03-24 22:18:10 -0300
committerChirayu Krishnappa2013-07-14 23:11:46 -0700
commit2a5c3555829da51f55abd810a828c73b420316d3 (patch)
tree5e5dc32e42284c63aba7486259b48a69f205e97b
parentd884eb80a10a336f6765d5fe20554933eda23545 (diff)
downloadangular.js-2a5c3555829da51f55abd810a828c73b420316d3.tar.bz2
feat($q): added support to promise notification
It is now possible to notify a promise through deferred.notify() method. Notifications are useful to provide a way to send progress information to promise holders.
-rw-r--r--src/ng/q.js48
-rw-r--r--test/ng/qSpec.js317
2 files changed, 345 insertions, 20 deletions
diff --git a/src/ng/q.js b/src/ng/q.js
index 994ecd8d..fe05b37f 100644
--- a/src/ng/q.js
+++ b/src/ng/q.js
@@ -199,7 +199,7 @@ function qFactory(nextTick, exceptionHandler) {
var callback;
for (var i = 0, ii = callbacks.length; i < ii; i++) {
callback = callbacks[i];
- value.then(callback[0], callback[1]);
+ value.then(callback[0], callback[1], callback[2]);
}
});
}
@@ -212,8 +212,25 @@ function qFactory(nextTick, exceptionHandler) {
},
+ 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) {
+ then: function(callback, errback, progressback) {
var result = defer();
var wrappedCallback = function(value) {
@@ -234,10 +251,18 @@ function qFactory(nextTick, exceptionHandler) {
}
};
+ var wrappedProgressback = function(progress) {
+ try {
+ result.notify((progressback || defaultCallback)(progress));
+ } catch(e) {
+ exceptionHandler(e);
+ }
+ };
+
if (pending) {
- pending.push([wrappedCallback, wrappedErrback]);
+ pending.push([wrappedCallback, wrappedErrback, wrappedProgressback]);
} else {
- value.then(wrappedCallback, wrappedErrback);
+ value.then(wrappedCallback, wrappedErrback, wrappedProgressback);
}
return result.promise;
@@ -359,7 +384,7 @@ function qFactory(nextTick, exceptionHandler) {
* @param {*} value Value or a promise
* @returns {Promise} Returns a promise of the passed value or promise
*/
- var when = function(value, callback, errback) {
+ var when = function(value, callback, errback, progressback) {
var result = defer(),
done;
@@ -381,15 +406,26 @@ function qFactory(nextTick, exceptionHandler) {
}
};
+ var wrappedProgressback = function(progress) {
+ try {
+ return (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));
+ 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));
});
});
diff --git a/test/ng/qSpec.js b/test/ng/qSpec.js
index 8fc5c50a..316f8add 100644
--- a/test/ng/qSpec.js
+++ b/test/ng/qSpec.js
@@ -43,7 +43,7 @@ describe('q', function() {
return map(sliceArgs(args), _argToString).join(',');
}
- // Help log invocation of success(), always() and error()
+ // Help log invocation of success(), always(), progress() and error()
function _logInvocation(funcName, args, returnVal, throwReturnVal) {
var logPrefix = funcName + '(' + _argumentsToString(args) + ')';
if (throwReturnVal) {
@@ -94,6 +94,22 @@ describe('q', function() {
/**
* Creates a callback that logs its invocation in `log`.
*
+ * @param {(number|string)} name Suffix for 'progress' name. e.g. progress(1) => progress
+ * @param {*=} returnVal Value that the callback should return. If unspecified, the passed in
+ * value is returned.
+ * @param {boolean=} throwReturnVal If true, the `returnVal` will be thrown rather than returned.
+ */
+ function progress(name, returnVal, throwReturnVal) {
+ var returnValDefined = (arguments.length >= 2);
+ name = 'progress' + (name || '');
+ return function() {
+ return _logInvocation(name, arguments, (returnValDefined ? returnVal : arguments[0]), throwReturnVal);
+ }
+ }
+
+ /**
+ * Creates a callback that logs its invocation in `log`.
+ *
* @param {(number|string)} name Suffix for 'error' name. e.g. error(1) => error
* @param {*=} returnVal Value that the callback should return. If unspecified, the passed in
* value is first passed to q.reject() and the result is returned.
@@ -126,6 +142,13 @@ describe('q', function() {
}
+ /** helper for synchronous notification of deferred */
+ function syncNotify(deferred, progress) {
+ deferred.notify(progress);
+ mockNextTick.flush();
+ }
+
+
/** converts the `log` to a '; '-separated string */
function logStr() {
return log.join('; ');
@@ -377,6 +400,114 @@ describe('q', function() {
});
+ describe('notify', function() {
+ it('should execute all progress callbacks in the registration order',
+ function() {
+ promise.then(success(1), error(1), progress(1));
+ promise.then(success(2), error(2), progress(2));
+ expect(logStr()).toBe('');
+
+ deferred.notify('foo');
+ mockNextTick.flush();
+ expect(logStr()).toBe('progress1(foo)->foo; progress2(foo)->foo');
+ });
+
+
+ it('should do nothing if a promise was previously resolved', function() {
+ promise.then(success(1), error(1), progress(1));
+ expect(logStr()).toBe('');
+
+ deferred.resolve('foo');
+ mockNextTick.flush();
+ expect(logStr()).toBe('success1(foo)->foo');
+
+ log = [];
+ deferred.notify('bar');
+ expect(mockNextTick.queue.length).toBe(0);
+ expect(logStr()).toBe('');
+ });
+
+
+ it('should do nothing if a promise was previously rejected', function() {
+ promise.then(success(1), error(1), progress(1));
+ expect(logStr()).toBe('');
+
+ deferred.reject('foo');
+ mockNextTick.flush();
+ expect(logStr()).toBe('error1(foo)->reject(foo)');
+
+ log = [];
+ deferred.reject('bar');
+ deferred.resolve('baz');
+ deferred.notify('qux')
+ expect(mockNextTick.queue.length).toBe(0);
+ expect(logStr()).toBe('');
+
+ promise.then(success(2), error(2), progress(2));
+ expect(logStr()).toBe('');
+ mockNextTick.flush();
+ expect(logStr()).toBe('error2(foo)->reject(foo)');
+ });
+
+
+ it('should not apply any special treatment to promises passed to notify', function() {
+ var deferred2 = defer();
+ promise.then(success(), error(), progress());
+
+ deferred.notify(deferred2.promise);
+ mockNextTick.flush();
+ expect(logStr()).toBe('progress({})->{}');
+ });
+
+
+ it('should call the progress callbacks in the next turn', function() {
+ promise.then(success(), error(), progress(1));
+ promise.then(success(), error(), progress(2));
+ expect(logStr()).toBe('');
+
+ deferred.notify('foo');
+ expect(logStr()).toBe('');
+
+ mockNextTick.flush();
+ expect(logStr()).toBe('progress1(foo)->foo; progress2(foo)->foo');
+ });
+
+
+ it('should ignore notifications sent out in the same turn before listener registration',
+ function() {
+ deferred.notify('foo');
+ promise.then(success(), error(), progress(1));
+ expect(logStr()).toBe('');
+ expect(mockNextTick.queue).toEqual([]);
+ });
+
+
+ it('should support non-bound execution', function() {
+ var notify = deferred.notify;
+ promise.then(success(), error(), progress());
+ notify('detached');
+ mockNextTick.flush();
+ expect(logStr()).toBe('progress(detached)->detached');
+ });
+
+
+ it("should not save and re-emit progress notifications between ticks", function () {
+ promise.then(success(1), error(1), progress(1));
+ deferred.notify('foo');
+ deferred.notify('bar');
+ mockNextTick.flush();
+ expect(logStr()).toBe('progress1(foo)->foo; progress1(bar)->bar');
+
+ log = [];
+ promise.then(success(2), error(2), progress(2));
+ deferred.notify('baz');
+ mockNextTick.flush();
+ expect(logStr()).toBe('progress1(baz)->baz; progress2(baz)->baz');
+ });
+
+ });
+
+
describe('promise', function() {
it('should have a then method', function() {
expect(typeof promise.then).toBe('function');
@@ -388,7 +519,7 @@ describe('q', function() {
describe('then', function() {
- it('should allow registration of a success callback without an errback ' +
+ it('should allow registration of a success callback without an errback or progressback ' +
'and resolve', function() {
promise.then(success());
syncResolve(deferred, 'foo');
@@ -404,7 +535,15 @@ describe('q', function() {
});
- it('should allow registration of an errback without a success callback and ' +
+ it('should allow registration of a success callback without an progressback and notify',
+ function() {
+ promise.then(success());
+ syncNotify(deferred, 'doing');
+ expect(logStr()).toBe('');
+ });
+
+
+ it('should allow registration of an errback without a success or progress callback and ' +
' reject', function() {
promise.then(null, error());
syncReject(deferred, 'oops!');
@@ -420,12 +559,44 @@ describe('q', function() {
});
+ it('should allow registration of an errback without a progress callback and notify',
+ function() {
+ promise.then(null, error());
+ syncNotify(deferred, 'doing');
+ expect(logStr()).toBe('');
+ });
+
+
+ it('should allow registration of an progressback without a success or error callback and ' +
+ 'notify', function() {
+ promise.then(null, null, progress());
+ syncNotify(deferred, 'doing');
+ expect(logStr()).toBe('progress(doing)->doing');
+ });
+
+
+ it('should allow registration of an progressback without a success callback and resolve',
+ function() {
+ promise.then(null, null, progress());
+ syncResolve(deferred, 'done');
+ expect(logStr()).toBe('');
+ });
+
+
+ it('should allow registration of an progressback without a error callback and reject',
+ function() {
+ promise.then(null, null, progress());
+ syncReject(deferred, 'oops!');
+ expect(logStr()).toBe('');
+ });
+
+
it('should resolve all callbacks with the original value', function() {
- promise.then(success('A', 'aVal'), error());
- promise.then(success('B', 'bErr', true), error());
- promise.then(success('C', q.reject('cReason')), error());
- promise.then(success('D', q.reject('dReason'), true), error());
- promise.then(success('E', 'eVal'), error());
+ 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('');
syncResolve(deferred, 'yup');
@@ -438,10 +609,10 @@ describe('q', function() {
it('should reject all callbacks with the original reason', function() {
- promise.then(success(), error('A', 'aVal'));
- promise.then(success(), error('B', 'bEr', true));
- promise.then(success(), error('C', q.reject('cReason')));
- promise.then(success(), error('D', 'dVal'));
+ 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('');
syncReject(deferred, 'noo!');
@@ -449,6 +620,23 @@ describe('q', function() {
});
+ it('should notify all callbacks with the original value', function() {
+ promise.then(success(), error(), progress('A', 'aVal'));
+ promise.then(success(), error(), progress('B', 'bErr', true));
+ promise.then(success(), error(), progress('C', q.reject('cReason')));
+ promise.then(success(), error(), progress('C_reject', q.reject('cRejectReason'), true));
+ promise.then(success(), error(), progress('Z', 'the end!'));
+
+ expect(logStr()).toBe('');
+ syncNotify(deferred, 'yup');
+ expect(log).toEqual(['progressA(yup)->aVal',
+ 'progressB(yup)->throw(bErr)',
+ 'progressC(yup)->{}',
+ 'progressC_reject(yup)->throw({})',
+ 'progressZ(yup)->the end!']);
+ });
+
+
it('should propagate resolution and rejection between dependent promises', function() {
promise.then(success(1, 'x'), error('1')).
then(success(2, 'y', true), error('2')).
@@ -466,6 +654,23 @@ describe('q', function() {
});
+ it('should propagate notification between dependent promises', function() {
+ promise.then(success(), error(), progress(1, 'a')).
+ then(success(), error(), progress(2, 'b')).
+ then(success(), error(), progress(3, 'c')).
+ then(success(), error(), progress(4)).
+ then(success(), error(), progress(5));
+
+ expect(logStr()).toBe('');
+ syncNotify(deferred, 'wait');
+ expect(log).toEqual(['progress1(wait)->a',
+ 'progress2(a)->b',
+ 'progress3(b)->c',
+ 'progress4(c)->c',
+ 'progress5(c)->c']);
+ });
+
+
it('should reject a derived promise if an exception is thrown while resolving its parent',
function() {
promise.then(success(1, 'oops', true), error(1)).
@@ -484,6 +689,18 @@ describe('q', function() {
});
+ it('should stop notification propagation in case of error', function() {
+ promise.then(success(), error(), progress(1)).
+ then(success(), error(), progress(2, 'ops!', true)).
+ then(success(), error(), progress(3));
+
+ expect(logStr()).toBe('');
+ syncNotify(deferred, 'wait');
+ expect(log).toEqual(['progress1(wait)->wait',
+ 'progress2(wait)->throw(ops!)']);
+ });
+
+
it('should call success callback in the next turn even if promise is already resolved',
function() {
deferred.resolve('done!');
@@ -744,6 +961,18 @@ describe('q', function() {
});
+ describe('notification', function() {
+ it('should call the progressback when the value is a promise and gets notified',
+ function() {
+ q.when(deferred.promise, success(), error(), progress());
+ mockNextTick.flush();
+ expect(logStr()).toBe('');
+ syncNotify(deferred, 'notification');
+ expect(logStr()).toBe('progress(notification)->notification');
+ });
+ });
+
+
describe('optional callbacks', function() {
it('should not require success callback and propagate resolution', function() {
q.when('hi', null, error()).then(success(2), error());
@@ -775,6 +1004,16 @@ describe('q', function() {
mockNextTick.flush();
expect(logStr()).toBe('error2(sorry)->reject(sorry)');
});
+
+
+ it('should not require progressback and propagate notification', function() {
+ q.when(deferred.promise).
+ then(success(), error(), progress());
+ mockNextTick.flush();
+ expect(logStr()).toBe('');
+ syncNotify(deferred, 'notification');
+ expect(logStr()).toBe('progress(notification)->notification');
+ });
});
@@ -838,9 +1077,10 @@ describe('q', function() {
it('should call success callback only once even if the original promise gets fullfilled ' +
'multiple times', function() {
var evilPromise = {
- then: function(success, error) {
+ then: function(success, error, progress) {
evilPromise.success = success;
evilPromise.error = error;
+ evilPromise.progress = progress;
}
};
@@ -863,9 +1103,10 @@ describe('q', function() {
it('should call errback only once even if the original promise gets fullfilled multiple ' +
'times', function() {
var evilPromise = {
- then: function(success, error) {
+ then: function(success, error, progress) {
evilPromise.success = success;
evilPromise.error = error;
+ evilPromise.progress = progress;
}
};
@@ -879,6 +1120,29 @@ describe('q', function() {
evilPromise.success('take this');
expect(logStr()).toBe('error(failed)->reject(failed)');
});
+
+
+ it('should not call progressback after promise gets fullfilled, even if original promise ' +
+ 'gets notified multiple times', function() {
+ var evilPromise = {
+ then: function(success, error, progress) {
+ evilPromise.success = success;
+ evilPromise.error = error;
+ evilPromise.progress = progress;
+ }
+ };
+
+ q.when(evilPromise, success(), error(), progress());
+ mockNextTick.flush();
+ expect(logStr()).toBe('');
+ evilPromise.progress('notification');
+ evilPromise.success('ok');
+ mockNextTick.flush();
+ expect(logStr()).toBe('progress(notification)->notification; success(ok)->ok');
+
+ evilPromise.progress('muhaha');
+ expect(logStr()).toBe('progress(notification)->notification; success(ok)->ok');
+ });
});
});
@@ -921,6 +1185,21 @@ describe('q', function() {
});
+ it('should not forward notifications from individual promises to the combined promise',
+ function() {
+ var deferred1 = defer(),
+ deferred2 = defer();
+
+ q.all([promise, deferred1.promise, deferred2.promise]).then(success(), error(), progress());
+ expect(logStr()).toBe('');
+ deferred.notify('x');
+ deferred2.notify('y');
+ expect(logStr()).toBe('');
+ mockNextTick.flush();
+ expect(logStr()).toBe('');
+ });
+
+
it('should ignore multiple resolutions of an (evil) array promise', function() {
var evilPromise = {
then: function(success, error) {
@@ -1064,6 +1343,16 @@ describe('q', function() {
});
+ it('should log exceptions throw in a progressack and stop propagation, but shoud NOT reject ' +
+ 'the promise', function() {
+ promise.then(success(), error(), progress(1, 'failed', true)).then(null, error(1), progress(2));
+ syncNotify(deferred, '10%');
+ expect(logStr()).toBe('progress1(10%)->throw(failed)');
+ expect(mockExceptionLogger.log).toEqual(['failed']);
+ log = [];
+ syncResolve(deferred, 'ok');
+ expect(logStr()).toBe('success(ok)->ok');
+ });
});