Lecture: 4 min.
Il y a quelques semaines, nous avons annoncé le lancement des Dynamic Workers, une nouvelle fonctionnalité de la plateforme Workers qui vous permet de charger du code Worker à la volée dans un sandbox sécurisé. L'API Dynamic Worker Loader fournit essentiellement un accès direct à la primitive d'isolation de calcul de base sur laquelle Workers s'est appuyée depuis toujours : des isolats et non des conteneurs. Les isolats sont beaucoup plus légers que les conteneurs et, en tant que tels, peuvent se charger 100 fois plus rapidement en consommant un dixième de mémoire. Ils sont tellement efficaces qu'ils peuvent être considérés comme « jetables » : il suffit d'en lancer un pour exécuter quelques lignes de code, puis de s'en débarrasser. Comme une version sécurisée de eval().
Les Dynamic Workers peuvent servir de nombreuses utilisations. Dans l’annonce initiale, nous nous sommes concentrés sur la manière de les utiliser pour exécuter du code généré par agent IA au lieu d'appeler des outils. Dans ce scénario d’utilisation, un agent d’IA effectue des actions à la demande d’un utilisateur en écrivant quelques lignes de code et en les exécutant. Le code est à usage unique, destiné à exécuter une tâche une fois seulement, puis il est éliminé immédiatement après son exécution.
Mais comment faire si vous voulez qu’une IA génère du code plus persistant ? Et si vous souhaitez que votre IA développe une petite application dotée d’une interface utilisateur personnalisée, avec laquelle l’utilisateur peut interagir ? Et si vous souhaitez un état durable pour cette application ? Par ailleurs, vous souhaitez évidemment toujours l’exécuter dans une sandbox sécurisée.
Vous avez la possibilité d’utiliser des Dynamic Workers, et de vous contenter de fournir au Worker une API RPC qui lui donne accès au stockage. À l’aide de liaisons, vous pouvez donner au Dynamic Worker une API qui pointe vers votre base de données SQL distante (peut-être étayée par Cloudflare D1, ou une base de données Postgres à laquelle vous accédez via Hyperdrive, c’est à vous de choisir).
Cependant, Workers dispose également d’un type de stockage unique et extrêmement rapide qui peut être parfaitement adapté à ce cas d’utilisation : les Durable Objects. Un Durable Object est une forme particulière de Worker désignée par un nom unique, avec une seule instance dans le monde entier pour chaque nom. Cette instance associée à une base de données SQLite qui réside sur le disque local de la machine sur laquelle s’exécute le Durable Object. L’accès au stockage devient ainsi ridiculement rapide : il y a en pratique une latence nulle.
Ensuite, ce que vous souhaitez vraiment, c’est peut-être que votre IA écrive du code pour un Durable Object, que vous voudrez exécuter dans un Dynamic Worker.
Voilà un problème étrange. Normalement, pour utiliser la solution Durable Objects, vous devez :
Écrire une classe pour étendre DurableObject.
L'exporter depuis le module principal de votre Worker.
Indiquer dans votre configuration Wrangler que le stockage doit être provisionné pour cette classe. Cette opération crée un espace de noms Durable Object qui pointe vers votre classe pour le traitement des requêtes entrantes.
Déclarer une liaison d’espace de noms Durable Object pointant vers votre espace de noms (ou utiliser ctx.exports), et l'utiliser pour effectuer des requêtes vers votre instance Durable Object.
Cela ne s’étend pas naturellement aux Dynamic Workers. Il se pose d'abord un problème évident : le code est dynamique. Vous l’exécutez sans faire appel du tout à l’API Cloudflare. Cependant, le stockage Durable Objects doit être provisionné par le biais de l’API, et l’espace de noms doit pointer vers une classe de mise en oeuvre. Il ne peut pas pointer vers votre Dynamic Worker.
Mais il existe un problème plus profond : même si vous pouviez, d’une manière ou d’une autre, configurer un espace de noms Durable Object de façon qu’il pointe directement vers un Dynamic Worker, le souhaiteriez-vous ? Souhaitez-vous que votre agent (ou votre utilisateur) puisse créer un espace de noms complet contenant des Durable Objects ? Qu'il utilise un stockage illimité et réparti dans le monde entier ?
Probablement non. Vous souhaitez probablement garder un certain contrôle. Vous souhaitez certainement limiter (ou, au moins, suivre) le nombre d’objets qu’il crée. Vous souhaitez peut-être le limiter à un seul objet (probablement suffisant pour les applications personnelles codées en vibe coding). Vous pouvez ajouter la journalisation et d’autres fonctionnalités d’observabilité. Mesures. Facturation. Etc.
Pour effectuer tout cela, ce que vous souhaitez vraiment, c’est que les requêtes adressées à ces Durable Objects soient transmises à votre code en premier, où vous pouvez ensuite effectuer toute la « logistique », avant de transmettre la requête au code de l’agent. Vous souhaitez écrire un superviseur qui s’exécute dans le cadre de chaque Durable Object.
Solution : Durable Object Facets
Nous lançons aujourd’hui, en bêta ouverte, une fonctionnalité qui résout ce problème.
Les facettes Durable Object vous permettent de charger et d’instancier une classe Durable Object dynamiquement, tout en lui fournissant une base de données SQLite à utiliser pour le stockage. Avec les facettes :
Vous commencez par créer un espace de noms Durable Objects normal, pointant vers une classe que vous écrivez.
Dans cette classe, vous chargez le code de l’agent en tant que Dynamic Worker et vous l’appelez.
Le code de Dynamic Worker peut mettre directement en oeuvre une classe Durable Object. C’est-à-dire qu’il exporte littéralement une classe comportant l'instruction extends DurableObject.
Vous instanciez cette classe en tant que « facette » de votre propre Durable Object.
La facette obtient sa propre base de données SQLite, qu’elle peut utiliser via les API normales de stockage Durable Objects. Cette base de données se distingue de la base de données du superviseur, mais les deux sont stockées ensemble dans le même Durable Object global.
Vous trouverez ci-dessous une mise en œuvre simple et complète d’une plateforme d’applications permettant de charger et d’exécuter dynamiquement une classe Durable Objects :
import { DurableObject } from "cloudflare:workers";
// For the purpose of this example, we'll use this static
// application code, but in the real world this might be generated
// by AI (or even, perhaps, a human user).
const AGENT_CODE = `
import { DurableObject } from "cloudflare:workers";
// Simple app that remembers how many times it has been invoked
// and returns it.
export class App extends DurableObject {
fetch(request) {
// We use storage.kv here for simplicity, but storage.sql is
// also available. Both are backed by SQLite.
let counter = this.ctx.storage.kv.get("counter") || 0;
++counter;
this.ctx.storage.kv.put("counter", counter);
return new Response("You've made " + counter + " requests.\\n");
}
}
`;
// AppRunner is a Durable Object you write that is responsible for
// dynamically loading applications and delivering requests to them.
// Each instance of AppRunner contains a different app.
export class AppRunner extends DurableObject {
async fetch(request) {
// We've received an HTTP request, which we want to forward into
// the app.
// The app itself runs as a child facet named "app". One Durable
// Object can have any number of facets (subject to storage limits)
// with different names, but in this case we have only one. Call
// this.ctx.facets.get() to get a stub pointing to it.
let facet = this.ctx.facets.get("app", async () => {
// If this callback is called, it means the facet hasn't
// started yet (or has hibernated). In this callback, we can
// tell the system what code we want it to load.
// Load the Dynamic Worker.
let worker = this.#loadDynamicWorker();
// Get the exported class we're interested in.
let appClass = worker.getDurableObjectClass("App");
return { class: appClass };
});
// Forward request to the facet.
// (Alternatively, you could call RPC methods here.)
return await facet.fetch(request);
}
// RPC method that a client can call to set the dynamic code
// for this app.
setCode(code) {
// Store the code in the AppRunner's SQLite storage.
// Each unique code must have a unique ID to pass to the
// Dynamic Worker Loader API, so we generate one randomly.
this.ctx.storage.kv.put("codeId", crypto.randomUUID());
this.ctx.storage.kv.put("code", code);
}
#loadDynamicWorker() {
// Use the Dynamic Worker Loader API like normal. Use get()
// rather than load() since we may load the same Worker many
// times.
let codeId = this.ctx.storage.kv.get("codeId");
return this.env.LOADER.get(codeId, async () => {
// This Worker hasn't been loaded yet. Load its code from
// our own storage.
let code = this.ctx.storage.kv.get("code");
return {
compatibilityDate: "2026-04-01",
mainModule: "worker.js",
modules: { "worker.js": code },
globalOutbound: null, // block network access
}
});
}
}
// This is a simple Workers HTTP handler that uses AppRunner.
export default {
async fetch(req, env, ctx) {
// Get the instance of AppRunner named "my-app".
// (Each name has exactly one Durable Object instance in the
// world.)
let obj = ctx.exports.AppRunner.getByName("my-app");
// Initialize it with code. (In a real use case, you'd only
// want to call this once, not on every request.)
await obj.setCode(AGENT_CODE);
// Forward the request to it.
return await obj.fetch(req);
}
}
Dans cet exemple :
AppRunner est un Durable Object « normal » écrit par le développeur de la plateforme (vous).
Chaque instance de AppRunner gère une application. Elle stocke le code de l’application et le charge à la demande.
L’application elle-même met en oeuvre et exporte une classe Durable Object, dont la plateforme s’attend à ce qu'elle soit nommée App.
AppRunner charge le code de l’application à l’aide de Dynamic Workers, puis exécute le code en tant que facette Durable Object.
Chaque instance de AppRunner est un Durable Object composé de deux bases de données SQLite : l’une appartenant au parent (AppRunner lui-même) et l’autre appartenant à la facette (App). Ces bases de données sont isolées : l’application ne peut pas lire la base de données de AppRunner, seulement la sienne.
Pour exécuter l’exemple, copiez le code ci-dessus dans un fichier worker.js, associez-le au fichier wrangler.jsonc suivant, et exécutez-le en local avec npx wrangler dev.
// wrangler.jsonc for the above sample worker.
{
"compatibility_date": "2026-04-01",
"main": "worker.js",
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": [
"AppRunner"
]
}
],
"worker_loaders": [
{
"binding": "LOADER",
},
],
}
Les facettes sont une fonctionnalité des Dynamic Workers, disponible immédiatement en version bêta pour les utilisateurs de l’offre payante Workers.
Consultez la documentation pour en savoir plus sur les Dynamic Workers et les Facettes.