use eventQueue; process Reactions in proper order

This commit is contained in:
Brian Sakal
2023-05-14 15:39:22 -04:00
parent 26383090ba
commit ad41407ac6
5 changed files with 198 additions and 150 deletions
+74 -49
View File
@@ -4,47 +4,50 @@ export interface Machine_T {
}
export interface State_T {
name: string;
ons: Array<On_T>;
eventReactionCouplings: Array<EventReactionCouplings_T>;
}
export interface On_T {
export interface EventReactionCouplings_T {
eventName: string;
reactions: Array<Do_T | Goto_T>;
reactions: Array<Reaction_T>;
};
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<State_T>) : Machine_T { return {states}; };
export const State = function(name:string, ...ons:Array<On_T>) : State_T{ return {name, ons}; };
export const On = function(eventName:string, ...reactions:Array<Do_T | Goto_T>) : On_T{ return {eventName, reactions}; };
export const Do = function<S,E extends Event_T, C>(fn:DoFn_T) : Do_T{ return {type:'Do', fn}; };
export const State = function(name:string, ...eventReactionCouplings:Array<EventReactionCouplings_T>) : State_T{ return {name, eventReactionCouplings}; };
export const On = function(eventName:string, ...reactions:Array<Reaction_T>) : 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<DoFn_T>
};
const Tick = function(doFunctions : Array<DoFn_T>) : Tick_T{ return {doFunctions}; };
export interface Interpreter_T {
machine: Machine_T;
state: string;
context: any;
tickQueues:Array<TickQueue_T>
eventQueue:Array<Event_T>;
isTransitioning: boolean;
}
type TickQueue_T = Array<Tick_T>;
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<On_T>{
return state.ons.filter((on)=>on.eventName===event[0]);
function getMatchingEventReactionCouplings(state : State_T, event:Event_T) : Array<EventReactionCouplings_T>{
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<Reaction_T>) : {sideEffects:Array<SideEffect_T>, contextMutations:Array<ContextMutation_T>, goto_:Goto_T|null}{
let
sideEffects:Array<SideEffect_T> = [],
contextMutations:Array<ContextMutation_T> = [],
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(){};
+7 -7
View File
@@ -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')
+12 -13
View File
@@ -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')
)
),