# JavaScript & TypeScript Source: https://actorcore.org/clients/javascript The ActorCore JavaScript client allows you to connect to and interact with actors from browser and Node.js applications. ## Installation Install the ActorCore client package: ```sh npm npm add actor-core ``` ```sh pnpm pnpm add actor-core ``` ```sh yarn yarn add actor-core ``` ```sh bun bun add actor-core ``` ## Connecting to an Actor ```typescript import { createClient } from "actor-core/client"; import type { App } from "./app"; // Import type of ActorCore app for end-to-end type safety // Create a client with the connection address const client = createClient("http://localhost:6420"); // Connect to an actor const counter = await client.counter.get({ id: "counter-1" }); // Listen for events counter.on("newCount", (count) => { console.log(`Count updated: ${count}`); }); // Call an action on the actor const newCount = await counter.increment(5); console.log(`New count: ${newCount}`); ``` ## Next Steps See the [Interacting with Actors](/concepts/interacting-with-actors) documentation for information on how to use the client. # Rust Source: https://actorcore.org/clients/rust The ActorCore Rust client provides a way to connect to and interact with actors from Rust applications. ## Installation Install the `actor-core-client` package with: ```sh cargo add actor-core-client serde-json ``` Make sure you have [Tokio](https://tokio.rs/) set up as your async runtime. ## Connecting to an Actor ```rust use actor_core_client::{Client, GetOptions, TransportKind, EncodingKind}; use serde_json::json; #[tokio::main] async fn main() -> Result<(), Box> { // Create a client with connection address and configuration let client = Client::new( "http://localhost:6420".to_string(), // Connection address TransportKind::WebSocket, // Transport type EncodingKind::Cbor, // Encoding format ); // Connect to an actor let tags = vec![ ("id".to_string(), "counter-1".to_string()), ]; let options = GetOptions { tags: Some(tags), params: None, no_create: None, create: None, }; let counter = client.get("counter", options) .await?; // Call an action on the actor let result = counter.action("increment", vec![json!(5)]) .await?; let new_count = result.as_i64().unwrap(); println!("New count: {}", new_count); // Listen for events counter.on_event("newCount", move |args| { let count = args[0].as_i64().unwrap(); println!("Count updated: {}", count); }).await; Ok(()) } ``` ## Next Steps See the [Interacting with Actors](/concepts/interacting-with-actors) documentation for information on how to use the client. # Actions Source: https://actorcore.org/concepts/actions Actions are how clients & other actors communicate with actors. Actions are defined as functions in the actor configuration and can be called from clients. **Performance** Actions are very lightweight. They can be called hundreds of times per second to send realtime data to the actor. ## Writing Actions Actions are defined in the `actions` object when creating an actor: ```typescript import { actor } from "actor-core"; const mathUtils = actor({ state: {}, actions: { // This is an action multiplyByTwo: (c, x) => { return x * 2; } } }); ``` Each action receives a context object (commonly named `c`) as its first parameter, which provides access to state, connections, and other utilities. Additional parameters follow after that. ### Private Helper Functions You can define helper functions outside the actions object to keep your code organized. These functions cannot be called directly by clients: ```typescript import { actor } from "actor-core"; // Private helper function - not callable by clients const calculateFee = (amount) => { return amount * 0.05; }; const paymentProcessor = actor({ state: { transactions: [] }, actions: { // Public action - callable by clients processPayment: (c, amount) => { const fee = calculateFee(amount); // Process payment logic... c.state.transactions.push({ amount, fee }); return { amount, fee }; } } }); ``` ### Streaming Return Data Actions have a single return value. To stream realtime data in response to an action, use [events](/concepts/events). ## Calling Actions Calling actions from the client is simple: ```typescript import { createClient } from "actor-core/client"; import type { App } from "./src/index"; const client = createClient("http://localhost:6420"); const counter = await client.counter.get(); const result = await counter.increment(42); console.log(result); // The value returned by the action ``` Calling actions from the client are async and require an `await`, even if the action itself is not async. ### Type Safety The actor client includes type safety out of the box. When you use `createClient()`, TypeScript automatically infers action parameter and return types: ```typescript src/index.ts import { setup } from "actor-core"; // Create simple counter const counter = actor({ state: { count: 0 }, actions: { increment: (c, count: number) => { c.state.count += count; return c.state.count; } } }); // Create and export the app const app = setup({ actors: { counter } }); export type App = typeof app; ``` ```typescript client.ts import { createClient } from "actor-core/client"; import type { App } from "./src/index"; const client = createClient("http://localhost:6420"); // Type-safe client usage const counter = await client.counter.get(); await counter.increment(123); // OK await counter.increment("non-number type"); // TypeScript error await counter.nonexistentMethod(123); // TypeScript error ``` ## Error Handling Actors provide robust error handling out of the box for actions. ### User Errors `UserError` can be used to return rich error data to the client. You can provide: * A human-readable message * A machine-readable code that's useful for matching errors in a try-catch (optional) * A metadata object for providing richer error context (optional) For example: ```typescript actor.ts import { actor, UserError } from "actor-core"; const user = actor({ state: { users: [] }, actions: { registerUser: (c, username) => { // Validate username if (username.length > 32) { // Throw a simple error with a message throw new UserError("Invalid username", { code: "invalid_username", meta: { maxLength: 32 } }); } // Rest of the user registration logic... } } }); ``` ```typescript client.ts try { await userActor.registerUser("extremely_long_username_that_exceeds_limit"); } catch (error) { console.log("Message", error.message); // "Invalid username" console.log("Code", error.code); // "invalid_username" console.log("Metadata", error.metadata); // { maxLength; 32 } } ``` {/* Read the documentation for `UserError` [here](https://jsr.io/@rivet-gg/actor/doc/~/UserError). */} ### Internal Errors All other errors will return an error with the code `internal_error` to the client. This helps keep your application secure, as errors can sometimes expose sensitive information. ## Schema Validation Data schemas are not validated by default. For production applications, use a library like [zod](https://zod.dev/) to validate input types. For example, to validate action parameters: ```typescript import { actor, UserError } from "actor-core"; import { z } from "zod"; // Define schema for action parameters const IncrementSchema = z.object({ count: z.number().int().positive() }); const counter = actor({ state: { count: 0 }, actions: { increment: (c, params) => { // Validate parameters try { const { count } = IncrementSchema.parse(params); c.state.count += count; return c.state.count; } catch (err) { throw new UserError("Invalid parameters", { code: "invalid_params", meta: { errors: err.errors } }); } } } }); ``` Native runtime type validation is coming soon to ActorCore. ## Authentication By default, clients can call all actions on an actor without restriction. Make sure to implement authentication if needed. Documentation on authentication is available [here](/concepts/authentication). ## Using `ActionContext` Type Externally When writing complex logic for actions, you may want to extract parts of your implementation into separate helper functions. When doing this, you'll need a way to properly type the context parameter. ActorCore provides the `ActionContextOf` utility type for exactly this purpose: ```typescript import { actor, ActionContextOf } from "actor-core"; const counter = actor({ state: { count: 0 }, actions: { increment: (c) => { return incrementCount(c); } } }); // Simple helper function with typed context function incrementCount(c: ActionContextOf) { c.state.count += 1; return c.state.count; } ``` See [Helper Types](/concepts/types) for more details on using `ActionContextOf` and other type utilities. # Authentication Source: https://actorcore.org/concepts/authentication Authentication can be handled through the `onBeforeConnect` or `createConnState` lifecycle hook, which acts as middleware before allowing clients to interact with your actor. ## Using `onBeforeConnect` or `createConnState` The `onBeforeConnect` and `createConnState` hook is called whenever a new client attempts to connect to your actor. It receives a context object that contains the client's connection parameters. `createConnState` should return an object that will become the connection state. Throwing an error in `onBeforeConnect` or `createConnState` will abort the connection. Here's a basic example: ```typescript import { actor } from "actor-core"; const exampleActor = actor({ state: { // Actor state... }, createConnState: async (c, { params }) => { // Verify the token with your authentication system const userData = await myValidateAuthToken(params.authToken); if (!userData) { throw new Error('Invalid auth token'); } // Return the user data to store with the connection return { userId: userData.id, role: userData.role }; }, actions: { // Actor actions... } }); ``` ## Accessing Connection State After authentication, you can access the connection state in any action through the context object: ```typescript import { actor } from "actor-core"; const authenticatedActor = actor({ state: { // Actor state... }, createConnState: (c) => { // Authentication logic... return { userId: "user_123", role: "admin" }; }, actions: { exampleAdminCommand: (c) => { // Example of validating admin access if (c.conn.state.role !== 'admin') { throw new Error('User must be an admin'); } // Admin-only functionality... return { success: true }; } } }); ``` ## Integration Examples ### With API Server Authentication ```typescript import { actor } from "actor-core"; const apiAuthenticatedActor = actor({ state: { // Actor state... }, createConnState: async (c, { params }) => { // Validate API key with your server const response = await fetch('https://api.yourserver.com/validate', { method: 'POST', headers: { Authorization: `Bearer ${params.apiKey}` } }); if (!response.ok) { throw new Error('Invalid API key'); } const user = await response.json(); return { userId: user.id }; }, actions: { // Actor actions... } }); ``` When authentication fails, throwing an error in `createConnState` will prevent the connection from being established, and the client will receive the error message. ### With JWT Authentication ```typescript import { actor } from "actor-core"; import jwt from "jsonwebtoken"; const JWT_SECRET = process.env.JWT_SECRET; const jwtAuthenticatedActor = actor({ state: { // Actor state... }, createConnState: (c, { params }) => { try { // Verify JWT token const decoded = jwt.verify(params.jwt, JWT_SECRET); return { userId: decoded.sub, permissions: decoded.permissions }; } catch (error) { throw new Error('Invalid or expired JWT token'); } }, actions: { secureAction: (c, data) => { // Check permissions before proceeding if (!c.conn.state.permissions.includes('write')) { throw new Error('Permission denied: requires write access'); } // Perform action with data... return { success: true }; } } }); ``` # Connections Source: https://actorcore.org/concepts/connections Connections represent client connections to your actor. They provide a way to handle client authentication, manage connection-specific data, and control the connection lifecycle. ## Parameters When clients connect to an actor, they can pass connection parameters that are handled during the connection process. For example: ```typescript actor.ts import { actor } from "actor-core"; const gameRoom = actor({ state: {}, // Handle connection setup createConnState: (c, { params }) => { // Validate authentication token const authToken = params.authToken; if (!authToken || !validateToken(authToken)) { throw new Error("Invalid auth token"); } // Create connection state return { userId: getUserIdFromToken(authToken), role: "player" }; }, actions: { // ... } }); ``` ```typescript client.ts import { createClient } from "actor-core/client"; import type { App } from "./src/index"; const client = createClient("http://localhost:6420"); const gameRoom = await client.gameRoom.get({ params: { authToken: "supersekure" } }); ``` ## Connection State There are two ways to define an actor's connection state: ### Method 1: `ConnState` constant ```typescript import { actor } from "actor-core"; const chatRoom = actor({ state: { messages: [] }, // Define default connection state as a constant connState: { role: "guest", joinedAt: 0 }, onConnect: (c) => { // Update join timestamp when a client connects c.conn.state.joinedAt = Date.now(); }, actions: { // ... } }); ``` ### Method 2: `createConnState` function The data returned from `createConnState` is used as the initial state of the connection. The connection state can be accessed through `conn.state`. ```typescript import { actor } from "actor-core"; const chatRoom = actor({ state: { messages: [] }, // Create connection state dynamically createConnState: (c) => { // Validate any connection parameters // ... // Return the connection state return { userId: generateUserId(), role: "guest", joinedAt: Date.now() }; }, actions: { sendMessage: (c, message) => { const username = c.conn.state.userId; c.state.messages.push({ username, message }); c.broadcast("newMessage", { username, message }); } } }); ``` ## Lifecycle Hooks The connection lifecycle has several hooks: * `onBeforeConnect`: Called before a client connects, returns the connection state * `onConnect`: Called when a client successfully connects * `onDisconnect`: Called when a client disconnects See the documentation on [Actor Lifecycle](/concepts/lifecycle) for more details. ## Connection List All active connections can be accessed through the context object's `conns` property. This is an array of all current connections. This is frequently used with `conn.send(name, event)` to send messages directly to clients. For example: ```typescript import { actor } from "actor-core"; const chatRoom = actor({ state: { users: {} }, actions: { sendDirectMessage: (c, recipientId, message) => { // Find the recipient's connection const recipientConn = c.conns.find(conn => conn.state.userId === recipientId); if (recipientConn) { // Send a private message to just that client recipientConn.send('directMessage', { from: c.conn.state.userId, message: message }); } } } }); ``` ## Disconnecting clients Connections can be disconnected from within an action: ```typescript import { actor } from "actor-core"; const secureRoom = actor({ state: {}, actions: { kickUser: (c, targetUserId, reason) => { // Find the connection to kick const targetConn = c.conns.find(conn => conn.state.userId === targetUserId); if (targetConn) { // Disconnect with a reason targetConn.disconnect(reason || "Kicked by admin"); } } } }); ``` If you need to wait for the disconnection to complete, you can use `await`: ```typescript await c.conn.disconnect('Too many requests'); ``` This ensures the underlying network connections close cleanly before continuing. ## Offline & Auto-Reconnection See [Interacting with Actors](/concepts/interacting-with-actors#offline-and-auto-reconnection) for details on reconnection behavior. # Cross-Origin Resource Sharing Source: https://actorcore.org/concepts/cors Cross-Origin Resource Sharing (CORS) is a security mechanism that allows a web application running at one origin to access resources from a different origin. Without CORS, browsers block cross-origin HTTP requests by default as a security measure. You'll need to configure CORS when: * **Local Development:** You're developing locally and your client runs on a different port than your actor service * **Different Domain:** Your frontend application is hosted on a different domain than your actor service ## Example ```ts import { setup } from "actor-core"; import counter from "./counter"; const app = setup({ actors: { counter }, // Change this to match your frontend's origin cors: { origin: "https://yourdomain.com" } }); ``` ## Options ### `origin` `string | string[] | (origin:string, c:Context) => string` (optional) Specifies which domains can access your resources. Options: * Single domain: `"https://example.com"` * Multiple domains: `["https://app.com", "https://admin.com"]` * Dynamic validation: `(origin) => origin.endsWith('.example.com') ? origin : null` * All domains (not recommended for production): `"*"` (default) ### `allowMethods` `string[]` (optional) HTTP methods clients are allowed to use when accessing your resources. Default includes all standard methods: `['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH']`. ### `allowHeaders` `string[]` (optional) Custom HTTP headers clients can send in requests. Empty by default `[]`. Common examples: * `["Content-Type", "Authorization", "X-API-Key"]` ### `maxAge` `number` (optional) How long browsers should cache CORS response (in seconds). Higher values improve performance by reducing preflight requests. ### `credentials` `boolean` (optional) Whether requests can include user credentials like cookies or HTTP authentication. Must specify exact origins when enabled (cannot use with `origin: "*"`). ### `exposeHeaders` `string[]` (optional) Server headers that browsers are allowed to access. Empty by default `[]`. Example: * `["Content-Length", "X-Request-Id"]` # Events Source: https://actorcore.org/concepts/events Events are used for clients to receive realtime data from actors. Events are used for actors to publish updates to clients. Clients call actions to communicate with the actor. ## Publishing from actors Actors can publish events to clients using `c.broadcast` and `conn.send`. ### Broadcasting events Actors can publish events to all connected clients with `c.broadcast(name, data)`. For example: ```typescript chat_room.ts import { actor } from "actor-core"; const chatRoom = actor({ state: {}, actions: { sendMessage: (c, message) => { c.broadcast('newMessage', { message }); } } }); ``` ```typescript client.ts import { createClient } from "actor-core/client"; import type { App } from "./src/index"; const client = createClient("http://localhost:6420"); const chatRoom = await client.chatRoom.get(); await chatRoom.sendMessage('Hello, world!'); ``` ### Sending events to specific connections Actors can send messages to specific client connections. All connections are available through the context object. For example: ```typescript chat_room.ts import { actor } from "actor-core"; const chatRoom = actor({ state: {}, actions: { sendPrivateMessage: (c, connId, message) => { const conn = c.conns.find(conn => conn.id === connId); if (conn) { conn.send('newMessage', { message }); } } } }); ``` ```typescript client.ts import { createClient } from "actor-core/client"; import type { App } from "./src/index"; const client = createClient("http://localhost:6420"); const chatRoom = await client.chatRoom.get(); await chatRoom.sendPrivateMessage(123, 'Hello, world!'); ``` ## Subscribing from clients Clients can subscribe to events from actors using `on` and `once`. ### `on(eventName, callback)` {/* [Documentation](https://jsr.io/@rivet-gg/actor-client/doc/~/ActorHandleRaw.prototype.on.html) */} Clients can subscribe to events that will happen repeatedly using `actor.on(name, callback)`. For example: ```typescript client.ts import { createClient } from "actor-core/client"; import type { App } from "./src/index"; const client = createClient("http://localhost:6420"); const chatRoom = await client.chatRoom.get(); chatRoom.on('newMessage', ({ message }) => { console.log('Message', message); }); ``` ```typescript chat_room.ts import { actor } from "actor-core"; const chatRoom = actor({ state: {}, actions: { sendMessage: (c, message) => { c.broadcast('newMessage', { message }); } } }); ``` ### `once(eventName, callback)` {/* [Documentation](https://jsr.io/@rivet-gg/actor-client/doc/~/ActorHandleRaw.prototype.once.html) */} Clients can listen for an event only one time with `actor.once(name, callback)`. For example: ```typescript client.ts import { createClient } from "actor-core/client"; import type { App } from "./src/index"; const client = createClient("http://localhost:6420"); const chatRoom = await client.chatRoom.get(); chatRoom.once('joinRequestApproved', () => { // This will only be called once console.log('Join request accepted'); }); await chatRoom.requestJoin(); ``` ```typescript chat_room.ts import { actor } from "actor-core"; const chatRoom = actor({ state: { pendingJoinRequests: [] }, actions: { requestJoin: (c) => { // ...add to pending requests... }, approveJoinRequest: (c, connId) => { const conn = c.conns.find(conn => conn.id === connId); if (conn) { conn.send('joinRequestApproved'); } } } }); ``` ## Connections Connections are used to communicate with clients from the actor. Read more about connections [here](/concepts/connections). # External SQL Database Source: https://actorcore.org/concepts/external-sql While actors can serve as a complete database solution, they can also complement your existing databases. For example, you might use actors to handle frequently-changing data that needs real-time access, while keeping less frequently accessed data in your traditional database. Actors can be used with common SQL databases, such as PostgreSQL and MySQL. ## Libraries To facilitate interaction with SQL databases, you can use either ORM libraries or raw SQL drivers. Each has its own use cases and benefits: * **ORM Libraries**: Type-safe and easy way to interact with your database * [Drizzle](https://orm.drizzle.team/) * [Prisma](https://www.prisma.io/) * **Raw SQL Drivers**: Direct access to the database for more flexibility * [PostgreSQL](https://node-postgres.com/) * [MySQL](https://github.com/mysqljs/mysql) ## Hosting Providers There are several options for places to host your SQL database: * [Supabase](https://supabase.com/) * [Neon](https://neon.tech/) * [PlanetScale](https://planetscale.com/) * [AWS RDS](https://aws.amazon.com/rds/) * [Google Cloud SQL](https://cloud.google.com/sql) ## Example Here's a basic example of how you might set up a connection to a PostgreSQL database using the `pg` library: ```typescript actor.ts import { actor } from "actor-core"; import { Pool } from "pg"; // Create a database connection pool const pool = new Pool({ user: "your_db_user", host: "localhost", database: "your_db_name", password: "your_db_password", port: 5432, }); // Create the actor const databaseActor = actor({ state: { // Local state if needed lastQueryTime: 0 }, // Initialize any resources onStart: (c) => { console.log("Database actor started"); }, // Clean up resources if needed onShutdown: async (c) => { await pool.end(); console.log("Database connections closed"); }, // Define actions actions: { // Example action to fetch data from database fetchData: async (c) => { try { const result = await pool.query("SELECT * FROM your_table"); c.state.lastQueryTime = Date.now(); return result.rows; } catch (error) { console.error("Error fetching data:", error); throw new Error("Failed to fetch data"); } }, // Example action to insert data into database insertData: async (c, data) => { try { await pool.query( "INSERT INTO your_table (column1, column2) VALUES ($1, $2)", [data.value1, data.value2] ); c.state.lastQueryTime = Date.now(); return { success: true }; } catch (error) { console.error("Error inserting data:", error); throw new Error("Failed to insert data"); } } } }); export default databaseActor; ``` ## With Drizzle ORM Here's an example using Drizzle ORM for more type-safe database operations: ```typescript actor.ts import { actor } from "actor-core"; import { drizzle } from "drizzle-orm/node-postgres"; import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core"; import { Pool } from "pg"; // Define your schema const users = pgTable("users", { id: serial("id").primaryKey(), name: text("name").notNull(), email: text("email").notNull().unique(), createdAt: timestamp("created_at").defaultNow() }); // Create a database connection const pool = new Pool({ connectionString: process.env.DATABASE_URL }); // Initialize Drizzle with the pool const db = drizzle(pool); // Create the actor const userActor = actor({ state: { // Actor state (frequently accessed data can be cached here) userCache: {} }, actions: { // Get a user by ID getUser: async (c, userId) => { // Check cache first if (c.state.userCache[userId]) { return c.state.userCache[userId]; } // Query the database const result = await db.select().from(users).where(eq(users.id, userId)); if (result.length === 0) { throw new Error(`User ${userId} not found`); } // Cache the result c.state.userCache[userId] = result[0]; return result[0]; }, // Create a new user createUser: async (c, userData) => { const result = await db.insert(users).values({ name: userData.name, email: userData.email }).returning(); // Broadcast the new user event c.broadcast("userCreated", result[0]); return result[0]; } } }); export default userActor; ``` # Interacting with Actors Source: https://actorcore.org/concepts/interacting-with-actors This guide covers how to connect to and interact with actors from client applications. ## Setting Up the Client The first step is to create a client that will connect to your actor service: ```typescript TypeScript import { createClient } from "actor-core/client"; import type { App } from "../src/index"; // Create a client with the connection address and app type const client = createClient(/* CONNECTION ADDRESS */); ``` ```rust Rust use actor_core_client::{Client, TransportKind, EncodingKind}; // Create a client with connection address and configuration let client = Client::new( "http://localhost:6420".to_string(), // Connection address TransportKind::WebSocket, // Transport (WebSocket or SSE) EncodingKind::Cbor, // Encoding (Json or Cbor) ); ``` See the setup guide for your platform for details on how to get the connection address. ## Finding & Connecting to Actors ActorCore provides several methods to connect to actors: ### `get(tags, opts)` - Find or Create The most common way to connect is with `get()`, which finds an existing actor matching the provided tags or creates a new one: ```typescript TypeScript // Connect to a chat room for the "general" channel const room = await client.chatRoom.get({ name: "chat_room", channel: "general" }); // Now you can call methods on the actor await room.sendMessage("Alice", "Hello everyone!"); ``` ```rust Rust use actor_core_client::GetOptions; use serde_json::json; // Connect to a chat room for the "general" channel let tags = vec![ ("name".to_string(), "chat_room".to_string()), ("channel".to_string(), "general".to_string()), ]; let mut options = GetOptions { tags: Some(tags), ..Default::default() }; let room = client.get("chatRoom", options) .await .expect("Failed to connect to chat room"); // Now you can call methods on the actor room.action("sendMessage", vec![json!("Alice"), json!("Hello everyone!")]) .await .expect("Failed to send message"); ``` ### `create(opts)` - Explicitly Create New When you specifically want to create a new actor instance: ```typescript TypeScript // Create a new document actor const doc = await client.myDocument.create({ create: { tags: { name: "my_document", docId: "123" } } }); await doc.initializeDocument("My New Document"); ``` ```rust Rust use actor_core_client::{CreateOptions}; use actor_core_client::client::CreateRequestMetadata; use serde_json::json; // Create a new document actor let tags = vec![ ("name".to_string(), "my_document".to_string()), ("docId".to_string(), "123".to_string()), ]; let create_options = CreateOptions { params: None, create: CreateRequestMetadata { tags, region: None, }, }; let doc = client.create("myDocument", create_options) .await .expect("Failed to create document"); // Initialize the document doc.action("initializeDocument", vec![json!("My New Document")]) .await .expect("Failed to initialize document"); ``` ### `getWithId(id, opts)` - Connect by ID Connect to an actor using its internal ID: ```typescript TypeScript // Connect to a specific actor by its ID const myActorId = "55425f42-82f8-451f-82c1-6227c83c9372"; const doc = await client.myDocument.getWithId(myActorId); await doc.updateContent("Updated content"); ``` ```rust Rust use actor_core_client::GetWithIdOptions; // Connect to a specific actor by its ID let my_actor_id = "55425f42-82f8-451f-82c1-6227c83c9372"; let options = GetWithIdOptions { params: None, }; let doc = client.get_with_id("myDocument", my_actor_id, options) .await .expect("Failed to connect to document"); // Update content doc.action("updateContent", vec![json!("Updated content")]) .await .expect("Failed to update document"); ``` It's usually better to use tags for discovery rather than directly using actor IDs. ## Calling Actions Once connected, calling actor actions are straightforward: ```typescript TypeScript // Call an action const result = await mathUtils.multiplyByTwo(5); console.log(result); // 10 // Call an action with multiple parameters await chatRoom.sendMessage("Alice", "Hello everyone!"); // Call an action with an object parameter await gameRoom.updateSettings({ maxPlayers: 10, timeLimit: 300, gameMode: "capture-the-flag" }); ``` ```rust Rust use serde_json::json; // Call an action let result = math_utils.action("multiplyByTwo", vec![json!(5)]) .await .expect("Failed to call multiplyByTwo"); println!("Result: {}", result.as_i64().unwrap()); // 10 // Call an action with multiple parameters chat_room.action("sendMessage", vec![json!("Alice"), json!("Hello everyone!")]) .await .expect("Failed to send message"); // Call an action with an object parameter let settings = json!({ "maxPlayers": 10, "timeLimit": 300, "gameMode": "capture-the-flag" }); game_room.action("updateSettings", vec![settings]) .await .expect("Failed to update settings"); ``` All actor action calls are asynchronous and require `await`, even if the actor's action is not async. ## Listening for Events Actors can send realtime updates to clients using events: ### `on(eventName, callback)` - Continuous Listening To listen for events that will happen repeatedly: ```typescript TypeScript // Listen for new chat messages chatRoom.on("newMessage", ({ sender, text, timestamp }) => { console.log(`${sender}: ${text}`); updateChatUI(sender, text, timestamp); }); // Listen for game state updates gameRoom.on("stateUpdate", (gameState) => { updateGameUI(gameState); }); ``` ```rust Rust use std::sync::Arc; // Listen for new chat messages chat_room.on_event("newMessage", move |args| { let message = &args[0]; let sender = message["sender"].as_str().unwrap(); let text = message["text"].as_str().unwrap(); println!("{}: {}", sender, text); // Update UI with message data }).await; // Listen for game state updates let game_ui = Arc::new(GameUI::new()); let game_ui_clone = game_ui.clone(); game_room.on_event("stateUpdate", move |args| { let game_state = &args[0]; game_ui_clone.update(game_state); }).await; ``` ### `once(eventName, callback)` - One-time Listening For events you only need to hear once: ```typescript TypeScript // Listen for when a request is approved actor.once("requestApproved", () => { showApprovalNotification(); unlockFeatures(); }); ``` ```rust Rust // `once` is not implemented in Rust ``` ## Connection Options ### Authentication and Connection Parameters Pass information with your connection using the `params` option: ```typescript TypeScript const chatRoom = await client.chatRoom.get({ channel: "super-secret" }, { params: { userId: "1234", authToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", displayName: "Alice" } }); ``` ```rust Rust use serde_json::json; use actor_core_client::GetOptions; let tags = vec![ ("channel".to_string(), "super-secret".to_string()), ]; let params = json!({ "userId": "1234", "authToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "displayName": "Alice" }); let options = GetOptions { tags: Some(tags), params: Some(params), no_create: None, create: None, }; let chat_room = client.get("chatRoom", options) .await .expect("Failed to connect to chat room"); ``` The actor can access these parameters in the `onBeforeConnect` or `createConnState` hook: ```typescript import { actor } from "actor-core"; const chatRoom = actor({ state: { messages: [] }, createConnState: (c, { params }) => { // Validate authentication token const { userId, authToken, displayName } = params; if (!validateToken(userId, authToken)) { throw new Error("Invalid authentication"); } // Return connection state return { userId, displayName }; }, actions: { // ... } }); ``` Read more about [connection parameters](/concepts/connections). ### Additional Options #### `opts.noCreate` Connect only if an actor exists, without creating a new one: ```typescript try { const doc = await client.document.get({ documentId: "doc-123" }, { noCreate: true }); await doc.doSomething(); } catch (error) { console.log("Document doesn't exist"); } ``` ## Client Options ```typescript TypeScript // Example with all client options const client = createClient( "https://actors.example.com", { // Data serialization format encoding: "cbor", // or "json" // Network transports in order of preference supportedTransports: ["websocket", "sse"] } ); ``` ```rust Rust use actor_core_client::{Client, TransportKind, EncodingKind}; // Create client with specific options let client = Client::new( "https://actors.example.com".to_string(), TransportKind::WebSocket, // or TransportKind::Sse EncodingKind::Cbor, // or EncodingKind::Json ); // Rust does not support accepting multiple transports ``` ### `encoding` `"cbor" | "json"` (optional) Specifies the data encoding format used for communication: * `"cbor"` (default): Binary format that's more efficient for data transfer * `"json"`: Text-based format with wider compatibility across environments ### `supportedTransports` `("websocket" | "sse")[]` (optional) Configures which network transport mechanisms the client will use to communicate with actors, sorted by priority: * `"websocket"`: Real-time bidirectional communication, best for most applications * `"sse"` (Server-Sent Events): Works in more restricted environments where WebSockets may be blocked Default is `["websocket", "sse"]`, which automatically negotiates the best available option. ## Error Handling ActorCore provides specific error types to help you handle different failure scenarios: ### Action Errors When an action fails, it throws an error with details about the failure: ```typescript try { await actor.someAction(); } catch (error) { console.error(`Action failed: ${error.code} - ${error.message}`); // Handle specific error codes if (error.code === "permission_denied") { // Handle permission errors } } ``` These errors can be thrown from within the actor with `UserError`: ```typescript import { actor, UserError } from "actor-core"; const documentActor = actor({ state: { content: "" }, actions: { editDocument: (c, userId, newContent) => { // Check if user has permission to edit if (!hasPermission(userId, "edit")) { throw new UserError("You don't have permission to edit this document", { code: "permission_denied", meta: { userId } }); } c.state.content = newContent; } } }); ``` ActorCore doesn't expose internal errors to clients for security, helping to prevent the exposure of sensitive information or internal implementation details. ### Other Errors Other common errors you might encounter: * `InternalError`: Error from your actor that's not a subclass of `UserError` * `ManagerError`: Issues when connecting to or communicating with the actor manager * `NoSupportedTransport`: When the client and server have no compatible transport ## Disconnecting and Cleanup The client connection is automatically cleaned up when it goes out of scope. If you need to explicitly disconnect: ```typescript TypeScript // Disconnect from the actor await actor.dispose(); // Disconnect the entire client await client.dispose(); ``` ```rust Rust // Disconnect from the actor actor.disconnect().await; // The client will be cleaned up automatically when it goes out of scope // Or explicitly drop it with: drop(client); ``` ## Offline and Auto-Reconnection Clients automatically attempt to reconnect (with [exponential backoff](https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/retry-backoff.html)) when disconnected. Actions made while disconnected are queued. On reconnection, event subscriptions are reestablished & queued actions are executed. This makes your applications resilient to temporary network failures without any extra code. ## Next Steps Learn how state works in actors Learn more about actor events Add security to your actors Manage client connections # Lifecycle Source: https://actorcore.org/concepts/lifecycle ## Lifecycle Hooks Actor lifecycle hooks are defined as functions in the actor configuration. ### `createState` and `state` {/* [Documentation](https://jsr.io/@rivet-gg/actor/doc/~/Actor.prototype._onInitialize) */} The `createState` function or `state` constant defines the initial state of the actor (see [state documentation](/concepts/state)). The `createState` function is called only once when the actor is first created. ### `createVars` and `vars` The `createVars` function or `vars` constant defines ephemeral variables for the actor (see [state documentation](/concepts/state)). These variables are not persisted and are useful for storing runtime-only objects or temporary data. 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 "actor-core"; // Using vars constant const counter1 = actor({ state: { count: 0 }, vars: { lastAccessTime: 0 }, actions: { /* ... */ } }); // Using createVars function const counter2 = actor({ state: { count: 0 }, createVars: () => { // Initialize with non-serializable objects return { lastAccessTime: Date.now(), emitter: createNanoEvents() }; }, actions: { /* ... */ } }); // Access driver-specific context const exampleActor = actor({ state: { count: 0 }, // Access driver context in createVars createVars: (c, rivet) => ({ ctx: rivet.ctx, }), actions: { doSomething: (c) => { // Use driver-specific context console.log(`Region: ${c.vars.rivet.metadata.region.name}`); } } }); ``` ### `onCreate` The `onCreate` hook is called at the same time as `createState`, but unlike `createState`, it doesn't return any value. Use this hook for initialization logic that doesn't affect the initial state. ```typescript import { actor } from "actor-core"; // Using state constant const counter1 = actor({ state: { count: 0 }, actions: { /* ... */ } }); // Using createState function const counter2 = actor({ createState: () => { // Initialize with a count of 0 return { count: 0 }; }, actions: { /* ... */ } }); // Using onCreate const counter3 = actor({ state: { count: 0 }, // Run initialization logic (logging, external service setup, etc.) onCreate: (c) => { console.log("Counter actor initialized"); // Can perform async operations or setup // No need to return anything }, actions: { /* ... */ } }); ``` ### `onStart` {/* [Documentation](https://jsr.io/@rivet-gg/actor/doc/~/Actor.prototype._onStart) */} This hook is called any time the actor is started (e.g. after restarting, upgrading code, or crashing). This is called after the actor has been initialized but before any connections are accepted. Use this hook to set up any resources or start any background tasks, such as `setInterval`. ```typescript import { actor } from "actor-core"; const counter = actor({ state: { count: 0 }, onStart: (c) => { console.log('Actor started with count:', c.state.count); // Set up interval for automatic counting const intervalId = setInterval(() => { c.state.count++; console.log('Auto-increment:', c.state.count); }, 10000); // Store interval ID to clean up later if needed c.custom.intervalId = intervalId; }, actions: { /* ... */ } }); ``` ### `onStateChange` {/* [Documentation](https://jsr.io/@rivet-gg/actor/doc/~/Actor.prototype._onStateChange) */} Called whenever the actor's state changes. This is often used to broadcast state updates. ```typescript import { actor } from "actor-core"; const counter = actor({ state: { count: 0 }, onStateChange: (c, newState) => { // Broadcast the new count to all connected clients c.broadcast('countUpdated', { count: newState.count }); }, actions: { increment: (c) => { c.state.count++; return c.state.count; } } }); ``` ### `createConnState` and `connState` {/* [Documentation](https://jsr.io/@rivet-gg/actor/doc/~/Actor.prototype._createConnState) */} There are two ways to define the initial state for connections: 1. `connState`: Define a constant object that will be used as the initial state for all connections 2. `createConnState`: A function that dynamically creates initial connection state based on connection parameters ### `onBeforeConnect` {/* [Documentation](https://jsr.io/@rivet-gg/actor/doc/~/Actor.prototype._onBeforeConnect) */} The `onBeforeConnect` hook is called whenever a new client connects to the actor. Clients can pass parameters when connecting, accessible via `params`. This hook is used for connection validation and can throw errors to reject connections. The `onBeforeConnect` hook does NOT return connection state - it's used solely for validation. ```typescript import { actor } from "actor-core"; const chatRoom = actor({ state: { messages: [] }, // Method 1: Use a static default connection state connState: { role: "guest", joinTime: 0, }, // Method 2: Dynamically create connection state createConnState: (c, { params }) => { return { userId: params.userId || "anonymous", role: params.role || "guest", joinTime: Date.now() }; }, // Validate connections before accepting them onBeforeConnect: (c, { params }) => { // Validate authentication const authToken = params.authToken; if (!authToken || !validateToken(authToken)) { throw new Error("Invalid authentication"); } // Authentication is valid, connection will proceed // The actual connection state will come from connState or createConnState }, actions: { /* ... */ } }); ``` Connections cannot interact with the actor until this method completes successfully. Throwing an error will abort the connection. This can be used for authentication - see [Authentication](/concepts/authentication) for details. ### `onConnect` {/* [Documentation](https://jsr.io/@rivet-gg/actor/doc/~/Actor.prototype._onConnect) */} Executed after the client has successfully connected. ```typescript import { actor } from "actor-core"; const chatRoom = actor({ state: { users: {}, messages: [] }, onConnect: (c) => { // Add user to the room's user list using connection state const userId = c.conn.state.userId; c.state.users[userId] = { online: true, lastSeen: Date.now() }; // Broadcast that a user joined c.broadcast("userJoined", { userId, timestamp: Date.now() }); console.log(`User ${userId} connected`); }, actions: { /* ... */ } }); ``` Messages will not be processed for this actor until this hook succeeds. Errors thrown from this hook will cause the client to disconnect. ### `onDisconnect` {/* [Documentation](https://jsr.io/@rivet-gg/actor/doc/~/Actor.prototype._onDisconnect) */} Called when a client disconnects from the actor. Use this to clean up any connection-specific resources. ```typescript import { actor } from "actor-core"; const chatRoom = actor({ state: { users: {}, messages: [] }, onDisconnect: (c) => { // Update user status when they disconnect const userId = c.conn.state.userId; if (c.state.users[userId]) { c.state.users[userId].online = false; c.state.users[userId].lastSeen = Date.now(); } // Broadcast that a user left c.broadcast("userLeft", { userId, timestamp: Date.now() }); console.log(`User ${userId} disconnected`); }, actions: { /* ... */ } }); ``` ## Destroying Actors Actors can be shut down gracefully with `c.shutdown()`. Clients will be gracefully disconnected. ```typescript import { actor } from "actor-core"; const temporaryRoom = actor({ state: { createdAt: 0, expiresAfterMs: 3600000 // 1 hour }, createState: () => ({ createdAt: Date.now(), expiresAfterMs: 3600000 // 1 hour }), onStart: (c) => { // Check if room is expired const now = Date.now(); const expiresAt = c.state.createdAt + c.state.expiresAfterMs; if (now > expiresAt) { console.log("Room expired, shutting down"); c.shutdown(); } else { // Set up expiration timer const timeUntilExpiry = expiresAt - now; setTimeout(() => { console.log("Room lifetime reached, shutting down"); c.shutdown(); }, timeUntilExpiry); } }, actions: { closeRoom: (c) => { // Notify all clients c.broadcast("roomClosed", { reason: "Admin closed the room" }); // Shutdown the actor c.shutdown(); } } }); ``` This action is permanent and cannot be reverted. ## Using `ActorContext` Type Externally When extracting logic from lifecycle hooks or actions into external functions, you'll often need to define the type of the context parameter. ActorCore provides helper types that make it easy to extract and pass these context types to external functions. ```typescript import { actor, ActorContextOf } from "actor-core"; const myActor = actor({ state: { count: 0 }, // Use external function in lifecycle hook onStart: (c) => logActorStarted(c) }); // Simple external function with typed context function logActorStarted(c: ActorContextOf) { console.log(`Actor started with count: ${c.state.count}`); } ``` See [Helper Types](/concepts/types) for more details on using `ActorContextOf`. ## Full Example ```typescript import { actor } from "actor-core"; const counter = actor({ // Initialize state createState: () => ({ count: 0 }), // Initialize actor (run setup that doesn't affect initial state) onCreate: (c) => { console.log('Counter actor initialized'); // Set up external resources, etc. }, // Define default connection state connState: { role: "guest" }, // Dynamically create connection state based on params createConnState: (c, { params }) => { // Get auth info from validation const authToken = params.authToken; const authInfo = validateAuthToken(authToken); return { userId: authInfo?.userId || "anonymous", role: authInfo?.role || "guest" }; }, // Lifecycle hooks onStart: (c) => { console.log('Counter started with count:', c.state.count); }, onStateChange: (c, newState) => { c.broadcast('countUpdated', { count: newState.count }); }, onBeforeConnect: (c, { params }) => { // Validate auth token const authToken = params.authToken; if (!authToken) { throw new Error('Missing auth token'); } // Validate with your API and determine the user const authInfo = validateAuthToken(authToken); if (!authInfo) { throw new Error('Invalid auth token'); } // If validation succeeds, connection proceeds // Connection state will be set by createConnState }, onConnect: (c) => { console.log(`User ${c.conn.state.userId} connected`); }, onDisconnect: (c) => { console.log(`User ${c.conn.state.userId} disconnected`); }, // Define actions actions: { increment: (c) => { c.state.count++; return c.state.count; }, reset: (c) => { // Check if user has admin role if (c.conns.state.role !== "admin") { throw new Error("Unauthorized: requires admin role"); } c.state.count = 0; return c.state.count; } } }); export default counter; ``` # Logging Source: https://actorcore.org/concepts/logging Actors provide a built-in way to log complex data to the console. When dealing with lots of data, `console.log` often doesn't cut it. Using the context's log object (`c.log`) allows you to log complex data using structured logging. Using the actor logging API is completely optional. ## Log levels There are 5 log levels: | Level | Call | Description | | -------- | ----------------------------------- | ---------------------------------------------------------------- | | Critical | `c.log.critical(message, ...args);` | Severe errors that prevent core functionality | | Error | `c.log.error(message, ...args);` | Errors that affect functionality but allow continued operation | | Warning | `c.log.warn(message, ...args);` | Potentially harmful situations that should be addressed | | Info | `c.log.info(message, ...args);` | General information about significant events & state changes | | Debug | `c.log.debug(message, ...args);` | Detailed debugging information, usually used only in development | ## Structured logging The built-in logging API (using `c.log`) provides structured logging to let you log key-value pairs instead of raw strings. Structures logs are readable by both machines & humans to make them easier to parse & search. Passing an object to a log will print as structured data. For example: ```typescript c.log.info('increment', { connection: c.conn.id, count }); // Prints: level=INFO msg=increment connection=123 count=456 ``` The first parameter in each log method is the message. The rest of the arguments are used for structured logging. ## `c.log` vs `console.log` logging `c.log` makes it easier to manage complex logs, while `console.log` can become unmaintainable at scale. Consider this example: ```typescript structured_logging.ts import { actor } from "actor-core"; const counter = actor({ state: { count: 0 }, actions: { increment: (c, count) => { // Prints: level=INFO msg=increment connection=123 count=456 c.log.info('increment', { connection: c.conn.id, count }); c.state.count += count; return c.state.count; } } }); ``` ```typescript unstructured_logging.ts import { actor } from "actor-core"; const counter = actor({ state: { count: 0 }, actions: { increment: (c, count) => { // Prints: Increment for connection 123 with count 456 console.log(`Increment for connection ${c.conn.id} with count ${count}`); c.state.count += count; return c.state.count; } } }); ``` If you need to search through a lot of logs, it's easier to read the structured logs. To find increments for a single connection, you can search `connection=123`. Additionally, structured logs can be parsed and queried at scale using tools like Elasticsearch, Loki, or Datadog. For example, you can parse the log `level=INFO msg=increment connection=123 count=456` in to the JSON object `{"level":"INFO","msg":"increment","connection":123,"count":456}` and then query it as you would any other structured data. ## Usage in lifecycle hooks The logger is available in all lifecycle hooks: ```typescript import { actor } from "actor-core"; const loggingExample = actor({ state: { events: [] }, onStart: (c) => { c.log.info('actor_started', { timestamp: Date.now() }); }, onBeforeConnect: (c, { params }) => { c.log.debug('connection_attempt', { ip: params.ip, timestamp: Date.now() }); return { authorized: true }; }, onConnect: (c) => { c.log.info('connection_established', { connectionId: c.conn.id, timestamp: Date.now() }); c.state.events.push({ type: 'connect', connectionId: c.conn.id, timestamp: Date.now() }); }, onDisconnect: (c) => { c.log.info('connection_closed', { connectionId: c.conn.id, timestamp: Date.now() }); c.state.events.push({ type: 'disconnect', connectionId: c.conn.id, timestamp: Date.now() }); }, actions: { // Actor actions... } }); ``` # Metadata Source: https://actorcore.org/concepts/metadata Metadata provides information about the currently running actor. ## Region Region can be accessed from the context object via `c.region`. `c.region` is only supported on Rivet at the moment. ## Tags Tags can be accessed from the context object via `c.tags`. For example: ```typescript chat_room.ts import { actor } from "actor-core"; const chatRoom = actor({ state: { messages: [] }, actions: { // Method used to get the channel ID getChannelId: (c) => { return c.tags['channel']; }, addMessage: (c, message) => { // Use the channel for logging or filtering const channel = c.tags['channel'] || 'default'; console.log(`Adding message to channel: ${channel}`); c.state.messages.push({ channel, message, timestamp: Date.now() }); c.broadcast('newMessage', { channel, message }); } } }); export default chatRoom; ``` ```typescript client.ts import { createClient } from "actor-core/client"; import type { App } from "./src/index"; const client = createClient("http://localhost:6420"); // Connect to a specific channel const randomChannel = await client.chatRoom.get({ channel: "random" }); // Check the channel ID const channelId = await randomChannel.getChannelId(); console.log("Connected to channel:", channelId); // "random" // Or connect with multiple parameters const teamChannel = await client.chatRoom.get({ channel: "team-chat", teamId: "engineering" }); ``` ## Actor Name You can access the actor name with: ```typescript const actorName = c.name; ``` This is useful when you need to know which actor type is running, especially if you have generic utility functions that are shared between different actor implementations. # Overview Source: https://actorcore.org/concepts/overview Actors combine compute and storage into unified entities for simplified architecture. Actors seamlessly integrate with your existing infrastructure or can serve as a complete standalone solution. ## Quickstart Run this to get started: ```sh npx npx create-actor@latest ``` ```sh npm npm create actor@latest ``` ```sh pnpm pnpm create actor@latest ``` ```sh yarn yarn create actor@latest ``` ```sh bun bun create actor@latest ``` ## What are actors good for? Actors in ActorCore are ideal for applications requiring: * **Stateful Services**: Applications where maintaining state across interactions is critical. For example, **Collaborative Apps** with shared editing and automatic persistence. * **Realtime Systems**: Applications requiring fast, in-memory state modifications or push updates to connected clients. For example, **Multiplayer Games** with game rooms and player state. * **Long-Running Processes**: Tasks that execute over extended periods or in multiple steps. For example, **AI Agents** with ongoing conversations and stateful tool calls. * **Durability**: Processes that must survive crashes and restarts without data loss. For example, **Durable Execution** workflows that continue after system restarts. * **Horizontal Scalability**: Systems that need to scale by distributing load across many instances. For example, **Realtime Stream Processing** for stateful event handling. * **Local-First Architecture**: Systems that synchronize state between offline clients. For example, **Local-First Sync** between devices. ## Core Concepts In ActorCore, each actor has these key characteristics: * **State Is Automatically Persisted**: State automatically persists between restarts, upgrades, & crashes * **State Is Stored In-Memory**: State is stored in memory for high-performance reads/writes while also automatically persisted * **Isolated State Ownership**: Actors only manage their own state, which can only be modified by the actor itself * **Communicates via Actions**: How clients and other actors interact with an actor * **Actions Are Low-Latency**: Actions provide WebSocket-like performance for time-sensitive operations * **Broadcast Updates With Events**: Actors can publish real-time updates to connected clients ## Code Example Here's a complete chat room actor that maintains state and handles messages. We'll explore each component in depth throughout this document: ```typescript chat_room.ts import { actor } from "actor-core"; // Define a chat room actor const chatRoom = actor({ // Initialize state when the actor is first created createState: () => ({ messages: [] }), // Define actions clients can call actions: { // Action to send a message sendMessage: (c, sender, text) => { // Update state c.state.messages.push({ sender, text }); // Broadcast to all connected clients c.broadcast("newMessage", { sender, text }); }, // Action to get chat history getHistory: (c) => { return c.state.messages; } } }); export default chatRoom; ``` ## Using the App To start using your actor, create an app and serve it: ```typescript app.ts import { setup, serve } from "actor-core"; import chatRoom from "./chat_room"; // Create the application const app = setup({ actors: { chatRoom } }); // Start serving on default port serve(app); // Export the app type for client usage export type App = typeof app; ``` ## Key Actor Components ### State Actors maintain state that's stored in memory and automatically persisted. State is defined either as a constant or via a `createState` function: ```typescript import { actor } from "actor-core"; // Method 1: State constant const counter1 = actor({ state: { count: 0 }, actions: { // ... } }); // Method 2: CreateState function const counter2 = actor({ createState: () => ({ count: 0 }), actions: { // ... } }); ``` Update state by modifying `c.state` in your actions: ```typescript import { actor } from "actor-core"; const counter = actor({ state: { count: 0 }, actions: { // Example of state update in an action increment: (c) => { c.state.count += 1; return c.state.count; } } }); ``` These changes are durable and are automatically persisted across updates, restarts, and crashes. Learn more about [state management](/concepts/state). ### Actions Actions are functions defined in your actor configuration that clients & other actors can call: ```typescript import { actor } from "actor-core"; const mathUtils = actor({ state: {}, actions: { multiplyByTwo: (c, x) => { return x * 2; } } }); ``` Each action receives a context object (commonly named `c`) as its first parameter, which provides access to state, connections, and other utilities. Learn more about [actions](/concepts/actions). ### Events Actors can broadcast events to connected clients: ```typescript import { actor } from "actor-core"; const inventory = actor({ createState: () => ({ items: [] }), actions: { addItem: (c, item) => { // Add to state c.state.items.push(item); // Notify all clients about the new item c.broadcast("itemAdded", { item }); } } }); ``` You can also send events to specific clients: ```typescript import { actor } from "actor-core"; const messageService = actor({ state: {}, actions: { sendPrivateMessage: (c, userId, text) => { // Send to a specific connection const conn = c.conns.find(conn => conn.params.userId === userId); if (conn) { conn.send("privateMessage", { text }); } } } }); ``` Learn more about [events](/concepts/events). ## Actor Tags Tags are key-value pairs attached to actors that serve two purposes: 1. **Actor Discovery**: Find specific actors using `client.get(tags)` 2. **Organization**: Group related actors for management purposes For example, you can query chat rooms by tag like: ```typescript client.ts await client.chatRoom.get({ channel: "random" }); ``` ### Common Tag Patterns ```typescript import { createClient } from "actor-core/client"; import type { App } from "./src/index"; const client = createClient("http://localhost:6420"); // Game room with ID parameter const gameRoom = await client.gameRoom.get({ roomId: "ABC123" }); // User profile with ID const userProfile = await client.userProfile.get({ profileId: "1234" }); // Document with multiple parameters const document = await client.document.get({ workspaceId: "team-alpha", documentId: "budget-2024" }); ``` ## Actor Lifecycle Actors are created automatically when needed and persist until explicitly shutdown. To shut down an actor, use `c.shutdown()` from within an action: ```typescript import { actor } from "actor-core"; const chatRoom = actor({ createState: () => ({ messages: [] }), actions: { closeRoom: (c) => { // Do any cleanup needed c.broadcast("roomClosed"); // Shutdown the actor c.shutdown(); } } }); ``` Learn more about the [actor lifecycle](/concepts/lifecycle). ## Next Steps Learn how to connect to actors from clients Deep dive into actor state management Learn more about actor actions Learn more about realtime events # Scaling & Concurrency Source: https://actorcore.org/concepts/scaling This document covers how actors are able to scale better than traditional applications & provides tips on architecting your actors. ## How actors scale Actors scale by design through these key properties: | Property | Description | | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Independent State** | Each actor manages its own private data separately from other actors, so they never conflict with each other when running at the same time (i.e. using locking mechanisms). | | **Action- & Event-Based Communication** | Actors communicate through asynchronous [actions](/concepts/actions) or [events](/concepts/events), making it easy to distribute them across different machines. | | **Location Transparency** | Unlike traditional servers, actors don't need to know which machine other actors are running on in order to communicate with each other. They can run on the same machine, across a network, and across the world. Actors handle the network routing for you under the hood. | | **Horizontal Scaling** | Actors distribute workload by splitting responsibilities into small, focused units. Since each actor handles a limited scope (like a single user, document, or chat room), the system automatically spreads load across many independent actors rather than concentrating it in a single place. | ## Tips for architecting actors for scale Here are key principles for architecting your actor system: **Single Responsibility** * Each actor should represent one specific entity or concept from your application (e.g., `User`, `Document`, `ChatRoom`). * This makes your system scale better, since actors have small scopes and do not conflict with each other. **State Management** * Each actor owns and manages only its own state * Use [actions](/concepts/actions) to request data from other actors * Keep state minimal and relevant to the actor's core responsibility **Granularity Guidelines** * Too coarse: Actors handling too many responsibilities become bottlenecks * Too fine: Excessive actors create unnecessary communication overhead * Aim for actors that can operate independently with minimal cross-actor communication ### Examples **Good actor boundaries** * `User`: Manages user profile, preferences, and authentication * `Document`: Handles document content, metadata, and versioning * `ChatRoom`: Manages participants and message history **Poor actor boundaries** * `Application`: Too broad, handles everything * `DocumentWordCount`: Too granular, should be part of DocumentActor # Schedule Source: https://actorcore.org/concepts/schedule Scheduling is used to trigger events in the future. The actor scheduler is like `setTimeout`, except the timeout will persist even if the actor restarts, upgrades, or crashes. ## Use Cases Scheduling is helpful for long-running timeouts like month-long billing periods or account trials. ## Scheduling ### `c.schedule.after(duration, fn, ...args)` Schedules a function to be executed after a specified duration. This function persists across actor restarts, upgrades, or crashes. Parameters: * `duration` (number): The delay in milliseconds. * `fn` (string): The name of the action to be executed. * `...args` (unknown\[]): Additional arguments to pass to the function. ### `c.schedule.at(timestamp, fn, ...args)` Schedules a function to be executed at a specific timestamp. This function persists across actor restarts, upgrades, or crashes. Parameters: * `timestamp` (number): The exact time in milliseconds since the Unix epoch when the function should be executed. * `fn` (string): The name of the action to be executed. * `...args` (unknown\[]): Additional arguments to pass to the function. ## Scheduling Private Actions Currently, scheduling can only trigger public actions. If the scheduled action is private, it needs to be secured with something like a token. ## Full Example ```typescript import { actor } from "actor-core"; const reminderService = actor({ state: { reminders: {} }, actions: { setReminder: (c, userId, message, delayMs) => { const reminderId = crypto.randomUUID(); // Store the reminder in state c.state.reminders[reminderId] = { userId, message, scheduledFor: Date.now() + delayMs }; // Schedule the sendReminder action to run after the delay c.after(delayMs, "sendReminder", reminderId); return { reminderId }; }, sendReminder: (c, reminderId) => { const reminder = c.state.reminders[reminderId]; if (!reminder) return; // Find the user's connection if they're online const userConn = c.conns.find( conn => conn.state.userId === reminder.userId ); if (userConn) { // Send the reminder to the user userConn.send("reminder", { message: reminder.message, scheduledAt: reminder.scheduledFor }); } else { // If user is offline, store reminder for later delivery // ... } // Clean up the processed reminder delete c.state.reminders[reminderId]; } } }); ``` # State Source: https://actorcore.org/concepts/state Actor state provides the best of both worlds: it's stored in-memory and persisted automatically. This lets you work with the data without added latency while still being able to survive crashes & upgrades. **Using External SQL Databases** Actors can also be used with external SQL databases. This can be useful to integrate actors with existing applications or for storing relational data. Read more [here](/concepts/external-sql). ## Initializing State There are two ways to define an actor's initial state: **Method 1: Static Initial State** ```typescript import { actor } from "actor-core"; // Simple state with a constant const counter = actor({ // Define state as a constant state: { count: 0 }, actions: { // ... } }); ``` **Method 2: Dynamic Initial State** ```typescript import { actor } from "actor-core"; // State with initialization logic const counter = actor({ // Define state using a creation function createState: () => { return { count: 0 }; }, actions: { // ... } }); ``` The `createState` function is called once when the actor is first created. See [Lifecycle](/concepts/lifecycle) for more details. ## Modifying State To update state, modify the `state` property on the context object (`c.state`) in your actions: ```typescript import { actor } from "actor-core"; const counter = actor({ state: { count: 0 }, actions: { // Define action to update state increment: (c) => { // Update state, this will automatically be persisted c.state.count += 1; return c.state.count; }, add: (c, value) => { c.state.count += value; return c.state.count; } } }); ``` Only state stored in the `state` object will be persisted. Any other variables or properties outside of this are not persisted. ## State Saves Actors automatically handle persisting state transparently. This happens at the end of every action if the state has changed. In the rare occasion you need to force a state change mid-action, you can use `c.saveState()`. This should only be used if your action makes an important state change that needs to be persisted before the action completes. ```typescript import { actor } from "actor-core"; const criticalProcess = actor({ state: { steps: [], currentStep: 0 }, actions: { processStep: async (c) => { // Update to current step c.state.currentStep += 1; c.state.steps.push(`Started step ${c.state.currentStep}`); // Force save state before the async operation c.saveState(); // Long-running operation that might fail await someRiskyOperation(); // Update state again c.state.steps.push(`Completed step ${c.state.currentStep}`); return c.state.currentStep; } } }); ``` ## State Isolation Each actor's state is completely isolated, meaning it cannot be accessed directly by other actors or clients. This allows actors to maintain a high level of security and data integrity, ensuring that state changes are controlled and predictable. To interact with an actor's state, you must use [Actions](/concepts/actions). Actions provide a controlled way to read from and write to the state. ## Sharing State Between Actors If you need a shared state between multiple actors, you have two options: 1. Create an actor that holds the shared state that other actors can make action calls to 2. Use an external database, see [External SQL Databases](/concepts/external-sql) ## Ephemeral Variables In addition to persisted state, ActorCore provides a way to store ephemeral data that is not saved to permanent storage using `vars`. This is useful for temporary data that only needs to exist while the actor is running or data that cannot be serialized. `vars` is designed to complement `state`, not replace it. Most actors should use both: `state` for critical business data and `vars` for ephemeral or non-serializable data. ### Initializing Variables There are two ways to define an actor's initial vars: **Method 1: Static Initial Variables** ```typescript import { actor } from "actor-core"; // Define vars as a constant const counter = actor({ state: { count: 0 }, // Define ephemeral variables vars: { lastAccessTime: 0, emitter: createNanoEvents() }, actions: { // ... } }); ``` When using static `vars`, all values must be compatible with `structuredClone()`. If you need to use non-serializable objects, use `createVars` instead, which allows you to create these objects on the fly. **Method 2: Dynamic Initial Variables** ```typescript import { actor } from "actor-core"; // Define vars with initialization logic const counter = actor({ state: { count: 0 }, // Define vars using a creation function createVars: () => { return { lastAccessTime: Date.now(), emitter: createNanoEvents() }; }, actions: { // ... } }); ``` ### Using Variables Vars can be accessed and modified through the context object with `c.vars`: ```typescript import { actor } from "actor-core"; import { createNanoEvents } from "nanoevents"; const counter = actor({ // Persistent state - saved to storage state: { count: 0 }, // Create ephemeral objects that won't be serialized createVars: () => { // Create an event emitter (can't be serialized) const emitter = createNanoEvents(); // Set up event listener directly in createVars emitter.on('count-changed', (newCount) => { console.log(`Count changed to: ${newCount}`); }); return { emitter }; }, actions: { increment: (c) => { // Update persistent state c.state.count += 1; // Use non-serializable emitter c.vars.emitter.emit('count-changed', c.state.count); return c.state.count; } } }); ``` ### When to Use `vars` vs `state` In practice, most actors will use both: `state` for critical business data and `vars` for ephemeral, non-serializable, or performance-sensitive data. Use `vars` when: * You need to store temporary data that doesn't need to survive restarts * You need to maintain runtime-only references that can't be serialized (database connections, event emitters, class instances, etc.) Use `state` when: * The data must be preserved across actor sleeps, restarts, updates, or crashes * The information is essential to the actor's core functionality and business logic ## Limitations State is constrained to the available memory. 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. # Testing Source: https://actorcore.org/concepts/testing ActorCore provides a straightforward testing framework to build reliable and maintainable applications. This guide covers how to write effective tests for your actor-based services. ## Setup To set up testing with ActorCore: ```bash # Install Vitest npm install -D vitest # Run tests npm test ``` ## Basic Testing Setup ActorCore 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. ```ts tests/my-actor.test.ts import { test, expect } from "vitest"; import { setupTest } from "actor-core/test"; import { app } from "../src/index"; test("my actor test", async () => { const { client } = await setupTest(app); // Now you can interact with your actor through the client const myActor = await client.myActor.get(); // Test your actor's functionality await myActor.someAction(); // Make assertions const result = await myActor.getState(); expect(result).toEqual("updated"); }); ``` ```ts src/index.ts import { actor, setup } from "actor-core"; const myActor = actor({ state: { value: "initial" }, actions: { someAction: (c) => { c.state.value = "updated"; return c.state.value; }, getState: (c) => { return c.state.value; } } }); export const app = setup({ actors: { myActor } }); export type App = typeof app; ``` ## 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. ```ts tests/counter.test.ts import { test, expect } from "vitest"; import { setupTest } from "actor-core/test"; import { app } from "../src/index"; test("actor should persist state", async () => { const { client } = await setupTest(app); const counter = await client.counter.get(); // Initial state expect(await counter.getCount()).toBe(0); // Modify state await counter.increment(); // Verify state was updated expect(await counter.getCount()).toBe(1); }); ``` ```ts src/index.ts import { setup } from "actor-core"; const counter = actor({ state: { count: 0 }, actions: { increment: (c) => { c.state.count += 1; c.broadcast("newCount", c.state.count); return c.state.count; }, getCount: (c) => { return c.state.count; } } }); export const app = setup({ actors: { counter } }); export type App = typeof app; ``` ## Testing Events For actors that emit events, you can verify events are correctly triggered by subscribing to them: ```ts tests/chat-room.test.ts import { test, expect, vi } from "vitest"; import { setupTest } from "actor-core/test"; import { app } from "../src/index"; test("actor should emit events", async () => { const { client } = await setupTest(app); const chatRoom = await client.chatRoom.get(); // Set up event handler with a mock function const mockHandler = vi.fn(); chatRoom.on("newMessage", mockHandler); // Trigger the event await chatRoom.sendMessage("testUser", "Hello world"); // Wait for the event to be emitted await vi.waitFor(() => { expect(mockHandler).toHaveBeenCalledWith("testUser", "Hello world"); }); }); ``` ```ts src/index.ts import { actor, setup } from "actor-core"; export const chatRoom = actor({ state: { messages: [] }, actions: { sendMessage: (c, username: string, message: string) => { c.state.messages.push({ username, message }); c.broadcast("newMessage", username, message); }, getHistory: (c) => { return c.state.messages; }, }, }); // Create and export the app export const app = setup({ actors: { chatRoom } }); // Export type for client type checking export type App = typeof app; ``` ## Testing Schedules ActorCore's schedule functionality can be tested using Vitest's time manipulation utilities: ```ts tests/scheduler.test.ts import { test, expect, vi } from "vitest"; import { setupTest } from "actor-core/test"; import { app } from "../src/index"; test("scheduled tasks should execute", async () => { // setupTest automatically configures vi.useFakeTimers() const { client } = await setupTest(app); const scheduler = await client.scheduler.get(); // Set up a scheduled task await scheduler.scheduleTask("reminder", 60000); // 1 minute in the future // Fast-forward time by 1 minute await vi.advanceTimersByTimeAsync(60000); // Verify the scheduled task executed expect(await scheduler.getCompletedTasks()).toContain("reminder"); }); ``` ```ts src/index.ts import { actor, setup } from "actor-core"; const scheduler = actor({ state: { tasks: [], completedTasks: [] }, actions: { scheduleTask: (c, taskName: string, delayMs: number) => { c.state.tasks.push(taskName); // Schedule "completeTask" to run after the specified delay c.schedule.after(delayMs, "completeTask", taskName); return { success: true }; }, completeTask: (c, taskName: string) => { // This action will be called by the scheduler when the time comes c.state.completedTasks.push(taskName); return { completed: taskName }; }, getCompletedTasks: (c) => { return c.state.completedTasks; } } }); export const app = setup({ actors: { scheduler } }); export type App = typeof app; ``` 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. ## 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. 4. **Use realistic data**: Test with data that resembles production scenarios. ActorCore's testing framework automatically handles server setup and teardown, so you can focus on writing effective tests for your business logic. # Topologies Source: https://actorcore.org/concepts/topology ActorCore supports three topologies that define how actors are distributed and scale. Each platform configures a default topology appropriate for that environment. In most cases, you can rely on these defaults unless you have specific distribution needs. ## Configuration ```typescript const config = { topology: "standalone" // or "partition" or "coordinate" }; ``` ## Types of Topologies ### Standalone * **How it works**: Runs all actors in a single process * **When to use**: Development, testing, simple apps with low traffic * **Limitations**: No horizontal scaling, single point of failure * **Default on**: Node.js, Bun ### Partition * **How it works**: Each actor has its own isolated process. Clients connect directly to the actor for optimal performance. * **When to use**: Production environments needing horizontal scaling * **Limitations**: Minimal - balanced performance and availability for most use cases * **Default on**: Rivet, Cloudflare Workers ### Coordinate * **How it works**: Creates a peer-to-peer network between multiple servers with leader election with multiple actors running on each server. Clients connect to any server and data is transmitted to the leader over a pubsub server. * **When to use**: High-availability scenarios needing redundancy and failover * **Limitations**: Added complexity, performance overhead, requires external data source * **Default on**: *None* ## Choosing a Topology In most cases, use your platform's default: 1. **Standalone**: Simple, great for development 2. **Partition**: Best scaling & cost for production 3. **Coordinate**: Good for specialized deployment scenarios # Helper Types Source: https://actorcore.org/concepts/types ActorCore 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. ActorCore provides helper types to extract the context types from actor definitions. ### `ActorContextOf` Extracts the full actor context type from an actor definition. This is the type of the context object (`c`) available in lifecycle hooks such as `onCreate`, `onStart`, etc. ```typescript import { actor, ActorContextOf } from "actor-core"; const chatRoom = actor({ state: { messages: [] }, actions: { sendMessage: (c, message) => { // ... } } }); // Extract the chat room context type type ChatRoomContext = ActorContextOf; // Now you can use this type elsewhere function processChatRoomContext(context: ChatRoomContext) { // Access context properties with full type safety console.log(context.state.messages); context.broadcast("newEvent", { type: "system" }); } ``` ### `ActionContextOf` Extracts the action context type from an actor definition. This is the type of the context object (`c`) available in action handlers. ```typescript import { actor, ActionContextOf } from "actor-core"; const counter = actor({ state: { count: 0 }, actions: { increment: (c) => { c.state.count++; return c.state.count; } } }); // Extract the action context type type CounterActionContext = ActionContextOf; // Use in other functions that need to work with the action context function processCounterAction(context: CounterActionContext) { // Access context with full type safety context.state.count++; } ``` # Building Drivers Source: https://actorcore.org/drivers/build Each driver implements common interfaces defined by ActorCore, including: * **ActorDriver**: Manages actor state, lifecycle, and persistence * **ManagerDriver**: Handles actor discovery, routing, and scaling * **CoordinateDriver**: Facilitates peer-to-peer communication between actor instances (only relevant for the coordinated topology) ## Source Code Locations Get started by looking at source code for the driver interfaces and existing drivers: * **Driver Interfaces** * **ActorDriver**\* `packages/actor-core/src/actor/runtime/driver.ts` * **ManagerDriver**\* `packages/actor-core/src/actor/runtime/driver.ts` * **CoordinateDriver**: `packages/actor-core/src/topologies/coordinate/driver.ts` * **Driver Implementations** * **Memory driver**: `packages/drivers/memory/` * **Redis driver**: `packages/drivers/redis/` * More drivers are available in `packages/drivers/` # Durable Objects Source: https://actorcore.org/drivers/cloudflare-workers The Cloudflare Workers Driver is an implementation that uses Cloudflare's Durable Objects for actor state persistence and coordination. It leverages Cloudflare's global network for low-latency, distributed actor systems that can scale automatically. You rarely need to manually configure drivers. The platform package you're using will configure the appropriate drivers for you. See the documentation for your platform for details. ## Compatibility | Platforms | Topologies | | ----------------------------------------------------------- | -------------------------------------------------------------- | | Node.js | Standalone | | Bun | Coordinate | | Cloudflare Workers | Partition | | Rivet | | ## Usage There's no need to explicitly configure drivers when using ActorCore with Cloudflare Workers. The platform package automatically sets up the appropriate drivers. See the [Cloudflare Workers Platform](/platforms/cloudflare-workers) documentation for complete setup instructions. ## Limitations The Cloudflare Workers driver has several limitations to be aware of: * **Storage Limits**: Storage is limited to Cloudflare's Durable Objects size limits * **Cost**: Requires Cloudflare Workers Paid plan for Durable Objects usage * **Platform Lock-in**: Your implementation will be specific to the Cloudflare Workers platform For a platform-agnostic approach with similar global distribution capabilities, consider using [Rivet](/platforms/rivet) instead. # Memory Source: https://actorcore.org/drivers/memory The Memory Driver is a simple in-memory implementation designed for development and testing environments. It stores all actor state in memory, which means data is not persisted between application restarts. You rarely need to manually configure drivers. The platform package you're using will configure the appropriate drivers for you. See the documentation for your platform for details. The Memory Driver is not recommended for production environments as it doesn't provide persistence or distributed capabilities. ## Compatibility | Platforms | Topologies | | ---------------------------------------------------------------------- | -------------------------------------------------------------- | | Node.js | Standalone | | Bun | Partition | | Cloudflare Workers | Coordinate | | Rivet | | ## Installation Install the required packages: ```bash npm install @actor-core/memory @actor-core/nodejs ``` ```bash yarn add @actor-core/memory @actor-core/nodejs ``` ```bash pnpm add @actor-core/memory @actor-core/nodejs ``` ```bash bun add @actor-core/memory @actor-core/nodejs ``` Create a simple server using the Memory driver: ```typescript src/index.ts import { serve } from "@actor-core/nodejs" import { MemoryManagerDriver } from "@actor-core/memory/manager"; import { MemoryActorDriver } from "@actor-core/memory/actor"; serve(app, { topology: "standalone", drivers: { manager: new MemoryManagerDriver(), actor: new MemoryActorDriver(), }, }); ``` Start your server: ```bash npm run dev ``` ```bash yarn dev ``` ```bash pnpm dev ``` ```bash bun dev ``` ## Limitations The Memory driver has several limitations to be aware of: * **No Persistence**: All data is stored in memory and lost when the application restarts * **Single Process**: Only works within a single process - not suitable for distributed environments * **Scalability**: Cannot scale beyond a single instance * **Coordination**: Limited support for coordinated topology, as actors can only communicate within the same process For production environments or applications requiring persistence and distributed capabilities, consider using the [Rivet](/platforms/rivet) or [Cloudflare Workers](/platforms/cloudflare-workers) instead. # Drivers Source: https://actorcore.org/drivers/overview Drivers in ActorCore the infrastructure layer between your actor code and the underlying systems. You rarely need to manually configure drivers. The platform package you're using will configure the appropriate drivers for you. See the documentation for your platform for details. ## Accessing Driver Context Drivers expose a custom driver context for functionality specific to the given driver. This can be accessed as a parameter to `createVars` then later accessed with `c.vars`. Read more about the [`createVars` lifecycle hook](/concepts/lifecycle). ## Available Drivers Choose a driver based on your deployment needs - [Memory](/drivers/memory) for development, [Rivet](/drivers/rivet) or [Cloudflare Workers](/drivers/cloudflare-workers) for production environments, and [Redis](/drivers/redis) for specialized self-hosted scenarios. In-memory driver for development and testing. Simple and lightweight with no external dependencies. Self-hosted option for custom deployments where you need fine-grained control over your infrastructure. Suitable for specialized deployment scenarios. Fully-managed cloud platform with built-in scaling, deployment, and monitoring. Recommended for production deployments. Edge computing platform for global distribution and low latency. Recommended for production deployments. # Redis Source: https://actorcore.org/drivers/redis The Redis Driver is a production-ready implementation that uses Redis for actor state persistence, coordination, and communication. It supports distributed actor systems and enables horizontal scaling across multiple instances. You rarely need to manually configure drivers. The platform package you're using will configure the appropriate drivers for you. See the documentation for your platform for details. ActorCore requires AOF (Append Only File) persistence to be enabled on your Redis server. See the [Redis Persistence Documentation](https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/#append-only-file) for setup instructions. ## Compatibility | Platforms | Topologies | | ---------------------------------------------------------------------- | ------------------------------------------------------------- | | Node.js | Standalone | | Bun | Coordinate | | Cloudflare Workers | Partition | | Rivet | | ## Installation Install the required packages: ```bash npm install @actor-core/redis @actor-core/nodejs ioredis ``` ```bash yarn add @actor-core/redis @actor-core/nodejs ioredis ``` ```bash pnpm add @actor-core/redis @actor-core/nodejs ioredis ``` ```bash bun add @actor-core/redis @actor-core/nodejs ioredis ``` Create a Redis connection and set up your server: ```typescript src/index.ts import { serve } from "@actor-core/nodejs" import { RedisManagerDriver } from "@actor-core/redis/manager"; import { RedisActorDriver } from "@actor-core/redis/actor"; import { RedisCoordinateDriver } from "@actor-core/redis/coordinate"; import Redis from "ioredis"; // Create a Redis connection const redis = new Redis(); serve(app, { topology: "coordinate", // Can be "standalone" or "coordinate" drivers: { manager: new RedisManagerDriver(redis), actor: new RedisActorDriver(redis), coordinate: new RedisCoordinateDriver(redis), }, }); ``` Start your server: ```bash npm run dev ``` ```bash yarn dev ``` ```bash pnpm dev ``` ```bash bun dev ``` ## Redis Configuration The Redis driver requires an [ioredis](https://github.com/redis/ioredis) connection instance when creating the drivers. Custom configuration parameters can be passed like: ```typescript import Redis from "ioredis"; import { RedisManagerDriver } from "@actor-core/redis/manager"; import { RedisActorDriver } from "@actor-core/redis/actor"; import { RedisCoordinateDriver } from "@actor-core/redis/coordinate"; // Create a Redis connection const redis = new Redis({ host: "localhost", port: 6379, password: "foobar", }); // Create the Redis drivers const managerDriver = new RedisManagerDriver(redis); const actorDriver = new RedisActorDriver(redis); const coordinateDriver = new RedisCoordinateDriver(redis); ``` See the [ioredis documentation](https://github.com/redis/ioredis#connect-to-redis) for more connection configuration options. ## Hosted Redis Providers For production deployments, consider using these managed Redis providers: * [Amazon ElastiCache](https://aws.amazon.com/elasticache/) - AWS managed Redis service * [Azure Cache for Redis](https://azure.microsoft.com/en-us/products/cache) - Microsoft Azure managed Redis service * [Google Cloud Memorystore](https://cloud.google.com/memorystore) - Google Cloud managed Redis service * [Upstash](https://upstash.com/) - Serverless Redis with pay-per-use pricing * [Redis Cloud](https://redis.com/redis-enterprise-cloud/overview/) - Official Redis offering with enterprise features * [Dragonfly](https://www.dragonflydb.io/) - Redis-compatible database with higher performance For local development where persistence isn't required, the [Memory Driver](/drivers/memory) offers a simpler setup with no external dependencies. ## Limitations The Redis driver has several limitations to be aware of: * **Very Limited Storage**: Storage is limited to the available memory of your Redis server * **Single Region Support**: Only supports deployment within a single region, not globally distributed * **Performance Bottleneck**: All operations go through Redis, which can become a bottleneck under high load * **Single Point of Failure**: Redis becomes a single point of failure if not configured with proper failover mechanisms For multi-region support, built-in redundancy, and unlimited storage capacity, consider using [Rivet](/platforms/rivet) or [Cloudflare Workers](/platforms/cloudflare-workers) instead. # Rivet Source: https://actorcore.org/drivers/rivet The Rivet Driver is a production-ready implementation that uses Rivet's managed infrastructure for actor state persistence, coordination, and communication. It provides a fully managed environment for ActorCore with built-in scaling, monitoring, and global distribution. You rarely need to manually configure drivers. The platform package you're using will configure the appropriate drivers for you. See the documentation for your platform for details. ## Compatibility | Platforms | Topologies | | ---------------------------------------------------------------------- | -------------------------------------------------------------- | | Node.js | Standalone | | Bun | Coordinate | | Cloudflare Workers | Partition | | Rivet | | ## Usage There's no need to explicitly configure drivers when using ActorCore with Rivet. The platform package automatically sets up the appropriate drivers based on your project configuration. See the [Rivet Platform](/platforms/rivet) documentation for complete setup instructions. ## Benefits The Rivet driver offers several advantages: * **Fully Managed**: No infrastructure to provision or maintain * **Global Distribution**: Deploy actors globally with automatic region selection * **Monitoring**: Built-in observability and performance metrics * **Cost Efficiency**: Pay only for what you use with no upfront costs * **Partition Topology Support**: Optimized for the Partition topology for efficient scaling Rivet is a great choice for production deployments, offering enterprise-grade reliability without the operational overhead. # React Source: https://actorcore.org/frameworks/react The ActorCore React framework provides a convenient way to use ActorCore in React applications with standard React patterns. ## Installation Install the package: ```sh npm npm add @actor-core/react ``` ```sh pnpm pnpm add @actor-core/react ``` ```sh yarn yarn add @actor-core/react ``` ```sh bun bun add @actor-core/react ``` ## Quick Start ```tsx import { createClient } from "actor-core/client"; import { createReactActorCore } from "@actor-core/react"; import type { App } from "../counter/src/index"; import React, { useState } from "react"; // Create a client const client = createClient("http://your-actor-core-server.com"); // Create React hooks for your actors const { useActor, useActorEvent } = createReactActorCore(client); function ReactApp() { return ( <> ); } function Counter() { // Get or create an actor const [{ actor }] = useActor("counter"); return (
); } function CounterValue({ actor }) { const [count, setCount] = useState(0); // Listen to events useActorEvent({ actor, event: "newCount" }, (newCount) => { setCount(newCount); }); return count; } render(, document.getElementById("root")); ``` ## API Reference The React integration leverages React's hooks system to provide an idiomatic way to interact with ActorCore in React applications. ### `createReactActorCore` The main function that creates React hooks for interacting with ActorCore. It takes a client instance and returns hook functions. ```tsx const { useActor, useActorEvent } = createReactActorCore(client); ``` #### Parameters * `client`: The ActorCore client created with `createClient`. #### Returns An object containing React hooks: * `useActor`: Hook for connecting to actors * `useActorEvent`: Hook for subscribing to actor events ### `useActor` Hook that connects to an actor, creating it if necessary. It manages the actor connection and returns the actor handle. ```tsx const [{ actor, error, isLoading, state }] = useActor(actorName, options); ``` #### Parameters * `actorName`: The name of the actor to connect to (string). * `options`: Optional connection options (same options as `client.actorName.get()`). * `id`: String identifier for the actor instance. * `tags`: Key-value pairs for actor identification. * `params`: Parameters to pass during connection. * `noCreate`: Boolean to prevent actor creation if it doesn't exist. #### Returns Returns an array with a single object containing: * `actor`: The actor handle if connected, or `undefined` if still connecting. * `error`: Any error that occurred during connection. * `isLoading`: Boolean indicating if the connection is in progress. * `state`: String representing the internal connection state ("init", "creating", "created", or "error"). ### `useActorEvent` Hook that subscribes to events from an actor. ```tsx useActorEvent({ actor, event }, cb); ``` #### Parameters * `opts`: Object containing: * `actor`: The actor handle from `useActor`, or undefined. * `event`: The name of the event to subscribe to. * `cb`: Function called when the event is fired. The arguments passed to this function depend on the event type. #### Returns This hook doesn't return a value. The subscription is automatically managed by the hook lifecycle. ## Example Usage ### Basic Counter Example ```tsx import { createClient } from "actor-core/client"; import { createReactActorCore } from "@actor-core/react"; import type { App } from "./app"; import { useState } from "react"; const client = createClient("http://localhost:6420"); const { useActor, useActorEvent } = createReactActorCore(client); function Counter() { // Connect to a counter actor with ID "main-counter" const [{ actor, isLoading }] = useActor("counter", { id: "main-counter" }); const [count, setCount] = useState(0); // Subscribe to counter updates useActorEvent({ actor, event: "newCount" }, (newCount) => { setCount(newCount); }); // Handle increment const handleIncrement = async () => { if (actor) { await actor.increment(1); } }; if (isLoading) return
Loading...
; return (

Current count: {count}

); } ``` ### Error Handling ```tsx function Counter() { const [{ actor, error, isLoading }] = useActor("counter", { id: "main-counter" }); if (isLoading) return
Loading...
; if (error) return
Error: {error.message}
; if (!actor) return
Actor not available
; return (
); } ``` ### Dynamic Actor Selection ```tsx function CounterSelector() { const [counterId, setCounterId] = useState("counter-1"); const [{ actor }] = useActor("counter", { id: counterId }); return (
{actor && }
); } function CounterDisplay({ actor }) { const [count, setCount] = useState(0); useActorEvent({ actor, event: "newCount" }, (newCount) => { setCount(newCount); }); return
Count: {count}
; } ``` ## Next Steps See the [Interacting with Actors](/concepts/interacting-with-actors) documentation for more information about the client API. # Hono Source: https://actorcore.org/integrations/hono [Hono](https://hono.dev/) is a lightweight web framework that works well with ActorCore across multiple deployment platforms. This guide explains how to integrate ActorCore with Hono on different platforms. ## Mounting The ActorCore Router When mounting the ActorCore router at a custom path, you **must** specify the same path in the router configuration using `basePath`: ```typescript // Setup the ActorCore app const app = setup({ actors: { counter }, // IMPORTANT: Must specify the same basePath where your router is mounted basePath: "/my-path" }); // Create a router from the app const { router: actorRouter } = createRouter(app); // Mount at the same path specified in basePath honoApp.route("/my-path", actorRouter); ``` This ensures that WebSocket connections and other functionality work correctly when accessing your actors through the custom path. ## Platform-Specific Examples Each platform has specific requirements for integrating Hono with ActorCore. ### Cloudflare Workers ```typescript import { createRouter } from "@actor-core/cloudflare-workers"; import { setup } from "actor-core"; import { Hono } from "hono"; import counter from "./counter"; // Create your Hono app inside the fetch handler const honoApp = new Hono(); // Add your custom routes honoApp.get("/", (c) => c.text("Welcome to my app!")); honoApp.get("/hello", (c) => c.text("Hello, world!")); // Setup the ActorCore app const app = setup({ actors: { counter }, // IMPORTANT: Must specify the same basePath where your router is mounted basePath: "/my-path" }); // Create a router and handler from the app const { router: actorRouter, ActorHandler } = createRouter(app); // Mount the ActorCore router at /my-path honoApp.route("/my-path", actorRouter); // Export the app type for client usage export type App = typeof app; // IMPORTANT: Must export `ActorHandler` as this exact name export { honoApp as default, ActorHandler }; ``` Make sure to update your client connection URL to include the custom path: ```typescript // If you mounted ActorCore at /my-path import { createClient } from "actor-core/client"; import type { App } from "./src/index"; const client = createClient("https://your-worker.workers.dev/my-path"); ``` For this to work with Cloudflare Workers, your `wrangler.json` **must** include specific Durable Object and KV namespace bindings with the exact names expected by ActorCore: ```json { "name": "counter", "main": "src/index.ts", "compatibility_date": "2025-01-29", "migrations": [ { "new_classes": ["ActorHandler"], "tag": "v1" } ], "durable_objects": { "bindings": [ { "class_name": "ActorHandler", // Must match exported class "name": "ACTOR_DO" // Must use this exact name } ] }, "kv_namespaces": [ { "binding": "ACTOR_KV", // Must use this exact name "id": "YOUR_KV_NAMESPACE_ID" // Replace with your KV ID } ] } ``` ### Node.js ```typescript import { serve } from "@hono/node-server"; import { Hono } from "hono"; import { setup, createRouter } from "@actor-core/nodejs"; import counter from "./counter"; // Create your Hono app const honoApp = new Hono(); // Add your custom routes honoApp.get("/", (c) => c.text("Welcome to my app!")); honoApp.get("/hello", (c) => c.text("Hello, world!")); // Setup the ActorCore app const app = setup({ actors: { counter }, // IMPORTANT: Must specify the same basePath where your router is mounted basePath: "/my-path" }); // Create a router from the app const { router: actorRouter, injectWebSocket } = createRouter(app); // Mount the ActorCore router at /my-path honoApp.route("/my-path", actorRouter); // Export the app type for client usage export type App = typeof app; // Create server with the combined app const server = serve({ fetch: honoApp.fetch, port: 6420, }); // IMPORTANT: Inject the websocket handler into the server injectWebSocket(server); console.log("Server running at http://localhost:6420"); ``` Make sure to update your client connection URL to include the custom path: ```typescript // If you mounted ActorCore at /my-path import { createClient } from "actor-core/client"; import type { App } from "./src/index"; const client = createClient("http://localhost:6420/my-path"); ``` ### Bun ```typescript import { Hono } from "hono"; import { setup, createRouter } from "@actor-core/bun"; import counter from "./counter"; // Create your Hono app const honoApp = new Hono(); // Add your custom routes honoApp.get("/", (c) => c.text("Welcome to my app!")); honoApp.get("/hello", (c) => c.text("Hello, world!")); // Setup the ActorCore app const app = setup({ actors: { counter }, // IMPORTANT: Must specify the same basePath where your router is mounted basePath: "/my-path" }); // Create a router from the app const { router: actorRouter, webSocketHandler } = createRouter(app); // Mount the ActorCore router at /my-path honoApp.route("/my-path", actorRouter); // Export the app type for client usage export type App = typeof app; // Create and start the server export default { port: 6420, fetch: honoApp.fetch, // IMPORTANT: Pass the webSocketHandler to Bun websocket: webSocketHandler, }; ``` Make sure to update your client connection URL to include the custom path: ```typescript // If you mounted ActorCore at /my-path import { createClient } from "actor-core/client"; import type { App } from "./src/index"; const client = createClient("http://localhost:6420/my-path"); ``` # All Integrations Source: https://actorcore.org/integrations/overview ## Integrations ## Drivers # ActorCore Source: https://actorcore.org/introduction Stateful, Scalable, Realtime Backend Framework

