
2026/04/11 17:53
何もしなくて大丈夫です。この文章は既に体裁が整っています。 ただし、あなたの指示「不要な記号を削除する」の解釈については、「タイトルとしての装飾記号(ハイフンやアンダーバーなど)」が含まれている場合は削除しますが、今回の入力にはそのような記号は見当たりません。 ご提示された文章はそのまま使用できます: What We Learned Building a Rust Runtime for TypeScript
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
Encore は、重要なパフォーマンスボトルネックを解消し、マルチスレッドのインフラストラクチャ処理を可能にするため、コアランタイムを Go から Rust に基づいて根本的に再構築しました。以前は、Node.js と Go サイドカル之间的 IPC シリアライゼーションオーバーヘッドにより、リクエスト遅延が 2〜4ms を超えていましたが、Rust への移行に伴い、現在では I/O およびネットワーク処理を管理する高性能レイヤーを利用し、TypeScript コードは純粋にビジネスロジックに集中できるようになっています。このアーキテクチャにより、アプリケーションは標準的な Express.js サーバーのthroughput の最大 9 倍を実現でき、検証遅延も劇的に削減できます。この移行には、ネイティブバインディング用の特別製クレートを開発し、共有メモリの使用を最適化するために Pingora API ゲートウェイをプロセスに直接統合する作業が含まれ、その過程で Pingora への Windows サポートの提供も行われました。現在では、ユーザーはシリアライゼーションオーバーヘッドなしでネイティブな Windows 互換性と簡素化したローカル開発を手に入れることができます。さらに、ランタイムは HTTP ライフサイクル全体、データベース管理、パブ/サブ、トレーシング、およびメトリクスを管理するために 67,000 行の Rust コードを活用しており、アプリケーションメタデータはコンパイル時 に TypeScript パーサーによって生成され、ランタイム設定はデプロイ時に Protobuf を通じて処理されます。
napi-rs のスレッドセーフな呼び出し用のカスタマイズや将来のドロップに対応する CancellationGuard の実装など、エンジニアリング上の課題が存在するにもかかわらず、この移行は高トラフィックワークロードを複数のクラウドプロバイダで処理可能でありながら、簡素化されたデプロイ構成を維持できる堅牢でスケーラブルなフレームワークを提供しています。本文
Encore はもともと、Go ランタイム、Go CLI、Go パーサー、そして Go コンパイラーを備えた Go フレームワークとしてスタートしました。TypeScript サポートを決断した際、真っ先に思いつくのはランタイム自体を TypeScript で記述することか、あるいは何らかのブリッジを用いて Go ランタイムを拡張することでしたが、結局私たちは新しいランタイムを完全にゼロから Rust で再構築する道を選びました。
この決定には、Go サイドキャーのプロトタイプが示唆した理由以外にも、2 つの主要な要因がありました(後述します)。第一に、Encore を将来的に他の言語にも拡大したいと考えており、Prisma や Pydantic のようなプロジェクトで Rust コアを Node.js や Python 向けにバインディングし、成功させた事例を目にしていました。Rust で一度にコアロジックを書いて各言語のランタイムにバインディングすることで、追加する言語ごとにインフラストラクチャの処理を再実装する必要がなくなります。第二に、Node.js は本質的にシングルスレッドで動作します。すべてのビジネスロジックを除いた部分を Rust に移行させることで、HTTP リクエストのライフサイクル、データベース接続管理、Pub/Sub、トレーシングといった機能すべてが Tokio を介して完全にマルチスレッドで実行されます。これは Node.js 単体では達成できないパフォーマンス向上です。
現在に至るまで、2 年間と約 67,000 行のコードの開発を経て、ランタイムは HTTP リクエストの全ライフサイクル(ルーティング、リクエストパースと検証、レスポンスシリアライゼーション)、データベース接続プールおよびクエリ処理、3 つのクラウドプロバイダを跨ぐ Pub/Sub、分散トレーシング、メトリクス収集、オブジェクトストレージ、キャッシング、Pingora を駆動する API ゲートウェイなどの機能をすべて処理できるようになりました。アプリケーションを実行する TypeScript コードは純粋なビジネスロジックであり、その下層にあるすべての処理は Rust が担当します。この記事では、そこにたどり着くまでの決断過程、当初は明らかではなかった課題、そして今後どのように異なるアプローチを取るべきかについて解説します。
なぜ Go ランタイムを拡張しなかったのか?
Go ランタイムは Go アプリケーションに対して非常に優秀で、今もなおその機能を維持しています。それはアプリケーションバイナリにコンパイルされ、フレームワークレベルでのインフラストラクチャの懸念事項を処理します。TypeScript サポートへの直感的アプローチは、Go ランタイムをサイドキャープロセスとして Node.js 旁边に実行し、両者の間で IPC(Inter-Process Communication)を通じて通信させることでしたが。
私たちはこのプロトタイプを開発しましたが、プロセス境界をまたいで各データベースクエリ、Pub/Sub メッセージ、およびトレーシングイベントのシリアライゼーションが発生することによるレイテンシオーバーヘッドが急激に積み上がることが分かりました。データベースにアクセスし、イベントを公開する単一の API リクエストでも、IPC 境界を 6 回から 7 回横断する必要があり、ベンチマーク結果ではサイドキャーアプローチは実際の処理が始まる前に、シリアライゼーションとコンテキストスイッチングだけで各リクエストあたり 2〜4ms のオーバーヘッドを追加していました。
別の問題は運用面に関するものでした。2 つのプロセスということは、監視すべき対象が 2 つ増え、独立してクラッシュするリスクも 2 つ増え、相関させるログセットも 2 つ増えることを意味します。ローカル開発であれば管理可能ですが高負荷の生産環境で数十ものサービスに跨る場合、失敗モードは倍増します。したがって、ランタイムは Node.js のイベントループと同じプロセス内に存在する必要があり、そのためには C/C++ で N-API バインディングを使用するか、あるいは Rust で napi-rs を使用する必要がありました。Rust は Go ランタイムと同様の安全保証(メモリ安全性、スレッド安全性、データレースなし)を提供する一方で、Node.js のイベントループをブロッキングすることなく数千の同時接続を処理するための非同期エコシステム(Tokio)へのアクセスも可能にしました。
最初の 10,000 行コード
TypeScript サポートは内部開発を通じて数ヶ月間、多くのプルリクエストにわたって漸進的に実装されました。一般公開リリース (#1073) ではコアランタイム、JavaScript N-API バインディング、および TypeScript パーサーの 3 つの Rust クレートを同時に提供しました。フレームワークはこれらのすべての部品が配置されている場合にのみ機能するため、開発が非同期であったにもかかわらず、リリース自体は原子操作として実装する必要がありました。
コアランタイム (
runtimes/core) はインフラストラクチャ上の各懸念事項を担当するマネージャの集合体として構成されています:
pub struct Runtime { api: api::Manager, // HTTP ライフサイクル、ルーティング、認証 sqldb: sqldb::Manager, // データベース接続、プール管理、クエリ pubsub: pubsub::Manager, // トピックの公開と購読 objects: objects::Manager, // オブジェクトストレージ (S3, GCS) metrics: metrics::Manager, // メトリクスの収集と出力 secrets: secrets::Manager, // シークレットの取得 // ... }
各マネージャは 2 つの異なるプロトobuf設定から遅延して初期化されます。最初の構成はアプリケーションメタデータであり、システム自体を記述します。これは TypeScript パーサーによってコンパイル時に生成され、アプリケーションコードを読み取りインフラストラクチャ宣言を抽出します:
// アプリケーションメタデータ — システムの完全な説明 // TypeScript/Go パーサーによってコンパイル時に生成される message Data { string module_path = 1; repeated Service svcs = 5; optional AuthHandler auth_handler = 6; repeated CronJob cron_jobs = 7; repeated PubSubTopic pubsub_topics = 9; repeated CacheCluster cache_clusters = 11; repeated SQLDatabase sql_databases = 14; repeated Gateway gateways = 15; repeated Bucket buckets = 17; // ... }
2 つ目の構成はランタイム設定で、ランタイムを実際にどのように実行するべきかを示します:各 Pub/Sub トピックに使用するクラウドプロバイダの実装、認証方法、各データベースクラスタの場所、サービス間の呼び出しのためのサービスディスカバリ、および可観測性の設定など。これはデプロイ時によってターゲット環境に基づいて生成されます:
// ランタイム設定 — 環境ごとにランタイムがどのように構成されるか // デプロイ時に生成される message RuntimeConfig { Environment environment = 1; // クラウドプロバイダ、環境タイプ (dev/prod/test) Infrastructure infra = 2; // SQL クラスタ、Pub/Sub、Redis、シークレット、バケット Deployment deployment = 3; // サービスディスカバリ、認証方法、可観測性設定 }
この分離は重要であり、同じアプリケーションメタデータをローカル開発環境(NSQ と Docker Postgres)と生産環境(AWS SNS/SQS と RDS)にデプロイするためには、ランタイム設定だけを切り替えるだけで十分です。アプリケーションコードもそのメタデータも変更する必要がありません。
Node.js(および Bun)から Rust に呼び出しを行う方法 (返答をもらう)
Node.js の NAPI(N-API)は JavaScript からネイティブコードを呼ぶために設計されています:関数を登録し、JavaScript からそれを呼び出すと、作業を行い値を返します。しかし、ネイティブコードから JavaScript への呼び出しの方が困難で、これが必要なのは Pub/Sub メッセージが到着して TypeScript ハンドラーにディスパッチする時や、HTTP リクエストが TypeScript エンドポイント関数を実行する必要がある時に起こります。
NAPI はこのためのスレッドセーフな関数を提供しており、napi-rs はそれらをきれいにラップしています。問題は、標準的な抽象化は引数を JavaScript に送信するだけであり、返り値をキャプチャすることができないことです。Pub/Sub メッセージハンドラーが処理を終了した後、メッセージを承認 (ack) または拒否 (nack) するため成否を確認する必要があります。API エンドポイントハンドラーがレスポンスを返した際、Rust 側でそのレスポンスを受信しシリアライズして送信する必要があります。
私たちは napi-rs の
ThreadSafeFunction をフォークし、JavaScript 関数を手動で呼び出し、その戻り値をキャプチャできるようにしました:
// ThreadSafeFunction をフォークしたもので、JS 関数を呼ぶ代わりに引数のみを返すのではなく、 // JS 関数を手動で呼び出して戻り値をキャプチャすることを可能にする。 // これにより、関数の戻り値を使用できるようになる。 pub struct ThreadSafeCallContext<T: 'static> { pub env: Env, pub value: T, pub callback: Option<JsFunction>, }
別の微妙な点は Promise です。TypeScript エンドポイントハンドラーは Promise を返す非同期関数です。JavaScript から値を受け取る際、それが Promise かどうかを検出し、そうであれば Rust チャネル経由で解決される
.then() カallback をチェーンする必要があります。これは JavaScript の非同期モデルを Rust のモデルと接続しています:
// JS の戻り値が Promise かどうかを検出し、Promise である場合は // .then() でチェーンして結果を Rust チャネルに解決する fn await_promise(env: &Env, value: JsUnknown) -> Result<()> { if value.is_promise()? { // .then() をチェーンして結果を送信 // tokio oneshot チャネル経由で Rust 側に返す } }
この構成における Node.js と Rust の関係は従来のホスト/ゲストアライメントではなく、シンバイオティック(共生)的なものです。Node.js(または我们也支持的 Bun)がプロセスを開始し Encore ライブラリをネイティブモジュールとしてインポートします。その際、ライブラリがルーティング、データベース接続、トレーシング、Pub/Sub などのインフラストラクチャ懸念事項を引き継ぎます。プロセスのライフサイクルは Node.js が管理し、インフラストラクチャ層は Rust が担当します。
これは Fredrik が最近対処した興味深いエッジケースを生み出します:Rust の Future はいつでもドロップされ(例えば Cloud Run のリクエストタイムアウトで接続が閉じられた場合)、しかし JavaScript ハンドラーは Node.js イベントループ上仍在実行中でキャンセルできません。Rust 側は
request_span_end に至らず、トレーシングにはルートスパンがないままになります。解決策は、Future がドロップされた際に検出し、JavaScript ハンドラーの完了を待機するために独立した Tokio タスクを生成する CancellationGuard を導入することです:
/// カンセレーション時にハンドラをバックグラウンドタスクとして生成するガードで、 /// `request_span_end` が常に発火されるように保証します。正常なパス( /// キャンセルの前にハンドラーが完了した場合)では、これは何もしない操作です。 struct CancellationGuard<'a> { call: &'a mut HandlerCall, info: Option<CancellationGuardInfo>, }
通常、キャンセル前にハンドラーが完了する場合、ガードは何もいません。Future が途中でのドロップが発生すると、ガードの
Drop 実装は進行中の HandlerCall の所有権を受け取り、バックグラウンドタスクとして生成し、JavaScript 側がきれいに完了しトレーシングスパンが閉じられるようにします。
Pingora を埋め込んだ API ゲートウェイの実装
Encore アプリケーションは複数のサービスで構成され、ネットワークを跨いで通信します。生産環境では、ゲートウェイが外部リクエストを適切なサービスにルーティングし、認証、CORS、リクエスト検証を手配します。一般的なアプローチとしては、nginx や envoy のようにサービスの前に配置する別プロセスとしてこの機能を動かすことです。代わりに、私たちはこれをランタイムに直接埋め込みました。
ゲートウェイ層としては Cloudflare のオープンソース HTTP プロキシライブラリである Pingora を使用します。Pingora はスタンドアロンのバイナリではなくライブラリとして使用するよう設計されており、まさに私たちが必要としたものです。ゲートウェイは Pingora の
ProxyHttp トレイトを実装し、ランタイムの残りの部分と同じプロセスにプラグインされます:
pub struct Gateway { inner: Arc<Inner>, } struct Inner { service_registry: Arc<ServiceRegistry>, router: router::Router, cors_config: CorsHeadersConfig, // ゲートウェイを介したプッシュ型 Pub/Sub 購読 proxied_push_subs: HashMap<String, ProxiedPushSub>, // ... }
ゲートウェイは認証システム、サービスレジストリ、トレーシングコレクターとメモリを共有します。「プロキシがこのリクエストを認証済みであること」と「エンドポイントハンドラーの実行」の間にはシリアライゼーション境界がありません。認証結果は
Arc された構造体であり、直接ハンドラーに渡されます。
プロセス内アプローチの決定的な利点は、TypeScript で記述したユーザー定義の認証ハンドラがゲートウェイ内部で直接実行できることです。Pingora は Node.js からあなたの認証ハンドラを実行し、結果を受け取り、認証コンテキストを付加してリクエストがゲートウェイを通じて続行されます。これは別のプロキシプロセスを使用する場合、シリアライゼーションと IPC を必要とします。
Pingora にはアップストリームサービスへの接続プール化、HTTP/2 支持、デプロイ時のグレースフルな接続ドレインなど、通常は自分で構築するか外部から追加する必要がある機能も提供されます。
面白い事実: 私たちが Pingora を使用する際に、Windows ではサポートされていませんでした。Encore はローカル開発用に Windows で実行する必要があり、私たちは Pingora に Windows 支持を追加し、それを上流にコントリビュートしました。その結果、今日では Encore おかげで Pingora も Windows で動作しています🙌。
ジェネラティク地獄なしでの 3 つのクラウドプロバイダへの抽象化
Pub/Sub システムは NSQ(ローカル開発)、GCP Pub/Sub、および AWS SNS+SQS を跨いで完全に同様に機能する必要があります。Rust の直感的なアプローチはジェネラリティを多用することになりますが、それはプロバイダの選択がコードベース内のすべての型シグネチャに漏れ出すことになります。代わりに私たちはトレイトオブジェクトを使用します:
trait Cluster: Debug + Send + Sync { fn topic(&self, cfg: &PubSubTopic, publisher_id: xid::Id) -> Arc<dyn Topic + 'static>; fn subscription(&self, cfg: &PubSubSubscription, meta: &Subscription) -> Arc<dyn Subscription + 'static>; } trait Topic: Debug + Send + Sync { fn publish(&self, msg: MessageData, ordering_key: Option<String>) -> Pin<Box<dyn Future<Output = Result<MessageId>> + Send + '_>>; } trait Subscription: Debug + Send + Sync { fn subscribe(&self, handler: Arc<SubHandler>) -> Pin<Box<dyn Future<Output = APIResult<()>> + Send + 'static>>; }
3 つのトレイト、各々 3 つの実装。マネージャは起動時ランタイム設定に基づいて適切なクラスタ実装を選択し、すべてを
Arc<dyn Trait> でラップします。コードベースの残りの部分はどのクラウドプロバイダが下層にあるかを知りません、気にもしません。
各プロバイダには独自の癖があります。NSQ は Tokio の起動した生成ループとメッセージチャネルを使用したアクター様のパターンを使用します。GCP では
tokio::sync::OnceCell を使用してクライアントの遅延初期化を実装しており、Cluster::topic() メソッドは同期的に返す必要がある(呼び出し側はトピック参照を取得するために await する必要はない)が、GCP クライアントの作成は認証を含むネットワークコールを伴う非同期操作です。この OnceCell は同期インターフェースの背後でその非同期初期化を隠し、初回使用まで延期します。AWS SQS/SNS では他のプロバイダが気にしない FIFO メッセージ順序付けのためにパブリッサー ID が必要です。
これらのすべての違いはそれぞれのモジュール内に存在します。マネージャは
Arc<dyn Topic> を見て .publish() を呼び出します。内部で一つの実装が TCP でローカル NSQ デモンに話しかけ、別の実装が認証済み HTTPS コールを AWS に行っていることが不透明です。オブジェクトストレージも同じパターンに従い、S3 互換ストレージと Google Cloud Storage 用の実装があります。
カスタムバイナリトレーシングプロトコルの実装
Encore アプリケーションのすべての操作は自動的にトレーシングされます:API コール、データベースクエリ、Pub/Sub の公開、外部サービスへの HTTP コール、キャッシュ操作など。トレーシングにはタイミング、ネスト(どのデータベースクエリがどの API コール中に発生したか)、リクエスト/レスポンスボディ、エラー詳細が含まれます。これは各リクエストで生成される莫大なデータであり、プロトobuf メッセージを構築してエンコードする代わりに、カスタムバイナリトレーシングプロトコルを実装しました。
EventBuffer はトレーシングイベントを可変長整数エンコーディング付きの連続バイトバッファに書き込むための専用シリアライザです:
pub struct EventBuffer { scratch: [u8; 10], buf: BytesMut, }
スクラッチバッファーは varint エンコーディンのためのアロケーションを回避します。トレーシング ID とスパン ID はワイヤー上で生のバイトとして(それぞれ 16 バイトと 8 バイト)書き込まれます。これは OpenTelemetry がバイナリプロトコルで使用している同じアプローチです。各リクエストで数十のスパンイベントが生成される場合、ミリオン単位のトレーシング全体でこの節約は累積されます。
また、单调時間(正確な持続時間の測定用)と壁時計時間(表示用)を相関させる必要がありました。
TimeAnchor は tokio::time::Instant と chrono::DateTime を同時にキャプチャし、その後のすべてのイベントでは単に单调オフセットだけを記録します。これは分散トレーシングシステムで事件が親イベントよりも前に発生するように見えてしまうような時計ずれの問題を回避します。
トレーシングサンプリングはエンドポイント別、サービス別、およびグローバルに設定可能です。サンプリング決定はリクエストの開始時に発生し、すべての子スパンに伝播するため、部分トレーシングが発生することはありません。API コールは見えても、それが行ったデータベースクエリが見えないトレーシングは、トレーシングなしの方がマシなので、これは必須要件でした。
Rust から TypeScript をパースする
TypeScript パーサー (
tsparser) は独立した Rust クレートで、アプリケーションの TypeScript ソースコードを読み取り Encoure フレームワーク宣言を抽出します:どのサービスが存在し、それらがどのようなエンドポイントを提供し、どのデータベースと Pub/Sub トピックを宣言しているか、リクエストとレスポンスの型がどのように見えるかなど。
私たちは SWC の TypeScript パーサーの上で AST を構築し、独自の解析パスを実装しました。パーサーはインポートの解決、再エクスポートの追跡、およびジェネラリティ、ユニオン、マップされた型を含むリクエストとレスポンス型の形状を抽出するための TypeScript 型システムの十分な理解が必要です。TypeScript の型システムの全複雑さはまだサポートされていませんが、 Coverage を拡大し続けています(最近の追加にはキーのリマッピング、メソッドシグネチャ、コールシグネチャ、インターセクション型が含まれます)。
パーサーはコードからインフラストラクチャが機能するための鍵です。
SQLDatabase("orders", { migrations: "./migrations" }) などの新しい宣言を書いた際、パーサーはその宣言を見てデータベース名とマイグレーションパスを抽出し、ランタイムが起動時に読むアプリケーションメタデータプロトobufに含めます。ランタイム自体は TypeScript をパースする必要はなく、構造化されたアプリケーションの説明を受け取りそれに応じて自身を設定します。
このパーサーはまた、MCP サーバ、アーキテクチャ図、および API ドキュメント生成を可能にし、これらはすべてパーサーが生成する同じメタデータを消費します。
異なるアプローチを取るべき点
- 早期にエラーコンテキストへの投資を行う。 Rust のエラー処理は言語レベルでは優れていますが、当初は
のような汎用的なエラーハンドリングに過度に依存し、具体的なエラータイプを定義するのを怠っていました。Pub/Sub スタックの 3 層目に何か失敗した場合、「メッセージの公開に失敗」というエラーよりも、トピック名、メッセージサイズ、プロバイダ、および特定の失敗モードを含む構造化エラーの方が有用です。私たちはより良いエラータイプを段階的に改造しています。anyhow::Context - OpenTelemetry アダプタを早めに提供する。 カスタムトレーシング形式は OpenTelemetry で表現しにくい情報(リクエスト/レスポンスペイロード、自動非表示)を収集しており、それは価値がありました。しかし、顧客は最初から既存の可観測性スタックにトレーシングをエクスポートすることを望み、「オープン標準を使用していない」という反論は正当です。私たちは現在 OpenTelemetry アダプタを開発していますが、これよりも優先すべきだったでしょう。
- スナップショットテストへの投資を倍増させる。 私たちはスナップショットテストを使用しており、特に TypeScript 型システムパースに関連して、それはコードベースの中で最も効果的なテスト戦略の一つです。新しい TypeScript 構文をサポートするたびに、スナップショットテストは全体のサーフェイエリアにわたって後退をキャッチします。私たちはこれに最初からより多く投資すべきでした。
- Go のための Rust ランタイムの使用を検討する。 単一のランタイムを持つことで保守負荷を大幅に削減できます。しかし、Go と Rust 間の FFI は Node.js ケースよりも困難で、Go のガーベジコレクタの動作によるものです。メモリ所有権は Go-Rust 境界で難しく、特に Go が GC 中にオブジェクトを移動できる場合です。また
も独自のパフォーマンスオーバーヘッドがあります。私たちはこれを探索し始めていますが、それは簡単ではありません。cgo
数値データ
Rust ランタイムは Encore Cloud デプロイメントを通じて毎日数十億回のリクエストを処理しており、それ以上に自社ホスト企業からのリクエストも発生していますが、そのトラフィックは見えていません。
インフラストラクチャ層を Rust に移行させるパフォーマンス影響は測定可能です。150 人の同時ワーカーで 10 秒間にわたるベンチマーク(上位 5 回の結果平均、
oha で負荷を生成)では:
| フレームワーク | リクエスト/秒 | 検証を含むリクエスト/秒 | P99 レイテンシ | 検証を含む P99 レイテンシ |
|---|---|---|---|---|
| Encore.ts | 121,005 | 107,018 | 2.3ms | 3.6ms |
| Bun + Zod | 101,611 | 33,772 | 3.7ms | 14.9ms |
| Elysia + TypeBox | 82,617 | 35,124 | — | — |
| Hono + TypeBox | 71,202 | 33,150 | — | — |
| Fastify + Ajv | 62,207 | 48,397 | 4.1ms | 5.4ms |
| Express + Zod | 15,707 | 11,878 | 11.9ms | 18.2ms |
Encore.ts は Express.js のスループットの 9 倍を処理し、レイテンシは 80% も低いです。検証が有効な場合、このギャップはさらに広がります。Encore はパーサーで既に抽出した型情報を使用して Rust レイヤーでリクエストを検証するのに対し、他のフレームワークは JavaScript で検証を実行します。パフォーマンスの詳しい内訳については、「Encore.ts — Express.js の 9 倍高速」をご覧ください。
コードベースはコアランタイム、JavaScript バインディング、TypeScript パーサー、プロセスサバイバー全体で Rust が 67,077 行あります。隣接する Go ランタイムは 42,629 行あり、現在も Go アプリケーションのためにアクティブにメンテナンスされています。それらは同じプロトobuf ベースの構成形式を共有しますが、それ以外は独立したコードベースです。
ランタイムはアプリケーション層の下にあるすべての処理を行います:接続受信用、リクエストルーティング、入力パースと検証、データベースプール管理、メッセージ公開、トレーシング収集、メトリクスエクスポートなど。あなたのアプリケーション内の TypeScript コードは純粋なビジネスロジックであり、express をインポートしたり、データベース接続を設定したり、Pub/Sub コンシューマーを設定することはありません。必要なインフラストラクチャを宣言するだけで、67,000 行の Rust がそれを可能にします。
Encore はオープンソースです。ランタイムコードは主要リポジトリの
runtimes/core(共有 Rust ランタイム)、runtimes/js(Node.js バインディング)、および tsparser(TypeScript 解析)下に存在します。これらの仕組みについて見る場合は、コードがあります。