世界を動かす技術を、日本語で。

Cap'n Web: ブラウザとウェブサーバーのための新しいRPCシステム

概要

  • Cap'n Web は、TypeScript製の新しい RPCプロトコル で、Web環境に特化した設計。
  • スキーマ不要JSONベース のシリアライズで、シンプルかつ人間に読みやすい通信を実現。
  • 双方向呼び出しオブジェクト・関数の参照渡しPromiseパイプライン など高度な機能をサポート。
  • HTTP/WS/postMessage に対応し、主要なJavaScript環境で動作。
  • TypeScriptとの親和性 が高く、型安全なAPI設計が可能。

Cap'n Webとは

  • Cap'n Web は、 Cap'n Proto の精神的後継であり、Webスタックに最適化された TypeScript製RPCプロトコル
  • スキーマレス設計 で、JavaScriptネイティブのRPCのように使える。
  • シリアライズ形式はJSON を採用し、前処理・後処理のみを実施。
  • HTTP/WS/postMessage に標準対応し、他のトランスポートも容易に拡張可能。
  • 主要ブラウザ、Cloudflare Workers、Node.js などで動作。
  • 10kB未満・依存なし の軽量実装。
  • MITライセンス のオープンソース。

オブジェクト・キャパビリティモデルの特徴

  • 双方向呼び出し をサポートし、クライアント・サーバー双方から呼び出し可能。
  • 関数参照渡し により、コールバック関数をRPC経由で渡し、リモートで実行可能。
  • オブジェクト参照渡し も可能で、 RpcTarget 継承クラスは参照としてリモートに渡せる。
  • Promiseパイプライン により、複数の依存RPCを1ラウンドトリップで実行可能。
  • キャパビリティベースのセキュリティ パターンに対応。

導入例

  • クライアント側は 1行で初期化 し、サーバーのメソッドを直接呼び出し可能。
  • サーバー側は RpcTarget継承クラス を用意し、HTTPハンドラでラップするだけのシンプル構成。
  • TypeScriptインターフェース を使い、クライアント・サーバー双方で型安全な実装が可能。

なぜRPCか?

  • RPC は、ネットワーク越しの関数呼び出しを、ローカルAPIのように扱える通信方式。
  • HTTP/REST ではリクエスト・レスポンスのパースや設計が必要だが、RPCはAPI設計に集中できる。
  • Promise/async/await の普及で、モダンな非同期RPCが実用的に。
  • プログラマの思考モデル に合致し、開発効率向上。

Cap'n Webの利用シーン

  • JavaScript同士のネットワーク通信 全般(クライアント・サーバー、マイクロサービス間など)。
  • 特に リアルタイム協調Webアプリ や、 複雑なセキュリティ境界 を持つ用途に最適。
  • 実験的・先進的なプロジェクト 向け。

主要機能詳細

  • HTTPバッチモード で、WebSocketを使わず一括RPC呼び出しが可能。
    • バッチ内で複数のRPCを一度に実行し、まとめてawait可能。
  • Promiseパイプライン で、依存するRPCをawaitせずにチェーン実行。
    • 返却オブジェクトのメソッドも即時呼び出し可能。
  • オブジェクト・キャパビリティによるセキュリティ
    • 例:authenticate()で認証済みセッションオブジェクトを返し、以降の操作はこのオブジェクト経由でのみ可能。
    • セッションの偽造が不可能で、認可漏れを防止。

TypeScriptとの連携

  • TypeScriptインターフェース を1つ定義し、クライアント・サーバー双方で型安全に利用。
  • 型補完・型検査 が効き、開発体験が向上。
  • ランタイム型チェックは未対応だが、Zod等の導入で補完可能。

GraphQLとの比較(抜粋)

  • GraphQLに類似した柔軟性 を持つが、 オブジェクト指向API設計 が可能。
  • 型安全・高表現力 を両立。

Cap'n Webは、 Web時代の新しいRPC として、 シンプルさ・柔軟性・型安全性 を兼ね備えた革新的な選択肢。 リアルタイム・セキュア・型安全 なAPI設計を求める開発者に最適。

Hackerたちの意見

