スポンサーLobeHubLobeHub詳しく見る
dshfind

第 12 課:コンポーネントローダーと Koishi の事例

一言でいうと:この課では「コンポーネントローダー」を扱います。ローダーは、オーケストレーターが書き下した「どのコンポーネントが欲しいか」という宣言的設定を、実行中のファイバーへの最小限の変更へと翻訳します。エントリーはフィールド単位で増分リコンシルされ、コード変更は HMR によって再起動なしでホットスワップされます。そして 4000+ のコミュニティプラグインを持つ Koishi が、この設計の表現力と汎用性を実際の本番環境で検証しています。


1. なぜ「宣言的設定レイヤー」が必要なのか?

まず役割分担を振り返りましょう。 前の課では Cordis コアライブラリの命令型プリミティブを学びました:ctx.effect(エフェクトのインストール)、ctx.use(コンポーネントのロード)、ctx.set(サービスの提供)——これらはコンポーネント開発者がコンポーネント内部でコードを書くときに使う道具です。

しかし「アプリケーションオーケストレーター」(既製のコンポーネント群を組み立てて一つの実行システムにする人)が直面するのは、別種の問題です。

「コンポーネントをどう書くか」ではなく、「システムにどのコンポーネントが必要で、その組み合わせがシステムのライフサイクルの中でどう変化するか」です。

Cordis の答えは、宣言的設定レイヤーを導入することです。

  • オーケストレーターは永続化されたデータ構造で「欲しい組み合わせ」を記述します——何が欲しいかを宣言することだけを担当します。
  • ローダーはこの仕様へのあらゆる変更を、対応する命令型のファイバー操作へと翻訳します——実行を担当します。

たとえるなら、オーケストレーターは「発注者」で、設計図上の要件を変更するだけ。ローダーは「施工隊」で、設計図の変更箇所を、すでに建っているビルへ正確に反映させる役目です——ビルを取り壊して建て直すのではなく。

1.1 設定ツリー:エントリーは「作業指示書」

定義 26 によれば、エントリー(entry)は一つのファイバーを宣言し、次のフィールドを記録します。

フィールド意味平易な説明
id安定した識別子。所属グループのサブエントリーリストが変化したときのリコンシルキーとなる人を見分けるための ID 番号
urlインスタンス化するコンポーネントモジュールの URLこのコンポーネントのコードをどこから取ってくるか
isolateこのエントリーのコンテキストに適用される隔離アノテーションコンポーネントに区画を割り当てる
interceptこのエントリーのコンテキストに適用されるインターセプトアノテーションコンポーネントの入り口に監視装置を取り付ける
configコンポーネントに束縛される設定で、コンポーネントのエフェクト関数 apply を構成するコンポーネントの「取扱説明書」
disabledこのエントリーが管理的に無効化されているかどうか停止スイッチ

一文で覚えましょう。エントリー = ファイバーがあるべき姿を宣言する一枚の「作業指示書」。実行時には、このエントリーが宣言したファイバーを管理し、フィールドの変化に応答します。

これらのエントリーは組織化されて一本の設定ツリーを構成します。これは「システムが現在何をロードしているか」の権威ある記録です。

  • 葉ノードのエントリー:単一のファイバーに対応します。
  • 分岐ノードのエントリー:そのコンポーネントがさらに多くのコンポーネントをロードするため、サブツリーが生えます。

Cordis はさらに二つの専用コンポーネントを提供します。@cordisjs/group はサブエントリーのリストを設定として受け取り、サブグループとしてロードします。@cordisjs/include は外部設定ファイル(YAML または JSON)をロードし、ファイル内のエントリーを入れ子のサブツリーとして接ぎ木します。

1.2 増分リコンシリエーション:変化したフィールドだけを処理する

設定が変わったとき、ローダーはどう実行するのでしょうか?ツリー全体を取り壊して再構築することは決してせず増分リコンシリエーションを行います。変化したフィールドを検査し、フィールドごとに最も影響の小さい操作を実行します。

変化したフィールドローダーの動作
idurlそのエントリーを再構築——アイデンティティまたはコンポーネントが変わったため
isolateマイグレーション:そのエントリーのドメインマッピングテーブルを書き換え、提供しているすべてのコーエフェクトを移行し、解決結果が変化した依存側に通知する
interceptその場で更新——インターセプトのメタデータは読み取り時にしか問い合わせられないため、再ロードは不要
configコンポーネントに処理を委ねる。典型的には以前のペイロードと差分を取り、実質的な変化があるときだけ再ロードする
disabled真に設定 → ファイバーを破棄。解除 → ファイバーを再ロード

