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.
This commit is contained in:
Caio Cunha 2013-03-24 22:18:10 -03:00 committed by Chirayu Krishnappa
parent d884eb80a1
commit 2a5c355582
2 changed files with 345 additions and 20 deletions

View file

@ -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));
});
});

View file

@ -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) {
@ -91,6 +91,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`.
*
@ -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');
});
});