これはOCapN [0]と似ているところもあれば、かなりの違いもあるね。能力の移転や約束のパイプラインは両方に含まれていて、どちらもスキーマレスなんだ。ただ、Cap'n Webは、OCapNが持っている堅牢参照(sturdyrefs)というURIの形でのバンド外能力がないのが特徴だと思う。この違いがあるから、例ではAPIキー認証が示されてるのかな。誰でもCap'n Webのエンドポイントに接続できるからね。OCapNでは、堅牢参照が推測できないトークンだから、それを持っていることで指定されたエンドポイントにメッセージを送る権限があるんだ。Cap'n Webは、アリスがボブをキャロルに紹介する機能、OCapNの「サードパーティハンドオフ」がないみたいだね。ハンドオフは分散アプリケーションには必要だから、Cap'n Webは伝統的なクライアントサーバーSaaS向けって感じかな、でも少しocapsも入ってる。 [0] https://ocapn.org/

将来的には3PHサポートを追加したいけど、初期リリースでは優先事項じゃなかったんだ。特にブラウザとウェブサーバーの通信を可能にすることに集中してたからね。堅牢参照は扱いが難しい。RPCプロトコル自体にはあまり適してない気がするんだ。堅牢参照を復元するメカニズムは、実行しているプラットフォームにかなり依存するから。例えば、Cloudflare Workersは、まもなくDurable Objectストレージに能力を保存できるようになるかもしれないけど、その仕組みはCloudflare Workersプラットフォームに非常に依存してるんだ。Sandstormも同様に、永続的な能力メカニズムを持ってたけど、それはSandstormの中でしか意味がなかったから、Cap’n Proto自体から永続的な能力の概念を削除したんだ。堅牢参照に関するウェブ標準に最も近いのはOAuthだと思う。OAuthのリフレッシュトークンに基づいて堅牢参照のメカニズムを定義するのも面白いけど、特定のプラットフォーム、例えばSandstormやWorkersの中では、実際には求められるものとは違うかもしれないね。

これ、すごくいい感じで、trpc/orpcの代わりに試すのが楽しみだよ。GraphQLが解決した問題の一つを解決するようだけど、trpcが解決できてない(リスト内のアイテムからネストされた情報やオブジェクトのプロパティをリクエストする能力)みたいだね。ただ、サーバーサイドの問題に対する解決策は含まれてないみたいで、データローダーパターンを解決するために意図されていたものだよね。ナイーブなGraphQLサーバー実装は、リスト内のアイテムごとにデータベースクエリを行うから。サーバーサイドのツールが成熟して、データローダーパターンや永続化/許可リストクエリなどの同等物が出てくるまでは、これをサーバー間(ワーカー間)やクライアントのiframe通信にしか使わないと思う。クライアントサーバー通信は、もっと事前に定義された境界を持たせておくつもり。

一般的に言うと、.map()トリックはサーバーサイドの最適化なしにGraphQLを置き換えることはできないと思う。ただ、データベースがCloudflareのDurable Object内のsqliteで、RPCプロトコルがそれに直接話しかけているなら、N+1セレクトは全然問題ないよ。 https://www.sqlite.org/np1queryprob.html

