6 ポイント 投稿者 GN⁺ 2025-09-23 | 1件のコメント | WhatsAppで共有
  • Cap'n WebTypeScriptで実装された新しいRPCプロトコルで、Web環境に最適化され、さまざまなJavaScriptランタイムで動作する
  • スキーマや煩雑なボイラープレートなしで、JSONベースのシリアライズ人間が読めるデータ形式を提供する
  • オブジェクトケーパビリティベースのモデルにより、双方向呼び出し、関数・オブジェクト参照の受け渡し、Promiseパイプライニング、セキュリティパターンの実装が可能
  • WebSocket, HTTP, postMessage など多様なネットワーク環境をサポートし、10kB以下の軽量オープンソース
  • GraphQLに似たwaterfall問題の解決だけでなく、一般的なJavaScript APIのような自然なRPCモデリングを可能にする

Cap'n Webとは何か

  • Cap'n WebはCloudflareが開発したTypeScriptベースのオープンソースRPC(protocol)システム
  • Cap'n Protoに着想を得ているが、別途スキーマ定義なしで動作し、JSONを活用した人間にやさしいシリアライズ方式を採用
  • TypeScriptと統合されており、自動補完や型チェックなど開発者体験を向上させ、ランタイムの型検証は別途(type guardなど)で処理できる
  • HTTP, WebSocket, postMessageなどのネットワークプロトコルをサポートし、主要ブラウザ・Cloudflare Workers・Node.jsなどで動作
  • 依存関係のない軽量な構造で、minify + gzip時に10kB未満で提供される

Cap'n Webのオブジェクトケーパビリティベースモデル(OCap)

  • オブジェクトケーパビリティ(object-capability)ベースのモデルを採用し、既存のRPCシステムより多様な表現が可能
    • 双方向呼び出し: クライアントとサーバーが互いに関数を呼び出せる
    • 関数・オブジェクト参照の受け渡し: 関数やオブジェクトをRPCで渡すと、相手はスタブを受け取り、呼び出し時に元の場所で実行される
    • Promise Pipelining: 複数のRPCをチェーンでつなぐ際、1回のネットワーク往復で処理
    • セキュリティパターン: 権限付与やセッション管理などのセキュリティ制御を自然に実装できる

基本的な使い方

  • クライアントの例

    import { newWebSocketRpcSession } from "capnweb"  
    let api = newWebSocketRpcSession("wss://example.com/api")  
    let result = await api.hello("World")  
    console.log(result)  
    
  • サーバーの例(Cloudflare Workerベース)

    import { RpcTarget, newWorkersRpcResponse } from "capnweb"  
    class MyApiServer extends RpcTarget {  
      hello(name) {  
        return `Hello, ${name}!`  
      }  
    }  
    export default {  
      fetch(request, env, ctx) {  
        let url = new URL(request.url)  
        if (url.pathname === "/api") {  
          return newWorkersRpcResponse(request, new MyApiServer())  
        }  
        return new Response("Not found", {status: 404})  
      }  
    }  
    
  • APIへのメソッド追加、クライアントのコールバック関数の受け渡し、TypeScriptインターフェースの定義と適用を簡単に行える

RPCとは何か、そしてCap'n Webの特徴

  • **RPC(Remote Procedure Call)**は、ネットワーク上の2つのプログラムがあたかも関数呼び出しのように通信できるようにする概念
  • 従来のHTTP/RESTプロトコルと異なり、RPCでは関数呼び出しの抽象化によって開発者の思考様式に一致するコードを書ける
  • Cap'n Webはasync/await, Promise, Exceptionサポートなど、最新のJavaScriptの流れとよく合う
  • RPCをめぐる歴史的な論争(同期呼び出し、ネットワーク障害)とは異なり、現代のJS環境ではより安全かつ効率的に利用できる

Cap'n Webの活用シナリオ

  • 2つのJavaScriptアプリケーション間でネットワーク通信が必要なあらゆる環境で活躍
    • クライアント-サーバー、マイクロサービス間呼び出しなど
    • 特にリアルタイム共同作業Webアプリや、複雑なセキュリティ境界をまたぐ相互作用に適している
  • 実験的な段階にあり、新しい技術の導入に前向きな開発者にとって特に有益

さまざまな機能

HTTPバッチモード

  • 継続的な接続が不要な場合、HTTPバッチ(batch)モードで複数のRPC呼び出しを一度にまとめて処理できる

    import { newHttpBatchRpcSession } from "capnweb"  
    let batch = newHttpBatchRpcSession("https://example.com/api";)  
    let result = await batch.hello("World")  
    console.log(result)  
    
  • 1つのバッチ内で複数の呼び出しを同時実行し、結果を並列に受け取れる

    let promise1 = batch.hello("Alice")  
    let promise2 = batch.hello("Bob")  
    let [result1, result2] = await Promise.all([promise1, promise2])  
    

