ReactNativeのAsyncStorageをNodeのREPLから操作する

背景

ReactNativeにはAsyncStorageというkey-valueストレージシステムがある。 valueにはstringしか入れられない本当に簡素なものだけど、JavaScriptのプレーンなオブジェクトはJSONシリアライズ可能であるので、さほど困らない。

クライアントサイドで永続化したい情報はAsyncStorageに突っ込んでおく。アプリケーションを作っている過程において、値を少しづつ変更しながら開発を進めたい場面が結構あった。

ところでRailsにはrails consoleというものがあり、railsアプリケーション内のinitialize処理を通した後のREPLを立ち上げることができる。このREPLでは、railsアプリ内のクラスはすでにロード済みであるため、その全てを呼び出すことができる。その中でデータベースアクセスを担うクラス群も呼び出せるため、rails consoleから自由にデータの参照や改変を行える。

普段Railsで開発していることもあり、そんな感じにNodeのREPLからReactNativeのAsyncStorageにアクセスしたいな〜、と思ってそんなツールを作った。

github.com

動作

https://cloud.githubusercontent.com/assets/4954534/26222189/09924ee2-3c54-11e7-8247-ed15afa7cd31.gif

このアプリケーションは、コメントの投稿と同時にAsyncStorageと同期し、コメントリストを永続化している。 REPLからAsyncStorageにコメントを追加し、その後アプリケーションをリロードした時に、コメントが追加されていることを確認している。

仕組み

NodeとReactNativeとの通信はwebsocketを利用している。(ReactNativeは標準でwebsocketをサポートしています。)

REPL起動時にchild.spawnで子プロセスとしてwebsocket serverを立ち上げて、ReactNativeアプリケーションからのレスポンスを受け取る。 REPLのプロセスとwebsocket serverはプロセス間通信を行う。 通信のイメージは以下

f:id:joe-re:20170519070432p:plain

データフローの最後の部分で、websocket serverが結果をファイルに書き込み、それをREPLのプロセスで監視しているところがある。なんで??感がすごい。

今回の構成ではWebSocketを挟んでいたり、そもそもAsyncAtorageはその名の通り非同期なAPIを提供していたりするので、REPLから発行したコマンドがAsyncStorageのAPIを叩いて結果が返るまでは、どう頑張っても非同期処理になる。これを無理やり同期処理にするために、ここでファイルを見ている。

REPLで操作しているのにPromiseが返ってくるのは使いづらい。しかし、Node.jsの世界では、プロセスを止めて処理の結果を待つことが基本的にはできない。(bindingを書けばその限りではないけど。) top levelでawaitする方法があればどうにかなりそうだけど、今のJavaScriptの世界ではそれは許されていない。

Top-level `await` is a footgun · GitHub

Clarifying question: can await be used in top-level code? · Issue #9 · tc39/ecmascript-asyncawait · GitHub

というわけで以下のようになった。

出力結果の監視 is ファイルが書き込まれたかどうか。node-sleepでプロセスを停止させつつ、一定周期でファイル出力がないか監視している。internalなネットワークの通信なので、大抵はほとんど待たずに結果を受け取れるはず。ReactNative側とwebsocketの接続ができていなかったりした場合には、一定時間後にタイムアウトになる。 無理やり感が漂っているので、もっといい方法をご存知の方はぜひ教えてください。

結構便利です

結構便利に使えている。こいつにstorageの状態のスナップショットの取得/復元をする機能とかあってもいいかなー。 興味のある方はぜひ使ってみてください。