Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

Durable Object Facets

最終更新 Markdown で表示Agent セットアップ

Durable Object Facets を使うと、Dynamic Worker から Durable Object クラスを読み込み、自分の Durable Object の子として実行できます。子(Facet)は独自の分離された SQLite データベースを持ち、自分のクラスはアクセスを制御するスーパーバイザーとして動きます。

動的に生成したコード(たとえば AI エージェントが書いたコード)に永続ストレージを持たせたい一方で、Durable Object 名前空間への直接アクセスは渡したくない場合に向いています。スーパーバイザーがコードを読み込み、Facet を作成し、リクエストを転送します。動的コードができることの制御は、自分側に残ります。

モデルを理解する

Facet を使う構成は 3 層です。

  • スーパーバイザークラス — 自分で書いてデプロイする、通常の Durable Object クラスです。ほかの Durable Object と同じく、SQLite ストレージバックエンドを設定します。
  • 動的コードWorker Loader API 経由で実行時に読み込むコードです。このコードは DurableObject を継承するクラスをエクスポートします。
  • Facet — スーパーバイザー内で this.ctx.facets.get() を呼んで作る、動的クラスのインスタンスです。各 Facet は、スーパーバイザーとは別の独自 SQLite データベースを持ちます。

スーパーバイザーのデータベースと Facet のデータベースは、同じ Durable Object 全体の一部としてまとめて保存されます。動的コードはスーパーバイザーのデータベースを読めません。アクセスできるのは自分のデータベースだけです。

Facet の構成図。リクエストは Worker のエントリポイントから、独自の SQLite DB を持つ Supervisor を含む Durable Object インスタンスへ流れます。Supervisor は ctx.facets.get() で分離された Facet(別の SQLite DB)を作成し、facet.fetch() でリクエストを転送します。

Worker を設定する

Worker に必要なものは 2 つです。SQLite ストレージバックエンドを持つ Durable Object クラスと、Worker Loader バインディングです。

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  // Set this to today's date
  "compatibility_date": "2026-09-20",
  "main": "src/index.ts",
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": [
        "AppRunner"
      ]
    }
  ],
  "worker_loaders": [
    {
      "binding": "LOADER"
    }
  ]
}
# Set this to today's date
compatibility_date = "2026-09-20"
main = "src/index.ts"

[[migrations]]
tag = "v1"
new_sqlite_classes = ["AppRunner"]

[[worker_loaders]]
binding = "LOADER"

動的クラスを読み込んで実行する

次の例では、スーパーバイザー Durable Object(AppRunner)が動的コードを読み込み、そこから Facet を作成し、HTTP リクエストを Facet へ転送します。

動的コードは、受け取ったリクエスト数を独自の SQLite バックエンドストレージで数える、シンプルなカウンターアプリです。本番では、このコードは静的な文字列ではなく、AI エージェントやユーザーのアップロードから来ます。

import { DurableObject } from "cloudflare:workers";

// In production, this code would come from an AI agent, a database,
// or user input — not a static string.
const AGENT_CODE = `
  import { DurableObject } from "cloudflare:workers";

  export class App extends DurableObject {
    fetch(request) {
      // Note: storage.kv provides simple KV storage backed by SQLite,
			// but you can also use SQL directly via storage.sql. See:
			// https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/

			let counter = this.ctx.storage.kv.get("counter") || 0;
      ++counter;
      this.ctx.storage.kv.put("counter", counter);

      return new Response("You have made " + counter + " requests.\\n");
    }
  }
`;

// AppRunner is your supervisor. Each instance manages one
// dynamically-loaded application.
export class AppRunner extends DurableObject {
	async fetch(request) {
		// Get a stub pointing to the "app" facet. If the facet has not
		// started yet (or has hibernated), the callback runs to tell the
		// runtime what code to load.
		const facet = this.ctx.facets.get("app", async () => {
			const worker = this.#loadDynamicWorker();

			// Extract the Durable Object class named "App" from the
			// dynamic Worker's exports.
			const appClass = worker.getDurableObjectClass("App");

			return { class: appClass };
		});

		// Forward the request to the facet.
		// You can also call RPC methods on the stub.
		return await facet.fetch(request);
	}

	#loadDynamicWorker() {
		// Use get() so the Worker stays warm across requests.
		// Each unique code version needs a unique ID.
		const codeId = "agent-code-v1";

		return this.env.LOADER.get(codeId, async () => {
			return {
				compatibilityDate: "2026-04-01",
				mainModule: "worker.js",
				modules: { "worker.js": AGENT_CODE },
				globalOutbound: null, // block network access
			};
		});
	}
}

export default {
	async fetch(request, env, ctx) {
		// Look up the AppRunner instance named "my-app".
		const obj = ctx.exports.AppRunner.getByName("my-app");

		// Forward the request to it.
		return await obj.fetch(request);
	},
};
import { DurableObject } from "cloudflare:workers";

// In production, this code would come from an AI agent, a database,
// or user input — not a static string.
const AGENT_CODE = `
  import { DurableObject } from "cloudflare:workers";

  export class App extends DurableObject {
    fetch(request) {
      // Note: storage.kv provides simple KV storage backed by SQLite,
			// but you can also use SQL directly via storage.sql. See:
			// https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/

			let counter = this.ctx.storage.kv.get("counter") || 0;
      ++counter;
      this.ctx.storage.kv.put("counter", counter);

      return new Response("You have made " + counter + " requests.\\n");
    }
  }
`;

