UNPKG

@little-bonsai/ingrates

Version:
237 lines (208 loc) 5.42 kB
import { customAlphabet } from "nanoid"; import createDefaultRAMRealizer from "./defaultRAMRealizer.js"; export { createDefaultRAMRealizer }; export const makeAddress = customAlphabet( "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz", 14, ); function noop() {} async function promiseChain(ps) { for (const p of ps) { const x = await p(); if (x) { return x; } } } export function createActorSystem({ enhancers = [], realizers = [createDefaultRAMRealizer], transports = [], addressFn = makeAddress, onErr = console.error, } = {}) { let draining = {}; const knownActors = {}; const mountingActors = {}; const msgQueue = {}; const transporters = transports.map((x) => x(doDispatch, createActorSystem)); const realizerInstances = realizers.map((x) => x(createActorSystem)); function register(actorDefinition) { knownActors[actorDefinition.name] = actorDefinition; } function doSpawn(parent, nickname, { name }, ...args) { if (!knownActors[name]) { onErr("StartError", name, "unregistered actor"); return null; } const self = addressFn(); (msgQueue[parent] || []).unshift({ special: "ADD_CHILD", nickname, child: self, }); setTimeout(doDrain, 0, self); msgQueue[self] = [ { type: "Start", src: null }, { type: "Mount", src: null }, ]; mountingActors[self] = true; Promise.all( realizerInstances.map((realizer) => realizer.set( { children: {}, name, parent, nickname, self, args, state: undefined, }, knownActors, ), ), ) .then(() => { mountingActors[self] = false; setTimeout(doDrain, 0, self); }) .catch((e) => onErr("StartError", e, { self, name })); return self; } function doDispatch(src, snk, rawMessage) { const msg = Object.assign({ src }, rawMessage); if (msgQueue[snk]) { msgQueue[snk].push(msg); } else { // TODO test that Mount is being called when first dispatching to an actor that // existed in a persisted realizer msgQueue[snk] = [{ type: "Mount", src: null }, msg]; } setTimeout(doDrain, 0, snk); return snk; } async function doDrain(self) { if (draining[self] || msgQueue[self].length === 0 || mountingActors[self]) { return; } draining[self] = true; const msg = msgQueue[self].shift(); if (msg.special === "ADD_CHILD") { await promiseChain( realizerInstances.map((realizer) => () => realizer.get(self, knownActors)), ).then((bundle) => Promise.all( realizerInstances.map((realizer) => realizer.set( Object.assign({}, bundle, { children: Object.assign({}, bundle.children, { [msg.nickname]: msg.child, }), }), ), ), ), ); } else { await Promise.resolve( transporters.some((x) => x(self, msg)) || promiseChain( realizerInstances.map((realizer) => () => realizer.get(self, knownActors)), ).then((bundle) => bundle ? runActor(Object.assign({ msg }, bundle)) .then((output) => Object.assign({}, bundle, { state: output === undefined ? bundle.state : output, }), ) .then((bundle) => Promise.all( realizerInstances.map((realizer) => realizer.set(bundle), ), ), ) : null, ), ); } setTimeout(doDrain, 0, self); draining[self] = false; } function doKill(parent, self) { // TODO change the parent param to callerBundle. Detect if it's a valid call here realizerInstances.forEach((realizer) => realizer.kill({ self, parent }, knownActors)); } async function runActor(meta) { const { args, children, msg, name, parent, self, state } = meta; const provisions = getProvisionsForActor({ children, msg, name, parent, self, state, }); try { const newState = await knownActors[name](provisions, ...args); if (typeof newState === "function") { return newState(state); } else { return newState; } } catch (error) { onErr("RunError", error, { self, name, msg, state, parent }); const escalate = 1; const restart = 2; const resume = 3; const retry = 4; const stop = 5; const supervisionResponse = (knownActors[name].supervision || noop)(error, meta, { escalate, restart, resume, retry, stop, }); switch (supervisionResponse) { case escalate: doDispatch(self, parent, { error, msg }); //eslint-disable-next-line case stop: doKill(parent, self); break; default: //onErr("RunError (unhandled)", error, { self, name, msg, state, parent }); } } } function getProvisionsForActor(inputs) { const { self } = inputs; const kill = doKill.bind(null, self); const dispatch = doDispatch.bind(null, self); const spawn = new Proxy( function nakedSpawn() { return doSpawn(self, addressFn(), ...arguments); }, { get: (_, nickname, __) => doSpawn.bind(null, self, nickname), }, ); const baseProvisions = Object.assign(inputs, { dispatch, spawn, kill }); return Object.assign( inputs, enhancers.reduce((acc, val) => Object.assign(val(acc), acc), baseProvisions), ); } return getProvisionsForActor({ register, self: null, parent: null, mount: (addr) => { msgQueue[addr] = [{ type: "Mount", src: null }]; setTimeout(doDrain, 0, addr); }, }); }