Skip to content

Commit

Permalink
feat($resource): add support for cancelling requests using promises
Browse files Browse the repository at this point in the history
Previously, it was not possible to use a promise as value for the
`timeout` property of an action's `config`, because that value would be
copied before being passed to `$http`.
This commit introduces a special value for the `timeout`, namely
`'promise'`. Setting an action's `timeout` configuration property to
`'promise'`, will create a new promise and pass it to `$http` for each
request. The associated deferred, is attached to the resource instance,
under `$timeoutDeferred`.

Example usage:

```js
var Post = $resource('/posts/:id', {id: '@id'}, {
  query: {
    method: 'GET',
    isArray: true,
    timeout: 'promise'
  }
});

var posts = Post.query();   // GET /posts
...
// Later we decide to cancel the request
// in order to make a new one
posts.$timeoutDeferred.resolve();
posts.query({author: 'me'});   // GET /posts?author=me
...
```

Fixes angular#9332
Closes angular#13050
  • Loading branch information
gkalpak committed Oct 9, 2015
1 parent 8226ff8 commit 5ed04eb
Show file tree
Hide file tree
Showing 2 changed files with 153 additions and 16 deletions.
13 changes: 10 additions & 3 deletions src/ngResource/resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,7 @@ angular.module('ngResource', ['ng']).

Resource.prototype.toJSON = function() {
var data = extend({}, this);
delete data.$timeoutDeferred;
delete data.$promise;
delete data.$resolved;
return data;
Expand Down Expand Up @@ -574,19 +575,24 @@ angular.module('ngResource', ['ng']).
undefined;

forEach(action, function(value, key) {
if (key != 'params' && key != 'isArray' && key != 'interceptor') {
if (key !== 'params' && key !== 'isArray' && key !== 'interceptor') {
httpConfig[key] = copy(value);
}
});
if (action.timeout === 'promise') {
var deferred = value.$timeoutDeferred = $q.defer();
httpConfig.timeout = deferred.promise;
}

if (hasBody) httpConfig.data = data;
route.setUrlParams(httpConfig,
extend({}, extractParams(data, action.params || {}), params),
action.url);

var promise = $http(httpConfig).then(function(response) {
var data = response.data,
promise = value.$promise;
var data = response.data;
var promise = value.$promise;
var timeoutDeferred = value.$timeoutDeferred;

if (data) {
// Need to convert action.isArray to boolean in case it is undefined
Expand All @@ -613,6 +619,7 @@ angular.module('ngResource', ['ng']).
} else {
shallowClearAndCopy(data, value);
value.$promise = promise;
value.$timeoutDeferred = timeoutDeferred;
}
}

Expand Down
156 changes: 143 additions & 13 deletions test/ngResource/resourceSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,20 @@ describe("resource", function() {
headers: {
'If-None-Match': '*'
}
},
cancellableQuery: {
method: 'GET',
isArray: true,
timeout: 'promise'
},
cancellableGet: {
method: 'GET',
timeout: 'promise'
},
cancellablePut: {
method: 'PUT',
timeout: 'promise'
}

});
callback = jasmine.createSpy();
}));
Expand Down Expand Up @@ -674,21 +686,25 @@ describe("resource", function() {
expect(person2).toEqual(jasmine.any(Person));
});

it('should not include $promise and $resolved when resource is toJson\'ed', function() {
$httpBackend.expect('GET', '/CreditCard/123').respond({id: 123, number: '9876'});
var cc = CreditCard.get({id: 123});
$httpBackend.flush();
it('should not include $promise, $resolved and $timeoutDeferred when resource is toJson\'ed',
function() {
$httpBackend.expect('GET', '/CreditCard/123').respond({id: 123, number: '9876'});
var cc = CreditCard.cancellableGet({id: 123});
$httpBackend.flush();

cc.$myProp = 'still here';
cc.$myProp = 'still here';

expect(cc.$promise).toBeDefined();
expect(cc.$resolved).toBe(true);
expect(cc.$promise).toBeDefined();
expect(cc.$resolved).toBe(true);
expect(cc.$timeoutDeferred).toBeDefined();

var json = JSON.parse(angular.toJson(cc));
expect(json.$promise).not.toBeDefined();
expect(json.$resolved).not.toBeDefined();
expect(json).toEqual({id: 123, number: '9876', $myProp: 'still here'});
});
var json = JSON.parse(angular.toJson(cc));
expect(json.$promise).not.toBeDefined();
expect(json.$resolved).not.toBeDefined();
expect(json.$timeoutDeferred).not.toBeDefined();
expect(json).toEqual({id: 123, number: '9876', $myProp: 'still here'});
}
);