ActorCore

Stateful, Scalable, Realtime
Backend Framework

The modern way to build multiplayer, realtime, or AI agent backends.

Runs on Rivet, Durable Objects, Bun, and Node.js. Supports JavaScript, TypeScript, and Rust. Integrates with Hono and Redis.

Get Started
window.copyCommand && window.copyCommand(e.currentTarget)}>
npx create-actor\@latest
```typescript import { actor } from "actor-core"; const chatRoom = actor({ state: { messages: [] }, actions: { // receive an action call from the client sendMessage: (c, username, message) => { // save message to persistent storage c.state.messages.push({ username, message }); // broadcast message to all clients c.broadcast("newMessage", username, message); }, // allow client to request message history getMessages: (c) => c.state.messages } }); ```
Fast in-memory access with built-in durability — no external databases or caches needed. Real-time state updates with ultra-low latency, powered by co-locating compute and data. Integrated support for state, actions, events, scheduling, and multiplayer — no extra boilerplate code needed. Effortless scaling, scale-to-zero, and easy deployments on any serverless runtime.
Features

Everything you need to build realtime, stateful backends

ActorCore provides a solid foundation with the features you'd expect for modern apps.

Feature ActorCore Vanilla Durable Objects Socket.io Redis AWS Lambda
In-Memory State
Persisted State
Actions
Events (Pub/Sub)
Scheduling
Edge Computing ¹
No Vendor Lock

