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()
:
- Python
- TypeScript
from_account = Account.ref(request.from_account_id)
const fromAccount = Account.ref(request.fromAccountId);
Or in React using one of the generated hooks:
const account = useAccount({ id });
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.
Within one of your servicer methods you can get the ID of the current
instance via context.state_id
(Python), context.stateId
(TypeScript).
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.
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:
- Python
- TypeScript
counter = Counter.ref(id)
# Will implicitly construct if not already constructed!
await counter.Increment(context)
const counter = Counter.ref(id);
// Will implicitly construct if not already constructed!
await counter.increment(context);
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
:
- Protobuf
- Zod
rpc Open(OpenRequest) returns (OpenResponse) {
option (rbt.v1alpha1.method).writer = {
constructor: {},
};
}
export const api = {
Account: {
// ...
methods: {
// ...
open: {
kind: "writer",
// Must use this function to construct an instance of `Account`.
factory: {},
// ...
},
},
},
};
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:
- Python
- TypeScript
account, response = await Account.Open(
context,
customer_name=request.customer_name,
)
const [account, response] = await Account.open(context, {
customerName: request.customerName,
});
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:
- Python
- TypeScript
account, _ = await Account.Open(
context,
new_account_id,
customer_name=request.customer_name,
)
const [account, response] = await Account.open(context, newAccountId, {
customerName: request.customerName,
});
Idempotent construction
You can call explicit constructors using
idempotently()
external to a Reboot
application (i.e., using an ExternalContext
):
- Python
- TypeScript
account, response = await Account.idempotently().Open(
context,
customer_name=request.customer_name,
)
const [account, response] = await Account.idempotently().open(context, {
customerName: request.customerName,
});