Skip to main content

Overview

Usually you'll call methods via the generated Python, TypeScript, or React client code (or libraries that wrap the generated code).

In order to call a method on an instance of your data types you need to first get a reference to it, e.g., on the backend via ref():

from_account = Account.ref(request.from_account_id)

Or in React using one of the generated hooks:

const account = useAccount({ id });
important

You can think of a reference like a handle (and it's similar to a "stub" in gRPC), but getting a reference via ref() or use...() does not mean that an instance of the data type has been constructed!

In fact, the type returned from calling .ref() is a WeakReference, where "weak" indicates that the instance may or may not have been constructed.

State IDs

We call the ID that you use to get a reference to your data type its "state ID". This ID is unique within your application, i.e., you'll always get the same instance of your data type for that ID.

The ID is also used to automatically partition your data type instances across CPU cores. You can think of the state ID like a primary key in a database.

tip

Within one of your servicer methods you can get the ID of the current instance via context.state_id (Python), context.stateId (TypeScript).

note

Singletons

Sometimes you might want a data type to act as a "singleton" where there is a single well-known ID that is always used. But be warned, to avoid making the singleton a scalability bottleneck you should avoid storing too much data in a single instance of a data type.

It's a good practice to ensure your singletons are constructed in the initialize function that you pass to Application.

Constructing instances

You construct an instance of your data types either implicitly or explicitly, depending on whether or not you have explicitly designated certain methods as constructors in your .proto, or a factory, in your Zod schema.

tip

If your state can be default constructed, prefer implicit construction! Only use explicit construction if you have some initialization that must be performed.

Implicit construction

Any data types that don't have an explicit constructor/factory will be implicitly constructed when you call a writer or transaction method:

counter = Counter.ref(id)

# Will implicitly construct if not already constructed!
await counter.Increment(context)
tip

A state will not be implicitly constructed when you call a reader method so you may need to properly ensure you've constructed your state by calling a writer or transaction first (which may just be a no-op method that acts like a "constructor" but can be called multiple times unlike explicit constructors which can only be called once).

Explicit construction

If you've defined a constructor/factory for your data type then you must use it for constructing an instance.

Here's an example of both using Zod and a .proto for designating that the Open method is an explicit constructor for Account:

rpc Open(OpenRequest) returns (OpenResponse) {
option (rbt.v1alpha1.method).writer = {
constructor: {},
};
}

A constructor/factory is exposed as a static or class method in the generated code, and returns a tuple where the first element is a reference to the constructed state (same thing returned from ref()) and the second is the response from calling the method. For example:

account, response = await Account.Open(
context,
customer_name=request.customer_name,
)
caution

You can only call an explicit constructor once! Otherwise you'll get back a StateAlreadyConstructed error. This can be useful if you're trying to check if you've already constructed the state, but you may also consider using implicit construction and calling a no-op method to ensure that the state has been constructed.

Calling an explicit constructor will generate a new unique ID (a UUID) for the constructed state. Alternatively, you can specify an ID explicitly upon construction by passing it after context. Here is an example of constructing an Account with an explicit ID using the Open constructor:

account, _ = await Account.Open(
context,
new_account_id,
customer_name=request.customer_name,
)

Idempotent construction

You can call explicit constructors using idempotently() external to a Reboot application (i.e., using an ExternalContext):

account, response = await Account.idempotently().Open(
context,
customer_name=request.customer_name,
)