Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

feat($q): Add optional canceler and promise.cancel method #2452

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions src/ng/q.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,17 @@
*
* # The Deferred API
*
* A new instance of deferred is constructed by calling `$q.defer()`.
* A new instance of deferred is constructed by calling `$q.defer(canceler)`.
*
* 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 of the task.
*
* `$q.defer` can optionally take a canceler function. This function will cause resulting promises,
* and any derived promises, to have a `cancel()` method, and will be invoked if the promise is
* canceled. The canceler receives the reason the promise was canceled as its argument. The promise
* is rejected with the canceler's return value or the original cancel reason if nothing is
* returned.
*
* **Methods**
*
* - `resolve(value)` – resolves the derived promise with the `value`. If the value is a rejection
Expand Down Expand Up @@ -97,6 +103,11 @@
* specification](https://github.com/kriskowal/q/wiki/API-Reference#promisefinallycallback) for
* more information.
*
* - `cancel(reason)` - optionally available if a canceler was provided to `$q.defer`. The canceler
* is invoked and the promise rejected. A reason may be sent to the canceler explaining why it's
* being canceled. Returns true if the promise has not been resolved and was successfully
* canceled.
*
* # Chaining promises
*
* Because calling `then` api of a promise returns a new derived promise, it is easily possible
Expand Down Expand Up @@ -180,9 +191,10 @@ function qFactory(nextTick, exceptionHandler) {
* @description
* Creates a `Deferred` object which represents a task which will finish in the future.
*
* @param {function(*)=} canceler Function which will be called if the task is canceled.
* @returns {Deferred} Returns a new instance of deferred.
*/
var defer = function() {
var defer = function(canceler) {
var pending = [],
value, deferred;

Expand Down Expand Up @@ -214,7 +226,7 @@ function qFactory(nextTick, exceptionHandler) {

promise: {
then: function(callback, errback) {
var result = defer();
var result = defer(wrappedCanceler);

var wrappedCallback = function(value) {
try {
Expand Down Expand Up @@ -281,6 +293,27 @@ function qFactory(nextTick, exceptionHandler) {
}
};

if (isFunction(canceler)) {
var wrappedCanceler = function(reason) {
try {
var value = canceler(reason);
if (isDefined(value)) reason = value;
} catch(e) {
exceptionHandler(e);
reason = e;
}
when(reason).then(deferred.reject, deferred.reject);
return reason;
};

deferred.promise.cancel = function(reason) {
if (pending) {
return !(wrappedCanceler(reason) instanceof Error);
}
return false;
};
}

return deferred;
};

Expand Down
88 changes: 88 additions & 0 deletions test/ng/qSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,94 @@ describe('q', function() {
expect(typeof promise.always).toBe('function');
});

it('should not have a cancel method if no canceler is provided', function() {
expect(promise.cancel).not.toBeDefined();
});

it('should have a cancel method if a canceler is provided', function() {
deferred = defer(noop);
promise = deferred.promise;
expect(promise.cancel).toBeDefined();
promise = promise.always(noop);
expect(promise.cancel).toBeDefined();
});


describe('cancel', function() {
var canceler;

beforeEach(function() {
canceler = jasmine.createSpy();
deferred = defer(canceler);
promise = deferred.promise;
});

it('should cancel a pending task and reject the promise', function() {
promise.then(success(), error());
expect(promise.cancel('foo')).toBe(true);
expect(canceler).toHaveBeenCalledWith('foo');
mockNextTick.flush();
expect(logStr()).toBe('error(foo)');
});

it('should reject the promise with a reason returned from the canceler', function() {
canceler.andReturn('bar');
promise.then(success(), error());
expect(promise.cancel('foo')).toBe(true);
expect(canceler).toHaveBeenCalledWith('foo');
mockNextTick.flush();
expect(logStr()).toBe('error(bar)');
});

it('should log exceptions thrown in the canceler', function() {
canceler.andThrow(Error('oops'));
promise.then(success(), error());
expect(promise.cancel('foo')).toBe(false);
expect(canceler).toHaveBeenCalledWith('foo');
mockNextTick.flush();
expect(logStr()).toBe('error(Error: oops)');
});

it('should not cancel a resolved promise', function() {
promise.then(success(), error());
syncResolve(deferred, 'foo');
expect(promise.cancel('bar')).toBe(false);
expect(canceler).not.toHaveBeenCalled();
expect(logStr()).toBe('success(foo)');
});

it('should propagate the cancel method and reasons', function() {
promise = promise.then(success(1), error(1)).then(success(2), error(2));
expect(promise.cancel).toBeDefined();
expect(promise.cancel('foo')).toBe(true);
expect(canceler).toHaveBeenCalledWith('foo');
mockNextTick.flush();
expect(logStr()).toBe('error1(foo); error2(foo)');
});

it('should reject all derived promises', function() {
var promiseA = promise.then(success('A'), error('A'));
var promiseB = promise.then(success('B'), error('B'));
var promiseC = promiseB.then(success('C'), error('C'));
var promiseD = promiseB.then(success('D'), error('D'));
var promiseE = promiseD.always(error('E'));
expect(promiseC.cancel('foo')).toBe(true);
expect(canceler).toHaveBeenCalledWith('foo');
mockNextTick.flush();
expect(logStr()).toBe('errorA(foo); errorB(foo); errorC(foo); errorD(foo); errorE()');
});

it('should resolve promises returned by the canceler', function() {
var deferred2 = defer();
canceler.andReturn(deferred2.promise.then(success('A')));
promise.then(success('B'), error('B'));
expect(promise.cancel('foo')).toBe(true);
expect(canceler).toHaveBeenCalledWith('foo');
syncResolve(deferred2, 'bar');
expect(logStr()).toBe('successA(bar); errorB(bar)');
})
});


describe('then', function() {
it('should allow registration of a success callback without an errback and resolve',
Expand Down