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");