monstro monstro - 2 months ago 12
TypeScript Question

Chaining AngularJS promises - specific case

in my AngularJS TypeScript code, I call GmailService service from my MainController. I need to retrieve data necessary for me to compose a list of gmail messages of this simple format (all in TypeScript AngularJS):

export interface GmailMessage {
from: string;
subject: string;
body: string;
}


and return this collection to the controller as a promise. So I have written this function in GmailService service to return such collection:

getGmailMessages(): any {
const deferred = this.$q.defer();
let gmailMessages: GmailMessage[] = [];
console.log('getGmailMessages');
gapi.client["gmail"].users.threads.list({ userId: 'me', labelIds: ['INBOX', 'UNREAD'] }).then(response => {
let threads = response.result.threads;
angular.forEach(threads, thread => {
gapi.client["gmail"].users.messages.get({ userId: 'me', id: thread.id }).then(response => {
let message = response.result;
var gmailMessage = {
from: message.payload.headers[17].value,
subject: message.payload.headers[21].value,
body: thread.snippet
};
gmailMessages.push(gmailMessage);
});
});

deferred.resolve(gmailMessages);
});

return deferred.promise;
}


I understand that I am missing some understanding of how this should be accomplished. To get required data I am using Google gmail API. The point is to make a call to retrieve Gmail threads, threads contain one important property called snippets, that I want to assign to each of my GmailMessage.body property (for display), the trick is that for each thread object received as a result of prmis call, I need to make individual requests for one message in threads collection (the last one) The logic is this, after retrieving a list of threads, threadId is equal to message.id and can be used a parameter passed to get info about the message (from, subject, etc...) Here I am using forEach loop, which is not working properly in the concept of promises, I know about promise chaining, but in this situation, for one call to get threads I need to make multiple calls to get message info for each thread. I dont understand how to accomplish it properly using promises. I tried to use $q.all(promises) to collect all the promises, but I just cant understand how to handle all that. My brain is exploding of misunderstanding of how this whole thing should work. At the end, all I want is to return a collection of GmailMessage objects, where each one would contain 3 properties populated from:

var gmailMessage = {
from: message.payload.headers[17].value,
subject: message.payload.headers[21].value,
body: thread.snippet
};


17 is the index of "From"
21 is the index of "Subject"

Could anyone clarify this situation please? What should be the most correct way to handle this? For instance

deferred.resolve(gmailMessages);

is not in the right place and is called immediately before the result comes back from the server.

If someone has written anything like that in AngularJS / TypeScript / Google Gmail API, please let me know if you have an idea of how to implement the logic of combining multiple promises and handling them when they all are completed.

Thanks.

Answer Source

Yeah chaining promises in loops gets a little complicated. There are 2 ways to do it: in parallel and in series. In this situation you probably want to send the requests in parallel, since the outcome of one request does not affect the next request.

First of all, you should rarely need to use $q.defer() and I think this case is no exception. You should be able to do this purely with promise chaining (for parallel or series).

It sounds like you're on the right track for sending requests in parallel - you just need to store an array of Promise<GmailMessage>[] instead of GmailMessage[], then call $q.all on that array. e.g.

getGmailMessages(): gapi.client.Request<GmailMessage[]> {
    return gapi.client.gmail.users.threads.list({ userId: 'me', labelIds: ['INBOX', 'UNREAD'] }).then(response => {
        const requests: gapi.client.Request<GmailMessage>[] = response.result.threads.map(thread => {
            return gapi.client.gmail.users.messages.get({ userId: 'me', id: thread.id }).then(response => {
                let message = response.result;
                return {
                    from: message.payload.headers[17].value,
                    subject: message.payload.headers[21].value,
                    body: thread.snippet
                };
            });
        });

        return this.$q.all(requests);
    });
}

What I'm doing here is iterating using map instead of forEach - that maps each thread into a gapi.client.Request<GmailMessage> (which is similar to ng.IPromise<GmailMessage>, but using Google's promise object instead). Then I wait for all promises to return using $q.all. I also replaced gapi.client["gmail"] with gapi.client.gmail - there should be no reason to use the former syntax if you're using Typescript >= 2.0.

This is assuming, of course, that the gapi.client.Request promise is compatible with ng.IPromise. If not, you'll need to write a utility function to convert from one promise type to another (probably using $q.defer()).