From e6b61d27a6711330f43e7bab8ed8bb9665e625d4 Mon Sep 17 00:00:00 2001 From: le0-0 Date: Wed, 22 Jan 2025 15:50:39 +0100 Subject: [PATCH 1/5] Added unit tests for contravariance of LocalActorRef. The tests are written like other jest tests but will fail on 'npm run typecheck' instead of 'npm run test'. --- @nact/core/actor.test.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/@nact/core/actor.test.ts b/@nact/core/actor.test.ts index b5463b2..28af23c 100644 --- a/@nact/core/actor.test.ts +++ b/@nact/core/actor.test.ts @@ -3,7 +3,7 @@ import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; import { SupervisionContext } from './actor'; -import { start, spawn, spawnStateless, dispatch, stop, query, milliseconds, ActorContext, applyOrThrowIfStopped } from './index'; +import { ActorContext, applyOrThrowIfStopped, dispatch, milliseconds, query, spawn, spawnStateless, start, stop } from './index'; import { LocalActorRef, LocalActorSystemRef, nobody } from './references'; chai.use(chaiAsPromised); @@ -59,6 +59,17 @@ describe('LocalActorRef', function () { child.path.system!.should.equal(system.path.system); grandchild.path.parts.slice(0, child.path.parts.length - 2).should.deep.equal(child.path.parts); }); + + it('should be contravariant to message type', function () { + let actor: LocalActorRef = spawnStateless(system, (_msg: string | number) => {}); + + let smallerActor: LocalActorRef = actor; + dispatch(smallerActor, "a"); + + // @ts-expect-error + let biggerActor: LocalActorRef = actor; + dispatch(biggerActor, "a"); + }); }); describe('Actor', function () { From ea055cb14a95a5765ad0ffa84f7e31c9ae62bb59 Mon Sep 17 00:00:00 2001 From: le0-0 Date: Wed, 22 Jan 2025 16:02:57 +0100 Subject: [PATCH 2/5] Fixed type inconsistency between StatelessActorProps and ActorProps. This caused StatelessActorProps to be considered instead of in some cases. --- @nact/core/actor.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/@nact/core/actor.ts b/@nact/core/actor.ts index 8340827..a50d0d6 100644 --- a/@nact/core/actor.ts +++ b/@nact/core/actor.ts @@ -1,13 +1,13 @@ -import { ActorSystemRef, localActorRef, LocalActorRef, LocalActorSystemRef, localTemporaryRef } from "./references"; +import { Milliseconds } from "."; +import assert from './assert'; import { Deferral } from './deferral'; +import { ICanAssertNotStopped, ICanDispatch, ICanHandleFault, ICanManageTempReferences, ICanQuery, ICanReset, ICanStop, IHaveChildren, IHaveName, InferResponseFromMsgFactory, QueryMsgFactory } from "./interfaces"; +import { addMacrotask, clearMacrotask } from './macrotask'; +import { ActorPath } from "./paths"; +import { ActorSystemRef, localActorRef, LocalActorRef, LocalActorSystemRef, localTemporaryRef } from "./references"; +import { defaultSupervisionPolicy, SupervisionActions } from './supervision'; import { applyOrThrowIfStopped, find } from './system-map'; import Queue from './vendored/denque'; -import assert from './assert'; -import { defaultSupervisionPolicy, SupervisionActions } from './supervision'; -import { ActorPath } from "./paths"; -import { Milliseconds } from "."; -import { addMacrotask, clearMacrotask } from './macrotask' -import { ICanAssertNotStopped, ICanDispatch, ICanHandleFault, ICanManageTempReferences, ICanQuery, ICanReset, ICanStop, IHaveChildren, IHaveName, InferResponseFromMsgFactory, QueryMsgFactory } from "./interfaces"; function unit(): void { }; @@ -360,17 +360,17 @@ export type ActorProps) => void | Promise }; -export type StatelessActorProps> = { +export type StatelessActorProps> = { name?: string, shutdownAfter?: Milliseconds, - onCrash?: SupervisionActorFunc, ParentRef>, + onCrash?: SupervisionActorFunc, }; export function spawn, Func extends ActorFunc>( parent: ParentRef, f: Func, - properties?: ActorProps, InferMsgFromFunc, ParentRef> | StatelessActorProps + properties?: ActorProps, InferMsgFromFunc, ParentRef> | StatelessActorProps, ParentRef> ): LocalActorRef> { return applyOrThrowIfStopped( parent, @@ -390,7 +390,7 @@ const statelessSupervisionPolicy = (_: unknown, __: unknown, ctx: SupervisionCon export function spawnStateless, Func extends StatelessActorFunc>( parent: ParentRef, f: Func, - propertiesOrName?: StatelessActorProps + propertiesOrName?: StatelessActorProps, ParentRef> ): LocalActorRef> { return spawn( parent, From 1f59a0b440d0983235ebd49c22caa8a640241303 Mon Sep 17 00:00:00 2001 From: le0-0 Date: Wed, 22 Jan 2025 16:04:06 +0100 Subject: [PATCH 3/5] Made Dispatchabe and LocalActorRef contravariant. --- @nact/core/references.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/@nact/core/references.ts b/@nact/core/references.ts index 3754098..65dd970 100644 --- a/@nact/core/references.ts +++ b/@nact/core/references.ts @@ -9,7 +9,7 @@ enum DispatchableMarker { } export type Dispatchable = - { __dispatch__: DispatchableMarker, protocol: Msg } & Ref; + { __dispatch__: DispatchableMarker, protocol: (msg: Msg) => void } & Ref; enum StoppableMarker { _ = "" From 850c250c2002c0cdb8e2047903f9f98e6ed89d7a Mon Sep 17 00:00:00 2001 From: le0-0 Date: Sun, 2 Feb 2025 17:03:21 +0100 Subject: [PATCH 4/5] Added test cases for the interface of dispatch and query when supported message types are partially unknown. --- @nact/core/functions.test.ts | 100 +++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 @nact/core/functions.test.ts diff --git a/@nact/core/functions.test.ts b/@nact/core/functions.test.ts new file mode 100644 index 0000000..2f90c9a --- /dev/null +++ b/@nact/core/functions.test.ts @@ -0,0 +1,100 @@ +import { spawnStateless } from "."; +import { dispatch, query } from "./functions"; +import { start, stop } from "./index"; +import { Dispatchable, LocalActorRef, LocalActorSystemRef } from "./references"; + +describe("query", function () { + let system: LocalActorSystemRef; + beforeEach(() => { + system = start(); + }); + afterEach(() => stop(system)); + + it("should accept only supported messages", function () { + let actor = spawnStateless( + system, + (msg: { + readonly sender: Dispatchable; + readonly valueToDouble: string | number; + }) => { + const doubledValued = + typeof msg.valueToDouble === "string" + ? msg.valueToDouble + msg.valueToDouble + : msg.valueToDouble + msg.valueToDouble; + dispatch(msg.sender, doubledValued); + } + ); + + query(actor, (x) => ({ sender: x, valueToDouble: 10 }), 30); + query(actor, (x) => ({ sender: x, valueToDouble: "ten" }), 30); + // @ts-expect-error + query(actor, (x) => ({ sender: x, valueToDouble: null }), 30); + }); + + it("should accept only known supported messages even with no upper bound on supported message types", function () { + let scopeThatUsesSmallerActor = < + TActor extends LocalActorRef<{ + sender: Dispatchable; + valueToDouble: string | number; + }> + >( + actor: TActor + ) => { + query(actor, (x) => ({ sender: x, valueToDouble: 10 }), 30); + query(actor, (x) => ({ sender: x, valueToDouble: "ten" }), 30); + // @ts-expect-error + query(actor, (x) => ({ sender: x, valueToDouble: null }), 30); + }; + + let actor = spawnStateless( + system, + (msg: { + readonly sender: Dispatchable; + readonly valueToDouble: string | number; + }) => { + const doubledValued = + typeof msg.valueToDouble === "string" + ? msg.valueToDouble + msg.valueToDouble + : msg.valueToDouble + msg.valueToDouble; + dispatch(msg.sender, doubledValued); + } + ); + scopeThatUsesSmallerActor(actor); + }); +}); + +describe("dispatch", function () { + let system: LocalActorSystemRef; + beforeEach(() => { + system = start(); + }); + afterEach(() => stop(system)); + + it("should accept only supported messages", function () { + let actor = spawnStateless(system, (_msg: string | number) => {}); + + dispatch(actor, "text"); + dispatch(actor, 1000); + // @ts-expect-error + dispatch(actor, null); + }); + + it("should accept only known supported messages even with no upper bound on supported message types", function () { + let scopeThatUsesSmallerActor = < + TActor extends LocalActorRef + >( + actor: TActor + ) => { + dispatch(actor, "text"); + dispatch(actor, 1000); + // @ts-expect-error + dispatch(actor, null); + }; + + let biggerActor: LocalActorRef = spawnStateless( + system, + (_msg: string | number | symbol) => {} + ); + scopeThatUsesSmallerActor(biggerActor); + }); +}); From 97194cf361ff7a4e737f4d9c1ead93c3a2f0e996 Mon Sep 17 00:00:00 2001 From: le0-0 Date: Sun, 2 Feb 2025 17:06:37 +0100 Subject: [PATCH 5/5] Implemented changes to dispatch and query to fix type edge case. (Look description`) Functions dispatch and query now accepts message types that are definitely supported, and only those, even if the actual type of messages that can be processed is partially unknown. Previous implementation did not accept ANY message type if processable messages was partially unknown. --- @nact/core/functions.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/@nact/core/functions.ts b/@nact/core/functions.ts index f10e42d..9e2570e 100644 --- a/@nact/core/functions.ts +++ b/@nact/core/functions.ts @@ -1,7 +1,7 @@ +import { ICanDispatch, ICanQuery, ICanStop } from "./interfaces"; import { Dispatchable, Stoppable } from "./references"; -import { Milliseconds } from "./time"; import { find } from './system-map'; -import { ICanDispatch, ICanQuery, ICanStop } from "./interfaces"; +import { Milliseconds } from "./time"; export function stop(actor: Stoppable) { let concreteActor = find(actor); @@ -15,7 +15,7 @@ export type QueryMsgFactory = (tempRef: Dispatchable) => Req; export type InferResponseFromMsgFactory> = T extends QueryMsgFactory ? Res : never; type Maybe = Partial; -export function query, MsgFactory extends QueryMsgFactory ? Msg : never, any>>(actor: ActorRef, queryFactory: MsgFactory, timeout: Milliseconds): +export function query>(actor: Dispatchable, queryFactory: MsgFactory, timeout: Milliseconds): Promise> { if (!timeout) { throw new Error('A timeout is required to be specified'); @@ -28,9 +28,7 @@ export function query, MsgFactory extends Que : Promise.reject(new Error('Actor stopped or never existed. Query can never resolve')); }; -export function dispatch>(actor: ActorRef, msg: ActorRef extends Dispatchable ? Msg : never): void { - let concreteActor = find>(actor); - concreteActor && - concreteActor.dispatch && - concreteActor.dispatch(msg); -}; +export function dispatch(actor: Dispatchable, msg: Msg): void { + let concreteActor = find>(actor); + concreteActor && concreteActor.dispatch && concreteActor.dispatch(msg); +}