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 acplished 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?
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 acplished 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?
Share Improve this question edited Feb 13, 2014 at 23:22 Bergi 665k161 gold badges1k silver badges1.5k bronze badges asked Feb 13, 2014 at 21:59 automatonautomaton 3,1285 gold badges30 silver badges40 bronze badges 7-
myapicall()
returns the jqXHR object. Why wouldn'tvar api = myapicall(); api.abort();
work? Also, I don't think yourreturn result + 5;
does anything. – gen_Eric Commented Feb 13, 2014 at 22:03 - 1 the line def = def.then(...) means it's a chained promise. if you try it in code, you'll see what you get back is not a jqXHR, but rather a real jQuery.Promise, without abort. it doesn't return the jqXHR from the ajax request, but the chained promise. – automaton Commented Feb 13, 2014 at 22:06
-
Ah! I didn't notice that. What if you removed the
def=
beforedef.then()
? Would that work? – gen_Eric Commented Feb 13, 2014 at 22:09 - 1 A solution would be to use a proper Promise library that does support cancellation and can assimilate jQuery deferreds. Unfortunately, I can't name one yet; but maybe you have more luck at searching. – Bergi Commented Feb 13, 2014 at 23:32
- 1 Hi @automaton, any news with this question? I stumbled at same problem – Pavel 'Strajk' Dolecek Commented Nov 25, 2014 at 9:37
5 Answers
Reset to default 4My solution inspired from all the
With jQuery promises
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;
With Q + jQuery promises
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;
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};
}
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.
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.
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.