ジンジャー研究室

長めのつぶやき。難しいことは書きません。

Cloudflare Workers + Durable Objects でホワイトボードを作ってみた

f:id:jinjor:20220321214948p:plain

Cloudflare の比較的新しい機能、 Durable Objects を使ってオンラインホワイトボードを作ってみた。

github.com

エレベーターピッチ(テンプレはこちらからお借りしました)

- 1秒で適当な概念図を描き始めて議論をしたい
- ソフトウェア開発者向けの、
- Whiteboard というプロダクトは、
- オンライン共同編集アプリです。
- これは直感的かつ雑念の入る余地のない最低限の操作ができ、
- Google Jamboard とは違って、
- Slack コマンドから一瞬でログインできる仕組みが備わっている。

本当に Jamboard に勝ってるの?まあ、お遊びなので許して。 あと、今のところ組織内で使う想定だから自分でデプロイしないと使えないよ。

Durable Object を雑に紹介

ベータ版の公式のアナウンスが分かりやすい。エッジから状態を管理するオブジェクトにアクセスできる。各オブジェクトは JavaScript クラスのインスタンスで、それぞれ裏側にストレージを持てる。Workers KV と違って強い一貫性がある。

f:id:jinjor:20220321222300p:plain

オブジェクトは ID が同じであれば1個しかないので、例えばチャットアプリで WebSocket を使う場合、部屋に対応したオブジェクトを作っておけば全ての接続を1箇所に集められる。オブジェクトは地理的に有利な位置に自動的に配置されるので、主に国内で使う分にはあまり遅延を気にしなくてよさそう。

上の絵もホワイトボードで描いたよ。

個人的なモチベーション

無料で手軽にアプリ作りたいの民なので大手クラウドを使いたくなくて Heroku とか Netlify とかを使ってたんだけど、エッジで動作してデータも持てるというのは初めて見た。よく見たら無料じゃなかったけど月5ドルだから安い。

で、ずっと触ってみたいと思いつつ作るネタがないなと言ってたら、リモート全盛で手軽に使えるオンラインホワイトボードが欲しくなった。そんなの既にあるじゃん?でも、もっとこう思い立ったらパッと雑に書き始められるシンプルなやつ。ということで Slack コマンドを叩いたら部屋が出来るようにした。

感想

チャットアプリのサンプルコードがあったので、そこから適当にいじれば1週間くらいで終わるかな〜などと甘く考えていたら1ヶ月半かかってしまった。が、結果的には満足のいくものができた。 意外と Workers に癖があるので、一度あらゆる罠を踏みながらテンプレを作っておくと次から楽できそう。

以下、得られた知見など。

基本的にはサンプルと同じ

チャットがホワイトボードになるだけなので、基本的にやることは一緒。部屋の状態を管理するのに Room というクラスを作る。 ただし、今回の仕様では部屋を際限なく作っていいわけではない。存在しない部屋にアクセスしたらエラーにする必要があるし、アクティブな部屋の数に上限を設けるために部屋の数を数える必要がある。ここで問題になるのが、全てのオブジェクトを一覧したり存在確認をするための API が用意されていないこと(REST API にはあるが、これは管理者用でアプリケーション内で使うものではない気がするし、何より遅い)。 仕方がないのでそれ専用の RoomManager なるクラスのシングルトンを作って管理することにした。当たり前だけどシングルトンにしてしまうと分散しないのでまあ微妙。(かといって KV は結果整合だし、いい方法ないかな)

Miniflare を使う

公式のツール wranglerクラウド環境に接続して開発する想定なのか、知らずに本番環境に繋いでしまって驚いたりした。ローカルで動作確認するなら Miniflare を使おう。これはコミュニティ製のツールだったのが本家に取り入れられたものらしい。.env ファイルが読めたり、気が利いている。 ちなみに開発中の wrangler 2.0 ではローカル実行がサポートされるらしい。期待。

Node.js 環境ではない

