Tiny (~400 bytes) wrapper for Cloudflare Durable Objects, adding the following functionality to the native RPC implementation:
- Auto persists properties to storage
- Optionally define your own external store (e.g. R2, KV, supabase, any async service/API, etc)
- Use with any router/framework... itty, Hono, etc
Bonus - Middleware is included for itty-router and Hono to allow easier accessing of stubs by string identifier (simplifies access from Worker). Documentation to come...
This is a next
tag release, for playtesting/confirming the final public version. While I've tried to cover the bases, there is not yet test coverage (until the API is finalized), and the API may change as discussions/feedback unfolds. Use at your own risk!
npm install itty-durable@next
import { IttyDurable } from 'itty-durable'
export class Counter extends IttyDurable {
// anything in this.persisted will be automatically stored
$persisted = {
value = 0
}
increment(by: number = 1) {
this.$persisted.value += by
return this.$props() // optionally return the props
}
}
Under the hood, IttyDurable
(which directy extends DurableObject
) returns a Proxy to itself. When methods on your Durable Object are called, it intercepts these requests to do the following lifecycle steps:
- Sync props from storage if not already done. Thanks to RPC being fully async, we can safely await this, allowing slow requests from any data source.
- Execute the original method call.
- Register a debounced timer (default delay is 2000ms, subject to change) to persist the props back to storage. This prevents rapid method executions from writing more than necessary to storage. When action on the DO stops, the write executes well before the DO expires.
The API of IttyDurable
is intentionally minimalist. In fact, we only expose a handful of properties/methods, each prefixed with a $
for clarity/separation:
The $persisted
property in your DO is automatically synced via the store (defaults to using DO internal storage using a single key). Anything you put in here will be saved. Anything outside will reside in memory only, resetting when the DO resets/sleeps.
export class Counter extends IttyDurable {
// this property will not be persisted
foo = 'bar'
// but this will be
$persisted = {
value = 0
}
increment(by: number = 1) {
this.persisted.value += by
}
}
The $store
property can be used to swap in your own external storage. The required interface is simple, consisting of a get/put method pair. These functions can be sync/async - as they'll be awaited regardless. To use the original DO as this
within your functions (e.g. to access this.ctx
or bindings within this.env
), you must write these in function syntax (shown below), rather than arrow functions. If you don't need this
, then either will work.
const CustomStore = {
get() {
// return some data here
},
put(value: any) {
// handle storage of value
}
}
export class Counter extends IttyDurable {
$store = CustomStore
// ...
}
To give you a better idea of a store implementation, here's the actual default store (writes to a single key in default DO storage):
$store: DurableStore = {
get() {
return this.ctx.storage.get('v')
},
put(value: any) {
this.ctx.storage.put('v', value)
}
}
The $persist()
method is automatically called internally when methods are executed, but you can always call it yourself manually if you want an immediate (or delayed) write.
export class Counter extends IttyDurable {
value = 20
increment(by: number = 1) {
this.value += by
this.$persist() // calls persist with 0 delay
this.$persist(10000) // calls persist with 10 second delay
}
}
The $props()
method is used to simplify the extraction of your own properties, including the contents within the memory-only $
object. This is a helpful return for your functions that are called from a Worker, if they need to see/return the contents of the DO after exection. This function naturally omits the env
, ctx,
$store` properties on the DO.
export class Counter extends IttyDurable {
value = 20
foo = 'bar'
increment(by: number = 1) {
this.value += by
return this.$props() // { $: {}, value: 21, foo: 'bar' }
}
}