Back to Rivet

Communicating Between Actors

website/src/content/docs/actors/communicating-between-actors.mdx

2.2.18.1 KB
Original Source

Actors can communicate with each other using the server-side actor client, enabling complex workflows and data sharing between different actor instances.

<Note> We recommend reading the [clients documentation](/docs/clients) first. This guide focuses specifically on communication between actors. </Note>

Using the Server-Side Actor Client

The server-side actor client allows actors to call other actors within the same registry. Access it via c.client() in your actor context:

<Note> If two actors call each other and their return types are inferred from the other actor's response, you may hit circular type errors (`TS2322`, `TS2722`, or `c.state` becoming `unknown`). Fix this by writing explicit return types on those actions. </Note>
typescript
import { actor, setup } from "rivetkit";

interface Order {
  id: string;
  customerId: string;
  quantity: number;
  amount: number;
}

interface ProcessedOrder extends Order {
  status: string;
  paymentResult: { transactionId: string };
}

const inventory = actor({
  state: { stock: 100 },
  actions: {
    reserveStock: (c, quantity: number) => {
      c.state.stock -= quantity;
      return { reserved: quantity };
    }
  }
});

const payment = actor({
  state: {},
  actions: {
    processPayment: (c, amount: number) => ({ transactionId: "tx-123" })
  }
});

const orderProcessor = actor({
  state: { orders: [] as ProcessedOrder[] },

  actions: {
    processOrder: async (c, order: Order) => {
      const client = c.client<typeof registry>();

      // Reserve the stock
      const inventoryHandle = client.inventory.getOrCreate(["main"]);
      await inventoryHandle.reserveStock(order.quantity);

      // Process payment through payment actor
      const paymentHandle = client.payment.getOrCreate([order.customerId]);
      const result = await paymentHandle.processPayment(order.amount);

      // Update order state
      c.state.orders.push({ ...order, status: "completed", paymentResult: result });

      return { success: true, orderId: order.id };
    }
  }
});

const registry = setup({ use: { inventory, payment, orderProcessor } });

Use Cases and Patterns

Actor Orchestration

Use a coordinator actor to manage complex workflows:

typescript
import { actor, setup } from "rivetkit";

interface WorkflowResult {
  workflowId: string;
  result: { finalized: boolean };
  completedAt: number;
}

const dataProcessor = actor({
  state: {},
  actions: {
    initialize: (c, workflowId: string) => ({ workflowId, data: "initialized" })
  }
});

const validator = actor({
  state: {},
  actions: {
    validate: (c, data: { workflowId: string; data: string }) => ({ valid: true, data })
  }
});

const finalizer = actor({
  state: {},
  actions: {
    finalize: (c, validationResult: { valid: boolean }) => ({ finalized: validationResult.valid })
  }
});

const workflowActor = actor({
  state: { workflows: [] as WorkflowResult[] },

  actions: {
    executeWorkflow: async (c, workflowId: string) => {
      const client = c.client<typeof registry>();

      // Step 1: Initialize data
      const dataProcessorHandle = client.dataProcessor.getOrCreate(["main"]);
      const data = await dataProcessorHandle.initialize(workflowId);

      // Step 2: Process through multiple actors
      const validatorHandle = client.validator.getOrCreate(["main"]);
      const validationResult = await validatorHandle.validate(data);

      // Step 3: Finalize
      const finalizerHandle = client.finalizer.getOrCreate(["main"]);
      const result = await finalizerHandle.finalize(validationResult);

      c.state.workflows.push({ workflowId, result, completedAt: Date.now() });
      return result;
    }
  }
});

const registry = setup({ use: { dataProcessor, validator, finalizer, workflowActor } });

Data Aggregation

Collect data from multiple actors:

typescript
import { actor, setup } from "rivetkit";

interface Stats {
  count: number;
  total: number;
}

interface Report {
  id: string;
  type: string;
  data: { users: Stats; orders: Stats; system: Stats };
  generatedAt: number;
}

const userMetrics = actor({
  state: {},
  actions: {
    getStats: (c): Stats => ({ count: 100, total: 500 })
  }
});

const orderMetrics = actor({
  state: {},
  actions: {
    getStats: (c): Stats => ({ count: 50, total: 10000 })
  }
});

const systemMetrics = actor({
  state: {},
  actions: {
    getStats: (c): Stats => ({ count: 5, total: 99 })
  }
});

const analyticsActor = actor({
  state: { reports: [] as Report[] },

  actions: {
    generateReport: async (c, reportType: string) => {
      const client = c.client<typeof registry>();

      // Collect data from multiple sources
      const userMetricsHandle = client.userMetrics.getOrCreate(["main"]);
      const orderMetricsHandle = client.orderMetrics.getOrCreate(["main"]);
      const systemMetricsHandle = client.systemMetrics.getOrCreate(["main"]);

      const [users, orders, system] = await Promise.all([
        userMetricsHandle.getStats(),
        orderMetricsHandle.getStats(),
        systemMetricsHandle.getStats()
      ]);

      const report: Report = {
        id: crypto.randomUUID(),
        type: reportType,
        data: { users, orders, system },
        generatedAt: Date.now()
      };

      c.state.reports.push(report);
      return report;
    }
  }
});

const registry = setup({ use: { userMetrics, orderMetrics, systemMetrics, analyticsActor } });

Event-Driven Architecture

Use connections to listen for events from other actors:

typescript
import { actor, setup } from "rivetkit";

interface User {
  id: string;
  name: string;
}

interface Order {
  id: string;
  amount: number;
}

interface AuditLog {
  event: string;
  data: User | Order;
  timestamp: number;
}

const userActor = actor({
  state: {},
  actions: {
    createUser: (c, name: string) => {
      const user = { id: crypto.randomUUID(), name };
      c.broadcast("userCreated", user);
      return user;
    }
  }
});

const orderActor = actor({
  state: {},
  actions: {
    completeOrder: (c, amount: number) => {
      const order = { id: crypto.randomUUID(), amount };
      c.broadcast("orderCompleted", order);
      return order;
    }
  }
});

const auditLogActor = actor({
  state: { logs: [] as AuditLog[] },

  actions: {
    startAuditing: async (c) => {
      const client = c.client<typeof registry>();

      // Connect to multiple actors to listen for events
      const userActorConn = client.userActor.getOrCreate(["main"]).connect();
      const orderActorConn = client.orderActor.getOrCreate(["main"]).connect();

      // Listen for user events
      userActorConn.on("userCreated", (user: User) => {
        c.state.logs.push({
          event: "userCreated",
          data: user,
          timestamp: Date.now()
        });
      });

      // Listen for order events
      orderActorConn.on("orderCompleted", (order: Order) => {
        c.state.logs.push({
          event: "orderCompleted",
          data: order,
          timestamp: Date.now()
        });
      });

      return { status: "auditing started" };
    }
  }
});

const registry = setup({ use: { userActor, orderActor, auditLogActor } });

Batch Operations

Process multiple items in parallel:

typescript
import { actor, setup } from "rivetkit";
import { createClient } from "rivetkit/client";

interface Item {
  type: string;
  data: string;
}

const processor = actor({
  state: {},
  actions: {
    process: (c, item: Item) => ({ processed: true, item })
  }
});

const registry = setup({ use: { processor } });
const client = createClient<typeof registry>("http://localhost:6420");

// Process items in parallel
const items: Item[] = [
  { type: "typeA", data: "data1" },
  { type: "typeB", data: "data2" }
];

const results = await Promise.all(
  items.map(item => client.processor.getOrCreate([item.type]).process(item))
);

API Reference