これ、めっちゃすごいね!Cloudflareの製品だけじゃないのが嬉しい(Cap'n WebはCloudflare Workersと一緒に存在してる)。このセクションを読んでるんだけど、もう少し詳しく教えてくれる? > 現時点では、両者の機能セットは完全に同じではない。時間をかけて、両方に欠けている機能を追加していくことで、マッチさせることを目指している。二つが同じレベルに達したら、その状態は維持されると思う?それとも、Cap'n WebがCloudflare Workersに遅れをとる可能性が高い?もしそうなら、どれくらいの時間差があると思う? [1] https://github.com/cloudflare/capnweb/tree/main?tab=readme-o...

機能的に両者が意味のあるものについては、かなり同期を保つと思うよ。もし何かあるとすれば、Cap'n WebがWorkers RPCよりも先に進むことを期待してる(実際にそうなってるし、新しいパイプライン機能で)。Cap'n Webの実装はWorkersのものよりもずっとシンプルだからね。Cap'n Webは新機能を試す場所になると思う。

RPCは分散コンピューティングの多くの誤謬を犯すとよく言われる。 > でもこの評判は古い。RPCが40年前に最初に発明されたとき、非同期プログラミングはほとんど存在しなかった。プロミスもなければ、asyncやawaitもなかった。ちょっと混乱してるんだけど、もしその核心的な前提が特定の言語の特定の並行性の実装に依存しているなら、これが「プロトコル」ってどういうこと?

どういう意味?非同期プログラミングはたくさんの言語に存在するよ。思いつく限りでは、JavaScript、C++、Python、Rust、C#でasync/awaitを使ったことがあるし…。とにかく、ここでのポイントは、初期のRPCシステムは、ネットワークリクエストを行う間、呼び出しスレッドをブロックすることで動作していたってこと。これは明らかにひどいアイデアだったよね。

"RPC"はもともと、リモートコールが他のメソッドコールと同じように見えるプログラミングパラダイムを指していて、実装がプロセス内か別のマシンかはプログラマーには関係なかったんだよね。これには明らかにワイヤープロトコルやクライアント・サーバーライブラリなどが必要だった。ツールには復活があったけど、今では主に「REST」エンドポイントのように使われていて、関数の型シグネチャを持ってる。FutureやOptionalのようなプログラミング言語の機能は、「これには時間がかかるかもしれない」とか「これが失敗するかもしれない」といった特性を明確に区別するのを楽にしてくれるけど、以前のRPCではこれらの特性はちょっと隠れてたんだよね。

ざっと見た感じ、インポートとエクスポートのテーブルや各オブジェクトの状態を保持するために、ステートフルなサーバーが必要(または強く推奨?)みたいだね。すべての呼び出しがトップレベルで、毎回キーを渡すような従来のRPCシステムの一つの特徴は、連続した複数の呼び出しが異なるサーバーに分散しても問題なく動作することなんだ。ここでも同じことをするために、インポート/エクスポートテーブルをデータベースにシリアライズして保存する方法はあるのかな?それとも、サーバーの親和性やDurable Objectsのようなものが本当に必要なの?

状態は一回のRPCセッションだけで生きてるんだ。WebSocketを使ってるときは、そのWebSocketの寿命がそのままセッションの寿命になる。でも、HTTPバッチトランスポートを使うと、セッションは一回のHTTPリクエストで、すべての呼び出しを一度に実行する感じ。だから、Cap'n Webに関しては、複数のHTTPリクエストや接続を跨いで状態を保持する必要は実際にはないんだよね。つまり、セッションが突然切れて、すべての機能を失うようなプロトコルを設計するのは避けるべきだね。再接続してそれらを再構築できるようにするべきだよ。

これを読んで気づいたんだけど、サーバーのアフィニティはWebSocketを使って実現されてるみたい。HTTPバッチはリクエストを一度に送信して、レスポンスを待つだけ。これがあまり好きじゃないのは、ロードバランシングが難しくなるから。おしゃべりなクライアントが同じサーバーに接続すると、そのサーバーが負担を抱えることになって、オーバーロードの可能性も出てくる。さらに、サーバーのスケーリングも面倒になるしね。持続的な長時間接続は扱いが難しいから、「複数のリクエストが飛んでるとき、どうする?」って問題が出てくる。もう一つあまり好きじゃない点は、タイムリーなクライアントが必要ってこと。クライアントがプッシュイベントのストリームを送り続けて、プルしないだけでDDOSが簡単にできそうだよね。サーバーはクライアントが接続している限り、そのレスポンスを保持し続けなきゃいけないから、これは良くないと思う。

Cap'n'Protoについての知識は限られてるけど(数年前にドキュメントを一度読んだだけ)、サーバーとクライアントはピアスタブを持つことができるんだ。つまり、サーバーCがクライアントBを通じてサーバーAから発信されたスタブを受け取ったら、CはAに直接呼び出しを試みることができるってこと。

配列をどう解決したかのセクションは、面白いけど同時に恐ろしいね。 https://blog.cloudflare.com/capnweb-javascript-rpc-library/#.... > .map()は特別なんだ。JavaScriptのコードをサーバーに送るわけじゃなくて、ドメイン特化型の非チューリング完全な言語に制限された「コード」のようなものを送るんだ。 > でも、アプリケーションコードはただJavaScriptのメソッドを指定しただけだよ。これを狭いDSLにどうやって変換するのか?答えはレコード・リプレイだよ。クライアント側では、特別なプレースホルダー値を渡してコールバックを一度実行するんだ。このパラメータはRPCのプロミスのように振る舞う。ただし、コールバックは同期である必要があるから、このプロミスを待つことはできない。できることは、プロミスパイプラインを使ってパイプライン呼び出しをすることだけ。これらの呼び出しは実装によってインターセプトされ、指示として記録されて、サーバーに送信され、必要に応じて再生されるんだ。

条件分岐は禁止されてると思うけど、フックのルールみたいに、どうやってるの?

C#には、こういうことを扱うための式ツリーがあって、Entity Frameworkが与えられたラムダをSQLに変換できるのはそのおかげなんだ。つまり、実行されるのではなく、検査や変換ができるコードを渡すことができるってこと。例えば、このEntityFrameworkのスニペットを見てみて:db.People.Where(p => p.Name == "Joe") WhereExpression> predicateを受け取るんだ。Funcそのものを取るのではなく、そのExpressionを取ることで、コードを実行するのではなく、見ることができるんだ。それがNameフィールドを"Joe"という値にマッチさせようとしているのを見て、それをSQLのWHERE句に変換できる。JSにはこれがないから、特別なプレースホルダー値を渡して、その値に対してコードが何をしているかを記録しようとしなきゃいけないんだ。

この記録と再生のトリックは、最近Tanstack DBのクエリDSLを実装するのに使ったものにすごく似てるよ(https://tanstack.com/db/latest/docs/guides/live-queries)。where/select/joinのコールバックにRefProxyオブジェクトを渡して、実行されるすべてのプロパティや式をトレースするんだ。他の人も言ってるけど、jsの演算子を使ってアクションを実行することはできないから、トレースできる小さな関数(eq, gt, notなど)を作ったよ。このコールバックは一度だけ実行されて、呼び出しをトレースしてクエリのIRを構築するんだ。驚いたことに、jsのスプレッド操作をトレースできることがわかった。これはJSでインターセプトできる珍しいケースだからね。Kenton、これを読んでたら、リモートでトレースして実行できるように、いくつかのフェイク演算子(eq, gt, inなど)を追加してくれない?

おい、こっち来て!新しいkentonvライブラリが出たよ!GitHubのリポジトリ見たけど、実際に使われてるコードが少ないのに驚いた。これだけで本当に大丈夫なの?理論的にはサーバーサイドを別の言語に移植するのはそんなに難しくないよね?JS/TSのフロントエンド用にElixirサーバーで使ってみたいな。言語の移植って、結構いいLLMのタスクになりそう。これのリポジトリにはLLM生成のコードをたくさん使ったの?数ヶ月前にkentonvが完全にAI生成(もちろん人間のレビューあり)の概念実証をやってたのを思い出すんだけど。

テストの一部はLLM生成だけど、ライブラリ自体はそうじゃないよ。今のところ、LLMがこのライブラリを書くのは無理だと思う。パズルみたいにすごく複雑に組み合わさってるからね。実際にコーディングするより、どうやって正しくやるかを考えるのにずっと時間をかけたよ。自分のworkers-oauth-providerライブラリとは全然違って、あれはよく知られた仕様を新しい(でもシンプルな)APIで実装しただけだったから。コードはPythonみたいな別の動的言語に移植できそうだけど、静的型付けの言語に移植するのは大変だと思う。型がわからないまま任意のオブジェクトをたくさん繰り返し処理してるからね。

再帰関数をmap()に渡したらどうなるの? let traverse = (dir) => { name: dir.name, files: api.ls(dir).map(traverse) // api.ls()はファイルに対して[]を返す }; let tree = api.ls("/").map(traverse);

これ、クライアント側でスタックオーバーフローすると思う。コールバックは.map()を呼び出したときに同期的に記録モードで実行されるから。ネストされたマップは許可されてるけど、このケースは無限にネストされちゃうから、最終的には記録中にスタックオーバーフローすることになるよ。

これ、暇な週末にちょっといじるやつだね。

JSONシリアライゼーション、マジで???なんで「application/octet-stream」ヘッダーにしてArrayBufferをネットワーク越しに送信しないの?