Skip to content

Commit 98528be

Browse files
gkalpakpetebacondarwin
authored andcommitted
feat($resource): add proper support for cancelling requests
Introduced changes: - Deprecate passing a promise as `timeout` (for `$resource` actions). It never worked correctly anyway. Now a warning is logged (using `$log.debug()`) and the property is removed. - Add support for a boolean `cancellable` property in actions' configuration, the `$resource` factory's `options` parameter and the `$resourceProvider`'s `defaults` property. If true, the `$cancelRequest` method (added to all returned values for non-instance calls) will abort the request (if it's not already completed or aborted). If there is a numeric `timeout` specified on the action's configuration, the value of `cancellable` will be ignored. Example usage: ```js var Post = $resource('/posts/:id', {id: '@id'}, { get: { method: 'GET', cancellable: true } }); var currentPost = Post.get({id: 1}); ... // A moment later the user selects another post, so // we don't need the previous request any more currentPost.$cancelRequest(); currentPost = Post.get({id: 2}); ... ``` BREAKING CHANGE: Using a promise as `timeout` is no longer supported and will log a warning. It never worked the way it was supposed to anyway. Before: ```js var deferred = $q.defer(); var User = $resource('/api/user/:id', {id: '@id'}, { get: {method: 'GET', timeout: deferred.promise} }); var user = User.get({id: 1}); // sends a request deferred.resolve(); // aborts the request // Now, we need to re-define `User` passing a new promise as `timeout` // or else all subsequent requests from `someAction` will be aborted User = $resource(...); user = User.get({id: 2}); ``` After: ```js var User = $resource('/api/user/:id', {id: '@id'}, { get: {method: 'GET', cancellable: true} }); var user = User.get({id: 1}); // sends a request instance.$cancelRequest(); // aborts the request user = User.get({id: 2}); ``` Fixes angular#9332 Closes angular#13050 Closes angular#13058 Closes angular#13210
1 parent 9190d4c commit 98528be

File tree

2 files changed

+296
-43
lines changed

2 files changed

+296
-43
lines changed

src/ngResource/resource.js

