import { IdentitySet, Value } from 'preserves'; export type Assertion = Value; export type Handle = number; export type ExitReason = null | { ok: true } | { ok: false, err: Error }; export type LocalAction = (t: Turn) => void; export interface Entity { assert(turn: Turn, assertion: Assertion, handle: Handle): void; retract(turn: Turn, handle: Handle): void; message(turn: Turn, body: Assertion): void; sync(turn: Turn, peer: Ref): void; } export interface Ref { readonly relay: Actor, readonly target: Partial }; export class Actor { readonly outbound: Map; exitReason: ExitReason = null; constructor(initialAssertions = new Map()) { this.outbound = initialAssertions; } terminateWith(t: Turn, reason: Exclude) { if (this.exitReason !== null) return; this.exitReason = reason; this.outbound.forEach((peer, h) => t._retract(peer, h)); } execute(proc: () => void): void { queueMicrotask(() => { if (this.exitReason !== null) return; try { proc(); } catch (err) { console.error(Actor, err); Turn.for(this, t => this.terminateWith(t, { ok: false, err })); } }); } } let nextHandle = 0; export function _sync(turn: Turn, e: Partial, peer: Ref): void { e.sync ? e.sync!(turn, peer) : turn.message(peer, true); } export class Turn { readonly actor: Actor; readonly queues = new Map(); static for(actor: Actor, f: LocalAction): void { const t = new Turn(actor); f(t); t.queues.forEach((q, a) => a.execute(() => q.forEach(f => Turn.for(a, f)))); } private constructor(actor: Actor) { this.actor = actor; } ref(e: Partial): Ref { return { relay: this.actor, target: e }; } spawn(bootProc: LocalAction, initialAssertions = new IdentitySet()): void { this.enqueue(this.actor, () => { const newOutbound = new Map(); initialAssertions.forEach(key => { newOutbound.set(key, this.actor.outbound.get(key)!); // we trust initialAssertions this.actor.outbound.delete(key); }); const child = new Actor(newOutbound); child.execute(() => Turn.for(child, bootProc)); }); } quit(): void { this.enqueue(this.actor, t => this.actor.terminateWith(t, { ok: true })); } assert(ref: Ref, assertion: Assertion): Handle { const h = nextHandle++; this.enqueue(ref.relay, t => { this.actor.outbound.set(h, ref); ref.target.assert?.(t, assertion, h); }); return h; } retract(h: Handle): void { this._retract(this.actor.outbound.get(h)!, h); } replace(ref: Ref, h: Handle | undefined, assertion: Assertion): Handle { const newHandle = this.assert(ref, assertion); if (h !== void 0) this.retract(h); return newHandle; } _retract(ref: Ref, handle: Handle): void { this.enqueue(ref.relay, t => { this.actor.outbound.delete(handle); ref.target.retract?.(t, handle); }); } sync(ref: Ref): Promise { return new Promise(resolve => this.enqueue(ref.relay, t => _sync(t, ref.target, this.ref({ message: resolve })))); } message(ref: Ref, assertion: Assertion): void { this.enqueue(ref.relay, t => ref.target.message?.(t, assertion)); } enqueue(relay: Actor, a: LocalAction): void { this.queues.get(relay)?.push(a) ?? this.queues.set(relay, [a]); } } export type AttenuationFilter = (assertion: Assertion) => Assertion | null; export class Attenuation implements Entity { readonly target: Partial; readonly filter: AttenuationFilter; constructor(target: Partial, filter: AttenuationFilter) { this.target = target; this.filter = filter; } assert(turn: Turn, assertion: Assertion, handle: Handle): void { const filtered = this.filter(assertion); if (filtered !== null) this.target.assert?.(turn, filtered, handle); } retract(turn: Turn, handle: Handle): void { // TODO: consider whether we want targets to even see blocked handles this.target.retract?.(turn, handle); } message(turn: Turn, body: Assertion): void { const filtered = this.filter(body); if (filtered !== null) this.target.message?.(turn, filtered); } sync(turn: Turn, peer: Ref): void { _sync(turn, this.target, peer); } } export function attenuate(ref: Ref, filter: AttenuationFilter): Ref { return { relay: ref.relay, target: new Attenuation(ref.target, filter) }; }