ジンジャー研究室

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

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 かなり遊べるよ!

負債展

技術的負債、色々あると思ったので並べてみた。

  • スピード重視負債「ビジネスが軌道に乗るまではスピード重視ね」
  • ビジネス要求の変化負債「頑張ってやってもらったけど、その機能もう要らないわ」
  • マーケティングの失敗負債「鳴り物入りでリリースしたのに誰も使ってくれない...」
  • 要求分析失敗負債「この機能欲しいって言ってたよね...違うの...」
  • 重要顧客負債「どうしても必要な機能だと言うので if 分岐で対応しますね...」
  • 法改正負債「この実装だと今後はダメだって...」
  • 上司の無理解負債「リファクタリングの時間が全然取れないんだけど...」
  • コーディング能力不足負債「なんで動いてるのか分からないけどヨシ...」
  • 技術知識不足負債「そんな綺麗なやり方あったの知らなかった...」
  • 怠惰負債「本当は共通化した方がいいけど、めんどくさいからやめた」
  • TODO 負債「// TODO: あとで時間がある時に直す」
  • 設計負債「この API で実現するの不可能じゃね?いや、ここにフィールド生やせば...」
  • 野心負債「この設計はグローバル展開を見据えたもので...」
  • YAGNI すぎ負債「このコード、後先のこと何も考えてないでしょ...」
  • 中途半端負債「最低限動くところまでは終わったので、残りは Issue に積んで完了にします」
  • 無茶な納期負債「これ1週間でやるのマジ...」
  • 無茶なデザイン負債「この UI を実現するには...専用の API 生やさないと」
  • 人間関係負債「あの部分の担当者、話しかけにくくて...」
  • マイナー機能負債「誰が使ってるか分からないけど...とりあえず残しておいて」
  • プロトタイプ負債「そこは当時 UI でなんとかしてた時の名残りです...」
  • 流行負債「当時はそのフレームワークが流行ってて...」
  • 言語機能負債「当時はその書き方しか出来なくて...」
  • 文化継承負債「意味があるのか分からないけど、昔からこのやり方なので...」
  • 退職負債「あの人が辞めたので分かる人が誰もいません...」

謎ツール

今の会社に入ってから作ったものを思い出してみた(順不同)

  • Slack の新規チャンネルを通知するチャンネル
  • 社員の休暇予定を Google カレンダーに同期したり、今日休みの人を Slack に投稿したりするツール
  • ユーザーストーリーマッピングを管理する Web アプリ(引退)
  • 同僚氏の DSLシンタックスハイライトする VSCode 拡張
  • CircleCI の workflow のボトルネックになっている job を可視化するツール
  • ドキュメントのスクリーンショットが古くないかチェックするツール
  • Elm の UI ドキュメント的なやつ
  • フロントエンドの生成物に feature flag とか SVG パスとか言語とかを色々埋め込むビルドツール
  • フロントエンドのファイルサイズ増減を PullRequest に報告するツール
  • フロントエンドが API を正しく叩いているかをチェックするテストフレームワーク
  • Open API 定義からテスト補助モジュールを自動生成するツール
  • API を動的に生やして、そこに対するリクエストを記録する Web アプリ(テスト用)
  • UI デザインの更新差分を Slack に通知するツール(引退)
  • 深夜に Slack や Twitter にアクセスしないようにするツール(引退)

結構、面白おかしくやってた。

自分のやりたいことを正確に把握するのは難しい

キャリアの話になると、学生に向けて「自分の本当にやりたいことに向き合いましょう」というメッセージが発せられることが多い。それに対する学生側のよくある悩みは「やりたいことが見つかりません」「やりたいことがよく分かりません」というものだ。その様子を見ていて「やりたいことなんて考えなくても常にあるし簡単じゃん」と思ってたんだけど、最近は「本当にやりたいことを把握するのはめちゃくちゃ難しい」と思っている。

以下は個人的な話で、キャリアというよりは趣味レベルの話だけど、それでも難しいと感じている。

他人のやりたいことの影響を受ける

