ES7 async/await でのエラーハンドリング

async/await は ES7 の機能で、非同期処理を記述する上で非常に便利な機能である(仕様は安定していないと思う) まだ実装している処理系はないと思うが、babel などの transpiler をつかうと利用できる

async/await をつかうと非同期処理を以下のように書くことができる

function a() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() { resolve('hello, ') }, 0) 
  })
}

async function b() {
  var value = await a()
  return value + 'world'
}

async function c() {
  var value = await b()
  console.log(value)
  return 'this is async world'
}

c().then(function(val) { console.log(val) })

上のコードを babel-node などで実行すると hallo, world に続いて this is async world が表示される。

async function は Promise を返し、 await キーワードにより Promise が解決されるのを待つ(かのように見せる)ことができるので、async/await をつかうと boilerplate なコードを減らして非同期処理を扱うことができる。

TypeScript 実装での async/await の動作

現在の TypeScript の実装から await の動作をも少し詳しく見てみる(仕様は確認していない。。) 上のコードを TypeScript 1.6 で --target ES6 --experimentalAsyncFunctions オプションをつけてコンパイルすると、以下のようなコードが出力される

// runtime
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promise, generator) {
    return new Promise(function (resolve, reject) {
        generator = generator.call(thisArg, _arguments);
        function cast(value) { return value instanceof Promise && value.constructor === Promise ? value : new Promise(function (resolve) { resolve(value); }); }
        function onfulfill(value) { try { step("next", value); } catch (e) { reject(e); } }
        function onreject(value) { try { step("throw", value); } catch (e) { reject(e); } }
        function step(verb, value) {
            var result = generator[verb](value);
            result.done
              ? resolve(result.value)
              : cast(result.value).then(onfulfill, onreject);
        }
        step("next", void 0);
    });
};
// 上の async function b はこのように展開される
function b() {
    return __awaiter(this, void 0, Promise, function* () { // --- (2)
        var val = yield a();
        return val + 'world';
    });
}
  • async function は必ず Promise オブジェクトを返す。その Promise オブジェクトは
    • async function が return した場合、その値で resolve する
    • async function の中で throw されると、その値で reject する(例外は catch される)
      • 特別な場合として、一度も await されてない状態で例外が投げられると、その例外は補足されることなくそのまま throw される --- (※)
  • await は何らかの値をとり、その値が Promise でなければ、そのままその値を返す
    • 正確には、Promise でなければ Promise.resolve でラップされる
  • await に渡した値が Promise であれば
    • Promise が resolve した場合 > T (resolve した値) を返す
    • Promise が reject した場合 > reject した値をもって (generator を) throw する

となっている。(余談だが、__awaiter のコードは GitHub - tj/co: The ultimate generator based flow-control goodness for nodejs (supports thunks, promises, etc) とほぼ同様である)

async/await のエラー処理

async/await をつかったときのエラーの流れを見てみる。以下のコードは

function a() { return new Promise(resolve) { setTimeout(function() { resolve('hoge') }, 0) } }
async function b() {
  console.log(await a())
  throw 'hello, '
}
async function c() {
  try {
    await b() // --- (1)
  } catch(e) {  // --- (2)
    throw e + 'error'
  }
}
async function d() {
  var val = await c()
  console.log("control don't reach here")
}
d().catch(function(val) { console.log(val) }) // --- (3)
  1. a で発生したエラーは async function 自体が捕まえ、自身が返す Promise を reject する
  2. (1) の await は Promise が reject したのを受けて、例外を投げるが、すぐに (2) で補足される
  3. エラー内容を更新して再度例外を投げるが、b が async function であるためやはり補足され、b が返す Promise を reject する
  4. c でまた同様に、 catch -> reject -> throw -> catch が行われ、(3) で出力される

となる。 内部的には Promise のエラー処理が走っているのだが、async function より先のコードでは .then .catch はいっさい出てこない。 エラー処理の書き方が同期プログラミングに置ける try ~ catch と同様になるので、Promise のエラー処理に手こずった人でも問題なくなっている

まとめ

ES7 async/await でのエラー処理(throw -> catch -> reject -> throw -> ...)の流れを追った。このフローは try ~ catch が多発してあまりパフォーマンスが欲はなさそうだけど、実際に ES7 として実装されるときはもっと効率的なものになるかもしれない(ので今 TypsScript がエミュレートしているコードを見ても仕方ないかもしれない、そもそも async/await の仕様自体まだ固まってない) ただ、今の時点でこれまでよりずっと直感的な記述が可能になっているし、この後そこまで大きな変更はない?かもしれない。tranpiler で使うのもいいけどはやく普通に使いたいですね。

