0

I am using transaction in my realtime database as follows:

admin.database().ref(`/users/${uid}`).transaction((user) => {
  if (user) {
    console.log("user exists");
    // ... modify the user ...
    admin.database().ref(`/notifications/${user.guid}`).transaction((notification) => {
      // modify notification object
    }
    return user;
  } else {
    console.log("user did not exist");
    return { /* information about a new user */ };
  }
});

The results have been strange. I am seeing two console.logs as follows:

> user did not exist
> user exists

Even though the user object existed. What ends up happening is that I have a user that gets created, overwriting the old user.

I've tried removing the nested notification transaction within the transaction, and it seems to work then, but I still get the same first > user did not exist console.log() output, and it's not clear why that would be.

I guess there are two questions here. First is why the non-existing code path executes at all, and the second is how, if not like this, should I pass along a value that I retrieved from read part of the transaction to another write.

Thanks!

umop
  • 2,122
  • 2
  • 18
  • 22

1 Answers1

1

As explained in the doc, the Transaction function is called multiple Times

Your transaction handler is called multiple times and must be able to handle null data. Even if there is existing data in your database it may not be locally cached when the transaction function is run.

This is why the console first write that there is no user and then write that there is a user.

Note that you should not call a Transaction within a Transaction.

Renaud Tarnec
  • 79,263
  • 10
  • 95
  • 121
  • Sure. I understand that, but what I don't understand is why the first (or even why any) transactions are reporting that the object is empty when it has been long since populated. – umop Feb 03 '23 at 18:55
  • Also, this is all in a cloud function, so internet connectivity shouldn't be an issue. Regarding the additional question (about how to use the data from the transaction in another transaction), since I know that the field that I want to use will either be blank or valid (it will never change once set), I guess I can just save it inside a higher scoped object (`const transactionValues = {propGoesHere}`) if it exists. I was just hoping that there was a cleaner way to do this. – umop Feb 03 '23 at 19:04
  • I see the note you were linking to now: `... Even if there is existing data in your database it may not be locally cached when the transaction function is run.`. The follow up question still stands, though. I think I should be using the [completion handler](https://firebase.google.com/docs/reference/node/firebase.database.Reference#optional-oncomplete:-a:-error-|-null,-b:-boolean,-c:-datasnapshot-|-null-=-void) so I know that I can rely on the data being correct. In my case, I could probably save some time by using any non-null value in the transaction callback, but this isn't time critical. – umop Feb 03 '23 at 19:28
  • So, it looks like the best place for this is maybe as a follow-up `.then()` or `await` to the promise returned by the transaction as it also appears to be post-completion. Then the additional transaction won't be nested and I can use the `.snapshot` property to get to the values without storing them outside the async calls. That seems to be working for me. it would still be good to know how to make this more expedient in cases where latency was more of a concern. Perhaps something equivalent to a complex SQL query or a stored procedure, but this works for me for now. – umop Feb 03 '23 at 20:42
  • Yes you should use the Promise returned by the Transaction. – Renaud Tarnec Feb 04 '23 at 19:44