From 006415f708afe36aefc3732ebc1f341ef85cbb8d Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Fri, 12 Jun 2026 13:17:09 -0700 Subject: [PATCH 1/4] [SLOP(claude-opus-4-8-medium)] fix(rivetkit-wasm): add missing inspector_tabs field to ActorConfigInput conversion --- rivetkit-typescript/packages/rivetkit-wasm/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rivetkit-typescript/packages/rivetkit-wasm/src/lib.rs b/rivetkit-typescript/packages/rivetkit-wasm/src/lib.rs index 96678fc889..b0ce74520b 100644 --- a/rivetkit-typescript/packages/rivetkit-wasm/src/lib.rs +++ b/rivetkit-typescript/packages/rivetkit-wasm/src/lib.rs @@ -239,7 +239,8 @@ impl From for ActorConfigInput { .map(|action| rivetkit_core::ActionDefinition { name: action.name }) .collect() }), - // The wasm runtime does not expose custom inspector tabs yet. + // Custom inspector tabs serve assets from a filesystem `root`, which is a + // native/server feature that has no meaning in a browser wasm host. inspector_tabs: None, } } From a8e58ed102483eccb7ba9452ccd1ddfbfebdb7cb Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Fri, 12 Jun 2026 13:19:59 -0700 Subject: [PATCH 2/4] [SLOP(claude-opus-4-8-medium)] docs(quickstart): clean up rust quickstart to import shared actor and auto-download engine --- .../content/docs/actors/quickstart/rust.mdx | 126 +++++++++--------- 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/website/src/content/docs/actors/quickstart/rust.mdx b/website/src/content/docs/actors/quickstart/rust.mdx index e411f7ef60..dc106d5859 100644 --- a/website/src/content/docs/actors/quickstart/rust.mdx +++ b/website/src/content/docs/actors/quickstart/rust.mdx @@ -7,7 +7,7 @@ skill: true import { Hosting } from "@/components/docs/Hosting"; -Rust support is in preview. The supported public Rust API is `rivetkit` and `rivetkit-client`; lower-level crates are internal implementation details and do not carry a stability guarantee. +Rust support is in preview. The supported public Rust API is `rivetkit` and `rivetkit-client`; lower-level crates are internal implementation details and do not carry a stability guarantee. See the full API reference on [docs.rs/rivetkit](https://docs.rs/rivetkit). ## Steps @@ -15,7 +15,7 @@ Rust support is in preview. The supported public Rust API is `rivetkit` and `riv -Add the `rivetkit` crate: +Add the `rivetkit` crate and its companions: ```sh cargo add rivetkit@2.3.0-rc.12 anyhow async-trait @@ -25,11 +25,11 @@ cargo add tokio --features full - + -An actor is a type that implements `Actor`, plus one `Handles` implementation for each action. Persisted state lives in `type State`; ephemeral runtime state is just fields on your actor struct. +Put the actor in `src/lib.rs` so both your server and your client can share the same types. An actor is a type that implements `Actor`, plus one `Handles` implementation for each action. Persisted state lives in `type State`; ephemeral runtime state is just fields on your actor struct. -```rust src/main.rs +```rust src/lib.rs use std::{future::Future, pin::Pin, sync::Arc}; use async_trait::async_trait; @@ -38,16 +38,16 @@ use serde::{Deserialize, Serialize}; type BoxFuture = Pin> + Send>>; -struct Counter; +pub struct Counter; #[derive(Default, Serialize, Deserialize)] -struct CounterState { - count: i64, +pub struct CounterState { + pub count: i64, } #[derive(Serialize, Deserialize)] -struct Increment { - amount: i64, +pub struct Increment { + pub amount: i64, } impl Action for Increment { @@ -57,8 +57,8 @@ impl Action for Increment { } #[derive(Serialize, Deserialize)] -struct NewCount { - count: i64, +pub struct NewCount { + pub count: i64, } impl Event for NewCount { @@ -101,27 +101,44 @@ impl Handles for Counter { } } -#[tokio::main] -async fn main() -> Result<()> { +pub fn registry() -> Registry { let mut registry = Registry::new(); registry.register_actor::("counter"); - registry.start().await + registry +} +``` + + + + + +Your `src/main.rs` just starts the registry from the library: + +```rust src/main.rs +#[tokio::main] +async fn main() -> anyhow::Result<()> { + counter::registry().start().await } ``` +Replace `counter` with your crate name (the package `name` in `Cargo.toml`, with dashes as underscores). + -The Rust runtime connects to the Rivet Engine. Build the engine binary once, then start your server. `RIVET_ENGINE_BINARY_PATH` tells the runtime where to find the engine; it spawns or reuses a local engine at `http://localhost:6420`. +The Rust runtime connects to the Rivet Engine. Setting `RIVETKIT_ENGINE_AUTO_DOWNLOAD=1` lets the runtime download and cache a matching engine binary the first time you run, so there is nothing else to install: ```sh -cargo build -p rivet-engine -RIVET_ENGINE_BINARY_PATH=./target/debug/rivet-engine cargo run +RIVETKIT_ENGINE_AUTO_DOWNLOAD=1 cargo run ``` Your server now connects to the Rivet Engine on `http://localhost:6420`. Clients connect directly to the engine on this port. + +Already have an engine binary? Set `RIVET_ENGINE_BINARY_PATH=/path/to/rivet-engine` to point at it instead. If you are working inside the [Rivet monorepo](https://github.com/rivet-dev/rivet), a local `cargo build -p rivet-engine` is discovered automatically from `target/debug`. + + @@ -132,55 +149,15 @@ This code can run either in your frontend or within your backend: -```rust src/client.rs -use anyhow::Result; +Add a `src/bin/client.rs` that imports the same actor types from your library. There is no need to redefine the actor on the client. + +```rust src/bin/client.rs +use counter::{Counter, Increment, NewCount}; use rivetkit::{ - client::{Client, ClientConfig, GetOrCreateOptions}, + client::{Client, ClientConfig}, prelude::*, TypedClientExt, }; -use serde::{Deserialize, Serialize}; - -struct Counter; - -#[derive(Serialize, Deserialize)] -struct Increment { - amount: i64, -} - -impl Action for Increment { - type Output = i64; - - const NAME: &'static str = "increment"; -} - -#[derive(Serialize, Deserialize)] -struct NewCount { - count: i64, -} - -impl Event for NewCount { - const NAME: &'static str = "newCount"; -} - -impl Actor for Counter { - type State = (); - type Input = (); - type Actions = (Increment,); - type Events = (NewCount,); - type Queue = (); - type ConnParams = (); - type ConnState = (); - type Action = action::Raw; -} - -impl Handles for Counter { - type Future = std::future::Ready>; - - fn handle(self: std::sync::Arc, _ctx: Ctx, _action: Increment) -> Self::Future { - unreachable!("client-only type marker") - } -} #[tokio::main] async fn main() -> Result<()> { @@ -200,6 +177,12 @@ async fn main() -> Result<()> { } ``` +With the server still running, start the client in another terminal: + +```sh +cargo run --bin client +``` + See the [`hello-world-rust`](https://github.com/rivet-dev/rivet/tree/main/examples/hello-world-rust) example for a complete runnable counter. @@ -276,3 +259,20 @@ See the [React documentation](/docs/clients/react) for more information. + +## Next Steps + + + + Full `rivetkit` crate documentation on docs.rs. + + + Define the RPC surface clients call on your actor. + + + Persist and load actor state across sleeps and restarts. + + + Broadcast realtime updates to connected clients. + + From e1326aae68f5241ca3fac9d74ae9bb6914bf2b13 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Fri, 12 Jun 2026 13:26:25 -0700 Subject: [PATCH 3/4] [SLOP(claude-opus-4-8-medium)] docs(pool-configuration): fix runner-config SDK snippet to use RivetClient from engine-api-full --- .../src/content/docs/connect/freestyle.mdx | 28 +++++++++++-------- .../docs/general/pool-configuration.mdx | 4 +-- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/website/src/content/docs/connect/freestyle.mdx b/website/src/content/docs/connect/freestyle.mdx index 6db6b40bff..09a60e74ff 100644 --- a/website/src/content/docs/connect/freestyle.mdx +++ b/website/src/content/docs/connect/freestyle.mdx @@ -107,27 +107,31 @@ Run this deployment script to push your application to Freestyle. Update the runner configuration on the Rivet side to connect with your Freestyle deployment. Create a configuration script and run it after your Freestyle deployment is live: ```typescript @nocheck -import { RivetClient } from "rivetkit/client"; +import { RivetClient } from "@rivetkit/engine-api-full"; const rivet = new RivetClient({ - endpoint: "api.rivet.dev", + environment: "https://api.rivet.dev", token: process.env.RIVET_API_TOKEN, }); const FREESTYLE_DOMAIN = "my-domain.style.dev"; // Change to your desired Freestyle domain const RIVET_NAMESPACE = "my-rivet-namespace"; // Change to your Rivet namespace -await rivet.runnerConfigs.upsert("freestyle-runner", { - serverless: { - url: `https://${FREESTYLE_DOMAIN}/start`, - runnersMargin: 1, - minRunners: 1, - maxRunners: 1, - slotsPerRunner: 1, - // Must be shorter than Freestyle request `timeout` config - requestLifespan: 60 * 5 - 5, - }, +await rivet.runnerConfigsUpsert("freestyle-runner", { namespace: RIVET_NAMESPACE, + datacenters: { + default: { + serverless: { + url: `https://${FREESTYLE_DOMAIN}/start`, + runnersMargin: 1, + minRunners: 1, + maxRunners: 1, + slotsPerRunner: 1, + // Must be shorter than Freestyle request `timeout` config + requestLifespan: 60 * 5 - 5, + }, + }, + }, }); ``` diff --git a/website/src/content/docs/general/pool-configuration.mdx b/website/src/content/docs/general/pool-configuration.mdx index 6a15088d36..780af355e8 100644 --- a/website/src/content/docs/general/pool-configuration.mdx +++ b/website/src/content/docs/general/pool-configuration.mdx @@ -17,7 +17,7 @@ Configure a pool via the dashboard, the API directly, or the TypeScript SDK: -```typescript SDK +```typescript SDK @nocheck import { RivetClient } from "@rivetkit/engine-api-full"; const rivet = new RivetClient({ @@ -28,7 +28,7 @@ const rivet = new RivetClient({ await rivet.runnerConfigsUpsert("default", { namespace: "default", datacenters: { - "us-east-1": { + default: { serverless: { url: "https://my-app.example.com/api/rivet", requestLifespan: 60 * 15, From c3fbd9f00726c25d06f6a46e3132e43b45cf1655 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Fri, 12 Jun 2026 15:20:06 -0700 Subject: [PATCH 4/4] [SLOP(claude-opus-4-8)] docs(actors): rewrite in-memory state page, remove sandbox page, fix stateSaveInterval default, forbid em dashes --- CLAUDE.md | 2 +- website/src/content/docs/actors/lifecycle.mdx | 6 +- website/src/content/docs/actors/limits.mdx | 2 +- website/src/content/docs/actors/sandbox.mdx | 24 - website/src/content/docs/actors/state.mdx | 555 +++++++++--------- website/src/sitemap/mod.ts | 11 - 6 files changed, 278 insertions(+), 322 deletions(-) delete mode 100644 website/src/content/docs/actors/sandbox.mdx diff --git a/CLAUDE.md b/CLAUDE.md index 30d98141df..71fe123a19 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -336,7 +336,7 @@ When the user asks to track something in a note, store it in `~/.agents/notes/` ### Comments - Write comments as normal, complete sentences. Avoid fragmented structures with parentheticals and dashes like `// Spawn engine (if configured) - regardless of start kind`. Instead, write `// Spawn the engine if configured`. Especially avoid dashes (hyphens are OK). -- Do not use em dashes (—). Use periods to separate sentences instead. +- Never use em dashes (—) in any plain-English writing (docs, comments, PR descriptions, prose). Use periods to separate sentences instead. - Documenting deltas is not important or useful. A developer who has never worked on the project will not gain extra information if you add a comment stating that something was removed or changed because they don't know what was there before. The only time you would be adding a comment for something NOT being there is if its unintuitive for why its not there in the first place. ### Match statements diff --git a/website/src/content/docs/actors/lifecycle.mdx b/website/src/content/docs/actors/lifecycle.mdx index 88ae401359..ccccf82ec4 100644 --- a/website/src/content/docs/actors/lifecycle.mdx +++ b/website/src/content/docs/actors/lifecycle.mdx @@ -913,8 +913,8 @@ const myActor = actor({ // Total graceful shutdown budget for both sleep and destroy. Default: 15000ms. sleepGracePeriod: 15_000, - // Interval for saving state (default: 10000ms) - stateSaveInterval: 10_000, + // Interval for saving state (default: 1000ms) + stateSaveInterval: 1_000, // Timeout for action execution (default: 60000ms) actionTimeout: 60_000, @@ -943,7 +943,7 @@ const myActor = actor({ | `createConnStateTimeout` | 5000ms | Timeout for `createConnState` function | | `onConnectTimeout` | 5000ms | Timeout for `onConnect` hook | | `sleepGracePeriod` | 15000ms | Total graceful shutdown window for both sleep and destroy | -| `stateSaveInterval` | 10000ms | Interval for persisting state | +| `stateSaveInterval` | 1000ms | Interval for persisting state | | `actionTimeout` | 60000ms | Timeout for action execution | | `connectionLivenessTimeout` | 2500ms | Timeout for connection liveness check | | `connectionLivenessInterval` | 5000ms | Interval for connection liveness check | diff --git a/website/src/content/docs/actors/limits.mdx b/website/src/content/docs/actors/limits.mdx index 22b1750181..7da039d792 100644 --- a/website/src/content/docs/actors/limits.mdx +++ b/website/src/content/docs/actors/limits.mdx @@ -133,7 +133,7 @@ See [Actor Input](/docs/actors/input) for details. | On connect timeout | 5 seconds | — | Timeout for `onConnect` hook. Configurable via `onConnectTimeout`. | | Sleep grace period | 15 seconds | — | Total graceful shutdown budget for both sleep and destroy. Covers `onSleep`/`onDestroy`, run handler shutdown, `waitUntil`, `keepAwake`, async raw WebSocket handlers, and connection cleanup. Configurable via `sleepGracePeriod`. | | Sleep timeout | 30 seconds | — | Time of inactivity before actor hibernates. Configurable via `sleepTimeout`. | -| State save interval | 10 seconds | — | Interval between automatic state saves. Configurable via `stateSaveInterval`. | +| State save interval | 1 second | — | Interval between automatic state saves. Configurable via `stateSaveInterval`. | ### Serverless Shutdown diff --git a/website/src/content/docs/actors/sandbox.mdx b/website/src/content/docs/actors/sandbox.mdx deleted file mode 100644 index 20cdb50e7f..0000000000 --- a/website/src/content/docs/actors/sandbox.mdx +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: "Sandbox Actor" -description: "The legacy TypeScript sandbox actor has been removed while the replacement runtime is rebuilt." -skill: true ---- - -The legacy TypeScript sandbox actor and provider exports were removed from -`rivetkit` while the replacement runtime is rebuilt. - -## Current status - -- The `rivetkit/sandbox` package path does not exist on this branch. -- The old `sandbox-agent` wrapper was intentionally deleted. -- The old code examples were removed so the docs stop advertising broken imports. - -## What to use instead - -- For actor hosting, use `Registry.startEnvoy()` and the native `rivetkit-core` - path. -- If you still need sandbox orchestration immediately, integrate - `sandbox-agent` directly in your own application code instead of relying on a - removed `rivetkit` wrapper. - -This page will be replaced when the new runtime lands. diff --git a/website/src/content/docs/actors/state.mdx b/website/src/content/docs/actors/state.mdx index 72a22beb00..32636bd6bf 100644 --- a/website/src/content/docs/actors/state.mdx +++ b/website/src/content/docs/actors/state.mdx @@ -1,252 +1,130 @@ --- -title: "State & Storage" -description: "Choose where to store data in your actors: in-memory state for small serializable values, embedded SQLite for large or queryable data, and ephemeral variables for connections to external databases and non-serializable runtime objects." +title: "In-Memory State" +description: "Actors store state in memory for instant reads and writes. State can be persisted automatically or kept ephemeral." skill: true --- -Actors give you several places to store data. Choosing the right one keeps your actor fast, durable, and easy to reason about. +## Durable vs Ephemeral -## Choosing Where to Store Data +There are three ways to store data in an actor, depending on what it looks like and whether it needs to survive restarts. -| Need | Use | -|---|---| -| Small, simple, serializable values (counters, flags, a small map) | `c.state` | -| Large / relational / queryable / durable data | SQLite (`c.db`) — see [SQLite docs](/docs/actors/sqlite) | -| Data in an external database, or non-serializable runtime objects (connections, clients, emitters) | `createVars` / `c.vars` | +### Durable -In-memory state (`c.state`) is the simplest option and the right default for small amounts of data. As soon as your data grows large, becomes relational, or needs to be queried, reach for [SQLite](/docs/actors/sqlite) instead. Use [ephemeral variables](#ephemeral-variables) (`c.vars`) for runtime-only objects like database clients or for loading data from an external database. +Simple, serializable data on `c.state` that is automatically persisted and restored across restarts. The default starting point. -## In-Memory 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 surviving crashes and upgrades. - -In-memory state is meant for **small, simple values** such as counters, flags, or a small map. When your data grows large or needs querying, use [SQLite](#sqlite) instead. - -### Initializing State - -There are two ways to define an actor's initial state: - - - - - -Define an actor state as a constant value: - -```typescript +```typescript Basic import { actor } from "rivetkit"; -// Simple state with a constant const counter = actor({ - // Define state as a constant + // Constant initial state state: { count: 0 }, actions: { - // ... + get: (c) => c.state.count, + + // Update state, changes are persisted automatically + increment: (c) => { + c.state.count += 1; + return c.state.count; + } } }); ``` -This value will be cloned for every new actor using `structuredClone`. - - - - - -Create actor state dynamically on each actors' creation: - -```typescript +```typescript Dynamic init import { actor } from "rivetkit"; -// State with initialization logic +interface CounterState { + count: number; +} + const counter = actor({ - // Define state using a creation function - createState: () => { - return { count: 0 }; - }, + // Compute the initial state when the actor is created + createState: (): CounterState => ({ count: 0 }), actions: { - // ... + get: (c) => c.state.count, + + increment: (c) => { + c.state.count += 1; + return c.state.count; + } } }); ``` -To accept a custom input parameters for the initial state, use: - -```typescript +```typescript With input import { actor } from "rivetkit"; -interface CounterInput { - startingCount: number; -} - interface CounterState { count: number; } -// State with initialization logic const counter = actor({ - state: { count: 0 } as CounterState, - // Define state using a creation function - createState: (c, input: CounterInput): CounterState => { - return { count: input.startingCount }; - }, + // Compute the initial state from input passed at creation + createState: (c, input: { startingCount: number }): CounterState => ({ + count: input.startingCount, + }), actions: { - increment: (c) => c.state.count++ + get: (c) => c.state.count, + + increment: (c) => { + c.state.count += 1; + return c.state.count; + } } }); ``` -Read more about [input parameters](/docs/actors/input) here. - - -If accepting arguments to `createState`, you **must** define the types: `createState(c: CreateContext, input: MyType)` + -Otherwise, the return type will not be inferred and `c.state` will be of type `unknown`. - +### Ephemeral - +Live objects on `c.vars` like database connections, API clients, and event emitters, or data loaded from an external source. Never persisted. - + -The `createState` function is called once when the actor is first created. See [Lifecycle](/docs/actors/lifecycle) for more details. - -### Modifying State - -To update state, modify the `state` property on the context object (`c.state`) in your actions: - -```typescript +```typescript Basic import { actor } from "rivetkit"; const counter = actor({ state: { count: 0 }, + // Constant ephemeral value, reset each time the actor starts + vars: { lastAccessedAt: 0 }, + actions: { - // Define action to update state increment: (c) => { - // Update state, this will automatically be persisted - c.state.count += 1; - return c.state.count; + // Read and write the ephemeral var + c.vars.lastAccessedAt = Date.now(); + return ++c.state.count; }, - add: (c, value: number) => { - c.state.count += value; - return c.state.count; - } + getLastAccessed: (c) => c.vars.lastAccessedAt } }); ``` -Only state stored in the `state` object will be persisted. Any other variables or properties outside of this are not persisted. - -### State Saves - -Actors automatically handle persisting state transparently. This happens at the end of every action if the state has changed. State is also automatically saved after `onFetch` and `onWebSocket` handlers finish executing. - -For `onWebSocket` handlers specifically, you'll need to manually save state using `c.saveState()` while the WebSocket connection is open if you want state changes to be persisted immediately. This is because WebSocket connections can remain open for extended periods, and state changes made during event handlers (like `message` events) won't be automatically saved until the connection closes. - -In other cases where you need to force a state change mid-action, you can use `c.saveState()`. This should only be used if your action makes an important state change that needs to be persisted before the action completes. - -#### Immediate vs Throttled Saves - -`c.saveState()` supports two modes: - -- **`c.saveState({ immediate: true })`** saves state to storage right away. `await` resolves once the write completes. Use this when you need to guarantee persistence before continuing (e.g. before a risky async operation). -- **`c.saveState()`** (without `immediate: true`) schedules a throttled save. `await` will not resolve until the next flush cycle, which can take up to `stateSaveInterval` (default: 10 seconds). This batches rapid state changes to reduce write frequency, but means the caller blocks until the flush fires. - -If you want to save state promptly during a WebSocket message handler, use `immediate: true`. - -```typescript +```typescript Dynamic init import { actor } from "rivetkit"; -// Mock risky operation -async function someRiskyOperation() { - await new Promise(resolve => setTimeout(resolve, 1000)); -} +const chatRoom = actor({ + state: { messages: [] as string[] }, -const criticalProcess = actor({ - state: { - steps: [] as string[], - currentStep: 0 - }, + // Build a non-serializable emitter on each start + createVars: () => ({ emitter: createEventEmitter() }), actions: { - processStep: async (c) => { - // Update to current step - c.state.currentStep += 1; - c.state.steps.push(`Started step ${c.state.currentStep}`); - - // Force save state before the async operation - await c.saveState({ immediate: true }); - - // Long-running operation that might fail - await someRiskyOperation(); - - // Update state again - c.state.steps.push(`Completed step ${c.state.currentStep}`); - - return c.state.currentStep; + broadcast: (c, text: string) => { + c.state.messages.push(text); + // Use the ephemeral emitter + c.vars.emitter.emit("message", text); } } }); -``` - -### State Isolation - -Each actor's state is completely isolated, meaning it cannot be accessed directly by other actors or clients. - -To interact with an actor's state, you must use [Actions](/docs/actors/actions). Actions provide a controlled way to read from and write to the state. - -If you need a shared state between multiple actors, see [sharing and joining state](/docs/actors/sharing-and-joining-state). - -### Type Limitations - -State is currently constrained to the following types: - -- `null` -- `undefined` -- `boolean` -- `string` -- `number` -- `BigInt` -- `Date` -- `RegExp` -- `Error` -- Typed arrays (`Uint8Array`, `Int8Array`, `Float32Array`, etc.) -- `Map` -- `Set` -- `Array` -- Plain objects - -## SQLite - -For data that is large, relational, queryable, or larger than memory, use the embedded SQLite database available on `c.db`. - -Each actor instance has its own SQLite database, scoped to that actor. Because Rivet Actors keep compute and storage together, queries avoid network round trips to an external database. SQLite stores data on disk, so you can work with datasets that do not fit in actor memory, and you get a full relational engine with tables, indexes, `JOIN`s, constraints, and transactions. - -For complete documentation, see: - -- [SQLite](/docs/actors/sqlite) — raw SQL queries against the embedded per-actor database. -- [SQLite + Drizzle](/docs/actors/sqlite-drizzle) — typed schema and query APIs with the Drizzle ORM. - -## Ephemeral Variables - -In addition to persisted state, actors can store ephemeral data that is not saved to permanent storage using `vars`. This is useful for temporary data that only needs to exist while the actor is running, non-serializable objects like database connections or event emitters, and loading initial data from an external database. - -`vars` is designed to complement `state`, not replace it. Most actors that need it will use both: `state` for critical business data and `vars` for ephemeral or non-serializable data. - -### Initializing Variables - -There are two ways to define an actor's initial vars: - - - - - -Define an actor vars as a constant value: - -```typescript -import { actor } from "rivetkit"; // Mock event emitter for demonstration interface EventEmitter { @@ -266,154 +144,215 @@ function createEventEmitter(): EventEmitter { } }; } +``` -// Define vars as a constant -const counter = actor({ - state: { count: 0 }, +```typescript @nocheck External database +import { actor } from "rivetkit"; +import { Pool } from "pg"; - // Define ephemeral variables - vars: { - lastAccessTime: 0, - emitter: createEventEmitter() +const userActor = actor({ + state: { profile: null as Record | null }, + + // Open a connection and load initial data on every start + createVars: async (c) => { + const pool = new Pool({ connectionString: process.env.DATABASE_URL }); + const result = await pool.query("SELECT * FROM users WHERE id = $1", [c.key[0]]); + return { pool, profile: result.rows[0] }; }, actions: { - increment: (c) => ++c.state.count + updateEmail: async (c, email: string) => { + await c.vars.pool.query("UPDATE users SET email = $1 WHERE id = $2", [email, c.key[0]]); + } } }); ``` -This value will be cloned for every new actor using `structuredClone`. + - +### SQLite - +Rivet also provides an embedded SQLite database (`c.db`) for when your data needs to be queried, requires safe schema migrations, or grows too large to hold in memory. See [SQLite](/docs/actors/sqlite). -Create actor vars dynamically on each actors' start. Unlike `createState`, which runs only once when the actor is first created, `createVars` runs every time the actor starts. This makes it the right place to open a database client or connection and load initial data from an external source: + -```typescript +```typescript @nocheck Basic import { actor } from "rivetkit"; +import { db } from "rivetkit/db"; + +const todoList = actor({ + db: db({ + onMigrate: async (db) => { + await db.execute(` + CREATE TABLE IF NOT EXISTS todos ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL + ); + `); + }, + }), -// Mock event emitter for demonstration -interface EventEmitter { - on: (event: string, callback: (data: unknown) => void) => void; - emit: (event: string, data: unknown) => void; -} + actions: { + add: async (c, title: string) => { + await c.db.execute("INSERT INTO todos (title) VALUES (?)", title); + }, -function createEventEmitter(): EventEmitter { - const listeners: Record void)[]> = {}; - return { - on: (event, callback) => { - listeners[event] = listeners[event] || []; - listeners[event].push(callback); + list: async (c) => { + return (await c.db.execute( + "SELECT id, title FROM todos ORDER BY id DESC", + )) as { id: number; title: string }[]; }, - emit: (event, data) => { - listeners[event]?.forEach(cb => cb(data)); - } - }; -} + }, +}); +``` + +```typescript @nocheck Load into memory +import { actor } from "rivetkit"; +import { db } from "rivetkit/db"; -// Define vars with initialization logic const counter = actor({ - state: { count: 0 }, + db: db({ + onMigrate: async (db) => { + await db.execute(` + CREATE TABLE IF NOT EXISTS counter ( + id INTEGER PRIMARY KEY CHECK (id = 1), + count INTEGER NOT NULL + ); + `); + await db.execute("INSERT OR IGNORE INTO counter (id, count) VALUES (1, 0)"); + }, + }), - // Define vars using a creation function - createVars: () => { - return { - lastAccessTime: Date.now(), - emitter: createEventEmitter() - }; + // Load the count from SQLite into memory on every start + createVars: async (c) => { + const rows = (await c.db.execute( + "SELECT count FROM counter WHERE id = 1", + )) as { count: number }[]; + return { count: rows[0].count }; }, actions: { - increment: (c) => ++c.state.count - } + get: (c) => c.vars.count, + + increment: async (c) => { + // Update the in-memory value and write it back to SQLite + c.vars.count += 1; + await c.db.execute("UPDATE counter SET count = ? WHERE id = 1", c.vars.count); + return c.vars.count; + }, + }, }); ``` - -If accepting arguments to `createVars`, you **must** define the types: `createVars(c: CreateVarsContext, driver: any)` + + +## State Isolation + +Each actor's state is fully isolated. Other actors and clients can't touch it directly; all reads and writes go through the actor's own [Actions](/docs/actors/actions). To share state across actors, see [sharing and joining state](/docs/actors/sharing-and-joining-state). + +## Durable State + +`c.state` lives in memory and is persisted automatically, so reads and writes have no added latency while the data still survives sleeps, restarts, upgrades, and crashes. Use it for small, simple values like counters, flags, and small maps. + +`createState` runs once when the actor is first created. On later starts, state is loaded from storage instead of recreated. See [Lifecycle](/docs/actors/lifecycle). -Otherwise, the return type will not be inferred and `c.vars` will be of type `unknown`. - +### When state saves - +Mutating `c.state` schedules a save automatically. Rapid mutations are batched into a single write on a throttle (`stateSaveInterval`, default 1 second). Reads never trigger a save, saves aren't tied to action or handler boundaries, and state is also flushed when the actor sleeps or shuts down. - +To force a save mid-action, call `c.saveState()`: -### Using Variables +- `c.saveState({ immediate: true })` writes immediately and resolves once the write completes. +- `c.saveState()` schedules a throttled save and returns right away, without waiting for the write. -Vars can be accessed and modified through the context object with `c.vars`: +Force an immediate save before a risky side effect so a crash can't lose progress: ```typescript import { actor } from "rivetkit"; -// Mock event emitter for demonstration -interface EventEmitter { - on: (event: string, callback: (data: number) => void) => void; - emit: (event: string, data: number) => void; -} +const checkout = actor({ + state: { status: "pending" as "pending" | "charged" | "fulfilled" }, -function createEventEmitter(): EventEmitter { - const listeners: Record void)[]> = {}; - return { - on: (event, callback) => { - listeners[event] = listeners[event] || []; - listeners[event].push(callback); - }, - emit: (event, data) => { - listeners[event]?.forEach(cb => cb(data)); + actions: { + fulfill: async (c) => { + c.state.status = "charged"; + // Persist before the side effect so a crash can't undo it + await c.saveState({ immediate: true }); + + await chargeExternalProvider(); + + c.state.status = "fulfilled"; + return c.state.status; } - }; + } +}); + +async function chargeExternalProvider() { + await new Promise((resolve) => setTimeout(resolve, 100)); } +``` -const counter = actor({ - // Persistent state - saved to storage - state: { count: 0 }, +### Supported types - // Create ephemeral objects that won't be serialized - createVars: () => { - // Create an event emitter (can't be serialized) - const emitter = createEventEmitter(); +State must be serializable. - // Set up event listener directly in createVars - emitter.on('count-changed', (newCount) => { - console.log(`Count changed to: ${newCount}`); - }); + + - return { emitter }; - }, +- `null`, `undefined`, `boolean`, `string`, `number`, `BigInt` +- `Date`, `RegExp`, `Error` +- Typed arrays (`Uint8Array`, `Int8Array`, `Float32Array`, etc.) +- `Map`, `Set`, `Array` +- Plain objects - actions: { - increment: (c) => { - // Update persistent state - c.state.count += 1; + + - // Use non-serializable emitter - c.vars.emitter.emit('count-changed', c.state.count); +When data grows large or needs querying, store it in [Embedded SQLite](#embedded-sqlite) instead. - return c.state.count; +## Ephemeral State + +`c.vars` holds data that exists only while the actor runs and is never saved. Use it for live objects that can't be serialized (connections, clients, emitters) or for data loaded from an external source. Most actors use both: `state` for durable data, `vars` for live objects. + +`createVars` runs on every actor start, unlike `createState` which runs once. That makes it the place to open connections and load data each time the actor wakes. + +### Runtime objects + +Build non-serializable objects in `createVars` and use them from actions: + +```typescript +import { actor } from "rivetkit"; + +const room = actor({ + state: { messages: [] as string[] }, + + // EventTarget can't be serialized, so it lives in vars + createVars: () => ({ events: new EventTarget() }), + + actions: { + send: (c, text: string) => { + c.state.messages.push(text); + c.vars.events.dispatchEvent(new CustomEvent("message", { detail: text })); } } }); ``` -### Connecting to External Databases +### Loading from external sources -Because `createVars` runs on every actor start, it's the natural place to open a connection to an external database such as Postgres or Redis and load any data your actor needs. The connection lives only in memory and is never serialized: +`createVars` can be `async`, so open a connection and load initial data on each start. The connection lives only in memory: ```typescript @nocheck import { actor } from "rivetkit"; import { Pool } from "pg"; -const userActor = actor({ - state: { profile: null as Record | null }, +const profile = actor({ + state: { cachedName: "" }, - // Open a connection to the external database and load initial data on every start createVars: async (c) => { const pool = new Pool({ connectionString: process.env.DATABASE_URL }); - const result = await pool.query("SELECT * FROM users WHERE id = $1", [c.key[0]]); - return { pool, profile: result.rows[0] }; + const { rows } = await pool.query("SELECT * FROM users WHERE id = $1", [c.key[0]]); + return { pool, user: rows[0] }; }, actions: { @@ -424,22 +363,74 @@ const userActor = actor({ }); ``` -Use this pattern when your source of truth lives in an external database. For data owned entirely by the actor, prefer [in-memory state](#in-memory-state) or [SQLite](#sqlite), which require no external infrastructure. +When the actor owns its data, prefer [durable state](#durable-state) or [SQLite](#embedded-sqlite), which need no external infrastructure. + +### Cleanup + +`vars` is dropped when the actor stops, but external resources are not closed for you. Release them in `onSleep` and `onDestroy`: + +```typescript @nocheck +const profile = actor({ + createVars: () => ({ pool: new Pool() }), + + // Close the connection before the actor sleeps or is destroyed + onSleep: (c) => c.vars.pool.end(), + onDestroy: (c) => c.vars.pool.end(), + + actions: { /* ... */ } +}); +``` -### When to Use `vars` vs `state` +## Embedded SQLite -In practice, most actors that need both will use them together: `state` for critical business data and `vars` for ephemeral or non-serializable data. +`c.db` is a SQLite database scoped to each actor and stored on disk. Use it for queryable, relational, or larger-than-memory data. Because compute and storage live together, queries run locally with no network round trips. -Use `vars` when: +A common pattern is to treat SQLite as the source of truth and keep a working copy in `c.vars`: load rows in `createVars`, serve reads from memory, and write changes back to `c.db`. -- You need to store temporary data that doesn't need to survive restarts. -- You need to maintain runtime-only references that can't be serialized (database connections, event emitters, class instances, etc.). -- You need to load data from or write through to an external database. +```typescript @nocheck +import { actor } from "rivetkit"; +import { db } from "rivetkit/db"; + +const leaderboard = actor({ + db: db({ + onMigrate: async (db) => { + await db.execute(` + CREATE TABLE IF NOT EXISTS scores ( + player TEXT PRIMARY KEY, + score INTEGER NOT NULL + ); + `); + }, + }), + + // Load the table into memory once per start + createVars: async (c) => { + const rows = (await c.db.execute("SELECT player, score FROM scores")) as { + player: string; + score: number; + }[]; + return { scores: new Map(rows.map((r) => [r.player, r.score])) }; + }, + + actions: { + top: (c) => [...c.vars.scores].sort((a, b) => b[1] - a[1]).slice(0, 10), + + record: async (c, player: string, score: number) => { + c.vars.scores.set(player, score); + // Write through to SQLite + await c.db.execute( + "INSERT INTO scores (player, score) VALUES (?, ?) ON CONFLICT(player) DO UPDATE SET score = ?", + player, score, score, + ); + }, + }, +}); +``` -Use `state` when: +For the full query API, schema migrations, transactions, and the Drizzle ORM, see: -- The data must be preserved across actor sleeps, restarts, updates, or crashes. -- The information is essential to the actor's core functionality and business logic. +- [SQLite](/docs/actors/sqlite): raw SQL against the embedded per-actor database. +- [SQLite + Drizzle](/docs/actors/sqlite-drizzle): typed schema and query APIs. ## Debugging diff --git a/website/src/sitemap/mod.ts b/website/src/sitemap/mod.ts index e64e4a7810..1eecc53763 100644 --- a/website/src/sitemap/mod.ts +++ b/website/src/sitemap/mod.ts @@ -206,17 +206,6 @@ export const sitemap = [ // }, ] }, - { - title: "Extensions", - pages: [ - { - title: "Sandbox Actor", - href: "/docs/actors/sandbox", - icon: faSquareTerminal, - badge: "Beta", - }, - ] - }, { title: "Concepts", pages: [