Skip to content

Commit 8c18cb1

Browse files
committed
feat($resource): add support for cancelling requests using promises
Previously, it was not possible to use a promise as value for the `timeout` property of an actions `config`, because that value would be copied before being passed to `$http`. This commit introduces a special value for the `timeout`, namely `'promise'`. Setting 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
1 parent 8226ff8 commit 8c18cb1

File tree

2 files changed

+153
-16
lines changed

2 files changed

+153
-16
lines changed

src/ngResource/resource.js

+10-3
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,7 @@ angular.module('ngResource', ['ng']).
517517

518518
Resource.prototype.toJSON = function() {
519519
var data = extend({}, this);
520+
delete data.$timeoutDeferred;
520521
delete data.$promise;
521522
delete data.$resolved;
522523
return data;
@@ -574,19 +575,24 @@ angular.module('ngResource', ['ng']).
574575
undefined;
575576

576577
forEach(action, function(value, key) {
577-
if (key != 'params' && key != 'isArray' && key != 'interceptor') {
578+
if (key !== 'params' && key !== 'isArray' && key !== 'interceptor') {
578579
httpConfig[key] = copy(value);
579580
}
580581
});
582+
if (action.timeout === 'promise') {
583+
var deferred = value.$timeoutDeferred = $q.defer();
584+
httpConfig.timeout = deferred.promise;
585+
}
581586

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

587592
var promise = $http(httpConfig).then(function(response) {
588-
var data = response.data,
589-
promise = value.$promise;
593+
var data = response.data;
594+
var promise = value.$promise;
595+
var timeoutDeferred = value.$timeoutDeferred;
590596

591597
if (data) {
592598
// Need to convert action.isArray to boolean in case it is undefined
@@ -613,6 +619,7 @@ angular.module('ngResource', ['ng']).
613619
} else {
614620
shallowClearAndCopy(data, value);
615621
value.$promise = promise;
622+
value.$timeoutDeferred = timeoutDeferred;
616623
}
617624
}
618625

test/ngResource/resourceSpec.js

+143-13
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,20 @@ describe("resource", function() {
2525
headers: {
2626
'If-None-Match': '*'
2727
}
28+
},
29+
cancellableQuery: {
30+
method: 'GET',
31+
isArray: true,
32+
timeout: 'promise'
33+
},
34+
cancellableGet: {
35+
method: 'GET',
36+
timeout: 'promise'
37+
},
38+
cancellablePut: {
39+
method: 'PUT',
40+
timeout: 'promise'
2841
}
29-
3042
});
3143
callback = jasmine.createSpy();
3244
}));
@@ -674,21 +686,25 @@ describe("resource", function() {
674686
expect(person2).toEqual(jasmine.any(Person));
675687
});
676688

677-
it('should not include $promise and $resolved when resource is toJson\'ed', function() {
678-
$httpBackend.expect('GET', '/CreditCard/123').respond({id: 123, number: '9876'});
679-
var cc = CreditCard.get({id: 123});
680-
$httpBackend.flush();
689+
it('should not include $promise, $resolved and $timeoutDeferred when resource is toJson\'ed',
690+
function() {
691+
$httpBackend.expect('GET', '/CreditCard/123').respond({id: 123, number: '9876'});
692+
var cc = CreditCard.cancellableGet({id: 123});
693+
$httpBackend.flush();
681694

682-
cc.$myProp = 'still here';
695+
cc.$myProp = 'still here';
683696

684-
expect(cc.$promise).toBeDefined();
685-
expect(cc.$resolved).toBe(true);
697+
expect(cc.$promise).toBeDefined();
698+
expect(cc.$resolved).toBe(true);
699+
expect(cc.$timeoutDeferred).toBeDefined();
686700

687-
var json = JSON.parse(angular.toJson(cc));
688-
expect(json.$promise).not.toBeDefined();
689-
expect(json.$resolved).not.toBeDefined();
690-
expect(json).toEqual({id: 123, number: '9876', $myProp: 'still here'});
691-
});
701+
var json = JSON.parse(angular.toJson(cc));
702+
expect(json.$promise).not.toBeDefined();
703+
expect(json.$resolved).not.toBeDefined();
704+
expect(json.$timeoutDeferred).not.toBeDefined();
705+
expect(json).toEqual({id: 123, number: '9876', $myProp: 'still here'});
706+
}
707+
);
692708

693709
describe('promise api', function() {
694710

@@ -1033,6 +1049,120 @@ describe("resource", function() {
10331049
});
10341050
});
10351051

