Actors

The Dapr actors package allows you to interact with Dapr virtual actors from a JavaScript application. The examples below demonstrate how to use the JavaScript SDK for interacting with virtual actors.

For a more in-depth overview of Dapr actors, visit the actors overview page.

Pre-requisites

Scenario

The below code examples loosely describe the scenario of a Parking Garage Spot Monitoring System, which can be seen in this video by Mark Russinovich.

A parking garage consists of hundreds of parking spaces, where each parking space includes a sensor that provides updates to a centralized monitoring system. The parking space sensors (our actors) detect if a parking space is occupied or available.

To jump in and run this example yourself, clone the source code, which can be found in the JavaScript SDK examples directory.

Actor Interface

The actor interface defines the contract that is shared between the actor implementation and the clients calling the actor. In the example below, we have created an interace for a parking garage sensor. Each sensor has 2 methods: carEnter and carLeave, which defines the state of the parking space:

  1. export default interface ParkingSensorInterface {
  2. carEnter(): Promise<void>;
  3. carLeave(): Promise<void>;
  4. }

Actor Implementation

An actor implementation defines a class by extending the base type AbstractActor and implementing the actor interface (ParkingSensorInterface in this case).

The following code describes an actor implementation along with a few helper methods.

  1. import { AbstractActor } from "@dapr/dapr";
  2. import ParkingSensorInterface from "./ParkingSensorInterface";
  3. export default class ParkingSensorImpl extends AbstractActor implements ParkingSensorInterface {
  4. async carEnter(): Promise<void> {
  5. // Implementation that updates state that this parking spaces is occupied.
  6. }
  7. async carLeave(): Promise<void> {
  8. // Implementation that updates state that this parking spaces is available.
  9. }
  10. private async getInfo(): Promise<object> {
  11. // Implementation of requesting an update from the parking space sensor.
  12. }
  13. /**
  14. * @override
  15. */
  16. async onActivate(): Promise<void> {
  17. // Initialization logic called by AbstractActor.
  18. }
  19. }

Configuring Actor Runtime

To configure actor runtime, use the DaprClientOptions. The various parameters and their default values are documented at How-to: Use virtual actors in Dapr.

Note, the timeouts and intervals should be formatted as time.ParseDuration strings.

  1. import { CommunicationProtocolEnum, DaprClient, DaprServer } from "@dapr/dapr";
  2. // Configure the actor runtime with the DaprClientOptions.
  3. const clientOptions = {
  4. daprHost: daprHost,
  5. daprPort: daprPort,
  6. communicationProtocol: CommunicationProtocolEnum.HTTP,
  7. actor: {
  8. actorIdleTimeout: "1h",
  9. actorScanInterval: "30s",
  10. drainOngoingCallTimeout: "1m",
  11. drainRebalancedActors: true,
  12. reentrancy: {
  13. enabled: true,
  14. maxStackDepth: 32,
  15. },
  16. remindersStoragePartitions: 0,
  17. },
  18. };
  19. // Use the options when creating DaprServer and DaprClient.
  20. // Note, DaprServer creates a DaprClient internally, which needs to be configured with clientOptions.
  21. const server = new DaprServer({ serverHost, serverPort, clientOptions });
  22. const client = new DaprClient(clientOptions);

Registering Actors

Initialize and register your actors by using the DaprServer package:

  1. import { DaprServer } from "@dapr/dapr";
  2. import ParkingSensorImpl from "./ParkingSensorImpl";
  3. const daprHost = "127.0.0.1";
  4. const daprPort = "50000";
  5. const serverHost = "127.0.0.1";
  6. const serverPort = "50001";
  7. const server = new DaprServer({
  8. serverHost,
  9. serverPort,
  10. clientOptions: {
  11. daprHost,
  12. daprPort,
  13. },
  14. });
  15. await server.actor.init(); // Let the server know we need actors
  16. server.actor.registerActor(ParkingSensorImpl); // Register the actor
  17. await server.start(); // Start the server
  18. // To get the registered actors, you can invoke `getRegisteredActors`:
  19. const resRegisteredActors = await server.actor.getRegisteredActors();
  20. console.log(`Registered Actors: ${JSON.stringify(resRegisteredActors)}`);

