3

I have a function that is decrementing user credits on firebase realtime database values with a transaction. As suggested in Firebase transaction API call current data is null transaction current value occasionally returns as null.

I've made a guard for the null case and returned 0 so the transaction function fires again until it gets the actual credit value.

function charge(cost, description) {
  return new Promise((resolve, reject) => {
    const creditRef = db.ref(`credits/${userid}`)
    ref.transaction(function(current) {
      console.log(`${description} current value: ${current}`)
      if (current === null) {
        console.log(`${description} returns 0 `)
        return 0
      }
      if (cost > current || current === 0) {
        //not enough credits return without committing
        console.log(`${description} aborts `)
        return
      }
      //commit the new credit value
      console.log(`${description} returns ${current} - ${cost}`)
      return current - cost
    },
    (error, commited, snapshot) => {
      if (error) {
        reject(error)
      }
      else {
        if (commited) {
            //user has enough credits
            resolve()
        }
        else {
            //not enough credits
            reject('no commit')
        }
    }
  })
}

However, in a case where 2 charge functions are fired back to back, the second call will get a current value of 0 (which is probably the returned 0 on the first charge call). So it will prematurely exit assuming that the user doesn't have enough credits. When both functions resolve the final credit value will be 3 and the second charge call will be ignored.

// User has 5 credits 
charge(3, 'first call').then(() => console.log('first call success')
// transaction function returns 0 since current value is null
charge(2, 'second call').then(() => console.log('second call success')

Console log output:

first call current value: null

first call returns 0

second call current value: 0

second call aborts

first call current value: 5

first call returns 5 - 3

first call success

second call no commit

So the second charge call ends up not going through when user had enough credits. What is the proper way to handle firebase transaction null value cases?

Community
  • 1
  • 1
Saccarab
  • 1,399
  • 12
  • 25
  • I'm not sure I follow what the problem is. If the user has no credits (either because their `credits` value is `0`, or because they don't have a value for `credits`), their transaction will be rejected. If the rejection turns out to be wrong (because the value in the database has changed), the function will be retried. – Frank van Puffelen Jan 02 '20 at 23:53
  • If you suspect the `return 0` is causing your problem, I'd suggest replacing it with a `return` without a value and see if you can still reproduce the problem. – Frank van Puffelen Jan 02 '20 at 23:54
  • But if I do `return` instead `return 0` function will complete without committing, when I want it to be retried with the actual current value. (which is not null in this case) – Saccarab Jan 02 '20 at 23:59
  • 1
    Returning without a value aborts the transaction. See https://firebase.google.com/docs/reference/js/firebase.database.Reference.html#transaction – Frank van Puffelen Jan 03 '20 at 00:01
  • If I do `return` neither of the `charge()` calls actually decrement the credit value. – Saccarab Jan 03 '20 at 00:05
  • 1
    As I said, I think I'm not understanding the problem. Can you set up a reproduction that I can look at somewhere? If that is not feasible, can you change your code to: 1) `console.log(current)` at the start, 2) `console.log` the return value (or lack thereof) at all three `return` statements, 3) edit your question to include what you get (including an indication about which client/run logged which lines)? – Frank van Puffelen Jan 03 '20 at 00:11
  • I've edited the question with more clarification. – Saccarab Jan 03 '20 at 01:09
  • Thanks! That was really helpful, and allowed me to answer your question. – Frank van Puffelen Jan 03 '20 at 02:46

1 Answers1

4

In the case you get null from the database, you should make no change. Since you get null, also return null.

So:

const ref = firebase.database().ref(`59571450`)
function charge(cost, description) {
  return new Promise((resolve, reject) => {
    ref.transaction(function(current) {
      console.log(`${description} current value: ${current}`);
      if (current === null) {
        console.log(`${description} returns null`)
        return null;
      }
      if (cost > current) {
        //not enough credits return without committing
        console.log(`${description} aborts `)
        return
      }
      //commit the new credit value
      console.log(`${description} returns ${current} - ${cost}`)
      return current - cost
    },
    (error, commited, snapshot) => {
      if (error) {
        reject(error)
      }
      else {
        if (commited) {
            //user has enough credits
            resolve()
        }
        else {
            //not enough credits
            reject('no commit')
        }
      }
    });
  });
}
                     

ref.set(5).then(() => {
  charge(3, 'first call').then(() => console.log('first call success'))
  charge(2, 'second call').then(() => console.log('second call success'))
})

When run this gives this output:

first call current value: null

first call returns null

second call current value: null

second call returns null

first call current value: 5

first call returns 5 - 3

second call current value: 2

second call returns 2 - 2

first call success

second call success

For a working version of this code, see: https://jsbin.com/xefebul/1/edit?js,console

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