0

I'm struggling to send multiple AJAX calls in the following task. The API returns takes two parameters: userId and offsetValue and returns last 10 messages for the specified user, starting from specified offset. If the offset is more than the total count of messages for the user, API returns an empty string.

I wrote a function that returns an individual promise to get 10 messages for specified userId and offsetValue.

function getMessages(userId, offsetValue) {
    return new Promise(function (resolve, reject) {
        $.ajax(
        {
            url: 'https://example.com/api.php',
            type: 'POST',
            data: {
                action: 'get_messages',
                offset: offsetValue,
                user: userId
            },
            success: function (response) {
                if (response != '') {
                    resolve(response);
                } else {
                    reject(response);
                }
            },
            error: function (response) {
                reject(response);
            }
        });
    });
}

I need to run parallel tasks using .all() for multiple userId, however I cannot run parallel subtasks for each userId (incrementing offsetValue by 10 each time) as I don't know in advance how many messages does each user have, so the execution should stop when first individual promise is rejected (i.e. offsetValue is more than total messages count). Something like this:

var messages = '';

getMessages('Alex', 0)
    .then(function(result) {
        messages += result;

        getMessages('Alex', 10);
            .then(function(result) {
                messages += result;

                getMessages('Alex', 20)
                ....
            });
    });

So, is there any way to run sequental promises with iterating parameter sequentially one by one and resolve the overall concatenated result upon first reject?

ttaaoossuuuu
  • 7,786
  • 3
  • 28
  • 58

1 Answers1

5

First off, you want to avoid the promise anti-pattern where you unnecessarily wrap your code in a new promise when $.ajax() already returns a promise that you can just use. To fix that, you would change to this:

// retrieves block of messages starting with offsetValue
// resolved response will be empty if there are no more messages
function getMessages(userId, offsetValue) {
    return $.ajax({
        url: 'https://example.com/api.php',
        type: 'POST',
        data: {
            action: 'get_messages',
            offset: offsetValue,
            user: userId
        }
    });
}

Now, for your main question. Given that you want to stop requesting new items when you get a rejection or empty response and you don't know how many requests there will be in advance, you pretty much have to request things serially and stop requesting the next one when you get an empty response or an error. The key to doing that is to chain sequential promises together by returning a new promise from a .then() handler.

You can do that like this:

function getAllMessagesForUser(userId) {
    var offsetValue = 0;
    var results = [];

    function next() {
        return getMessages(userId, offsetValue).then(function(response) {
            // if response not empty, continue getting more messages
            if (response !== '') {
                // assumes API is returning 10 results at a time
                offsetValue += 10;
                results.push(response);
                // chain next request promise onto prior promise
                return next();
            } else {
                // empty response means we're done retrieving messages
                // so just return results accumulated so far
                return results.join("");
            }
        });
    }
    return next();
}

This creates an internal function that returns a promise and each time it gets some messages, it chains a new promise onto the original promise. So, getAllMessagesForUser() returns a single promise that resolves with all the messages it has retrieved or rejects with an error.

You would use it like this:

getAllMessagesForUser('Bob').then(function(messages) {
    // got all messages here
}, function(err) {
    // error here
});

You could parallelize multiple users (as long as you're sure you're not overloading the server or running into a rate limiting issue) like this:

$.when.apply($, ['Bob', 'Alice', 'Ted'].map(function(item) {
    return getAllMessagesForUser(item);
})).then(function() {
    // get results into a normal array
    var results = Array.prototype.slice.call(arguments);
});

P.S. Promise.all() is much nicer to use than $.when() (since it takes an array and resolves to an array), but since there are already jQuery promises involved here and I didn't know your browser compatibility requirements, I stuck with jQuery promise management rather than ES6 standard promises.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • +1. For switching to standard promises, see [here](http://stackoverflow.com/a/31327725/1048572). – Bergi Dec 06 '16 at 01:55