AWS AppSync Chat Starterのメッセージ送信部分を調査する

2019.02.12

前回まで

AppSync Chat Starterをクローンして起動するところまで前回取り組んだ。今回はメッセージ送信部分のコードを読んでいく。

送信部分のコードを見つける

Google Chromeのデベロッパーツールで送信ボタンを選択してクラス名等を見てみる。

AWS AppSync Chat Starterのメッセージ送信部分を調査する

「Type a Message」という文字列はアプリケーションの他の部分では出てきそうになかったので、プロジェクト全体から「Type a Message」で検索してみると予想通りsrc/app/chat-app/chat-input/chat-input.component.htmlのみがヒットした。



<div class="rounded p-2 mt-2 bg border border-dark rounded">
  <div class="input-group">
    <input type="text" class="form-control no-focus"
    required placeholder="Type a Message"
    id="message" [(ngModel)]="message" name="message"
    (keyup.enter)="createNewMessage()" />
    <span class="input-group-btn">
      <button class="btn btn-dark" (click)="createNewMessage()" type="button">
        Send&nbsp;<i class='ion-chatbubble-working'></i>
      </button>
    </span>
  </div>
</div>

ボタンをクリックするとcreateNewMessageメソッドが呼ばれるように実装されているので、ChatInputコンポーネントの動作が定義されているchat-input.component.tsを見てみる。



import { Component, Input } from '@angular/core';
import { AppsyncService } from '../appsync.service';
import { v4 as uuid } from 'uuid';
import createMessage from '../graphql/mutations/createMessage';
import getConversationMessages from '../graphql/queries/getConversationMessages';
import { unshiftMessage, constants } from '../chat-helper';
import Message from '../types/message';
import { Analytics } from 'aws-amplify';

@Component({
  selector: 'app-chat-input',
  templateUrl: './chat-input.component.html',
  styleUrls: ['./chat-input.component.css']
})
export class ChatInputComponent {

  message = '';

  @Input() conversation: any;
  @Input() senderId: string;
  constructor(private appsync: AppsyncService) {}

  createNewMessage() {
    if (!this.message || this.message.trim().length === 0) {
      this.message = '';
      return;
    }
    const id = `${new Date().toISOString()}_${uuid()}`;
    const message: Message = {
      conversationId: this.conversation.id,
      content: this.message,
      createdAt: id,
      sender: this.senderId,
      isSent: false,
      id : id
    };
    console.log('new message', message);
    this.message = '';
    this.appsync.hc().then(client => {
      client.mutate({
        mutation: createMessage,
        variables: message,

        optimisticResponse: () => ({
          createMessage: {
            ...message,
            __typename: 'Message'
          }
        }),

        update: (proxy, {data: { createMessage: _message }}) => {

          const options = {
            query: getConversationMessages,
            variables: { conversationId: this.conversation.id, first: constants.messageFirst }
          };

          const data = proxy.readQuery(options);
          const _tmp = unshiftMessage(data, _message);
          proxy.writeQuery({...options, data: _tmp});
        }
      }).then(({data}) => {
        console.log('mutation complete', data);
      }).catch(err => console.log('Error creating message', err));
    });
    Analytics.record('Chat MSG Sent');
  }
}

このコンポーネントから直接AppsyncServiceを呼び出してAPIをコールしているらしい。

messageという変数に送信するメッセージの情報を構築していき、client.mutateメソッドでGraphQLのMutationを実行している。OptimisticResponseは、キャッシュに書き込めた時点でコールされるメソッド。

optimisticResponseについて深掘りする

まだ肌感覚だが、optimisticResponseの部分がオフライン動作に一役買っている気がする。 Apollo Clientの公式ドキュメントのOptimistic UIの記事を読んでみる。

Optimistic UI - Apollo Client

