2021-03-23 18:18:26 +00:00
|
|
|
import { IdentitySet, Value } from '@preserves/core';
|
2021-03-04 18:54:12 +00:00
|
|
|
import { Attenuation, runRewrites } from './rewrite.js';
|
2021-03-02 12:58:03 +00:00
|
|
|
import { queueTask } from './task.js';
|
2021-02-24 20:48:55 +00:00
|
|
|
|
|
|
|
//---------------------------------------------------------------------------
|
2021-02-17 19:57:15 +00:00
|
|
|
|
2021-03-02 08:50:23 +00:00
|
|
|
if ('stackTraceLimit' in Error) {
|
|
|
|
Error.stackTraceLimit = Infinity;
|
|
|
|
}
|
|
|
|
|
2021-02-23 13:35:23 +00:00
|
|
|
export type Assertion = Value<Ref>;
|
2021-02-22 18:37:47 +00:00
|
|
|
export type Handle = number;
|
2021-02-22 09:12:10 +00:00
|
|
|
export type ExitReason = null | { ok: true } | { ok: false, err: Error };
|
2021-02-23 14:41:54 +00:00
|
|
|
export type LocalAction = (t: Turn) => void;
|
2021-02-17 19:57:15 +00:00
|
|
|
|
2021-02-22 09:12:10 +00:00
|
|
|
export interface Entity {
|
2021-02-23 15:16:15 +00:00
|
|
|
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;
|
2021-02-17 19:57:15 +00:00
|
|
|
}
|
|
|
|
|
2021-02-24 20:48:55 +00:00
|
|
|
export interface Ref {
|
2021-04-16 18:29:16 +00:00
|
|
|
readonly relay: Facet;
|
2021-02-24 20:48:55 +00:00
|
|
|
readonly target: Partial<Entity>;
|
|
|
|
readonly attenuation?: Attenuation;
|
|
|
|
}
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
export function isRef(v: any): v is Ref {
|
2021-04-16 18:29:16 +00:00
|
|
|
return 'relay' in v && v.relay instanceof Facet && 'target' in v;
|
2021-02-24 20:48:55 +00:00
|
|
|
}
|
|
|
|
|
2021-03-23 11:18:57 +00:00
|
|
|
export function toRef(_v: any): Ref | undefined {
|
|
|
|
return isRef(_v) ? _v : void 0;
|
2021-03-12 19:49:18 +00:00
|
|
|
}
|
|
|
|
|
2021-04-16 18:29:16 +00:00
|
|
|
type OutboundAssertion = { handle: Handle, peer: Ref, established: boolean };
|
|
|
|
type OutboundMap = Map<Handle, OutboundAssertion>;
|
|
|
|
|
2021-03-02 08:50:23 +00:00
|
|
|
let nextActorId = 0;
|
2021-03-02 15:42:53 +00:00
|
|
|
export const __setNextActorId = (v: number) => nextActorId = v;
|
2021-03-02 08:50:23 +00:00
|
|
|
|
2021-02-22 18:51:19 +00:00
|
|
|
export class Actor {
|
2021-03-02 08:50:23 +00:00
|
|
|
readonly id = nextActorId++;
|
2021-04-16 18:29:16 +00:00
|
|
|
readonly root: Facet;
|
2021-02-17 19:57:15 +00:00
|
|
|
exitReason: ExitReason = null;
|
2021-03-03 15:21:47 +00:00
|
|
|
readonly exitHooks: Array<LocalAction> = [];
|
2021-02-17 19:57:15 +00:00
|
|
|
|
2021-04-16 18:29:16 +00:00
|
|
|
constructor(bootProc: LocalAction, initialAssertions: OutboundMap = new Map()) {
|
|
|
|
this.root = new Facet(this, null, initialAssertions);
|
|
|
|
Turn.for(new Facet(this, this.root), stopIfInertAfter(bootProc));
|
2021-02-17 19:57:15 +00:00
|
|
|
}
|
|
|
|
|
2021-03-03 15:21:47 +00:00
|
|
|
atExit(a: LocalAction): void {
|
|
|
|
this.exitHooks.push(a);
|
|
|
|
}
|
2021-03-02 08:50:23 +00:00
|
|
|
|
2021-02-22 18:37:47 +00:00
|
|
|
terminateWith(t: Turn, reason: Exclude<ExitReason, null>) {
|
2021-02-23 10:09:41 +00:00
|
|
|
if (this.exitReason !== null) return;
|
2021-02-22 21:30:36 +00:00
|
|
|
this.exitReason = reason;
|
2021-05-31 10:01:50 +00:00
|
|
|
if (!reason.ok) {
|
|
|
|
console.error(`Actor ${this.id} crashed:`, reason.err);
|
2021-03-02 08:50:23 +00:00
|
|
|
}
|
2021-03-03 15:21:47 +00:00
|
|
|
this.exitHooks.forEach(hook => hook(t));
|
2021-05-31 10:01:50 +00:00
|
|
|
queueTask(() => Turn.for(this.root, t => this.root._terminate(t, reason.ok), true));
|
2021-02-22 09:12:10 +00:00
|
|
|
}
|
2021-02-17 19:57:15 +00:00
|
|
|
}
|
|
|
|
|
2021-04-16 18:29:16 +00:00
|
|
|
export class Facet {
|
|
|
|
readonly id = nextActorId++;
|
|
|
|
readonly actor: Actor;
|
|
|
|
readonly parent: Facet | null;
|
|
|
|
readonly children = new Set<Facet>();
|
|
|
|
readonly outbound: OutboundMap;
|
|
|
|
readonly shutdownActions: Array<LocalAction> = [];
|
|
|
|
// ^ shutdownActions are not exitHooks - those run even on error. These are for clean shutdown
|
|
|
|
isLive = true;
|
|
|
|
inertCheckPreventers = 0;
|
|
|
|
|
|
|
|
constructor(actor: Actor, parent: Facet | null, initialAssertions: OutboundMap = new Map()) {
|
|
|
|
this.actor = actor;
|
|
|
|
this.parent = parent;
|
|
|
|
if (parent) parent.children.add(this);
|
|
|
|
this.outbound = initialAssertions;
|
|
|
|
}
|
|
|
|
|
|
|
|
onStop(a: LocalAction): void {
|
|
|
|
this.shutdownActions.push(a);
|
|
|
|
}
|
|
|
|
|
|
|
|
isInert(): boolean {
|
|
|
|
return this.children.size === 0 && this.outbound.size === 0 && this.inertCheckPreventers === 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
preventInertCheck(): () => void {
|
|
|
|
let armed = true;
|
|
|
|
this.inertCheckPreventers++;
|
|
|
|
return () => {
|
|
|
|
if (!armed) return;
|
|
|
|
armed = false;
|
|
|
|
this.inertCheckPreventers--;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
_terminate(t: Turn, orderly: boolean): void {
|
|
|
|
if (!this.isLive) return;
|
|
|
|
this.isLive = false;
|
|
|
|
|
|
|
|
const parent = this.parent;
|
|
|
|
if (parent) parent.children.delete(this);
|
|
|
|
|
|
|
|
t._inFacet(this, t => {
|
|
|
|
this.children.forEach(child => child._terminate(t, orderly));
|
|
|
|
if (orderly) this.shutdownActions.forEach(a => a(t));
|
|
|
|
this.outbound.forEach(e => t._retract(e));
|
|
|
|
|
|
|
|
if (orderly) {
|
|
|
|
queueTask(() => {
|
|
|
|
if (parent) {
|
|
|
|
if (parent.isInert()) {
|
|
|
|
Turn.for(parent, t => parent._terminate(t, true));
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
Turn.for(this.actor.root, t => this.actor.terminateWith(t, { ok: true }), true);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2021-02-17 19:57:15 +00:00
|
|
|
|
2021-03-02 08:50:23 +00:00
|
|
|
export function _sync_impl(turn: Turn, e: Partial<Entity>, peer: Ref): void {
|
2021-02-23 15:16:15 +00:00
|
|
|
e.sync ? e.sync!(turn, peer) : turn.message(peer, true);
|
|
|
|
}
|
|
|
|
|
2021-04-16 18:29:16 +00:00
|
|
|
let nextHandle = 0;
|
2021-03-02 08:50:23 +00:00
|
|
|
let nextTurnId = 0;
|
|
|
|
|
2021-02-22 09:12:10 +00:00
|
|
|
export class Turn {
|
2021-03-02 08:50:23 +00:00
|
|
|
readonly id = nextTurnId++;
|
2021-04-16 18:29:16 +00:00
|
|
|
readonly activeFacet: Facet;
|
|
|
|
queues: Map<Facet, LocalAction[]> | null;
|
2021-02-22 09:12:10 +00:00
|
|
|
|
2021-04-16 18:29:16 +00:00
|
|
|
static for(facet: Facet, f: LocalAction, zombieTurn = false): void {
|
|
|
|
if (!zombieTurn) {
|
|
|
|
if (facet.actor.exitReason !== null) return;
|
|
|
|
if (!facet.isLive) return;
|
|
|
|
}
|
|
|
|
const t = new Turn(facet);
|
2021-03-04 10:29:28 +00:00
|
|
|
try {
|
|
|
|
f(t);
|
2021-05-31 10:01:33 +00:00
|
|
|
t.queues!.forEach((q, facet) => queueTask(() => Turn.for(facet, t=> q.forEach(f => f(t)))));
|
2021-03-04 10:29:28 +00:00
|
|
|
t.queues = null;
|
|
|
|
} catch (err) {
|
2021-04-16 18:29:16 +00:00
|
|
|
Turn.for(facet.actor.root, t => facet.actor.terminateWith(t, { ok: false, err }));
|
2021-03-04 10:29:28 +00:00
|
|
|
}
|
2021-02-22 09:12:10 +00:00
|
|
|
}
|
|
|
|
|
2021-04-16 18:29:16 +00:00
|
|
|
private constructor(facet: Facet, queues = new Map<Facet, LocalAction[]>()) {
|
|
|
|
this.activeFacet = facet;
|
|
|
|
this.queues = queues;
|
|
|
|
}
|
|
|
|
|
|
|
|
_inFacet(facet: Facet, f: LocalAction): void {
|
|
|
|
const t = new Turn(facet, this.queues!);
|
|
|
|
f(t);
|
|
|
|
t.queues = null;
|
2021-02-17 19:57:15 +00:00
|
|
|
}
|
|
|
|
|
2021-03-02 08:50:23 +00:00
|
|
|
ref<T extends Partial<Entity>>(e: T): Ref {
|
2021-04-16 18:29:16 +00:00
|
|
|
return { relay: this.activeFacet, target: e };
|
|
|
|
}
|
|
|
|
|
|
|
|
facet(bootProc: LocalAction): Facet {
|
|
|
|
const newFacet = new Facet(this.activeFacet.actor, this.activeFacet);
|
|
|
|
this._inFacet(newFacet, stopIfInertAfter(bootProc));
|
|
|
|
return newFacet;
|
|
|
|
}
|
|
|
|
|
|
|
|
stop(facet: Facet = this.activeFacet, continuation?: LocalAction) {
|
|
|
|
this.enqueue(facet.parent!, t => {
|
|
|
|
facet._terminate(t, true);
|
|
|
|
if (continuation) continuation(t);
|
|
|
|
});
|
2021-02-22 18:37:47 +00:00
|
|
|
}
|
|
|
|
|
2021-02-23 10:17:38 +00:00
|
|
|
spawn(bootProc: LocalAction, initialAssertions = new IdentitySet<Handle>()): void {
|
2021-04-16 18:29:16 +00:00
|
|
|
this.enqueue(this.activeFacet, () => {
|
|
|
|
const newOutbound: OutboundMap = new Map();
|
2021-02-23 10:13:41 +00:00
|
|
|
initialAssertions.forEach(key => {
|
2021-04-16 18:29:16 +00:00
|
|
|
newOutbound.set(key, this.activeFacet.outbound.get(key)!); // we trust initialAssertions
|
|
|
|
this.activeFacet.outbound.delete(key);
|
2021-02-23 10:13:41 +00:00
|
|
|
});
|
2021-04-16 18:29:16 +00:00
|
|
|
queueTask(() => new Actor(bootProc, newOutbound));
|
2021-02-22 09:12:10 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-04-16 18:29:16 +00:00
|
|
|
stopActor(): void {
|
|
|
|
this.enqueue(this.activeFacet.actor.root, t => this.activeFacet.actor.terminateWith(t, { ok: true }));
|
2021-02-22 18:37:47 +00:00
|
|
|
}
|
|
|
|
|
2021-03-02 08:50:23 +00:00
|
|
|
crash(err: Error): void {
|
2021-04-16 18:29:16 +00:00
|
|
|
this.enqueue(this.activeFacet.actor.root, t => this.activeFacet.actor.terminateWith(t, { ok: false, err }));
|
2021-03-02 08:50:23 +00:00
|
|
|
}
|
|
|
|
|
2021-02-23 14:38:57 +00:00
|
|
|
assert(ref: Ref, assertion: Assertion): Handle {
|
2021-03-04 18:54:12 +00:00
|
|
|
const h = nextHandle++;
|
2021-03-03 10:45:01 +00:00
|
|
|
this._assert(ref, assertion, h);
|
|
|
|
return h;
|
|
|
|
}
|
|
|
|
|
|
|
|
_assert(ref: Ref, assertion: Assertion, h: Handle) {
|
2021-02-24 21:21:14 +00:00
|
|
|
const a = runRewrites(ref.attenuation, assertion);
|
|
|
|
if (a !== null) {
|
2021-04-16 18:29:16 +00:00
|
|
|
const e = { handle: h, peer: ref, established: false };
|
|
|
|
this.activeFacet.outbound.set(h, e);
|
2021-02-24 21:21:14 +00:00
|
|
|
this.enqueue(ref.relay, t => {
|
2021-04-16 18:29:16 +00:00
|
|
|
e.established = true;
|
2021-02-24 20:48:55 +00:00
|
|
|
ref.target.assert?.(t, a, h);
|
2021-02-24 21:21:14 +00:00
|
|
|
});
|
|
|
|
}
|
2021-02-22 09:12:10 +00:00
|
|
|
}
|
|
|
|
|
2021-03-03 10:48:07 +00:00
|
|
|
retract(h: Handle | undefined): void {
|
|
|
|
if (h !== void 0) {
|
2021-04-16 18:29:16 +00:00
|
|
|
const e = this.activeFacet.outbound.get(h);
|
|
|
|
if (e === void 0) return;
|
|
|
|
this._retract(e);
|
2021-03-03 10:48:07 +00:00
|
|
|
}
|
2021-02-22 09:12:10 +00:00
|
|
|
}
|
|
|
|
|
2021-04-16 12:38:08 +00:00
|
|
|
replace(ref: Ref, h: Handle | undefined, assertion: Assertion | undefined): Handle | undefined {
|
|
|
|
const newHandle = assertion === void 0 ? void 0 : this.assert(ref, assertion);
|
2021-03-03 10:48:07 +00:00
|
|
|
this.retract(h);
|
2021-02-22 18:37:47 +00:00
|
|
|
return newHandle;
|
|
|
|
}
|
|
|
|
|
2021-04-16 18:29:16 +00:00
|
|
|
_retract(e: OutboundAssertion): void {
|
|
|
|
this.activeFacet.outbound.delete(e.handle);
|
|
|
|
this.enqueue(e.peer.relay, t => {
|
|
|
|
if (e.established) {
|
|
|
|
e.established = false;
|
|
|
|
e.peer.target.retract?.(t, e.handle);
|
|
|
|
}
|
2021-02-22 09:12:10 +00:00
|
|
|
});
|
2021-02-17 19:57:15 +00:00
|
|
|
}
|
|
|
|
|
2021-02-23 14:38:57 +00:00
|
|
|
sync(ref: Ref): Promise<Turn> {
|
2021-03-02 08:50:23 +00:00
|
|
|
return new Promise(resolve => this._sync(ref, this.ref({ message: resolve })));
|
|
|
|
}
|
|
|
|
|
|
|
|
_sync(ref: Ref, peer: Ref): void {
|
|
|
|
this.enqueue(ref.relay, t => _sync_impl(t, ref.target, peer));
|
2021-02-22 09:12:10 +00:00
|
|
|
}
|
|
|
|
|
2021-02-23 14:38:57 +00:00
|
|
|
message(ref: Ref, assertion: Assertion): void {
|
2021-02-24 21:19:37 +00:00
|
|
|
const a = runRewrites(ref.attenuation, assertion);
|
2021-02-24 20:48:55 +00:00
|
|
|
if (a !== null) this.enqueue(ref.relay, t => ref.target.message?.(t, assertion));
|
2021-02-22 09:12:10 +00:00
|
|
|
}
|
|
|
|
|
2021-04-16 18:29:16 +00:00
|
|
|
enqueue(relay: Facet, a: LocalAction): void {
|
2021-03-02 08:50:23 +00:00
|
|
|
if (this.queues === null) {
|
|
|
|
throw new Error("Attempt to reuse a committed Turn");
|
|
|
|
}
|
2021-02-23 14:40:43 +00:00
|
|
|
this.queues.get(relay)?.push(a) ?? this.queues.set(relay, [a]);
|
2021-02-22 09:12:10 +00:00
|
|
|
}
|
2021-03-02 08:50:23 +00:00
|
|
|
|
|
|
|
freshen(a: LocalAction): void {
|
|
|
|
if (this.queues !== null) {
|
|
|
|
throw new Error("Attempt to freshen a non-stale Turn");
|
|
|
|
}
|
2021-04-16 18:29:16 +00:00
|
|
|
Turn.for(this.activeFacet, a);
|
2021-03-02 08:50:23 +00:00
|
|
|
}
|
2021-02-22 09:12:10 +00:00
|
|
|
}
|
2021-04-16 18:29:16 +00:00
|
|
|
|
|
|
|
function stopIfInertAfter(a: LocalAction): LocalAction {
|
|
|
|
return t => {
|
|
|
|
a(t);
|
|
|
|
t.enqueue(t.activeFacet, t => {
|
|
|
|
if ((t.activeFacet.parent && !t.activeFacet.parent.isLive) || t.activeFacet.isInert()) {
|
|
|
|
t.stop();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
}
|