GraphQLにおけるSubscription処理について(実装例: Amplify + AppSync)


# はじめに
* こんにちは ! KDDI アジャイル開発センターの小板橋です。
* この記事は、[KDDI Engineer Advent Calendar 2020](https://qiita.com/advent-calendar/2020/kddi_engineer)の15日目の記事となります。
* GraphQLの記事はよく見かけますが、GraphQLにおけるSubscription処理についてはあまり目にすることがありません。そこで本記事は、Subscriptionに着目したもので、実装時の助けになれれば幸いです。
* GraphQLについては、以前事細かく書いたので、そちらをご覧ください。[GraphQL入門](https://qiita.com/yoshii0110/items/fbee34d5c235a2a064f9)

# GraphQLにおけるSubscription処理
## 仕組み
### realtime API
pub/subのような仕組みのAPIをrealtime APIというらしいです。
[参考: GraphQL Subscription](https://github.com/graphql/graphql-spec/blob/master/rfcs/Subscriptions.md)

このrealtime APIなのですが、大きく分けて3つのタイプに分類されます。

**1.ポーリング**
クライアントは、定期的にリクエストを発行し、関心のあるデータの状態を確認します。
ポーリングの特徴としては、調整が困難なことです。というのも、更新の頻度が低い場合は、ポーリングは無駄になります。逆に、更新が頻繁に行われると、ポーリングによる待ち時間が長くなります。

**2.イベントベースのサブスクリプション**
クライアントは、1つ以上のイベントを処理することをサーバーに通知します。これらのイベントがトリガーされるたびに、サーバーはクライアントに通知します。
このモデルでは、サーバーがイベントを識別し、それらを事前に通知する方法を特定する必要があります。

**3.ライブクエリ**
クライアントは、クエリを発行します。クエリへの返答が変わるたびに、サーバーは新しいデータをクライアントにプッシュします。
ライブクエリとイベントベースのサブスクリプションの主な違いは、ライブクエリはイベントの概念に依存しないことです。
データ自体はライブであり、変更を通知する方法が含まれています。

### ではGraphQLのSubscriptionは？？
GraphQLのSubscriptionを使用すると、クライアントはサーバーにGraphQLのクエリとクエリ変数を送信します。
サーバーは、これらの入力を特定のイベント処理にマップし、イベントがトリガーされたときにクエリを実行します。

#### Pub/Subの仕組み(ライフサイクル)
Pub/Subのライフサイクルなのですが、次のようになります。

* **Subscribeの設定**
→ クライアントは、クエリとそのクエリ変数をサーバーに送信することにより、Subscriptionを初期化します。Subscriptionが作成されると、クエリとクエリ変数は、Subscriptionを設定しているイベントにマップされます。

* **Subscribeの解除**
→ Subscriptionの登録を解除すると、ペイロードを受信しなくなります。
これは、クライアントが明示的にサブスクライブを解除した場合、またはサーバーがサブスクライブの解除を行う必要があると判断した場合、または、クライアントが切断された場合に発生する可能性があります。
![スクリーンショット 2020-12-14 21.41.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563635/a85b8d75-876a-5272-ef04-71e555a4b1a7.png)

* **Publishの実行**
→ Subscriptionに関連付けられたイベントがトリガーされると、Subscriptionはクエリ、クエリ変数、およびペイロードを実行し、結果をクライアントに送信します。
![スクリーンショット 2020-12-14 21.43.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563635/191b92e5-bd63-1a13-0b5e-43061c2b4276.png)

## 実装する
* 今回はAWSのAppSync + Client側はAmplifyを利用した場合の、Subscription処理を実装していきます。

### AppSyncにおけるSubscription処理
AppSyncのSubscriptionは、ミューテーションに対する応答として呼び出される形になります。
その為、スキーマのミューテーションで指定することで、AppSyncで任意のデータソースをリアルタイム対応にすることができます。
また、AppSyncの便利なところは、AppSyncクライアントのSDKは、Subscriptionの接続管理を自動的に処理するところです。
ちなみに、クライアントとサービス間のネットワークプロトコルとして、WebSocketまたはMQTT over WebSocketsのいずれかを使用するようです。

#### スキーマの設定方法
例えば下記のようなミューテーションがあったとします。

```sample1.graphql
type Post {
 id: ID!
 user: String!
}

input CreatePostInput {
 id: ID
 user: String!
}

type Mutation {
 createPost(input: CreatePostInput!): Post
}

type Subscription {
    CreatePostInput: Post
}
```

そして、通知を受け取る各サブスクリプションに @aws_subscribe(mutations: ["mutation_field_1", "mutation_field_2"]) ディレクティブを追加することで、これらのフィールドをリアルタイムに対応できます。

```sample2.graphql
type Subscription {
    addedPost: Post
    @aws_subscribe(mutations: ["CreatePostInput"])
}
```

これで、AppSync側の設定は良いのですが、気をつけなければならないのは、Client側の実装です。

### Client側の実装
まず重要なことは、client側のSubscriptionの定義の仕方によって欲しいデータを定義になければならない点です。上記の例ですと、下記のようになります。

```
subscription CreatePostInputSub {
    CreatePostInput {
        id
        user
    }
}
```

### Amplify GraphQLクライアントの使用方法
* AppSyncのSubscriptionを使用する場合は、aws-exports.ts(js)のAppSync設定を下記のように設定します。

```aws-exports.ts
Amplify.configure({
  Auth: {
    identityPoolId: 'xxx',
    region: 'xxx' ,
    cookieStorage: {
      domain: 'xxx',
      path: 'xxx',
      secure: true
    }
  },
  aws_appsync_graphqlEndpoint: 'xxxx',
  aws_appsync_region: 'xxxx',
  aws_appsync_authenticationType: 'xxxx',
  aws_appsync_apiKey: 'xxxx'
});

```

* 実際のコードとしては下記のようになります。

```sanple3.ts
import Amplify, { API, graphqlOperation } from 'aws-amplify';
import * as subscriptions from './graphql/subscriptions';

const subscription = API.graphql(
    graphqlOperation(subscriptions.onCreateTodo)
).subscribe({
    next: ({ provider, value }) => console.log({ provider, value })
});
```

Subscriptionにより取得したデータを利用しclient側で処理を行いたい場合は、subscribeにより取得した引数のvalueを使い回すようにすれば良いのです。

```
subscribe({
    next: ({ provider, value }) => console.log({ provider, value })
})
```

### おまけ
ちなみに上の実装だと、tsを利用している場合、GraphQLにおけるSubscriptionの型で怒られます。。
それを回避する方法を下記に残しておきます。
Observableについては、また次回別記事で書いていこうと思います。

```sanple4.ts
import Observable from 'zen-observable'

const subscription = API.graphql(graphqlOperation(subscriptions.onCreateTodo) as Observable<object>
```



# 引用元
[GraphQL Subscriptions](https://github.com/graphql/graphql-spec/blob/master/rfcs/Subscriptions.md)
[AWS公式ドキュメント](https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/real-time-data.html#aws-appsync-real-time-data)
[Amplifyドキュメント](https://docs.amplify.aws/lib/graphqlapi/subscribe-data/q/platform/js)