例えば SNS に、個人で Web サービスを作っている人とか、 OSS に貢献している人とかがいると、なんとなく自分もそれがやりたいような気持ちになってしまう。実際に Web サービスを作り始めてみると、ある程度動くようになったところでふと「よく考えたら別に Web サービスを運営したいわけでもないしお金を稼ぐモチベーションもないな」と気づいてしまう。

OSS も自作ライブラリは作るけど、他人のライブラリを一緒に良くしていこうとはあまり思わないので「貢献する」というモチベーションがない。パッケージとして公開するけど、ユーザーから issue や PR が来ると面倒臭いので最近は公開したくないほどだ。

フォローしている人の中には自作のプログラミング言語を作る人も多い。自分も面白そうだなと思ってちょっと作ってみて、ある程度動いたところで「こんなもんか」と思って満足した。でも好きな人は理論を体系的に学んでいるし、何より継続している。やはり冷静になると彼等ほどは好きでないことに気づいてしまう。

仕事の影響を受ける

仕事でなんらかのテーマを扱っていたとして、それが趣味に波及するパターン。例えば、 Swagger が面倒臭いからシュッとしたツールが欲しいみたいな場合に、プライベートでもそういうライブラリを作ったり色々調べ物をしていたりする。でも良く考えたら Swagger はほとんど仕事でしか使わないので、それに時間をかけるのは勿体無い。

あとは繁盛しているサービスでもないのに無駄にスケーラブルなアーキテクチャを考えてしまったりする。もっと手軽な方法もあるが、仕事と全然違うことをすると仕事に活かせなくてコスパが悪いと考えてしまう。

スキルの影響を受ける

仕事で使っているのが Web 関連の技術なので、なんとなく新しいアプリを作り始めると Web アプリになる。本当は VST プラグインが作りたいが C++ が分からないので WebAudio API で作ろうとしたり、その延長で OAuth とか SNS でシェアする機能とかを付けたりしてしまう。しかし WebAudio API をいくらこねくり回しても VST プラグインは作れない。

流行りに乗ってしまう

「これからはこの技術が来る!」とか言われると、「今これに乗っておけば将来それが流行った時に楽できるのではないか」と思って飛びついてしまう。かつては Kubernates であり、今は Rust であったりする。アーリーアダプターはエコシステムが貧弱だろうがものすごい労力をかけて進んでいけるので「将来」を「現在」にすることができるが、ただ「楽したい」程度のモチベーションでは将来は永遠に将来のままだ。ぶっちゃけ既存の道具を使う方が楽。

それから、こういうフロンティアを目の前にすると「画期的なシステムを発明したら一発当てられるのでは?」などと妄想してしまう。それは数年かけて会社を作ってやるくらいの覚悟が必要な話なのだが、 PoC とか言ってそれっぽいものを作って時間を無駄にしたりする。そりゃ PoC は簡単だよ。

ただ手を動かすのが楽しいだけ

プログラミングは楽しいから、ただ手を動かして何かを作って満足したいだけという場合がある。そう自覚しているのならそれでいいのだが、実際には「なんらかの目的のため」にやっていると思い込んで手を動かしていることが多く、ある程度手を動かして満足すると「実はそんな目的はなかったな、俺は一体なんのために...」となったりする。もちろんその過程で得られるものは多いので全く無駄な時間ではないのだが、何かもっと別の目的があるのであれば遠回りだ。

興味の対象が移り変わる

何かに夢中になっていたら、その過程で別の何かを発見してそちらに興味が移るパターン。例えば WebAudio API を触っていたら限界が見えてきたので wasm をやり始めて、 wasm をやっているうちに自作言語を作りたくなったりする。

連想式にやりたいことが増えていくので、優先順位をつけるべきだ。オーディオをやりたいのなら自作言語は我慢して適当な言語を wasm にコンパイルするのが一番早いはずだ。ただ問題は「やりたい順」に優先順位をつけると、その時点では自作言語が一番やりたいことなのでトップに来てしまう。ここで「いや、言語は今は別にいい」と言えなければならない。

短期的な報酬を求めてしまう

これが一番本質的で、ぶっちゃけここまでの話をなくしてこれだけでも話が成り立つのだが、具体的な話があった方が面白いかと思ってここまでダラダラ書いた。

