5

Whenever I define a Firebase transaction in NodeJS I notice it always runs three times - the first two times with null data, then finally a third time with actually data. Is this normal/intended?

For example this code:

firebaseOOO.child('ref').transaction(function(data) {
    console.log(data);
    return data;
});

outputs the following:

null
null
i1: { a1: true }

I would have expected that it only print the last item.

To answer a question in the comments, here is the same with a callback:

firebaseOOO.child('ref').transaction(function(data) {
    console.log(data);
    return data;
}, function(error, committed, snapshot) {
    if (error) 
        console.log('failed');
    else if (!committed)
        console.log('aborted');
    else 
        console.log('committed');
    console.log('fin');
});

Which yields the following output:

null
null
i1: { a1: true }
committed
fin

I had read the details of how transactions work before posting the question, so I had tried setting applyLocally to false like this:

firebaseOOO.child('ref').transaction(function(data) {
    console.log('hit'); 
    return data; 
}, function(){}, false);

But it still hits 3 times (just double-checked) so I thought it was something different. Getting the 'value' before transacting does "work" as expected, in that it only hits once, and that's regardless of what applyLocally is set to, so I'm not sure what applyLocally does? This is what I mean by getting the value before transacting:

firebaseOOO.child('ref').once('value', function(data) {
    console.log('1');
    firebaseOOO.child('ref').transaction(function(data) {
        console.log('2');
        return data;
    });
});

Outputs:

1
2

@Michael: How can one make use of this behavior? Transactions are primarily for having data use itself to modify itself - the prototypical increment++ scenario. So if I need to add 1 to the existing value of 10, and continue working with the result of 11, the first two times the function hits I will have an erroneous result of 1 that I need to handle, and finally the correct result of 11 on the third hit. How can I make use of those two initial 1's? Another scenario (and maybe I shouldn't be using transactions for this, but if it worked like I expected it makes for cleaner code) is to insert a value if it does not yet exist. If transactions only hit once, a null value would mean the value does not exist, and so you could, for example, init the counter to 1 in that case, otherwise add 1 to whatever the value is. With the noisy nulls, this is not possible.

It seems the takeaway from all this is to simply use the 'once' pattern more often than not?

ONCE TRANSACTION PATTERN:

firebaseOOO.child('ref').once('value', function(data) {
    console.log('1');
    firebaseOOO.child('ref').transaction(function(data) {
        console.log('2');
        return data;
    });
});
Baz
  • 2,167
  • 1
  • 22
  • 27
  • This is quite a strange use for a transaction. Why would you want to set it back to exactly the same value? If you set up the second callback, what does it say? Does the commit succeed? Is there an error? – Kato Apr 20 '13 at 13:43
  • Hey Kato, this is the absolute simplest case to demonstrate my issue. My real code has a lot more logic. There is no error message, and the results are recorded successfully at the end. In fact there is only 1 commit message at the end as expected. I have updated my question to reflect these results. – Baz Apr 20 '13 at 20:47

2 Answers2

2

The behavior you're seeing here is related to how Firebase fires local events and then eventually synchronizes with the Firebase servers. In this specific example, the "running three times" will only happen the very first time you run the code—after that, the state has been completely synchronized and it'll just trigger once from then on out. This behavior is detailed here: https://www.firebase.com/docs/transactions.html (See the "When a Transaction is run, the following occurs" section.)

If, for example, you have an outstanding on() at the same location and then, at some later time, run this same transaction code, you'll see that it'll just run once. This is because everything is in sync prior to the transaction running (in the ideal case; barring any normal conflicts, etc).

Vikrum
  • 1,941
  • 14
  • 13
  • 1
    How can one differentiate between nulls due to the internal events you describe vs. a "real" null where an object does not yet exist in Firebase? – Baz Apr 21 '13 at 04:01
  • 2
    You can add a listener for the 'value' event at the same location, and set applyLocally to false in your transaction call. This way, your value event listener will only trigger once the final value of the transaction has been set. See transactions docs here, look for the applyLocally parameter: https://www.firebase.com/docs/javascript/firebase/transaction.html – Greg Soltis Apr 21 '13 at 20:52
  • 1
    Additionally, I'd be interested in your use case and if the nulls are actually causing trouble. Right now we optimistically run transactions before we have the data synchronized, which is why you see the 'null' and so far this has worked pretty well. But we've considered changing the behavior. Ping michael at firebase com if you'd be interested in sharing more details about your use case! – Michael Lehenbauer Apr 22 '13 at 16:20
  • Thank you Greg and Michael, I've updated my question with comments/answers to your questions/comments. Michael I'm going to email you so we could exchange contact info. – Baz Apr 22 '13 at 19:46
  • @MichaelLehenbauer i'm here because i wanted to run a safe mutation - update x.field only if x already exists. the first two nulls make this impossible. nesting/chaining once or on for that sucks. Also that info about transaction behavior is no longer surfaced in the new API docs – Joseph Fraley Dec 15 '16 at 08:27
0

transaction() will be 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.

firebaseOOO.child('ref').transaction(function(data) {

if(data!=null){
    console.log(data);
    return data;
}
else {
   return data;
 }
}, function(error, committed, snapshot) {
    if (error) 
        console.log('failed');
    else if (!committed)
        console.log('aborted');
    else 
        console.log('committed');
    console.log('fin');
});
Shahzain ali
  • 1,679
  • 1
  • 20
  • 33