TypeScript & React & Firebase で何かつくってみる8 Functions 5

Cloud Functions の話は前回で一旦終了のつもりだったが, 設計を進めている途中でもう一つ試したいことができたため, それについて書く.

Cloud Firestore トリガー

これまでは ユーザーから特定のアドレスにリクエストを出したときに実行するようにつくってきた.

以下の機能を使うと データベース上の何らかの変化をトリガーにして関数を実行することができる.

これを試すに当たり, プロトタイプの「一枚引く」機能の仕様を変更する. 

1. firestore に 'actions' コレクションを用意する.
2. 「一枚引く」ときは 'actions'  に対して「deckからhandへ移動」という情報を add する.
3. 'actions' 変更をトリガーに Functions を起動し「カードの移動」を行う.
4. 実行したアクションを 'actions' から削除する.

これだけ見ると回りくどいが, 将来の拡張を考えると 1案としては悪くない気はする.

まず, クライアント側に以下の関数を作って button.onClick に結びつける.

 const actionsRef = firestore.collection('actions');
 const draw: React.MouseEventHandler<HTMLButtonElement> = (e) => {
   actionsRef.add({from: 'fields/deck', to: 'fields/hand'});
 };

fields/deck および fields/hand は ドキュメントへのパスを示す. 

firestore.collection('fields').doc('hand') という書き方は, パス表現を使えば firestore.doc('fields/hand') のように書けるらしい.

このように, クライアントは 直接 deck, hand を操作せず, actions に2つのパスを送る.

サーバ (Cloud Functions) 側は, サンプルコードを参考に draw 関数を以下のように書き換える.

export const draw = functions.region('asia-northeast1').firestore
.document('actions/{actionId}').onWrite(async (change, context) => {
 const action = change.after.data() as Action;
 const fromRef = fireStore.doc(action.from);
 const toRef = fireStore.doc(action.to);

 await fireStore.runTransaction(transaction => {
   return transaction.getAll(fromRef, toRef).then(snapshot => {
     const deck = snapshot[0].data() as Deck;
     const hand = snapshot[1].data() as Deck;
     if (deck.cards.length > 0) {
       hand.cards.push(deck.cards[0]);
       deck.cards.shift();
       transaction.update(fromRef, deck);
       transaction.update(toRef, hand);
     }
   }).then(() => {
     return change.after.ref.delete();
   });
 });
});

トランザクション内の処理は変わっていないし, 修正部分も参考サイトに書かれているテクニックしか使っていない.

強いてポイントを挙げるなら, トランザクション成功後に change.after.ref.delete() を呼ぶことで 使用済みアクションを削除している.

Functions のエラー

Functions のデプロイでエラーがでた.

$ npx firebase deploy --only functions
  :
!  functions: failed to update function draw
HTTP Error: 400, Change of function trigger type or event provider is not allowed

これについては以下が参考になった.

関数の種類が変わったことで上書き登録ができなくなるのか.

$ npx firebase functions:delete draw
$ npx firebase deploy --only functions:draw

つまづいたおかげで 関数を削除する方法と 1個だけアップデートする方法を知る.


よし,意図したとおりに動いている・・・と思ったが Firebase Console を覗くとエラーが出ている.

画像1

draw
TypeError: Cannot read property 'from' of undefined at exports.draw.functions.region.firestore.document.onWrite (/srv/lib/index.js:29:42) at cloudFunction (/srv/node_modules/firebase-functions/lib/cloud-functions.js:131:23) at /worker/worker.js:825:24 at <anonymous> at process._tickDomainCallback (internal/process/next_tick.js:229:7)

「undefined は from を持ってないよ」と言ってるので どうも 

const action = change.after.data() as Action;

この action が undefined のことがあるらしい. console.log の内容もここに出るようなので, 少しログを仕込んでみる.

・・・わかった. 原因は以下の行だ.

     return change.after.ref.delete();

「使用済みアクションの削除」という処理がトリガーになって もう一度 onWrite コールバックが呼ばれていた. この時の change.after.data() が undefined だったというわけだ.

 onWrite は create, update, delete すべての変更がトリガーになる. これを onCreate にすると create だけがトリガーになる.

修正して再デプロイするとエラーは発生しなくなった.
だが, 調子に乗ってボタンを連打したら 違うエラーが出た.

Error: quota exceeded (Quota exceeded for quota group 'FunctionCallsNonbillable' and limit 'Function invocations per 100 seconds' of service 'cloudfunctions.googleapis.com' for consumer 'project_number:XXXXXXXXXX'.); to increase quotas, enable billing in your project at https://console.cloud.google.com/billing?project=card-game-field. Function cannot be executed.

利用制限に引っかかっただけか. 無料枠ではあまり一度にたくさんは実行できないようだ.

完全に失敗したかと思ったが, すこしディレイがあったのち続きが実行された. 気が利いてるなぁ.

結果

「どこからどこへ」を指定できるようにしたことで 引くだけじゃなく戻すこともできるようになった.

画像2

しかしなんか反応が悪い. 速いこともあるが.

ログによれば function 自体は 200~300ミリ秒くらいで実行できているらしいが, 体感では1秒以上かかっている. 別の場所にボトルネックがあるのか.

こういうやり方もありかと思ったが, イマイチかも知れない.

この記事が気に入ったらサポートをしてみませんか?