GraphQLとMessagePackは相性がよさそう

MessagePackはJSONのようなデータをシリアライズできるbinary formatで、JavaScript実装である msgpack-javascriptを基準で考えると次のような特徴があります:

  • JSONよりencodeもdecodeも少し速い
    • かつ、streaming decodeができるので fetch() のresponseのdecodeの効率がとてもよい
    • とはいえ実用上は「JSONより遅くない」ということのほうが重要ではある
  • binaryを直接扱える
    • これに対してJSONでbinaryを扱うときははbase64などでエンコードする必要がある
  • timestamp型があり、デフォルトではJSのDateにマッピングされる
    • Intl.DateTimeFormatへの入力としてならこれで必要十分
    • マッピングをあとから変えることはできる

特にバイナリを直接扱えるのはJSONとくらべて非常にすぐれた点で、たとえばGraphQL APIを実装するときはファイルのアップロードの扱いに常に悩まされるわけですが、MessagePackを使えばオーバーヘッドなしにバイナリを扱えます。

あとは現在は手元にコードは一切ないのですが、GraphQL schemaを読みながら適切にGraphQLのcustom scalar typeを適切なオブジェクトにマッピングするMessagePack decoderがあるとさらに捗りそうです。

MessagePackを単なるJSONの代替として考えるとJavaScript的にはメリットに乏しいのですが、JSONよりも表現が豊富で拡張性にすぐれたシリアライズフォーマットと捉えるとGraphQLと相性がいいなあ…ということを考えています。というわけで最近MessagePack for JavaScriptの実装を頑張っているのでした。まずは優れた実装がないと話になりませんからね。

JavaScript libraryのREADMEにブラウザ互換性表バッジを表示することについての諸々

ブラウザ互換性表 (a.k.a. browser matrix) とは、こういうやつです。

f:id:gfx:20190513093113p:plain
ブラウザ互換性表 powered by Sauce Labs

TypeScriptで MessagePack encoder/decoder を実装した - Islands in the byte stream で作った msgpack/msgpack-javascript にこのバッジをつけようとして苦労しました。今回は単に一度やってみたかったというのもあって頑張りましたが、いろいろ大変だったので記録を残しておきます。

しかし、どんなプロジェクトでもやるべきかというと微妙で、ブラウザの機能に大きく依存するライブラリでもない限りはバッジは頑張らなくてもいいかなあという結論です。ブラウザに依存した機能をもっと多用するのであれば、バッジの価値があるのかもしれません。

今回はブラウザテストをはじめてから1つだけIE11で発現するバグを直したんですが、修正サイクル自体はIE11 on VirtualBoxで回しましたしね。

一方で、CI環境という意味ではIE11などテストするのが少し面倒なブラウザでCIを回すのは意味があるとは思います。

ブラウザ互換性表バッジサービス

SauceLabsTestingBottestling などがあります…が、testlingは今は動いてなさそう? SauceLabs は時間制限があるもののfree trialがあって、OSSの場合は別途Open Sauceを申請することもできます*1

今回は一番つかわれてそうなSauceLabsにしました。なおSauceLabsにsignupしたら即アカウントがdisableされました…。問い合わせたら「乱用防止ロジックが誤動作したのかも。確認するのでテストしたいリポジトリを教えてくれる?」みたいなやり取りをして有効にしてもらったりしました。サポートのやりとり自体はスムーズにできたのでよかったです。

SauceLabsはそれなりに設定すればちゃんと動きます。が、今回使ったKarmaだと設定方法がいまいち不明で、githubで filename:karma.conf.js saucelabs などして動いているものをみつけて参考にする、などのがんばりが必要でした。

Karma

任意のユニットテストをブラウザで実行するフレームワークです。

karma-runner.github.io

karma-chrome-launcherkarma-firefox-launcher などを使ってヘッドレスブラウザで動かすとローカルでもサクッと動くしTravisなどでも実行できます。IE11も modern.IE のVMをもともとVirtualBoxにいれていて、 karma-virtualbox-ie11-launcher で無事テストできました。

