From da14b71f8b05e0d64b6fde48f15554690bdb2eee Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Fri, 8 Aug 2025 17:10:49 -0700 Subject: [PATCH] chore(site): update docs on new lifecycle hooks --- .../content/docs/actors/authentication.mdx | 31 +++++----- site/src/content/docs/actors/connections.mdx | 2 +- .../docs/actors/ephemeral-variables.mdx | 10 +++- site/src/content/docs/actors/events.mdx | 4 +- site/src/content/docs/actors/external-sql.mdx | 16 +++--- site/src/content/docs/actors/helper-types.mdx | 4 +- site/src/content/docs/actors/input.mdx | 41 +++++++------ site/src/content/docs/actors/keys.mdx | 2 +- site/src/content/docs/actors/lifecycle.mdx | 41 +++++++------ site/src/content/docs/actors/state.mdx | 57 ++++++++++++++++--- site/src/sitemap/mod.ts | 2 +- 11 files changed, 132 insertions(+), 78 deletions(-) diff --git a/site/src/content/docs/actors/authentication.mdx b/site/src/content/docs/actors/authentication.mdx index 1606a07652..6980946796 100644 --- a/site/src/content/docs/actors/authentication.mdx +++ b/site/src/content/docs/actors/authentication.mdx @@ -14,11 +14,10 @@ The `onAuth` hook runs on the HTTP server before clients can access actors. This import { actor, UserError } from "@rivetkit/actor"; const chatRoom = actor({ - onAuth: async (opts) => { - const { req, params, intents } = opts; + onAuth: async (params: { authToken?: string }, { request, intents }) => { // Extract token from params or headers - const token = params.authToken || req.headers.get("Authorization"); + const token = params.authToken || request.headers.get("Authorization"); if (!token) { throw new UserError("Authentication required"); @@ -83,8 +82,8 @@ const userProfileActor = actor({ } }, - createConnState: (c, opts) => { - return { userId: opts.params.userId }; + createConnState: (c, opts, params: { userId: string }) => { + return { userId: params.userId }; }, actions: { @@ -128,9 +127,7 @@ The `onAuth` hook receives an `intents` parameter indicating what the client wan ```typescript const secureActor = actor({ - onAuth: async (opts) => { - const { intents, params } = opts; - + onAuth: async (params: { token: string }, { intents }) => { // Different validation based on intent if (intents.has("action")) { // Requires higher privileges for actions @@ -162,8 +159,8 @@ Use specific error types for different authentication failures: import { UserError, Unauthorized, Forbidden } from "@rivetkit/actor/errors"; const protectedActor = actor({ - onAuth: async (opts) => { - const token = opts.params.authToken; + onAuth: async (params: { authToken?: string }) => { + const token = params.authToken; if (!token) { throw new Unauthorized("Authentication token required"); @@ -226,9 +223,9 @@ import { actor, UserError } from "@rivetkit/actor"; import jwt from "jsonwebtoken"; const jwtActor = actor({ - onAuth: async (opts) => { - const token = opts.params.jwt || - opts.req.headers.get("Authorization")?.replace("Bearer ", ""); + onAuth: async (params: { jwt?: string }, { request }) => { + const token = params.jwt || + request.headers.get("Authorization")?.replace("Bearer ", ""); if (!token) { throw new UserError("JWT token required"); @@ -265,9 +262,9 @@ const jwtActor = actor({ ```typescript const apiActor = actor({ - onAuth: async (opts) => { - const apiKey = opts.params.apiKey || - opts.req.headers.get("X-API-Key"); + onAuth: async (params: { apiKey?: string }, { request }) => { + const apiKey = params.apiKey || + request.headers.get("X-API-Key"); if (!apiKey) { throw new UserError("API key required"); @@ -331,7 +328,7 @@ export function requirePermission(permission: string) { // usage in actor const forumActor = actor({ - onAuth: async (opts) => { + onAuth: async (params: { token: string }) => { // ... authenticate and return user with role/permissions }, diff --git a/site/src/content/docs/actors/connections.mdx b/site/src/content/docs/actors/connections.mdx index 3feec6f9d8..3bbbba255d 100644 --- a/site/src/content/docs/actors/connections.mdx +++ b/site/src/content/docs/actors/connections.mdx @@ -27,7 +27,7 @@ const gameRoom = actor({ state: {}, // Handle connection setup - createConnState: (c, { params }) => { + createConnState: (c, opts, params: { authToken: string }) => { // Validate authentication token const authToken = params.authToken; diff --git a/site/src/content/docs/actors/ephemeral-variables.mdx b/site/src/content/docs/actors/ephemeral-variables.mdx index ca249d58d2..20f9506a53 100644 --- a/site/src/content/docs/actors/ephemeral-variables.mdx +++ b/site/src/content/docs/actors/ephemeral-variables.mdx @@ -42,14 +42,14 @@ This value will be cloned for every new actor using `structuredClone`. Create actor state dynamically on each actors' start: ```typescript -import { actor } from "@rivetkit/actor"; +import { actor, ActorInitContext } from "@rivetkit/actor"; // Define vars with initialization logic const counter = actor({ state: { count: 0 }, // Define vars using a creation function - createVars: () => { + createVars: (c: ActorInitContext, driver: any) => { return { lastAccessTime: Date.now(), emitter: createNanoEvents() @@ -62,6 +62,12 @@ const counter = actor({ }); ``` + +If accepting arguments to `createVars`, you **must** define the types: `createVars(c: ActorInitContext, driver: any)` + +Otherwise, the return type will not be inferred and `c.vars` will be of type `unknown`. + + diff --git a/site/src/content/docs/actors/events.mdx b/site/src/content/docs/actors/events.mdx index 37d8aa1406..ed5b3a5335 100644 --- a/site/src/content/docs/actors/events.mdx +++ b/site/src/content/docs/actors/events.mdx @@ -50,7 +50,7 @@ const gameRoom = actor({ players: {} as Record }, - createConnState: (c, { params }) => ({ + createConnState: (c, opts, params: { playerId: string, role?: string }) => ({ playerId: params.playerId, role: params.role || "player" }), @@ -86,7 +86,7 @@ const gameRoom = actor({ players: {} as Record }, - createConnState: (c, { params }) => ({ + createConnState: (c, opts, params: { playerId: string, role?: string }) => ({ playerId: params.playerId, role: params.role || "player" }), diff --git a/site/src/content/docs/actors/external-sql.mdx b/site/src/content/docs/actors/external-sql.mdx index 3b6d94ff69..56c8d5b16a 100644 --- a/site/src/content/docs/actors/external-sql.mdx +++ b/site/src/content/docs/actors/external-sql.mdx @@ -36,7 +36,7 @@ Here's a basic example of a user actor that creates a database record on start a ```typescript {{ "title": "registry.ts" }} -import { actor, setup } from "@rivetkit/actor"; +import { actor, setup, ActorInitContext } from "@rivetkit/actor"; import { Pool } from "pg"; interface ActorInput { @@ -55,10 +55,10 @@ const pool = new Pool({ // Create the user actor export const userActor = actor({ - createState: (opts: { input: ActorInput }) => ({ + createState: (c: ActorInitContext, input: ActorInput) => ({ requestCount: 0, - username: opts.input.username, - email: opts.input.email, + username: input.username, + email: input.email, lastActive: Date.now() }), @@ -140,7 +140,7 @@ Here's the same user actor pattern using Drizzle ORM for more type-safe database ```typescript {{ "title": "registry.ts" }} -import { actor, setup } from "@rivetkit/actor"; +import { actor, setup, ActorInitContext } from "@rivetkit/actor"; import { drizzle } from "drizzle-orm/node-postgres"; import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; import { eq } from "drizzle-orm"; @@ -169,10 +169,10 @@ const db = drizzle(pool); // Create the user actor export const userActor = actor({ - createState: (opts: { input: ActorInput }) => ({ + createState: (c: ActorInitContext, input: ActorInput) => ({ requestCount: 0, - username: opts.input.username, - email: opts.input.email, + username: input.username, + email: input.email, lastActive: Date.now() }), diff --git a/site/src/content/docs/actors/helper-types.mdx b/site/src/content/docs/actors/helper-types.mdx index 6708875091..3e53f37588 100644 --- a/site/src/content/docs/actors/helper-types.mdx +++ b/site/src/content/docs/actors/helper-types.mdx @@ -2,9 +2,10 @@ Rivet provides several TypeScript helper types to make it easier to work with actors in a type-safe way. + ## `Context` Types -When working with actors, you often need to access the context object. Rivet provides helper types to extract the context types from actor definitions. +When working with actors, you often need to access the context object outside of the actor's handlers. Rivet provides helper types to extract the context types from actor definitions. ### `ActorContextOf` @@ -57,4 +58,3 @@ function processCounterAction(context: ActionContextOf) { context.state.count++; } ``` - diff --git a/site/src/content/docs/actors/input.mdx b/site/src/content/docs/actors/input.mdx index b56c86ffe5..13fa613f89 100644 --- a/site/src/content/docs/actors/input.mdx +++ b/site/src/content/docs/actors/input.mdx @@ -32,21 +32,26 @@ const gameHandle = client.game.getOrCreate(["game-456"], { Input is available in lifecycle hooks via the `opts.input` parameter: ```typescript +interface ChatRoomInput { + roomName: string, + isPrivate: boolean, +} + const chatRoom = actor({ - createState: (c, opts) => ({ - name: opts.input?.roomName ?? "Unnamed Room", - isPrivate: opts.input?.isPrivate ?? false, - maxUsers: opts.input?.maxUsers ?? 50, + createState: (c: ActorInitContext, input: ChatRoomInput) => ({ + name: input?.roomName ?? "Unnamed Room", + isPrivate: input?.isPrivate ?? false, + maxUsers: input?.maxUsers ?? 50, users: {}, messages: [], }), - onCreate: (c, opts) => { - console.log(`Creating room: ${opts.input?.roomName}`); + onCreate: (c, opts, input: ChatRoomInput) => { + console.log(`Creating room: ${input.roomName}`); // Setup external services based on input - if (opts.input?.isPrivate) { - setupPrivateRoomLogging(opts.input.roomName); + if (input.isPrivate) { + setupPrivateRoomLogging(input.roomName); } }, @@ -76,9 +81,9 @@ const GameInputSchema = z.object({ }); const game = actor({ - createState: (c, opts) => { + createState: (c: ActorInitContext, inputRaw: z.infer) => { // Validate input - const input = GameInputSchema.parse(opts.input); + const input = GameInputSchema.parse(inputRaw); return { gameMode: input.gameMode, @@ -142,10 +147,10 @@ interface GameInput { } const game = actor({ - createState: (c, opts: { input?: GameInput }) => ({ - gameMode: opts.input?.gameMode ?? "casual", - maxPlayers: opts.input?.maxPlayers ?? 4, - difficulty: opts.input?.difficulty ?? "medium", + createState: (c: ActorInitContext, input: GameInput) => ({ + gameMode: input.gameMode, + maxPlayers: input.maxPlayers, + difficulty: input.difficulty ?? "medium", }), actions: { @@ -160,12 +165,12 @@ If you need to access input data in actions, store it in the actor's state: ```typescript const game = actor({ - createState: (c, opts) => ({ + createState: (c: ActorInitContext, input: GameInput) => ({ // Store input configuration in state config: { - gameMode: opts.input?.gameMode ?? "casual", - maxPlayers: opts.input?.maxPlayers ?? 4, - difficulty: opts.input?.difficulty ?? "medium", + gameMode: input.gameMode, + maxPlayers: input.maxPlayers, + difficulty: input?.difficulty ?? "medium", }, // Runtime state players: {}, diff --git a/site/src/content/docs/actors/keys.mdx b/site/src/content/docs/actors/keys.mdx index 57e2c69a03..86c4db13c7 100644 --- a/site/src/content/docs/actors/keys.mdx +++ b/site/src/content/docs/actors/keys.mdx @@ -111,7 +111,7 @@ Use keys to provide basic actor configuration: ```typescript {{"title":"registry.ts"}} const userSession = actor({ - createState: (c) => ({ + createState: () => ({ userId: c.key[0], // Extract user ID from key loginTime: Date.now(), preferences: {} diff --git a/site/src/content/docs/actors/lifecycle.mdx b/site/src/content/docs/actors/lifecycle.mdx index b0bd85de1f..e0babd2b24 100644 --- a/site/src/content/docs/actors/lifecycle.mdx +++ b/site/src/content/docs/actors/lifecycle.mdx @@ -22,7 +22,9 @@ The `createVars` function or `vars` constant defines ephemeral variables for the The `createVars` function can also receive driver-specific context as its second parameter, allowing access to driver capabilities like Rivet KV or Cloudflare Durable Object storage. ```typescript -import { actor } from "@rivetkit/actor"; +import { actor, ActorInitContext } from "@rivetkit/actor"; +// In this example, assume we're using Redis +import type { DriverContext } from "@rivetkit/redis"; // Using vars constant const counter1 = actor({ @@ -48,7 +50,7 @@ const counter2 = actor({ const exampleActor = actor({ state: { count: 0 }, // Access driver context in createVars - createVars: (c, driverCtx) => ({ + createVars: (c: ActorInitContext, driverCtx: DriverContext) => ({ driverCtx, }), actions: { @@ -87,10 +89,10 @@ const counter3 = actor({ state: { count: 0 }, // Run initialization logic (logging, external service setup, etc.) - onCreate: (c, opts) => { + onCreate: (c, opts, input: { foo: string }) => { console.log("Counter actor initialized"); // Access input parameters if provided - console.log("Input:", opts.input); + console.log("Input:", input); // Can perform async operations or setup // No need to return anything }, @@ -198,7 +200,7 @@ const chatRoom = actor({ }, // Method 2: Dynamically create connection state - createConnState: (c, { params }) => { + createConnState: (c, opts, params: { userId?: string, role?: string }) => { return { userId: params.userId || "anonymous", role: params.role || "guest", @@ -207,7 +209,7 @@ const chatRoom = actor({ }, // Validate connections before accepting them - onBeforeConnect: (c, { params }) => { + onBeforeConnect: (c, opts, params: { authToken?: string }) => { // Validate authentication const authToken = params.authToken; if (!authToken || !validateToken(authToken)) { @@ -369,8 +371,8 @@ import { actor } from "@rivetkit/actor"; const secureActor = actor({ // Authentication handler - runs on HTTP server - onAuth: async (opts) => { - const authHeader = opts.req.headers.get("authorization"); + onAuth: async (params: undefined, { request }) => { + const authHeader = request.headers.get("authorization"); if (!authHeader?.startsWith("Bearer ")) { throw new Error("Missing or invalid authorization header"); } @@ -537,9 +539,15 @@ See [Helper Types](/docs/actors/helper-types) for more details on using `ActorCo ## Full Example ```typescript -import { actor } from "@rivetkit/actor"; +import { actor, ActorInitContext } from "@rivetkit/actor"; import { z } from "zod"; +interface CounterInput { + initialCount?: number; + stepSize?: number; + name?: string; +} + const counter = actor({ // Authentication handler (runs on HTTP server) onAuth: async (opts) => { @@ -561,16 +569,16 @@ const counter = actor({ }, // Initialize state with input - createState: (c, opts) => ({ - count: opts.input?.initialCount ?? 0, - stepSize: opts.input?.stepSize ?? 1, - name: opts.input?.name ?? "Unnamed Counter", + createState: (c: ActorInitContext, input: CounterInput) => ({ + count: input.initialCount ?? 0, + stepSize: input.stepSize ?? 1, + name: input.name ?? "Unnamed Counter", requestCount: 0, }), // Initialize actor (run setup that doesn't affect initial state) - onCreate: (c, opts) => { - console.log(`Counter "${opts.input?.name}" initialized`); + onCreate: (c, opts, input: { name: string }) => { + console.log(`Counter "${input.name}" initialized`); // Set up external resources, logging, etc. }, @@ -579,8 +587,7 @@ const counter = actor({ role: "guest" }, - // Dynamically create connection state based on params - createConnState: (c, { params }) => { + createConnState: (c, opts) => { // Auth data is available from onAuth return { userId: c.conn.auth.userId, diff --git a/site/src/content/docs/actors/state.mdx b/site/src/content/docs/actors/state.mdx index 5dd9c4e5d4..33eac8b92c 100644 --- a/site/src/content/docs/actors/state.mdx +++ b/site/src/content/docs/actors/state.mdx @@ -55,6 +55,36 @@ const counter = actor({ }); ``` +To accept a custom input parameters for the initial state, use: + +```typescript +import { actor, ActorInitContext } from "@rivetkit/actor"; + +interface CounterInput { + startingCount: number; +} + +// State with initialization logic +const counter = actor({ + // Define state using a creation function + createState: (c: ActorInitContext, input: CounterInput) => { + return { count: input.startingCount }; + }, + + actions: { + // ... + } +}); +``` + +Read more about [input parameters](/docs/actors/input) here. + + +If accepting arguments to `createState`, you **must** define the types: `createSTate(c: ActorInitContext, input: MyType)` + +Otherwise, the return type will not be inferred and `c.vars` will be of type `unknown`. + + @@ -141,13 +171,22 @@ In addition to persisted state, actors can store ephemeral data that is not save For complete documentation on ephemeral variables, see [Ephemeral Variables](/docs/actors/ephemeral-variables). -## Limitations - -State is currently constrained to the available memory on the machine. - -Only JSON-serializable types can be stored in state. In serverless runtimes that support it (Rivet, Cloudflare Workers), state is persisted under the hood in a compact, binary format. This is because JavaScript classes cannot be serialized & deserialized. - - -SQLite in Rivet Actors (coming soon) will provide a way of dynamically querying data with in-memory performance without being constrained to memory limits. - +## 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 diff --git a/site/src/sitemap/mod.ts b/site/src/sitemap/mod.ts index f8582c14a0..027ec08680 100644 --- a/site/src/sitemap/mod.ts +++ b/site/src/sitemap/mod.ts @@ -200,7 +200,7 @@ export const sitemap = [ collapsible: true, pages: [ { - title: "Variables", + title: "Ephemeral Variables", href: "/docs/actors/ephemeral-variables", //icon: faMemory, },