novy-syndicate/actor.ts

116 lines
3.3 KiB
TypeScript
Raw Normal View History

2021-02-17 19:57:15 +00:00
import { Value } from 'preserves';
type Assertion = Value<Peer>;
type ActorId = number;
type ExitReason = null | { ok: true } | { ok: false, err: Error };
let nextActorId: ActorId = 0;
type AssertionHandle = object;
const assert = Symbol('assert');
const retract = Symbol('retract');
const message = Symbol('message');
type RestParameters<T extends (arg: any, ...args: any) => any> =
T extends (arg: any, ...args: infer P) => any ? P : never;
type Event =
| { type: typeof assert, args: RestParameters<Facet[typeof assert]> }
| { type: typeof retract, args: RestParameters<Facet[typeof retract]> }
| { type: typeof message, args: RestParameters<Facet[typeof message]> }
type Action = Event;
class Peer {
readonly actor: Actor;
readonly target: Facet;
constructor(actor: Actor, target: Facet) {
this.actor = actor;
this.target = target;
}
}
interface Facet {
[assert](turn: Turn, assertion: Assertion, handle: AssertionHandle): void;
[retract](turn: Turn, handle: AssertionHandle): void;
[message](turn: Turn, message: Assertion): void;
}
class Actor {
readonly id: ActorId = nextActorId++;
exitReason: ExitReason = null;
scheduleTurn(target: Facet, turn: Turn) {
queueMicrotask(() => {
if (this.alive) {
try {
const event = turn.event;
(target as any)[event.type](turn, ...event.args);
// ^ This is safe. Try replacing it with the following to see:
//
// switch (event.type) {
// case assert: target[event.type](turn, ...event.args); break;
// case retract: target[event.type](turn, ...event.args); break;
// case message: target[event.type](turn, ...event.args); break;
// }
turn.finish();
} catch (err) {
this.terminateWith({ ok: false, err });
}
}
});
}
get alive(): boolean {
return this.exitReason === null;
}
stop() {
this.terminateWith({ ok: true });
}
terminateWith(reason: Exclude<ExitReason, null>) {
if (this.alive) {
this.exitReason = reason;
// TODO cleanup
}
}
}
class Turn {
readonly recipient: Actor;
readonly event: Event;
readonly actions: Map<Actor, Map<Facet, Action[]>> = new Map();
constructor(recipient: Actor, event: Event) {
this.recipient = recipient;
this.event = event;
}
enqueueAction(peer: Peer, action: Action) {
let targetMap = this.actions.get(peer.actor);
if (targetMap === void 0) {
targetMap = new Map();
this.actions.set(peer.actor, targetMap);
}
let actions = targetMap.get(peer.target);
if (actions === void 0) {
actions = [];
targetMap.set(peer.target, actions);
}
actions.push(action);
}
finish() {
this.actions.forEach((targetMap, actor) => {
targetMap.forEach((actions, target) => {
actions.forEach(action => actor.scheduleTurn(target, new Turn(actor, action)));
});
});
}
}