【Firebase】Cloud Functionsで学ぶPromiseとasync/await

# はじめに
[Cloud Functions for Firebase の学習（動画シリーズ）](https://firebase.google.com/docs/functions/video-series/?hl=ja)が、Cloud Functions入門としてだけでなく、
[Promise](https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Using_promises)や[async/await構文](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/async_function)の解説としても、とてもわかりやすかったので、メモとして残すことにしました。

# セットアップ

## Firebaseコンソールからプロジェクトを事前に作成
- https://console.firebase.google.com/ にアクセスして、「プロジェクトの追加」をクリックして、プロジェクトを作成
- 今回の記事で使用するプロジェクト名は、`cloud_functions_sample`にします

## CLIからの操作
- Node.js v6以上
- npm(v5.6.0以上)

```bash
mkdir cloud_functions_sample
cd cloud_functions_sample

npm i -g firebase-tools

# Googleアカウントでの認証が要求されるので、許可しましょう
firebase login

# プロジェクトの選択やランタイム
# 途中、どの機能を使用するか聞かれるので、Cloud Functionsのみを矢印キーとスペースキーで選択して、Enter
firebase init
```

`Cloud Functions`のランタイムは、今回は、`TypeScript`を使用します。
理由としては、`async/await`構文を使用したいからですが、TypeScript以外のランタイムは、Node.jsのみで、そのバージョンが、 **v6.11.5** のため、`async/await`が使用できません。

コマンド処理が完了すると、プロジェクトのルートディレクトリ以下に、`functions`というディレクトリができているので、`functions`ディレクトリ内にある`index.ts`

# デプロイ
- 実行したい関数ができたとき、`Cloud Functions`のみデプロイしたい場合は、以下コマンドを実行

```
firebase deploy --only functions
```

今回は、TypeScriptを使用しているため、通常なら素のJavaScriptにコンパイルする必要がありますが、Firebaseは、デプロイ時にそのへんもよしなにやってくれます。やったぜ😉

# 主な特徴
- イベントトリガーで、バックグラウンドに実行
  - HTTPによるトリガー
    - レスポンスを返す
  - バックグラウンドによるトリガー
    - Promiseを返す
- 対応言語は、JavaScript, TypeScript, Node.js
- TypeScriptとVS Codeにすると、FirebaseのAPIのレスポンスの型が補完で出てくるので、便利
  - レスポンスの型は基本的にPromise
- HTTPSでのみ動作可能

# イベントトリガーの種類

## HTTPトリガー
`functions.https.onRequest`の部分

例：とあるエンドポイントURLにアクセスした時に実行

```typescript
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'

export const getBostonWeather = functions.https.onRequest((req, res) => {
  // something action
})
```

## バックグラウンドトリガー
- データベース(`Firestore`, `Realtime Database`)のいずれかのCRUD処理をトリガーに自動実行
- 他にも、`Cloud Storage`へのアップロードをトリガーに実行できるなど、イベントトリガーの種類はいくつかある
  - 詳細は、[こちら](https://firebase.google.com/docs/functions/firestore-events?hl=ja#top_of_page)

例：Firestoreの更新時に実行

```typescript
export const onBostonWeatherUpdate = 
  functions.firestore.document("cities-weather/boston-ma-us").onUpdate(change => {
    // something action
  })
```

# Cloud FunctionsにおけるPromiseによる逐次処理と並列処理

## ユースケース
- ボストンだけでなく、他の地域の天気を取得したり、更新したいとき
- Firestoreのデータ構造

```yaml
- areas
  - greater-boston
    - cities
      - bostom-ma-us: true
      - cambridge-ma-us: true
      - somerville-ma-us: true
```

- それぞれの地域のデータ取得・更新は、並列処理したい
- HTTPトリガーなので、並列処理で全ての処理を終えてから１つのレスポンスを返すようにしたい
- 並列処理を実現するためには、`Promise.all`を使う

```typescript
export const getBostonAreaWeather =
  functions.https.onRequest((request, response) => {
    admin.firestore().doc("area/greater-boston").get()
    .then(areaSnapshot => {
      const cities = areaSnapshot.data().cities
      const promises = []
      for (const city in cities) {
        const p = admin.firestore().doc(`cities-weather/${city}`).get()
        promises.push(p)
      }
      return Promise.all(promises)
    })
    .then(citySnapshots => {
      const results = []
      citySnapshots.forEach(citySnap => {
        const data = citySnap.data()
        data.city = citySnap.id
        results.push(data)
      })
      response.send(results)
    })
    .catch(error => {
      console.log(error)
      response.status(500).send(error)
    })
  })
```

結果

```json
[
  {
    "city": "somerville-ma-us",
    "conditions": "partly-cloudy",
    "temp": 8
  },
  {
    "city": "boston-ma-us",
    "conditions": "sunny",
    "temp": 10
  },
  {
    "city": "cambridge-ma-us",
    "conditions": "sunny",
    "temp": 9
  }
]
```

ここまでが、Promiseを使った非同期処理となる
これを後述する`async/await`を使った処理だと、より同期的な見た目で非同期処理を書くことができる

イメージ

```typescript
const p1 = doSomeWork(1)
const p2 = doSomeWork(2)
const p3 = doSomeWork(3)
const pMany = [p1, p2, p3]
const finalPromise = Promise.all(pMany)
finalPromise.then(results => {
  results.forEach(result => {...})
})
.catch(error => {...})
```

# `async/await`によるリファクタリング

例

```typescript
async function myFunction(): Promise<any> {
  try {
    // const rankPromise = getRank()
    const rank = await getRank()
    return "firebase"
  }
  catch (err) {
    return "Error: " + err
  }
}

function getRank() {
  return Promise.resolve(1)
}
```

## `async/await`の特徴

1. 常にPromiseを返す
2. `async`を付けた関数は、Promiseを返す処理でなかったとしても、元々の返り値をPromise型にラップして、返す
3. `await`を付けた関数は、必ずPromiseが完了(resolve)した値を返す
4. `try~catch`構文を使用して、途中で失敗した`await`関数のエラーを補足できる
5. `await`キーワードは、`async`関数内でしか、使用できない

## `async/await`で、Cloud Functions内のコードを書き換える

先程、ボストンの天気を取得する関数を作成したので、それを書き換えます。
今の実装は以下の通り。

```typescript
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
admin.initializeApp()

export const getBostonWeather =
  functions.https.onRequest((request, response) => {
    admin.firestore().doc("cities-weather/boston-ma-us").get()
    .then(snapshot => {
      const data = snapshot.data()
      response.send(data)
    })
    .catch(error => {
      console.log(error)
      response.status(500).send(error)
    })
  })
```

これを`async/await`構文を使用して、以下のように変更

```diff
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
admin.initializeApp()

export const getBostonWeather =
+  functions.https.onRequest(async (request, response) => {
+   try {
+     const snapshot = await admin.firestore().doc("cities-weather/boston-ma-us").get()
+     const data = snapshot
+     response.send(data)
+   }
+   catch (error) {
+     console.log(error)
+     response.status(500).send(error)
+   }
-    admin.firestore().doc("cities-weather/boston-ma-us").get()
-    .then(snapshot => {
-      const data = snapshot.data()
-      response.send(data)
-    })
-    .catch(error => {
-      console.log(error)
-      response.status(500).send(error)
-    })
  })
```

またボストンだけでなく、他の地域も並列で取得し、全地域の天気情報が取得できたら、何かしらのレスポンスを返す関数も作成していました。
現状の関数は、以下のとおりです。
複数の`then`と`Promise.all`を使って、それぞれの地域の天気情報を並列で取得し、更新していました。

```typescript
export const getBostonAreaWeather =
  functions.https.onRequest((request, response) => {
    admin.firestore().doc("area/greater-boston").get()
    .then(areaSnapshot => {
      const cities = areaSnapshot.data().cities
      const promises = []
      for (const city in cities) {
        const p = admin.firestore().doc(`cities-weather/${city}`).get()
        promises.push(p)
      }
      return Promise.all(promises)
    })
    .then(citySnapshots => {
      const results = []
      citySnapshots.forEach(citySnap => {
        const data = citySnap.data()
        data.city = citySnap.id
        results.push(data)
      })
      response.send(results)
    })
    .catch(error => {
      console.log(error)
      response.status(500).send(error)
    })
  })
```

`async/await`に書き換えたのが以下のとおりです。

```diff
export const getBostonAreaWeather =
+  functions.https.onRequest(async (request, response) => {
+   try {
+     admin.firestore().doc("area/greater-boston").get()
+     const cities = areaSnapshot.data().cities
+     const promises = []
+     cities.forEach(city => {
+       const p = admin.firestore().doc(`cities-weather/${city}`).get()
+       promises.push(p)
+     })
+     const snapshots = await Promise.all(promises)
+
+     const results = []
+     citySnapshots.forEach(citySnap => {
+       const data = citySnap.data()
+       data.city = citySnap.id
+       results.push(data)
+     })
+     response.send(results)
+   }
+   catch (error) {
+     console.log(error)
+     response.status(500).send(error)
+   }
-    admin.firestore().doc("area/greater-boston").get()
-    .then(areaSnapshot => {
-      const cities = areaSnapshot.data().cities
-      const promises = []
-      for (const city in cities) {
-        const p = admin.firestore().doc(`cities-weather/${city}`).get()
-        promises.push(p)
-      }
-      return Promise.all(promises)
-    })
-    .then(citySnapshots => {
-      const results = []
-      citySnapshots.forEach(citySnap => {
-        const data = citySnap.data()
-        data.city = citySnap.id
-        results.push(data)
-      })
-      response.send(results)
-    })
-    .catch(error => {
-      console.log(error)
-      response.status(500).send(error)
-    })
-  })
```

`const snapshots = await Promise.all(promises)`に変更することによぅて、`Promise.all`として完了した値を担保してくれます。
`async/await`を使うことで、`then`式が不要になり、同期的なシンタックスが実現でき、可読性が上がります。