DOM をストーカーする

新年あけましておめでとうございます。

唐突だけど、 tumlbr の検索画面でスクロールしていくと、画面から見えなくなった部分の画像は DOM から remove されていく。 コンソールからdocument.querySelectorAll('img').length とかするとそれを確認すること出来る。

たぶん描画の負荷を減らすためなのかなーと思うけど(DOM書き換えるより display:none した方がいいような気もする?)、とにかく DOM 上からいなくなってしまうので下までスクロールして表示された画像の url を全部集めるとかが出来ない。 そこで DOM をストーキングしてみることにした。 

MutationObserver

昔は DOMNodeInserted というイベントをハンドルするという方法があったようだけど、今は非推奨のようだ。MutationObserver はより新しい機能で、 DOM の挿入だけでなく属性の変更や子孫要素の挿入なども監視することが出来る。Chrome 拡張のような環境ではこちらを使っておけば良さそう。使い方はリンク先を参照。

Let's stalk Tumblr.

上にも書いたように tumblr 検索での img タグは見えなくなった時点で DOM から除去されていく。ひとしきり autopager で表示した後にページ内の画像 URL を集めるためには、消え行く img タグの url を消えない要素の data-attribute に保存してあげれば良さそうだ.

function helper(nodeList) {
    for (var i = 0; i < nodeList.length; i++) {
        var article = nodeList[i];
        var divs = article.querySelectorAll('[data-lightbox]');
        var urls = $(divs).map(function (idx, div) {
            return JSON.parse(div.getAttribute('data-lightbox'));
        }).toArray();
        article.setAttribute('data-image-urls', JSON.stringify(urls));
    }
}
var mutationObserver = new MutationObserver(function (records) {
    records.forEach(function (record) {
        if (!$(record.target).is('#search_posts'))
            return;
        helper(record.addedNodes);
    });
});
var container = document.querySelector('#search_posts');
mutationObserver.observe(container, { childList: true, subtree: true });
var articles = container.querySelectorAll('article');
helper(articles);

こんな感じで実現できる. #search_posts に検索結果が挿入されるので、ページ読み込み時と autopager による要素の追加時に必要な URL の保存処理を行う. やってる処理自体は、子孫要素からそれっぽい url を拾ってちょうど良さそうな先祖要素の data-attribute に追加するだけ。 tumblr の仕様がかわればすぐに使えなくなるようなものだけど、とりあえずストーキングは完了.

Chrome extension

以前つくった Chrome拡張 に上の機能をつけてみた. 個別の Tumblr サイトからのダウンロードはうまくいかないが Tumblr 検索から zip でガッとダウンロード出来る. 年末のイベントの画像収集などにお使いください.

Golang の websocket サンプルを書き直してみた

Golang の勉強がてらちょっとしたツールを作ろうと思って、まず websocket サーバーを書いてみた。 書いてみたと言っても go の websocket chat は golang-samples/websocket · GitHub ここにサンプルがあって、これをそのまま使っても良かったのだけれどいくつか気になったのでよりシンプルに書き直してみた。

気になったのは二つあって、一個は client id が競合しそうに見えた点。 https://github.com/golang-samples/websocket/blob/master/websocket-chat/src/chat/client.go#L35 ここで maxId をインクリメントしている. この NewClient 関数は https://github.com/golang-samples/websocket/blob/master/websocket-chat/src/chat/client.go#L35 ここで呼ばれていて、この onConnected はハンドラとして渡されているので GOMAXPROCS が 1 じゃなければ同時に実行されることがありそうとおもった。インクリメント処理はアトミックじゃない(と思ってる)ので、ここでインクリメントすると maxId を破壊することがありそう?? こういう時例えば Java の Thread とかだと maxId を synchronized で排他制御するのが考えられるけど、go では channel を使って制御するのがオシャレなようだ。

func (server *Server) WebsocketHandler() websocket.Handler {
    return websocket.Handler(func (ws *websocket.Conn) {
        client := NewClient(ws, server.removeClientCh, server.messageCh)
        server.addClientCh <- client
        client.Start()
    })
}

競合を避けるために、server に client 追加用の channel(addClientCh) を持たせ、websocket で接続されたらサーバーオブジェクトに client を渡してその先で client id を入れてもらうようにした。この addClientCh を liesten してるのは単一の goroutine なので競合することはない(が、詰まる可能性はある??)

