Actor
Actor
Overview
Actors are stateful compute units that maintain persistent data and handle requests with a unique identity. Each actor instance remembers its state between requests and can coordinate complex workflows over time.
Actors serve as persistent “mini-services” where each user session, shopping cart, or game room operates as its own isolated instance. When requests arrive, you route them to specific actors by ID, and those actors maintain all relevant state.
Key benefits include persistent state without external databases, unique identity for request routing, scheduled operations through built-in alarms, and complete isolation between instances.
Prerequisites
- Active Raindrop project with manifest configuration
- Understanding of TypeScript classes and async/await patterns
- Familiarity with stateful vs stateless service architectures
- Knowledge of actor model concepts and distributed systems
Creating/Getting Started
Define actors in your manifest file to enable stateful request handling:
application "user-sessions" { actor "session-manager" { visibility = "public" domain { fqdn = "sessions.example.com" } }
service "api" { visibility = "public" domain { fqdn = "api.example.com" } }}
Generate the actor implementation:
raindrop build generate
This creates the base actor class structure:
export class SessionManager extends Actor<Env> { async fetch(request: Request): Promise<Response> { return new Response('fetch method not implemented for actors', { status: 501 }); }}
Accessing/Basic Usage
Access actors through services that route requests to specific instances:
export default class extends Service<Env> { async fetch(request: Request): Promise<Response> { // Extract user ID from request const userId = this.getUserId(request);
// Get specific actor instance const actorId = this.env.SESSION_MANAGER.idFromName(userId); const actor = this.env.SESSION_MANAGER.get(actorId);
// Call custom actor methods return await actor.handleSession(request); }
private getUserId(request: Request): string { const url = new URL(request.url); return url.searchParams.get('userId') || 'anonymous'; }}
Create custom methods in your actor class:
export class SessionManager extends Actor<Env> { async fetch(request: Request): Promise<Response> { return new Response('fetch method not implemented for actors', { status: 501 }); }
async handleSession(request: Request): Promise<Response> { // Access persistent state const sessionData = await this.state.storage.get('session');
if (!sessionData) { // Initialize new session await this.state.storage.put('session', { userId: this.state.id.name, createdAt: Date.now(), lastActivity: Date.now() }); }
return new Response(JSON.stringify(sessionData)); }}
## Core Concepts
Actors follow the actor model pattern where each instance maintains isolated state and processes requests sequentially. This architecture ensures data consistency without complex locking mechanisms.
**Actor Identity**: Each actor has a unique ID that determines routing. You can create predictable IDs using `idFromName(string)` or generate random IDs with `newUniqueId()`.
**State Persistence**: Actor state persists across requests and survives restarts. The storage API provides key-value operations with atomic guarantees within a single actor instance.
**Request Routing**: Services route requests to specific actor instances based on business logic (user ID, session token, etc.). Multiple requests to the same actor ID reach the same instance.
**Alarm System**: Actors can schedule future operations using alarms. When an alarm triggers, the actor's `alarm()` method executes automatically.
## Domain Configuration
Actors can be configured with custom domains for direct external access, similar to services. This allows clients to communicate directly with specific actor instances.
### Domain Block Structure
Define domains within a `domain` block in your actor configuration:
```hclactor "my-actor" { visibility = "public" domain { fqdn = "sessions.mycompany.com" # Custom domain }}
# OR
actor "my-actor" { visibility = "public" domain { cname = "my-sessions" # Auto-generated subdomain }}
Configuration Requirements:
- Each
domain
block must specify eithercname
ORfqdn
(but not both) - Domain access is automatically enabled when a domain block is present
fqdn
: Fully qualified domain name (e.g., “sessions.example.com”)cname
: Subdomain prefix that generates “<cname>.<org-id>.lmapp.run
”
Domain Validation Rules
Actor domain configurations follow the same validation rules as services:
FQDN Validation:
- Must be a valid domain name format
- Maximum length: 63 characters
- Pattern:
/^[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z]{2,6}$/
CNAME Validation:
- Must be a valid subdomain prefix
- Maximum length: ~35 characters (63 minus platform suffix)
- Pattern validates as subdomain format
Actor Domain Examples
application "stateful-app" { # User session actor with custom domain actor "user-session" { visibility = "public" domain { fqdn = "sessions.myapp.com" } }
# Game room actor with auto-generated subdomain actor "game-room" { visibility = "public" domain { cname = "game-rooms" } # Results in: game-rooms.org_abc123.lmapp.run }
# Private actor accessible only through bindings actor "data-processor" { # No domain = internal access only }}
Storage Management
Actor storage provides persistent key-value operations that survive restarts and maintain data consistency across requests.
get(key)
Retrieves values by key with support for batch operations and type safety:
// Get single value with type safetyconst userName = await this.state.storage.get<string>("user:name");
// Get multiple values atomicallyconst values = await this.state.storage.get<UserData>(["user:123", "user:456"]);
// Handle missing valuesconst data = await this.state.storage.get("session") ?? defaultSessionData;
Parameters:
key
:string | string[]
- Single key or array for batch retrieval- Generic type
T
- Expected value type for type safety
Returns: T | undefined
for single keys, Map<string, T>
for multiple keys
put(key, value)
Stores values with atomic guarantees for single actor instance operations:
// Store single valueawait this.state.storage.put("user:123", { name: "John Doe", lastLogin: Date.now()});
// Batch storage operationawait this.state.storage.put({ "session:abc": sessionData, "preferences:123": userPrefs, "cache:latest": cacheData});
Parameters:
key
:string | Record<string, any>
- Key or object with key-value pairsvalue
:any
- Value to store (omit for batch operations)
list(options?)
Lists stored keys with filtering and pagination support:
// List all dataconst allData = await this.state.storage.list();
// Filter by prefix with paginationconst userSessions = await this.state.storage.list({ prefix: "session:", limit: 50, start: "session:abc123"});
// Reverse chronological orderconst recentActivity = await this.state.storage.list({ prefix: "activity:", reverse: true, limit: 10});
Options:
start
:string
- Start from this key (inclusive)startAfter
:string
- Start after this key (exclusive)end
:string
- End before this key (exclusive)prefix
:string
- Filter keys by prefixreverse
:boolean
- List in reverse orderlimit
:number
- Maximum keys to return
Returns: Map<string, T>
of matching key-value pairs
delete(key)
Removes keys with confirmation of deletion count:
// Delete single key (returns boolean)const wasDeleted = await this.state.storage.delete("temp:data");
// Delete multiple keys (returns count)const deletedCount = await this.state.storage.delete([ "session:expired1", "session:expired2"]);
Parameters:
key
:string | string[]
- Key or array of keys to delete
Returns: boolean
for single keys, number
(count) for multiple keys
Alarm Scheduling
Schedule future operations that trigger automatically through the actor’s alarm()
method.
getAlarm()
Check the currently scheduled alarm time:
const alarmTime = await this.state.storage.getAlarm();if (alarmTime) { const timeRemaining = alarmTime - Date.now(); console.log(`Alarm in ${timeRemaining}ms`);}
Returns: number | null
- Unix timestamp or null if no alarm set
setAlarm(scheduledTime)
Schedule an alarm for automatic execution:
// Schedule 30 minutes from nowconst thirtyMinutes = Date.now() + (30 * 60 * 1000);await this.state.storage.setAlarm(thirtyMinutes);
// Schedule using Date objectawait this.state.storage.setAlarm( new Date('2024-12-31T23:59:59Z').getTime());
Parameters:
scheduledTime
:number | Date
- Unix timestamp or Date object
deleteAlarm()
Cancel the scheduled alarm:
await this.state.storage.deleteAlarm();
Implement the alarm handler in your actor class:
export class SessionManager extends Actor<Env> { async alarm(): Promise<void> { // Clean up expired sessions const sessions = await this.state.storage.list({ prefix: "session:" }); const now = Date.now();
for (const [key, session] of sessions) { if (session.expiresAt < now) { await this.state.storage.delete(key); } }
// Schedule next cleanup await this.state.storage.setAlarm(now + (60 * 60 * 1000)); // 1 hour }}
Actor Lifecycle
Control actor execution timing and concurrency for initialization and cleanup operations.
waitUntil(promise)
Register promises that must complete before the actor can exit or be garbage collected:
export class DataProcessor extends Actor<Env> { async processLargeFile(request: Request): Promise<Response> { const fileUrl = await request.json();
// Process file in background, ensure completion before actor exits const processingPromise = this.processFileAsync(fileUrl); this.state.waitUntil(processingPromise);
return new Response('Processing started'); }
private async processFileAsync(url: string): Promise<void> { // Heavy processing that should complete even if HTTP response sent const data = await fetch(url).then(r => r.json()); await this.state.storage.put('processed-data', data); }}
Parameters:
promise
:Promise<any>
- Promise that must complete before actor exits
blockConcurrencyWhile(callback)
Block concurrent requests while executing initialization or critical sections:
export class SessionManager extends Actor<Env> { private initialized = false;
constructor(state: ActorState, env: Env) { super(state, env);
// Block all incoming requests until initialization completes this.state.blockConcurrencyWhile(async () => { if (!this.initialized) { await this.loadFromStorage(); this.initialized = true; } }); }
private async loadFromStorage(): Promise<void> { const config = await this.state.storage.get('config'); if (config) { // Initialize actor state from stored configuration } }}
Parameters:
callback
:() => Promise<T>
- Function to execute while blocking concurrency
Returns: Promise<T>
- Result of the callback function
Actor Identity
Access the unique identifier that routes requests to this specific actor instance.
state.id
Get information about the current actor instance:
// Get string representation for loggingconst actorId = this.state.id.toString();console.log(`Processing in actor: ${actorId}`);
// Compare actor instancesconst isSameActor = this.state.id.equals(otherActorId);
// Access name if created with idFromName()const userName = this.state.id.name; // e.g., "user-123"
Properties:
toString()
:string
- String representation for logging/debuggingequals(other)
:boolean
- Compare with another ActorIdname
:string | undefined
- Human-readable name if set
Actor Namespace Operations
Create and route to actor instances using the namespace interface available through environment bindings.
idFromName(name)
Generate deterministic actor IDs from string names:
export default class extends Service<Env> { async fetch(request: Request): Promise<Response> { const userId = this.extractUserId(request);
// Same name always produces same ID const actorId = this.env.SESSION_MANAGER.idFromName(userId); const actor = this.env.SESSION_MANAGER.get(actorId);
return await actor.handleRequest(request); }}
Parameters:
name
:string
- Name to generate ID from
Returns: ActorId
- Deterministic actor identifier
get(id, options?)
Get or create an actor instance by ID:
// Basic actor accessconst actor = this.env.MY_ACTOR.get(actorId);
// With location hint for performanceconst actor = this.env.MY_ACTOR.get(actorId, { locationHint: 'wnam' // West North America});
Parameters:
id
:ActorId
- Target actor identifieroptions.locationHint?
:ActorLocationHint
- Preferred geographic location
Location Hints:
wnam
- West North Americaenam
- East North Americasam
- South Americaweur
- West Europeeeur
- East Europeapac
- Asia Pacificoc
- Oceaniaafr
- Africame
- Middle East
Returns: ActorStub<T>
- RPC interface for making calls to the actor
jurisdiction(jurisdiction)
Create a namespace scoped to a specific legal jurisdiction:
// Access actors within EU jurisdictionconst euNamespace = this.env.USER_ACTOR.jurisdiction('eu');const euActorId = euNamespace.idFromName('user-123');const euActor = euNamespace.get(euActorId);
// Access actors within FedRAMP jurisdictionconst fedrampNamespace = this.env.USER_ACTOR.jurisdiction('fedramp');
Parameters:
jurisdiction
:'eu' | 'fedramp'
- Legal jurisdiction for data residency
Returns: ActorNamespace<T>
- Namespace scoped to the jurisdiction
Code Examples
Complete implementations demonstrating actor patterns and common use cases.
User Session Management
// manifest.hclapplication "user-app" { actor "session-manager" {} service "api" { domain = "api.example.com" }}
// Actor implementationexport class SessionManager extends Actor<Env> { async fetch(request: Request): Promise<Response> { return new Response('Use custom methods only', { status: 501 }); }
async createSession(userId: string, userData: any): Promise<string> { const sessionId = crypto.randomUUID(); const session = { id: sessionId, userId, userData, createdAt: Date.now(), expiresAt: Date.now() + (24 * 60 * 60 * 1000) // 24 hours };
await this.state.storage.put(`session:${sessionId}`, session);
// Set cleanup alarm await this.state.storage.setAlarm(session.expiresAt);
return sessionId; }
async getSession(sessionId: string): Promise<any | null> { const session = await this.state.storage.get(`session:${sessionId}`);
if (!session || session.expiresAt < Date.now()) { await this.state.storage.delete(`session:${sessionId}`); return null; }
return session; }
async updateActivity(sessionId: string): Promise<void> { const session = await this.state.storage.get(`session:${sessionId}`); if (session) { session.lastActivity = Date.now(); await this.state.storage.put(`session:${sessionId}`, session); } }
async alarm(): Promise<void> { // Clean up expired sessions const sessions = await this.state.storage.list({ prefix: "session:" }); const now = Date.now(); const expiredKeys = [];
for (const [key, session] of sessions) { if (session.expiresAt < now) { expiredKeys.push(key); } }
if (expiredKeys.length > 0) { await this.state.storage.delete(expiredKeys); }
// Schedule next cleanup await this.state.storage.setAlarm(now + (60 * 60 * 1000)); }}
// Service routing to actorsexport default class extends Service<Env> { async fetch(request: Request): Promise<Response> { const url = new URL(request.url); const userId = this.extractUserId(request);
// Route to user-specific actor const actorId = this.env.SESSION_MANAGER.idFromName(userId); const actor = this.env.SESSION_MANAGER.get(actorId);
if (url.pathname === '/session/create') { const userData = await request.json(); const sessionId = await actor.createSession(userId, userData); return Response.json({ sessionId }); }
if (url.pathname === '/session/get') { const sessionId = url.searchParams.get('sessionId'); const session = await actor.getSession(sessionId); return Response.json({ session }); }
return new Response('Not found', { status: 404 }); }
private extractUserId(request: Request): string { const authHeader = request.headers.get('Authorization'); // Extract from JWT or session token return authHeader?.replace('Bearer ', '') || 'anonymous'; }}
Shopping Cart Actor
export class ShoppingCart extends Actor<Env> { async fetch(request: Request): Promise<Response> { return new Response('Use custom methods only', { status: 501 }); }
async addItem(productId: string, quantity: number, price: number): Promise<void> { const cart = await this.getCart(); const itemKey = `item:${productId}`;
const existingItem = cart.get(itemKey); if (existingItem) { existingItem.quantity += quantity; existingItem.totalPrice = existingItem.quantity * existingItem.price; } else { cart.set(itemKey, { productId, quantity, price, totalPrice: quantity * price, addedAt: Date.now() }); }
await this.saveCart(cart); await this.setExpirationAlarm(); }
async removeItem(productId: string): Promise<void> { const cart = await this.getCart(); cart.delete(`item:${productId}`); await this.saveCart(cart); }
async getCartSummary(): Promise<any> { const cart = await this.getCart(); const items = Array.from(cart.values()); const totalAmount = items.reduce((sum, item) => sum + item.totalPrice, 0);
return { items, totalAmount, itemCount: items.length, cartId: this.state.id.name }; }
private async getCart(): Promise<Map<string, any>> { const items = await this.state.storage.list({ prefix: "item:" }); return items; }
private async saveCart(cart: Map<string, any>): Promise<void> { const updates: Record<string, any> = {}; for (const [key, value] of cart) { updates[key] = value; } await this.state.storage.put(updates); }
private async setExpirationAlarm(): Promise<void> { // Cart expires in 2 hours const expirationTime = Date.now() + (2 * 60 * 60 * 1000); await this.state.storage.setAlarm(expirationTime); }
async alarm(): Promise<void> { // Clear expired cart const items = await this.state.storage.list({ prefix: "item:" }); const itemKeys = Array.from(items.keys());
if (itemKeys.length > 0) { await this.state.storage.delete(itemKeys); } }}
## raindrop.manifest
Configure actors in your manifest file to enable stateful request handling:
```hclapplication "session-app" { actor "session-manager" { visibility = "public" domain { fqdn = "sessions.myapp.com" } # Optional: Specify jurisdiction for data residency jurisdiction = "eu" # or "fedramp" }
actor "shopping-cart" { visibility = "public" domain { cname = "shopping-carts" } # Actors with domains can handle direct requests }
actor "data-processor" { visibility = "application" # Internal actor accessible only through bindings }
service "api" { visibility = "public" domain { fqdn = "api.example.com" } # Service can route requests to actors or handle directly }}