116 lines
3.3 KiB
TypeScript
116 lines
3.3 KiB
TypeScript
|
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)));
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
}
|