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

feat($http): add promise.cancel support #2523

Closed
wants to merge 4 commits 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
42 changes: 35 additions & 7 deletions src/ng/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,7 @@ function $HttpProvider() {
headers[(config.xsrfHeaderName || defaults.xsrfHeaderName)] = xsrfValue;
}


var requestPromise;
var serverRequest = function(config) {
var reqData = transformData(config.data, headersGetter(headers), config.transformRequest);

Expand All @@ -678,11 +678,15 @@ function $HttpProvider() {
}

// send request
return sendReq(config, reqData, headers).then(transformResponse, transformResponse);
requestPromise = sendReq(config, reqData, headers);
return requestPromise.then(transformResponse, transformResponse);
};

var chain = [serverRequest, undefined];
var promise = $q.when(config);
var canceled, promise = $q.when(config).then(function(request) {
// handle case where canceled before next digest cycle
return canceled ? $q.reject(request) : request;
});

// apply interceptors
forEach(reversedInterceptors, function(interceptor) {
Expand All @@ -699,7 +703,26 @@ function $HttpProvider() {
var rejectFn = chain.shift();

promise = promise.then(thenFn, rejectFn);
};
}

var defer = $q.defer(function() {
canceled = true;
if (!requestPromise) return {
data: null,
status: 0,
headers: headersGetter({}),
config: config
};
requestPromise.cancel();
});

promise.then(function(response) {
defer.resolve(response);
}, function(response) {
defer.reject(response);
});

promise = defer.promise;

promise.success = function(fn) {
promise.then(function(response) {
Expand Down Expand Up @@ -862,7 +885,12 @@ function $HttpProvider() {
* $httpBackend, defaults, $log, $rootScope, defaultCache, $http.pendingRequests
*/
function sendReq(config, reqData, reqHeaders) {
var deferred = $q.defer(),
var deferred = $q.defer(function() {
canceled = true;
cancelReq && cancelReq();
}),
canceled,
cancelReq,
promise = deferred.promise,
cache,
cachedResp,
Expand Down Expand Up @@ -901,7 +929,7 @@ function $HttpProvider() {

// if we won't have the response in cache, send the request to the backend
if (!cachedResp) {
$httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout,
cancelReq = $httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout,
config.withCredentials, config.responseType);
}

Expand All @@ -925,7 +953,7 @@ function $HttpProvider() {
}

resolvePromise(response, status, headersString);
$rootScope.$apply();
!canceled && $rootScope.$apply();
}


Expand Down
38 changes: 23 additions & 15 deletions src/ng/httpBackend.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ function $HttpBackendProvider() {
function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument, locationProtocol) {
// TODO(vojta): fix the signature
return function(method, url, post, callback, headers, timeout, withCredentials, responseType) {
var status;
$browser.$$incOutstandingRequestCount();
url = url || $browser.url();

Expand All @@ -42,13 +43,11 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument,
callbacks[callbackId].data = data;
};

jsonpReq(url.replace('JSON_CALLBACK', 'angular.callbacks.' + callbackId),
var jsonpDone = jsonpReq(url.replace('JSON_CALLBACK', 'angular.callbacks.' + callbackId),
function() {
if (callbacks[callbackId].data) {
completeRequest(callback, 200, callbacks[callbackId].data);
} else {
completeRequest(callback, -2);
}
var data = callbacks[callbackId].data || undefined;
status = status || (data ? 200 : -2);
completeRequest(callback, status, data);
delete callbacks[callbackId];
});
} else {
Expand All @@ -58,8 +57,6 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument,
if (value) xhr.setRequestHeader(key, value);
});

var status;

// In IE6 and 7, this might be called synchronously when xhr.send below is called and the
// response is in the cache. the promise api will ensure that to the app code the api is
// always async
Expand Down Expand Up @@ -90,7 +87,7 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument,
// responseText is the old-school way of retrieving response (supported by IE8 & 9)
// response and responseType properties were introduced in XHR Level2 spec (supported by IE10)
completeRequest(callback,
status || xhr.status,
status = status || xhr.status,
(xhr.responseType ? xhr.response : xhr.responseText),
responseHeaders);
}
Expand All @@ -105,20 +102,30 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument,
}

xhr.send(post || '');
}

if (timeout > 0) {
$browserDefer(function() {
status = -1;
xhr.abort();
}, timeout);
}
if (timeout > 0) {
var timeoutId = $browserDefer(cancelRequest, timeout);
}

return cancelRequest;


function cancelRequest() {
if (!status) {
status = -1;
jsonpDone && jsonpDone();
xhr && xhr.abort();
}
}

function completeRequest(callback, status, response, headersString) {
// URL_MATCH is defined in src/service/location.js
var protocol = (url.match(SERVER_MATCH) || ['', locationProtocol])[1];

// cancel timeout
timeoutId && $browserDefer.cancel(timeoutId);

// fix status code for file protocol (it's always 0)
status = (protocol == 'file') ? (response ? 200 : 404) : status;

Expand Down Expand Up @@ -152,5 +159,6 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument,
}

rawDocument.body.appendChild(script);
return doneWrapper;
}
}
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
45 changes: 31 additions & 14 deletions src/ngMock/angular-mocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -916,7 +916,9 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
function $httpBackend(method, url, data, callback, headers) {
var xhr = new MockXhr(),
expectation = expectations[0],
wasExpected = false;
wasExpected = false,
complete,
timeoutId;

function prettyPrint(data) {
return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp)
Expand All @@ -937,12 +939,8 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
expectations.shift();

if (expectation.response) {
responses.push(function() {
var response = expectation.response(method, url, data, headers);
xhr.$$respHeaders = response[2];
callback(response[0], response[1], xhr.getAllResponseHeaders());
});
return;
responses.push(completeRequest(expectation));
return cancelRequest;
}
wasExpected = true;
}
Expand All @@ -952,21 +950,40 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
if (definition.match(method, url, data, headers || {})) {
if (definition.response) {
// if $browser specified, we do auto flush all requests
($browser ? $browser.defer : responsesPush)(function() {
var response = definition.response(method, url, data, headers);
xhr.$$respHeaders = response[2];
callback(response[0], response[1], xhr.getAllResponseHeaders());
});
timeoutId = ($browser ? $browser.defer : responsesPush)(completeRequest(definition));
return cancelRequest;
} else if (definition.passThrough) {
$delegate(method, url, data, callback, headers);
return $delegate(method, url, data, callback, headers);
} else throw Error('No response defined !');
return;
}
}
throw wasExpected ?
Error('No response defined !') :
Error('Unexpected request: ' + method + ' ' + url + '\n' +
(expectation ? 'Expected ' + expectation : 'No more request expected'));

function cancelRequest() {
if (complete) {
if ($browser && timeoutId) {
$browser.defer.cancel(timeoutId);
} else {
for (var i = 0, l = responses.length; i < l; i++) {
if (complete === responses[i]) responses.splice(i, 1);
}
}
callback(-1, null, null);
complete = null;
}
}

function completeRequest(request) {
return complete = function() {
var response = request.response(method, url, data, headers);
xhr.$$respHeaders = response[2];
callback(response[0], response[1], xhr.getAllResponseHeaders());
complete = null;
};
}
}

/**
Expand Down
Loading