describe('promise api', function() {

Expand Down Expand Up @@ -1033,6 +1049,120 @@ describe("resource", function() {
});
});

describe('timeoutDeferred api', function() {
var $rootScope;


beforeEach(inject(function(_$rootScope_) {
$rootScope = _$rootScope_;
}));


describe('single resource', function() {
beforeEach(inject(function(_$rootScope_) {
$httpBackend.whenGET('/CreditCard/123').respond({id: {key: 123}, number: '9876'});
}));


it('should add $timeoutDeferred to the result object iff `timeout === "promise"`',
function() {
var cc = CreditCard.cancellableGet({id: 123});
expect(cc.$timeoutDeferred).toBeDefined();

cc = CreditCard.get({id: 123});
expect(cc.$timeoutDeferred).not.toBeDefined();
}
);


it('should keep $timeoutDeferred around after resolution', function() {
var cc = CreditCard.cancellableGet({id: 123});
var deferred = cc.$timeoutDeferred;

$httpBackend.flush();

expect(cc.$timeoutDeferred).toBeDefined();
expect(cc.$timeoutDeferred).toBe(deferred);
});


it('should create a new $timeoutDeferred for each request', function() {
$httpBackend.whenPUT('/CreditCard/123').respond({id: {key: 123}, number: '9876'});

var cc = CreditCard.cancellableGet({id: 123});
$httpBackend.flush();
var deferred1 = cc.$timeoutDeferred;

cc.$cancellablePut();
$httpBackend.flush();
var deferred2 = cc.$timeoutDeferred;

cc.$cancellablePut();
$httpBackend.flush();
var deferred3 = cc.$timeoutDeferred;

expect(deferred1).toBeDefined();
expect(deferred2).toBeDefined();
expect(deferred3).toBeDefined();
expect(deferred1).not.toBe(deferred2);
expect(deferred1).not.toBe(deferred3);
expect(deferred2).not.toBe(deferred3);
});


it('should allow cancelling the request', function() {
var cc = new CreditCard({id: {key: 123}});

$httpBackend.expectPUT('/CreditCard/123').respond({id: {key: 123}});
cc.$cancellablePut();
expect($httpBackend.flush).not.toThrow();

$httpBackend.expectPUT('/CreditCard/123').respond({id: {key: 123}});
cc.$cancellablePut();
cc.$timeoutDeferred.resolve();
expect($httpBackend.flush).toThrow('No pending request to flush !');
});
});


describe('resource collection', function() {
beforeEach(inject(function(_$rootScope_) {
$httpBackend.whenGET('/CreditCard').respond([{id: {key: 1}}, {id: {key: 2}}]);
}));


it('should add $timeoutDeferred to the result object iff `timeout === "promise"`',
function() {
var ccs = CreditCard.query();
expect(ccs.$timeoutDeferred).not.toBeDefined();

ccs = CreditCard.cancellableQuery();
expect(ccs.$timeoutDeferred).toBeDefined();
}
);


it('should keep $timeoutDeferred around after resolution', function() {
var ccs = CreditCard.cancellableQuery();
var deferred = ccs.$timeoutDeferred;

$httpBackend.flush();

expect(ccs.$timeoutDeferred).toBeDefined();
expect(ccs.$timeoutDeferred).toBe(deferred);
});


it('should allow cancelling the request', function() {
var ccs = CreditCard.cancellableQuery();
expect($httpBackend.flush).not.toThrow();

ccs = CreditCard.cancellableQuery();
ccs.$timeoutDeferred.resolve();
expect($httpBackend.flush).toThrow('No pending request to flush !');
});
});
});

describe('failure mode', function() {
var ERROR_CODE = 500,
Expand Down

0 comments on commit 5ed04eb

Please sign in to comment.