import { IdentitySet, Value } from 'preserves'; export type Assertion = Value>; export type Handle = number; export type ExitReason = null | { ok: true } | { ok: false, err: Error }; export const assert = Symbol('assert'); export const retract = Symbol('retract'); export const message = Symbol('message'); export interface Entity { [assert]?(turn: Turn, assertion: Assertion, handle: Handle): void; [retract]?(turn: Turn, handle: Handle): void; [message]?(turn: Turn, message: Assertion): void; } export class Ref { readonly actor: Actor; readonly target: T; constructor(actor: Actor, target: T) { this.actor = actor; this.target = target; } sync(turn: Turn, syncable: Ref) { turn.enqueue(syncable.actor, t => syncable.target(t)); } } export type OutboundMap = Map>; export class Actor { readonly outbound: OutboundMap; exitReason: ExitReason = null; constructor(initialAssertions: OutboundMap = new Map()) { this.outbound = initialAssertions; } get alive(): boolean { return this.exitReason === null; } terminateWith(t: Turn, reason: Exclude) { if (this.alive) { this.exitReason = reason; this.outbound.forEach((peer, h) => t._retract(peer, h)); } } execute(proc: () => void): void { queueMicrotask(() => { if (this.alive) { try { proc(); } catch (err) { console.error(Actor, err); Turn.for(this, t => this.terminateWith(t, { ok: false, err })); } } }); } } let nextHandle = 0; type LocalAction = (t: Turn) => void; export class Turn { readonly actor: Actor | null; // whose turn it is to act during this Turn readonly queues: Map = new Map(); readonly localActions: Array = []; 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; } _ensureActor(what: string): Actor { if (this.actor === null) { throw new Error(`Cannot ${what} from non-Actor context`); } return this.actor; } ref(t: T, what: string = "ref"): Ref { return new Ref(this._ensureActor(what), t); } spawn(bootProc: (t: Turn) => void, initialAssertions?: IdentitySet): void { if ((initialAssertions !== void 0) && (initialAssertions.size > 0)) { this._ensureActor("spawn with initialAssertions"); } 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)); }); } quit(): void { this.localActions.push(t => this._ensureActor("quit").terminateWith(t, { ok: true })); } assert(location: Ref, assertion: Assertion): Handle { const h = nextHandle++; this.enqueue(location.actor, t => { this._ensureActor("assert").outbound.set(h, location); location.target[assert]?.(t, assertion, h); }); return h; } retract(h: Handle): void { this._retract(this._ensureActor("retract").outbound.get(h)!, h); } replace(location: Ref, h: Handle | undefined, assertion: Assertion): Handle { const newHandle = this.assert(location, assertion); if (h !== void 0) this.retract(h); return newHandle; } _retract(location: Ref, handle: Handle): void { this.enqueue(location.actor, t => { this.actor!.outbound.delete(handle); location.target[retract]?.(t, handle); }); } sync(location: Ref): Promise { return new Promise(resolve => this.enqueue(location.actor, t => location.sync(t, this.ref(resolve, "sync")))); } message(location: Ref, assertion: Assertion): void { this.enqueue(location.actor, t => location.target[message]?.(t, assertion)); } enqueue(actor: Actor, a: LocalAction): void { this.queues.has(actor) ? this.queues.get(actor)!.push(a) : this.queues.set(actor, [a]); } complete(): void { if (this.completed) throw new Error("Reuse of completed Turn!"); this.completed = true; this.queues.forEach((queue, actor) => actor.execute(() => queue.forEach(f => Turn.for(actor, f)))); if (this.localActions.length > 0) { queueMicrotask(() => this.localActions.forEach(f => Turn.for(this.actor, f))); } } } function extractFromMap(map: Map, keys?: IdentitySet): Map { const result: Map = 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); } }); } return result; }