(() => { // src/index.ts var Machine = function(...states) { return { states }; }; var State = function(name, ...eventReactionCouplings) { return { name, eventReactionCouplings }; }; var On = function(eventName, ...reactions) { return { eventName, reactions }; }; var SideEffect = function(fn) { return { type: "SideEffect", fn }; }; var Goto = function(targetStateName) { return { type: "Goto", targetStateName }; }; var Context = function(fn) { return { type: "ContextMutation", fn }; }; var Peer = function(name, peerCreationFunction) { return { type: "Peering", name, peerCreationFunction }; }; function Interpreter(machine, initialContext, initialStateName) { if (typeof initialStateName === "undefined") { initialStateName = machine.states[0].name; } const interpreter = { machine, state: initialStateName, context: initialContext, eventQueue: [], isTransitioning: false, peers: {}, peerSubscriptionIds: /* @__PURE__ */ new Map(), subscriptionsToEvents: {}, subscriptionsToState: {}, subscriptionsToSettledState: {}, isPaused: true }; interpreter.start = () => { start(interpreter); return interpreter; }; send(interpreter, ["entry", null]); return interpreter; } function start(interpreter) { if (interpreter.isPaused === true) { interpreter.isPaused = false; processEvents(interpreter); } } function getState(interpreter) { return interpreter.machine.states.find((state) => state.name === interpreter.state); } function getMatchingEventReactionCouplings(state, event) { return state.eventReactionCouplings.filter((eventReactionCoupling) => eventReactionCoupling.eventName === event[0]); } function send(interpreter, event) { interpreter.eventQueue.push(event); if (interpreter.isTransitioning === false) { processEvents(interpreter); } } function processEvents(interpreter) { interpreter.isTransitioning = true; while (interpreter.eventQueue.length > 0 && interpreter.isPaused === false) { processNextEvent(interpreter); } interpreter.isTransitioning = false; Object.values(interpreter.subscriptionsToSettledState).forEach((callbackFunction) => { callbackFunction(interpreter); }); } function processNextEvent(interpreter) { const event = interpreter.eventQueue.shift(); if (typeof event !== "undefined") { const state = getState(interpreter); const eventReactionCouplings = getMatchingEventReactionCouplings(state, event); const reactions = eventReactionCouplings.map((eventReactionCoupling) => eventReactionCoupling.reactions).flat(); const { sideEffects, contextMutations, peerings, goto_ } = categorizeReactions(reactions); const originalContext = interpreter.context; contextMutations.forEach((contextMutation) => { interpreter.context = contextMutation.fn(interpreter.context, event, interpreter); }); if (goto_ !== null) { send(interpreter, ["exit", null]); interpreter.state = goto_.targetStateName; Object.values(interpreter.subscriptionsToState).forEach((callbackFunction) => { callbackFunction(event, interpreter); }); send(interpreter, ["entry", null]); } peerings.forEach((peering) => { addPeer(interpreter, peering.name, peering.peerCreationFunction(interpreter.context, event, interpreter)); }); Object.values(interpreter.subscriptionsToEvents).forEach((callbackFunction) => { callbackFunction(event, interpreter); }); sideEffects.forEach((sideEffect) => { sideEffect.fn(interpreter.context, event, interpreter, originalContext); }); } } function categorizeReactions(reactions) { let sideEffects = [], contextMutations = [], peerings = [], goto_ = null; reactions.forEach((reaction) => { if (reaction.type === "SideEffect") { sideEffects.push(reaction); } else if (reaction.type === "ContextMutation") { contextMutations.push(reaction); } else if (reaction.type === "Peering") { peerings.push(reaction); } else if (reaction.type === "Goto") { goto_ = reaction; } }); return { sideEffects, contextMutations, peerings, goto_ }; } var subscriptionId = 0; function subscribeToEvents(interpreter, callback) { subscriptionId++; interpreter.subscriptionsToEvents[subscriptionId.toString()] = callback; return subscriptionId.toString(); } function addPeer(self, name, peer) { self.peers[name] = peer; subscribeToEvents(peer, (e, peer2) => { if (self.isTransitioning === false) { send(self, [name + "." + e[0], e[1]]); } }); } // src/tests/01-ping-pong.ts var wait = (ms) => new Promise((resolve) => { setTimeout(() => { resolve(1); }, ms); }); var makeRequest = (ctx, e, self) => { send(self.peers.server, ["received-request", self]); }; var sendResponse = (ctx, e, self) => { send(ctx.client, ["received-response", self]); }; var startTimer = async (ctx, e, self) => { await wait(1500); console.log(" timer actually finished"); send(self, ["timer-finished", null]); }; var logServerStats = (ctx, e, self) => { console.log("server", ctx.requestsReceived, ctx.responsesSent); }; var saveClient = (ctx, e, self) => ({ ...ctx, client: e[1] }); var createServer = (ctx, e, self) => Interpreter(server, { requestsReceived: 0, responsesSent: 0 }).start(); var client = Machine( State( "initializing", On( "entry", Peer("server", createServer), //SideEffect(log('client')), Goto("idle") ) ), State( "idle", On( "entry", //SideEffect(log('client')), Goto("making-request") ) ), State( "making-request", On( "entry", //SideEffect(log('client')), SideEffect(makeRequest), Context((ctx) => ({ ...ctx, requestsMade: ctx.requestsMade + 1 })), Goto("awaiting-response") ) ), State( "awaiting-response", On( "entry" //SideEffect(log('client')), ), On( "received-response", Context((ctx) => ({ ...ctx, responsesReceived: ctx.responsesReceived + 1 })), //SideEffect(log('client')), Goto("making-request") ) ) ); var server = Machine( State( "awaiting-request", On( "entry", //SideEffect(log('server')), Context((ctx) => ({ ...ctx, requestsReceived: ctx.requestsReceived + 1 })) ), On( "received-request", //SideEffect(log('server')), Context(saveClient), Goto("sending-response") ) ), State( "sending-response", On( "entry", //SideEffect(log('server')), SideEffect(startTimer) ), On( "timer-finished", //SideEffect(log('server')), SideEffect(logServerStats), SideEffect(sendResponse), Context((ctx) => ({ ...ctx, responsesSent: ctx.responsesSent + 1 })), Goto("awaiting-request") // for some reason, at this point there's a "received-request" waiting in the eventQueue, which gets processed before the "exit" then "entry" that get appended to the queue due to this Goto, which makes the Interpreter come right back to this State /* Server gets timer-finished, which sends response to client. But client, at the time, is not transitioning, so it immediately begins processing that event. The problem is that one of the sideeffects involved in processing that event is to send another request to the server, which hasn't yet even queued `exit`-then-`entry` events for its next state! So we have to ensure they get queued first, before processing the client. */ ) ) ); var clientActor = Interpreter(client, { requestsMade: 0, responsesReceived: 0 }).start(); })();