🎁 見落としがちな妙味が一つあります。@cordisjs/groupconfig はまさにそのサブエントリーリストなので、ローダーはサブエントリーの id をキーに差分を取り、個々のサブエントリーを作成・削除・更新します。そして「存続しているサブエントリーの更新」は、再び同じフィールド単位のディスパッチプロセスに入ります——つまりグループのリコンシリエーションとエントリーの更新は、ツリーに沿って再帰的に進んでいくのです。一つのルールセットが、どこにでも適用されます。

さらに Cordis では、コンポーネントが実行時に自分で config を更新したり、自分自身を無効化したりすることもできます。どちらの場合も、ローダーは変更を設定レイヤーに書き戻します——「永続化された仕様」が常に「実行中のシステム」を忠実に反映し、両者が決して食い違わないようにするためです。


2. HMR:再起動なしでコードを変える三フェーズ

HMR(ホットモジュールリプレースメント、Hot Module Replacement) は、「ロールバック可能なエフェクト」パターンをモジュール層に適用したものです。

ソースファイルが変わったとき(通常は開発中)、システムは影響を受けるモジュールをその場で置き換えプロセスを再起動しません

なぜ Cordis にはこれができるのでしょうか? ファイバーがすでにコンポーネントのすべてのエフェクトとコーエフェクトに境界を引いているからです。古いファイバーを破棄する = コンポーネントがインストールしたすべてを自動的に取り消す。再ロードされたモジュールから新しいファイバーをインスタンス化する = すべてを元通りにインストールし直す。 モジュールそのものがコンポーネントなので、置き換えに必要なのは二つのファイバー操作だけです。

Webpack / Vite との比較:これらの HMR では、開発者が accept のような受け入れ境界を手書きする必要があります(ビルドツールに「このモジュールはホットリプレース可能です」と伝えるもの)。書き忘れるとページ全体のリフレッシュに格下げされます。Cordis の HMR は開発者のアノテーションを一切必要としません

@cordisjs/hmr コンポーネントが HMR エンジンを提供し、三つのフェーズで実行されます。

フェーズ 1:モジュール分類(アルゴリズム 7)

エンジンは二つの入力を受け取ります。

  • stash セット(stashed):前回の再ロード以降に内容が変化したファイルの URL。
  • 外部モジュールセット(externals):ホットリプレースできず、完全な再起動を引き起こすモジュール。

次に、変化に関わる依存サブグラフに対して固定点計算を行い、各モジュールに「受け入れ済み(accepted)」または「拒否(declined)」のマークを付けます。

ルール結論
あるモジュールのいずれかのインポートが受け入れ済み→ それを受け入れる
あるモジュールのすべてのインポートが拒否→ それを拒否する
いつまでも未決のままインポートの循環に陥っているモジュールデフォルトで拒否に分類

(「固定点」とは、stash されたファイルのインポートを種として繰り返し伝播させ、もう新しいモジュールにマークできないラウンドに達するまで続ける、という意味です。)

フェーズ 2:失効エントリーの検出(アルゴリズム 8)

分類が終わると、エンジンは「受け入れ済み / 拒否」を使ってコンポーネントエントリーを選別し、依存ツリーが変更されたモジュールに到達できる失効エントリーだけを残します。

  • get_dependencies は依存ツリーに沿ってモジュールの推移的インポートを集め、declined モジュールに出会うと停止します(それが走査の境界です)。
  • エントリーはその依存ツリーが accepted と交差したときにちょうど失効します。その後そのツリーは accepted に併合されます——したがって、途中の失効モジュールはすべて次のフェーズで無効化されます。

フェーズ 3:トランザクショナルな再ロード(アルゴリズム 9)

最後に、エンジンはまずバックアップし、それから手を動かし、失敗したらロールバックするという手順を取ります。

function reload(ctx, accepted, staleEntries) {
  const backup = invalidateCaches(accepted);                       // ① accepted モジュールのキャッシュを無効化し、バックアップする
  try {
    for (const entry of staleEntries) {
      entry.fiber.dispose();                                        // ② 古いファイバーを破棄:インストールしたものすべてを自動的に取り消す
      entry.fiber = ctx.use(import(entry.url), entry.config);      // ③ 新しいモジュールをインポートし、新しいファイバーに差し替える
    }
  } catch (error) {
    restoreCaches(backup);                                          // ④ 失敗時:まずキャッシュを復元する
    for (const entry of staleEntries) {
      entry.fiber.dispose();
      entry.fiber = ctx.use(backup[entry.url], entry.config);      // ⑤ バックアップ内の古いコンポーネントで各失効エントリーを再構築する
    }
    throw error;                                                    // ⑥ エラーをそのまま再スローする
  }
}

