Skip to content

chore: add possibility to create actors without the implementation #697

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ The Dapr community can be found on [Discord](https://discord.com/invite/ptHhX6jc

## Contributing

Please see our [Contributing Overview](https://docs.dapr.io/contributing/sdk-contrib/js-contributing/).
Please see our [Contributing Overview](https://docs.dapr.io/contributing/sdk-contrib/js-contributing/) and [Development Guide](./documentation//development.md) for more information on how to contribute to the Dapr JS SDK.

### Good First Issues

Expand Down
41 changes: 38 additions & 3 deletions daprdocs/content/en/js-sdk-docs/js-actors/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,42 @@ 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.
After Actors are registered, we can create a Proxy object that uses a implementation stub class (as we require the methods through reflection internally). 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.

```typescript
export default class ParkingSensorContract {
async carEnter(): Promise<void> {
throw new Error("Not implemented");
}

async carLeave(): Promise<void> {
throw new Error("Not implemented");
}
}
```

```typescript
import { ActorId, DaprClient } from "@dapr/dapr";
import ParkingSensorContract from "./ParkingSensorContract";

const daprHost = "127.0.0.1";
const daprPort = "50000";

const client = new DaprClient({ daprHost, daprPort });

// Create a new actor builder for the registered actor ParkingSensorContract with interface ParkingSensorContract. It can be used to create multiple actors of a type.
const builder = new ActorProxyBuilder<ParkingSensorContract>("ParkingSensorContract", ParkingSensorContract, client);

// Create a new actor instance.
const actor = builder.build(new ActorId("my-actor"));
// Or alternatively, use a random ID
// const actor = builder.build(ActorId.createRandomId());

// Invoke the method.
await actor.carEnter();
```

Alternatively, you can also use the existing implementation (if you have access to it):

```typescript
import { ActorId, DaprClient } from "@dapr/dapr";
Expand All @@ -148,8 +183,8 @@ const daprPort = "50000";

const client = new DaprClient({ daprHost, daprPort });

// Create a new actor builder. It can be used to create multiple actors of a type.
const builder = new ActorProxyBuilder<ParkingSensorInterface>(ParkingSensorImpl, client);
// Create a new actor builder for the registered actor ParkingSensorImpl with interface ParkingSensorImpl. It can be used to create multiple actors of a type.
const builder = new ActorProxyBuilder<ParkingSensorInterface>("ParkingSensorImpl", ParkingSensorImpl, client);

// Create a new actor instance.
const actor = builder.build(new ActorId("my-actor"));
Expand Down
47 changes: 24 additions & 23 deletions documentation/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,30 @@ The command below runs the build process and will rebuild each time we change a
npm run start:dev
```

## Running Tests

Tests are written per protocol layer: http or grpc. This is done because Dapr requires endpoints to be registered for for pubsub and bindings, making us having to start up the test, initialize those endpoints and then run. Since Dapr is a sidecar architecture, we thus have to start 2 test suites seperately. It requires the following containers:

- **EMQX:** Used for Binding Tests
- Credentials: http://localhost:18083 (user: admin, pass: public)
- **MongoDB:** Used for State Query API

```bash
# Start Container
docker run -d --rm --name emqx -p 1883:1883 -p 8081:8081 -p 8083:8083 -p 8883:8883 -p 8084:8084 -p 18083:18083 emqx/emqx
docker run -d --rm --name mongodb -p 27017:27017 mongo

# Run Unit Tests
npm run test:unit:main
npm run test:unit:actors

# Start gRPC tests
npm run test:e2e:grpc

# Start HTTP tests
npm run test:e2e:http
```

## Publishing Package Package Maintenance

To publish a new package to [https://www.npmjs.com/package/@dapr/dapr](https://www.npmjs.com/package/@dapr/dapr) we need to do the following building and publishing steps.
Expand All @@ -45,29 +69,6 @@ For **publishing** the package, we simply cut a new release by:

Publishing is automated in the CI/CD pipeline. Each time a version is release (GitHub ref starting with `refs/tags/v`) then the pipeline will deploy the package as described in [build.yml](./.github/workflows/build.yml).

## Running Tests

Tests are written per protocol layer: http or grpc. This is done because Dapr requires endpoints to be registered for for pubsub and bindings, making us having to start up the test, initialize those endpoints and then run. Since Dapr is a sidecar architecture, we thus have to start 2 test suites seperately. It requires the following containers:

- **EMQX:** Used for Binding Tests
- Credentials: http://localhost:18083 (user: admin, pass: public)
- **MongoDB:** Used for State Query API

```bash
# Start Container
docker run -d --rm --name emqx -p 1883:1883 -p 8081:8081 -p 8083:8083 -p 8883:8883 -p 8084:8084 -p 18083:18083 emqx/emqx
docker run -d --rm --name mongodb -p 27017:27017 mongo

# Run Unit Tests
npm run test:unit:main
npm run test:unit:actors

# Start gRPC tests
npm run test:e2e:grpc

# Start HTTP tests
npm run test:e2e:http
```

## Setup GitHub actions

Expand Down
76 changes: 58 additions & 18 deletions src/actors/client/ActorProxyBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { CommunicationProtocolEnum, DaprClient } from "../..";
import Class from "../../types/Class";
import { CommunicationProtocolEnum, DaprClient, DaprClientOptions } from "../..";
import ActorClient from "./ActorClient/ActorClient";
import ActorId from "../ActorId";
import { DaprClientOptions } from "../../types/DaprClientOptions";
import Class from "../../types/Class";

export default class ActorProxyBuilder<T> {
// The registered actor name
actorTypeName: string;
actorClient: ActorClient;
actorTypeClass: Class<T>;
actorAbstractClass: Class<T>;

constructor(actorTypeClass: Class<T>, daprClient: DaprClient);
constructor(
Expand All @@ -29,44 +30,83 @@ export default class ActorProxyBuilder<T> {
communicationProtocol: CommunicationProtocolEnum,
clientOptions: DaprClientOptions,
);
constructor(actorTypeClass: Class<T>, ...args: any[]) {
this.actorTypeClass = actorTypeClass;
constructor(
actorTypeName: string,
actorTypeClass: Class<T>,
daprClient: DaprClient
);
constructor(
actorTypeName: string,
actorTypeClass: Class<T>,
host: string,
port: string,
communicationProtocol: CommunicationProtocolEnum,
clientOptions: DaprClientOptions,
);
constructor(...args: any[]) {
let actorTypeName: string;
let actorTypeClass: Class<T>;
let rest: any[];

// Determine if the first argument is a string (actorTypeName) or a class
if (typeof args[0] === "string") {
actorTypeName = args[0];
actorTypeClass = args[1];
rest = args.slice(2);
} else {
actorTypeClass = args[0];
actorTypeName = actorTypeClass.name;
rest = args.slice(1);
}

if (args.length == 1) {
const [daprClient] = args;
this.actorTypeName = actorTypeName;
this.actorAbstractClass = actorTypeClass;

// Create the actor client based on the provided arguments
if (rest.length == 1) {
const [daprClient] = rest;
this.actorClient = new ActorClient(
daprClient.options.daprHost,
daprClient.options.daprPort,
daprClient.options.communicationProtocol,
daprClient.options,
);
} else {
const [host, port, communicationProtocol, clientOptions] = args;
const [host, port, communicationProtocol, clientOptions] = rest;
this.actorClient = new ActorClient(host, port, communicationProtocol, clientOptions);
}
}

build(actorId: ActorId): T {
const actorTypeClassName = this.actorTypeClass.name;
build(actorId?: ActorId | string): T {
const actorIdParsed = actorId ? (actorId instanceof ActorId ? actorId : new ActorId(actorId)) : ActorId.createRandomId();
const actorClient = this.actorClient;
const actorTypeName = this.actorTypeName;

// Create an instance of the abstract class to inspect its methods
// This won't be used directly but helps with method discovery
const methodNames = Object.getOwnPropertyNames(this.actorAbstractClass.prototype)
.filter(prop => prop !== 'constructor');

// Create the handler for the proxy
const handler = {
get(_target: any, propKey: any, _receiver: any) {
get: (_target: any, prop: any) => {
// Ensure the property exists on the abstract class prototype
if (!methodNames.includes(prop)) {
throw new Error(`Method ${prop} is not defined in the actor class.`);
}

// Proxy the method call to the actor client
return async function (...args: any) {
const body = args.length > 0 ? args : null;
const res = await actorClient.actor.invoke(actorTypeClassName, actorId, propKey, body);

const res = await actorClient.actor.invoke(actorTypeName, actorIdParsed, prop, body);
return res;
};
},
};

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy
// we implement a handler that will take a method and forward it to the actor client
const proxy = new Proxy(this.actorTypeClass, handler);

// Return a NOT strongly typed API
// @todo: this should return a strongly typed API as well, but requires reflection. How to do this in typescript?
const proxy = new Proxy(this.actorAbstractClass, handler);
return proxy as unknown as T;
}
}
1 change: 1 addition & 0 deletions src/implementation/Client/GRPCClient/GRPCClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { SDK_VERSION } from "../../../version";
import communicationProtocolEnum from "../../../enum/CommunicationProtocol.enum";
import { GrpcEndpoint } from "../../../network/GrpcEndpoint";


export default class GRPCClient implements IClient {
readonly options: DaprClientOptions;

Expand Down
1 change: 1 addition & 0 deletions src/implementation/Client/GRPCClient/actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export default class GRPCClientActor implements IClientActorBuilder {
// this means that we can't use T to new up an object (sadly enough) so we have to pass it
create<T>(actorTypeClass: Class<T>): T {
const builder = new ActorProxyBuilder<T>(
actorTypeClass.name,
actorTypeClass,
this.client.options.daprHost,
this.client.options.daprPort,
Expand Down
1 change: 1 addition & 0 deletions src/implementation/Client/HTTPClient/actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export default class HTTPClientActor implements IClientActorBuilder {
// this means that we can't use T to new up an object (sadly enough) so we have to pass it
create<T>(actorTypeClass: Class<T>): T {
const builder = new ActorProxyBuilder<T>(
actorTypeClass.name,
actorTypeClass,
this.client.options.daprHost,
this.client.options.daprPort,
Expand Down
26 changes: 26 additions & 0 deletions test/actor/DemoActorCounterContract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
Copyright 2022 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

export default class DemoActorCounterContract {
count(): Promise<void> {
throw new Error("Method not implemented.");
}

countBy(_amount: number, _multiplier: number): Promise<void> {
throw new Error("Method not implemented.");
}

getCounter(): Promise<number> {
throw new Error("Method not implemented.");
}
}
Loading