Chained jQuery promises with abort

By | January 12, 2018
Questions:

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?

Answers:

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};
}

Questions:
Answers:

My 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;

Questions:
Answers:

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.

Questions:
Answers:

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.

Questions:
Answers:

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.

Leave a Reply

Your email address will not be published. Required fields are marked *