TypeScriptで MessagePack encoder/decoder を実装した
npm install @msgpack/msgpack でインスコできます。NodeJS v12 でベンチマークしたかぎり、JSONと同程度の速度で、これまで最速といわれてきた msgpack-lite よりもさらに少しだけ高速です。
もともとこのリポジトリには 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
を使えます。
まだブラウザでのテスト(特にIE11)が十分でないので @msgpack/msgpack
のバージョンは v1.0.0 にしてませんが、ブラウザでの挙動を十分に確認したら v1.0.0
にしようと思っています。
エンジニアHubにTypeScriptの記事を書きました
「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.TransformationContext
に ts.Program
や ts.TypeChecker
が存在しないためです。これは、TypeScriptコンパイラ開発チームによれば意図的な設計ということです。
(あると便利だから再検討してほしい!とコメントはしましたが、望みは薄いでしょう)
つまり、標準的なtransformerができることは、babel pluginとできることに本質的な違いがない、ということで、これではあまり面白くないですね。
しかし先に紹介したエントリではコンパイル時型情報を実行時型情報(=JavaScriptのオブジェクト)に変化しています。コンパイラAPIを駆使すればtransformerが型情報も扱えるということですね。しかしそれはtransformer用にカスタムコンパイラを作ることに等しいため、transformerは開発するのもテストするのも利用するのも難易度が高い状態です。
とはいえ、2019年現在では、有志によって開発されたプラガブルなカスタムコンパイラでtransformerの開発・利用が少しだけ楽になりました。
- ts-loader
- getCustomTransformers はtransformerに
ts.Program
を与えます ts.Program#getTypeChecker()
でts.TypeChecker
にアクセスできるので、これを通じて型情報を利用できます
- getCustomTransformers はtransformerに
- ttypescript (TypeScript with Transformers)
- TypeScript compilerのかわりに利用できるカスタムコンパイラで、tsconfig.jsonにtransformerを設定できます
- 設定できるtransformerは
ts.Program
やts.TypeChecker
を受けとるようにできます
- 設定できるtransformerは
ts-node
は--typescript
オプションでカスタムコンパイラを設定できるため、ts-node --typescript=ttypescript
とすると ts-node でも型情報を利用するtransformerを利用できますts-loader
もtypescript
オプションでttypescript
を利用できます
- TypeScript compilerのかわりに利用できるカスタムコンパイラで、tsconfig.jsonにtransformerを設定できます
このあたりは先のエントリの作者による 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。
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 専用です。
potatotips #56 で 「OSS開発のリテラシー / Android編」という発表をしました
追記: 加筆修正してGitHub projectにしました: https://github.com/gfx/android-oss-best-practices
(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
というパッケージを使っています。
しかし、 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に依存するライブラリにあったんでしょうね。
- Reactをv16にアップグレードしたら webpack で production build ができなくなった
- pluginを着脱しながら調べた結果、 uglifyjs がクラッシュしていることがわかった
- uglifyjs は現在 オリジナル + 2つのforkがあり、すべてで再現する
- original: https://github.com/mishoo/UglifyJS
- fork 1: https://github.com/mishoo/UglifyJS2 (a.k.a. uglify-es)
- fork 2: https://github.com/terser-js/terser (uglifyjs2 の fork)
- ugfilyjs-webpack-plugin は v0.x で uglifyjs に依存し、 v1.0 は uglify-es に依存を変えていたが、2018年9月にリリースされた v2.0 から uglifyjs に戻している
- それによれば、 uglify-es はメンテされていないとのこと
- Release v2.0.0 · webpack-contrib/uglifyjs-webpack-plugin · GitHub
- uglifyjsの系譜でメンテされているものは、現状では terser のみということに
- terserではいまこの問題の最小限のコードが特定された、という状況なので、修正の期待がもてる
- TypeError: Cannot read property '_walk' of null · Issue #120 · terser-js/terser · GitHub
- このissueによれば
collapse_vars: false
にすることで問題は回避できるそうで、試してみるとそのとおりだった
というわけで 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, }; // ... }