= requires significant boilerplate code or external service

¹ = on supported platforms

Overview

```typescript Actor import { actor, setup } from "actor-core"; const chatRoom = actor({ state: { messages: [] }, actions: { // receive an action call from the client sendMessage: (c, username: string, message: string) => { // save message to persistent storage c.state.messages.push({ username, message }); // broadcast message to all clients c.broadcast("newMessage", username, message); }, // allow client to request message history getMessages: (c) => c.state.messages }, }); export const app = setup({ actors: { chatRoom }, cors: { origin: "http://localhost:8080" } }); export type App = typeof app; ``` ```typescript Browser (TypeScript) import { createClient } from "actor-core/client"; import type { App } from "../src/index"; const client = createClient(/* manager endpoint */); // connect to chat room const chatRoom = await client.chatRoom.get({ channel: "random" }); // listen for new messages chatRoom.on("newMessage", (username: string, message: string) => console.log(`Message from ${username}: ${message}`), ); // send message to room await chatRoom.sendMessage("william", "All the world's a stage."); ``` ```javascript Browser (JavaScript) import { createClient } from "actor-core/client"; const client = createClient(/* manager endpoint */); // connect to chat room const chatRoom = await client.chatRoom.get(); // listen for new messages chatRoom.on("newMessage", (username, message) => console.log(`Message from ${username}: ${message}`), ); // send message to room await chatRoom.sendMessage("william", "All the world's a stage."); ``` {/*

Overview

Resources to help you use LLMs with ActorCore to build AI agents and tools. The following guides provide information on how to use ActorCore with your vibe coding workflow of choice. */}