SauceLabs用の karma-sacelabs-launcher もありましたが、READMEに書いてあることがあまり信頼できないわ複数のブラウザを起動するテストが進まないバグがあって1つづつ起動する必要があるわと、ほんとにこれメンテされてるのかってレベルで荒れてました。最終的には動きましたが、バッジを表示させるのでなければターゲットはIE11だけでいいかもしれません。

さらに、TypeScriptのためには karma-typescriptkarma-webpack (+ts-loader) を使う必要があります。前者は結局モジュールのバンドルを自前でやっているようで、それなら一般的に使われているであろうwebpackを利用する karma-webpack にしました。ただ karma-webpack もイマイチ安定していなくて、現行の karma-webpack 3.x + ts-loader だとなぜか node_modules/ 以下のdtsを読んでくれません。やむを得ず transpileOnly: true で型チェックはしないことに。とはいえ、いずれにせよ他のフェーズで型チェックはするのでこれによってテスト結果が信頼できなくなるということはないのでいいでしょう。

さらに、 msgpack-javascriptmocha + NodeJS assert module でテストを書いています。ブラウザで動かすにはこのassert moduleのpolyfillを行なわなければならず、webpackはデフォルトでこのpolyfill差し込みを行います。それを管理しているのがこのモジュール:

github.com

これによれば assert module のpolyfillは browserify/commonjs-assert なんですが、NodeJS用のテストを実際にブラウザで実行しても動きません。どうも commonjs-assert 1.x はベースにしているNodeJS assertが古かったようです。ちょうど最近 commonjs-assert が v2.0.0 になって最新のNodeJSに追従したんですが、 node-libs-browser のほうがまだこのメジャーバージョンアップに対応されていないので webpack 組み込みのpolyfillを無効化して事なきを得ました。というか、webpackがnodejs modulesのpolyfillを自動で差し込むのはいろいろ罠が多いのでデフォルト無効化してほしいくらいです。

最終的には karma.conf.ts はこうなりました。

https://github.com/msgpack/msgpack-javascript/blob/master/karma.conf.ts

ローカルのNodeJSテストが5秒くらいで終わるのに対して、Karma+ChromeHeadlessテストが8秒くらい、Karma+FirefoxHeadlessでのテストが15秒くらいです。これくらい高速に実行できるとブラウザでユニットテストを走らせるのも苦ではないですね。

まあしかし、設定は苦労しましたが、総じて見るとKarmaはよく出来てるなあと思います。Headless ChromeやHeadless Firefoxの安定版が普通に使えるようになったこともあり、ユニットテストをブラウザで実行するのはわりと簡単にできるようになりましたね。

*1:その後 Open Sauce 申請が通ったので時間制限なしで使えるようになりました

TypeScriptで MessagePack encoder/decoder を実装した

npm install @msgpack/msgpack でインスコできます。NodeJS v12 でベンチマークしたかぎり、JSONと同程度の速度で、これまで最速といわれてきた msgpack-lite よりもさらに少しだけ高速です。

github.com

もともとこのリポジトリには uupaaさんによる実装(tagged as classic) があったんですが、メンテされなくなって久しく npmjs.com にもリリースされていないという状況でした。

https://github.com/msgpack/msgpack-javascript/tree/classic

その後kawanetさんが msgpack-lite を実装したのが2015年。これが2019年現在、もっとも週間ダウンロード数の多いMessagePack for JSの実装です。

msgpack-lite ピュアJavaScript実装の速いMessagePackライブラリ - Qiita

msgpack-liteはちゃんと動いてメンテされていて stream encoder / decoder もある優秀な実装なんですが、NodeJS向けに最適化されていて、実際くだんのmsgpack-liteの紹介エントリでもBufferに大きく依存しているとあり、ブラウザサポートは「Bufferのpolyfillがあればブラウザでも動く」という水準です。用途を察するに fluentd client for NodeJS で使うことを想定しての実装であり、これはこれでベストな実装です。

ただ今回はブラウザ向けに最適化された実装がほしかったのと、ES2018 (async iteration) とTypeScriptという条件で設計したらいろいろ変わるはずだなあという思いがありました。

