novy-syndicate/actor.ts

179 lines
5.3 KiB
TypeScript
Raw Normal View History

2021-02-22 09:12:10 +00:00
import { IdentitySet, Value } from 'preserves';
2021-02-17 19:57:15 +00:00
2021-02-22 09:12:10 +00:00
export type Assertion = Value<Ref<Entity>>;
2021-02-17 19:57:15 +00:00
2021-02-22 18:37:47 +00:00
export type Handle = number;
2021-02-17 19:57:15 +00:00
2021-02-22 09:12:10 +00:00
export type ExitReason = null | { ok: true } | { ok: false, err: Error };
2021-02-17 19:57:15 +00:00
2021-02-22 09:12:10 +00:00
export const assert = Symbol('assert');
export const retract = Symbol('retract');
export const message = Symbol('message');
2021-02-17 19:57:15 +00:00
2021-02-22 09:12:10 +00:00
export interface Entity {
[assert]?(turn: Turn, assertion: Assertion, handle: Handle): void;
[retract]?(turn: Turn, handle: Handle): void;
[message]?(turn: Turn, message: Assertion): void;
2021-02-22 09:12:10 +00:00
}
2021-02-17 19:57:15 +00:00
2021-02-22 18:51:19 +00:00
export class Ref<T> {
2021-02-17 19:57:15 +00:00
readonly actor: Actor;
2021-02-22 09:12:10 +00:00
readonly target: T;
2021-02-22 18:51:19 +00:00
2021-02-22 09:12:10 +00:00
constructor(actor: Actor, target: T) {
2021-02-17 19:57:15 +00:00
this.actor = actor;
this.target = target;
}
2021-02-22 18:51:19 +00:00
sync(turn: Turn, syncable: Ref<LocalAction>) {
turn.enqueue(syncable.actor, t => syncable.target(t));
}
2021-02-17 19:57:15 +00:00
}
2021-02-22 20:08:14 +00:00
export type OutboundMap = Map<Handle, Ref<Entity>>;
2021-02-17 19:57:15 +00:00
2021-02-22 18:51:19 +00:00
export class Actor {
2021-02-22 09:12:10 +00:00
readonly outbound: OutboundMap;
2021-02-17 19:57:15 +00:00
exitReason: ExitReason = null;
2021-02-22 09:12:10 +00:00
constructor(initialAssertions: OutboundMap = new Map()) {
this.outbound = initialAssertions;
2021-02-17 19:57:15 +00:00
}
get alive(): boolean {
return this.exitReason === null;
}
2021-02-22 18:37:47 +00:00
terminateWith(t: Turn, reason: Exclude<ExitReason, null>) {
2021-02-17 19:57:15 +00:00
if (this.alive) {
this.exitReason = reason;
2021-02-22 20:08:14 +00:00
this.outbound.forEach((peer, h) => t._retract(peer, h));
2021-02-17 19:57:15 +00:00
}
}
2021-02-22 09:12:10 +00:00
execute(proc: () => void): void {
queueMicrotask(() => {
if (this.alive) {
try {
proc();
} catch (err) {
2021-02-22 18:37:47 +00:00
console.error(Actor, err);
Turn.for(this, t => this.terminateWith(t, { ok: false, err }));
2021-02-22 09:12:10 +00:00
}
}
});
}
2021-02-17 19:57:15 +00:00
}
2021-02-22 09:12:10 +00:00
let nextHandle = 0;
2021-02-17 19:57:15 +00:00
2021-02-22 09:12:10 +00:00
type LocalAction = (t: Turn) => void;
export class Turn {
readonly actor: Actor | null; // whose turn it is to act during this Turn
readonly queues: Map<Actor, LocalAction[]> = new Map();
readonly localActions: Array<LocalAction> = [];
completed = false;
static for(actor: Actor | null, f: (t: Turn) => void): void {
const t = new Turn(actor);
f(t);
t.complete();
}
private constructor(actor: Actor | null) {
this.actor = actor;
2021-02-17 19:57:15 +00:00
}
2021-02-22 09:12:10 +00:00
_ensureActor(what: string): Actor {
if (this.actor === null) {
throw new Error(`Cannot ${what} from non-Actor context`);
2021-02-17 19:57:15 +00:00
}
2021-02-22 09:12:10 +00:00
return this.actor;
}
2021-02-22 18:37:47 +00:00
ref<T>(t: T, what: string = "ref"): Ref<T> {
2021-02-22 19:45:19 +00:00
return new Ref(this._ensureActor(what), t);
2021-02-22 18:37:47 +00:00
}
2021-02-22 09:12:10 +00:00
spawn(bootProc: (t: Turn) => void, initialAssertions?: IdentitySet<Handle>): void {
if ((initialAssertions !== void 0) && (initialAssertions.size > 0)) {
this._ensureActor("spawn with initialAssertions");
2021-02-17 19:57:15 +00:00
}
2021-02-22 09:12:10 +00:00
this.localActions.push(() => {
const transferred = this.actor === null
? void 0
: extractFromMap(this.actor.outbound, initialAssertions);
const child = new Actor(transferred);
child.execute(() => Turn.for(child, bootProc));
});
}
2021-02-22 18:37:47 +00:00
quit(): void {
2021-02-22 19:55:49 +00:00
this.localActions.push(t => this._ensureActor("quit").terminateWith(t, { ok: true }));
2021-02-22 18:37:47 +00:00
}
2021-02-22 18:51:19 +00:00
assert(location: Ref<Entity>, assertion: Assertion): Handle {
2021-02-22 09:12:10 +00:00
const h = nextHandle++;
this.enqueue(location.actor, t => {
2021-02-22 20:08:14 +00:00
this._ensureActor("assert").outbound.set(h, location);
location.target[assert]?.(t, assertion, h);
2021-02-22 09:12:10 +00:00
});
return h;
}
retract(h: Handle): void {
2021-02-22 20:08:14 +00:00
this._retract(this._ensureActor("retract").outbound.get(h)!, h);
2021-02-22 09:12:10 +00:00
}
2021-02-22 18:51:19 +00:00
replace(location: Ref<Entity>, h: Handle | undefined, assertion: Assertion): Handle {
2021-02-22 18:37:47 +00:00
const newHandle = this.assert(location, assertion);
if (h !== void 0) this.retract(h);
return newHandle;
}
2021-02-22 18:51:19 +00:00
_retract(location: Ref<Entity>, handle: Handle): void {
2021-02-22 09:12:10 +00:00
this.enqueue(location.actor, t => {
this.actor!.outbound.delete(handle);
location.target[retract]?.(t, handle);
2021-02-22 09:12:10 +00:00
});
2021-02-17 19:57:15 +00:00
}
2021-02-22 18:51:19 +00:00
sync(location: Ref<any>): Promise<Turn> {
2021-02-22 20:04:19 +00:00
return new Promise(resolve =>
this.enqueue(location.actor, t => location.sync(t, this.ref(resolve, "sync"))));
2021-02-22 09:12:10 +00:00
}
2021-02-22 18:51:19 +00:00
message(location: Ref<Entity>, assertion: Assertion): void {
this.enqueue(location.actor, t => location.target[message]?.(t, assertion));
2021-02-22 09:12:10 +00:00
}
enqueue(actor: Actor, a: LocalAction): void {
2021-02-22 20:00:32 +00:00
this.queues.has(actor) ? this.queues.get(actor)!.push(a) : this.queues.set(actor, [a]);
2021-02-22 09:12:10 +00:00
}
complete(): void {
2021-02-22 19:55:49 +00:00
if (this.completed) throw new Error("Reuse of completed Turn!");
2021-02-22 09:12:10 +00:00
this.completed = true;
this.queues.forEach((queue, actor) =>
actor.execute(() => queue.forEach(f => Turn.for(actor, f))));
2021-02-22 18:37:47 +00:00
if (this.localActions.length > 0) {
queueMicrotask(() => this.localActions.forEach(f => Turn.for(this.actor, f)));
}
2021-02-22 09:12:10 +00:00
}
}
function extractFromMap<K, V>(map: Map<K, V>, keys?: IdentitySet<K>): Map<K, V> {
const result: Map<K, V> = new Map();
if (keys !== void 0) {
keys.forEach(key => {
const value = map.get(key);
if (value !== void 0) {
map.delete(key);
result.set(key, value);
}
2021-02-17 19:57:15 +00:00
});
}
2021-02-22 09:12:10 +00:00
return result;
2021-02-17 19:57:15 +00:00
}