Promise Pipelining(チェーン呼び出し)

  • 前の呼び出し結果を待たずに、その結果をすぐ次の呼び出しの引数として使う方式をサポート

  • 例)getMyName()の結果Promiseをそのままhello()に渡し、1回のネットワーク往復で処理

    let namePromise = batch.getMyName()  
    let result = await batch.hello(namePromise)  
    
  • Cap'n WebのPromiseはプロキシ(proxy)オブジェクトとして動作し、追加メソッド呼び出し時も遅延なくチェーン処理できる

    let sessionPromise = batch.authenticate(apiKey)  
    let name = await sessionPromise.whoami()  
    

セキュリティ: 認証とオブジェクトケーパビリティ

  • authenticateメソッドにより成功時に権限(セッション)オブジェクトを割り当て、その後は追加認証なしで機能を呼び出せる
  • 従来のRPCと異なりセッションオブジェクトを偽造できず、認証なしでは権限が必要なメソッドにアクセスできない
  • WebSocketの構造的な制約を自然に克服し、認証ロジックの一貫性を保証
  • TypeScriptでAPIインターフェースを宣言すれば、クライアント〜サーバーに自動適用でき、自動補完と型安全性を確保できる

GraphQLとの比較とCap'n Webの差別化ポイント

  • GraphQLはRESTのwaterfall(多段階呼び出し)問題を緩和するが、新しい言語・スキーマ・ツールチェーンの導入が必要

  • Cap'n WebはJavaScriptコードだけでwaterfall問題を解決し、

    • Promiseパイプライニング / オブジェクト参照のサポートにより、ネストした呼び出しや複合トランザクションロジックを自然にモデル化できる
    let user = api.createUser({ name: "Alice" })  
    let friendRequest = await user.sendFriendRequest("Bob")  
    
  • GraphQLの複雑さや学習・運用コストなしに、JavaScript APIのように活用できる

配列演算(array.mapなど)と最適化

  • Cap'n Webでは配列の各要素に対して追加のネットワーク往復なしでmap演算が可能

  • mapコールバック関数をクライアントで一度実行して演算内容を記録(record-replay)し、サーバーへ送信してサーバー側で一括処理する

    let friendsWithPhotos = friendsPromise.map(friend => {  
      return {friend, photo: api.getUserPhoto(friend.id)}  
    })  
    let results = await friendsWithPhotos  
    
  • 制限付きのドメイン特化言語(DSL)により、JavaScript関数のように表現しつつ、実際にはCap'n Webプロトコルを活用して複数呼び出しを最適化処理する

内部プロトコル構造と通信フロー

  • JSON + 特別な前処理による構造化データ転送で、配列・日付などの特別な型もサポート
  • 対称的なプロトコルによりクライアント・サーバーの区別なく双方向通信が可能
  • 各パーティ(例: AliceとBob)はexport/importテーブルを管理し、オブジェクト・関数参照をIDで区別
  • push/pullメッセージやPromise IDの割り当てなどにより、1回のラウンドトリップで多数の呼び出しを反映できる

現状と適用事例

  • Cap'n Webはまだ実験的なオープンソースで、Cloudflare Wranglerのremote bindingsなど実サービスで活用されている
  • 追加のブログ投稿やさまざまなフロントエンド実験も予定されている
  • MITライセンスで公開され、誰でも自由に適用可能
  • GitHubリポジトリはこちら

