0

I have a shopping cart system where order of requests is quite important. The system has to be able to create users before it adds items to their user account. To do this I'm using promises and I'm trying to chain them so that they appear one after the other. Maybe my understanding is flawed but I'm not getting this to work.

processUsers function. Returns a promise. The order of this function is important as well because we need to create all users accounts in the order before we start connecting them together (there is a link between a child and parents in this system)

this.createUsers and this.createUserRelations are both functions that return a list of Promises

    processUsers(child, parents) {
        return new Promise((resolve, reject) => {
            Promise.all(
                this.createUsers(child, parents)
            ).then((userResponse) => {
                Promise.all(
                    this.createUserRelations(
                        child,
                        parents
                    )
                ).then((relationResponse) => {
                    resolve(relationResponse)
                }).catch(reject)
            }).catch(reject)
        })
    }

This works. The order of this is correct. I'm testing this by making the create_or_update function on the server sleep for 5 seconds and indeed the createUserRelations function waits for that.

After I've created the users I use the same logic to add items to each user

    /**
    * process a single ticket
    * @param {Array} ticket
    */
    processTicket(ticket) {
        var self = this
        return new Promise((resolve, reject) => {
            var ticketUser = {}
            const userPromises = ticket.filter(
                (t) => t.item.item_type === ITEM_TYPE_TICKET
            ).map((m) => {
                ticketUser = m.user
                return self.processUsers(m.user, m.parents)
            })
            const itemPromises = ticket.map(
                (t) => {
                    if(t.item.item_type === ITEM_TYPE_BUS) {
                        t.user = ticketUser
                    }
                    return self.processItem(t)
                }
            )

            Promise.all(userPromises).then((data) => {
                Promise.all(itemPromises).then((data) => {
                    resolve(data)
                }).catch(reject)
            }).catch(reject)
        })
    }

This doesn't work. The itemPromises doesn't wait for userPromises to finish and therefor I get an error because the server cannot find the user to link the item with. I know that Promise.all() doesn't run promises in serial but I thought it would start running userPromises and once they are resolved it would run itemPromises. It seems it does not do this. I've tried multiple other things such as using p-queue.

Here is the processItem function

    processItem(item) {
        // returns a Promise 
        return users.add_order_item(
            this.sessionUser.id,
            item.user.email,
            item.item.id,
            item.delivery
        )
    }

And finally the main function handling tickets for the entire order

    processOrder() {
        const items = this.orderSessionItems
        const reduced = this.groupBy(
            items, (i) => i.reference_number)
        var self = this
        const promises = Object.keys(reduced).map((key, index) => {
            return self.processTicket(reduced[key])
        })

        return Promise.all(promises)
    }

UPDATE: Turns out I was indeed misunderstanding how Promises work. When mapping over the list (two times) in processTicket the processItem promise is invoked immediately. I thought this was not the case but it's called before I do Promise.all().

What I ended up with is this

    processTicket(ticket) {
        return new Promise((resolve, reject) => {
            var self = this
            var ticketUser = {}
            const userPromises = ticket.filter(
                (t) => t.item.item_type === ITEM_TYPE_TICKET
            ).map((m) => {
                ticketUser = m.user
                return self.processUsers(m.user, m.parents)
            })

            Promise.all(userPromises).then(() => {
                const itemPromises = ticket.map(
                    (t) => {
                        if(t.item.item_type === ITEM_TYPE_BUS) {
                            t.user = ticketUser
                        }
                        return self.processItem(t)
                    }
                )
                Promise.all(itemPromises)
                       .then((data) => resolve(data))
                       .catch(reject)
            }).catch(reject)
        })
    }

and now it works!

Donna
  • 657
  • 1
  • 8
  • 18
  • 2
    You should rarely need to `new` up promises like that: the first function should just be `return Promise.all(...).then(() => Promise.all(...)).catch(reject)`; if you return a promise from a callback it becomes part of the chain. – jonrsharpe Apr 07 '19 at 10:20
  • @jonrsharpe yeah that's what I thought. However the order of the processUsers function does not work if I do this. Promise.all(something).then(Promise.all(somethingElse)). Do I need to pass it the anonymous function first like you do? Promise.all(something).then(() => Promise.all()).catch(reject) – Donna Apr 07 '19 at 10:24
  • 1
    Yes, otherwise it won't work as `.then` expects a callback, not a promise (that callback can return a promise for sure) – Jonas Wilms Apr 07 '19 at 10:25
  • 1
    In your example you're passing the second promise.all *as the callback function* to the first, which doesn't make sense; it needs to be *returned from* a callback function, like the anonymous arrow function in my example. – jonrsharpe Apr 07 '19 at 10:25
  • Ahh ok of course. My bad. Thank you so much for pointing this out! – Donna Apr 07 '19 at 10:26

1 Answers1

2

I know that Promise.all() doesn't run promises in serial but I thought it would start running userPromises and once they are resolved it would run itemPromises.

No, promises aren't being "run", Promise.all doesn't "start" anything. A promise is just something that you can wait for, and Promise.all combines multiple of these things into one promise that you can wait for.

The work is getting started when you call processItem(), and you do call that immediately. If you do the calls inside the then callback, it will wait for the userPromises before it starts to process the items.

Btw, also avoid the Promise constructor antipattern:

processTicket(ticket) {
    var ticketUser = {}
    const userPromises = ticket.filter((t) =>
        t.item.item_type === ITEM_TYPE_TICKET
    ).map((m) => {
        ticketUser = m.user
        return this.processUsers(m.user, m.parents)
    })
    return Promise.all(userPromises).then(() => {
//  ^^^^^^
        const itemPromises = ticket.map((t) => {
            if(t.item.item_type === ITEM_TYPE_BUS) {
                t.user = ticketUser
            }
            return this.processItem(t)
        })
        return Promise.all(itemPromises)
//      ^^^^^^
    })
}
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • absolutely you are correct. I was misunderstanding I thought that the Promise itself would not be invoked until I called "then" or until I used it in Promise.all but processItem runs as soon as I call it. – Donna Apr 07 '19 at 12:17