ここでの報酬はほぼ承認欲求のことで、要するに「 Twitter で活動報告するといいねを押してもらえること」だったり「仕事で同僚に褒めてもらえること」だ。まあそれが長期的な目標に向けた取り組みの一環だったりすれば良いのだが、そこで報酬を得るために何かして後に何も残らないのは虚しい。このエントリも正直いいねが欲しくて書いているのでまあ虚しいが、こういう積み重ねが将来的に転職やら何かしら良いことに繋がると信じて書いている。

これが行き過ぎると本当に一発芸しかできなくなる危惧がある。あれをするのもこれをするのも、結局は SNS にアップするためなのだ...そして小さな満足を繰り返して消費される人生。

成果が出ないと面白くなくなる

上と言っていることは同じなのだが、長期的な報酬を得る前のフェーズでコストに対するメリットが伸び悩むと「これは自分のやりたいことじゃなかったな」と思ってしまう。途中で投げ出したことを認めないための防衛本能でもある。 ここまで書いてきたのは「大してやりたくないことに時間をかけてしまう」ことだが、下手をすると「実は本当にやりたかったことだが努力したくないのでやりたくなかったことにした」だけという可能性もある。

今まで「大してやりたかったことじゃなかった」と言って切り捨ててきたものの中に、実は本当にやりたかったことがあっただろうか?と自問するのが良いかもしれない。まあキャリアレベルの話をすると人生やり直さないと無理というのはあるだろうけど。

どうすればいいのか

今まで「気の向くままに色々やってきたが実はあんまりやりたくもないことに時間を割いていた」という話が多かったが、では自分の本当にやりたいことに時間を割くにはどうすればいいのか。ここまで書いてきてなんだが、実はまだ答えが出ていない。おそらく「他人は他人だから振り回されない」のも一つの答えだし、「自分のルーツを探る」も一つの答えだと思う。あるいは「そんなに難しく考えなくても総合的に自分の最もパフォーマンスの出るやり方を本能が選んでいるのだ」と楽観的に考えていいのかもしれない。あと、旅に出るとか。

ただ、一つ確かなのは冒頭の「自分の本当にやりたいことに向き合いましょう」はそんなに簡単ではないということ。 この言葉を聞いて勘違いしていたのは「やりたいことがあっても周りがそうさせてくれないから必死に抵抗しろ」というメッセージだと思ってたんだけど、実際には周りに何をされなくても自分自身をうまくコントロールできないということだった。

ああ、朝がきた。

Electron と Go で音を鳴らしてみた話

SoundEngine とか Audacity みたいな波形編集ソフトを作りたいと思った時に、本当はデスクトップアプリが作りたいんだけど Web 技術の方が慣れてるから(ここでの Web 技術は HTML や JavaScript など)という理由で、わざわざローカルでサーバーを起動してブラウザで WebAudio API を利用するアプリを作ったりしてしていた。でもユーザー体験は確実に悪いので、まずそれが嫌になった。面倒がらずに Electron を使おう。

もう一つ、 WebAudio API はとても良く出来ているのだがレイヤーが高すぎて細かいことができなかったり、パフォーマンスを求めるときは AudioWorklet を使わないといけないのが面倒だった。 AudioWorklet は既存のノードの拡張として使うのが行儀が良いのだろうけど、最近は他の言語で書かれたアプリケーションを丸ごとドーンと wasm に変換して、 WebAudio API は単にオーディオバッファをやり取りするインターフェースとして使う事例が増えていそう。その流れなら WebAudio API は一旦脇に置いてパフォーマンスの良い言語で自由に書いてみようかと。

というわけで、使い慣れた Web 技術で GUI を書きつつ音声処理はパフォーマンスを出すために Electron + Go という組み合わせを採用するに至った。特に Web サービスを作るという動機はないので AudioWorklet や wasm も使わずに両者は IPC で繋ぐことにする。

┌───────────── Electron ─────────────┐
┌────────────┐           ┌───────────┐           ┌───────┐
│ ui         │<-- IPC -->│ core      │<-- IPC -->│ audio │<---- MIDI
│ (TS/React) │           │ (TS/Node) │           │ (Go)  │<---> File
└────────────┘           └───────────┘           └───────┘

