/do-proxy

A library for writing type-safe Durable Objects in Rust.

Primary LanguageRustMIT LicenseMIT

do-proxy

Crates.io Docs.rs MIT licensed

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 in load_from_storage. When a user of the library sends a request to the Person object, they'll be able to optionally add DoProxy::Init data. If that data is present, init will be called before load_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.

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!" }