diff --git a/README.md b/README.md
index 80fd746..cb445e5 100644
--- a/README.md
+++ b/README.md
@@ -82,14 +82,14 @@ Create a new actor inside the actorSystem. The options parameter are as follow:
#### From an Actor
```TypeScript
-this.at(actorRef).yourMethodName(payload?);
+this.sendTo(actorRef).yourMethodName(payload?);
```
**actorRef**: _(Required)_ the target actorRef where we send the message to
**payload**: _(As defined by the target actor)_ payload of the message as defined by the target actor
```TypeScript
-this.at(address).yourMethodName(payload?);
+this.sendTo(address).yourMethodName(payload?);
```
**address**: _(Required)_ the target address where we send the message to, if `TargetActorAPI` is not specified then there will be no compile-time check
@@ -98,7 +98,7 @@ this.at(address).yourMethodName(payload?);
#### From Everywhere Else
```TypeScript
-actorRef.invoke(sender?).yourMethodName(payload?);
+actorRef.send(sender?).yourMethodName(payload?);
```
This is the typical way to send a message to an actor from outside of actors. Sender parameter is optional, but if you need to use it, better to just use the previous API.
@@ -110,7 +110,7 @@ This is the typical way to send a message to an actor from outside of actors. Se
```TypeScript
const senderRef = this.context.senderRef;
-this.at(senderRef).yourMethodName(payload?);
+this.sendTo(senderRef).yourMethodName(payload?);
```
### Getting Address of Actors
diff --git a/src/Actor.ts b/src/Actor.ts
index d63cf49..c15a1f4 100644
--- a/src/Actor.ts
+++ b/src/Actor.ts
@@ -9,7 +9,34 @@ export type MailBoxMessage = {
callback: (error?: any, result?: any) => void;
};
-export type ValidActorMethodProps = Pick>;
+export type ActorSendAPI = {
+ [K in ValidActorMethodPropNames]: T[K] extends () => any
+ ? () => void
+ : T[K] extends (arg1: infer A1) => any
+ ? (arg: A1) => void
+ : T[K] extends (arg1: infer A1, arg2: infer A2) => any
+ ? (arg1: A1, arg2: A2) => void
+ : T[K] extends (arg1: infer A1, arg2: infer A2, arg3: infer A3) => any
+ ? (arg1: A1, arg2: A2, arg3: A3) => void
+ : T[K] extends (
+ arg1: infer A1,
+ arg2: infer A2,
+ arg3: infer A3,
+ arg4: infer A4
+ ) => any
+ ? (arg1: A1, arg2: A2, arg3: A3, arg4: A4) => void
+ : T[K] extends (
+ arg1: infer A1,
+ arg2: infer A2,
+ arg3: infer A3,
+ arg4: infer A4,
+ arg5: infer A5
+ ) => any
+ ? (arg1: A1, arg2: A2, arg3: A3, arg4: A4, arg5: A5) => void
+ : never
+};
+
+export type ActorAskAPI = Pick>;
export type ValidActorMethodPropNames = {
[K in Exclude]: T[K] extends (...args: any[]) => infer R
? R extends Promise ? K : never
@@ -28,24 +55,45 @@ export type ActorCons, K = undefined> = new (
strategies?: Strategy[]
) => T;
+function createProxy(
+ actorSystem: ActorSystem,
+ targetAddressorActorRef: Address | ActorRef,
+ sender?: Address,
+ ask = false
+) {
+ return new Proxy(
+ {},
+ {
+ get: (target, prop, receiver) => {
+ return (...payload: any[]) => {
+ return ask
+ ? actorSystem.sendMessageAndWait(
+ targetAddressorActorRef,
+ prop as any,
+ sender || null,
+ ...payload
+ )
+ : actorSystem.sendMessage(
+ targetAddressorActorRef,
+ prop as any,
+ sender || null,
+ ...payload
+ );
+ };
+ }
+ }
+ );
+}
+
export class ActorRef {
constructor(public address: Address, private actorSystem: ActorSystem) {}
- invoke(sender?: Address) {
- return new Proxy(
- {},
- {
- get: (target, prop, receiver) => {
- return (...payload: any[]) =>
- this.actorSystem.sendMessage(
- this.address,
- prop as any,
- sender || null,
- ...payload
- );
- }
- }
- ) as ValidActorMethodProps;
+ send(sender?: Address) {
+ return createProxy(this.actorSystem, this.address, sender) as ActorSendAPI;
+ }
+
+ ask(sender?: Address) {
+ return createProxy(this.actorSystem, this.address, sender, true) as ActorAskAPI;
}
}
@@ -78,37 +126,6 @@ export abstract class Actor {
});
}
- at(targetRef: ActorRef | Address) {
- return new Proxy(
- {},
- {
- get: (target, prop, receiver) => {
- return (...payload: any[]) =>
- this.actorSystem.sendMessage(
- targetRef,
- prop as any,
- this.address,
- ...payload
- );
- }
- }
- ) as Handler;
- }
-
- // For some reason the typings is not working properly
- atSelf() {
- // return this.at>(this.address);
- return this.at(this.address); // TODO: introduce generic for actor
- }
-
- onNewMessage = , L extends PayloadPropNames>(
- type: K,
- senderAddress: Address | null,
- ...payload: L[]
- ) => {
- // should be overridden by implementator (when necessary)
- };
-
pushToMailbox = , L extends PayloadPropNames>(
type: K,
senderAddress: Address | null,
@@ -150,11 +167,34 @@ export abstract class Actor {
return promise;
};
- // TODO: 'ref' vs 'at' will confuse people
- ref = (address: Address) => {
+ protected ref = (address: Address) => {
return this.actorSystem.ref(address);
};
+ protected sendTo(targetRef: ActorRef | Address) {
+ return createProxy(this.actorSystem, targetRef, this.address) as ActorSendAPI;
+ }
+
+ protected askTo(targetRef: ActorRef | Address) {
+ return createProxy(this.actorSystem, targetRef, this.address, true) as ActorAskAPI;
+ }
+
+ // For some reason the typings is not working properly
+ protected sendToSelf() {
+ // return this.at>(this.address);
+ return this.sendTo(this.address); // TODO: introduce generic for actor
+ }
+
+ protected onNewMessage = <
+ K extends ValidActorMethodPropNames,
+ L extends PayloadPropNames
+ >(
+ type: K,
+ senderAddress: Address | null,
+ ...payload: L[]
+ ) => {
+ // should be overridden by implementator (when necessary)
+ };
protected init(options?: InitParam) {
// can be implemented by the concrete actor
}
@@ -217,6 +257,7 @@ export abstract class Actor {
this.currentPromise = this.handleMessage(type, ...payload);
try {
result = await this.currentPromise;
+ this.log("Output of the handled message", result);
success = true;
} catch (error) {
this.log("Caught an exception when handling message", error);
diff --git a/src/ActorSystem.ts b/src/ActorSystem.ts
index add039c..99f6edc 100644
--- a/src/ActorSystem.ts
+++ b/src/ActorSystem.ts
@@ -1,4 +1,4 @@
-import { Actor, ActorCons, ActorRef, ValidActorMethodProps } from "./Actor";
+import { Actor, ActorCons, ActorRef, ActorAskAPI } from "./Actor";
import { EventEmitter } from "events";
import {
Message,
@@ -68,13 +68,13 @@ export class ActorSystem {
`Sending the message to the appropriate actor. Type: ${type}, sender: ${senderAddress}, and payload:`,
payload
);
- this.sendMessage(actorRef, type, senderAddress, ...payload);
+ this.sendMessageAndWait(actorRef, type, senderAddress, ...payload);
} else {
this.log(
`Sending the question to the appropriate actor. Type: ${type}, sender: ${senderAddress}, and payload:`,
payload
);
- this.sendMessage(actorRef, type, senderAddress, ...payload).then(
+ this.sendMessageAndWait(actorRef, type, senderAddress, ...payload).then(
message => {
this.log(
`Received an answer, sending the answer "${message}" for the question with type: ${type}, sender: ${senderAddress}, and payload:`,
@@ -107,7 +107,7 @@ export class ActorSystem {
(options as any).paramOptions,
(options as any).strategies
);
- return this.ref>(fullAddress);
+ return this.ref>(fullAddress);
};
removeActor = (refOrAddress: ActorRef | Address) => {
@@ -154,6 +154,32 @@ export class ActorSystem {
type: string,
senderAddress: Address | null,
...payload: any[]
+ ): void => {
+ this.sendMessageAndWait(target, type, senderAddress, ...payload).then(
+ () => {
+ /* do nothing */
+ },
+ error => {
+ this.log(
+ `Catch an error when executing message with type ${type}`,
+ "Target",
+ target,
+ "Sender",
+ senderAddress,
+ "Payload",
+ payload,
+ "Error",
+ error
+ );
+ }
+ );
+ };
+
+ sendMessageAndWait = (
+ target: ActorRef | Address,
+ type: string,
+ senderAddress: Address | null,
+ ...payload: any[]
): Promise => {
this.log(
`Received a request to send a message with type: ${type}`,
diff --git a/test/Actor.test.ts b/test/Actor.test.ts
index 56818f1..80a0ebd 100644
--- a/test/Actor.test.ts
+++ b/test/Actor.test.ts
@@ -69,7 +69,7 @@ describe("Actor", () => {
}).toThrowError();
});
- it("should be possible to send messages with proper payloads", done => {
+ it("should be possible to send message", done => {
const counterActor = new ActorSystem().createActor({
name: "myCounter",
actorClass: CounterActor,
@@ -78,7 +78,32 @@ describe("Actor", () => {
done();
}
});
- counterActor.invoke().increment();
+ counterActor.send().increment();
+ });
+
+ it("should be possible to ask question", async () => {
+ const counterActor = new ActorSystem().createActor({
+ name: "myCounter",
+ actorClass: CounterActor
+ });
+ await counterActor.ask().increment();
+ await expect(counterActor.ask().currentCounterValue()).resolves.toBe(1);
+ });
+
+ it("should not crash when sending message which handled incorrectly", () => {
+ const dummyActor = new ActorSystem().createActor({
+ name: "dummy",
+ actorClass: DummyActor
+ });
+ dummyActor.send().dummyCrash();
+ });
+
+ it("should crash when asking which handled incorrectly", async () => {
+ const dummyActor = new ActorSystem().createActor({
+ name: "dummy",
+ actorClass: DummyActor
+ });
+ await expect(dummyActor.ask().dummyCrash()).rejects.toBeInstanceOf(Error);
});
it("should be possible to send messages with more than 1 payload", done => {
@@ -86,12 +111,12 @@ describe("Actor", () => {
name: "myDummy",
actorClass: DummyActor
});
- dummyActor.invoke().registerCallback((param1, param2) => {
+ dummyActor.send().registerCallback((param1, param2) => {
expect(param1).toBe("one");
expect(param2).toBe("two");
done();
});
- dummyActor.invoke().dummy2Param("one", "two");
+ dummyActor.send().dummy2Param("one", "two");
});
it("should be able to send message to another actor", () => {
@@ -99,7 +124,7 @@ describe("Actor", () => {
name: "myDummy",
actorClass: DummyActor
});
- dummyActor.invoke().dummy();
+ dummyActor.send().dummy();
});
it("should be able to cancel execution", done => {
@@ -111,39 +136,9 @@ describe("Actor", () => {
done();
}
});
- switcherActor
- .invoke()
- .changeRoom("one")
- .then(
- () => {
- /* nothing */
- },
- () => {
- /* nothing */
- }
- );
- switcherActor
- .invoke()
- .changeRoom("two")
- .then(
- () => {
- /* nothing */
- },
- () => {
- /* nothing */
- }
- );
- switcherActor
- .invoke()
- .changeRoom("three")
- .then(
- () => {
- /* nothing */
- },
- () => {
- /* nothing */
- }
- );
+ switcherActor.send().changeRoom("one");
+ switcherActor.send().changeRoom("two");
+ switcherActor.send().changeRoom("three");
});
it("should be able to ignore older messages with the same type", done => {
@@ -156,39 +151,9 @@ describe("Actor", () => {
},
strategies: ["IgnoreOlderMessageWithTheSameType"]
});
- switcherActor
- .invoke()
- .changeRoom("one")
- .then(
- () => {
- /* nothing */
- },
- () => {
- /* nothing */
- }
- );
- switcherActor
- .invoke()
- .changeRoom("two")
- .then(
- () => {
- /* nothing */
- },
- () => {
- /* nothing */
- }
- );
- switcherActor
- .invoke()
- .changeRoom("three")
- .then(
- () => {
- /* nothing */
- },
- () => {
- /* nothing */
- }
- );
+ switcherActor.send().changeRoom("one");
+ switcherActor.send().changeRoom("two");
+ switcherActor.send().changeRoom("three");
});
});
@@ -198,19 +163,20 @@ type DummyAPI = {
replyDummy: () => Promise;
registerCallback: (callback: (param1: string, param2: string) => void) => Promise;
dummy2Param: (param1: string, param2: string) => Promise;
+ dummyCrash: () => Promise;
};
class DummyActor extends Actor implements DummyAPI {
counter = 0;
callback: ((param1: string, param2: string) => void) | undefined;
dummy = async () => {
- this.at(this.address).replyDummy();
+ this.sendTo(this.address).replyDummy();
};
replyDummy = async () => {
const senderRef: ActorRef = this.context.senderRef!;
if (this.counter === 0) {
- this.at(senderRef).replyDummy();
+ this.sendTo(senderRef).replyDummy();
}
this.counter++;
};
@@ -222,18 +188,27 @@ class DummyActor extends Actor implements DummyAPI {
dummy2Param = async (param1: string, param2: string) => {
this.callback && this.callback(param1, param2);
};
+
+ dummyCrash = async () => {
+ throw new Error("Crash!");
+ };
}
// Counter
type CounterAPI = {
increment: () => Promise;
+ currentCounterValue: () => Promise;
};
class CounterActor extends Actor> implements CounterAPI {
counter = 0;
listener: Listener | undefined;
+ currentCounterValue = async () => {
+ return this.counter;
+ };
+
increment = async () => {
this.counter = await asyncInc(this.counter);
this.listener && this.listener(this.counter);
diff --git a/test/ActorSystem.test.ts b/test/ActorSystem.test.ts
index e9f438a..c598e3e 100644
--- a/test/ActorSystem.test.ts
+++ b/test/ActorSystem.test.ts
@@ -13,7 +13,7 @@ describe("Actor System", () => {
it("should handle exception when message is sent to an actor in a non-existent ActorSystem", async done => {
const actorSystem = new ActorSystem();
try {
- await actorSystem.sendMessage(
+ await actorSystem.sendMessageAndWait(
{ actorSystemName: "non-existent actor", localAddress: "random address" },
"random-type",
null,
@@ -52,7 +52,7 @@ describe("Multi-Actor System", () => {
});
it("should allow actors to send message in different actor system", done => {
- serverActor.invoke().registerListener((param1, param2) => {
+ serverActor.send().registerListener((param1, param2) => {
expect(param1).toBe("1");
expect(param2).toBe("2");
done();
@@ -65,12 +65,12 @@ describe("Multi-Actor System", () => {
actorClass: ClientActor
});
setTimeout(() => {
- actorRef.invoke().trigger();
+ actorRef.send().trigger();
}, 3000); // give time for the handshake
});
- it("should throw exception when trying to send message to an actor of a disconnected actor system", done => {
- serverActor.invoke().registerListener(() => {
+ it("should throw exception when trying to ask question to an actor of a disconnected actor system", done => {
+ serverActor.send().registerListener(() => {
fail();
});
const socket = ioClient.connect(`http://localhost:${port}`);
@@ -83,7 +83,7 @@ describe("Multi-Actor System", () => {
setTimeout(() => {
socket.disconnect();
actorRef
- .invoke()
+ .ask()
.trigger()
.then(
() => {
@@ -95,8 +95,9 @@ describe("Multi-Actor System", () => {
);
}, 1000); // give time for the handshake
});
+
it("should allow actors to send message in different actor system after reconnection", done => {
- serverActor.invoke().registerListener(() => {
+ serverActor.send().registerListener(() => {
done();
});
const socket = ioClient.connect(`http://localhost:${port}`, {
@@ -113,7 +114,7 @@ describe("Multi-Actor System", () => {
socket.disconnect();
socket.connect();
setTimeout(() => {
- actorRef.invoke().trigger();
+ actorRef.send().trigger();
}, 1000);
}, 1000); // give time for the handshake
});
@@ -125,7 +126,7 @@ type ClientAPI = {
class ClientActor extends Actor implements ClientAPI {
trigger = async () => {
- await this.at({
+ await this.askTo({
actorSystemName: "server",
localAddress: "serverActor"
}).connect("1", "2");