Node.js の標準モジュールが使えず、代わりに Web 標準の API が用意されている。 Web Crypto API とかはあんまりカジュアルに使えない感じで苦労した。 セキュリティも厳しく eval が使えない。それ自体は別にいいのだが ajv が内部で eval を使っているらしくエラーを出してしまった。困って調べたら@cfworker/json-schemaなるものを発見。なんとかなった。リポジトリを見ると他にも色々あるようだ。

itty-router

express みたいな router 欲しいなと思って探したら、まさに Cloudflare Workers のために作られたitty-routerというライブラリがあったので、便利に使っている。

静的ファイルの配信

「そんなの public フォルダ指定するだけでしょ」って思ったら違って、 Workers KV を使うらしい。(ちなみに手軽な用途であればチャットのサンプルコードみたいに HTML ファイルを直接バイナリとして import することもできる) @cloudflare/kv-asset-handler をインストールしてサンプルの通りにやる。

その中で __STATIC_CONTENT_MANIFEST を import するという妙な仕様があり、そのままだとモジュールを解決できない。結論から言うと TypeScript + esbuild ならこうするのが楽。

// @ts-ignore
import manifest from "__STATIC_CONTENT_MANIFEST";
esbuild --external:__STATIC_CONTENT_MANIFEST ...

これをちゃんとやらないとアップロードしたファイルのパスが解決できない(ハッシュを含めたファイルパスとのマッピングをやっているっぽい)。ちなみに Miniflare は env 変数の中に __STATIC_CONTENT_MANIFEST を含めてくれているが、これは独自仕様なので本番では動かない。

secret を一括で追加

環境変数にあたるものを CLI で暗号化して送信するのだが、1つずつしかできなくて面倒だった。

wrangler secret put <name>

.env から自動で同期するスクリプトを書いた。

GraphQL API で利用状況を確認する

もちろん利用状況はダッシュボードに書いてあるのだが、微妙に自分が見たい情報からピントが外れているというか、このページに書いてあるような料金の計算をするには情報が足りない。 そこで、GraphQL API を使う。 REST API もあるが Analytics 関連は deprecated になっていて現在は GraphQL API のみ。で、 API ドキュメントが見つからないと思って探してみたら、Introspection 機能を使って調べてねとのこと。要するにクエリを投げると型情報とかが返ってくるので自力で探ってみてね、と。なにそれ辛い。 get-graphql-schema みたいなツールもあるけどあんまりパッとした結果も得られなかったので、結局 GraphQL クライアントをインストールしてサジェストを出しながら頑張った。

そして自作ダッシュボードを得た(図は一部)。

f:id:jinjor:20220321235410p:plain

ところで、請求額が無限に膨らむのが怖いので、アプリケーションの仕様として、アクティブな部屋数の上限、編集可能な時間の上限、部屋に入れる人数の上限などを細かく自由に設定できるようにしている。

Durable Object が消えてくれない

Durable Object を消すための API がない。フォーラム によると、ストレージを空にすれば勝手に消えてくれるようなことが書いてある。やってみたが、消えてくれなくて困っている。

Cloudflare 以外でハマったところ

  • Slack の OAuth はブラウザであらかじめワークスペースにログインしていないとフォームが出せない
    • public distribution を有効にするとこの問題は解消するが、サーバー側でワークスペースをチェックする必要がある
  • SameSite 属性が strict だとリダイレクトと同時にクッキーをセットできない
    • lax にした
  • websocket プロトコルに upgrade する時に HTTP エラーを返しても、 JavaScript 側でレスポンスにアクセスする手段がない
    • 一旦 WebSocket 接続し、瞬時に切断するしかない
  • iOS で input にフォーカスするのはユーザーによるアクションと同期していなければいけない
    • ロングタップの処理で setTimeout していたのでダメだった
    • touchend 時にフォーカスすることにした
  • spawn したサーバーを終了しても子プロセスが残る

一言

Durable Objects かなり遊べるよ!