do-proxy
A library for writing type-safe Durable Objects (DOs) in Rust.
With do-proxy
you can:
- Easily write type-safe APIs for Durable Objects.
- Abstract over
fetch
,alarm
and request-response glue code.
Overview
do-proxy provides a core trait DoProxy
that abstracts over ser/de request response code, object initalization and loading, and Error handling glue code.
After a struct implements DoProxy
, the macro do_proxy!
creates the workers-rs' #[DurableObject]
struct which ends up generating the final object.
Object Lifecycle
This library provides two separate Request
type. A normal Request
which is what the durable object will be sent 99% of the time and an Init
type, which is optionally sent to initialize the object.
For example, lets say we have a Person
object. The struct might look like:
struct Person {
birthday: DateTime<Utc>,
name: String
}
impl DoProxy for Person {
// ...
}
do_proxy!(Person, PersonObject);
The birthday
and name
fields are non-optional and required. However, when constructing a durable object with new
in Rust or constructor
in TypeScript, the only information you get is the State
and Storage
. So when you're loading the Person
object for the first time, you'll have to use bogus values for name
and birthday
because they haven't been set yet.
The request that prompted the creation of the object will likely be some kind of "create" command which sets birthday
and name
to something realistic. But that's not guaranteed.
What if the person receives a command CalculateNextBirthday
before its been created? Now you need to explicitly check that those values aren't bogus or wrap everything in an Option
. Both of those options are either prone to errors (bogus value) or unergonomic (Option
).
To solve this, do-proxy
has two functions that are used to construct an object.
init
: crates and saves all information necessary to construct an object inload_from_storage
. When a user of the library sends a request to thePerson
object, they'll be able to optionally addDoProxy::Init
data. If that data is present,init
will be called beforeload_from_storage
.load_from_storage
: loads an object from storage. If the object is missing fields or hasn't been initialized, this function should error.
In the following example, we send both initialization information along with a command. The object is first initialized and then the command is handled:
let proxy = env.obj::<Person>("[email protected]");
let resp = proxy
.init(Person::new("Bob", bobs_birthday))
.and_send(Command::CalculateNextBirthday).await?;
If you know that the object must be initialized or can't create initialization information, you can just send the command:
let proxy = env.obj::<Person>("[email protected]");
let resp = proxy.send(Command::CalculateNextBirthday).await?;
This approach lets you avoid options, bogus values and the init
function is async
.
Examples
The crates under ./examples act as examples for the library, and in the future, they act as fully-fledged copy+paste building blocks for distributed systems.
Each crate has a hurl script that show how the wrapping worker can be queried.
inserter
: An InserterObject responds to a simple KV-like API for getting, inserting and deleting KV pairs in its storage. Example:
POST http://localhost:8787/test_do
{
"insert": {
"key": "hello",
"value": "world!"
}
}
POST http://localhost:8787/test_do
{
"get": {
"key": "hello"
}
}
# returns { "value": "world!" }