トランザクショナルな保証:システムが「半分だけ完了した再ロード」の状態で止まることは決してありません。いずれかのモジュールのインポートが失敗した場合(たとえば構文エラー)、キャッシュが復元され、backup[entry.url](キャッシュが復元されたばかりの、再ロード前のコンポーネント)ですべての失効エントリーが再構築されます——完了していた交換はすべて一括で取り消され、何事もなかったかのようになります。

💡 豆知識:Node.js では「キャッシュを無効化する」とは、ES モジュールと CommonJS の両方のモジュールシステムのキャッシュを同時にクリアすることを意味します——ES ローダー経由でインポートされたモジュールは両方に現れうるからです。

三つのフェーズをつなぐと、次のパイプラインになります。

① 模块变化改代码保存② 分类接受 / 拒绝(依赖子图)③ 失效条目依赖树可达已变更模块④ 事务性重载(失败)自动回滚)全程不重启:旧纤程释放(恢复效应),新纤程装上

分类 → 失效检测 → 事务性重载:与 Webpack/Vite 不同,无需手写接受边界


3. Koishi:4000+ プラグインによる本番検証

前の二節は「設計」の話でした。この節では「実践」を見ます。Koishi は Cordis の上に構築されたオープンソースのチャットボットアプリケーションフレームワークです。四年以上の開発を経て、4000+ のコミュニティ貢献プラグインを蓄積しており、インスタントメッセージング(IM)アダプター、データベースドライバー、管理コンソール、エンドユーザー向け機能をカバーしています。この規模と多様性が、本番環境における Cordis の動的コンポーザビリティを検証する代表的な事例にしています。

📌 用語の補足を二つ:Koishi が現在使用しているのは Cordis v3 で、論文が紹介しているのはエフェクト/コーエフェクトの意味論を精緻化しローダーを再設計した v4 です。両者はコアのコンポジションモデルを共有しています。また、Koishi でいう「プラグイン(plugin)」は、論文で形式化された「コンポーネント(component)」そのものです。

Koishi は Cordis モデルの二組の特性を裏付けています。

3.1 表現力と汎用性:一つのモデル、二つの世界

  • 表現力:Koishi はサーバーサイドボットとして動作し、そのすべての能力はコンテキストプリミティブの上にプラグインとして実装されています。Koishi 自身が提供するのはチャットボット領域の語彙だけです——プリミティブが完全な本番システムを担うのに十分であることを示しています。
  • 汎用性:同じモデルがまったく異なるランタイムにも現れます——Koishi の Web コンソールはもう一つの独立した Cordis アプリケーションで、そのプラグインはサーバープリミティブではなく、ブラウザとユーザーインターフェースのプリミティブを組み合わせています。

同じコンポジションモデルが、「サーバーサイドボット」と「ブラウザ UI」という二つの世界で同時に動いている——これこそ汎用性の直接的な証拠です。モデルはエフェクトとコーエフェクトがどう組み合わさるかだけを規定し、その意味は各アプリケーションの決定に委ねます。したがって、領域もランタイムも前提としません

3.2 時間的コンポーザビリティ:プラグインのオン・オフ、認知的オーバーヘッドゼロ

論文の 1.2.1 節を覚えていますか? 従来のプラグインシステムは、ホストを再起動せずに単一の拡張のエフェクトをアンロードすることができません。Koishi はこれを日常的に行っています。

  • オーケストレーターがコンソールからあるプラグインを無効化する → そのエフェクトは直ちにその場で取り消される
  • 開発中は、HMR エンジンが保存時に編集されたプラグインを再適用し、システムの他の部分のキャッシュ状態とアクティブな接続を保持したままにする。

重要なのは、プラグイン作者がこのために追加の作業をほとんど必要としないことです。コンテキスト経由でインストールされたエフェクトは自動的に追跡され、その逆関数も自動的に合成されます——経験の浅い作者がアンインストールパスを書かなくても、プラグイン内でコンテキスト経由でインストールされたエフェクトは順序通りにクリーンアップされます。「関心の局所性」という、本来なら作者一人ひとりの勤勉さに依存していた正確性の要件が、この抽象によって一括して統一的に果たされるのです。

