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

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

GraphQL.js を直接使って Subscription を定義してみる

(=˘ ꒳ ˘=) Apollo が目につきやすい GraphQL 界隈だけどやっぱり GraphQL の生の鼓動を感じたい...

lightbulbcat.hatenablog.com

の続きです。GraphQL.js のコードの中に Subscription についての処理が書かれているのを見つけたので、今回は subscribe メソッドの振る舞いを簡単に確認していきたいと思います。

TypeScript の型定義を覗いてみる

先にフィールド定義から見てみます。Query ではフィールドの値を定義するために各フィールドの resolve メソッドを定義しましたが Subscription では subscribe メソッドを定義するようです。GraphQLFieldConfig オブジェクトによると resolvesubscribe が同じ型であることがわかりますね。

// @types/graphql/type/definition.d.ts
export interface GraphQLFieldConfig<
  TSource,
  TContext,
  TArgs = { [argName: string]: any }
> {
  type: GraphQLOutputType;
  args?: GraphQLFieldConfigArgumentMap;
  resolve?: GraphQLFieldResolver<TSource, TContext, TArgs>;
  subscribe?: GraphQLFieldResolver<TSource, TContext, TArgs>;
  deprecationReason?: string;
  description?: string;
  astNode?: FieldDefinitionNode;
}

そう言えば querymutation もデータ変更の有無で使い分けているだけですし subscription もそれと同様、ということなのかもしれませんね。

次に実行メソッドです。Query は graphql で実行しましたが Subscription は subscribe メソッドで実行するようです。こちらも型定義から引数を見てみます。

// @types/graphql/subscription/subscribe.d.ts
export function subscribe(
  schema: GraphQLSchema,
  document: DocumentNode,
  rootValue?: any,
  contextValue?: any,
  variableValues?: {
    [key: string]: any;
  },
  operationName?: string,
  fieldResolver?: GraphQLFieldResolver<any, any>,
  subscribeFieldResolver?: GraphQLFieldResolver<any, any>,
): Promise<AsyncIterator<ExecutionResult> | ExecutionResult>;

graphql メソッドと似ていますが source だったところが document になっていますね。型定義を追いかけたとろこ DocumentNode は GraphQL Schema Language をパースしたAST(抽象構文木)を表しているようで、parse メソッドを利用することで GraphQL Schema Language の文字列から生成することができます。

graphql メソッドは文字列の queryString をそのまま実行できたのですが微妙なところに違いがありますね。

実行コードを書いてみる

とりあえず subscribe を実行することでフィールド定義の subscribe が実行されるところまでを確認したいので以下のコードを書きました。

import { subscribe, parse, GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql'

const schema = new GraphQLSchema({

  // query は省略不可みたいなので適当に定義
  query: new GraphQLObjectType({
    name: 'RootQueryType',
    fields: {
      dummy: { type: GraphQLString },
    },
  }),

  // とりあえず subscribe の呼ばれ具合を確認するだけの定義
  subscription: new GraphQLObjectType({
    name: 'RootSubscriptionType',
    fields: {
      somethingUpdated: {
        type: GraphQLString,
        args: {
          value: { type: GraphQLString },
        },
        subscribe(src, args, ctx) {
          console.log('Given args:', src, args, ctx) // 実行時に渡る引数を確認
        },
      },
    },
  }),
})

const requestString = `
  subscription SomethingUpdated($value: String) {
    somethingUpdated(value: $value)
  }
`

// 怒涛の引数たち (*˘꒳˘*) めげずにがんばる...
const document = parse(requestString)
const rootValue = { value: 'in rootValue'}
const context = { value: 'in context' }
const variables = { value: 'in variables' }
const operationName = 'SomethingUpdated'

subscribe(schema, document, rootValue, context, variables, operationName).then(console.log, console.error)

これを実行したところ、フィールド定義の subscriberootValue variables context の値が渡っていることが確認できました。

Given args: { value: 'in rootValue' } { value: 'in variables' } { value: 'in context' }
Error: Subscription field must return Async Iterable. Received: undefined

そしてエラーメッセージによると subscribe は Async Iterable を返す必要があるみたいです。AsyncIterator については以前 graphql-subscription を使ってみる - まず PubSub って何 - きみはねこみたいなにゃんにゃんなまほう で確認済みで、for..await..of 構文を使うことで非同期に延々とエントリを処理することができるイテレータでしたね。Async Iterable とは for..await..ofof.. の部分に渡せるオブジェクト、ということになりそうです。

つまり GraphQL.js が提供するsubscribe メソッドは非同期なデータパッシングを行うためのインタフェースとしてAsync Iterable を返している、ということですね。

なのでここを見る限り GraphQL.js のスタンスとしては、インタフェースは定義したのであとは利用者が subscribe 中に Async Iterable なオブジェクトを用意するなりして適当に非同期処理を実装しなよ、と言っているように見えます。

ということで... Async Iterable についての理解をまだ深めていない私にはこれ以上の理解はすぐにはできなさそうなので、今回は一旦ここまでとします。

兎にも角にも鍵は Async Iterator ですね。

雑感: node_modules 下を見ていて

最近 GitHub のソースをブラウジングするよりエディタの定義ジャンプを利用した方が断然早いということに遅まきながら気がつきまして、node_modules 内をふらつく頻度が格段に増えました。

Apollo は TypeScript で GraphQL.org は Flow

Apollo は TypeScript で作られているのに対して、GraphQL.js は Flow で書かれているんですね。TypeScript を使いたければ @types/graphql を使うという。 結局 TypeScript/Flow どちらも知らなければ生きづらい世の中になってしまっていますね。

そしてこれもどうでもいいことですが、たまにエディタが Flow で型情報をもってきているのか TypeScript でもってきているのかわからなくなります。

しかしドキュメントを読まなくてもある程度コードが書き進められるので型情報とは良いものです。

Flow の型定義ファイルが .js.flow になっている

node_modules/graphql を見ていて型定義が .js.flow ちょっと感動しました。これでもうあの「あれ、コピペしたけど動かない、何これ、JavaScript の新しい構文なの...?」みたいな悲劇は起こらない... この毎年構文が追加される時代にあって .js に拡張構文をしれっと紛れ込ませてくる facebook のやり方はちょっと辛いところがあります。

いえ、でも、やり方はどうあれ技術は好きであります。GraphQL とか React とか。