Durable Objects
Cloudflare Durable Objects provide a way to run stateful code on Cloudflare’s edge network.
To describe them simply (a task which is difficult to do justice), Durable Objects are:
- A place to store data (SQLite and KV storage).
- A single threaded sequential execution context.
- Capable of being sharded across any number of instances (think database-per-X).
Cloesce provides first class support for Durable Objects, allowing you to define them in your schema, generate a fully typed interface, injecting execution context into your API implementations, and using them as a Model backing.
Tip
Durable Objects are not a Model, but rather a place that any number of Models can be backed by.
A Durable Object instance can store any number of Models within its SQLite and KV storage, and can execute any code necessary to manage those Models.
For more on using Durable Objects as a Model backing, see the Models chapter.
Warning
Cloesce is only capable of using the modern SQLite backed Durable Objects, and does not support the legacy Durable Object storage API.
Defining a Durable Object Binding
To define a Durable Object environment binding, use the durable block:
durable MyShardedDo {
shard {
tenant: int
}
// ... define as many binding templates as necessary
settings() -> json {
"settings"
}
userMap(userId: int) -> json {
"user/{userId}"
}
}
durable MyGlobalDo {
settings() -> json {
"settings"
}
}
The above example defines two Durable Object bindings:
-
MyShardedDo: A sharded Durable Object, meaning any number of Durable Object instances can be created with different shard parameters. In this case, thetenantparameter is used to shard the Durable Object by tenant ID. -
MyGlobalDo: A global Durable Object, meaning Cloesce will treat it as if there is only one instance of the Durable Object, and will not allow any shard parameters to be defined.
In both bindings, KV templates can be defined to generate a typed interface for interacting with the Durable Object’s KV storage.
In the case of MyShardedDo, the userMap template will generate an interface for storing user data in the Durable Object’s KV storage, with keys formatted as "user/{userId}".
Generated Interface
Cloesce will create an abstract class to extend for each Durable Object binding defined in the schema, along with helper functions merged into the Cloesce Env type.
The generated abstract class provides:
-
KV template accessors (e.g.
this.settings,this.userMap(userId)) withget,put,list, andtemplatemethods for interacting with the Durable Object’s KV storage. -
A
cloescemethod for applying generated migrations as well as invoking the Cloesce Router.
The generated Env helpers provide:
env.MyShardedDo.template(tenant)— returns the shard key string for a given set of shard parameters.env.MyShardedDo.id(tenant)— returns aDurableObjectIdfor the given shard parameters.env.MyShardedDo.stub(tenant)— returns a typedDurableObjectStubfor the given shard parameters.
For global Durable Objects (no shard fields), these helpers take no arguments.
Extending the Durable Object Class
The Cloesce Router will forward HTTP requests bound for a particular Durable Object from the Worker to the fetch method of the generated Durable Object class.
To implement custom logic for handling these requests, extend the generated Durable Object class and implement the fetch method:
import * as clo from "@cloesce/backend.js";
export class MyShardedDo extends clo.MyShardedDo {
app: CloesceApp;
constructor(state: DurableObjectState, env: clo.CfEnv) {
super(state, env);
this.app = this.cloesce(env, [...migrations]);
this.app.register(...);
}
async fetch(request: Request): Promise<Response> {
return await this.app.run(request);
}
}
Here, the MyShardedDo class extends the generated clo.MyShardedDo class, and implements the fetch method to handle incoming HTTP requests.
The cloesce method is used to create a Cloesce application instance, which can be used to register API implementations and run the application.
Wrangler Configuration
A Wrangler configuration will be generated for each Durable Object binding defined in the schema:
[[durable_objects.bindings]]
class_name = "MyShardedDo"
name = "MyShardedDo"
[[durable_objects.bindings]]
class_name = "MyGlobalDo"
name = "MyGlobalDo"
[[migrations]]
new_sqlite_classes = [
"MyShardedDo",
"MyGlobalDo",
]
tag = "v1"
Backing Models
A Model may be backed by a Durable Object, meaning the data for that Model can be pulled from an instance of a Durable Object. Declarations must explicitly state each shard field on the Model (aliased to any name in shard field order):
durable MyShardedDo {
shard {
tenant: int
}
}
model Tenant for MyShardedDo(tenant) {
// ...
}
// can alias `tenant` to anything
model Tenant for MyShardedDo(aliasWhatever) {
// ...
}
In this case, the Tenant Model is backed by the MyShardedDo Durable Object, and the tenant shard field is declared on the Model.
Read more about backing Models with Durable Objects in the Models chapter.
Execution Context
Durable Objects provide a single threaded execution context in which their internal storage may be accessed and mutated.
Just because a Model is backed by a Durable Object does not mean that all API methods and Data Sources that interact with that Model are executed within the Durable Object’s execution context.
Explicitly inject the Durable Object’s execution context into any API method or Data Source method to interact with the Durable Object’s internal storage. For example:
api AnyModel {
[inject MyShardedDo(tenant)]
get doSomething(tenant: int) -> json
}
In order to instantiate the MyShardedDo execution context, the tenant shard parameter must be passed in as an argument to the API method, and the method must be decorated with the inject tag.
Read more about execution context injection in the API chapter and the Data Source chapter.