Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Contravariant Msg type in Dispatchable and LocalActorRef #148

Open
wants to merge 5 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion @nact/core/actor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<string | number> = spawnStateless(system, (_msg: string | number) => {});

let smallerActor: LocalActorRef<string> = actor;
dispatch(smallerActor, "a");

// @ts-expect-error
let biggerActor: LocalActorRef<string | number | symbol> = actor;
dispatch(biggerActor, "a");
});
});

describe('Actor', function () {
Expand Down
22 changes: 11 additions & 11 deletions @nact/core/actor.ts
Original file line number Diff line number Diff line change
@@ -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 { };

Expand Down Expand Up @@ -360,17 +360,17 @@ export type ActorProps<State, Msg, ParentRef extends ActorSystemRef | LocalActor
afterStop?: (state: State, ctx: ActorContext<Msg, ParentRef>) => void | Promise<void>
};

export type StatelessActorProps<ParentRef extends ActorSystemRef | LocalActorRef<any>> = {
export type StatelessActorProps<Msg, ParentRef extends ActorSystemRef | LocalActorRef<any>> = {
name?: string,
shutdownAfter?: Milliseconds,
onCrash?: SupervisionActorFunc<InferMsgFromStatelessFunc<any>, ParentRef>,
onCrash?: SupervisionActorFunc<Msg, ParentRef>,
};


export function spawn<ParentRef extends LocalActorSystemRef | LocalActorRef<any>, Func extends ActorFunc<any, any, ParentRef>>(
parent: ParentRef,
f: Func,
properties?: ActorProps<InferStateFromFunc<Func>, InferMsgFromFunc<Func>, ParentRef> | StatelessActorProps<ParentRef>
properties?: ActorProps<InferStateFromFunc<Func>, InferMsgFromFunc<Func>, ParentRef> | StatelessActorProps<InferMsgFromFunc<Func>, ParentRef>
): LocalActorRef<InferMsgFromFunc<Func>> {
return applyOrThrowIfStopped(
parent,
Expand All @@ -390,7 +390,7 @@ const statelessSupervisionPolicy = (_: unknown, __: unknown, ctx: SupervisionCon
export function spawnStateless<ParentRef extends LocalActorSystemRef | LocalActorRef<any>, Func extends StatelessActorFunc<any, ParentRef>>(
parent: ParentRef,
f: Func,
propertiesOrName?: StatelessActorProps<ParentRef>
propertiesOrName?: StatelessActorProps<InferMsgFromStatelessFunc<Func>, ParentRef>
): LocalActorRef<InferMsgFromStatelessFunc<Func>> {
return spawn(
parent,
Expand Down
100 changes: 100 additions & 0 deletions @nact/core/functions.test.ts
Original file line number Diff line number Diff line change
@@ -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<string | number>;
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<string | number>;
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<string | number>;
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<string | number>
>(
actor: TActor
) => {
dispatch(actor, "text");
dispatch(actor, 1000);
// @ts-expect-error
dispatch(actor, null);
};

let biggerActor: LocalActorRef<string | number | symbol> = spawnStateless(
system,
(_msg: string | number | symbol) => {}
);
scopeThatUsesSmallerActor(biggerActor);
});
});
16 changes: 7 additions & 9 deletions @nact/core/functions.ts
Original file line number Diff line number Diff line change
@@ -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<ICanStop>(actor);
Expand All @@ -15,7 +15,7 @@ export type QueryMsgFactory<Req, Res> = (tempRef: Dispatchable<Res>) => Req;
export type InferResponseFromMsgFactory<T extends QueryMsgFactory<any, any>> = T extends QueryMsgFactory<infer _Req, infer Res> ? Res : never;
type Maybe<T> = Partial<T>;

export function query<ActorRef extends Dispatchable<any>, MsgFactory extends QueryMsgFactory<ActorRef extends Dispatchable<infer Msg> ? Msg : never, any>>(actor: ActorRef, queryFactory: MsgFactory, timeout: Milliseconds):
export function query<Msg, MsgFactory extends QueryMsgFactory<Msg, any>>(actor: Dispatchable<Msg>, queryFactory: MsgFactory, timeout: Milliseconds):
Promise<InferResponseFromMsgFactory<MsgFactory>> {
if (!timeout) {
throw new Error('A timeout is required to be specified');
Expand All @@ -28,9 +28,7 @@ export function query<ActorRef extends Dispatchable<any>, MsgFactory extends Que
: Promise.reject(new Error('Actor stopped or never existed. Query can never resolve'));
};

export function dispatch<ActorRef extends Dispatchable<any>>(actor: ActorRef, msg: ActorRef extends Dispatchable<infer Msg> ? Msg : never): void {
let concreteActor = find<ICanDispatch<ActorRef>>(actor);
concreteActor &&
concreteActor.dispatch &&
concreteActor.dispatch(msg);
};
export function dispatch<Msg>(actor: Dispatchable<Msg>, msg: Msg): void {
let concreteActor = find<ICanDispatch<Msg>>(actor);
concreteActor && concreteActor.dispatch && concreteActor.dispatch(msg);
}
2 changes: 1 addition & 1 deletion @nact/core/references.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ enum DispatchableMarker {
}

export type Dispatchable<Msg> =
{ __dispatch__: DispatchableMarker, protocol: Msg } & Ref;
{ __dispatch__: DispatchableMarker, protocol: (msg: Msg) => void } & Ref;

enum StoppableMarker {
_ = ""
Expand Down