Platforms

Community & Support

{" "}
# Using Claude Code with ActorCore Source: https://actorcore.org/llm/claude [Claude Code](https://claude.ai/code) is a powerful CLI tool from Anthropic that integrates Claude directly into your development workflow. When working with ActorCore, you can enhance Claude's understanding of the codebase by providing project-specific context. ## Setting Up CLAUDE.md for ActorCore The `CLAUDE.md` file serves as a memory file for Claude Code, containing important information about your project that persists between sessions. To set up an effective `CLAUDE.md` file for ActorCore: Copy the ActorCore [prompt.txt](/llm/prompt) Create a `CLAUDE.md` file in your project root Paste the template into `CLAUDE.md` Run Claude Code and ask a simple question like `What lifecycle hook is used when an actor is first created?` to confirm it's reading your CLAUDE.md file correctly ## Example Commands for ActorCore Here are some useful ways to leverage Claude Code with ActorCore: ### Understand the Codebase ```bash # Get an overview of ActorCore's architecture claude "explain the architecture of ActorCore and how the different topologies work" # Understand how actors communicate claude "explain how actors communicate with each other in the coordinate topology" # Learn about specific concepts claude "explain the lifecycle hooks for actors and when each one is called" ``` ### Find Relevant Code ```bash # Find implementations of specific components claude "find the files that implement the Redis driver" # Locate examples of specific patterns claude "show me examples of RPC methods in the codebase" # Understand actor state management claude "explain how actor state is persisted between restarts" ``` ### Add New Features ```bash # Create a new actor implementation claude "help me create a new actor for managing user sessions" # Add authentication to an actor claude "show me how to add authentication to my actor's _onBeforeConnect method" # Implement error handling claude "help me implement proper error handling for my actor's RPC methods" ``` ### Debug Issues ```bash # Diagnose connection problems claude "my actor connections are dropping, help me debug why" # Fix state persistence issues claude "my actor state isn't persisting between restarts, what could be wrong?" # Address scaling problems claude "my actors are using too much memory, how can I optimize them?" ``` ## Additional Resources * [Claude Code Documentation](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview) * [Claude Code Tutorials](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/tutorials) # Using Cursor with ActorCore Source: https://actorcore.org/llm/cursor This guide shows how to integrate ActorCore with Cursor to enhance your developer experience through AI-assisted coding. ## Setting Up Cursor Rules for ActorCore Cursor rules allow you to customize how the AI assistant behaves when working with ActorCore code. By providing project-specific context, you'll get more accurate and relevant suggestions. ### Project Rules (Recommended) Project rules are stored in the `.cursor/rules` directory and apply to specific files or folders in your project. Open your ActorCore project in Cursor Press `Cmd + Shift + P` (Mac) or `Ctrl + Shift + P` (Windows/Linux). Select `New Cursor Rule` and name it `actorcore-development-guide`. Set `Auto Attach` to `**/*.ts`. Paste [prompt.txt](/llm/prompt) into the rule. ### Global Rules If you frequently work with ActorCore across multiple projects, you can add ActorCore-specific rules globally: Open Cursor and go to `Cursor Settings` > `Rules` > `User Rules` Paste [prompt.txt](/llm/prompt) into the textbox Global rules will apply to all of your projects when using Cursor. ## Example Queries for ActorCore Here are some useful prompts to try with Cursor when working with ActorCore: ### Understand the Codebase ``` # Get an overview of ActorCore's architecture Explain the architecture of ActorCore and how the different topologies work # Understand how actors communicate Explain how actors communicate with each other in the coordinate topology # Learn about specific concepts Explain the lifecycle hooks for actors and when each one is called ``` ### Find Relevant Code ``` # Find implementations of specific components Find the files that implement the Redis driver # Locate examples of specific patterns Show me examples of RPC methods in the codebase # Understand actor state management Explain how actor state is persisted between restarts ``` ### Add New Features ``` # Create a new actor implementation Help me create a new actor for managing user sessions # Add authentication to an actor Show me how to add authentication to my actor's _onBeforeConnect method # Implement error handling Help me implement proper error handling for my actor's RPC methods ``` ### Debug Issues ``` # Diagnose connection problems My actor connections are dropping, help me debug why # Fix state persistence issues My actor state isn't persisting between restarts, what could be wrong? # Address scaling problems My actors are using too much memory, how can I optimize them? ``` ## Additional Resources * [Cursor Rules Documentation](https://docs.cursor.com/context/rules-for-ai) # Docs As Markdown for LLMs Source: https://actorcore.org/llm/docs-as-markdown ActorCore documentation is designed to be easily accessible for Large Language Models (LLMs). All documentation pages are automatically available as plain Markdown files by simply appending `.md` to the URL. ## How to Access To access any ActorCore documentation page as Markdown: 1. Navigate to the regular documentation URL (e.g., `https://actorcore.org/concepts/state`) 2. Append `.md` to the URL (e.g., `https://actorcore.org/concepts/state.md`) This will return the raw Markdown content of the documentation page. ## Use Cases This feature enables seamless integration with AI tools by providing documentation in clean Markdown format. Key uses include: * **Context for LLM chats**: Importing relevant docs into LLM chat sessions for specific context * **Web crawling**: Enabling efficient crawling of documentation content * **AI agents**: Building AI agents with access to ActorCore documentation ## Additional Resources * [prompt.txt](/llm/prompt) * [llms.txt](/llm/llms) * [llms-full.txt](/llm/llms-full) # llms.txt Source: https://actorcore.org/llm/llms # llms-full.txt Source: https://actorcore.org/llm/llms-full # prompt.txt Source: https://actorcore.org/llm/prompt ## Using `prompt.txt` for ActorCore The `prompt.txt` file provides LLMs with comprehensive information about ActorCore's conventions, terminology, and best practices. To use it: 1. Copy the contents below to your clipboard 2. Paste it into your preferred AI assistant (Claude, ChatGPT, Cursor rules, Windsurf Rules, etc.) 3. Ask your ActorCore development questions after the prompt This structured information helps AI tools provide more accurate and contextually relevant guidance for your ActorCore development tasks. ## AI Editor Guides Read the integration guide for your editor of choice: ## `prompt.txt` ```markdown prompt.txt # ActorCore Development Guide This guide contains essential information for working with the ActorCore project. ## Project Naming and Terminology - Use `ActorCore` when referring to the project in documentation and plain English - Use `actor-core` (kebab-case) when referring to the project in code, package names, and imports ### Core Concepts - **Actor**: A stateful, long-lived entity that processes messages and maintains state - **Manager**: Component responsible for creating, routing, and managing actor instances - **Action**: Method for an actor to expose callable functions to clients - **Event**: Asynchronous message sent from an actor to connected clients - **Alarm**: Scheduled callback that triggers at a specific time ## Build Commands - **Type Check:** `yarn check-types` - Verify TypeScript types - **Check specific package:** `yarn check-types -F actor-core` - Check only specified package - **Build:** `yarn build` - Production build using Turbopack - **Build specific package:** `yarn build -F actor-core` - Build only specified package - **Format:** `yarn fmt` - Format code with Biome ## Driver Implementations Available driver implementations: - **Memory**: In-memory implementation for development and testing - **Redis**: Production-ready implementation using Redis for persistence and pub/sub - **Cloudflare Workers**: Uses Durable Objects for actor state persistence - **Rivet**: Fully-managed cloud platform with built-in scaling and monitoring ## Platform Support ActorCore supports multiple runtime environments: - **NodeJS**: Standard Node.js server environment - **Cloudflare Workers**: Edge computing environment - **Bun**: Fast JavaScript runtime alternative to Node.js - **Rivet**: Cloud platform with built-in scaling and management ## Package Import Resolution When importing from workspace packages, always check the package's `package.json` file under the `exports` field to determine the correct import paths: 1. Locate the package's `package.json` file 2. Find the `exports` object which maps subpath patterns to their file locations 3. Use these defined subpaths in your imports rather than direct file paths ## Code Style Guidelines - **Formatting:** Uses Biome for consistent formatting - **Imports:** Organized imports enforced, unused imports warned - **TypeScript:** Strict mode enabled, target ESNext - **Naming:** - camelCase for variables, functions - PascalCase for classes, interfaces, types - UPPER_CASE for constants - **Error Handling:** - Use `UserError` for client-safe errors - Use `InternalError` for internal errors ## Project Structure - Monorepo with Yarn workspaces and Turborepo - Core code in `packages/actor-core/` - Platform implementations in `packages/platforms/` - Driver implementations in `packages/drivers/` ## State Management - Each actor owns and manages its own isolated state via `c.state` - State is automatically persisted between action calls - State is initialized via `createState` function or `state` constant - Only JSON-serializable types can be stored in state - Use `onStateChange` to react to state changes ## Authentication and Security - Authentication is handled through the `onBeforeConnect` lifecycle hook - Connection state is accessed with `c.conn.state` - Access control should be implemented for each action - Throwing an error in `onBeforeConnect` will abort the connection - Use `UserError` for safe error messages to clients - Use data validation libraries like zod for input validation ## Actions and Events - **Actions**: Used for clients to call actor functions - **Events**: For actors to publish updates to clients - Actions are defined in the `actions` object of the actor configuration - Helper functions outside the `actions` object are not callable by clients - Broadcasting is done via `c.broadcast(name, data)` - Specific client messaging uses `conn.send(name, data)` - Clients subscribe to events with `actor.on(eventName, callback)` ## Lifecycle Hooks - `createState()`: Function that returns initial actor state - `onStart(c)`: Called any time actor is started (after restart/upgrade) - `onStateChange(c, newState)`: Called when actor state changes - `onBeforeConnect(c)`: Called when new client connects - `onConnect(c)`: Executed after client connection succeeds - `onDisconnect(c)`: Called when client disconnects ## Actor Management - App is configured with actors using `setup({ actors: { actorName }})` followed by `serve(app)` - Actors are accessed by client using `client.actorName.get()` - Actors can pass an ID parameter or object with `client.actorName.get(id)` or `client.actorName.get({key: value})` - Actors can be shut down with `c.shutdown()` from within the actor ## Scaling and Architecture Guidelines - Each actor should have a single responsibility - Keep state minimal and relevant to the actor's core function - Use separate actors for different entity types (users, rooms, documents) - Avoid too many cross-actor communications - Use appropriate topology based on your scaling needs ## Scheduling - Schedule future events with `c.after(duration, fn, ...args)` - Schedule events for specific time with `c.at(timestamp, fn, ...args)` - Scheduled events persist across actor restarts ## CORS Configuration - Configure CORS to allow cross-origin requests in production - Set allowed origins, methods, headers, and credentials - For development, use `cors: { origin: "http://localhost:3000" }` ## Development Best Practices - Prefer functional actor pattern with `actor({ ... })` syntax - Use zod for runtime type validation - Use `assertUnreachable(x: never)` for exhaustive type checking - Add proper JSDoc comments for public APIs - Run `yarn check-types` regularly during development - Use `tsx` CLI to execute TypeScript scripts directly ``` # Using Windsurf with ActorCore Source: https://actorcore.org/llm/windsurf This guide shows how to integrate ActorCore with Windsurf to enhance your developer experience through AI-assisted coding. ## Setting Up Windsurf Rules for ActorCore Windsurf rules allow you to customize how the AI assistant (Cascade) behaves when working with ActorCore code. By providing project-specific context, you'll get more accurate and relevant suggestions. ### Workspace Rules (Recommended) Workspace rules are stored in the `.windsurfrules` file in your project root and apply to your local workspace. Open your ActorCore project in Windsurf Navigate to "Windsurf - Settings" (at the bottom right) > "Settings" > "Cascade" > "Set Workspace AI Rules" and click "Edit Rules." This will create a `.windsurfrules` file in your project root. Paste [prompt.txt](/llm/prompt) into the `.windsurfrules` file. ### Global Rules If you frequently work with ActorCore across multiple projects, you can add ActorCore-specific rules globally: Navigate to "Windsurf - Settings" (at the bottom right) > "Settings" > "Cascade" > "Set Global AI Rules" and click "Edit Rules." Paste [prompt.txt](/llm/prompt) into the `global_rules.md` file. Global rules will apply to all of your projects when using Windsurf. ## Example Queries for ActorCore Here are some useful prompts to try with Windsurf when working with ActorCore: ### Understand the Codebase ``` # Get an overview of ActorCore's architecture Explain the architecture of ActorCore and how the different topologies work # Understand how actors communicate Explain how actors communicate with each other in the coordinate topology # Learn about specific concepts Explain the lifecycle hooks for actors and when each one is called ``` ### Find Relevant Code ``` # Find implementations of specific components Find the files that implement the Redis driver # Locate examples of specific patterns Show me examples of RPC methods in the codebase # Understand actor state management Explain how actor state is persisted between restarts ``` ### Add New Features ``` # Create a new actor implementation Help me create a new actor for managing user sessions # Add authentication to an actor Show me how to add authentication to my actor's _onBeforeConnect method # Implement error handling Help me implement proper error handling for my actor's RPC methods ``` ### Debug Issues ``` # Diagnose connection problems My actor connections are dropping, help me debug why # Fix state persistence issues My actor state isn't persisting between restarts, what could be wrong? # Address scaling problems My actors are using too much memory, how can I optimize them? ``` ## Additional Resources * [Windsurf Rules Documentation](https://docs.codeium.com/windsurf/memories#windsurfrules) # Bun Source: https://actorcore.org/platforms/bun ActorCore is still pre-v1.0. Please help us by report bugs on [GitHub Issues](https://github.com/rivet-gg/actor-core/issues)! ## Create New Project Run this command: ```sh bun bun create actor@latest ``` Follow the prompts: 1. **Where would you like to create your project?** - Choose your project directory 2. **To which platform would you like to deploy?** - Select Bun 3. **Which template would you like to use?** - Select counter, or your template of choice The CLI will set up your project and install all dependencies automatically. Start your development server with: ```sh cd your-project bun dev ``` This will start your ActorCore server in development mode. In a separate terminal, run the auto-generated test client: ```sh bun tests/client.ts # Outputs: # Event: 5 # Action: 5 ``` Run this again to see the state update. To build for production: ```sh bun build bun start ``` *Request a guide for deploying Bun to your preferred cloud provider on [GitHub Discussions](https://github.com/rivet-gg/actor-core/discussions).* ## Integrating With Existing Projects If you already have a Bun project and want to add ActorCore, you can follow these steps for manual integration. This approach gives you more control over how ActorCore fits into your existing codebase. Install Bun [here](https://bun.sh/). ```sh # Install ActorCore bun add actor-core @actor-core/bun ``` Create a new file for your actor at `src/app.ts`: ```typescript src/app.ts import { actor, setup } from "actor-core"; // Create actor const counter = actor({ state: { count: 0 }, actions: { increment: (c, x: number) => { c.state.count += x; c.broadcast("newCount", c.state.count); return c.state.count; } } }); // Create the application export const app = setup({ actors: { counter }, cors: { origin: "http://localhost:8080" } }); // Export app type for client usage export type App = typeof app; ``` Create `src/index.ts` to start your ActorCore server with: ```typescript src/index.ts import { setup, createHandler } from "@actor-core/bun" import { app } from "./app"; // Create a handler for HTTP requests export default createHandler(app); ``` If you already have an existing application and want to mount ActorCore on a subpath, see our [Hono integration guide](/integrations/hono). Remember to specify the same path in `config.basePath` as where you mount the router. Create a client to connect to your actor in `src/client.ts`: ```typescript src/client.ts import { createClient } from "actor-core/client"; import type { App } from "./app"; async function main() { const client = createClient("http://localhost:6420"); const counter = await client.counter.get(); counter.on("newCount", (count: number) => console.log("Event:", count)); const out = await counter.increment(5); console.log("Action:", out); await counter.dispose(); } main(); ``` Start your development server with: ```sh bun src/index.ts ``` Then run the client in another terminal with: ```sh bun src/client.ts # Outputs: # Event: 5 # Action: 5 ``` Run this again to see the state update. ## Available Regions Bun can only run in one region at the moment. See [Rivet](/platforms/rivet) and [Cloudflare Workers](/platforms/cloudflare-workers) for supporting multiple regions. ## Next Steps # Durable Objects Source: https://actorcore.org/platforms/cloudflare-workers The Cloudflare Durable Objects platform leverages Cloudflare Workers to run ActorCore. ActorCore relies on features only available in the Workers Paid plan ([more info](https://developers.cloudflare.com/durable-objects/platform/pricing/)). To deploy hobby projects with ActorCore for free, try deploying to [Rivet](/platforms/rivet). ActorCore is still pre-v1.0. Please help us by report bugs on [GitHub Issues](https://github.com/rivet-gg/actor-core/issues)! ## Create New Project Run this command: ```sh npx npx create-actor@latest ``` ```sh npm npm create actor@latest ``` ```sh pnpm pnpm create actor@latest ``` ```sh yarn yarn create actor@latest ``` ```sh bun bun create actor@latest ``` Follow the prompts: 1. **Where would you like to create your project?** - Choose your project directory 2. **To which platform would you like to deploy?** - Select Cloudflare Workers 3. **Which template would you like to use?** - Select counter, or your template of choice The CLI will set up your project and install all dependencies automatically. Start your development server with: ```sh cd your-project npm run dev ``` This will start your ActorCore server in development mode with Wrangler. In a separate terminal, run the auto-generated test client: ```sh npx tsx scripts/connect.ts # Outputs: # Event: 5 # Action: 5 ``` Run this again to see the state update. 1. Create a new KV namespace with: ```sh npx wrangler kv namespace create ACTOR_KV ``` 2. After creating the KV namespace, you will receive an ID. Update your `wrangler.json` file by replacing the placeholder in the `kv_namespaces` section with this ID. It should look like this: ```json { "kv_namespaces": [ { "binding": "ACTOR_KV", "id": "your-namespace-id-here" // Replace with your actual ID } ] // ...etc... } ``` 3. Deploy your project to Cloudflare Workers by running: ```sh npx wrangler deploy ``` 4. Update `tests/client.ts` (or wherever your client lives) to use the deployed endpoint. Replace the local endpoint in `client.ts` with your Cloudflare Workers URL: ```typescript import { createClient } from "actor-core/client"; import type { App } from "../src/index"; const client = createClient("https://your-worker-subdomain.workers.dev"); ``` Ensure you replace `your-worker-subdomain` with the actual subdomain assigned to your worker. ## Integrating With Existing Projects If you already have a Cloudflare Workers project and want to add ActorCore, you can follow these steps for manual integration. This approach gives you more control over how ActorCore fits into your existing Cloudflare Workers project. ```sh npm # Install ActorCore npm add actor-core @actor-core/cloudflare-workers ``` ```sh pnpm # Install ActorCore pnpm add actor-core @actor-core/cloudflare-workers ``` ```sh yarn # Install ActorCore yarn add actor-core @actor-core/cloudflare-workers ``` ```sh bun # Install ActorCore bun add actor-core @actor-core/cloudflare-workers ``` Create a new file for your actor at `src/counter.ts`: ```typescript src/app.ts import { actor, setup } from "actor-core"; // Create actor const counter = actor({ state: { count: 0 }, actions: { increment: (c, x: number) => { c.state.count += x; c.broadcast("newCount", c.state.count); return c.state.count; } } }); // Create the application export const app = setup({ actors: { counter }, cors: { origin: "http://localhost:8080" } }); // Export app type for client usage export type App = typeof app; ``` Update both `src/index.ts` and `wrangler.json` to look like this: ```typescript src/index.ts import { setup, createHandler } from "@actor-core/cloudflare-workers"; import { app } from "./app"; // Create handlers for Cloudflare Workers const { handler, ActorHandler } = createHandler(app); // Export the handlers for Cloudflare export { handler as default, ActorHandler }; ``` ```json wrangler.json { "name": "my-project", "main": "src/index.ts", "compatibility_date": "2025-01-29", "migrations": [ { "new_classes": ["ActorHandler"], "tag": "v1" } ], "durable_objects": { "bindings": [ { "class_name": "ActorHandler", "name": "ACTOR_DO" } ] }, "kv_namespaces": [ { "binding": "ACTOR_KV", "id": "FILL_ME_IN_WHEN_YOU_DEPLOY" } ] } ``` In `src/index.ts`, `handler` is used to handle HTTP requests made to the worker. `ActorHandler` is a Durable Object used to provide state for your actors. If you already have an existing application and want to mount ActorCore on a subpath, see our [Hono integration guide](/integrations/hono). Remember to specify the same path in `config.basePath` as where you mount the router. To start your development server, run: ```sh npx wrangler dev ``` Now, write a file named `client.ts` with this: ```typescript client.ts import { createClient } from "actor-core/client"; import type { App } from "./src/index"; async function main() { const client = createClient("http://localhost:8787"); const counter = await client.counter.get(); counter.on("newCount", (count: number) => console.log("Event:", count)); const out = await counter.increment(5); console.log("Action:", out); await counter.dispose(); } main(); ``` Run the file in a second terminal with: ```sh npx tsx client.ts # Outputs: # Event: 5 # Action: 5 ``` Run this again to see the state update. 1. Create a new KV namespace with: ```sh npx wrangler kv namespace create ACTOR_KV ``` 2. After creating the KV namespace, you will receive an ID. Update your `wrangler.json` file by replacing the placeholder in the `kv_namespaces` section with this ID. It should look like this: ```json { "kv_namespaces": [ { "binding": "ACTOR_KV", "id": "your-namespace-id-here" // Replace with your actual ID } ] // ...etc... } ``` 3. Deploy your project to Cloudflare Workers by running: ```sh npx wrangler deploy ``` 4. Update `tests/client.ts` (or wherever your client lives) to use the deployed endpoint. Replace the local endpoint in `client.ts` with your Cloudflare Workers URL: ```typescript import { createClient } from "actor-core/client"; import type { App } from "../src/index"; const client = createClient("https://your-worker-subdomain.workers.dev"); ``` Ensure you replace `your-worker-subdomain` with the actual subdomain assigned to your worker. ## Accessing Cloudflare Context And Bindings [Cloudflare-specific `DurableObjectState`](https://developers.cloudflare.com/durable-objects/api/state/) and environment bindings can be accessed from `createVars`. ```typescript import { actor } from "actor-core"; const myActor = actor({ // Load Cloudflare-specific variables createVars: (c, cloudflare) => ({ ctx: cloudflare.ctx, env: cloudflare.env, }), actions: { foo: async (c) => { // Access DurableObjectState await c.vars.ctx.storage.get("foo"); // Access bindings await c.vars.env.MY_BUCKET.put(key, "foo"); }, } }); ``` ## Available Regions See available regions [here](https://developers.cloudflare.com/durable-objects/reference/data-location/#supported-locations-1). Cloudflare does not guarantee your code will run in the requested region. ## Next Steps # Node.js Source: https://actorcore.org/platforms/nodejs ActorCore is still pre-v1.0. Please help us by report bugs on [GitHub Issues](https://github.com/rivet-gg/actor-core/issues)! ## Create New Project Run this command: ```sh npx npx create-actor@latest ``` ```sh npm npm create actor@latest ``` ```sh pnpm pnpm create actor@latest ``` ```sh yarn yarn create actor@latest ``` ```sh bun bun create actor@latest ``` Follow the prompts: 1. **Where would you like to create your project?** - Choose your project directory 2. **To which platform would you like to deploy?** - Select Node.js 3. **Which template would you like to use?** - Select counter, or your template of choice The CLI will set up your project and install all dependencies automatically. Start your development server with: ```sh cd your-project npm run dev ``` This will start your ActorCore server in development mode. In a separate terminal, run the auto-generated test client: ```sh npx tsx scripts/connect.ts # Outputs: # Event: 5 # RPC: 5 ``` Run this again to see the state update. *Request a guide for deploying Node.js to your preferred cloud provider on [GitHub Discussions](https://github.com/rivet-gg/actor-core/discussions).* ## Integrate With Existing Projects If you already have a Node.js project and want to add ActorCore, you can follow these steps for manual integration. This approach gives you more control over how ActorCore fits into your existing codebase. ```sh npm # Install ActorCore npm add actor-core @actor-core/nodejs ``` ```sh pnpm # Install ActorCore pnpm add actor-core @actor-core/nodejs ``` ```sh yarn # Install ActorCore yarn add actor-core @actor-core/nodejs ``` ```sh bun # Install ActorCore bun add actor-core @actor-core/nodejs ``` Create a new file for your actor at `src/app.ts`: ```typescript src/app.ts import { actor, setup } from "actor-core"; // Create actor const counter = actor({ state: { count: 0 }, actions: { increment: (c, x: number) => { c.state.count += x; c.broadcast("newCount", c.state.count); return c.state.count; } } }); // Create the application export const app = setup({ actors: { counter }, cors: { origin: "http://localhost:8080" } }); // Export app type for client usage export type App = typeof app; ``` Create `src/index.ts` to start your ActorCore server with: ```typescript src/index.ts import { setup, serve } from "@actor-core/nodejs"; import { app } from "./app"; serve(app); ``` If you already have an existing application and want to mount ActorCore on a subpath, see our [Hono integration guide](/integrations/hono). Remember to specify the same path in `config.basePath` as where you mount the router. Create a client to connect to your actor in `src/client.ts`: ```typescript src/client.ts import { createClient } from "actor-core/client"; import type { App } from "./app"; async function main() { const client = createClient("http://localhost:6420"); const counter = await client.counter.get(); counter.on("newCount", (count: number) => console.log("Event:", count)); const out = await counter.increment(5); console.log("Action:", out); await counter.dispose(); } main(); ``` Start your development server with: ```sh npx tsx src/index.ts ``` Then run the client in another terminal with: ```sh npx tsx src/client.ts # Outputs: # Event: 5 # Action: 5 ``` Run this again to see the state update. ## Available Regions Node.js can only run in one region at the moment. See [Rivet](/platforms/rivet) and [Cloudflare Workers](/platforms/cloudflare-workers) for supporting multiple regions. ## Next Steps # Rivet Source: https://actorcore.org/platforms/rivet ActorCore is still pre-v1.0. Please help us by report bugs on [GitHub Issues](https://github.com/rivet-gg/actor-core/issues)! ## Create New Project Run this command: ```sh npx npx create-actor@latest ``` ```sh npm npm create actor@latest ``` ```sh pnpm pnpm create actor@latest ``` ```sh yarn yarn create actor@latest ``` ```sh bun bun create actor@latest ``` Follow the prompts: 1. **Where would you like to create your project?** - Choose your project directory 2. **To which platform would you like to deploy?** - Select Rivet 3. **Which template would you like to use?** - Select counter, or your template of choice The CLI will set up your project and install all dependencies automatically. Deploy your project to Rivet with: ```sh cd your-project npm run deploy ``` This will prompt you to: 1. Login to Rivet and create a project 2. Select an environment After successful deployment, you'll receive important URLs: * **Actor Manager URL**: Used for client connections to your actors * **Actors Dashboard**: Manage deployed actors and view their status * **Builds Dashboard**: Monitor build history and deployment logs Make sure to save your Actor Manager URL as you'll need it to connect from your client code. Update `tests/client.ts` to use the deployed endpoint. Replace the local endpoint in client.ts with your Rivet deployment URL: ```typescript tests/client.ts import { createClient } from "actor-core/client"; import type { App } from "../src/app"; // Replace this URL with your Actor Manager URL from the deployment output const client = createClient("https://your-actor-manager-url-from-deployment.rivet.run:443"); async function main() { const counter = await client.counter.get(); counter.on("newCount", (count: number) => console.log("Event:", count)); const out = await counter.increment(5); console.log("Action:", out); await counter.dispose(); } main(); ``` Then run the client: ```sh npx tsx scripts/connect.ts # Outputs: # Event: 5 # Action: 5 ``` Run this again to see the state update as the actor maintains its state between calls. Monitor your actors, builds, and performance through the Rivet Hub using the links provided in the deployment output: * **Actors**: View and manage your deployed actors * **Builds**: Review build history and logs * **Actor Manager**: Access the actor manager endpoint for direct connections Visit the [Rivet Dashboard](https://hub.rivet.gg) to view your project. ![Dashboard](https://mintlify.s3.us-west-1.amazonaws.com/rivet-c4d395ab/media/platforms/rivet/dash.png) ## Accessing Rivet Context [Rivet's `ActorContext`](https://rivet.gg/docs/javascript-runtime#the-actor-context-object) can be accessed from `createVars`. ```typescript import { actor } from "actor-core"; const myActor = actor({ // Load Rivet-specific variables createVars: (c, rivet) => ({ rivet: rivet.ctx, }), actions: { foo: async (c) => { // Access ActorContext c.log.info(`Region: ${c.vars.rivet.metadata.region.name}`); await c.vars.rivet.kv.get("foo"); }, } }); ``` ## Available Regions Rivet supports deploying your actors to multiple regions automatically. You can specify region preferences in your Rivet project settings in the Rivet Hub. See available regions [here](https://rivet.gg/docs/regions). ## Next Steps # Enterprise Source: https://actorcore.org/support/enterprise ActorCore is developed and maintained by [Rivet](https://rivet.gg), offering comprehensive enterprise support and deployment solutions tailored to your organization's needs. Our enterprise services include: * Dedicated technical support * Custom deployment configurations * Training and implementation assistance For enterprise inquiries, please contact our solutions team at [rivet.gg/sales](https://rivet.gg/sales).