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 () { 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, 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); + }); +}); 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); +} 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 { _ = ""