diff --git a/rivetkit-typescript/packages/rivetkit/src/actor/config.ts b/rivetkit-typescript/packages/rivetkit/src/actor/config.ts index 836f31bbe1..807e53cf53 100644 --- a/rivetkit-typescript/packages/rivetkit/src/actor/config.ts +++ b/rivetkit-typescript/packages/rivetkit/src/actor/config.ts @@ -50,6 +50,7 @@ export const ActorConfigSchema = z .object({ onCreate: zFunction().optional(), onDestroy: zFunction().optional(), + run: zFunction().optional(), onWake: zFunction().optional(), onSleep: zFunction().optional(), onStateChange: zFunction().optional(), @@ -130,6 +131,13 @@ export const ActorConfigSchema = z message: "Cannot define both 'vars' and 'createVars'", path: ["vars"], }, + ) + .refine( + (data) => !(data.run !== undefined && data.onWake !== undefined), + { + message: "Cannot define both 'run' and 'onWake'. 'run' is an alias for 'onWake'.", + path: ["run"], + }, ); // Creates state config @@ -279,6 +287,24 @@ interface BaseActorConfig< >, ) => void | Promise; + /** + * Called when the actor is started and ready to receive connections and actions. + * + * This is an alias for `onWake`. Use either `run` or `onWake`, but not both. + * + * @returns Void or a Promise that resolves when startup is complete + */ + run?: ( + c: WakeContext< + TState, + TConnParams, + TConnState, + TVars, + TInput, + TDatabase + >, + ) => void | Promise; + /** * Called when the actor is started and ready to receive connections and action. * @@ -497,6 +523,7 @@ export type ActorConfig< | "actions" | "onCreate" | "onDestroy" + | "run" | "onWake" | "onStateChange" | "onBeforeConnect" @@ -557,6 +584,7 @@ export type ActorConfigInput< | "actions" | "onCreate" | "onDestroy" + | "run" | "onWake" | "onSleep" | "onStateChange" diff --git a/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts b/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts index 7960aec9a5..debf2fd4a9 100644 --- a/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts +++ b/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts @@ -852,8 +852,10 @@ export class ActorInstance { async #callOnStart() { this.#rLog.info({ msg: "actor starting" }); - if (this.#config.onWake) { - const result = this.#config.onWake(this.actorContext); + // `run` is an alias for `onWake` + const onWakeHandler = this.#config.run ?? this.#config.onWake; + if (onWakeHandler) { + const result = onWakeHandler(this.actorContext); if (result instanceof Promise) { await result; } diff --git a/website/src/content/docs/actors/lifecycle.mdx b/website/src/content/docs/actors/lifecycle.mdx index 72142d8933..4582dd558a 100644 --- a/website/src/content/docs/actors/lifecycle.mdx +++ b/website/src/content/docs/actors/lifecycle.mdx @@ -1,8 +1,6 @@ ---- -title: "Lifecycle" -description: "Actors follow a well-defined lifecycle with hooks at each stage. Understanding these hooks is essential for proper initialization, state management, and cleanup." -skill: true ---- +# Lifecycle + +Actors follow a well-defined lifecycle with hooks at each stage. Understanding these hooks is essential for proper initialization, state management, and cleanup. ## Lifecycle @@ -13,7 +11,7 @@ Actors transition through several states during their lifetime. Each transition 1. `createState` 2. `onCreate` 3. `createVars` -4. `onWake` +4. `run` **On Destroy** @@ -22,7 +20,7 @@ Actors transition through several states during their lifetime. Each transition **On Wake** (after sleep, restart, or crash) 1. `createVars` -2. `onWake` +2. `run` **On Sleep** (after idle period) @@ -147,7 +145,7 @@ const gameSession = actor({ }); ``` -### `onWake` +### `run` [API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) @@ -164,7 +162,7 @@ const counter = actor({ state: { count: 0 }, vars: { intervalId: null as NodeJS.Timeout | null }, - onWake: (c) => { + run: (c) => { console.log('Actor started with count:', c.state.count); // Set up interval for automatic counting @@ -189,6 +187,10 @@ const counter = actor({ }); ``` +### `onWake` + +`onWake` is an alias for [`run`](#run). Use either `run` or `onWake`, but not both. + ### `onSleep` [API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) @@ -206,7 +208,7 @@ const counter = actor({ state: { count: 0 }, vars: { intervalId: null as NodeJS.Timeout | null }, - onWake: (c) => { + run: (c) => { // Set up interval when actor wakes c.vars.intervalId = setInterval(() => { c.state.count++; @@ -278,35 +280,17 @@ The `onBeforeConnect` hook does NOT return connection state - it's used solely f ```typescript import { actor } from "rivetkit"; -// Helper function to validate tokens -function validateToken(token: string): boolean { - return token.startsWith("valid_"); -} - -interface ConnParams { - authToken?: string; - userId?: string; - role?: string; -} - -interface ConnState { - userId: string; - role: string; - joinTime: number; -} - const chatRoom = actor({ - state: { messages: [] as string[] }, + state: { messages: [] }, // Method 1: Use a static default connection state connState: { - userId: "anonymous", role: "guest", joinTime: 0, - } as ConnState, + }, // Method 2: Dynamically create connection state - createConnState: (c, params: ConnParams): ConnState => { + createConnState: (c, params: { userId?: string, role?: string }) => { return { userId: params.userId || "anonymous", role: params.role || "guest", @@ -315,7 +299,7 @@ const chatRoom = actor({ }, // Validate connections before accepting them - onBeforeConnect: (c, params: ConnParams) => { + onBeforeConnect: (c, params: { authToken?: string }) => { // Validate authentication const authToken = params.authToken; if (!authToken || !validateToken(authToken)) { @@ -326,7 +310,7 @@ const chatRoom = actor({ // The actual connection state will come from connState or createConnState }, - actions: {} + actions: { /* ... */ } }); ``` @@ -341,18 +325,8 @@ Executed after the client has successfully connected. Can be async. Receives the ```typescript import { actor } from "rivetkit"; -interface ConnState { - userId: string; -} - -interface UserInfo { - online: boolean; - lastSeen: number; -} - const chatRoom = actor({ - state: { users: {} as Record, messages: [] as string[] }, - connState: { userId: "" } as ConnState, + state: { users: {}, messages: [] }, onConnect: (c, conn) => { // Add user to the room's user list using connection state @@ -368,7 +342,7 @@ const chatRoom = actor({ console.log(`User ${userId} connected`); }, - actions: {} + actions: { /* ... */ } }); ``` @@ -383,18 +357,8 @@ Called when a client disconnects from the actor. Can be async. Receives the conn ```typescript import { actor } from "rivetkit"; -interface ConnState { - userId: string; -} - -interface UserInfo { - online: boolean; - lastSeen: number; -} - const chatRoom = actor({ - state: { users: {} as Record, messages: [] as string[] }, - connState: { userId: "" } as ConnState, + state: { users: {}, messages: [] }, onDisconnect: (c, conn) => { // Update user status when they disconnect @@ -410,7 +374,7 @@ const chatRoom = actor({ console.log(`User ${userId} disconnected`); }, - actions: {} + actions: { /* ... */ } }); ``` @@ -441,11 +405,11 @@ const apiActor = actor({ }); } - // Return a default response for unhandled paths - return new Response("Not Found", { status: 404 }); + // Return void to continue to default routing + return; }, - actions: {} + actions: { /* ... */ } }); ``` @@ -506,27 +470,37 @@ import { actor } from "rivetkit"; const loggingActor = actor({ state: { requestCount: 0 }, - onBeforeActionResponse: (c: unknown, actionName: string, args: unknown[], output: Out): Out => { + onBeforeActionResponse: (c, actionName, args, output) => { // Log action calls console.log(`Action ${actionName} called with args:`, args); console.log(`Action ${actionName} returned:`, output); - // Return the output unchanged (or transform as needed) - return output; + // Add metadata to all responses + return { + data: output, + metadata: { + actionName, + timestamp: Date.now(), + requestId: crypto.randomUUID(), + userId: c.conn.state?.userId + } + }; }, - + actions: { getUserData: (c, userId: string) => { c.state.requestCount++; - + + // This will be wrapped with metadata by onBeforeActionResponse return { userId, profile: { name: "John Doe", email: "john@example.com" }, lastActive: Date.now() }; }, - + getStats: (c) => { + // This will also be wrapped with metadata return { requestCount: c.state.requestCount, uptime: process.uptime() @@ -621,32 +595,25 @@ Common use cases: ```typescript import { actor } from "rivetkit"; -interface PlayerInfo { - joinedAt: number; -} - const gameRoom = actor({ - state: { - players: {} as Record, - scores: {} as Record - }, - + state: { players: {}, scores: {} }, + actions: { playerJoined: (c, playerId: string) => { c.state.players[playerId] = { joinedAt: Date.now() }; - - // Send analytics event without blocking using waitUntil - c.waitUntil( + + // Send analytics event without blocking + c.runInBackground( fetch('https://analytics.example.com/events', { method: 'POST', - body: JSON.stringify({ - event: 'player_joined', + body: JSON.stringify({ + event: 'player_joined', playerId, - timestamp: Date.now() + timestamp: Date.now() }) }).then(() => console.log('Analytics sent')) ); - + return { success: true }; }, } @@ -684,25 +651,17 @@ When extracting logic from lifecycle hooks or actions into external functions, y ```typescript import { actor, ActorContextOf } from "rivetkit"; -// Define the actor first const myActor = actor({ state: { count: 0 }, - actions: {} + + // Use external function in lifecycle hook + run: (c) => logActorStarted(c) }); -// Then define functions using the actor's context type +// Simple external function with typed context function logActorStarted(c: ActorContextOf) { console.log(`Actor started with count: ${c.state.count}`); } - -// Usage example: call the function from inside the actor -const myActorWithHook = actor({ - state: { count: 0 }, - onWake: (c) => { - console.log(`Actor woke up with count: ${c.state.count}`); - }, - actions: {} -}); ``` See [Types](/docs/actors/types) for more details on using `ActorContextOf`. @@ -710,7 +669,7 @@ See [Types](/docs/actors/types) for more details on using `ActorContextOf`. ## Full Example ```typescript -import { actor } from "rivetkit"; +import { actor, CreateContext } from "rivetkit"; interface CounterInput { initialCount?: number; @@ -737,23 +696,8 @@ interface ConnState { } const counter = actor({ - // Default state (needed for type inference) - state: { - count: 0, - stepSize: 1, - name: "Unnamed Counter", - requestCount: 0, - } as CounterState, - - // Default connection state (needed for type inference) - connState: { - userId: "", - role: "", - connectedAt: 0, - } as ConnState, - // Initialize state with input - createState: (c, input: CounterInput): CounterState => ({ + createState: (c: CreateContext, input: CounterInput): CounterState => ({ count: input.initialCount ?? 0, stepSize: input.stepSize ?? 1, name: input.name ?? "Unnamed Counter", @@ -776,7 +720,7 @@ const counter = actor({ }, // Lifecycle hooks - onWake: (c) => { + run: (c) => { console.log(`Counter "${c.state.name}" started with count:`, c.state.count); }, @@ -804,10 +748,17 @@ const counter = actor({ }, // Transform all action responses - onBeforeActionResponse: (c: unknown, actionName: string, args: unknown[], output: Out): Out => { - // Log action calls - console.log(`Action ${actionName} called`); - return output; + onBeforeActionResponse: (c, actionName, args, output) => { + c.state.requestCount++; + + return { + data: output, + metadata: { + action: actionName, + timestamp: Date.now(), + requestNumber: c.state.requestCount + } + }; }, // Define actions