+105-30
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ function shallowClearAndCopy(src, dst) {
6363
* @ngdoc service
6464
* @name $resource
6565
* @requires $http
66+
* @requires ng.$log
67+
* @requires $q
6668
*
6769
* @description
6870
* A factory which creates a resource object that lets you interact with
@@ -107,9 +109,9 @@ function shallowClearAndCopy(src, dst) {
107109
* URL `/path/greet?salutation=Hello`.
108110
*
109111
* If the parameter value is prefixed with `@` then the value for that parameter will be extracted
110-
* from the corresponding property on the `data` object (provided when calling an action method). For
111-
* example, if the `defaultParam` object is `{someParam: '@someProp'}` then the value of `someParam`
112-
* will be `data.someProp`.
112+
* from the corresponding property on the `data` object (provided when calling an action method).
113+
* For example, if the `defaultParam` object is `{someParam: '@someProp'}` then the value of
114+
* `someParam` will be `data.someProp`.
113115
*
114116
* @param {Object.<Object>=} actions Hash with declaration of custom actions that should extend
115117
* the default set of resource actions. The declaration should be created in the format of {@link
@@ -143,15 +145,23 @@ function shallowClearAndCopy(src, dst) {
143145
* `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` –
144146
* transform function or an array of such functions. The transform function takes the http
145147
* response body and headers and returns its transformed (typically deserialized) version.
146-
* By default, transformResponse will contain one function that checks if the response looks like
147-
* a JSON string and deserializes it using `angular.fromJson`. To prevent this behavior, set
148-
* `transformResponse` to an empty array: `transformResponse: []`
148+
* By default, transformResponse will contain one function that checks if the response looks
149+
* like a JSON string and deserializes it using `angular.fromJson`. To prevent this behavior,
150+
* set `transformResponse` to an empty array: `transformResponse: []`
149151
* - **`cache`** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the
150152
* GET request, otherwise if a cache instance built with
151153
* {@link ng.$cacheFactory $cacheFactory}, this cache will be used for
152154
* caching.
153-
* - **`timeout`** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} that
154-
* should abort the request when resolved.
155+
* - **`timeout`** – `{number}` – timeout in milliseconds.<br />
156+
* **Note:** In contrast to {@link ng.$http#usage $http.config}, {@link ng.$q promises} are
157+
* **not** supported in $resource, because the same value has to be re-used for multiple
158+
* requests. If you are looking for a way to cancel requests, you should use the `cancellable`
159+
* option.
160+
* - **`cancellable`** – `{boolean}` – if set to true, the request made by a "non-instance" call
161+
* will be cancelled (if not already completed) by calling `$cancelRequest()` on the call's
162+
* return value. Calling `$cancelRequest()` for a non-cancellable or an already
163+
* completed/cancelled request will have no effect.<br />
164+
* **Note:** If a timeout is specified in millisecondes, `cancellable` is ignored.
155165
* - **`withCredentials`** - `{boolean}` - whether to set the `withCredentials` flag on the
156166
* XHR object. See
157167
* [requests with credentials](https://developer.mozilla.org/en/http_access_control#section_5)
@@ -163,12 +173,13 @@ function shallowClearAndCopy(src, dst) {
163173
* with `http response` object. See {@link ng.$http $http interceptors}.
164174
*
165175
* @param {Object} options Hash with custom settings that should extend the
166-
* default `$resourceProvider` behavior. The only supported option is
167-
*
168-
* Where:
176+
* default `$resourceProvider` behavior. The supported options are:
169177
*
170178
* - **`stripTrailingSlashes`** – {boolean} – If true then the trailing
171179
* slashes from any calculated URL will be stripped. (Defaults to true.)
180+
* - **`cancellable`** – {boolean} – If true, the request made by a "non-instance" call will be
181+
* cancelled (if not already completed) by calling `$cancelRequest()` on the call's return value.
182+
* This can be overwritten per action. (Defaults to false.)
172183
*
173184
* @returns {Object} A resource "class" object with methods for the default set of resource actions
174185
* optionally extended with custom `actions`. The default set contains these actions:
@@ -216,7 +227,7 @@ function shallowClearAndCopy(src, dst) {
216227
* Class actions return empty instance (with additional properties below).
217228
* Instance actions return promise of the action.
218229
*
219-
* The Resource instances and collection have these additional properties:
230+
* The Resource instances and collections have these additional properties:
220231
*
221232
* - `$promise`: the {@link ng.$q promise} of the original server interaction that created this
222233
* instance or collection.
@@ -236,6 +247,11 @@ function shallowClearAndCopy(src, dst) {
236247
* rejection), `false` before that. Knowing if the Resource has been resolved is useful in
237248
* data-binding.
238249
*
250+
* The Resource instances and collections have these additional methods:
251+
*
252+
* - `$cancelRequest`: If there is a cancellable, pending request related to the instance or
253+
* collection, calling this method will abort the request.
254+
*
239255
* @example
240256
*
241257
* # Credit card resource
@@ -280,6 +296,11 @@ function shallowClearAndCopy(src, dst) {
280296
*
281297
* Calling these methods invoke `$http` on the `url` template with the given `method`, `params` and
282298
* `headers`.
299+
*
300+
* @example
301+
*
302+
* # User resource
303+
*
283304
* When the data is returned from the server then the object is an instance of the resource type and
284305
* all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD
285306
* operations (create, read, update, delete) on server-side data.
@@ -298,10 +319,10 @@ function shallowClearAndCopy(src, dst) {
298319
*
299320
```js
300321
var User = $resource('/user/:userId', {userId:'@id'});
301-
User.get({userId:123}, function(u, getResponseHeaders){
302-
u.abc = true;
303-
u.$save(function(u, putResponseHeaders) {
304-
//u => saved user object
322+
User.get({userId:123}, function(user, getResponseHeaders){
323+
user.abc = true;
324+
user.$save(function(user, putResponseHeaders) {
325+
//user => saved user object
305326
//putResponseHeaders => $http header getter
306327
});
307328
});
@@ -316,8 +337,11 @@ function shallowClearAndCopy(src, dst) {
316337
$scope.user = user;
317338
});
318339
```
319-
340+
*
341+
* @example
342+
*
320343
* # Creating a custom 'PUT' request
344+
*
321345
* In this example we create a custom method on our resource to make a PUT request
322346
* ```js
323347
* var app = angular.module('app', ['ngResource', 'ngRoute']);
@@ -345,6 +369,34 @@ function shallowClearAndCopy(src, dst) {
345369
* // This will PUT /notes/ID with the note object in the request payload
346370
* }]);
347371
* ```
372+
*
373+
* @example
374+
*
375+
* # Cancelling requests
376+
*
377+
* If an action's configuration specifies that it is cancellable, you can cancel the request related
378+
* to an instance or collection (as long as it is a result of a "non-instance" call):
379+
*
380+
```js
381+
// ...defining the `Hotel` resource...
382+
var Hotel = $resource('/api/hotel/:id', {id: '@id'}, {
383+
// Let's make the `query()` method cancellable
384+
query: {method: 'get', isArray: true, cancellable: true}
385+
});
386+
387+
// ...somewhere in the PlanVacationController...
388+
...
389+
this.onDestinationChanged = function onDestinationChanged(destination) {
390+
// We don't care about any pending request for hotels
391+
// in a different destination any more
392+
this.availableHotels.$cancelRequest();
393+
394+
// Let's query for hotels in '<destination>'
395+
// (calls: /api/hotel?location=<destination>)
396+
this.availableHotels = Hotel.query({location: destination});
397+
};
398+
```
399+
*
348400
*/
349401
angular.module('ngResource', ['ng']).
350402
provider('$resource', function() {
@@ -365,7 +417,7 @@ angular.module('ngResource', ['ng']).
365417
}
366418
};
367419

368-
this.$get = ['$http', '$q', function($http, $q) {
420+
this.$get = ['$http', '$log', '$q', function($http, $log, $q) {
369421

370422
var noop = angular.noop,
371423
forEach = angular.forEach,
@@ -524,6 +576,22 @@ angular.module('ngResource', ['ng']).
524576

525577
forEach(actions, function(action, name) {
526578
var hasBody = /^(POST|PUT|PATCH)$/i.test(action.method);
579+
var cancellable;
580+
581+
if (angular.isNumber(action.timeout)) {
582+
cancellable = false;
583+
} else if (action.timeout) {
584+
$log.debug('ngResource:\n' +
585+
' Only numeric values are allowed as `timeout`.\n' +
586+
' Promises are not supported in $resource, because the same value has to ' +
587+
'be re-used for multiple requests. If you are looking for a way to cancel ' +
588+
'requests, you should use the `cancellable` option.');
589+
delete action.timeout;
590+
} else {
591+
cancellable = angular.isDefined(action.cancellable) ? action.cancellable :
592+
(options && angular.isDefined(options.cancellable)) ? options.cancellable :
593+
provider.defaults.cancellable;
594+
}
527595

528596
Resource[name] = function(a1, a2, a3, a4) {
529597
var params = {}, data, success, error;
@@ -572,6 +640,7 @@ angular.module('ngResource', ['ng']).
572640
defaultResponseInterceptor;
573641
var responseErrorInterceptor = action.interceptor && action.interceptor.responseError ||
574642
undefined;
643+
var timeoutDeferred;
575644

576645
forEach(action, function(value, key) {
577646
switch (key) {
@@ -581,21 +650,23 @@ angular.module('ngResource', ['ng']).
581650
case 'params':
582651
case 'isArray':
583652
case 'interceptor':
584-
break;
585-
case 'timeout':
586-
httpConfig[key] = value;
653+
case 'cancellable':
587654
break;
588655
}
589656
});
590657

658+
if (!isInstanceCall && cancellable) {
659+
timeoutDeferred = $q.defer();
660+
httpConfig.timeout = timeoutDeferred.promise;
661+
}
662+
591663
if (hasBody) httpConfig.data = data;
592664
route.setUrlParams(httpConfig,
593665
extend({}, extractParams(data, action.params || {}), params),
594666
action.url);
595667

596668
var promise = $http(httpConfig).then(function(response) {
597-
var data = response.data,
598-
promise = value.$promise;
669+
var data = response.data;
599670

600671
if (data) {
601672
// Need to convert action.isArray to boolean in case it is undefined
@@ -620,24 +691,27 @@ angular.module('ngResource', ['ng']).
620691
}
621692
});
622693
} else {
694+
var promise = value.$promise; // Save the promise
623695
shallowClearAndCopy(data, value);
624-
value.$promise = promise;
696+
value.$promise = promise; // Restore the promise
625697
}
626698
}
627-
628-
value.$resolved = true;
629-
630699
response.resource = value;
631700

632701
return response;
633702
}, function(response) {
634-
value.$resolved = true;
635-
636703
(error || noop)(response);
637-
638704
return $q.reject(response);
639705
});
640706

707+
promise.finally(function() {
708+
value.$resolved = true;
709+
if (cancellable) {
710+
value.$cancelRequest = angular.noop;
711+
timeoutDeferred = httpConfig.timeout = null;
712+
}
713+
});
714+
641715
promise = promise.then(
642716
function(response) {
643717
var value = responseInterceptor(response);
@@ -652,6 +726,7 @@ angular.module('ngResource', ['ng']).
652726
// - return the instance / collection
653727
value.$promise = promise;
654728
value.$resolved = false;
729+
if (cancellable) value.$cancelRequest = timeoutDeferred.resolve;
655730

656731
return value;
657732
}

0 commit comments

Comments
 (0)