|
|
|
@ -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){
|
|
|
|
|
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 onsTree = getOns(state, event);
|
|
|
|
|
const reactions = onsTree
|
|
|
|
|
.map((on)=>on.reactions)
|
|
|
|
|
const eventReactionCouplings = getMatchingEventReactionCouplings(state, nextEvent);
|
|
|
|
|
const reactions = eventReactionCouplings
|
|
|
|
|
.map((eventReactionCoupling)=>eventReactionCoupling.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;
|
|
|
|
|
const {sideEffects, contextMutations, goto_} = categorizeReactions(reactions);
|
|
|
|
|
// can process sideEffects in parallel:
|
|
|
|
|
sideEffects.forEach((sideEffect)=>{
|
|
|
|
|
sideEffect.fn(interpreter.context, nextEvent, 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]);
|
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
const tick = Tick(functionsToRunInTick);
|
|
|
|
|
tick.doFunctions.forEach((fn)=>{ fn(interpreter.context, event, interpreter); });
|
|
|
|
|
return {sideEffects, contextMutations, goto_};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const Spawn = function(){};
|
|
|
|
|