0

I have a cloud function that is triggered when a sale/purchase is committed into firestore. This function's purpose is to update the inventory level centrally.

The function works just fine if I'm updating an item's inventory at only 1 warehouse, but doing so for multiple warehouses has unexpected behavior. I'm looping through all the warehouses that are affected to calculate the total inventory level changes, and every iteration kicks-off a javascript promise.

The problem seems to occur with the way the promises are invoked. E.g: if I want to update 3 warehouses and loop 3 times, somehow 5 promises are being kicked-off. This is visible through the logs. I've researched similar questions here, but the solutions were suggested while firestore was still in beta and might not be the right way forward. (Firestore transactions getting triggered multiple times resulting in wrong data)

Here is the code

export const onTransactionCreate = functions.firestore
    .document('/companies/{companyId}/sub_transactions/{transId}')
    .onCreate(async (snapshot, context) => {
        const transId = context.params.transId
        
        const stock_transaction: IStockTransaction = <IStockTransaction>snapshot.data()
        const trans_type: TRANS_TYPE = stock_transaction.trans_type

        const promises: any[] = []

        stock_transaction.lineItems.forEach((element, index) => {
            const ITEM_GUID = element.item_guid

            const is_increasing = isIncreasingTransaction(element.line_trans_type)
            const delta_stock = element.qty_transaction * (is_increasing ? 1 : -1)

            const TARGET_BRANCH_ID = element.target_branch_guid
            
            const itemRef = db.collection(FIRESTORE_PATHS.COL_COMPANIES).doc(companyId).
                collection(FIRESTORE_PATHS.SUB_COMPANIES_ITEMS).
                doc("" + ITEM_GUID)

            const item_promise = db.runTransaction(async t => {
                try {
                    const item_doc = await t.get(itemRef)

                    const item_branch_quantities: IBranchQuantity[] = (item_doc.data()!.branch_quantities || new Array())
                    const item_branch_ids: string[] = (item_doc.data()!.available_branch_ids || new Array())

                    const branch_index = item_branch_ids.indexOf(TARGET_BRANCH_ID)
                    console.log(`${transId} Line Item ${index}, after document.get(), search branch index: ${branch_index}`)
                    if (branch_index !== -1) {
                        const prev_qty = item_branch_quantities[branch_index]
                        const updated_qty = prev_qty.quantity + delta_stock
                        item_branch_quantities[branch_index] = {
                            item_guid: prev_qty.item_guid,
                            branch_guid: prev_qty.branch_guid,
                            quantity: updated_qty
                        }
                        console.log(`${transId} Line Item ${index} Updating qty @ item ${delta_stock}, prev qty ${prev_qty.quantity}`)
                    } else {
                        item_branch_ids.push(TARGET_BRANCH_ID)
                        item_branch_quantities.push({
                            item_guid: element.item_guid,
                            branch_guid: TARGET_BRANCH_ID,
                            quantity: delta_stock
                        })
                        console.log(`${transId} Line Item ${index} Adding qty @ item ${delta_stock}`)
                    }
                    
                    t.update(itemRef, {
                        branch_quantities: item_branch_quantities,
                        available_branch_ids: item_branch_ids
                    })
                } catch (err) {
                    throw new Error(err)
                }
            })
            promises.push(item_promise)

        });

        return Promise.all(promises)
    })

Log message for when the loop has 3 elements but is invoked 5 times

fuadj
  • 434
  • 4
  • 10

1 Answers1

0

we have found the solution by reading this article.

A transaction consists of any number of get() operations followed by any number of write operations such as set(), update(), or delete(). In the case of a concurrent edit, Cloud Firestore runs the entire transaction again. For example, if a transaction reads documents and another client modifies any of those documents, Cloud Firestore retries the transaction. This feature ensures that the transaction runs on up-to-date and consistent data.

        lineItems.forEach(element => {
            const delta_transaction = element.qty * (isLineTransIncrease(element.line_trans_type) ? 1 : -1)
            const itemRef = db.collection('companies').doc(companyId).collection('sub_items').doc("" + element.item_guid)

            const p = db.runTransaction(t => {
                return t.get(itemRef)
                    .then(doc => {
                        let item_branch_quantities: IBranchQuantity[] = doc.data()!.branch_quantities
                        let item_branch_ids: string[] = doc.data()!.available_branch_ids

                        if (!item_branch_quantities)
                            item_branch_quantities = new Array()
                        if (!item_branch_ids)
                            item_branch_ids = new Array()

                        const branch_index = item_branch_ids.indexOf(current_branch_id)
                        if (branch_index !== -1) {
                            const prev_qty = item_branch_quantities[branch_index]
                            const updated_qty: number = prev_qty.quantity + delta_transaction
                            item_branch_quantities[branch_index] = {
                                item_guid: prev_qty.item_guid,
                                branch_guid: prev_qty.branch_guid,
                                quantity: updated_qty
                            }
                        } else {
                            item_branch_ids.push(current_branch_id)
                            item_branch_quantities.push({
                                item_guid: element.item_guid,
                                branch_guid: current_branch_id,
                                quantity: delta_transaction
                            })
                        }
                        t.update(itemRef, {
                            branch_quantities: item_branch_quantities,
                            branch_ids: item_branch_ids
                        })
                    })
            })
            item_update_transactions.push(p)
        });

        return Promise.all(item_update_transactions)
    })

function isLineTransIncrease(line_trans: number): boolean {
    return (line_trans === 1) || (line_trans === 2)
}
Ulfe
  • 26
  • 2
  • Sorry, I don't understand what the cause of the multiple re-runs. Instead of just sharing code snippets and expecting me to understand it, can you also explain the reasoning why it is implemented like that? – fuadj Jul 08 '20 at 08:56