From ad41407ac6a05a9a14d45059e3a6669d0950f5fa Mon Sep 17 00:00:00 2001 From: Brian Sakal Date: Sun, 14 May 2023 15:39:22 -0400 Subject: [PATCH] use eventQueue; process Reactions in proper order --- dist/tests/00-basic.js | 88 ++++++++++++++------------ dist/tests/01-ping-pong.js | 98 ++++++++++++++++------------- src/index.ts | 123 ++++++++++++++++++++++--------------- src/tests/00-basic.ts | 14 ++--- src/tests/01-ping-pong.ts | 25 ++++---- 5 files changed, 198 insertions(+), 150 deletions(-) diff --git a/dist/tests/00-basic.js b/dist/tests/00-basic.js index 71debd2..dd04130 100644 --- a/dist/tests/00-basic.js +++ b/dist/tests/00-basic.js @@ -3,62 +3,74 @@ var Machine = function(...states) { return { states }; }; - var State = function(name, ...ons) { - return { name, ons }; + var State = function(name, ...eventReactionCouplings) { + return { name, eventReactionCouplings }; }; var On = function(eventName, ...reactions) { return { eventName, reactions }; }; - var Do = function(fn) { - return { type: "Do", fn }; + var SideEffect = function(fn) { + return { type: "SideEffect", fn }; }; var Goto = function(targetStateName) { return { type: "Goto", targetStateName }; }; - var Tick = function(doFunctions) { - return { doFunctions }; - }; function interpret(machine2, options) { let { state, context } = options; if (typeof state === "undefined") { state = machine2.states[0].name; } - const interpreter = { machine: machine2, state, context, tickQueues: [] }; - console.log(interpreter); + const interpreter = { machine: machine2, state, context, eventQueue: [], isTransitioning: false }; send(interpreter, ["entry", null]); return interpreter; } function getState(interpreter) { return interpreter.machine.states.find((state) => state.name === interpreter.state); } - function getOns(state, event) { - return state.ons.filter((on) => on.eventName === event[0]); - } - function noop() { + function getMatchingEventReactionCouplings(state, event) { + return state.eventReactionCouplings.filter((eventReactionCoupling) => eventReactionCoupling.eventName === event[0]); } function send(interpreter, event) { - const state = getState(interpreter); - const onsTree = getOns(state, event); - const reactions = onsTree.map((on) => on.reactions).flat(); - const indexOfFirstGoto = reactions.findIndex((reaction) => reaction.type === "Goto"); - const indexOfFinalReaction = indexOfFirstGoto === -1 ? reactions.length - 1 : indexOfFirstGoto; - const reactionsUntilFirstGoto = reactions.slice(0, indexOfFinalReaction + 1); - const functionsToRunInTick = reactionsUntilFirstGoto.map((reaction) => { - if (reaction.type === "Do") { - return reaction.fn; + interpreter.eventQueue.push(event); + if (interpreter.isTransitioning === false) { + interpreter.isTransitioning = true; + while (interpreter.eventQueue.length > 0) { + processNextEvent(interpreter); + } + interpreter.isTransitioning = false; + } + } + function processNextEvent(interpreter) { + const nextEvent = interpreter.eventQueue.shift(); + if (typeof nextEvent !== "undefined") { + const state = getState(interpreter); + const eventReactionCouplings = getMatchingEventReactionCouplings(state, nextEvent); + const reactions = eventReactionCouplings.map((eventReactionCoupling) => eventReactionCoupling.reactions).flat(); + const { sideEffects, contextMutations, goto_ } = categorizeReactions(reactions); + sideEffects.forEach((sideEffect) => { + sideEffect.fn(interpreter.context, nextEvent, interpreter); + }); + contextMutations.forEach((contextMutation) => { + interpreter.context = contextMutation.fn(interpreter.context, nextEvent, interpreter); + }); + if (goto_ !== null) { + interpreter.state = goto_.targetStateName; + send(interpreter, ["entry", null]); + } + } + } + function categorizeReactions(reactions) { + let sideEffects = [], contextMutations = [], goto_ = null; + reactions.forEach((reaction) => { + if (reaction.type === "SideEffect") { + sideEffects.push(reaction); + } else if (reaction.type === "ContextMutation") { + contextMutations.push(reaction); } else if (reaction.type === "Goto") { - return (ctx, e, self) => { - self.state = reaction.targetStateName; - send(self, ["entry", e]); - }; - } else { - return noop; + goto_ = reaction; } }); - const tick = Tick(functionsToRunInTick); - tick.doFunctions.forEach((fn) => { - fn(interpreter.context, event, interpreter); - }); + return { sideEffects, contextMutations, goto_ }; } // src/tests/00-basic.ts @@ -75,8 +87,8 @@ "green", On( "entry", - Do(beginTimer), - Do(log) + SideEffect(beginTimer), + SideEffect(log) ), On( "timer-finished", @@ -87,8 +99,8 @@ "yellow", On( "entry", - Do(beginTimer), - Do(log) + SideEffect(beginTimer), + SideEffect(log) ), On( "timer-finished", @@ -99,8 +111,8 @@ "red", On( "entry", - Do(beginTimer), - Do(log) + SideEffect(beginTimer), + SideEffect(log) ), On( "timer-finished", diff --git a/dist/tests/01-ping-pong.js b/dist/tests/01-ping-pong.js index a6e88df..dda4210 100644 --- a/dist/tests/01-ping-pong.js +++ b/dist/tests/01-ping-pong.js @@ -3,62 +3,74 @@ var Machine = function(...states) { return { states }; }; - var State = function(name, ...ons) { - return { name, ons }; + var State = function(name, ...eventReactionCouplings) { + return { name, eventReactionCouplings }; }; var On = function(eventName, ...reactions) { return { eventName, reactions }; }; - var Do = function(fn) { - return { type: "Do", fn }; + var SideEffect = function(fn) { + return { type: "SideEffect", fn }; }; var Goto = function(targetStateName) { return { type: "Goto", targetStateName }; }; - var Tick = function(doFunctions) { - return { doFunctions }; - }; function interpret(machine, options) { let { state, context } = options; if (typeof state === "undefined") { state = machine.states[0].name; } - const interpreter = { machine, state, context, tickQueues: [] }; - console.log(interpreter); + const interpreter = { machine, state, context, eventQueue: [], isTransitioning: false }; send(interpreter, ["entry", null]); return interpreter; } function getState(interpreter) { return interpreter.machine.states.find((state) => state.name === interpreter.state); } - function getOns(state, event) { - return state.ons.filter((on) => on.eventName === event[0]); - } - function noop() { + function getMatchingEventReactionCouplings(state, event) { + return state.eventReactionCouplings.filter((eventReactionCoupling) => eventReactionCoupling.eventName === event[0]); } function send(interpreter, event) { - const state = getState(interpreter); - const onsTree = getOns(state, event); - const reactions = onsTree.map((on) => on.reactions).flat(); - const indexOfFirstGoto = reactions.findIndex((reaction) => reaction.type === "Goto"); - const indexOfFinalReaction = indexOfFirstGoto === -1 ? reactions.length - 1 : indexOfFirstGoto; - const reactionsUntilFirstGoto = reactions.slice(0, indexOfFinalReaction + 1); - const functionsToRunInTick = reactionsUntilFirstGoto.map((reaction) => { - if (reaction.type === "Do") { - return reaction.fn; + interpreter.eventQueue.push(event); + if (interpreter.isTransitioning === false) { + interpreter.isTransitioning = true; + while (interpreter.eventQueue.length > 0) { + processNextEvent(interpreter); + } + interpreter.isTransitioning = false; + } + } + function processNextEvent(interpreter) { + const nextEvent = interpreter.eventQueue.shift(); + if (typeof nextEvent !== "undefined") { + const state = getState(interpreter); + const eventReactionCouplings = getMatchingEventReactionCouplings(state, nextEvent); + const reactions = eventReactionCouplings.map((eventReactionCoupling) => eventReactionCoupling.reactions).flat(); + const { sideEffects, contextMutations, goto_ } = categorizeReactions(reactions); + sideEffects.forEach((sideEffect) => { + sideEffect.fn(interpreter.context, nextEvent, interpreter); + }); + contextMutations.forEach((contextMutation) => { + interpreter.context = contextMutation.fn(interpreter.context, nextEvent, interpreter); + }); + if (goto_ !== null) { + interpreter.state = goto_.targetStateName; + send(interpreter, ["entry", null]); + } + } + } + function categorizeReactions(reactions) { + let sideEffects = [], contextMutations = [], goto_ = null; + reactions.forEach((reaction) => { + if (reaction.type === "SideEffect") { + sideEffects.push(reaction); + } else if (reaction.type === "ContextMutation") { + contextMutations.push(reaction); } else if (reaction.type === "Goto") { - return (ctx, e, self) => { - self.state = reaction.targetStateName; - send(self, ["entry", e]); - }; - } else { - return noop; + goto_ = reaction; } }); - const tick = Tick(functionsToRunInTick); - tick.doFunctions.forEach((fn) => { - fn(interpreter.context, event, interpreter); - }); + return { sideEffects, contextMutations, goto_ }; } // src/tests/01-ping-pong.ts @@ -85,11 +97,11 @@ "idle", On( "entry", - Do(log) + SideEffect(log) ), On( "server-created", - Do((_ctx, [_eventName, serverActor2], self) => { + SideEffect((_ctx, [_eventName, serverActor2], self) => { self.context.serverActor = serverActor2; }), Goto("making-request") @@ -99,8 +111,8 @@ "making-request", On( "entry", - Do(log), - Do(makeRequest), + SideEffect(log), + SideEffect(makeRequest), Goto("awaiting-response") ) ), @@ -108,11 +120,11 @@ "awaiting-response", On( "entry", - Do(log) + SideEffect(log) ), On( "received-response", - Do(log), + SideEffect(log), Goto("making-request") ) ) @@ -122,11 +134,11 @@ "awaiting-request", On( "entry", - Do(log) + SideEffect(log) ), On( "received-request", - Do((_ctx, [_eventName, clientActor2], self) => { + SideEffect((_ctx, [_eventName, clientActor2], self) => { self.context.clientActor = clientActor2; }), Goto("sending-response") @@ -136,12 +148,12 @@ "sending-response", On( "entry", - Do(log), - Do(startTimer) + SideEffect(log), + SideEffect(startTimer) ), On( "timer-finished", - Do(sendResponse), + SideEffect(sendResponse), Goto("awaiting-request") ) ) diff --git a/src/index.ts b/src/index.ts index de11d98..95dfe1c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,47 +4,50 @@ export interface Machine_T { } export interface State_T { name: string; - ons: Array; + eventReactionCouplings: Array; } -export interface On_T { +export interface EventReactionCouplings_T { eventName: string; - reactions: Array; + reactions: Array; }; -export interface Do_T { - type: 'Do'; - fn: DoFn_T; +export type Reaction_T = SideEffect_T | ContextMutation_T | Goto_T; +export interface SideEffect_T { + type: 'SideEffect'; + fn: SideEffectFunction_T; }; -export type DoFn_T = (ctx:any,e:Event_T,self:Interpreter_T)=>void; +export type SideEffectFunction_T = (ctx:any,e:Event_T,self:Interpreter_T)=>void; +export interface ContextMutation_T { + type: 'ContextMutation'; + fn: ContextMutationFunction_T; +}; +export type ContextMutationFunction_T = (ctx:any,e:Event_T,self:Interpreter_T)=>any; export interface Goto_T { type: 'Goto'; targetStateName: string; }; export const Machine = function(...states:Array) : Machine_T { return {states}; }; -export const State = function(name:string, ...ons:Array) : State_T{ return {name, ons}; }; -export const On = function(eventName:string, ...reactions:Array) : On_T{ return {eventName, reactions}; }; -export const Do = function(fn:DoFn_T) : Do_T{ return {type:'Do', fn}; }; +export const State = function(name:string, ...eventReactionCouplings:Array) : State_T{ return {name, eventReactionCouplings}; }; +export const On = function(eventName:string, ...reactions:Array) : EventReactionCouplings_T{ return {eventName, reactions}; }; +export const SideEffect = function(fn:SideEffectFunction_T) : SideEffect_T{ return {type:'SideEffect', fn}; }; export const Goto = function(targetStateName:string) : Goto_T { return {type:'Goto', targetStateName} }; +export const Context = function(fn:ContextMutationFunction_T) : ContextMutation_T { return {type:'ContextMutation', fn} }; + + -interface Tick_T { - doFunctions: Array -}; -const Tick = function(doFunctions : Array) : Tick_T{ return {doFunctions}; }; export interface Interpreter_T { machine: Machine_T; state: string; context: any; - tickQueues:Array + eventQueue:Array; + isTransitioning: boolean; } -type TickQueue_T = Array; export function interpret(machine:Machine_T, options:{state?:string, context:any}) : Interpreter_T{ let {state, context} = options; if(typeof state === 'undefined'){ state = machine.states[0].name; } - const interpreter = {machine, state, context, tickQueues:[]} - console.log(interpreter); - //@ts-ignore + const interpreter = {machine, state, context, eventQueue:[], isTransitioning:false} send(interpreter, ['entry', null] ); return interpreter; } @@ -56,12 +59,9 @@ function getState(interpreter : Interpreter_T) : State_T{ } /** Helper function for `send()` */ -function getOns(state : State_T, event:Event_T) : Array{ - return state.ons.filter((on)=>on.eventName===event[0]); +function getMatchingEventReactionCouplings(state : State_T, event:Event_T) : Array{ + return state.eventReactionCouplings.filter((eventReactionCoupling)=>eventReactionCoupling.eventName===event[0]); } -/** Helper function for `send()` - */ -function noop(){} /** Inject an Event into the Interpreter's "tick queue". * * An event can be signify something "new" happening, such that its reactions should run on the next Tick; @@ -73,32 +73,57 @@ function noop(){} * Tick, it is discarded. */ export function send(interpreter : Interpreter_T, event:Event_T){ - const state = getState(interpreter); - const onsTree = getOns(state, event); - const reactions = onsTree - .map((on)=>on.reactions) - .flat(); - const indexOfFirstGoto = reactions.findIndex((reaction)=>reaction.type==='Goto'); - const indexOfFinalReaction = indexOfFirstGoto === -1 ? reactions.length-1 : indexOfFirstGoto; - const reactionsUntilFirstGoto = reactions.slice(0, indexOfFinalReaction+1); - const functionsToRunInTick = reactionsUntilFirstGoto - .map((reaction)=>{ - if(reaction.type === 'Do'){ - return reaction.fn; - } - else if(reaction.type === 'Goto'){ - return (ctx:any,e:Event_T,self:Interpreter_T)=>{ - self.state = reaction.targetStateName; - //@ts-ignore - send(self, ['entry', e] ); - }; - } - else{ - return noop; - } + interpreter.eventQueue.push(event); + if(interpreter.isTransitioning === false){ + interpreter.isTransitioning = true; + while(interpreter.eventQueue.length > 0){ + processNextEvent(interpreter); + } + interpreter.isTransitioning = false; + } +} +export const enqueue = send; +function processNextEvent(interpreter:Interpreter_T){ + const nextEvent = interpreter.eventQueue.shift(); + if(typeof nextEvent !== 'undefined'){ + const state = getState(interpreter); + const eventReactionCouplings = getMatchingEventReactionCouplings(state, nextEvent); + const reactions = eventReactionCouplings + .map((eventReactionCoupling)=>eventReactionCoupling.reactions) + .flat(); + const {sideEffects, contextMutations, goto_} = categorizeReactions(reactions); + // can process sideEffects in parallel: + sideEffects.forEach((sideEffect)=>{ + sideEffect.fn(interpreter.context, nextEvent, interpreter); }); - const tick = Tick(functionsToRunInTick); - tick.doFunctions.forEach((fn)=>{ fn(interpreter.context, event, interpreter); }); + // must process contextMutations in-series: + contextMutations.forEach((contextMutation)=>{ + interpreter.context = contextMutation.fn(interpreter.context, nextEvent, interpreter); + }); + // processing of `goto` must be last: + if(goto_ !== null){ + interpreter.state = goto_.targetStateName; + send(interpreter, ['entry', null]); + } + } +} +function categorizeReactions(reactions:Array) : {sideEffects:Array, contextMutations:Array, goto_:Goto_T|null}{ + let + sideEffects:Array = [], + contextMutations:Array = [], + goto_:Goto_T|null = null; + reactions.forEach((reaction)=>{ + if(reaction.type === 'SideEffect'){ + sideEffects.push(reaction); + } + else if(reaction.type === 'ContextMutation'){ + contextMutations.push(reaction); + } + else if(reaction.type === 'Goto'){ + goto_ = reaction; + } + }); + return {sideEffects, contextMutations, goto_}; } export const Spawn = function(){}; diff --git a/src/tests/00-basic.ts b/src/tests/00-basic.ts index 44def95..e147aad 100644 --- a/src/tests/00-basic.ts +++ b/src/tests/00-basic.ts @@ -1,4 +1,4 @@ -import { Machine, State, On, Do, Goto, Spawn, Unspawn, interpret, Interpreter_T, send, Event_T } from '../index'; +import { Machine, State, On, SideEffect, Goto, Spawn, Unspawn, interpret, Interpreter_T, send, Event_T } from '../index'; const beginTimer = (ctx:C, e:Event_T, self:Interpreter_T)=>{ setTimeout(()=>{ send(self, ['timer-finished',null]); }, 800); }; const log = (ctx:C, e:Event_T, self:Interpreter_T)=>{ console.log(self.state); }; @@ -16,8 +16,8 @@ const machine = Machine( State('green', On('entry', - Do(beginTimer), - Do(log) + SideEffect(beginTimer), + SideEffect(log) ), On('timer-finished', Goto('yellow') @@ -25,8 +25,8 @@ const machine = ), State('yellow', On('entry', - Do(beginTimer), - Do(log) + SideEffect(beginTimer), + SideEffect(log) ), On('timer-finished', Goto('red') @@ -34,8 +34,8 @@ const machine = ), State('red', On('entry', - Do(beginTimer), - Do(log) + SideEffect(beginTimer), + SideEffect(log) ), On('timer-finished', Goto('green') diff --git a/src/tests/01-ping-pong.ts b/src/tests/01-ping-pong.ts index 091eca6..2c7b5a1 100644 --- a/src/tests/01-ping-pong.ts +++ b/src/tests/01-ping-pong.ts @@ -1,4 +1,4 @@ -import { Machine, State, On, Do, Goto, Spawn, Unspawn, interpret, Interpreter_T, send, Event_T } from '../index'; +import { Machine, State, On, SideEffect, Goto, Spawn, Unspawn, interpret, Interpreter_T, send, Event_T } from '../index'; const wait = (ms:number)=>new Promise((resolve)=>{ setTimeout(()=>{ resolve(1); }, ms); }); const makeRequest = (ctx,e,self)=>{ send(ctx.serverActor, ['received-request',self]); }; @@ -21,27 +21,26 @@ const client = Machine( State('idle', On('entry', - Do(log), + SideEffect(log), ), On('server-created', - - Do((_ctx,[_eventName,serverActor],self)=>{ self.context.serverActor=serverActor; }), + SideEffect((_ctx,[_eventName,serverActor],self)=>{ self.context.serverActor=serverActor; }), Goto('making-request') ) ), State('making-request', On('entry', - Do(log), - Do(makeRequest), + SideEffect(log), + SideEffect(makeRequest), Goto('awaiting-response') ), ), State('awaiting-response', On('entry', - Do(log), + SideEffect(log), ), On('received-response', - Do(log), + SideEffect(log), Goto('making-request') ), ), @@ -60,20 +59,20 @@ const server = Machine( State('awaiting-request', On('entry', - Do(log), + SideEffect(log), ), On('received-request', - Do((_ctx,[_eventName,clientActor],self)=>{ self.context.clientActor=clientActor; }), + SideEffect((_ctx,[_eventName,clientActor],self)=>{ self.context.clientActor=clientActor; }), Goto('sending-response') ), ), State('sending-response', On('entry', - Do(log), - Do(startTimer) + SideEffect(log), + SideEffect(startTimer) ), On('timer-finished', - Do(sendResponse), + SideEffect(sendResponse), Goto('awaiting-request') ) ),