INTEGRITY Документация

Фасеты Durable Object

Durable Object Facets позволяют загрузить Durable Object класс из Dynamic Worker и запустить его как дочерний объект вашего собственного Durable Object. Дочерний объект (facet) получает собственную изолированную базу данных SQLite, а ваш класс выступает как supervisor, контролирующий доступ.

Это полезно, когда вы хотите, чтобы динамически сгенерированный код, например код, написанный ИИ-агентом, получил постоянное хранилище, но не имел прямого доступа к пространству имён Durable Object. Ваш супервизор загружает код, создаёт фасет и перенаправляет в него запросы. Вы сохраняете полный контроль над тем, что может делать динамический код.

Как устроена модель

Конфигурация на основе facet состоит из трех уровней:

База данных супервизора и база данных фасета хранятся вместе как часть одного и того же Durable Object. Динамический код не может прочитать базу данных супервизора: у него есть доступ только к своей собственной.

Схема, показывающая архитектуру facet: запрос проходит через точку входа Worker в экземпляр Durable Object, содержащий Supervisor с собственной базой данных SQLite, который создаёт изолированный Facet с отдельной базой данных SQLite через ctx.facets.get() и пересылает ему запросы через facet.fetch()

Настройка Worker

Вашему Worker нужны две вещи: класс Durable Object с хранилищем на основе SQLite и привязка Worker Loader.

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

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

[[worker_loaders]]
binding = "LOADER"

Загрузка и запуск динамического класса

В следующем примере показан supervisor Durable Object (AppRunner) который загружает динамический код, создаёт из него фасет и перенаправляет HTTP-запросы этому фасету.

Динамический код представляет собой простое приложение-счетчик, которое отслеживает количество полученных запросов, используя собственное хранилище на основе SQLite. В реальном приложении этот код поступал бы от ИИ-агента или загружался пользователем, а не задавался статической строкой.

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);
	},
};

В этом примере:

this.ctx.facets справочник

this.ctx.facets объект доступен внутри любого класса Durable Object. Он предоставляет методы для создания, остановки и удаления facet. Один Durable Object может иметь любое количество facet с разными именами, и у каждого своя независимая база данных SQLite.

get

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

Создаёт или возобновляет facet с указанным именем и возвращает stub, который можно использовать для отправки ему запросов.

Если фасет ещё не запущен или перешёл в спящий режим, среда выполнения вызывает getStartupOptions чтобы определить, какой код загружать. В противном случае используется существующий facet, и колбэк не вызывается. callback может при желании быть async (то есть возвращая Promise<FacetStartupOptions>).

Возвращаемый stub ведет себя как Durable Object stub. Вы можете вызвать .fetch() для отправки HTTP-запросов или вызывайте методы RPC напрямую.

abort

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

Останавливает работающий facet и делает недействительными все существующие stub. Любой последующий вызов недействительного stub выбрасывает reason. Хранилище фасета сохраняется.

После прерывания можно вызвать get() снова, чтобы перезапустить facet, в том числе с другим классом. Это делает abort() полезно для обновления кода: прервите facet, в котором выполняется старая версия, затем вызовите get() с колбэком, который возвращает новый класс.

delete

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

Прерывает работу facet (если он выполняется) и безвозвратно удаляет его базу данных SQLite. Если вы вызовете get() с тем же именем позже, фасет начинает работу с пустой базой данных.

Используйте delete() для очистки хранилища facet, которые больше не нужны.

FacetStartupOptions

Объект, возвращаемый getStartupOptions колбэк.

class DurableObjectClass

Класс Durable Object, который нужно инстанцировать для facet. Получите его, вызвав worker.getDurableObjectClass("ClassName") у stub Dynamic Worker.

id DurableObjectId | string Optional

ID, который facet видит как собственный ctx.id. Если не указано, фасет наследует ID родительского Durable Object.

Хранилище изолята

У супервизора и каждого фасета отдельная база данных SQLite. Динамический код использует стандартный API хранилища Durable Object причём все операции выполняются с собственной базой данных этого фасета.

Такая изоляция означает, что вам не нужно доверять динамическому коду данные супервизора. Вы можете хранить метаданные, счётчики для выставления счетов или состояние контроля доступа в базе данных супервизора, и фасет не сможет ни прочитать, ни изменить эти данные.

В продакшене сам динамический код обычно хранится в базе данных супервизора и загружается в #loadDynamicWorker() метод. Это позволяет коду оставаться связанным с экземпляром Durable Object, который им управляет.