Reputation: 3108
I'm currently writing API code which, several layers deep, wraps $.ajax() calls.
One requirement is that the user must be able to cancel any request (if it's taking too long, for example).
Normally this is accomplished via something simple, like:
var jqXHR = $.ajax(..);
$(mycancelitem).click(function () {
jqXHR.abort();
});
However my code looks more like this:
function myapicall() {
var jqxhr = $.ajax(…);
var prms = def.then(function (result) {
// modify the result here
return result + 5;
});
return prms;
}
The problem here is someone calling myapicall()
only gets a jQuery.Promise with no way to abort it. And while the sample above is very simple, in my actual code there are several layers of chaining, in many places.
Is there a solution to this?
Upvotes: 8
Views: 6400
Reputation: 1183
I worked around this using a set of three helpers. It's pretty much manual and you must pay attention to really not forget to use the helpers in your API's implementation, or else well... abort won't abort.
function abort(prom) {
if( prom["abort"] ) {
prom["abort"]();
} else {
throw new Error("Not abortable");
}
}
function transfer_abort_from_one(abortable, other) {
other["abort"] = function() { abortable.abort() };
}
function transfer_abort_from_many(abortables, other) {
other["abort"] = function() {
for(let p of abortables) {
if(p["abort"]) {
p["abort"]();
}
}
};
}
Now
function api_call() {
let xhr = $.ajax(...);
let def = $.Deferred();
// along these lines
xhr.fail( def.reject );
xhr.done( def.resolve );
// then
let prom = def.promise();
transfer_abort_from_one(xhr, prom);
return prom;
}
function api_call2() {
let xhrs = [$.ajax(...), $.ajax(...)];
let prom = $.when.apply(null, xhrs);
transfer_abort_from_many(xhrs, prom);
return prom;
}
Using your API and aborting:
var api_prom = api_call();
abort(api_prom);
var api_prom2 = api_call2();
abort(api_prom2);
Note that depending on how your API is built, you must not forget to transfer the abort from the lower layers promises to the higher layer promises. All this is error-prone. It would be a lot better if JQuery (for example) did the transfer when when()
or then()
get called.
Upvotes: 0
Reputation: 128
Looked for solution to similar problem and solved it like this:
var xhr = $.ajax({ });
return xhr.then(function (res) {
var processed = processResponse(res);
return $.Deferred().resolve(processed).promise();
}).promise(xhr); // this is it. Extends xhr with new promise
This will return standard jqXHR object with abort and such + all promise functions.
Note that you may or may not want to return .promise(xhr) from .then() as well. Depends on how you want to treat response in .done() functions from API.
Upvotes: 1
Reputation: 724
My solution inspired from all the
client.js
var self = this;
function search() {
if (self.xhr && self.xhr.state() === 'pending') self.xhr.abort();
var self.xhr = api.flights(params); // Store xhr request for possibility to abort it in next search() call
self.xhr.then(function(res) {
// Show results
});
}
// repeatedly call search()
api.js
var deferred = new $.Deferred();
var xhr = $.ajax({ });
xhr.then(function (res) { // Important, do not assign and call .then on same line
var processed = processResponse(res);
deferred.resolve(processed);
});
var promise = deferred.promise();
promise.abort = function() {
xhr.abort();
deferred.reject();
};
return promise;
client.js
var self = this;
function search() {
if (self.xhr && self.xhr.isPending()) self.xhr.abort();
var self.xhr = api.flights(params); // Store xhr request for possibility to abort it in next search() call
self.xhr.then(function(res) {
// Show results
});
}
// repeatedly call search()
api.js
var deferred = Q.defer();
var xhr = $.ajax({ });
xhr.then(function (res) { // Important, do not assign and call .then on same line
var processed = processResponse(res);
deferred.resolve(processed);
});
deferred.promise.abort = function() {
xhr.abort();
deferred.reject();
};
return deferred.promise;
Upvotes: 4
Reputation: 1395
Basically, you have you make your own promise which will represent the entire operation and add a special abort function to it. Something like the following:
function myapicall() {
var currentAjax = $.ajax({ ... })
.then(function(data) {
...
return currentAjax = $.ajax({ ... });
},
function(reason) { wrapper.reject(reason); })
.then(...)
.then(...)
.then(...)
.then(...)
.then(...)
.then(...)
.then(...)
.then(...)
.then(...)
.then(...)
.then(function(data) {
...
wrapper.resolve(data);
},
function(reason) { wrapper.reject(reason); });
// haven't used jQuery promises, not sure if this is right
var wrapper = new $.Deferred();
wrapper.promise.abort = function() {
currentAjax.abort();
wrapper.reject('aborted');
};
return wrapper.promise;
}
This pattern (updating the currentAjax
variable) must be continued at each stage of the $.ajax
chain. In the last AJAX call, where everything has finally been loaded, you will resolve the wrapper promise with whatever data you wish.
Upvotes: 1
Reputation:
you could return a object that has both the jqXHR and promise
function myapicall() {
var jqXHR = $.ajax(..);
var promise = jqXHR.then(function (result) {
// modify the result here
return result + 5;
});
return {jqXHR:jqXHR,promise:promise};
}
Upvotes: 3