要件

  • ES2018+ な設計と実装
    • TypedArraysに依存した設計やESクラスの構文などを活用したい
    • AsyncIterable<Uint8Array> をうけとるストリーミングデコーダがほしい
  • ES5 (IE11) でも動く
    • そうはいってもまだIE11サポートを完全には切れないので…
    • TypeScriptのdownlevel compile (target: "es5"downlevelIteration: true を利用) と polyfill (core-js) を想定する
  • streaming decoder
    • Fetch APIの streaming download と繋いでstreaming decodeすると、特に回線の遅い環境で効果的だと思われる

実装

ブラウザ向けに最適化といいつつ、いろいろがんばったら NodeJS v12 ではmsgpack-liteより速くなりました。@msgpack/msgpack はDataViewに大きく依存していて、 Improving DataView performance in V8 · V8 が実装されたV8が入ったのがNodeJS v12 だからこそですね。

なお↓のベンチマークは mspgack-lite に同梱されているものを流用しています。 Buffer.from(JSON.stringify(...)) はI/Oを想定しているからで、JSON.stringifyの戻り値であるJavaScriptのstringはUTF8 encodeしてバイト列にしないとファイルに保存したりネットワークで転送したりできないため、ベンチマークの公平性という観点から入れたと思われます。MessagePackのエンコードはバイト列を生成するのでそのままI/Oに載せられるため、加工はしていません。

なおベンチマークはいまのところNodeJSでしか行ってませんが、@msgpack/msgpack はECMA-262の機能しか使ってないのでChromeでも同じ傾向になるはずです。またMessagePack実装はmsgpack-liteとしか行ってませんが、(すくなくとも2015年時点では)msgpack-liteが最速ということだったので他の実装との比較はここでは省略しています。

Benchmark on NodeJS v12

NodeJS v12 だと encodeは Buffer.from(JSON.stringify(obj)) よりも少し速く、decodeはJSONほぼ同水準という結果になりました。

Benchmark on NodeJS/v12.1.0

operation op ms op/s
buf = Buffer.from(JSON.stringify(obj)); 493600 5000 98720
buf = JSON.stringify(obj); 959600 5000 191920
obj = JSON.parse(buf); 346100 5000 69220
buf = require("msgpack-lite").encode(obj); 358300 5000 71660
obj = require("msgpack-lite").decode(buf); 270400 5000 54080
buf = require("@msgpack/msgpack").encode(obj); 594300 5000 118860
obj = require("@msgpack/msgpack").decode(buf); 343100 5000 68620

Benchmark on NodeJS v10

参考までに NodejS v10 の結果も載せておきますが、見ての通りかなり遅いです。

Benchmark on NodeJS/v10.15.3

operation op ms op/s
buf = Buffer.from(JSON.stringify(obj)); 369500 5000 73900
buf = JSON.stringify(obj); 942100 5000 188420
obj = JSON.parse(buf); 356800 5000 71360
buf = require("msgpack-lite").encode(obj); 283300 5000 56660
obj = require("msgpack-lite").decode(buf); 211200 5000 42240
buf = require("@msgpack/msgpack").encode(obj); 147000 5000 29400
obj = require("@msgpack/msgpack").decode(buf); 132400 5000 26480

ユースケース

Kibela Web API (beta) がMessagePack (application/x-msgpack) を扱えるので、こんな感じで decodeAsync を使えます。

github.com

まだブラウザでのテスト(特にIE11)が十分でないので @msgpack/msgpack のバージョンは v1.0.0 にしてませんが、ブラウザでの挙動を十分に確認したら v1.0.0 にしようと思っています。

エンジニアHubにTypeScriptの記事を書きました

employment.en-japan.com

「GraphQL」徹底入門 ─ RESTとの比較、API・フロント双方の実装から学ぶ に引き続き、TypeScriptの記事を書きました。

TypeScriptに苦手意識を持っている人に向けて再び触るきっかけを作りたいと考えて書いたので、細かい言語仕様にはまったく触れてませんが、「ああこれならやってみてもいいな」と思ってもらえたら幸いです。