Invoking Actor Methods

After Actors are registered, create a Proxy object that implements ParkingSensorInterface using the ActorProxyBuilder. You can invoke the actor methods by directly calling methods on the Proxy object. Internally, it translates to making a network call to the Actor API and fetches the result back.

  1. import { ActorId, DaprClient } from "@dapr/dapr";
  2. import ParkingSensorImpl from "./ParkingSensorImpl";
  3. import ParkingSensorInterface from "./ParkingSensorInterface";
  4. const daprHost = "127.0.0.1";
  5. const daprPort = "50000";
  6. const client = new DaprClient({ daprHost, daprPort });
  7. // Create a new actor builder. It can be used to create multiple actors of a type.
  8. const builder = new ActorProxyBuilder<ParkingSensorInterface>(ParkingSensorImpl, client);
  9. // Create a new actor instance.
  10. const actor = builder.build(new ActorId("my-actor"));
  11. // Or alternatively, use a random ID
  12. // const actor = builder.build(ActorId.createRandomId());
  13. // Invoke the method.
  14. await actor.carEnter();

Using states with Actor

  1. import { AbstractActor } from "@dapr/dapr";
  2. import ActorStateInterface from "./ActorStateInterface";
  3. export default class ActorStateExample extends AbstractActor implements ActorStateInterface {
  4. async setState(key: string, value: any): Promise<void> {
  5. await this.getStateManager().setState(key, value);
  6. await this.getStateManager().saveState();
  7. }
  8. async removeState(key: string): Promise<void> {
  9. await this.getStateManager().removeState(key);
  10. await this.getStateManager().saveState();
  11. }
  12. // getState with a specific type
  13. async getState<T>(key: string): Promise<T | null> {
  14. return await this.getStateManager<T>().getState(key);
  15. }
  16. // getState without type as `any`
  17. async getState(key: string): Promise<any> {
  18. return await this.getStateManager().getState(key);
  19. }
  20. }

Actor Timers and Reminders

The JS SDK supports actors that can schedule periodic work on themselves by registering either timers or reminders. The main difference between timers and reminders is that the Dapr actor runtime does not retain any information about timers after deactivation, but persists reminders information using the Dapr actor state provider.

This distinction allows users to trade off between light-weight but stateless timers versus more resource-demanding but stateful reminders.

The scheduling interface of timers and reminders is identical. For an more in-depth look at the scheduling configurations see the actors timers and reminders docs.

Actor Timers

  1. // ...
  2. const actor = builder.build(new ActorId("my-actor"));
  3. // Register a timer
  4. await actor.registerActorTimer(
  5. "timer-id", // Unique name of the timer.
  6. "cb-method", // Callback method to execute when timer is fired.
  7. Temporal.Duration.from({ seconds: 2 }), // DueTime
  8. Temporal.Duration.from({ seconds: 1 }), // Period
  9. Temporal.Duration.from({ seconds: 1 }), // TTL
  10. 50, // State to be sent to timer callback.
  11. );
  12. // Delete the timer
  13. await actor.unregisterActorTimer("timer-id");

Actor Reminders

  1. // ...
  2. const actor = builder.build(new ActorId("my-actor"));
  3. // Register a reminder, it has a default callback: `receiveReminder`
  4. await actor.registerActorReminder(
  5. "reminder-id", // Unique name of the reminder.
  6. Temporal.Duration.from({ seconds: 2 }), // DueTime
  7. Temporal.Duration.from({ seconds: 1 }), // Period
  8. Temporal.Duration.from({ seconds: 1 }), // TTL
  9. 100, // State to be sent to reminder callback.
  10. );
  11. // Delete the reminder
  12. await actor.unregisterActorReminder("reminder-id");

To handle the callback, you need to override the default receiveReminder implementation in your actor. For example, from our original actor implementation:

  1. export default class ParkingSensorImpl extends AbstractActor implements ParkingSensorInterface {
  2. // ...
  3. /**
  4. * @override
  5. */
  6. async receiveReminder(state: any): Promise<void> {
  7. // handle stuff here
  8. }
  9. // ...
  10. }

For a full guide on actors, visit How-To: Use virtual actors in Dapr.