Skip to content

Commit

Permalink
feat($resource): Allow functions in action timeout
Browse files Browse the repository at this point in the history
Support functions when configuring actions. This allows the timeout to change with time, when the function returns a number, and allows an action to be cancelled multiple times, when the function returns a new promise.

Closes angular#7974

Styling tweak
  • Loading branch information
JoaoPeixoto committed Oct 29, 2014
1 parent 43b1a37 commit 37a6031
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 8 deletions.
6 changes: 4 additions & 2 deletions src/ng/http.js
Expand Up @@ -595,8 +595,10 @@ function $HttpProvider() {
* GET request, otherwise if a cache instance built with
* {@link ng.$cacheFactory $cacheFactory}, this cache will be used for
* caching.
* - **timeout** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise}
* that should abort the request when resolved.
* - **`timeout`** – `{number|Promise|Function}` – timeout in milliseconds, or
* {@link ng.$q promise} that should abort the request when resolved or a function that returns
* either a number or a promise. The function allows for the timeout to change with time and
* provides a way to cancel an action multiple times (by providing a new promise each time).
* - **withCredentials** - `{boolean}` - whether to set the `withCredentials` flag on the
* XHR object. See [requests with credentials](https://developer.mozilla.org/docs/Web/HTTP/Access_control_CORS#Requests_with_credentials)
* for more information.
Expand Down
11 changes: 7 additions & 4 deletions src/ng/httpBackend.js
Expand Up @@ -29,6 +29,7 @@ function $HttpBackendProvider() {
function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDocument) {
// TODO(vojta): fix the signature
return function(method, url, post, callback, headers, timeout, withCredentials, responseType) {
var status, internalTimeout;
$browser.$$incOutstandingRequestCount();
url = url || $browser.url();

Expand Down Expand Up @@ -112,10 +113,12 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
xhr.send(post || null);
}

if (timeout > 0) {
var timeoutId = $browserDefer(timeoutRequest, timeout);
} else if (isPromiseLike(timeout)) {
timeout.then(timeoutRequest);
internalTimeout = angular.isFunction(timeout) ? timeout() : timeout;

if (internalTimeout > 0) {
var timeoutId = $browserDefer(timeoutRequest, internalTimeout);
} else if (internalTimeout && internalTimeout.then) {
internalTimeout.then(timeoutRequest);
}


Expand Down
6 changes: 4 additions & 2 deletions src/ngResource/resource.js
Expand Up @@ -150,8 +150,10 @@ function shallowClearAndCopy(src, dst) {
* GET request, otherwise if a cache instance built with
* {@link ng.$cacheFactory $cacheFactory}, this cache will be used for
* caching.
* - **`timeout`** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} that
* should abort the request when resolved.
* - **`timeout`** – `{number|Promise|Function}` – timeout in milliseconds, or
* {@link ng.$q promise} that should abort the request when resolved or a function that returns
* either a number or a promise. The function allows for the timeout to change with time and
* provides a way to cancel an action multiple times (by providing a new promise each time).
* - **`withCredentials`** - `{boolean}` - whether to set the `withCredentials` flag on the
* XHR object. See
* [requests with credentials](https://developer.mozilla.org/en/http_access_control#section_5)
Expand Down
22 changes: 22 additions & 0 deletions test/helpers/matchers.js
Expand Up @@ -120,6 +120,28 @@ beforeEach(function() {
return this.actual.callCount == 1;
},

toHaveBeenCalledTwice: function() {
if (arguments.length > 0) {
throw new Error('toHaveBeenCalledTwice does not take arguments, use toHaveBeenCalledWith');
}

if (!jasmine.isSpy(this.actual)) {
throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.');
}

this.message = function() {
var msg = 'Expected spy ' + this.actual.identity + ' to have been called twice, but was ',
count = this.actual.callCount;
return [
count === 0 ? msg + 'never called.' :
msg + 'called ' + count + ' times.',
msg.replace('to have', 'not to have') + 'called once.'
];
};

return this.actual.callCount == 2;
},


toHaveBeenCalledOnceWith: function() {
var expectedArgs = jasmine.util.argsToArray(arguments);
Expand Down
139 changes: 139 additions & 0 deletions test/ng/httpBackendSpec.js
Expand Up @@ -181,6 +181,26 @@ describe('$httpBackend', function() {
expect(callback).toHaveBeenCalledOnce();
});

it('should abort request on timeout provided by function', function() {
callback.andCallFake(function(status, response) {
expect(status).toBe(-1);
});

$backend('GET', '/url', null, callback, {}, function() { return 2000; });
xhr = MockXhr.$$lastInstance;
spyOn(xhr, 'abort');

expect(fakeTimeout.delays[0]).toBe(2000);

fakeTimeout.flush();
expect(xhr.abort).toHaveBeenCalledOnce();

xhr.status = 0;
xhr.readyState = 4;
xhr.onreadystatechange();
expect(callback).toHaveBeenCalledOnce();
});


it('should abort request on timeout promise resolution', inject(function($timeout) {
callback.andCallFake(function(status, response) {
Expand All @@ -200,6 +220,57 @@ describe('$httpBackend', function() {
}));


it('should abort request on timeout promise resolution provided by function',
inject(function($timeout) {
callback.andCallFake(function(status, response) {
expect(status).toBe(-1);
});

$backend('GET', '/url', null, callback, {}, function() { return $timeout(noop, 2000); });
xhr = MockXhr.$$lastInstance;
spyOn(xhr, 'abort');

$timeout.flush();
expect(xhr.abort).toHaveBeenCalledOnce();

xhr.status = 0;
xhr.readyState = 4;
xhr.onreadystatechange();
expect(callback).toHaveBeenCalledOnce();
}));


it('should abort multiple request on timeout promise resolution provided by function',
inject(function($timeout) {
callback.andCallFake(function(status, response) {
expect(status).toBe(-1);
});

$backend('GET', '/url', null, callback, {}, function() { return $timeout(noop, 2000); });
xhr = MockXhr.$$lastInstance;
spyOn(xhr, 'abort');

$timeout.flush();
expect(xhr.abort).toHaveBeenCalledOnce();

xhr.status = 0;
xhr.readyState = 4;
xhr.onreadystatechange();

$backend('GET', '/url', null, callback, {}, function() { return $timeout(noop, 2000); });
xhr = MockXhr.$$lastInstance;
spyOn(xhr, 'abort');

$timeout.flush();
expect(xhr.abort).toHaveBeenCalledOnce();

xhr.status = 0;
xhr.readyState = 4;
xhr.onreadystatechange();
expect(callback).toHaveBeenCalledTwice();
}));


it('should not abort resolved request on timeout promise resolution', inject(function($timeout) {
callback.andCallFake(function(status, response) {
expect(status).toBe(200);
Expand All @@ -218,6 +289,26 @@ describe('$httpBackend', function() {
}));


it('should not abort resolved request on timeout promise resolution provided by function',
inject(function($timeout) {
callback.andCallFake(function(status, response) {
expect(status).toBe(200);
});

$backend('GET', '/url', null, callback, {}, function() { return $timeout(noop, 2000); });
xhr = MockXhr.$$lastInstance;
spyOn(xhr, 'abort');

xhr.status = 200;
xhr.readyState = 4;
xhr.onreadystatechange();
expect(callback).toHaveBeenCalledOnce();

$timeout.flush();
expect(xhr.abort).not.toHaveBeenCalled();
}));


it('should cancel timeout on completion', function() {
callback.andCallFake(function(status, response) {
expect(status).toBe(200);
Expand All @@ -238,6 +329,54 @@ describe('$httpBackend', function() {
});


it('should cancel timeout provided by function on completion', function() {
callback.andCallFake(function(status, response) {
expect(status).toBe(200);
});

$backend('GET', '/url', null, callback, {}, function() { return 2000; });
xhr = MockXhr.$$lastInstance;
spyOn(xhr, 'abort');

expect(fakeTimeout.delays[0]).toBe(2000);

xhr.status = 200;
xhr.readyState = 4;
xhr.onreadystatechange();
expect(callback).toHaveBeenCalledOnce();

expect(fakeTimeout.delays.length).toBe(0);
expect(xhr.abort).not.toHaveBeenCalled();
});


it('should register onreadystatechange callback before sending', function() {
// send() in IE6, IE7 is sync when serving from cache
function SyncXhr() {
xhr = this;
this.open = this.setRequestHeader = noop;

this.send = function() {
this.status = 200;
this.responseText = 'response';
this.readyState = 4;
this.onreadystatechange();
};

this.getAllResponseHeaders = valueFn('');
}

callback.andCallFake(function(status, response) {
expect(status).toBe(200);
expect(response).toBe('response');
});

$backend = createHttpBackend($browser, function() { return new SyncXhr(); });
$backend('GET', '/url', null, callback);
expect(callback).toHaveBeenCalledOnce();
});


it('should set withCredentials', function() {
$backend('GET', '/some.url', null, callback, {}, null, true);
expect(MockXhr.$$lastInstance.withCredentials).toBe(true);
Expand Down
55 changes: 55 additions & 0 deletions test/ngResource/resourceSpec.js
Expand Up @@ -423,6 +423,61 @@ describe("resource", function() {
});


it("should not perform action due to timeout", inject(function($q) {
$httpBackend.when('GET').respond({value: 123});
var deferred = $q.defer();
var R = $resource('/Path', {param: null}, {get: {method: 'GET', timeout: deferred.promise}});
R.get({}, callback);
deferred.resolve();
expect(callback).not.toHaveBeenCalled();
}));


it("should not perform action due to timeout provided by function", inject(function($q) {
$httpBackend.when('GET').respond({value: 123});
var deferred = $q.defer();
var R = $resource('/Path', {param: null}, {get: {method: 'GET',
timeout: function() { return deferred.promise; }}});
R.get({}, callback);
deferred.resolve();
expect(callback).not.toHaveBeenCalled();
}));


it("should not perform first action due to timeout", inject(function($q) {
$httpBackend.when('GET').respond({value: 123});
var deferred = $q.defer();

var R = $resource('/Path', {param: null}, {get: {method: 'GET',
timeout: function() { return deferred.promise; }}});
R.get({}, callback);
deferred.resolve();
expect(callback).not.toHaveBeenCalled();

deferred = $q.defer();
R.get({}, callback);
$httpBackend.flush();
expect(callback).toHaveBeenCalled();
}));


it("should not perform both actions due to timeout", inject(function($q) {
$httpBackend.when('GET').respond({value: 123});
var deferred = $q.defer();

var R = $resource('/Path', {param: null}, {get: {method: 'GET',
timeout: function() { return deferred.promise; }}});
R.get({}, callback);
deferred.resolve();
expect(callback).not.toHaveBeenCalled();

deferred = $q.defer();
deferred.resolve();
R.get({}, callback);
expect(callback).not.toHaveBeenCalled();
}));


it("should read resource", function() {
$httpBackend.expect('GET', '/CreditCard/123').respond({id: 123, number: '9876'});
var cc = CreditCard.get({id: 123}, callback);
Expand Down

0 comments on commit 37a6031

Please sign in to comment.