Build with Rivet

State

Actor state provides the best of both worlds: it's stored in-memory and persisted automatically. This lets you work with the data without added latency while still being able to survive crashes & upgrades.

Actor state is isolated to itself and cannot be accessed from other actors or clients. All reads & writes to state are done via RPC.

There are two ways of storing actor state:

  • Native state is the most common persistence mechanism. State is a native JavaScript object stored in memory.
  • Key-Value (KV) state allows you deal with larger datasets than cannot fit in memory.

Native state

State is a native JavaScript object stored in-memory on this.state. This makes building realtime & stateful applications as simple as updating native JavaScript objects.

State type

Actor states can be typed in TypeScript using the first generic argument on Actor<State>. For example:

interface State {
  count: number;
}

class Counter extends Actor<State> {
  // ...
}
TypeScript

Initializing state

Each requires an initializeState method. This is only called once when the actor is created.

For example:

interface State {
  count: number;
}

class Counter extends Actor<State> {
  _onInitialize() {
    return { count: 0 };
  }
}
TypeScript

Updating state

State can be updated using this.state. State will automatically be persisted.

For example:

interface State {
  count: number;
}

class Counter extends Actor<State> {
  increment(rpc: Rpc<this>) {
    this.state.count += 1;
  }

  // ...
}
TypeScript

State saves

Rivet automatically handles persisting state transparently to recover from a crash or upgrade. This happens at the end of every remote procedure call if the state has changed.

In the rare occasion you need to force a state change mid-RPC, you can use _saveState. This should only be used if your remote procedure call makes an important state change that needs to be persisted before the RPC exits in case of a crash.

Valid data types

Only JSON-serializable types can be stored in state. State is persisted under the hood in a compact, binary format. This is because JavaScript classes cannot be serialized & deserialized.

Limitations

State is constrained to the available memory (see limitations). For larger datasets, use KV.


Key-Value (KV)

The KV state is used for storing large datasets that cannot fit in to memory.

Native & KV state can be used together side-by-side without issue..

Performance

KV has the same performance as using native state, but with a more flexible API & unlimited storage.

KV stores native JavaScript values in a compact binary format so you don't need to write extra serialization & deserialization code.

Operations

Raw KV operations can be called via this.#ctx.kv.<op>.

get

get(key: any, options?: GetOptions): Promise<any | null>

Retrieves a value from the key-value store.

Options:

{
    format?: "value" | "arrayBuffer";
}
JavaScript

getBatch

getBatch(keys: any[], options?: GetBatchOptions): Promise<Map<any, any>>

Retrieves a batch of key-value pairs.

Options:

{
    format?: "value" | "arrayBuffer";
}
JavaScript

list

list(options?: ListOptions): Promise<Map<any, any>>

Retrieves all key-value pairs in the KV store. When using any of the options, the keys lexicographic order is used for filtering.

Options:

{
    format?: "value" | "arrayBuffer";
    // The key to start listing results from (inclusive). Cannot be used with startAfter or prefix.
    start?: any;
    // The key to start listing results after (exclusive). Cannot be used with start or prefix.
    startAfter?: any;
    // The key to end listing results at (exclusive).
    end?: any;
    // Restricts results to keys that start with the given prefix. Cannot be used with start or startAfter.
    prefix?: any;
    // If true, results are returned in descending order.
    reverse?: boolean;
    // The maximum number of key-value pairs to return.
    limit?: number;
}
JavaScript

put

put(key: any, value: any | ArrayBuffer, options?: PutOptions): Promise<void>

Stores a key-value pair in the key-value store.

Options:

{
    format?: "value" | "arrayBuffer";
}
JavaScript

putBatch

putBatch(obj: Map<any, any | ArrayBuffer>, options?: PutBatchOptions): Promise<void>

Stores a batch of key-value pairs.

Options:

{
    format?: "value" | "arrayBuffer";
}
JavaScript

delete

delete(key: any): Promise<void>

Deletes a key-value pair from the key-value store.

deleteBatch

deleteBatch(keys: any[]): Promise<void>

Deletes a batch of key-value pairs from the key-value store.

deleteAll

deleteAll(): Promise<void>

Deletes all data from the key-value store. This CANNOT be undone.

Keys

Keys used for KV storage can be any JavaScript type that can be cloned via the structured clone algorithm:

let myKey = { foo: ['bar', 2] };

await this.#ctx.kv.put(myKey, [1, 2, 3]);

await this.#ctx.kv.get(myKey); // [1, 2, 3]
JavaScript

Structured Keys

Structured keys provide security and ease of use for applications with layered storage criteria such as lists within lists or deeply nested hashmaps.

In general, it is more efficient to your data structure with small chunks in individual keys instead of all in one key. This is where structured keys come in:

// Entire document in a single key (not recommended)
await this.#ctx.kv.put(["user", "kacper"], {
  inventory: /* ... */,
  stats: /* ... */,
  paymentMethod: /* ... */,
});

// Sharded document using structured keys (arrays)
await this.#ctx.kv.put(["user", "kacper", "inventory"], /* ... */);
await this.#ctx.kv.put(["user", "kacper", "stats"], /* ... */);
await this.#ctx.kv.put(["user", "kacper", "paymentMethod"], /* ... */);
JavaScript

It is strongly advised to always use structured keys instead of manually implementing them yourself to reduce possible attack vectors from end-users:

let userName = /* ... */;

// Manually building keys (**DON'T DO THIS**)
let user = await this.#ctx.kv.get(`user:${userName}`);

// Structured keys
let user = await this.#ctx.kv.get(["user", userName]);
JavaScript

The difference here is that with a manual approach it is possible to retrieve data that was otherwise not public via injection:

// Setting our username to this value lets us access the inventory
// of any user, which should otherwise be private.
let userName = 'nicholas:inventory';

// Manually building keys (**DON'T DO THIS**)
let user = await this.#ctx.kv.get(`user:${userName}`);

// Structured keys automatically provide protection against special
// token boundary attacks
let user = await this.#ctx.kv.get(['user', userName]);
JavaScript

Note that single-value keys are automatically converted into single item lists for consistency:

// The same
await this.#ctx.kv.get('my-key');
await this.#ctx.kv.get(['my-key']);
JavaScript

Sorting Keys

Keys are automatically sorted in lexicographic order. This means when using the list command, you can fetch all values between two keys in order:

// Fetch all users with usernames that start with "k" through "s"
// (note that the end is exclusive, so "t" is not included)
await this.#ctx.kv.list({
  start: ['users', 'k'],
  end: ['users', 't']
});
JavaScript

You can also use this to list all values under a common prefix key:

// Fetch all items in a user's inventory
await this.#ctx.kv.list({
  prefix: ['users', 'nathan', 'inventory']
});
JavaScript

Sorted keys also enable you to create ordered lists, like this:

// bar posted a score of 88
await this.#ctx.kv.put(["leaderboard", 88], { username: "bar", date: Date.now() });

// foo posted a score of 42
await this.#ctx.kv.put(["leaderboard", 42], { username: "foo", date: Date.now() });

// Returns 88, 42
await this.#ctx.kv.list({
  prefix: ['leaderboard'],
  reverse: true,  // Descending order
});
JavaScript

Values

Values stored in the KV can be any JavaScript type which can be cloned via the structured clone algorithm.

To store raw binary data, it is recommended to set the format option in your KV operation to arrayBuffer and pass in an ArrayBuffer object. Alternatively, you can put an ArrayBuffer or Blob directly without changing the format but this has additional space overhead from the JS type system.

Limitations

See limitations.