きみはねこみたいなにゃんにゃんなまほう

ねこもスクリプトをかくなり

graphql-subscription を使ってみる - まず PubSub って何

apollographql/graphql-subscriptions を使ってみたくて README の通りのコードを書いて理解しようと思ったのですが、PubSub やら AsyncIterator やら軽く聞いたことはあるもののよく理解していない用語だらけでちんぷんかんぷんでした。なので GraphQL の resolver と絡める前にもっと単純な例で試してみようと、コードを書いて試してみました。

以下のコードでは PubSub のインスタンスから直接 publish を定期的に叩くものです。

import { PubSub } from 'graphql-subscriptions'
const pubsub = new PubSub()

// トリガ名(メッセージの種類を決めるイベントハンドラ名みたいなもの)を指定して
// AsyncIterator というなんか難しいものを作成する
const iterator = pubsub.asyncIterator( 'something_changed')

// 500ms ごとに publish する
setInterval(() => {
  pubsub.publish( 'something_changed', { somethingChanged: { id: '123' } })
}, 500)

// とりあえず 10 回読み取ってみる (*˘꒳˘*) 残りはしらない
for (let i = 0; i < 10; i++) {
  iterator.next().then(console.log)
}

これで

{ value: { somethingChanged: { id: '123' } }, done: false }
{ value: { somethingChanged: { id: '123' } }, done: false }
:

のようなメッセージが10回表示されます。

AsyncIterator についてはまだよくわかっていないのですが、Promise(async/await)for...of を組み合わせられるすごい何か... なんですよね。単純に GraphQL の Subscription を試してみたかっただけなのですが、気をぬくと横道にどんどん逸れていくといういつものパターン...

AsyncIterator

AsyncIterator について簡単に眺めてみようと TypeScript の定義を見ると

interface AsyncIterator<T> {
  next(value?: any): Promise<IteratorResult<T>>;
  return?(value?: any): Promise<IteratorResult<T>>;
  throw?(e?: any): Promise<IteratorResult<T>>;
}

となっています。TypeScript は v2.3 から AsyncIterator に対応しており for..await..of 記法を利用できるみたいです。Iteratorfor...of で用いられるので、それの非同期版ですね。

next() メソッドを呼ぶことでイテレーション中の次の対象を得ることができるのは通常の Iterator と同じで、これだけを見ると返り値が Promise になっているだけの Iterator です。

しかし上のコードを for..await..of で書き換えて確認してみたところ、どうやら際限なくイテレーションが行えるようです。かなり Publish/Subscribe 感のある振る舞いですね。

// 怒られるのでシンボルを定義
(Symbol as any).asyncIterator = Symbol.for('Symbol.asyncIterator')

import { PubSub } from 'graphql-subscriptions'
const pubsub = new PubSub()

const iterator = pubsub.asyncIterator('something_changed')

// 500ms ごとに publish する
setInterval(() => {
  pubsub.publish('something_changed', { somethingChanged: { id: '123' } })
}, 500);

// イテレーションで無限に読み取れるようになった (*˘꒳˘*) しあわせ
(async () => {
  for await (const x of iterator) {
    console.log(x)
  }
})()

TypeScript のドキュメントにあるように Synbol.asyncIterator が未定義であることによるエラーが発生するので

(Symbol as any).asyncIterator = Symbol.for("Symbol.asyncIterator")

を追記しています。参考までに実行時の tsconfig.json をメモしておきます。

{
  "compilerOptions": {
    "target": "es2017",
    "lib": ["esnext"], 
}

イテレータやシンボルなど、今まで知らなくてもどうにかなってきたことがここにきて理解の足かせになってきたので、そろそろちゃんとしないとな... と思ったのでした。

なんにせよ、これで PubSub の大体の振る舞いはつかめたので、次は GraphQL の Subscription として使ってみたいところです。