TypeScriptの型情報を利用したCustom Transformerの現状確認

TypeScriptのコンパイラ・プラグインとして振る舞いASTの操作を行えるcustom transformer (AST preprocessor) が実装されたのは TypeScript 2.4 (2017年) でした。

そのときの様子は次のエントリに非常によくまとまっています。

[TypeScript 2.4] custom transformer を利用して実行時に型情報を参照可能にする - Qiita

さて、現在のTypeScript v3.3 でのTransformer APIの状況はというと、TypeScript compiler本体としては依然としてドキュメントに乗ってない水準の扱いです。特に、コンパイラAPI そのままではtransformerは型情報を利用できません。ts.TransformerFactory に渡される ts.TransformationContextts.Programts.TypeChecker が存在しないためです。これは、TypeScriptコンパイラ開発チームによれば意図的な設計ということです。

Include TypeChecker in TransformationContext so that custom transformers can use type information · Issue #25147 · Microsoft/TypeScript

(あると便利だから再検討してほしい!とコメントはしましたが、望みは薄いでしょう)

つまり、標準的なtransformerができることは、babel pluginとできることに本質的な違いがない、ということで、これではあまり面白くないですね。

しかし先に紹介したエントリではコンパイル時型情報を実行時型情報(=JavaScriptのオブジェクト)に変化しています。コンパイラAPIを駆使すればtransformerが型情報も扱えるということですね。しかしそれはtransformer用にカスタムコンパイラを作ることに等しいため、transformerは開発するのもテストするのも利用するのも難易度が高い状態です。

とはいえ、2019年現在では、有志によって開発されたプラガブルなカスタムコンパイラでtransformerの開発・利用が少しだけ楽になりました。

  • ts-loader
    • getCustomTransformers はtransformerに ts.Program を与えます
    • ts.Program#getTypeChecker()ts.TypeChecker にアクセスできるので、これを通じて型情報を利用できます
  • ttypescript (TypeScript with Transformers)
    • TypeScript compilerのかわりに利用できるカスタムコンパイラで、tsconfig.jsonにtransformerを設定できます
      • 設定できるtransformerは ts.Programts.TypeChecker を受けとるようにできます
    • ts-node--typescript オプションでカスタムコンパイラを設定できるため、 ts-node --typescript=ttypescript とすると ts-node でも型情報を利用するtransformerを利用できます
    • ts-loadertypescript オプションで ttypescript を利用できます

このあたりは先のエントリの作者による ts-transform-keys のREADMEに詳しく載っています。

https://github.com/kimamula/ts-transformer-keys

とはいえ、有志によるカスタムコンパイラに依存しすぎるのもあまり望ましくないので、できれば標準のコンパイラAPIとして型情報へのアクセス方法を解放してほしいところです。

Transformerが型情報にアクセスできると何ができるか

ぱっと考えただけですが、次のようなことができます。

