DOUG STEVENSON: Hey, everyone.
Welcome back to another Firecast about Cloud Functions
for Firebase.
My name is Doug Stevenson, and today's topic
is advanced programming with JavaScript promises
in Cloud Functions for Firebase.
In the last two videos of this series,
I talked about using promises to manage
the work happening in an HTTP trigger
and a Firestore trigger.
Each type of trigger had a slightly different way
of working with the promises returned by the Firestore
API and the Firebase Admin SDK.
In particular, you learn the basics of JavaScript promises
and how to use them.
And you also saw two rules for terminating a Cloud
Function, which are these--
first, HTTP triggers must send a response to the client
to terminate properly; and second,
for all other types of triggers, called background triggers,
you must return a promise that becomes fulfilled or rejected
when all the pending work in that function is complete.
This lets Cloud Functions know when
it's safe to clean up the function invocation
and move onto the next one.
The code I've showed you so far is fairly simple.
Let's go over again what it looks like.
My Firestore database contains a collection
called cities-weather, which contains documents
for a few different cities.
Each document stays up to date with the current weather
conditions at that city.
This HTTP trigger I have right now fetches
and returns the current weather conditions in Boston.
You can see it makes a request to Firestore
to get the document at the known location.
The get method returns a promise that's
fulfilled with a snapshot of the document, which I can then
convert to a JavaScript object and then send back
to the client in JSON format.
And, if the promise is rejected with an error,
I handle that too.
Now, what I want to do here is group neighboring cities
by a larger area and create an HTTP trigger that
returns the weather conditions for all
the cities in that area.
So what I've done is created another collection
in Firestore called areas, and the documents in there
contain a list of cities in that area.
Let's take a look.
Now, in Firestore, you can see that new collection,
and it contains a document for the greater Boston area.
This document has a property called
cities, which is an array of city IDs in that area.
Right now, it's just Boston, Cambridge, and Somerville.
This is an incomplete list, and I'll update that later.
So what I need to do is create a new HTTP trigger
that reads the Boston area document,
gets this list of cities, reads each of the matching documents
from the cities-weather collection,
and returns them to the client.
So I'll switch to VS code and start writing that.
What I'll do here is export a new HTTP function
called getBostonWeather.
In that function, I'm going to query that Boston area
document.
Get returns a promise, so I'll use the then method
on that to get the document snapshot when
the query is complete.
I'll convert that snapshot to a JavaScript object,
then access the document property called cities
that has the array of city IDs.
I can iterate this array, then build a reference
to each city's weather document that I need to read.
There's a little snag here.
Each of these calls to get returns a promise,
and there will be several for each area.
And I need to wait on all of them
to finish and get all of their data to send to the client.
Now, in this situation, I don't necessarily
know how many cities there will be,
but I need all their results.
And I don't need to wait for each one
to complete one after another.
They can happen all at the same time, in parallel.
So what I really want to do here is kick off
all the queries at the same time, then continue
after all of them are complete.
Once I have the entire set of documents,
I'll send a final response to the client.
And I can manage this all with a function called a promise.all.
Let's see how that works.
First, I'll collect all the promises into an array.
I can do that by creating an empty array before the loop.
Then inside the loop, I'll push each promise into that array.
Remember that get returns immediately,
so each one of these promises is still in the pending state
and should be fulfilled later on.
Once I have the promises collected,
my code needs to wait for all of these promises
to become fulfilled before moving on.
I can use the function promise.all,
pass at the array of promises, and it
will return a new promise that's fulfilled
only after all of those other promises are fulfilled.
With that, I can call its then method
to continue with more work.
But that means I'm nesting my code deeper than necessary.
The best thing to do here is to return that promise
from the callback.
Now, let's see what happens to a promise return like this.
If you return a promise from a then callback,
the then method itself returns a new promise that becomes
fulfilled or rejected at the same time
as that other return promise.
With that new promise, you can change another then method
to it to continue where that work left off.
You can use this to write another callback that
receives the results from the promise fulfilled
in the prior callback.
In this case, it's an array of document snapshots
with the result from each get.
So this snapshots' argument is the array of the document
snapshots that are fulfilled from the array of promises
above.
What I'll do here is iterate that array of snapshots
to collect all the weather condition
objects for each city.
But what should I do with all these objects?
Well, I'll create a new array with all the results
and push them there.
Then, I can send that array of results
to the client in JSON format.
Oh, and while I'm here, I'll do the right thing
and paste a call to catch after this so I can deal
with any errors that occur.
Remember-- it's critical to return a response
in all circumstances.
OK, let's see what that JSON looks like.
At my command shell, in my project functions folder,
what I'll do now is run the linter, and after that's OK,
compile the TypeScript to JavaScript.
Then I'll run the emulator for HTTP functions.
It gives me a local host URL for the getBostonAreaWeather
function.
So I'll copy it, switch to another shell tab,
and run this cURL command to see what it returned.
Cool.
And I can see all the Boston area weather documents here.
This is a good start, but I can't tell which city
each object refers to.
So I'll go back into VS code, change the callback
to add the unique document ID to each JavaScript object,
then push that modified object into the results array.
Then, I'll go back to my shell, compile the TypeScript,
run the emulator, and then use cURL again to view the results.
And there we go.
This is kind of hard to read, though,
so I'll pipe it to a Python pretty printer,
just to make that easier.
Nice.
Doesn't that look a lot better for testing and debugging?
I think so.
So we saw two new techniques here for dealing with promises.
First, you can chain together items of work
by returning a promise from the callback of the then method.
Just like returning a promise to Cloud Functions,
this promise must represent all the async work
that was started here.
When the promise returned by the callback is fulfilled,
the next promise callback will receive the results
generated by the prior one.
And the catch callback will capture errors
from any of the prior work.
The final promise, return from catch,
will be fulfilled or rejected after the entire sequence is
complete.
The second thing you learned is that you
can kick off many different items of work asynchronously.
Collect all their promises into an array
and use promise.all to create a new promise that's
fulfilled when all the other promises are also fulfilled.
That new promise delivers an array of results
to the next callback.
You should also know that if any of those promises
becomes rejected with an error, the promise returned
from promise.all will also become rejected, and that's it.
Once you get practice with these techniques,
it becomes easier to write more complicated code for Cloud
Functions while you're following the two rules to terminate
your functions correctly.
Now, for functions written in TypeScript,
which I am using here, there is an even easier way
to deal with series of promises.
TypeScript gives you a syntax called
async await that makes dealing with promises super easy.
So if you want to see how that works,
subscribe right here to the Firebase channel on YouTube
to get notified of the next video in this series.
And I'll see you here next time.