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.
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.
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>("bob@buzz.com");
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>("bob@buzz.com");
let resp = proxy.send(Command::CalculateNextBirthday).await?;
This approach lets you avoid options, bogus values and the init
function
is async
.
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!" }