3.3 空間的コンポーザビリティ:異なる作者のプラグインが組み合わさる

従来のプラグインシステムの多くはプラグイン間の依存関係を欠いています。一方、Koishi のエコシステムは本物の依存トポロジーを呈しています。

  • IM アダプターが各種メッセージングプラットフォームへのアクセスを提供する。
  • データベースドライバーが永続化ストレージを提供する。
  • 機能プラグインがこれらの能力をコーエフェクトとして宣言し、アクセスする。

実行時にプロバイダーを再設定しても(ストレージバックエンドの切り替え、アダプターの再接続など)、再アクティブ化されるのは解決結果が変化した依存側だけです。依存項が一時的に利用できないプラグインは、その依存項が現れるまで非アクティブのままです——エラーは発生しません

そして、プラグインとその依存項は通常異なる作者によって書かれており、作者同士は両者をつなぐコーエフェクト以外に何も調整する必要がありません。つまり、リアクティブなコーエフェクトが、この複合体を独立した貢献者からなるオープンなエコシステムの中で一貫した状態に保っているのです。


4. 妥当性への脅威:この証拠はどれほど堅いのか?

論文は最後に誠実な方法論的セルフチェックを行い、このケーススタディの二つの限界を指摘しています。

  1. 単一のホスト言語:証拠は単一のホスト言語(TypeScript)の単一のエコシステムから得られたものであり、「パラダイム自体の利点」を「TypeScript 実装や Koishi 固有の領域の利点」と区別することができません
  2. 観察的証拠:証拠は観察から得られたもので、代替アーキテクチャとの統制された比較実験ではありません。

したがって、結論は正確に読む必要があります。このケーススタディが証明するのは、このパラダイムが存在し、採用されているということであって、定量的な結果ではありません——あるベースラインを基準に、この抽象のオーバーヘッドと開発者の生産性への影響を測定することは、将来の課題として残されています

⚠️ 平たく言えば:「名シェフの料理がおいしい」ことはレシピが機能することの証明にはなりますが、「別のレシピより 30% おいしい」と言うには、より厳密な対照実験が必要です。


重要ポイントの振り返り

この課は情報量が少なくありませんが、次の五つの文を覚えておけば十分です。

  1. 宣言的設定レイヤー:オーケストレーターは「どのコンポーネントが欲しいか」を設定ツリーとして書き下す。エントリー(id / url / isolate / intercept / config / disabled)は一つのファイバーを宣言し、ローダーが設定の変更をファイバー操作へ翻訳する。
  2. 増分リコンシリエーション:変化したフィールドだけを処理する——id/url は再構築、isolate はマイグレーション、intercept はその場で更新、config は差分、disabled はアンロード/ロード。ツリー全体の再構築は決して行わない。
  3. HMR の三フェーズ:モジュール分類(固定点:いずれかのインポートが受け入れられれば受け入れ、すべて拒否なら拒否、循環内はデフォルトで拒否)→ 失効エントリー検出(依存ツリーが変更モジュールに到達できるエントリーが失効)→ トランザクショナルな再ロード(まずバックアップ、新しいファイバーに交換、失敗時はロールバック)。全工程で再起動は不要で、手書きの accept 境界も不要。
  4. Koishi による検証:4000+ プラグインの本番システムで、サーバーサイドボットと Web コンソールという二つの異なるランタイムの両方が Cordis で動いている——表現力、汎用性、認知的オーバーヘッドのない時間的コンポーザビリティ、オープンなエコシステムをまたぐ空間的コンポーザビリティを裏付けている。
  5. 妥当性への脅威:証拠は単一のホスト言語 + 観察に由来し、「パラダイムが存在し採用されている」ことを証明するもので、定量的な結論ではない。

🚀 次の課(第 13 課)では論文の締めくくり——考察、関連研究、結論——に入り、「時空間コンポーザビリティ」パラダイムをより広い座標系の中で眺めます。

セルフクイズ · ローダーと Koishi

回答を終えたら「解答を送信」をクリックすると、正誤と解説を確認できます。

1. ローダーが「増分リコンシリエーション」を行う際、あるエントリーの url フィールドが変化していることに気づきました。どうすべきですか?
2. Cordis の HMR エンジンは三つのフェーズで実行されます。正しい順序はどれですか?
3. トランザクショナルな再ロードのフェーズで、新しいモジュールのインポートが失敗した場合(たとえば構文エラー)、何が起こりますか?
4. Koishi の事例で、Cordis モデルの「汎用性」を最もよく示している証拠はどれですか?