0

I use the following Cloud Function:

exports.likePost = functions.https.onCall(async (data, context) => {

    // ...

    const db = admin.database();

    // check if post is already liked 
    const likeInteractionRef = db.ref(...); 
    const snapAlreadyLiked = await likeInteractionRef.once("value"); 
    const alreadyLiked = snapAlreadyLiked.exists();

    if (alreadyLiked) { 
        // remove the like 
        await likeInteractionRef.remove();

        // decrease post's like count await 
        likeCountRef.set(admin.database.ServerValue.increment(-1));

    } else { 
        // set as liked 
        await likeInteractionRef.set(true);

        // decrease post's like count 
        await likeCountRef.set(admin.database.ServerValue.increment(1));     
    }

    // return success 
    return { success: true }; 
});

There is one problem: It is not idempotent. If the function is called twice by the same user without delay, it will go to the same branch of the if-else-statement and the like-count will be incorrect after.

How can I fix this?

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
mathematics-and-caffeine
  • 1,664
  • 2
  • 15
  • 19

1 Answers1

0

This will take a multi-path approach:

  1. Use a single multi-path update call to combines the write operations (or a transaction to combine the read and write operations as Doug commented).
  2. Use security rules to prevent the user performing the same (now atomic) write multiple times.

Use a single multi-path update call to combines the write operations

First step is to replace the two separate write operations with a single multi-path write operation. So for this code that removes a like:

// remove the like 
await likeInteractionRef.remove();

// decrease post's like count await 
likeCountRef.set(admin.database.ServerValue.increment(-1));

That would look something like this:

db.ref().update({
  "/path/toLikeInteraction": null,                              // remove the like
  "/path/toLikeCount": admin.database.ServerValue.increment(-1) // decrement count
})

Now there is a single write operation to the database, which is executed atomically there. It is also evaluated against the security rules as a single write, with data and root contain all data from before the write, and newData containing all the updated data after the write (if it were to be allowed by the rules).


Use security rules to prevent the user from writing multiple times

Now you can use security rules to perform all the necessary checks. For the removal of a like, those are:

  1. In the write rule for the removing of /path/toLikeInteraction, make sure that the newData at /path/toLikeCount is also decremented.
  2. In the write rule for writing /path/toLikeCount, if the value is decremented, ensure that user's /path/toLikeInteraction node is also remove (so present in the root data, not no longer present in the newData).

There might be some more checks there, such as checking that the user can only write and remove their own likes. All in all this will be quite involved, which is why I didn't write the entire code here.

If you want a good starting point for the security rules, have a look at:

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807