DOUG STEVENSON: Thanks for joining me
for this third part in this series
about writing Realtime Database triggers with Cloud
Functions for Firebase.
My name is Doug.
And what I'm about to tell you here follows from the first two
videos, so be sure to go back and watch those first
if you haven't already.
In those first two videos, you saw
how I built using Cloud Functions a couple triggers
that automatically replaced the word "pizza" with a pizza emoji
in a chat room.
Why?
Because it adds pizzazz.
Those triggers respond to newly added messages
and also messages that were updated after they
were initially added.
But what do you do if you want to respond
to messages that were deleted?
Well, I'm sure it's no surprise that you'll
do it with an onDelete trigger.
And here's how I want to use it.
But it doesn't really involve pizza this time.
What I'd like is for my chat rooms
to have a message count to be queried easily.
Why do I need a message count?
Well, I'd like for client apps to be
able to know how many messages are in a room
without having to query, download, and count
all the message children in that room.
That's too much to do when you just want to count.
So I'm going to do two things.
First, I'll change my existing onCreate trigger
to read, increment, and write the message count
when a new message is added to a room.
Second, I'll add a new onDelete trigger
to read, decrement, and write the message count
when a message is deleted for whatever reason.
I'll store this count in a new child
value in the node for the room at the same level
as the messages node.
This is the value I'll increment and decrement as messages
are added and removed.
OK.
Let's go to the code.
Actually, no.
I need to think about this first and make a plan
before I start writing code.
There's one important thing you need to keep in mind when
writing Cloud Functions code.
It turns out that each invocation of your functions
may be running at the same time.
They don't just run one after another in sequence.
That strategy doesn't scale well at all.
Instead, the Cloud Function's runtime
needs to be able to run your functions in parallel
under load.
Imagine that you have many people chatting in a room
with each of them adding and deleting messages,
and they all need to update the message count for that room.
The problem here is like this.
Each invocation of onCreate and onDelete
needs to read the existing message
count for the room from the database,
make a change to it in memory, then write it
back to the same place.
But you could easily have a situation
where two function invocations, let's
say an onCreate and an onDelete, are running at the same time.
And they both initially read the same message count
from the database, make their changes to it
in local memory, whatever that might be, but one of the values
incorrectly overwrites the other,
and we don't even know which one it will be.
With this situation in this diagram, where
the message count is originally 4
and two functions try to make a change to it,
will the new message count be 3 or 5?
Well, I'd like it to go back to 4
after both an increment and a decrement.
But here, the final result will obviously
be wrong, no matter the order in which the writes complete.
This situation is called a race condition,
and that's the problem I'll have here if I don't do something
to correct it.
The good news is that it's easy to solve with a database
transaction.
So let's see how that works in code.
First, notice how this onCreate function
returns the promise from the call to update.
This isn't going to work anymore since I
have to do the transaction after the update completes.
The easy thing to do here is use async await.
First, I'll make this handler function
async by using that keyword where its definition starts.
Then, I'll use the "await" keyword instead of "return"
to pause the code until the update completes.
After that, I need to build a reference
to the location of the counter in the database,
I can use the snapshot ref as a start,
which points to the location of the node that was created.
Then, I'll work my way two nodes up the tree
using the parent property of the reference.
Now I have a reference that points to the node of the room
at rooms/[? roomId ?].
I need the message count child under this location,
so I'll use the child method to build another reference
to that.
The value referenced by countRef is
what I want to read, increment, and write.
I'll use the transaction method on this reference
to perform the transaction.
It requires a transaction update function,
and that function receives the value
of the database at the location of the reference.
Here, I'm expecting the message count value at that location.
And I'll simply increment it and return
it to tell the transaction the new value to write back.
Now I'll write the onDelete trigger.
It's just like onCreate, so I'll copy and paste it.
But of course, I have to change the name.
I'll also copy the transaction from onCreate.
It'll be exactly the same, except I'll decrement
the counter this time.
And I still need to return that promise from the transaction.
That should do it.
Now my onCreate and onDelete are working in tandem
to manage the message count of each room.
The transactions they're using are
said to be atomic, meaning they complete without
the possibility of interruption, which means we no longer have
a race condition.
If your code doesn't have the possibility of a race
condition, then don't use a transaction at all.
It'll just slow things down unnecessarily.
So be sure to think through your situation carefully.
If you want to learn more about Realtime Database transactions,
be sure to read the documentation.
The link is in the description below.
But right now, I want to test this code to see how it works.
If you remember from the first video in this series,
I have a script that simulates adding a series of messages
to a chat room.
I'll run that, then watch the results in the Firebase
console.
OK.
I'll run the script at the command line
and see that it simulates the messages.
At the same time, in the console you
can see the messages appear.
And you can also see the message count
going up with each new message.
Then, if I delete all the messages here in the console,
you can see the counter drop back down to zero.
That's pretty fun to watch, don't you think?
All right.
The code is done, but there's one more thing
to know about onDelete.
You probably noticed its handler function gets passed a snapshot
object as the first parameter.
That snapshot contains the old data
that was just deleted from the location of the trigger.
But if you don't use that object at all, or the context,
you can just leave them out of the handler function definition
just to keep things clean.
OK.
Using onCreate, onUpdate, and onDelete
triggers in Cloud Functions, you can do all sorts of useful work
in response to changes in your database.
And there's also an onWrite trigger
which you can use that gets fired for all the changes
that would also cause onCreate, onDelete, or onUpdate to fire.
But you have to dig into its snapshot changes
to figure out exactly what happened.
So I just use the other three triggers
because they're easier.
In all cases, your code is secure from hacking
and you can update its logic at any time
without having to build and publish your app again.
Cloud Functions are powerful.
But after working with them, you may
feel it's slow to write, deploy, and test
your function repeatedly.
You can speed up that cycle using the Cloud Functions
Emulator.
And I'll show you how to do that for database
triggers next time.
So be sure to tune in right here to the Firebase channel
on YouTube to watch that video when it's ready.
And I'll see you then.
[MUSIC PLAYING]