気になったもう一点は、クライアントをハンドルする goroutine が複数あること。client (クライアント、ブラウザとかをハンドリングするサーバー側のオブジェクト)はクライアントからのメッセージを listen しなくちゃいけないのだけれど、チャットなので他のクライアントからのメッセージを受け取って自身が担当するクライアントに投げないといけない。そのため client はクライアント(=ブラウザ)だけでなく server も listen している。たぶん下の図ような感じになっている

f:id:hatz48:20141221192131p:plain

黒線は生成線で、グレーの線が listen 関係を表している。白い箱が goroutine で色ついてるのがオブジェクトとか。request handler が点線になっているのは揮発性(すぐ終了する)goroutine を表現してみた。 これはパフォーマンス的にはいいのかもしれない(?)けどちょっと複雑になって読みづらかった。クライアント切断時に goroutine を殺し忘れないように終了処理もちゃんとしないといけない。以下のように、client を担当する goroutine が一つになっている方がわかりやすかった。

f:id:hatz48:20141221192136p:plain

client から server を listen する goroutine がいなくなったので、message を送る際に server が client のメソッドを直接呼び出している。そのまま呼び出すと server の goroutine がブロックするのでメソッド呼び出すだけの goroutine を生成してる。これだと client の終了処理も goroutine 一つ潰すだけで済む。

まとめ

Golang の勉強がてら websocket server 書き直してみた。 websocket は状態を持つので常駐する goroutine が複数存在することになるのだけど、なるべく簡素化したい。 とくに双方向に listen し合うと複雑になるなーと感じたのでそうならないように書いてみた。

とここまで書いてはみたものの Golang 初心者なので理解が間違ってるのかもしれない。できれば有益なマサカリをゲットしたい。

chrome 拡張をシークレットモードで有効にする

chrome 拡張は普通にインストールすると、シークレットモードで有効になっていない。 有効にするには拡張機能(chrome://extensions)から、「シークレットモードでの実行を許可する」にチェックを入れればよいのだけど、自分で書いた拡張をこれで有効にして実行したら以下のようなエラーが出た

Unchecked runtime.lastError while running tabs.executeScript: Cannot access a chrome:// URL at ...

試しにシークレットウィンドウから chrome://... の URL(拡張機能の js ファイル) にアクセスしても、禁止されていてみることができない。 シークレットモードからは executeScript することが出来ないのかなーと思って調べてみた

Overview - Google Chrome

ここに、シークレットモード (incognito mode) でのデータ保存に関するポリシーが書いてある。要約すると「ユーザーがどこで何をしたか、シークレットモードからは」保存するなよーと書いてあって、それは確かにという感じ。特に executeScript 出来ないとかは書いてない

いろいろ巡った結果、 manifest.json に一行追加すればいいことがわかった

Manifest - Incognito - Google Chrome

manifest の incognito はデフォルトが spanning になっている。 chrome 拡張は一つのプロセスとして動くのだけど、 spanning だとシークレットモードとそうでない普通のウィンドウに対して、1つのプロセスで処理する。対して split にすると、シークレットウィンドウの拡張は別のプロセスで動く(その拡張のプロセスが二つできる)。 どうやらシークレットモードに対して executeScript を実行するには split でないといけないようだ。プロセスが一緒で executeScript が実行出来ると、シークレットモードじゃない拡張からシークレットウィンドウにアクセス出来てしまいそう?

{
  ...
  "incognito": "split",
  ...
}

で解決した

YAPC::Asia 2014 に参加してきた

ブログを書くまでが YAPC らしいのでブログを書こうと思う(ブログを書かなければいつまでも YAPC ということか)

個人的には最近 Golang に興味があるので Go for perl mongers がすごく勉強になったし、ずっと Web アプリケーションばかり書いていたので ウェッブエンジニアのローレベルプログラミング このセッションもすごく面白かった。 こうして振り返ってみるとほとんど Perl の話は聞いていない気がする。そもそも Perl の話そんんなに多くなかったというと身も蓋もないかもしれない。YA'P'C とは?という感じでもあるけど、 Perl がどうこうということではなく、Perl に親しいエンジニアにはベテランエンジニアが多いのでそういう人たちの話を聞ける貴重なイベントという感じだった。

あと、自分より年下のエンジニアがスピーカーになっているのを見るといろんな意味でいい刺激になる。普段それなりに仕事しているつもりだけどアウトプットはあんまり出来ていなくて、なるべくアウトプットしていこうと思った。来年スピーカーとして話せるよう、がんばろうと思う。