サーバーを立てて WebSocket 使うよりパフォーマンス出てるんじゃないかな、多分。プロトコルはシンプルにスペース区切りの文字列を双方向にやり取りすることにする。

{url-encoded} {url-encoded} {url-encoded} ...
{url-encoded} {url-encoded} {url-encoded} ...
...

ここで一番検証したかったのは UI 操作や描画がスムーズかということだったんだけど、ここは何事もなく普通にサクサク動いた。トラブルのひとつやふたつあるかと思ったけど。ボタンを押したらすぐに音が鳴るし、波形も 60fps で描画できた。ということは、これから任意のデスクトップアプリの GUI を Web 技術で作りたくなった場合はこの構成が使えるということなので嬉しい。

今回は上の検証が主な目的でアプリとして作るものはあまり決めていなかったので、最初は波形編集ソフトを作る予定だったのが気づいたらシンセを作っていた。でもシンセを作るなら VST とかで作らないと実際に DAW で使えないよね、ということで一旦この構成での制作は打ち切りにして C++ (JUCE) に移行中。

途中まで作ったもの。 github.com

ちなみに Go のライブラリは hajimehoshi/oto を使わせてもらってます。BGM 制作で参加させてもらっている Odencat 製のゲームなどで実績があります。 github.com

C++ 初心者がハマったこと

普段 Web のぬるい言語しか触っていない C++ 初心者がハマったこと。

(なぜ C++ をやっているかというと VST プラグインを作成するのに C++ がほぼ必須だから。この話はまた改めて書く予定。)

配列がポインタ

関数に配列を渡そうと思ったところ、受け側の引数をどう書けばいいのか分からなかった。 が、どうやらポインタで書くらしい。 ということは、関数側では渡されたポインタの指す先が配列かどうかを知る術はない。 また、長さも分からない。そういう用途では vec を使うっぽい。

C がそうなのは知っていたが、 C++ もそういう所は C の親戚らしい。 「配列かどうかが区別できないとかそんな訳ないだろ」と思って検索しまくって時間が溶けた。

初期値が 0 ではない

宣言だけして初期化していない変数は 0 ではなく適当な値が入っている。

という話は聞いたことはあったものの、 0 で初期化される言語に親しんだ時間が長すぎて無意識下に刷り込まれていたようだ。 enum の初期値がセットされている前提でロジックを組んでいたので意味不明な挙動になった。 (ここからはオーディオ系のプログラミングに特有なのだが、ロジックが間違っていてもノイズが入る程度で致命的な挙動の変化にはならない。「気のせいかな」と思って放置していて間違いに気づくのに3日かかった。)

インデックス例外が発生しない

配列の範囲外にアクセスした時、特に何も言われず何かしらを壊しながら処理が進んでしまう。 そして全然関係ないところで起こるアクセス違反。 どこかでポインタの指す先を誤って解放してしまったか?と思い込んで2時間ほど消えた。

「インデックスが範囲を超えたら例外が発生してプログラムが止まる」というのが、これまた無意識下に刷り込まれているため、「なんか間違ってたら実行時に気づくでしょ」という安心感(?)からロジックをあまり確認せずに軽率に実行する癖がついてしまっているようだ。 C++ でそれをやると間違えていても普通に気づかないので、何かあった時の時間の溶け方が半端ない。

思ったよりポインタでは事故らない

フレームワーク(JUCE)のサンプルに倣っているので、今のところポインタ関連の事故はほぼ起きていない。 まあオーディオ系のプログラミングなので、短いライフサイクルでオブジェクトを生成・破棄していないというのはありそう。ライフサイクルの長いオブジェクトが所有しているオブジェクトへの参照を渡していってるだけ。

事故り始めたらスマートポインタに乗り換えようかな。

時間を溶かさないための対策

とにかくアサーションを入れる。 他の言語に比べて実行時例外が全然出ないので、不安なところにアサーションをどんどん入れていく。間違っていたら IDE が動作を一時停止してくれる。 パフォーマンスが不安な場所は条件コンパイルデバッグ用の処理を差し込んでいく。

ユニットテストでも良さそうだけど C++ でどう書くのかはまだ調べてない。