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

apollo-boost はサンプルコード専用と思ったほうがよい

追記: Apollo Boost は Apollo Client に統合される見込みのようです。Apollo Client 3.0 Roadmap · Issue #33 · apollographql/apollo-feature-requests · GitHub


TypeScript用のGraphQL clientであるところの Apollo ですが、これのGet Startedなどのドキュメントでは apollo-boost というパッケージを使っています。

www.apollographql.com

しかし、 apollo-boost はサンプルコードをシンプルにするための apollo-client などのラッパーと考えたほうがよく、プロダクションコードでは apollo-boost ではなく apollo-client および関連モジュールを直接使うほうがいいです。

以下理由:

  • apollo-boost は "zero-config" な apollo-client のラッパーだが、カスタマイズ性に乏しい
    • カスタマイズ不要なときに apollo-boost を使い、カスタマイズが必要になったら apollo-client を使え、という位置づけのよう
    • apollo-boost から apollo-client への移行ガイドもある:
    • Apollo Boost migration | Apollo Client
    • Apollo Boost is a great way to get started with Apollo Client quickly, but there are some advanced features it doesn’t support out of the box.

  • 特に apollo-client の強力な機能である "link" (Rack middleware のようなもの) をカスタマイズできないので、バッチリクエストや GraphQL subscriptions を利用する方法がない
  • エラーハンドリングも細かく制御したいときは apollo-client を生で使うほうがよい
  • 公式ドキュメントで import ApolloClient from "apollo-boost"import { ApolloClient } from "apollo-client" の2パターンがあるが、この2つの ApolloClient は(継承関係にあるものの)コンストラクタのオプションが全く違うので別物と考えたほうがよく、非常に混乱する

特に、 apollo を使うとすぐ batch request をしたくなるでしょうから、プロダクションコードでは最初から apollo-boost を使わないで apollo-client を使うほうが移行する手間が省けるというものです。

Reactをv16にアップグレードしたらuglifyjsの秘孔をついてハマった件

追記: Terser v3.10.11 でこの問題が修正されていることを確認しました。現在は collapse_vars: false というワークアラウンドは不要になりました。


webpackのproduction buildの話です。

Reactが悪いわけではないんですが、たまたま秘孔をつくコードがReactないし関連するReact v16に依存するライブラリにあったんでしょうね。

というわけで webpack.config.js の optimization の設定はこんな感じにしました。

if (production)
  config.optimization = {
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          sourceMap: true,
          parallel: true,
          compress: {
            collapse_vars: false, // workaround for a minifier's bug: https://github.com/terser-js/terser/issues/120
            drop_console: true,
          },
        },
      }),
    ],
    noEmitOnErrors: true,
  };
  // ...
}