1052+
describe('timeoutDeferred api', function() {
1053+
var $rootScope;
1054+
1055+
1056+
beforeEach(inject(function(_$rootScope_) {
1057+
$rootScope = _$rootScope_;
1058+
}));
1059+
1060+
1061+
describe('single resource', function() {
1062+
beforeEach(inject(function(_$rootScope_) {
1063+
$httpBackend.whenGET('/CreditCard/123').respond({id: {key: 123}, number: '9876'});
1064+
}));
1065+
1066+
1067+
it('should add $timeoutDeferred to the result object iff `timeout === "promise"`',
1068+
function() {
1069+
var cc = CreditCard.cancellableGet({id: 123});
1070+
expect(cc.$timeoutDeferred).toBeDefined();
1071+
1072+
cc = CreditCard.get({id: 123});
1073+
expect(cc.$timeoutDeferred).not.toBeDefined();
1074+
}
1075+
);
1076+
1077+
1078+
it('should keep $timeoutDeferred around after resolution', function() {
1079+
var cc = CreditCard.cancellableGet({id: 123});
1080+
var deferred = cc.$timeoutDeferred;
1081+
1082+
$httpBackend.flush();
1083+
1084+
expect(cc.$timeoutDeferred).toBeDefined();
1085+
expect(cc.$timeoutDeferred).toBe(deferred);
1086+
});
1087+
1088+
1089+
it('should create a new $timeoutDeferred for each request', function() {
1090+
$httpBackend.whenPUT('/CreditCard/123').respond({id: {key: 123}, number: '9876'});
1091+
1092+
var cc = CreditCard.cancellableGet({id: 123});
1093+
$httpBackend.flush();
1094+
var deferred1 = cc.$timeoutDeferred;
1095+
1096+
cc.$cancellablePut();
1097+
$httpBackend.flush();
1098+
var deferred2 = cc.$timeoutDeferred;
1099+
1100+
cc.$cancellablePut();
1101+
$httpBackend.flush();
1102+
var deferred3 = cc.$timeoutDeferred;
1103+
1104+
expect(deferred1).toBeDefined();
1105+
expect(deferred2).toBeDefined();
1106+
expect(deferred3).toBeDefined();
1107+
expect(deferred1).not.toBe(deferred2);
1108+
expect(deferred1).not.toBe(deferred3);
1109+
expect(deferred2).not.toBe(deferred3);
1110+
});
1111+
1112+
1113+
it('should allow cancelling the request', function() {
1114+
var cc = new CreditCard({id: {key: 123}});
1115+
1116+
$httpBackend.expectPUT('/CreditCard/123').respond({id: {key: 123}});
1117+
cc.$cancellablePut();
1118+
expect($httpBackend.flush).not.toThrow();
1119+
1120+
$httpBackend.expectPUT('/CreditCard/123').respond({id: {key: 123}});
1121+
cc.$cancellablePut();
1122+
cc.$timeoutDeferred.resolve();
1123+
expect($httpBackend.flush).toThrow('No pending request to flush !');
1124+
});
1125+
});
1126+
1127+
1128+
describe('resource collection', function() {
1129+
beforeEach(inject(function(_$rootScope_) {
1130+
$httpBackend.whenGET('/CreditCard').respond([{id: {key: 1}}, {id: {key: 2}}]);
1131+
}));
1132+
1133+
1134+
it('should add $timeoutDeferred to the result object iff `timeout === "promise"`',
1135+
function() {
1136+
var ccs = CreditCard.query();
1137+
expect(ccs.$timeoutDeferred).not.toBeDefined();
1138+
1139+
ccs = CreditCard.cancellableQuery();
1140+
expect(ccs.$timeoutDeferred).toBeDefined();
1141+
}
1142+
);
1143+
1144+
1145+
it('should keep $timeoutDeferred around after resolution', function() {
1146+
var ccs = CreditCard.cancellableQuery();
1147+
var deferred = ccs.$timeoutDeferred;
1148+
1149+
$httpBackend.flush();
1150+
1151+
expect(ccs.$timeoutDeferred).toBeDefined();
1152+
expect(ccs.$timeoutDeferred).toBe(deferred);
1153+
});
1154+
1155+
1156+
it('should allow cancelling the request', function() {
1157+
var ccs = CreditCard.cancellableQuery();
1158+
expect($httpBackend.flush).not.toThrow();
1159+
1160+
ccs = CreditCard.cancellableQuery();
1161+
ccs.$timeoutDeferred.resolve();
1162+
expect($httpBackend.flush).toThrow('No pending request to flush !');
1163+
});
1164+
});
1165+
});
10361166

10371167
describe('failure mode', function() {
10381168
var ERROR_CODE = 500,

0 commit comments

Comments
 (0)