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にアクセスしたいな〜、と思ってそんなツールを作った。
動作
このアプリケーションは、コメントの投稿と同時にAsyncStorageと同期し、コメントリストを永続化している。 REPLからAsyncStorageにコメントを追加し、その後アプリケーションをリロードした時に、コメントが追加されていることを確認している。
仕組み
NodeとReactNativeとの通信はwebsocketを利用している。(ReactNativeは標準でwebsocketをサポートしています。)
REPL起動時にchild.spawnで子プロセスとしてwebsocket serverを立ち上げて、ReactNativeアプリケーションからのレスポンスを受け取る。 REPLのプロセスとwebsocket serverはプロセス間通信を行う。 通信のイメージは以下
データフローの最後の部分で、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
というわけで以下のようになった。
execFileで子プロセスで出力処理をさせて、親プロセスではsleepのbindingライブラリで処理を停止(物理)して、出力結果を監視することで無理やり同期的にする荒技に辿り着いた
— じょう (@joe_re) 2017年5月6日
出力結果の監視 is ファイルが書き込まれたかどうか。node-sleepでプロセスを停止させつつ、一定周期でファイル出力がないか監視している。internalなネットワークの通信なので、大抵はほとんど待たずに結果を受け取れるはず。ReactNative側とwebsocketの接続ができていなかったりした場合には、一定時間後にタイムアウトになる。 無理やり感が漂っているので、もっといい方法をご存知の方はぜひ教えてください。
結構便利です
結構便利に使えている。こいつにstorageの状態のスナップショットの取得/復元をする機能とかあってもいいかなー。 興味のある方はぜひ使ってみてください。