Skip to content

Commit 6101b6a

Browse files
chore: add possibility to create actors without the implementation
Signed-off-by: Xavier Geerinck <[email protected]>
1 parent 20b89a2 commit 6101b6a

File tree

10 files changed

+158
-63
lines changed

10 files changed

+158
-63
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ The Dapr community can be found on [Discord](https://discord.com/invite/ptHhX6jc
2828

2929
## Contributing
3030

31-
Please see our [Contributing Overview](https://docs.dapr.io/contributing/sdk-contrib/js-contributing/).
31+
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.
3232

3333
### Good First Issues
3434

daprdocs/content/en/js-sdk-docs/js-actors/_index.md

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,42 @@ console.log(`Registered Actors: ${JSON.stringify(resRegisteredActors)}`);
136136

137137
## Invoking Actor Methods
138138

139-
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.
139+
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.
140+
141+
```typescript
142+
export default class ParkingSensorContract {
143+
async carEnter(): Promise<void> {
144+
throw new Error("Not implemented");
145+
}
146+
147+
async carLeave(): Promise<void> {
148+
throw new Error("Not implemented");
149+
}
150+
}
151+
```
152+
153+
```typescript
154+
import { ActorId, DaprClient } from "@dapr/dapr";
155+
import ParkingSensorContract from "./ParkingSensorContract";
156+
157+
const daprHost = "127.0.0.1";
158+
const daprPort = "50000";
159+
160+
const client = new DaprClient({ daprHost, daprPort });
161+
162+
// Create a new actor builder for the registered actor ParkingSensorContract with interface ParkingSensorContract. It can be used to create multiple actors of a type.
163+
const builder = new ActorProxyBuilder<ParkingSensorContract>("ParkingSensorContract", ParkingSensorContract, client);
164+
165+
// Create a new actor instance.
166+
const actor = builder.build(new ActorId("my-actor"));
167+
// Or alternatively, use a random ID
168+
// const actor = builder.build(ActorId.createRandomId());
169+
170+
// Invoke the method.
171+
await actor.carEnter();
172+
```
173+
174+
Alternatively, you can also use the existing implementation (if you have access to it):
140175

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

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

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

154189
// Create a new actor instance.
155190
const actor = builder.build(new ActorId("my-actor"));

documentation/development.md

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,30 @@ The command below runs the build process and will rebuild each time we change a
2020
npm run start:dev
2121
```
2222

23+
## Running Tests
24+
25+
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:
26+
27+
- **EMQX:** Used for Binding Tests
28+
- Credentials: http://localhost:18083 (user: admin, pass: public)
29+
- **MongoDB:** Used for State Query API
30+
31+
```bash
32+
# Start Container
33+
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
34+
docker run -d --rm --name mongodb -p 27017:27017 mongo
35+
36+
# Run Unit Tests
37+
npm run test:unit:main
38+
npm run test:unit:actors
39+
40+
# Start gRPC tests
41+
npm run test:e2e:grpc
42+
43+
# Start HTTP tests
44+
npm run test:e2e:http
45+
```
46+
2347
## Publishing Package Package Maintenance
2448

2549
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.
@@ -45,29 +69,6 @@ For **publishing** the package, we simply cut a new release by:
4569

4670
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).
4771

48-
## Running Tests
49-
50-
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:
51-
52-
- **EMQX:** Used for Binding Tests
53-
- Credentials: http://localhost:18083 (user: admin, pass: public)
54-
- **MongoDB:** Used for State Query API
55-
56-
```bash
57-
# Start Container
58-
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
59-
docker run -d --rm --name mongodb -p 27017:27017 mongo
60-
61-
# Run Unit Tests
62-
npm run test:unit:main
63-
npm run test:unit:actors
64-
65-
# Start gRPC tests
66-
npm run test:e2e:grpc
67-
68-
# Start HTTP tests
69-
npm run test:e2e:http
70-
```
7172

7273
## Setup GitHub actions
7374

src/actors/client/ActorProxyBuilder.ts

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,30 @@ See the License for the specific language governing permissions and
1111
limitations under the License.
1212
*/
1313

14-
import { CommunicationProtocolEnum, DaprClient } from "../..";
15-
import Class from "../../types/Class";
14+
import { CommunicationProtocolEnum, DaprClient, DaprClientOptions } from "../..";
1615
import ActorClient from "./ActorClient/ActorClient";
1716
import ActorId from "../ActorId";
18-
import { DaprClientOptions } from "../../types/DaprClientOptions";
1917

2018
export default class ActorProxyBuilder<T> {
19+
// The registered actor name
20+
actorTypeName: string;
2121
actorClient: ActorClient;
22-
actorTypeClass: Class<T>;
22+
actorAbstractClass: Function;
2323

24-
constructor(actorTypeClass: Class<T>, daprClient: DaprClient);
24+
constructor(actorTypeName: string, actorTypeClass: Function, daprClient: DaprClient);
2525
constructor(
26-
actorTypeClass: Class<T>,
26+
actorTypeName: string,
27+
abstractClass: Function,
2728
host: string,
2829
port: string,
2930
communicationProtocol: CommunicationProtocolEnum,
3031
clientOptions: DaprClientOptions,
3132
);
32-
constructor(actorTypeClass: Class<T>, ...args: any[]) {
33-
this.actorTypeClass = actorTypeClass;
33+
constructor(actorTypeName: string, abstractClass: Function, ...args: any[]) {
34+
this.actorTypeName = actorTypeName;
35+
this.actorAbstractClass = abstractClass;
3436

37+
// Create the actor client based on the provided arguments
3538
if (args.length == 1) {
3639
const [daprClient] = args;
3740
this.actorClient = new ActorClient(
@@ -46,27 +49,35 @@ export default class ActorProxyBuilder<T> {
4649
}
4750
}
4851

49-
build(actorId: ActorId): T {
50-
const actorTypeClassName = this.actorTypeClass.name;
51-
const actorClient = this.actorClient;
52+
build(actorId?: ActorId | string): T {
53+
const self = this;
54+
const actorIdParsed = actorId ? (actorId instanceof ActorId ? actorId : new ActorId(actorId)) : ActorId.createRandomId();
5255

56+
// Create an instance of the abstract class to inspect its methods
57+
// This won't be used directly but helps with method discovery
58+
const methodNames = Object.getOwnPropertyNames(this.actorAbstractClass.prototype)
59+
.filter(prop => prop !== 'constructor');
60+
61+
// Create the handler for the proxy
5362
const handler = {
54-
get(_target: any, propKey: any, _receiver: any) {
63+
get: (_target: any, prop: any) => {
64+
// Ensure the property exists on the abstract class prototype
65+
if (!methodNames.includes(prop)) {
66+
throw new Error(`Method ${prop} is not defined in the actor class.`);
67+
}
68+
69+
// Proxy the method call to the actor client
5570
return async function (...args: any) {
5671
const body = args.length > 0 ? args : null;
57-
const res = await actorClient.actor.invoke(actorTypeClassName, actorId, propKey, body);
58-
72+
const res = await self.actorClient.actor.invoke(self.actorTypeName, actorIdParsed, prop, body);
5973
return res;
6074
};
6175
},
6276
};
6377

6478
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy
6579
// we implement a handler that will take a method and forward it to the actor client
66-
const proxy = new Proxy(this.actorTypeClass, handler);
67-
68-
// Return a NOT strongly typed API
69-
// @todo: this should return a strongly typed API as well, but requires reflection. How to do this in typescript?
80+
const proxy = new Proxy(this.actorAbstractClass, handler);
7081
return proxy as unknown as T;
7182
}
7283
}

src/implementation/Client/GRPCClient/GRPCClient.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ import { Settings } from "../../../utils/Settings.util";
1919
import { Logger } from "../../../logger/Logger";
2020
import GRPCClientSidecar from "./sidecar";
2121
import DaprClient from "../DaprClient";
22-
import { SDK_VERSION } from "../../../version";
22+
import pkg from "../../../../package.json";
2323
import communicationProtocolEnum from "../../../enum/CommunicationProtocol.enum";
2424
import { GrpcEndpoint } from "../../../network/GrpcEndpoint";
2525

26+
2627
export default class GRPCClient implements IClient {
2728
readonly options: DaprClientOptions;
2829

@@ -115,7 +116,7 @@ export default class GRPCClient implements IClient {
115116
options["grpc-node.max_session_memory"] = Number.MAX_SAFE_INTEGER;
116117

117118
// Add user agent
118-
options["grpc.primary_user_agent"] = "dapr-sdk-js/v" + SDK_VERSION;
119+
options["grpc.primary_user_agent"] = "dapr-sdk-js/v" + pkg.version;
119120

120121
// Add interceptors if we have an API token
121122
if (this.options.daprApiToken !== "") {

src/implementation/Client/GRPCClient/actor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export default class GRPCClientActor implements IClientActorBuilder {
2929
// this means that we can't use T to new up an object (sadly enough) so we have to pass it
3030
create<T>(actorTypeClass: Class<T>): T {
3131
const builder = new ActorProxyBuilder<T>(
32+
actorTypeClass.name,
3233
actorTypeClass,
3334
this.client.options.daprHost,
3435
this.client.options.daprPort,

src/implementation/Client/HTTPClient/HTTPClient.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { Settings } from "../../../utils/Settings.util";
2121
import { THTTPExecuteParams } from "../../../types/http/THTTPExecuteParams.type";
2222
import { Logger } from "../../../logger/Logger";
2323
import HTTPClientSidecar from "./sidecar";
24-
import { SDK_VERSION } from "../../../version";
24+
import pkg from "../../../../package.json";
2525
import * as SerializerUtil from "../../../utils/Serializer.util";
2626
import communicationProtocolEnum from "../../../enum/CommunicationProtocol.enum";
2727
import { HttpEndpoint } from "../../../network/HttpEndpoint";
@@ -155,7 +155,7 @@ export default class HTTPClient implements IClient {
155155
clientOptions.headers["dapr-api-token"] = this.options.daprApiToken;
156156
}
157157

158-
clientOptions.headers["user-agent"] = `dapr-sdk-js/v${SDK_VERSION} http/1`;
158+
clientOptions.headers["user-agent"] = `dapr-sdk-js/v${pkg.version} http/1`;
159159

160160
// Set Body and Content-Type Header
161161
if (params?.body) {

src/implementation/Client/HTTPClient/actor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export default class HTTPClientActor implements IClientActorBuilder {
2929
// this means that we can't use T to new up an object (sadly enough) so we have to pass it
3030
create<T>(actorTypeClass: Class<T>): T {
3131
const builder = new ActorProxyBuilder<T>(
32+
actorTypeClass.name,
3233
actorTypeClass,
3334
this.client.options.daprHost,
3435
this.client.options.daprPort,
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
Copyright 2022 The Dapr Authors
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
export default class DemoActorCounterContract {
15+
count(): Promise<void> {
16+
throw new Error("Method not implemented.");
17+
}
18+
19+
countBy(amount: number, multiplier: number): Promise<void> {
20+
throw new Error("Method not implemented.");
21+
}
22+
23+
getCounter(): Promise<number> {
24+
throw new Error("Method not implemented.");
25+
}
26+
}

0 commit comments

Comments
 (0)