← Dynamic Workers / dynamic-workers / usage
Фасеты Durable Object
Durable Object Facets позволяют загрузить Durable Object класс из Dynamic Worker и запустить его как дочерний объект вашего собственного Durable Object. Дочерний объект (facet) получает собственную изолированную базу данных SQLite, а ваш класс выступает как supervisor, контролирующий доступ.
Это полезно, когда вы хотите, чтобы динамически сгенерированный код, например код, написанный ИИ-агентом, получил постоянное хранилище, но не имел прямого доступа к пространству имён Durable Object. Ваш супервизор загружает код, создаёт фасет и перенаправляет в него запросы. Вы сохраняете полный контроль над тем, что может делать динамический код.
Как устроена модель
Конфигурация на основе facet состоит из трех уровней:
- Класс supervisor : Обычный класс Durable Object, который вы пишете и разворачиваете. Он настроен с хранилищем на базе SQLite, как и любой другой Durable Object.
- Динамический код : Код, загружаемый во время выполнения через Worker Loader API. Этот код экспортирует класс, который расширяет
DurableObject. - Фасет : Экземпляр динамического класса, созданный вызовом
this.ctx.facets.get()внутри вашего supervisor. У каждого facet своя база данных SQLite, отдельная от базы supervisor.
База данных супервизора и база данных фасета хранятся вместе как часть одного и того же Durable Object. Динамический код не может прочитать базу данных супервизора: у него есть доступ только к своей собственной.
Настройка 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);
},
};В этом примере:
AppRunnerэто ваш supervisor Durable Object. Вы развёртываете его как обычно, и он владеет пространством имён Durable Object.- Динамический код экспортирует класс (
App) который расширяетDurableObject. Этот класс используетthis.ctx.storageдля чтения и записи данных, как и любой Durable Object. this.ctx.facets.get("app", callback)создает facet."app"строка задаёт имя facet, и для каждого имени создаётся своя база данных SQLite внутри родительского Durable Object.- База данных facet полностью изолирована от базы данных supervisor.
AppRunnerиAppу каждого свое хранилище, недоступное другому.
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
DurableObjectClassКласс Durable Object, который нужно инстанцировать для facet. Получите его, вызвав worker.getDurableObjectClass("ClassName") у stub Dynamic Worker.
id DurableObjectId | string Optional
DurableObjectId | string OptionalID, который facet видит как собственный ctx.id. Если не указано, фасет наследует ID родительского Durable Object.
Хранилище изолята
У супервизора и каждого фасета отдельная база данных SQLite. Динамический код использует стандартный API хранилища Durable Object причём все операции выполняются с собственной базой данных этого фасета.
Такая изоляция означает, что вам не нужно доверять динамическому коду данные супервизора. Вы можете хранить метаданные, счётчики для выставления счетов или состояние контроля доступа в базе данных супервизора, и фасет не сможет ни прочитать, ни изменить эти данные.
В продакшене сам динамический код обычно хранится в базе данных супервизора и загружается в #loadDynamicWorker() метод. Это позволяет коду оставаться связанным с экземпляром Durable Object, который им управляет.