As explained in the mutations section, optimistic UI is a pattern that you can use to simulate the results of a mutation and update the UI even before receiving a response from the server. Once the response is received from the server, the optimistic result is thrown away and replaced with the actual result. (意訳)Mutationのセクションで説明したように、Optimistic UIパターンはMutationの結果をシミュレートしてサーバーからのレスポンスを受け取る前にUIを更新できる設計手法である。サーバーからレスポンスが返ってきた時点で、仮置きしたレスポンスは破棄されて実際のレスポンスに置き換えられる。

要は実行結果をローカルで組み立ててしまい、それを元にしてUIを更新しようという方針らしい。サーバーから結果が返ってきたらしれっと上書きすると。

ChatQLのメッセージ送信部分で、Mutationを実行している部分は以下の通り。



client.mutate({
  mutation: createMessage,
  variables: message,

  optimisticResponse: () => ({
    createMessage: {
      ...message,
      __typename: 'Message'
    }
  }),

  update: (proxy, {data: { createMessage: _message }}) => {

    const options = {
      query: getConversationMessages,
      variables: { conversationId: this.conversation.id, first: constants.messageFirst }
    };

    const data = proxy.readQuery(options);
    const _tmp = unshiftMessage(data, _message);
    proxy.writeQuery({...options, data: _tmp});
  }
}).then(({data}) => {
  console.log('mutation complete', data);
}).catch(err => console.log('Error creating message', err));

mutateメソッドの引数のオブジェクトの中にoptimisticResponseというキーがある。ここでサーバーにリクエストを投げて、レスポンスが返ってくるまでの間に仮の結果として使うものを準備するらしい。

__typenameはデータをキャッシュする際に正規化するために使われるらしい。

Caching data - Apollo Client

サーバーからのレスポンスが返ってきたらupdateメソッドの部分が実行されそう。



update: (proxy, {data: { createMessage: _message }}) => {
  const options = {
    query: getConversationMessages,
    variables: { conversationId: this.conversation.id, first: constants.messageFirst }
  };

  const data = proxy.readQuery(options);
  const _tmp = unshiftMessage(data, _message);
  proxy.writeQuery({...options, data: _tmp});
}

OptimisticResponseとして書き込んだ内容を検索して、それを置換するような処理は無いように見える。その部分はApollo Clientで内部的にうまく処理しているのだろうか?

これは表示部分がどのような実装になっているか見てみないとわからないが、RxJSのObservableのような形で購読できて、変更が流れてくるのならApollo Clientがやってくれていると見ていいだろう。

OptimisticResponseを適当に変えて試してみる

optimisticResponseの戻り値を以下のように変更して試してみる。Chromeのデベロッパーツールを使って通信が遅い場合と完全にオフラインの場合で動作を検証する。



optimisticResponse: () => ({
  createMessage: {
    ...message,
    __typename: 'Message'
  }
}),



optimisticResponse: () => ({
  createMessage: {
    conversationId: this.conversation.id,
    content: 'OptimisticResponseの本文',
    createdAt: id,
    sender: this.senderId,
    isSent: false,
    id : id,
    __typename: 'Message'
  }
}),

どちらの場合にしても、まずoptimisticResponseの内容がUIに反映された。

AWS AppSync Chat Starterのメッセージ送信部分を調査する

サーバーとの通信が終わったタイミングで、レスポンスに含まれる内容に切り替わった。

Mutationに関する疑問

ここまで試してみてわかったのが、Apollo Clientによってオフラインのときの動作が実現していて、オンラインに復帰したら通信を再試行して同期してくれるということ。だが、オフライン時の通信要求が大量にあったとき、オンラインになったときに連続でAPIを叩き続ける自体にならないのか疑問に思った。

また、ローカルで保持できるデータにも限りがあると思うので、どこまでオフラインで機能を提供し続けられるのかも気になる。

まとめ

Apollo ClientのMutationで、OptimisticResponseを利用してオフラインの動作を実現している部分を調査した。次は、ローカルのキャッシュに書き込んだデータや、実際にサーバーに通信したレスポンスがどのようにUIに反映されるのかを表示部分を調査して学ぶ。