1件のコメント

 
GN⁺ 2025-09-23
Hacker Newsのコメント
  • 2つ気になる点がある。

    1. RPCのセマンティクスが更新されるアプリのデプロイをどう扱うのがよいのかが気になる。つまり、クライアントとサーバーが同じバージョンのRPCを使っていることをどう保証するのか、という質問だ。Protocol Buffers(gRPC/Avroなど)はこの問題を直接解決しようとしている。
    2. 不安定なネットワーク接続をどう扱うのがよいのかも気になる。export/importテーブルが状態を持つWebSocket接続に直接結びついているので、接続が切れると状態を失うはずだと思う。理論上はクライアント/サーバーが状態をキャッシュして再接続時に復元することも可能だろうが、テーブルにクロージャが含まれうるのでシリアライズが難しく、メモリ問題も起こりうると思う。チームでどう考えたのか気になる。
      本当に革新的な仕事だと思う。
      1. 既存の呼び出し元を壊さずにJavaScript APIを更新するのと同じように考えるとよい。ローカル関数呼び出しで守るべき互換性ルールと同じ基本を守れば、新しいメソッドやオプショナル引数などは追加してよい。
      2. 接続が切れたら再接続して、オブジェクトを最初から再構築する必要がある。実際のReactアプリでは、最上位コンポーネントにメインのRPCスタブを引数として渡す。このコンポーネントが下位オブジェクトを複数作って子へ渡す。接続が切れたら新しいスタブを作って最上位コンポーネントに再度渡す。すると他のstate changeと同じように再レンダリングが起き、すべての子が必要な下位オブジェクトを再fetchする。
        コールバックを持つ購読(subscription)オブジェクトがあるなら、開始時点で「最後に見たメッセージ」を指定できるようにAPIを設計すべきだ。そうすればすぐ続きからデータを受け取れ、途中で取りこぼさない。
        こういうデザインパターンをブログ記事シリーズとして一度まとめるべきかもしれない。
  • 配列の問題をどう解決したのかに関するセクションが本当に興味深く、同時に少し怖くもある。ブログリンク
    .map()の場合、サーバーにJavaScriptコードそのものを送るわけではないが、「コード」のような何かを送っていて、これは限定的なドメイン固有言語(DSL)を使っている。クライアント側でコールバックをプレースホルダー値を入れて一度実行してみて、その動作をrecord-replay方式で追跡し、サーバーへinstruction setを送る。サーバーではそのinstructionを受け取り、配列の各メンバーごとに実行することになる。
    つまり開発者はただJSメソッドを使っているだけなのに、実際にはこれを狭いDSLへ変換するトリックが使われている。コールバックは同期的にしか動作できず、awaitは不可能。その代わりpromise pipeliningだけを許可して、その過程をすべて捕捉してサーバーへ渡し、サーバーでは必要に応じて再実行する。

    • C#にはこの種の問題を扱うexpression treeがある。Entity Frameworkがラムダ式を受け取ってSQLクエリに変換するときにそれを活用している。つまり、コードを実行せずに走査したり変換したりしながら利用できる。
      たとえば、db.People.Where(p => p.Name == "Joe")では、Whereは実際のpredicate関数を受け取るのではなくexpressionを受け取るので、渡されたコードを走査してNameフィールドが"Joe"と一致するかを確認し、SQLのWHERE句へ変換する。
      JavaScriptにはこうした仕組みがないので、プレースホルダー値を入れてどう動くかを一つひとつ記録して真似しているわけだ。

    • 最近Tanstack DBのクエリDSLを作るときにも、このrecord-replayのトリックを使った。ガイドリンク。where/select/joinのコールバックにRefProxyオブジェクトを渡し、そのオブジェクトに対してどんなprop/演算が発生したかを追跡する。
      JSでは通常の演算子(==、> など)を直接フックできないので、eq/gt/notなどの追跡可能な小さな関数を作り、コールバックを一度だけ実行して連結された式を捕捉し、IRにする。
      驚いたことに、JSのspread演算子まで追跡できた。
      Kenton、もし可能ならこの概念をcapnwebにもfake operator(eq、gt、inなど)として追加して、リモートトレーシング機能を入れられないだろうか。

    • 条件分岐は禁止されているように見えるが(まるでReact Hooksのルールのように)、そうした制約をどう実装しているのか気になる。

  • このプロジェクトは興味深い。
    MLコンパイラライブラリ(TensorFlow 1、JAX jit、PyTorch compileなど)に似た側面がある。トレーシング方式でoperation graphを作り、それをcompileしたりVM向けに変換したりして実行する。
    現在は動的言語をフロントエンドとして新しいDSLを定義するのではなく、既存のスクリプト言語の中にAST変換を隠している。
    MLではGPU/linalgカーネルの実行を遅らせてカーネルを融合するが、Cap'n WebのようなRPCではネットワークリクエストを遅らせて複数のnetwork callをまとめられる。
    結局のところinstruction/data planeを分離するのが核心であり、ごく小さなスケールの単一CPUでさえ分散システム的な構造(命令/データキャッシュ分離)を持っている。
    Cap'n WebではRPC graph自体がinstructionの役割を果たす。
    こういうパターンは本当に面白いが、スタック構造(compilerの上にinterpreter、その上にcompiler...)が無限に繰り返されるようにも感じる。Lispyの code is data, data is code パターンの別バージョンを見ている気分だ。何かもっと根本的に深いストーリーがありそうだ。

    • 本当に同感だ――これを普遍的な抽象とみなす視点がすばらしい。
      動的言語はいまや新しいDSLのフロントエンドになりつつあり、その代わり新しい文法を定義するのではなく、スクリプトの中にAST生成を溶け込ませている。
      ここでゲームチェンジャーになるのがTypeScriptだと思う。JavaScriptのランタイムの柔軟性(Cap'n Webが巧みにProxyを使っているように)と型安全性を同時に得られるからだ。
      最近はORM界隈でこの考え方にハマっている。ほとんどのORMは直列的でeagerな方式なので、クエリ実行直前にしか操作できない。
      本当にcomposableなORMはcompilerのように動くべきだと思う。TypeScriptでSQL上に完全に型安全なDSLを定義してクエリASTを作り、最後にだけSQLへcompileすればよい。
      私が開発中のTypegresもまさにこのアイデアそのものだ。こういうパターンに興味があるなら参考になると思う。
  • RPCライブラリの本質的な問題は、round-tripがどこでどう発生するのかを隠そうとする点にある。
    Cap'n Webのarray .map()だけ見ても、実際にどこでnetwork round-tripが起きるのか分かりにくい。
    これは「機能」ではなく、むしろ「バグ」だと思う――コードを見れば動作がすぐ分かるべきで、それを隠すのは望ましくない。
    参考リンク

    • round-tripはawaitを使ったときに発生する。
      promise pipeliningでは複数のstatementをawaitなしで並べて設定できるので、その途中で追加のnetwork往復は発生しない。最後に一度awaitすれば、それで全部だ。
  • gRPCとWebを使ったことがあるなら、ProtobufをWebに適用するのがどれほどつらいか分かるはずだ。
    Cap'n Webのシンプルさは本当に良い。capnprotoドキュメント
    Cap'n WebはCap'n Protoと違ってスキーマがまったくない。不要なboilerplateがほとんどないので、Cloudflare WorkersのJavaScriptネイティブRPCにかなり近い感触がある。
    github参考

  • kentonvの新しいライブラリを見つけてすぐ飛んできた。
    GitHubに上がっているコードを見ると、意外なほど規模がとても小さくて驚いた。これで全部なのか気になる。
    理論的には、サーバーサイド側を別の言語に移植するのもそれほど難しくなさそうで、私はElixirサーバーとJS/TSフロントエンドで使ってみたくなった。
    LLMにこういう言語移植をやらせるのも面白そうだ。もしかしてこのリポジトリにLLMベースのコードは入っているのだろうか。数か月前にkentonvがAI生成(人間がレビューした)POCを作ったという話を見た覚えがある。

    • テストの一部にはLLMが生成したものを使ったが、ライブラリ本体はまったくそうではない。
      現時点では、LLMがこのライブラリを作るのは難しかったと思う。内部構造が非常に精巧にかみ合ったパズルのように設計されている。
      実際のコードより設計を考える時間のほうが長かった。
      well-known specをnovelな形で実装するworkers-oauth-providerライブラリとはまったく違う。
      コード構造は動的言語、たとえばPythonには移植しやすいかもしれないが、静的型付け言語では難しいと思う。任意オブジェクト型に依存する部分が多いからだ。
  • OCapNとの類似点と重要な違いもある。参考
    どちらもcapability transfer、promise pipelining、schemalessモデルをサポートしている。
    Cap'n Webには、OCapNのsturdyref(復元可能なURI)のようなout-of-band capabilityがない。このためAPI key認証が必要なのだろうと推測している。sturdyrefは一種の推測不能なトークンで、それを持っていればそのエンドポイントへのアクセス権が得られる。
    またCap'n Webには、AliceがBobをCarolに紹介するような三者ハンドオフ機能がない。これは分散アプリには必須なので、その意味でCap'n Webは伝統的なSaaSスタイルのclient-server用途にocap的な特徴を加えたサービスにより近い。

    • 3PHサポートは後で追加したいとは思っているが、今回の初期リリースではブラウザ<->Webサーバー通信に焦点を当てることのほうが優先順位が高かった。
      SturdyRefはプラットフォームごとに復元方法が異なるので、RPCプロトコルレベルよりも各プラットフォームに合わせて実装すべきだと考えている。
      たとえばCloudflare Workersでは、近いうちにDurable Object storageでcapabilityの永続化が可能になる予定だが、実装方法はWorkersプラットフォームに特化している。
      Sandstormにもpersistent capabilityはあるが、内部サービスに限られる。
      そのためCap’n Protoではpersistent capabilityの概念自体を取り除いており、Web標準でそれに最も近いものはOAuthだ。
      OAuth refresh tokenベースのsturdyref定義も想像はできるが、特定プラットフォーム以外でも使える構造にはならない。
  • ざっと見たところ、このシステムはimport/exportテーブルやオブジェクト状態をサーバー側でstatefulに保持することを要求している、あるいは推奨しているように見える。
    従来のRPCでは、すべての呼び出しがトップレベルに入り、各呼び出しにkeyなどを渡すので、複数サーバーにリクエストが分散されても問題ないが、Cap’n Webはそうではない。
    テーブルをシリアライズしてDBに保存することで同じようにサーバー分散できるのか、それともサーバーaffinityやDurable Objectsのような構造を必ず要求するのかが気になる。

    • 状態は単一のRPCセッションでのみ保持される。
      WebSocketを使う場合、WebSocket接続が維持されている間だけ状態も生きている。
      HTTP batch転送を使う場合、セッションは単一のHTTPリクエスト全体に限定され、その中で全呼び出しが一度に処理される。
      したがって、複数のHTTPリクエスト/接続にまたがって状態を保持する必要はCap’n Webにはない。
      ただし、セッションが途中で切れるとすべてのcapabilityを失ってしまうような設計が問題になるなら、そうしたデザインは避けるべきだ。いつでも接続を張り直してcapabilityを復元できるようにしておく必要がある。

    • ドキュメントを読む限り、WebSocketでaffinityを保つ構造に見える。
      HTTP batchingはすべてのリクエストを一度に送り、応答を待つ方式だ。
      こういう方式はロードバランシングが難しくなる。チャットクライアントが多いと特定サーバーに接続が集中する構造になりうる。その結果、そのサーバーが過負荷になるおそれがある。
      サーバーのスケールイン/アウトも面倒になる。長期接続を維持しながら複数のリクエストを同時処理していると、管理は非常に難しい。
      もう一つ、クライアントが常に応答を受け取らずpushイベントだけを送り続けると、サーバーはその応答をずっとメモリに保持しなければならず、DDoS攻撃が容易になると思う。

    • 以前Cap'n Protoのドキュメントを読んだときの記憶では、サーバーとクライアントはpeer stubをやり取りできる。
      サーバーCがクライアントBを介してAで生成されたstubを渡された場合、CはAを直接呼び出すこともできる。

  • RPC is often accused of committing many of the fallacies of distributed computing.

But this reputation is outdated. When RPC was first invented some 40 years ago, async programming barely existed. We did not have Promises, much less async and await. この部分を見て混乱した。RPCの中核的な前提が特定の言語や並行性モデルに強く依存するなら、どうしてこれがプロトコルたりうるのかが気になる。

  • 「RPC」はもともと、リモート呼び出しが内部関数呼び出しと区別不能に見えるようにするプログラミングパラダイムだ。
    実際にはそのためにwire protocolやクライアント/サーバーライブラリなどが必要になる。
    最近では認識がかなり変わっていて、RESTエンドポイントのように関数シグネチャを持つ構造が主流だ。
    Future、Optionalなどのプログラミング言語機能が現れたことで、「この動作は遅延する可能性がある」「失敗する可能性がある」といった性質を明確に区別できるようになった。
    過去のRPCではこうした属性がすべて隠されていた。

  • どういう意味なのか気になる。非同期プログラミングはさまざまな言語に存在する。JavaScript、C++、Python、Rust、C#など、ほぼすべて使ったことがある。
    要するに、初期のRPCシステムはネットワークリクエストが進行している間、呼び出しスレッドをブロックする構造で、これは本当に悪い設計だったので、今では非同期が当然になったということだ。

  • Cap'n WebがCloudflare製品だけに縛られず独立して存在しているのが非常に楽しみだ。
    ドキュメントのこの部分を読んで気になった。

    as of this writing, the feature set is not exactly the same between the two. We aim to fix this over time, by adding missing features to both sides until they match. 両者がfeature parityに達したあとも今後ずっと同期を保つつもりなのか、それとも最終的にはCap'n WebがCloudflare Workersに後れを取る構図になるのか、その差がどれくらい開くのかも気になる。

    • 両製品は、少なくとも共通して意味のある機能については、ほぼ継続的に同期していく計画だ。
      むしろCap'n WebのほうがWorkers RPCより先行する可能性すらあると思う(実際、パイプライン機能はすでに先を行っている)。
      Cap'n Webの構造のほうがずっと単純なので、新機能の実験もまずCap'n Webで行うことになるだろう。