diff --git a/website/src/content/docs/actors/access-control.mdx b/website/src/content/docs/actors/access-control.mdx index 25ad89d72e..6e272dacb1 100644 --- a/website/src/content/docs/actors/access-control.mdx +++ b/website/src/content/docs/actors/access-control.mdx @@ -126,6 +126,6 @@ Returning `undefined`, `null`, or any non-boolean throws an internal error. ## Notes - `canPublish` only applies to queue names defined in `queues`. -- Incoming queue messages for undefined queues are ignored and logged as warnings. +- Incoming queue messages for undefined queues are ignored and the publish succeeds as completed. - `canSubscribe` only applies to event names defined in `events`. -- Broadcasting an event not defined in `events` logs a warning but still publishes. +- Broadcasting an event not defined in `events` still publishes to subscribers. diff --git a/website/src/content/docs/actors/actions.mdx b/website/src/content/docs/actors/actions.mdx index 0fad4251c8..f53d2529b9 100644 --- a/website/src/content/docs/actors/actions.mdx +++ b/website/src/content/docs/actors/actions.mdx @@ -388,7 +388,6 @@ See [types](/docs/actors/types) for more details on using `ActionContextOf` and - `GET /inspector/rpcs` lists all available actions on an actor. - `POST /inspector/action/:name` executes an action with JSON args and returns output. -- `GET /inspector/traces` helps inspect action timings and failures. - In non-dev mode, inspector endpoints require authorization. ## API Reference diff --git a/website/src/content/docs/actors/ai-and-user-generated-actors.mdx b/website/src/content/docs/actors/ai-and-user-generated-actors.mdx deleted file mode 100644 index e0385745b2..0000000000 --- a/website/src/content/docs/actors/ai-and-user-generated-actors.mdx +++ /dev/null @@ -1,301 +0,0 @@ ---- -title: "AI and User-Generated Rivet Actors" -description: "This guide shows you how to programmatically create sandboxed Rivet environments and deploy custom actor code to them." -skill: true ---- - -import { faGithub } from "@rivet-gg/icons"; - - - - - -Complete example showing how to deploy user-generated Rivet Actor code. - - - -## Use Cases - -Deploying AI and user-generated Rivet Actors to sandboxed namespaces is useful for: - -- **AI-generated code deployments**: Deploy code generated by LLMs in sandboxed environments -- **User sandbox environments**: Give users their own sandboxed Rivet namespace to experiment -- **Preview deployments**: Create ephemeral environments for testing pull requests -- **Multi-tenant applications**: Isolate each customer in their own sandboxed namespace - -## Rivet Actors For AI-Generated Backends - -Traditional architectures require AI agents to coordinate across multiple disconnected systems: a database schemas, API logic, and synchronizing schemas & APIs. - -With Rivet Actors, **state and logic live together in a single actor definition**. This consolidation means: - -- **Less LLM context required**: No need to understand multiple systems or keep them in sync -- **Fewer errors**: State and behavior can't drift apart when they're defined together -- **More powerful generation**: AI agents can focus on business logic instead of infrastructure plumbing - -## How It Works - -The deployment process involves four key steps: - -1. **Create sandboxed Rivet namespace**: Programmatically create a sandboxed Rivet namespace using the Cloud API or self-hosted Rivet API -2. **Generate tokens**: Create the necessary tokens for authentication: - - **Runner token**: Authenticates the serverless runner to execute actors - - **Publishable token**: Used by frontend clients to connect to actors - - **Access token**: Provides API access for configuring the namespace -3. **Deploy AI or user-generated code**: Deploy the actor code and frontend programmatically to your serverless platform of choice (such as Vercel, Netlify, AWS Lambda, or any other provider). We'll be using [Freestyle](https://freestyle.sh) for this example since it's built for this use case. -4. **Connect Rivet to your deployed code**: Configure Rivet to run actors on your deployment in your sandboxed namespace - -## Setup - - - - - - Before you begin, ensure you have: - - Node.js 18+ installed - - A [Freestyle](https://freestyle.sh) account and API token - - A [Rivet Cloud](https://dashboard.rivet.dev/) account - - - - 1. Visit your project on [Rivet Cloud](https://dashboard.rivet.dev/) - 2. Click on "Tokens" in the sidebar - 3. Under "Cloud API Tokens" click "Create Token" - 4. Copy the token for use in your deployment script - - - - Install the required dependencies: - - ```bash - npm install @rivetkit/engine-api-full@^25.7.2 freestyle-sandboxes@^0.0.95 - ``` - - - - Write deployment code that handles namespace creation, token generation, Freestyle deployment, and runner configuration. This can be called from your backend to deploy actor and frontend code to an isolated Rivet namespace. - - ```typescript - import { execSync } from "child_process"; - import { RivetClient } from "@rivetkit/engine-api-full"; - import { FreestyleSandboxes } from "freestyle-sandboxes"; - import { prepareDirForDeploymentSync } from "freestyle-sandboxes/utils"; - - const CLOUD_API_TOKEN = "your-cloud-api-token"; - const FREESTYLE_DOMAIN = "your-app.style.dev"; - const FREESTYLE_API_KEY = "your-freestyle-api-key"; - - async function deploy(projectDir: string) { - // Step 1: Inspect API token to get project and organization - const { project, organization } = await cloudRequest("GET", "/tokens/api/inspect"); - - // Step 2: Create sandboxed namespace with a unique name - const namespaceName = `ns-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`; - - const { namespace } = await cloudRequest( - "POST", - `/projects/${project}/namespaces?org=${organization}`, - { displayName: namespaceName.substring(0, 16) }, - ); - const engineNamespaceName = namespace.access.engineNamespaceName; // NOTE: Intentionally different than namespace.name - - // Step 3: Generate tokens - // - Runner token: authenticates the serverless runner to execute actors - // - Publishable token: used by frontend clients to connect to actors - // - Access token: provides API access for configuring the namespace - const { token: runnerToken } = await cloudRequest( - "POST", - `/projects/${project}/namespaces/${namespace.name}/tokens/secret?org=${organization}`, - ); - - const { token: publishableToken } = await cloudRequest( - "POST", - `/projects/${project}/namespaces/${namespace.name}/tokens/publishable?org=${organization}`, - ); - - const { token: accessToken } = await cloudRequest( - "POST", - `/projects/${project}/namespaces/${namespace.name}/tokens/access?org=${organization}`, - ); - - // Step 4: Build the frontend with public environment variables. - execSync("npm run build", { - cwd: projectDir, - env: { - ...process.env, - VITE_RIVET_ENDPOINT: "https://api.rivet.dev", - VITE_RIVET_NAMESPACE: engineNamespaceName, - VITE_RIVET_TOKEN: publishableToken, - }, - stdio: "inherit", - }); - - // Step 5: Deploy actor code and frontend to Freestyle with backend - // environment variables. - const freestyle = new FreestyleSandboxes({ apiKey: FREESTYLE_API_KEY }); - const deploymentSource = prepareDirForDeploymentSync(projectDir); - - const { deploymentId } = await freestyle.deployWeb(deploymentSource, { - envVars: { - RIVET_ENDPOINT: "https://api.rivet.dev", - RIVET_NAMESPACE: engineNamespaceName, - RIVET_TOKEN: runnerToken, - }, - entrypoint: "src/backend/server.ts", - domains: [FREESTYLE_DOMAIN], - build: false, - }); - - // Step 6: Configure Rivet to run actors on the Freestyle deployment. - const rivet = new RivetClient({ - environment: "https://api.rivet.dev", - token: accessToken, - }); - - await rivet.runnerConfigsUpsert("default", { - datacenters: { - "us-west-1": { // Freestyle datacenter is on west coast - serverless: { - url: `https://${FREESTYLE_DOMAIN}/api/rivet`, - headers: {}, - runnersMargin: 0, - minRunners: 0, - maxRunners: 1000, - slotsPerRunner: 1, - requestLifespan: 60 * 5, - }, - }, - }, - namespace: engineNamespaceName, - }); - - console.log("Deployment complete!"); - console.log("Frontend:", `https://${FREESTYLE_DOMAIN}`); - console.log("Rivet Dashboard:", `https://dashboard.rivet.dev/orgs/${organization}/projects/${project}/ns/${namespace.name}`); - console.log("Freestyle Dashboard:", `https://admin.freestyle.sh/dashboard/deployments/${deploymentId}`); - } - - async function cloudRequest(method: string, path: string, body?: any) { - const res = await fetch(`https://api-cloud.rivet.dev${path}`, { - method, - headers: { - Authorization: `Bearer ${CLOUD_API_TOKEN}`, - ...(body && { "Content-Type": "application/json" }), - }, - ...(body && { body: JSON.stringify(body) }), - }); - return res.json(); - } - ``` - - See the [example repository](https://github.com/rivet-dev/rivet/tree/main/examples/ai-and-user-generated-actors-freestyle) for the complete project structure including the template directory and build process. - - For more information on Freestyle deployment, see the [Freestyle documentation](https://docs.freestyle.sh/web/overview). - - - - - - - - Before you begin, ensure you have: - - Node.js 18+ installed - - A [Freestyle](https://freestyle.sh) account and API key - - A [self-hosted Rivet instance](/docs/self-hosting) with endpoint and API token - - - - Install the required dependencies: - - ```bash - npm install @rivetkit/engine-api-full@^25.7.2 freestyle-sandboxes@^0.0.95 - ``` - - - - Write deployment code that handles namespace creation, Freestyle deployment, and runner configuration. This can be called from your backend to deploy actor and frontend code to an isolated Rivet namespace. - - ```typescript - import { execSync } from "child_process"; - import { RivetClient } from "@rivetkit/engine-api-full"; - import { FreestyleSandboxes } from "freestyle-sandboxes"; - import { prepareDirForDeploymentSync } from "freestyle-sandboxes/utils"; - - // Configuration - const RIVET_ENDPOINT = "http://your-rivet-instance:6420"; - const RIVET_TOKEN = "your-rivet-token"; - const FREESTYLE_DOMAIN = "your-app.style.dev"; - const FREESTYLE_API_KEY = "your-freestyle-api-key"; - - async function deploy(projectDir: string) { - // Step 1: Create sandboxed namespace using the self-hosted Rivet API - const rivet = new RivetClient({ - environment: RIVET_ENDPOINT, - token: RIVET_TOKEN, - }); - - const namespaceName = `ns-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`; - - const { namespace } = await rivet.namespaces.create({ - displayName: namespaceName, - name: namespaceName, - }); - - // Step 2: Build the frontend with public environment variables. - execSync("npm run build", { - cwd: projectDir, - env: { - ...process.env, - VITE_RIVET_ENDPOINT: RIVET_ENDPOINT, - VITE_RIVET_NAMESPACE: namespace.name, - VITE_RIVET_TOKEN: RIVET_TOKEN, - }, - stdio: "inherit", - }); - - // Step 3: Deploy actor and frontend to Freestyle with backend - // environment variables. - const freestyle = new FreestyleSandboxes({ apiKey: FREESTYLE_API_KEY }); - const deploymentSource = prepareDirForDeploymentSync(projectDir); - - const { deploymentId } = await freestyle.deployWeb(deploymentSource, { - envVars: { - RIVET_ENDPOINT, - RIVET_NAMESPACE: namespace.name, - RIVET_TOKEN, - }, - entrypoint: "src/backend/server.ts", - domains: [FREESTYLE_DOMAIN], - build: false, - }); - - // Step 4: Configure your self-hosted Rivet to run actors on the Freestyle - // deployment - await rivet.runnerConfigsUpsert("default", { - datacenters: { - "us-west-1": { // Freestyle datacenter is on west coast - serverless: { - url: `https://${FREESTYLE_DOMAIN}/api/rivet`, - headers: {}, - runnersMargin: 0, - minRunners: 0, - maxRunners: 1000, - slotsPerRunner: 1, - requestLifespan: 60 * 5, - }, - }, - }, - namespace: namespace.name, - }); - - console.log("Deployment complete!"); - console.log("Frontend:", `https://${FREESTYLE_DOMAIN}`); - console.log("Freestyle Dashboard:", `https://admin.freestyle.sh/dashboard/deployments/${deploymentId}`); - } - ``` - - See the [example repository](https://github.com/rivet-dev/rivet/tree/main/examples/ai-and-user-generated-actors-freestyle) for the complete project structure including the template directory and build process. - - - - diff --git a/website/src/content/docs/actors/appearance.mdx b/website/src/content/docs/actors/appearance.mdx index 98cae507bf..e6bf8089ea 100644 --- a/website/src/content/docs/actors/appearance.mdx +++ b/website/src/content/docs/actors/appearance.mdx @@ -151,10 +151,12 @@ const myCustomRunHandler = (_options: Record) => ({ const myActor = actor({ run: myCustomRunHandler({ /* options */ }), - // Automatically gets "My Custom Handler" name and "bolt" icon + // Picks up "My Custom Handler" name and "bolt" icon in registry metadata }); ``` +This run-handler metadata is currently applied through the registry and serverless metadata paths. The native runtime and inspector config read the actor's `options.name` and `options.icon` directly, so set those explicitly if you need the name or icon to appear everywhere. + Actor-level `options.name` and `options.icon` always take precedence, allowing users to override library defaults: ```typescript diff --git a/website/src/content/docs/actors/authentication.mdx b/website/src/content/docs/actors/authentication.mdx index 4c1b41a153..994c18a18e 100644 --- a/website/src/content/docs/actors/authentication.mdx +++ b/website/src/content/docs/actors/authentication.mdx @@ -235,7 +235,7 @@ function showError(message: string) { } const conn = actorHandle.connect(); -conn.on("error", (error: ActorError) => { +conn.onError((error: ActorError) => { if (error.code === "forbidden") { window.location.href = "/login"; } else if (error.code === "insufficient_permissions") { @@ -596,5 +596,5 @@ const cachedAuthActor = actor({ ## API Reference - [`AuthIntent`](/typedoc/types/rivetkit.mod.AuthIntent.html) - Authentication intent type -- [`BeforeConnectContext`](/typedoc/interfaces/rivetkit.mod.BeforeConnectContext.html) - Context for auth checks -- [`ConnectContext`](/typedoc/interfaces/rivetkit.mod.ConnectContext.html) - Context after connection +- [`OnBeforeConnectContext`](/typedoc/interfaces/rivetkit.mod.OnBeforeConnectContext.html) - Context for auth checks +- [`OnConnectContext`](/typedoc/interfaces/rivetkit.mod.OnConnectContext.html) - Context after connection diff --git a/website/src/content/docs/actors/connections.mdx b/website/src/content/docs/actors/connections.mdx index 40149acce6..0159f3a3f6 100644 --- a/website/src/content/docs/actors/connections.mdx +++ b/website/src/content/docs/actors/connections.mdx @@ -201,7 +201,7 @@ Connections are not visible in `c.conns` until `createConnState` completes succe ### `onBeforeConnect` -[API Reference](/typedoc/interfaces/rivetkit.mod.BeforeConnectContext.html) +[API Reference](/typedoc/interfaces/rivetkit.mod.OnBeforeConnectContext.html) The `onBeforeConnect` hook is called whenever a new client connects to the actor. Can be async. Clients can pass parameters when connecting, accessible via `params`. This hook is used for connection validation and can throw errors to reject connections. @@ -265,7 +265,7 @@ Connections cannot interact with the actor until this method completes successfu ### `onConnect` -[API Reference](/typedoc/interfaces/rivetkit.mod.ConnectContext.html) +[API Reference](/typedoc/interfaces/rivetkit.mod.OnConnectContext.html) Executed after the client has successfully connected. Can be async. Receives the connection object as a second parameter. @@ -355,7 +355,7 @@ const chatRoom = actor({ ## Connection List -All active connections can be accessed through the context object's `conns` property. This is an array of all current connections. +All active connections can be accessed through the context object's `conns` property. This is a `Map` of all current connections, keyed by connection ID. This is frequently used with `conn.send(name, event)` to send messages directly to clients. To send an event to all connections at once, use `c.broadcast()` instead. See [Events](/docs/actors/events) for more details on broadcasting. @@ -457,6 +457,6 @@ This ensures the underlying network connections close cleanly before continuing. - [`Conn`](/typedoc/interfaces/rivetkit.mod.Conn.html) - Connection interface - [`ConnInitContext`](/typedoc/interfaces/rivetkit.mod.ConnInitContext.html) - Connection initialization context - [`CreateConnStateContext`](/typedoc/interfaces/rivetkit.mod.CreateConnStateContext.html) - Context for creating connection state -- [`BeforeConnectContext`](/typedoc/interfaces/rivetkit.mod.BeforeConnectContext.html) - Pre-connection lifecycle hook context -- [`ConnectContext`](/typedoc/interfaces/rivetkit.mod.ConnectContext.html) - Post-connection lifecycle hook context +- [`OnBeforeConnectContext`](/typedoc/interfaces/rivetkit.mod.OnBeforeConnectContext.html) - Pre-connection lifecycle hook context +- [`OnConnectContext`](/typedoc/interfaces/rivetkit.mod.OnConnectContext.html) - Post-connection lifecycle hook context - [`ActorConn`](/typedoc/types/rivetkit.client_mod.ActorConn.html) - Typed connection from client side diff --git a/website/src/content/docs/actors/debugging.mdx b/website/src/content/docs/actors/debugging.mdx index 08a549ab4d..141baafb2b 100644 --- a/website/src/content/docs/actors/debugging.mdx +++ b/website/src/content/docs/actors/debugging.mdx @@ -34,13 +34,13 @@ The management API runs on the manager base path (default root path) and is used |---|---| | **Local development** | No authentication required. All endpoints are accessible without tokens. | | **Self-hosted engine** | Set `RIVET_TOKEN` to enable authenticated access to restricted endpoints like KV. | -| **Rivet Cloud** | Authentication is enforced by your deployment entrypoint. For manager KV access, use the manager token header below when enabled. | +| **Rivet Cloud** | Authentication is enforced by your deployment entrypoint. For manager KV access, use the bearer token header below when enabled. | -Restricted endpoints (like KV reads) require the `x-rivet-token` header when `RIVET_TOKEN` is configured: +Restricted endpoints (like KV reads) require the `Authorization: Bearer` header when `RIVET_TOKEN` is configured: ```bash curl "$RIVET_API/actors/{actor_id}/kv/keys/{base64_key}" \ - -H "x-rivet-token: $RIVET_TOKEN" + -H "Authorization: Bearer $RIVET_TOKEN" ``` ### List Actors @@ -128,7 +128,7 @@ Requires authentication (see above). ```bash curl "$RIVET_API/actors/{actor_id}/kv/keys/{base64_key}" \ - -H "x-rivet-token: $RIVET_TOKEN" + -H "Authorization: Bearer $RIVET_TOKEN" ``` Returns the value stored at the given key. @@ -271,9 +271,11 @@ Standard actor endpoints (health, actions, requests) and inspector endpoints hav Each actor generates a unique inspector token on first start and persists it in its internal KV store at key `0x03` (base64 `Aw==`). Pass it as a bearer token in the `Authorization` header. +Inspector endpoints always require the actor's inspector token, including in local development. There is no local-development bypass. + | Environment | Authentication | |---|---| -| **Local development** | No authentication required. | +| **Local development** | Bearer the actor's inspector token in the `Authorization` header. Fetch it through the management KV endpoint (see below). | | **Self-hosted engine** | Bearer the actor's inspector token in the `Authorization` header. The Rivet dashboard fetches it automatically; for direct API access, fetch it through the management KV endpoint (see below). | | **Rivet Cloud** | Bearer the actor's inspector token in the `Authorization` header. The Rivet dashboard fetches it automatically; for direct API access, fetch it through the management KV endpoint (see below). | @@ -282,9 +284,9 @@ curl "$RIVET_API/gateway/{actor_id}/inspector/summary" \ -H 'Authorization: Bearer YOUR_INSPECTOR_TOKEN' ``` -#### Retrieving the Inspector Token (Rivet Cloud) +#### Retrieving the Inspector Token -In Rivet Cloud, each actor generates a unique inspector token on first start and persists it in its internal KV store. The Rivet dashboard retrieves this token automatically, but if you need it for direct API access, fetch it from the management KV endpoint. +Each actor generates a unique inspector token on first start and persists it in its internal KV store. The Rivet dashboard retrieves this token automatically, but if you need it for direct API access, fetch it from the management KV endpoint. This applies in every environment, including local development. The inspector token is stored at internal KV key `0x03` (base64: `Aw==`). The response value is also base64-encoded. @@ -293,7 +295,7 @@ The inspector token is stored at internal KV key `0x03` (base64: `Aw==`). The re ACTOR_ID="your-actor-id" RESPONSE=$(curl -s "$RIVET_API/actors/$ACTOR_ID/kv/keys/Aw==" \ - -H "x-rivet-token: $RIVET_TOKEN") + -H "Authorization: Bearer $RIVET_TOKEN") # Extract and decode the base64 value INSPECTOR_TOKEN=$(echo "$RESPONSE" | jq -r '.value' | base64 -d) @@ -319,11 +321,6 @@ curl -X POST $RIVET_API/gateway/{actor_id}/action/myAction \ -H 'Content-Type: application/json' \ -d '{"args": [1, 2, 3]}' -# Send queue message (body includes queue name) -curl -X POST $RIVET_API/gateway/{actor_id}/queue \ - -H 'Content-Type: application/json' \ - -d '{"name":"jobs","body":{"id":"job-1"}}' - # Send queue message (queue name in path) curl -X POST $RIVET_API/gateway/{actor_id}/queue/jobs \ -H 'Content-Type: application/json' \ @@ -338,10 +335,16 @@ curl -X POST $RIVET_API/gateway/{actor_id}/queue/jobs \ curl $RIVET_API/gateway/{actor_id}/request/my/custom/path ``` -Queue send responses include: +Queue send responses always include a `status` field: + +```json +{ "status": "completed" } +``` + +The `response` field is only present when the queue handler returns a value: ```json -{ "status": "completed", "response": null } +{ "status": "completed", "response": { "result": "ok" } } ``` If `wait: true` and the timeout is reached, `status` is `"timedOut"`. @@ -350,6 +353,8 @@ If `wait: true` and the timeout is reached, `status` is `"timedOut"`. The inspector HTTP API exposes JSON endpoints for querying and modifying actor internals at runtime. These are designed for agent-based debugging and tooling. +Every inspector endpoint requires the actor's inspector token as a bearer token, including in local development. The examples below omit the `Authorization` header for brevity, but you must add `-H "Authorization: Bearer $INSPECTOR_TOKEN"` to each request. See [Retrieving the Inspector Token](#retrieving-the-inspector-token) above. + #### Get State ```bash @@ -451,40 +456,6 @@ Returns queue status with messages: } ``` -#### Get Traces - -Query trace spans in OTLP JSON format: - -```bash -curl "$RIVET_API/gateway/{actor_id}/inspector/traces?startMs=0&endMs=9999999999999&limit=100" -``` - -Returns: - -```json -{ - "otlp": { - "resourceSpans": [ - { - "scopeSpans": [ - { - "spans": [ - { - "traceId": "abc123", - "spanId": "def456", - "name": "increment", - "startTimeUnixNano": "1706000000000000000" - } - ] - } - ] - } - ] - }, - "clamped": false -} -``` - #### Get Workflow History ```bash @@ -654,18 +625,7 @@ Returns: } ``` -When workflow history is present in `/inspector/summary`, `workflowHistory` is returned as the same encoded byte array used by `/inspector/workflow-history`. - -#### Get Metrics (Experimental) - -```bash -curl $RIVET_API/gateway/{actor_id}/inspector/metrics -``` - -Returns in-memory metrics for the current actor wake cycle. Metrics are not persisted and reset when the actor sleeps and wakes again. - -Includes counters for `action_calls`, `action_errors`, `action_duration_ms`, `connections_opened`, `connections_closed`, `sql_statements`, `sql_duration_ms`, and `kv_operations`. - +When workflow history is present in `/inspector/summary`, `workflowHistory` is returned as the same decoded JSON shape as `/inspector/workflow-history`. ### Polling @@ -673,7 +633,9 @@ Inspector endpoints are safe to poll. For live monitoring, poll at 1-5 second in ## OpenAPI Spec -The full OpenAPI specification including all management and actor endpoints is available: +An OpenAPI specification covering many of the management and actor endpoints is available: - In the repository at [`rivetkit-openapi/openapi.json`](https://github.com/rivet-dev/rivet/tree/main/rivetkit-openapi) - Served at `/doc` on the manager when running locally + +The checked-in spec does not yet list every endpoint documented on this page (for example the actor metadata and queue routes and the inspector database routes), so treat this page as the authoritative reference where they differ. diff --git a/website/src/content/docs/actors/design-patterns.mdx b/website/src/content/docs/actors/design-patterns.mdx index 9126394ac6..5daefa96f0 100644 --- a/website/src/content/docs/actors/design-patterns.mdx +++ b/website/src/content/docs/actors/design-patterns.mdx @@ -475,7 +475,7 @@ await session.updateEmail("alice@example.com"); ### Syncing State Changes -Use `onStateChange` to automatically sync actor state changes to external resources. This hook is called whenever the actor's state is modified. +Use `onStateChange` to automatically sync actor state changes to external resources. This hook runs after state changes are flushed, which is coalesced to once per event loop tick rather than once per individual field mutation. Use this when: @@ -576,7 +576,7 @@ const userData = await user.getUser(); -`onStateChange` is called after every state modification, ensuring external resources stay in sync. +`onStateChange` is called once per flush with the final coalesced state, ensuring external resources stay in sync. In the `updateEmail` example above, the two synchronous assignments produce a single `onStateChange` call. Do not mutate `c.state` inside `onStateChange`; re-entrant state mutation is rejected. @@ -618,7 +618,7 @@ const processor = actor({ state: {}, actions: { process: (c, body: unknown) => ({ processed: true }), - destroy: (c) => {}, + destroySelf: (c) => c.destroy(), }, }); @@ -630,7 +630,7 @@ const app = new Hono(); app.post("/process", async (c) => { const actorHandle = client.processor.getOrCreate([crypto.randomUUID()]); const result = await actorHandle.process(await c.req.json()); - await actorHandle.destroy(); + await actorHandle.destroySelf(); return c.json(result); }); ``` diff --git a/website/src/content/docs/actors/destroy.mdx b/website/src/content/docs/actors/destroy.mdx index 51fc845294..b883fece2f 100644 --- a/website/src/content/docs/actors/destroy.mdx +++ b/website/src/content/docs/actors/destroy.mdx @@ -115,7 +115,7 @@ const userActor = actor({ ## Accessing Actor After Destroy -Once an actor is destroyed, any subsequent requests to it will return an `actor_not_found` error. The actor's state is permanently deleted. +Once an actor is destroyed, any subsequent requests to it will fail with an `actor.not_found` error (`{ group: "actor", code: "not_found" }`). The actor's state is permanently deleted. ## API Reference diff --git a/website/src/content/docs/actors/errors.mdx b/website/src/content/docs/actors/errors.mdx index fa3629a7d1..6916a4fc30 100644 --- a/website/src/content/docs/actors/errors.mdx +++ b/website/src/content/docs/actors/errors.mdx @@ -358,7 +358,7 @@ try { } catch (error) { if (error instanceof ActorError) { console.log(error.code); // "internal_error" - console.log(error.message); // "Internal error. Read the server logs for more details." + console.log(error.message); // "An internal error occurred" // Original error details are NOT exposed to the client // Check your server logs to see the actual error message @@ -392,7 +392,7 @@ try { } catch (error) { if (error instanceof ActorError) { console.log(error.code); // "internal_error" - console.log(error.message); // "Internal error. Read the server logs for more details." + console.log(error.message); // "An internal error occurred" // Original error details are NOT exposed to the client // Check your server logs to see the actual error message @@ -420,10 +420,7 @@ The client receives only a generic "Internal error" message for security, but yo **Warning:** Only enable error exposure in development environments. In production, this will leak sensitive internal details to clients. -For faster debugging during development, you can automatically expose internal error details to clients. This is enabled when: - -- `NODE_ENV=development` - Automatically enabled in development mode -- `RIVET_EXPOSE_ERRORS=1` - Explicitly enable error exposure +For faster debugging during development, you can expose internal error details to clients by setting `RIVET_EXPOSE_ERRORS=1`. With error exposure enabled, clients will see the full error message instead of the generic "Internal error" response: @@ -440,14 +437,14 @@ const registry = setup({ use: { payment } }); const client = createClient("http://localhost:6420"); const paymentActor = client.payment.getOrCreate([]); -// With NODE_ENV=development or RIVET_EXPOSE_ERRORS=1 +// With RIVET_EXPOSE_ERRORS=1 try { await paymentActor.processPayment(100); } catch (error) { if (error instanceof ActorError) { console.log(error.message); // "Payment API returned 402: Insufficient funds" - // Instead of: "Internal error. Read the server logs for more details." + // Instead of: "An internal error occurred" } } ``` diff --git a/website/src/content/docs/actors/events.mdx b/website/src/content/docs/actors/events.mdx index 698ea63f72..79734fe52d 100644 --- a/website/src/content/docs/actors/events.mdx +++ b/website/src/content/docs/actors/events.mdx @@ -76,9 +76,7 @@ const gameRoom = actor({ }>() }, - connState: { playerId: "", role: "player" } as ConnState, - - createConnState: (c, params: { playerId: string, role?: string }) => ({ + createConnState: (c, params: { playerId: string, role?: string }): ConnState => ({ playerId: params.playerId, role: params.role || "player" }), @@ -130,9 +128,7 @@ const gameRoom = actor({ }>() }, - connState: { playerId: "", role: "player" } as ConnState, - - createConnState: (c, params: { playerId: string, role?: string }) => ({ + createConnState: (c, params: { playerId: string, role?: string }): ConnState => ({ playerId: params.playerId, role: params.role || "player" }), diff --git a/website/src/content/docs/actors/index.mdx b/website/src/content/docs/actors/index.mdx index 60dcff17cc..b39d539e17 100644 --- a/website/src/content/docs/actors/index.mdx +++ b/website/src/content/docs/actors/index.mdx @@ -113,7 +113,6 @@ interface CounterState { } const counter = actor({ - state: { count: 0 } as CounterState, createState: (c, input: { start?: number }): CounterState => ({ count: input.start ?? 0, }), @@ -273,7 +272,7 @@ const chatRoom = actor({ ### Connections -Access the current connection via `c.conn` or all connected clients via `c.conns`. Use `c.conn.id` or `c.conn.state` to securely identify who is calling an action. Connection state is initialized via `connState` or `createConnState`, which receives parameters passed by the client on connect. +Access the current connection via `c.conn` or all connected clients via `c.conns`. Use `c.conn.id` or `c.conn.state` to securely identify who is calling an action. `c.conn` is only available for actions invoked through a connected client; stateless actor-handle calls run without a connection, so guard against that. Connection state is initialized via `connState` or `createConnState`, which receives parameters passed by the client on connect. @@ -486,9 +485,6 @@ interface ConnState { } const chatRoom = actor({ - state: { users: {} } as RoomState, - vars: { startTime: 0 }, - connState: { userId: "", joinedAt: 0 } as ConnState, events: { stateChanged: event(), }, @@ -602,12 +598,22 @@ c.state.username = username; ```ts -import { actor, setup } from "rivetkit"; +import { actor, setup, UserError } from "rivetkit"; import { createClient, ActorError } from "rivetkit/client"; const user = actor({ state: { username: "" }, - actions: { updateUsername: (c, username: string) => { c.state.username = username; } } + actions: { + updateUsername: (c, username: string) => { + if (username.length < 3) { + throw new UserError("Username too short", { + code: "username_too_short", + metadata: { minLength: 3, actual: username.length }, + }); + } + c.state.username = username; + }, + }, }); const registry = setup({ use: { user } }); diff --git a/website/src/content/docs/actors/input.mdx b/website/src/content/docs/actors/input.mdx index 2f533b61df..5f31abc0ee 100644 --- a/website/src/content/docs/actors/input.mdx +++ b/website/src/content/docs/actors/input.mdx @@ -21,7 +21,6 @@ interface GameInput { } const game = actor({ - state: { gameMode: "", maxPlayers: 0, difficulty: "medium" }, createState: (c, input: GameInput) => ({ gameMode: input.gameMode, maxPlayers: input.maxPlayers, @@ -53,7 +52,7 @@ const gameHandle2 = client.game.getOrCreate(["game-456"], { ## Accessing Input in Lifecycle Hooks -Input is available in lifecycle hooks via the `opts.input` parameter: +Input is available as the second argument to the `createState` and `onCreate` lifecycle hooks: ```typescript import { actor } from "rivetkit"; @@ -78,7 +77,6 @@ function setupPrivateRoomLogging(roomName: string) { } const chatRoom = actor({ - state: { name: "", isPrivate: false, maxUsers: 50, users: {}, messages: [] } as ChatRoomState, createState: (c, input: ChatRoomInput): ChatRoomState => ({ name: input?.roomName ?? "Unnamed Room", isPrivate: input?.isPrivate ?? false, @@ -133,7 +131,6 @@ interface GameState { } const game = actor({ - state: { gameMode: "", maxPlayers: 0, difficulty: "medium", players: {}, gameState: "waiting" } as GameState, createState: (c, inputRaw: GameInput): GameState => { // Validate input const input = GameInputSchema.parse(inputRaw); @@ -179,9 +176,7 @@ import { createClient } from "rivetkit/client"; interface RoomInput { roomName: string; isPrivate: boolean; } const chatRoom = actor({ - state: { name: "", isPrivate: false }, createState: (c, input: RoomInput) => ({ name: input.roomName, isPrivate: input.isPrivate }), - connState: { userId: "", displayName: "" }, createConnState: (c, params: { userId: string; displayName: string }) => ({ userId: params.userId, displayName: params.displayName, @@ -223,7 +218,6 @@ interface GameState { } const game = actor({ - state: { gameMode: "", maxPlayers: 0, difficulty: "medium" } as GameState, createState: (c, input: GameInput): GameState => ({ gameMode: input.gameMode, maxPlayers: input.maxPlayers, @@ -262,11 +256,6 @@ interface GameState { } const game = actor({ - state: { - config: { gameMode: "", maxPlayers: 0, difficulty: "medium" }, - players: {}, - gameState: "waiting" - } as GameState, createState: (c, input: GameInput): GameState => ({ // Store input configuration in state config: { @@ -292,4 +281,4 @@ const game = actor({ - [`CreateOptions`](/typedoc/interfaces/rivetkit.client_mod.CreateOptions.html) - Options for creating actors - [`CreateRequest`](/typedoc/types/rivetkit.client_mod.CreateRequest.html) - Request type for creation -- [`ActorDefinition`](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) - Interface for defining input types +- [`ActorDefinition`](/typedoc/classes/rivetkit.mod.ActorDefinition.html) - Actor definition returned by `actor()` diff --git a/website/src/content/docs/actors/inspector-tabs.mdx b/website/src/content/docs/actors/inspector-tabs.mdx index 1c31a56533..59f0fbb853 100644 --- a/website/src/content/docs/actors/inspector-tabs.mdx +++ b/website/src/content/docs/actors/inspector-tabs.mdx @@ -165,12 +165,10 @@ arrives. } ``` -For tabs with sub-views, the dashboard also sends `set-active-tab` -when the user switches: - -```ts @nocheck -{ type: "set-active-tab", v: 1, tab: string } -``` +Multi-view tabs can read the optional `activeTab` field on `init` to +seed their initial sub-view. The dashboard does not send a separate +message when the user switches custom tabs — it navigates the iframe +`src` instead, so the tab reloads and receives a fresh `init`. ### From the tab diff --git a/website/src/content/docs/actors/keys.mdx b/website/src/content/docs/actors/keys.mdx index 481e226f5e..31f998946c 100644 --- a/website/src/content/docs/actors/keys.mdx +++ b/website/src/content/docs/actors/keys.mdx @@ -182,7 +182,6 @@ interface UserSessionState { } const userSession = actor({ - state: { userId: "", loginTime: 0, preferences: {} } as UserSessionState, createState: (c): UserSessionState => ({ userId: c.key[0], // Extract user ID from key loginTime: Date.now(), @@ -234,7 +233,6 @@ interface ChatRoomInput { } const chatRoom = actor({ - state: { maxUsers: 0, isPrivate: false, moderators: [] as string[], settings: { allowImages: true, slowMode: false } }, createState: (c, input: ChatRoomInput) => ({ maxUsers: input.maxUsers, isPrivate: input.isPrivate, diff --git a/website/src/content/docs/actors/kv.mdx b/website/src/content/docs/actors/kv.mdx index c342f05afa..ae4e555e1f 100644 --- a/website/src/content/docs/actors/kv.mdx +++ b/website/src/content/docs/actors/kv.mdx @@ -34,7 +34,7 @@ const greetings = actor({ ## Value Types -You can store binary values by passing `Uint8Array` or `ArrayBuffer` directly. Use `type` when reading to get the right return type. +You can store binary values by passing `Uint8Array` or `ArrayBuffer`. Use `type` on both reads and writes to get the right value type: `binary` for `Uint8Array` and `arrayBuffer` for `ArrayBuffer`. ```typescript import { actor } from "rivetkit"; @@ -49,7 +49,10 @@ const assets = actor({ return await c.kv.get("avatar", { type: "binary" }); }, putSnapshot: async (c, data: ArrayBuffer) => { - await c.kv.put("snapshot", data); + await c.kv.put("snapshot", data, { type: "arrayBuffer" }); + }, + getSnapshot: async (c) => { + return await c.kv.get("snapshot", { type: "arrayBuffer" }); }, }, }); @@ -110,15 +113,11 @@ const example = actor({ state: {}, actions: { pruneAndScan: async (c) => { - const encoder = new TextEncoder(); - const active = await c.kv.listRange( - encoder.encode("job:"), - encoder.encode("joc:"), - { - keyType: "text", - }, - ); + const active = await c.kv.listRange("job:", "joc:", { + keyType: "text", + }); + const encoder = new TextEncoder(); await c.kv.deleteRange( encoder.encode("job:old:"), encoder.encode("job:old;"), @@ -132,7 +131,7 @@ const example = actor({ ## Batch Operations -KV supports batch operations for efficiency. Defaults are still `text` for both keys and values. +KV supports batch operations for efficiency. `batchPut` and `batchGet` work on raw `Uint8Array` keys and values, so encode strings before passing them in. ```typescript import { actor } from "rivetkit"; @@ -141,12 +140,17 @@ const example = actor({ state: {}, actions: { batchOps: async (c) => { - await c.kv.putBatch([ - ["alpha", "1"], - ["beta", "2"], + const encoder = new TextEncoder(); + + await c.kv.batchPut([ + [encoder.encode("alpha"), encoder.encode("1")], + [encoder.encode("beta"), encoder.encode("2")], ]); - const values = await c.kv.getBatch(["alpha", "beta"]); + const values = await c.kv.batchGet([ + encoder.encode("alpha"), + encoder.encode("beta"), + ]); }, }, }); diff --git a/website/src/content/docs/actors/lifecycle.mdx b/website/src/content/docs/actors/lifecycle.mdx index ccccf82ec4..fa959b8fbf 100644 --- a/website/src/content/docs/actors/lifecycle.mdx +++ b/website/src/content/docs/actors/lifecycle.mdx @@ -29,11 +29,12 @@ Loading ──Start──▶ Ready ──spawn driver──▶ Started **On Create** (runs once per actor) -1. `createState` -2. `onCreate` -3. `createVars` -4. `onWake` -5. `run` (background, does not block) +1. `onMigrate` +2. `createState` +3. `onCreate` +4. `createVars` +5. `onWake` +6. `run` (background, does not block) **On Destroy** @@ -41,9 +42,10 @@ Loading ──Start──▶ Ready ──spawn driver──▶ Started **On Wake** (after sleep, restart, or crash) -1. `createVars` -2. `onWake` -3. `run` (background, does not block) +1. `onMigrate` +2. `createVars` +3. `onWake` +4. `run` (background, does not block) **On Sleep** (after idle period) @@ -91,6 +93,26 @@ const counter = actor({ }); ``` +### `onMigrate` + +[API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) + +The `onMigrate` hook runs on every actor start, before `createState`, `onCreate`, `createVars`, and `onWake`. Can be async. It runs early so that database migrations are applied before any other lifecycle hook accesses the database. The second parameter is `true` when the actor is being created for the first time. + +```typescript +import { actor } from "rivetkit"; + +const counter = actor({ + state: { count: 0 }, + + onMigrate: (c, isNew) => { + // Run database migrations before any other lifecycle hook + }, + + actions: { /* ... */ } +}); +``` + ### `createState` [API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) @@ -126,7 +148,7 @@ const counter = actor({ [API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) -The `createVars` function dynamically initializes ephemeral variables. Can be async. Use this when you need to initialize values at runtime. The `driverCtx` parameter provides driver-specific context. See [ephemeral variables documentation](/docs/actors/state#ephemeral-variables) for more information. +The `createVars` function dynamically initializes ephemeral variables. Can be async. Use this when you need to initialize values at runtime. See [ephemeral variables documentation](/docs/actors/state#ephemeral-variables) for more information. ```typescript import { actor } from "rivetkit"; @@ -138,7 +160,7 @@ interface CounterVars { const counter = actor({ state: { count: 0 }, - createVars: (c, driverCtx): CounterVars => ({ + createVars: (c): CounterVars => ({ lastAccessTime: Date.now(), emitter: new EventTarget() }), diff --git a/website/src/content/docs/actors/limits.mdx b/website/src/content/docs/actors/limits.mdx index 7da039d792..f8b6530211 100644 --- a/website/src/content/docs/actors/limits.mdx +++ b/website/src/content/docs/actors/limits.mdx @@ -11,7 +11,7 @@ There are two types of limits: - **Soft Limit**: Application-level limit, configurable in RivetKit. These cannot exceed the hard limit. - **Hard Limit**: Infrastructure-level limit that cannot be configured. -Soft limits can be configured in RivetKit by passing options to `setup`: +Soft limits are configured in two places. Registry-level WebSocket message-size limits are passed to `setup`: ```typescript import { setup } from "rivetkit"; @@ -20,6 +20,20 @@ const rivet = setup({ use: { /* ... */ }, maxIncomingMessageSize: 1_048_576, maxOutgoingMessageSize: 10_485_760, +}); +``` + +Per-actor limits such as queue sizes and lifecycle timeouts are passed to `actor(...)` via `options`: + +```typescript +import { actor } from "rivetkit"; + +const myActor = actor({ + options: { + maxQueueSize: 1000, + actionTimeout: 60_000, + stateSaveInterval: 1_000, + }, // ... }); ``` diff --git a/website/src/content/docs/actors/metadata.mdx b/website/src/content/docs/actors/metadata.mdx index 6a86c80f19..508e8375a9 100644 --- a/website/src/content/docs/actors/metadata.mdx +++ b/website/src/content/docs/actors/metadata.mdx @@ -135,4 +135,4 @@ console.log("Actor metadata:", metadata); ## API Reference - [`ActorDefinition`](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) - Interface for defining metadata -- [`CreateOptions`](/typedoc/interfaces/rivetkit.client_mod.CreateOptions.html) - Includes metadata options +- [`CreateOptions`](/typedoc/interfaces/rivetkit.client_mod.CreateOptions.html) - Options for creating an actor, including `region` and `input` diff --git a/website/src/content/docs/actors/queues.mdx b/website/src/content/docs/actors/queues.mdx index 8155449ee9..ae5d02d870 100644 --- a/website/src/content/docs/actors/queues.mdx +++ b/website/src/content/docs/actors/queues.mdx @@ -59,8 +59,8 @@ await handle.send("increment", { amount: 5 }); Use this when you want explicit completion/ack semantics but do not need to return data. -- If processing fails before `message.complete()`, the message is not acknowledged. -- Unacknowledged messages are retried, so mutation handlers should be idempotent. +- `message.complete()` resolves a sender waiting on `wait: true` (or `enqueueAndWait`). It does not change durability: messages are removed from queue storage when they are received, not when they are completed. +- If processing fails before `message.complete()`, the message is not redelivered, and any waiting sender times out instead of receiving a completion. - `status: "timedOut"` means sender timeout elapsed before `message.complete(...)`. diff --git a/website/src/content/docs/actors/quickstart/backend.mdx b/website/src/content/docs/actors/quickstart/backend.mdx index b247c57664..ea7b3559f3 100644 --- a/website/src/content/docs/actors/quickstart/backend.mdx +++ b/website/src/content/docs/actors/quickstart/backend.mdx @@ -28,6 +28,12 @@ npx skills add rivet-dev/skills npm install rivetkit ``` +If you plan to connect from a React frontend, also install `@rivetkit/react`: + +```sh +npm install @rivetkit/react +``` + diff --git a/website/src/content/docs/actors/quickstart/next-js.mdx b/website/src/content/docs/actors/quickstart/next-js.mdx index 4bdcbc564c..f1ec9f85df 100644 --- a/website/src/content/docs/actors/quickstart/next-js.mdx +++ b/website/src/content/docs/actors/quickstart/next-js.mdx @@ -70,7 +70,7 @@ import { registry } from "@/rivet/registry"; export const maxDuration = 300; -export const { GET, POST, PUT, PATCH, HEAD, OPTIONS } = toNextHandler(registry); +export const { GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS } = toNextHandler(registry); ``` @@ -87,9 +87,12 @@ import { createRivetKit } from "@rivetkit/next-js/client"; import type { registry } from "@/rivet/registry"; import { useState } from "react"; -export const { useActor } = createRivetKit( - process.env.NEXT_RIVET_ENDPOINT ?? "http://localhost:3000/api/rivet", -); +export const { useActor } = createRivetKit({ + endpoint: + process.env.NEXT_PUBLIC_RIVET_ENDPOINT ?? "http://localhost:3000/api/rivet", + namespace: process.env.NEXT_PUBLIC_RIVET_NAMESPACE, + token: process.env.NEXT_PUBLIC_RIVET_TOKEN, +}); export function Counter() { const [count, setCount] = useState(0); diff --git a/website/src/content/docs/actors/quickstart/rust.mdx b/website/src/content/docs/actors/quickstart/rust.mdx index dc106d5859..2958a6b912 100644 --- a/website/src/content/docs/actors/quickstart/rust.mdx +++ b/website/src/content/docs/actors/quickstart/rust.mdx @@ -201,14 +201,13 @@ const counter = client.counter.getOrCreate(["my-counter"]); const count = await counter.increment(3); console.log("New count:", count); -const connection = counter.connect(); -connection.on("newCount", (newCount: { count: number }) => { - console.log("Count changed:", newCount.count); -}); - -await connection.increment(1); +await counter.connect().increment(1); ``` + +Events emitted by a Rust actor with `ctx.emit(...)` are broadcast as a single serialized struct value. The TypeScript and React clients deliver event arguments positionally, so consuming a Rust struct event from JavaScript is not supported yet. Call actions across languages, and subscribe to events from Rust clients. + + See the [JavaScript client documentation](/docs/clients/javascript) for more information. @@ -229,10 +228,9 @@ function Counter() { key: ["my-counter"], }); - counter.useEvent("newCount", (event: { count: number }) => setCount(event.count)); - const increment = async () => { - await counter.connection?.increment(1); + const next = await counter.connection?.increment(1); + if (typeof next === "number") setCount(next); }; return ( @@ -244,6 +242,10 @@ function Counter() { } ``` + +Events emitted by a Rust actor with `ctx.emit(...)` are broadcast as a single serialized struct value. The TypeScript and React clients deliver event arguments positionally, so `useEvent` cannot consume a Rust struct event yet. This example reads the action return value instead. + + See the [React documentation](/docs/clients/react) for more information. diff --git a/website/src/content/docs/actors/request-handler.mdx b/website/src/content/docs/actors/request-handler.mdx index 04a6336eb0..b04112d4fe 100644 --- a/website/src/content/docs/actors/request-handler.mdx +++ b/website/src/content/docs/actors/request-handler.mdx @@ -185,7 +185,7 @@ const response = await fetch( { method: "POST", headers: { - Authorization: `Bearer ${token}`, + "x-rivet-token": token, }, } ); @@ -195,7 +195,7 @@ console.log(data); // { count: 1 } ```bash curl -X POST "https://api.rivet.dev/gateway/{actorId}/request/increment" \ - -H "Authorization: Bearer {token}" + -H "x-rivet-token: {token}" ``` @@ -226,9 +226,14 @@ app.all("/actors/:id/:path{.*}", async (c) => { const actorId = c.req.param("id"); const actorPath = (c.req.param("path") || ""); - // Forward to actor's onRequest handler + // Rewrite the incoming request to the actor-relative path, preserving + // method, headers, and body + const url = new URL(actorPath, "http://actor"); + const actorRequest = new Request(url, c.req.raw); + + // Forward the rewritten Request to the actor's onRequest handler const actor = client.counter.get(actorId); - return await actor.fetch(actorPath, c.req.raw); + return await actor.fetch(actorRequest); }); serve(app); diff --git a/website/src/content/docs/actors/schedule.mdx b/website/src/content/docs/actors/schedule.mdx index 6ed141c9a6..9113eabeed 100644 --- a/website/src/content/docs/actors/schedule.mdx +++ b/website/src/content/docs/actors/schedule.mdx @@ -56,7 +56,7 @@ const reminderService = actor({ state: { reminders: {} } as ReminderState, actions: { - setReminder: (c, userId: string, message: string, delayMs: number) => { + setReminder: async (c, userId: string, message: string, delayMs: number) => { const reminderId = crypto.randomUUID(); // Store the reminder in state @@ -67,7 +67,7 @@ const reminderService = actor({ }; // Schedule the sendReminder action to run after the delay - c.schedule.after(delayMs, "sendReminder", reminderId); + await c.schedule.after(delayMs, "sendReminder", reminderId); return { reminderId }; }, diff --git a/website/src/content/docs/actors/sqlite-drizzle.mdx b/website/src/content/docs/actors/sqlite-drizzle.mdx index a594101a03..62f1fe2646 100644 --- a/website/src/content/docs/actors/sqlite-drizzle.mdx +++ b/website/src/content/docs/actors/sqlite-drizzle.mdx @@ -35,8 +35,9 @@ src/ ``` - `index.ts` is the actor implementation. -- `drizzle/` contains files managed by `drizzle-kit`. -- Commit generated migration files to source control. +- `drizzle/` holds the SQL migrations (`*.sql`) and `meta/_journal.json` generated by `drizzle-kit`. +- `migrations.js` is a small RivetKit glue file you maintain by hand. It imports the journal and each `*.sql` file and exports a `{ journal, migrations }` object keyed by migration (for example `m0000`). Add a new entry here whenever `db:generate` produces a new migration. +- Commit the generated migration files and `migrations.js` to source control. ## Basic setup @@ -216,9 +217,13 @@ await c.db.execute( ## Queues -Use queues for ordered mutations and keep actions read-only. +Use queues for ordered mutations and keep actions read-only. Import `queue` alongside `actor` from `rivetkit`. ```ts @nocheck +import { actor, queue } from "rivetkit"; + +// ... + queues: { addTodo: queue<{ title: string }>(), }, diff --git a/website/src/content/docs/actors/sqlite.mdx b/website/src/content/docs/actors/sqlite.mdx index 673075148b..8a5f07e2a1 100644 --- a/website/src/content/docs/actors/sqlite.mdx +++ b/website/src/content/docs/actors/sqlite.mdx @@ -251,7 +251,6 @@ console.log(todos); - `GET /inspector/database/schema` returns the tables and views discovered in the actor's SQLite database. - `GET /inspector/database/rows?table=...&limit=100&offset=0` returns paged rows for a specific table or view. - `POST /inspector/database/execute` lets you run ad-hoc SQL for debugging and data fixes with positional `args` or named `properties`. -- `GET /inspector/traces` helps inspect slow query paths and SQL-heavy actions. - Keep a small read-only action for quick query verification while debugging. - In non-dev mode, inspector endpoints require authorization. diff --git a/website/src/content/docs/actors/state.mdx b/website/src/content/docs/actors/state.mdx index 32636bd6bf..98661fa864 100644 --- a/website/src/content/docs/actors/state.mdx +++ b/website/src/content/docs/actors/state.mdx @@ -301,7 +301,7 @@ State must be serializable. - `null`, `undefined`, `boolean`, `string`, `number`, `BigInt` - `Date`, `RegExp`, `Error` -- Typed arrays (`Uint8Array`, `Int8Array`, `Float32Array`, etc.) +- `ArrayBuffer` and typed arrays (`Uint8Array`, `Int8Array`, `Float32Array`, etc.) - `Map`, `Set`, `Array` - Plain objects @@ -434,7 +434,7 @@ For the full query API, schema migrations, transactions, and the Drizzle ORM, se ## Debugging -- `GET /inspector/state` returns the actor's current persisted state and `isStateEnabled`. +- `GET /inspector/state` returns the actor's current state and `isStateEnabled`. - `PATCH /inspector/state` lets you set state directly while debugging. - In non-dev mode, inspector endpoints require authorization. diff --git a/website/src/content/docs/actors/statuses.mdx b/website/src/content/docs/actors/statuses.mdx index a61380f6a7..3579aa4988 100644 --- a/website/src/content/docs/actors/statuses.mdx +++ b/website/src/content/docs/actors/statuses.mdx @@ -10,17 +10,17 @@ These are the statuses you can see in the dashboard for each actor. | Status | Description | |---|---| -| **Starting** | The actor has been created and a runner has been allocated, but the actor process has not yet reported that it is ready. | +| **Starting** | The actor has been created but has not yet become connectable. | | **Running** | The actor is live and accepting connections. | -| **Stopped** | The actor has been gracefully destroyed. | +| **Destroyed** | The actor has been gracefully destroyed. | | **Crashed** | The actor failed to start or encountered a fatal error. See [Troubleshooting](/docs/actors/troubleshooting#actor-status-is-crashed) for common failure reasons. | | **Sleeping** | The actor has been put to sleep from inactivity. It will be woken up automatically when a new request arrives. | | **Pending** | The actor is waiting to be allocated to a runner. This happens when no runner is available to handle the actor. See [Troubleshooting](/docs/actors/troubleshooting#actor-status-is-pending) for common causes. | -| **Crash-Loop** | The actor failed to allocate and is waiting to retry with a backoff. This typically means repeated allocation failures. The backoff prevents overloading your infrastructure in the case of a widespread misconfiguration in your backend. See [Troubleshooting](/docs/actors/troubleshooting#actor-status-is-crashed) for common failure reasons. | +| **Crash Loop Backoff** | The actor failed to allocate and is waiting to retry with a backoff. This typically means repeated allocation failures. The backoff prevents overloading your infrastructure in the case of a widespread misconfiguration in your backend. See [Troubleshooting](/docs/actors/troubleshooting#actor-status-is-crashed) for common failure reasons. | ## API Representation -The actor object returned by the API includes the following timestamp fields used to derive status: +The actor object returned by the full engine API (used by the dashboard) includes the following timestamp fields used to derive status: | Field | Description | |---|---| diff --git a/website/src/content/docs/actors/testing.mdx b/website/src/content/docs/actors/testing.mdx index 4666b4da69..bce806032b 100644 --- a/website/src/content/docs/actors/testing.mdx +++ b/website/src/content/docs/actors/testing.mdx @@ -18,7 +18,7 @@ npm test ## Basic Testing Setup -Rivet includes a test helper called `setupTest` that configures a test environment with in-memory drivers for your actors. This allows for fast, isolated tests without external dependencies. +Rivet includes a test helper called `setupTest` that starts your registry in test mode and returns a client connected to it. This allows for fast, isolated tests without external dependencies. ```ts import { test, expect } from "vitest"; @@ -49,7 +49,7 @@ test("my actor test", async (testCtx) => { const { client } = await setupTest(testCtx, registry); // Now you can interact with your actor through the client - const myActorHandle = client.myActor.get(["test"]); + const myActorHandle = client.myActor.getOrCreate(["test"]); // Test your actor's functionality await myActorHandle.someAction(); @@ -62,7 +62,7 @@ test("my actor test", async (testCtx) => { ## Testing Actor State -The test framework uses in-memory drivers that persist state within each test, allowing you to verify that your actor correctly maintains state between operations. +State persists within each test, allowing you to verify that your actor correctly maintains state between operations. ```ts import { test, expect } from "vitest"; @@ -92,7 +92,7 @@ const registry = setup({ // Test state persistence test("actor should persist state", async (testCtx) => { const { client } = await setupTest(testCtx, registry); - const counterHandle = client.counter.get(["test"]); + const counterHandle = client.counter.getOrCreate(["test"]); // Initial state expect(await counterHandle.getCount()).toBe(0); @@ -143,7 +143,7 @@ const registry = setup({ // Test event emission test("actor should emit events", async (testCtx) => { const { client } = await setupTest(testCtx, registry); - const chatRoomHandle = client.chatRoom.get(["test"]); + const chatRoomHandle = client.chatRoom.getOrCreate(["test"]); // Set up event handler with a mock function const mockHandler = vi.fn(); @@ -162,13 +162,16 @@ test("actor should emit events", async (testCtx) => { ## Testing Schedules -Rivet's schedule functionality can be tested using Vitest's time manipulation utilities: +Rivet's schedule functionality can be tested by scheduling work and waiting for it to run: ```ts -import { test, expect, vi } from "vitest"; +import { test, expect } from "vitest"; import { setupTest } from "rivetkit/test"; import { actor, setup } from "rivetkit"; +// Helper to wait for a delay +const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + // Define the scheduler actor const scheduler = actor({ state: { @@ -200,32 +203,31 @@ const registry = setup({ // Test scheduled tasks test("scheduled tasks should execute", async (testCtx) => { - // setupTest automatically configures vi.useFakeTimers() const { client } = await setupTest(testCtx, registry); - const schedulerHandle = client.scheduler.get(["test"]); + const schedulerHandle = client.scheduler.getOrCreate(["test"]); // Set up a scheduled task - await schedulerHandle.scheduleTask("reminder", 60000); // 1 minute in the future + await schedulerHandle.scheduleTask("reminder", 100); // 100ms in the future - // Fast-forward time by 1 minute - await vi.advanceTimersByTimeAsync(60000); + // Wait for the scheduled task to run + await wait(150); // Verify the scheduled task executed expect(await schedulerHandle.getCompletedTasks()).toContain("reminder"); }); ``` -The `setupTest` function automatically calls `vi.useFakeTimers()`, allowing you to control time in your tests with functions like `vi.advanceTimersByTimeAsync()`. This makes it possible to test scheduled operations without waiting for real time to pass. +Use a short real-time delay to wait for scheduled work to run. `setupTest` does not install fake timers, so if you want to use `vi.useFakeTimers()` you must enable it yourself and confirm it works with your selected runtime. ## Best Practices 1. **Isolate tests**: Each test should run independently, avoiding shared state. 2. **Test edge cases**: Verify how your actor handles invalid inputs, concurrent operations, and error conditions. -3. **Mock time**: Use Vitest's timer mocks for testing scheduled operations. +3. **Test scheduled operations**: Use short real-time delays to wait for scheduled work to run. 4. **Use realistic data**: Test with data that resembles production scenarios. -Rivet's testing framework automatically handles server setup and teardown, so you can focus on writing effective tests for your business logic. +`setupTest` starts the registry and disposes the returned client when the test finishes, so you can focus on writing effective tests for your business logic. ## API Reference -- [`test`](/typedoc/functions/rivetkit.mod.test.html) - Test helper function +- [`setupTest`](/typedoc/functions/rivetkit.test_mod.setupTest.html) - Test setup helper function diff --git a/website/src/content/docs/actors/troubleshooting.mdx b/website/src/content/docs/actors/troubleshooting.mdx index ed684346eb..a4cf9002cc 100644 --- a/website/src/content/docs/actors/troubleshooting.mdx +++ b/website/src/content/docs/actors/troubleshooting.mdx @@ -65,6 +65,22 @@ The server running your actor lost its connection to Rivet. This is usually caus Your server is shutting down and the actor did not finish in time. Consider handling graceful shutdown in your actor or increasing your shutdown timeout. +### `concurrent_actor_limit_reached` + +The actor could not be allocated because the concurrent actor limit was reached. Reduce the number of concurrently running actors or increase your limit. + +### `no_envoys` + +No server was available to run your actor. This is equivalent to `no_capacity` on the current allocation path. See the `no_capacity` section above for the causes and fixes for your [runtime mode](/docs/general/runtime-modes). + +### `envoy_no_response` + +The server running your actor did not respond in time. This can happen if your server is overloaded or experienced a network issue. Try restarting your server or checking its health. + +### `envoy_connection_lost` + +The server running your actor lost its connection to Rivet. This is usually caused by a network interruption or your server restarting. + ### `serverless_http_error` Your serverless endpoint returned an HTTP error. Common causes: @@ -116,7 +132,7 @@ Without versioning, Rivet has no way to distinguish old deployments from new one - **Serverless**: Old requests may still be open from the previous deployment, so actors continue running on the old version's connection until those requests close. - **Runners**: The old runner container is still running and will continue accepting new actors. New actors may be scheduled on the old runner instead of the new one. -To fix this, configure a version number in your [registry configuration](/docs/connect/registry-configuration). When a new version is deployed, Rivet will allocate new actors to the latest version and optionally drain old actors to migrate them. +To fix this, configure a version number in your [registry configuration](/docs/general/registry-configuration). When a new version is deployed, Rivet will allocate new actors to the latest version and optionally drain old actors to migrate them. ## Actor status is pending diff --git a/website/src/content/docs/actors/types.mdx b/website/src/content/docs/actors/types.mdx index 2b6e5a5d93..7c5d3a72a5 100644 --- a/website/src/content/docs/actors/types.mdx +++ b/website/src/content/docs/actors/types.mdx @@ -12,8 +12,6 @@ Context types define what properties and methods are available in different part import { actor } from "rivetkit"; const counter = actor({ - state: { count: 0 }, - // CreateContext in createState hook createState: (c, input: { initial: number }) => { return { count: input.initial }; @@ -37,14 +35,9 @@ When writing helper functions that work with actor contexts, use context extract import { actor, CreateContextOf, ActionContextOf } from "rivetkit"; const gameRoom = actor({ - state: { - players: [] as string[], - score: 0 - }, - createState: (c, input: { roomId: string }) => { initializeRoom(c, input.roomId); - return { players: [], score: 0 }; + return { players: [] as string[], score: 0 }; }, actions: { diff --git a/website/src/content/docs/actors/versions.mdx b/website/src/content/docs/actors/versions.mdx index 400dc34504..ae936241b5 100644 --- a/website/src/content/docs/actors/versions.mdx +++ b/website/src/content/docs/actors/versions.mdx @@ -13,7 +13,7 @@ Each runner has a **version number**. When you deploy new code with a new versio - **Drain old actors**: When enabled, a runner connecting with a newer version number will gracefully stop old actors to be rescheduled to the new version -Versions are not configured by default. See [Registry Configuration](/docs/connect/registry-configuration) to learn how to configure the runner version. +Versions are not configured by default. See [Registry Configuration](/docs/general/registry-configuration) to learn how to configure the runner version. @@ -26,7 +26,7 @@ Versions are not configured by default. See [Registry Configuration](/docs/conne -When a new version is deployed, existing actors are immediately drained from the old runner and live migrated to the new version. +When a new version is deployed, existing actors are gracefully stopped on the old runner and rescheduled onto the new version. ```mermaid sequenceDiagram @@ -36,8 +36,8 @@ sequenceDiagram Note over R1: Currently running Note over R2: Deployed R2->>R1: Drain old actors - R1->>R2: Live migrate actors - Note over R1: Shut down when all actors migrated + R1->>R2: Reschedule actors + Note over R1: Shut down when all actors stopped ``` @@ -214,8 +214,8 @@ The `drainOnVersionUpgrade` option controls whether old actors are stopped when | Value | Behavior | |-------|----------| -| `false` (default in [runner mode](/docs/general/runtime-modes)) | Old actors continue running. New actors go to new version. Versions coexist. | -| `true` (default in [serverless mode](/docs/general/runtime-modes)) | Old actors receive stop signal and have 30m to finish gracefully. | +| `false` | Old actors continue running. New actors go to new version. Versions coexist. | +| `true` (default) | Old actors receive stop signal and have 30m to finish gracefully. | ## Upgrading Actor State diff --git a/website/src/content/docs/actors/websocket-handler.mdx b/website/src/content/docs/actors/websocket-handler.mdx index 1e3e13305d..073cca359a 100644 --- a/website/src/content/docs/actors/websocket-handler.mdx +++ b/website/src/content/docs/actors/websocket-handler.mdx @@ -45,7 +45,7 @@ See also the [raw WebSocket handler example](https://github.com/rivet-dev/rivet/ ### Via RivetKit Client -Use the `.websocket()` method on an actor handle to open a WebSocket connection to the actor's `onWebSocket` handler. This can be executed from either your frontend or backend. +Use the `.webSocket()` method on an actor handle to open a WebSocket connection to the actor's `onWebSocket` handler. This can be executed from either your frontend or backend. ```typescript index.ts @hide @nocheck @@ -87,7 +87,7 @@ ws.send(JSON.stringify({ type: "chat", text: "Hello!" })); ``` -The `.websocket()` method returns a standard WebSocket. +The `.webSocket()` method returns a standard WebSocket. ### Via getGatewayUrl @@ -206,6 +206,12 @@ import { actor, setup } from "rivetkit"; const chatActor = actor({ state: { messages: [] as string[] }, + onWebSocket: (c, websocket) => { + websocket.addEventListener("message", (event) => { + c.state.messages.push(event.data as string); + websocket.send(event.data as string); + }); + }, actions: {} }); diff --git a/website/src/content/docs/actors/workflows.mdx b/website/src/content/docs/actors/workflows.mdx index 164b81e8a6..4d7e262055 100644 --- a/website/src/content/docs/actors/workflows.mdx +++ b/website/src/content/docs/actors/workflows.mdx @@ -598,6 +598,8 @@ console.log(await handle.getState()); Use step timeouts and retries for slow or flaky dependencies. +Step timeouts are critical by default and fail immediately. Set `retryOnTimeout: true` if a timeout should retry like any other error using `maxRetries`. + ```ts import { actor, queue, setup } from "rivetkit"; import { type WorkflowContextOf, type WorkflowLoopContextOf, type WorkflowBranchContextOf, workflow } from "rivetkit/workflow"; @@ -620,6 +622,7 @@ export const timeoutActor = actor({ const chargeId = await loopCtx.step({ name: "charge-card", timeout: 5_000, + retryOnTimeout: true, maxRetries: 5, retryBackoffBase: 200, retryBackoffMax: 2_000, @@ -1573,8 +1576,10 @@ export const checkoutSagaActor = actor({ await loopCtx.step({ name: "reserve-inventory", run: async () => reserveInventoryForCheckout(loopCtx, checkout.orderId), + // Rollback callbacks only receive a rollback context, not actor + // APIs like client(). Compensate with direct external calls. rollback: async (_rollbackCtx, output) => { - await releaseInventoryForCheckout(loopCtx, output as string); + await releaseInventoryForCheckout(output as string); }, }); @@ -1582,7 +1587,7 @@ export const checkoutSagaActor = actor({ name: "charge-card", run: async () => chargeCheckout(loopCtx, checkout.amount), rollback: async (_rollbackCtx, output) => { - await refundCheckout(loopCtx, output as string); + await refundCheckout(output as string); }, }); @@ -1605,12 +1610,12 @@ async function reserveInventoryForCheckout( } async function releaseInventoryForCheckout( - ctx: WorkflowLoopContextOf, reservationId: string, ): Promise { - const client = ctx.client(); - const inventory = client.inventoryActor.getOrCreate(["main"]); - await inventory.release(reservationId); + await fetch("https://api.example.com/inventory/release", { + method: "POST", + body: JSON.stringify({ reservationId }), + }); } async function chargeCheckout( @@ -1623,12 +1628,12 @@ async function chargeCheckout( } async function refundCheckout( - ctx: WorkflowLoopContextOf, chargeId: string, ): Promise { - const client = ctx.client(); - const billing = client.billingActor.getOrCreate(["main"]); - await billing.refund(chargeId); + await fetch("https://api.example.com/billing/refund", { + method: "POST", + body: JSON.stringify({ chargeId }), + }); } function markOrderComplete( @@ -1724,12 +1729,13 @@ export const pollBackoffActor = actor({ return; } - await loopCtx.step("grow-backoff", async () => { + const retryDelay = await loopCtx.step("grow-backoff", async () => { loopCtx.state.status = "retrying"; loopCtx.state.backoffMs = Math.min(loopCtx.state.backoffMs * 2, 5_000); + return loopCtx.state.backoffMs; }); - await loopCtx.sleep("retry-delay", loopCtx.state.backoffMs); + await loopCtx.sleep("retry-delay", retryDelay); }); }), actions: { diff --git a/website/src/sitemap/mod.ts b/website/src/sitemap/mod.ts index 1eecc53763..8a07307074 100644 --- a/website/src/sitemap/mod.ts +++ b/website/src/sitemap/mod.ts @@ -324,10 +324,6 @@ export const sitemap = [ title: "Custom Inspector Tabs", href: "/docs/actors/inspector-tabs", }, - { - title: "AI & User-Generated Actors", - href: "/docs/actors/ai-and-user-generated-actors", - }, { title: "Types", href: "/docs/actors/types",