まず、TypeScriptの型情報を React の prop types に変換できるはずです。通常であればTypeScriptの型情報はコンパイル時に失われるので次のような typeToPropTypes<T>() は実装しようがないのですが、transformerがこの関数呼び出しをprop typesに置き換えてしまえばいいというわけです(まったく試していませんが、このアイデアを実装したtransformerはすでにあるようです https://github.com/joelday/ts-proptypes-transformer )。

// これはイメージです
interface Props { /* ... */ }

cass SomeComponent extends React.Component<Props> {
  static redonly propTypes = typeToPropTypes<Props>();
}

次に、TypeScriptの型情報をGraphQLのクエリ(クエリフラグメント)に変換できるのではないかと考えています。

// これはイメージです
interface Tweet {
  author: { id };
  content: string;
  tweetedAt: Date;
}

const GetTweet = gql`
  query {
    tweets {
      edges {
       // { author { id }; content; tweetedAt } に変換する
        ...${typeToQueryFragment<Tweet>()}
      }
    }
  }
`;

というわけで、TypeScriptの型情報を利用したtransformerを安定して開発・利用できるようになるといろいろ夢がありますね。ただし、繰り返しになりますが、現在はその利用のためにTypeScriptのカスタムコンパイラを作る必要があります。プロダクションに入れる際は自己責任でどうぞ。

Rails.application.credentials.foo は fetch(:foo) にすべき

表題のとおりで、fetch(...) はエントリが存在しないと例外を投げるため、エントリが存在しないときに nil を返すアクセサよりもtypoに対して安全です。

こういうのは rubocop-rails でカバーできるといいなと思って起票して、良さそうというコメントはもらったんですが:

https://github.com/rubocop-hq/rubocop-rails/issues/42

いかんせん rubocop-hq/rubocop-rails 自体がまだ開発中なのでいつ使えるかわかりません。

こういうときにquerlyを使うといいですね。

(cf. プロジェクト固有のルールを指定できるLinterであるところのQuerlyがめちゃ便利 - Islands in the byte stream)

というわけでruleはこんな感じです。

rules:
  - id: kibela.prefer_fetch_for_rails_secrets
    pattern:
      subject: "Rails.application.'credentials.'key()"
      where:
        key: '/.+/'
        credentials: '/(?:secrets|credentials)/'
    message: "`Rails.application.secrets.fetch(...)` を使いましょう。"
    before:
      - "Rails.application.secrets.foo"
      - "Rails.application.credentials.foo"
    after:
      - "Rails.application.secrets.fetch(:foo)"
      - "Rails.application.credentials.fetch(:foo)"

SafariにはMediaQueryList.prototype.addEventListenerがない

ないと困るので、 polyfillをつくりました。MediaQueryList すら実体がないので、やむをえず mathMedia() を上書きしています。

// MediaQueryList.prototype.addEventListener.ts 

if (typeof matchMedia !== "undefined" && !matchMedia("all").addEventListener) {
  console.log('installing polyfill: MediaQueryList.prototype.addEventListener');

  const originalMatchMedia = matchMedia;
  self.matchMedia = function matchMedia(mediaQuery: string): MediaQueryList {
    const mql = originalMatchMedia(mediaQuery);
    mql.addEventListener = function (eventName: "change", listener: (event: MediaQueryListEvent) => void) {
      // tslint:disable-next-line
      this.addListener(listener);
    };
    return mql;
  };
}

なぜないかというと、このAPIを定めたCSSOMで、最初は addListener というメソッドでイベントリスナを登録していたのが、途中からDOM標準の EventTarget interface を実装するものとして addEventListener を利用可能にしたから、ということのようです。

https://drafts.csswg.org/cssom-view/#mediaquerylist

Note: This specification initially had a custom callback mechanism with addListener() and removeListener(), and the callback was invoked with the associated media query list as argument. Now the normal event mechanism is used instead. For backwards compatibility, the addListener() and removeListener() methods are basically aliases for addEventListener() and removeEventListener(), respectively, and the change event masquerades as a MediaQueryList.

こういう経緯で TypeScript の lib.dom.d.ts では addListener がdeprecated扱いではあるものの、素直に addEventListener を使えない、と。こういうのはpolyfillするのがいいですね。npmにもありそうですが、実装するのは簡単なので自前実装ということにしました。

なぜTypeScriptに失望してしまうのか

追記: この件に関してエンジニアHubにもTypeScriptの記事を書きました: TypeScript再入門 ― 「がんばらないTypeScript」で、JavaScriptを“柔らかい”静的型付き言語に - エンジニアHub|Webエンジニアのキャリアを考える!

TypeScriptに対する失望は2パターンあって、その理由は理解できるのですが、いずれにせよそこでTypeScriptを捨てる判断をするのはもったいないと思っています。この2つの失望を感じたとしてもなお、TypeScriptには導入する価値があると思っています。

パターン1: 実はJavaScriptに対する失望である

そこらのブログやTwitterで観測していると、理由の7割くらいこれです。これは、TypeScriptが独立した言語ではなくJavaScriptへのトランスパイラ(言語変換ツール)であり、独立したランタイムを持たないことに由来します。

TypeScriptはJavaScriptの上位互換(=すべてのJavaScriptは、構文的にはTypeScriptとして正しく、コンパイルが通ればほぼJavaScript同じ振る舞いをする)なので、JavaScriptのいまいちイケてない言語機能をそのまま継承してしています。

なので、TypeScriptを単体の言語として評価するとありえないレベルのイケてない機能がたくさんありますが、それはTypeScriptではなくJavaScriptの欠点であることがほとんどです。

これについては仕組み上どうしようもないので、JavaScriptを学んでJavaScriptの罠にはまらないようにするしかありません。

パターン2: "noImplicitAny": true がつらくて失望する

"noImplicitAny": true というのは、暗黙の any を禁止するオプションで、tsconfig.json に設定します。

これをtrueにすると、型推論の効かない箇所(関数の引数など)で型アノテーションが必要になります。また、ライブラリのimport時にそのライブラリが型情報(.d.ts)を提供していることを要求します。

この「ライブラリに型情報を要求する」というのが制約として非常に強いため、チームの開発者全員がTypeScriptのエキスパートでないかぎりは、型付けに際して消耗を強いられます。そして消耗を強いられたメンバーは「TypeScriptを導入したらDXが下がった」と感じる可能性が高いでしょう。

私は "noImplicitAny": false を奨励しています。型付けに振り回されないように、がんばらないTypeScriptでいいと思っています。それでもJavaScriptに比べれば静的にチェックできる範囲がずっと広いので、DXの向上やアプリケーションの安定した運用には十分役にたちます。

アプリケーションコードではではなるべく型をつけたほうがいいので、ライブラリの型情報を要求しない弱い "noImplicitAny": true があればいいんですけどね。

最近は tsc --init で生成する tsconfig.json"noImplicitAny": true をデフォルトにしていますが、強い気持ちでこれをfalseにして「がんばらないTypeScript」を実践することをおすすめします。

TypeScriptはこの先どうなるか

ほかに代替がないし、考えられもしないので、生き残るでしょうね。

単体の言語としてはもっとまともなaltJSはすでにいくつかあります。しかし、JSがブラウザで動作する唯一の高級言語(低級言語としてはWASMがあるので)である限りは、それの限りなく薄いラッパーであるTypeScriptは価値を持ちしつづけるでしょう。

もっとも、発展的解消として別の名前を与えられて規格化する未来はあると思います。たとえば、SwaggerがOpenAPIとして規格化されたように。

エンジニアHubに『「GraphQL」徹底入門』という記事を寄稿しました

書きました*1

employment.en-japan.com

GraphQLという規格そのものについての解説が前半で、後半はRailsアプリのWeb APIをRESTful APIからGraphQL APIに書き換えるというハンズオンです。

GraphQLの特徴やら Swagger, gRPC, etc との比較はわりとよく見かけるので、そのあたりについては最低限触れるにとどめて、GraphQLという規格について知るための入門記事として書きました。特にRelay (Relay Server Specification) についてはあまり日本語の説明を見ないので、この記事できちんと紹介できてよかったなと思っています。

サンプルコードは https://github.com/gfx/graphql-blog にあります*2

この記事は結構長いんですが、それでも発展的な使い方に触れる余裕はあまりありませんでした。このあたりはまたどこかでフォローできたらと思います。

この記事で触れていないトピックについては、私を含めた数名が書いている次のscrapboxに少しあったりはします。 ただしこのscrapboxは graphql-ruby 専用です。

Notes on GraphQL for Ruby

*1:執筆期間は1ヶ月くらいでした。

*2:余談ですが、このサンプルコードのwebpackの設定(webpack.config.js)は、GraphQLとは関係ないので該当記事では一切ふれていませんが、それなりに真面目に書いてます。

potatotips #56 で 「OSS開発のリテラシー / Android編」という発表をしました

追記: 加筆修正してGitHub projectにしました: https://github.com/gfx/android-oss-best-practices


paper.dropbox.com

(potatotips #56 (iOS/Android開発Tips共有会) - connpass)

久しぶりのpotatotipsでのLTでした。

「OSS開発のリテラシー」は他の言語版もほしいですね。誰か書いてくれないかな〜|д゚)チラッ