// AppRunner is your supervisor. Each instance manages one
// dynamically-loaded application.
export class AppRunner extends DurableObject<Env> {
	async fetch(request: Request): Promise<Response> {
		// Get a stub pointing to the "app" facet. If the facet has not
		// started yet (or has hibernated), the callback runs to tell the
		// runtime what code to load.
		const facet = this.ctx.facets.get("app", async () => {
			const worker = this.#loadDynamicWorker();

			// Extract the Durable Object class named "App" from the
			// dynamic Worker's exports.
			const appClass = worker.getDurableObjectClass("App");

			return { class: appClass };
		});

		// Forward the request to the facet.
		// You can also call RPC methods on the stub.
		return await facet.fetch(request);
	}

	#loadDynamicWorker() {
		// Use get() so the Worker stays warm across requests.
		// Each unique code version needs a unique ID.
		const codeId = "agent-code-v1";

		return this.env.LOADER.get(codeId, async () => {
			return {
				compatibilityDate: "2026-04-01",
				mainModule: "worker.js",
				modules: { "worker.js": AGENT_CODE },
				globalOutbound: null, // block network access
			};
		});
	}
}

export default {
	async fetch(
		request: Request,
		env: Env,
		ctx: ExecutionContext,
	): Promise<Response> {
		// Look up the AppRunner instance named "my-app".
		const obj = ctx.exports.AppRunner.getByName("my-app");

		// Forward the request to it.
		return await obj.fetch(request);
	},
};

この例では、次のとおりです。

  • AppRunner はスーパーバイザー Durable Object です。通常どおりデプロイし、Durable Object 名前空間を所有します。
  • 動的コードは DurableObject を継承するクラス(App)をエクスポートします。このクラスは、ほかの Durable Object と同じく this.ctx.storage でデータを読み書きします。
  • this.ctx.facets.get("app", callback) が Facet を作成します。"app" という文字列が Facet の名前です。名前ごとに、親 Durable Object 内で独自の SQLite データベースを持ちます。
  • Facet のデータベースは、スーパーバイザーのデータベースから完全に分離されています。AppRunnerApp は、相手がアクセスできない独自のストレージを持ちます。

this.ctx.facets リファレンス

this.ctx.facets オブジェクトは、任意の Durable Object クラス内で使えます。Facet の作成、シャットダウン、削除のメソッドを提供します。1 つの Durable Object に、名前の異なる Facet をいくつでも持てます。それぞれが独立した SQLite データベースを持ちます。

get

this.ctx.facets.get(name string, callback () => FacetStartupOptions) Fetcher

指定した名前の Facet を作成または再開し、リクエストを送るためのスタブを返します。

Facet がまだ起動していない、またはハイバネーションしている場合、ランタイムは getStartupOptions を呼んで読み込むコードを決めます。それ以外では既存の Facet を再利用し、コールバックは呼ばれません。callback は任意で async にできます(つまり Promise<FacetStartupOptions> を返す)。

返されるスタブは Durable Object スタブ と同じように動きます。.fetch() で HTTP リクエストを送ることも、RPC メソッドを直接呼ぶこともできます。

abort

this.ctx.facets.abort(name string, reason any) void

稼働中の Facet をシャットダウンし、既存のスタブをすべて無効にします。無効になったスタブへの以降の呼び出しは reason をスローします。Facet のストレージは残ります。

abort したあと、get() を再度呼べば Facet を再起動できます。別のクラスでも構いません。そのため abort() はコード更新に向いています。古いバージョンを動かしている Facet を abort し、新しいクラスを返すコールバックで get() を呼びます。

delete

this.ctx.facets.delete(name string) void

Facet を abort し(稼働中なら)、その SQLite データベースを完全に削除します。あとから同じ名前で get() を呼ぶと、Facet は空のデータベースで開始します。

不要になった Facet のストレージを片付けるときは delete() を使います。

FacetStartupOptions

getStartupOptions コールバックが返すオブジェクトです。

class DurableObjectClass

Facet としてインスタンス化する Durable Object クラスです。Dynamic Worker スタブで worker.getDurableObjectClass("ClassName") を呼んで取得します。

id DurableObjectId | string 任意

Facet が自分の ctx.id として見る ID です。省略すると、Facet は親 Durable Object の ID を継承します。

ストレージを分離する

スーパーバイザーと各 Facet は、別々の SQLite データベースを持ちます。動的コードは標準の Durable Object ストレージ API を使い、操作はすべて Facet 自身のデータベースに対して行われます。

この分離があるため、動的コードにスーパーバイザーのデータを任せる必要はありません。メタデータ、課金カウンター、アクセス制御の状態はスーパーバイザーのデータベースに置けます。Facet はそれらを読んだり変更したりできません。

本番では、動的コード自体をスーパーバイザーのデータベースに保存し、#loadDynamicWorker() メソッドで読み込むのが一般的です。こうすると、コードとそのコードを管理する Durable Object インスタンスが対になります。

役に立ちましたか?