use eventQueue; process Reactions in proper order

main
Brian Sakal 2 years ago
parent 26383090ba
commit ad41407ac6

@ -3,62 +3,74 @@
var Machine = function(...states) { var Machine = function(...states) {
return { states }; return { states };
}; };
var State = function(name, ...ons) { var State = function(name, ...eventReactionCouplings) {
return { name, ons }; return { name, eventReactionCouplings };
}; };
var On = function(eventName, ...reactions) { var On = function(eventName, ...reactions) {
return { eventName, reactions }; return { eventName, reactions };
}; };
var Do = function(fn) { var SideEffect = function(fn) {
return { type: "Do", fn }; return { type: "SideEffect", fn };
}; };
var Goto = function(targetStateName) { var Goto = function(targetStateName) {
return { type: "Goto", targetStateName }; return { type: "Goto", targetStateName };
}; };
var Tick = function(doFunctions) {
return { doFunctions };
};
function interpret(machine2, options) { function interpret(machine2, options) {
let { state, context } = options; let { state, context } = options;
if (typeof state === "undefined") { if (typeof state === "undefined") {
state = machine2.states[0].name; state = machine2.states[0].name;
} }
const interpreter = { machine: machine2, state, context, tickQueues: [] }; const interpreter = { machine: machine2, state, context, eventQueue: [], isTransitioning: false };
console.log(interpreter);
send(interpreter, ["entry", null]); send(interpreter, ["entry", null]);
return interpreter; return interpreter;
} }
function getState(interpreter) { function getState(interpreter) {
return interpreter.machine.states.find((state) => state.name === interpreter.state); return interpreter.machine.states.find((state) => state.name === interpreter.state);
} }
function getOns(state, event) { function getMatchingEventReactionCouplings(state, event) {
return state.ons.filter((on) => on.eventName === event[0]); return state.eventReactionCouplings.filter((eventReactionCoupling) => eventReactionCoupling.eventName === event[0]);
}
function noop() {
} }
function send(interpreter, event) { function send(interpreter, event) {
const state = getState(interpreter); interpreter.eventQueue.push(event);
const onsTree = getOns(state, event); if (interpreter.isTransitioning === false) {
const reactions = onsTree.map((on) => on.reactions).flat(); interpreter.isTransitioning = true;
const indexOfFirstGoto = reactions.findIndex((reaction) => reaction.type === "Goto"); while (interpreter.eventQueue.length > 0) {
const indexOfFinalReaction = indexOfFirstGoto === -1 ? reactions.length - 1 : indexOfFirstGoto; processNextEvent(interpreter);
const reactionsUntilFirstGoto = reactions.slice(0, indexOfFinalReaction + 1); }
const functionsToRunInTick = reactionsUntilFirstGoto.map((reaction) => { interpreter.isTransitioning = false;
if (reaction.type === "Do") { }
return reaction.fn; }
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") { } else if (reaction.type === "Goto") {
return (ctx, e, self) => { goto_ = reaction;
self.state = reaction.targetStateName;
send(self, ["entry", e]);
};
} else {
return noop;
} }
}); });
const tick = Tick(functionsToRunInTick); return { sideEffects, contextMutations, goto_ };
tick.doFunctions.forEach((fn) => {
fn(interpreter.context, event, interpreter);
});
} }
// src/tests/00-basic.ts // src/tests/00-basic.ts
@ -75,8 +87,8 @@
"green", "green",
On( On(
"entry", "entry",
Do(beginTimer), SideEffect(beginTimer),
Do(log) SideEffect(log)
), ),
On( On(
"timer-finished", "timer-finished",
@ -87,8 +99,8 @@
"yellow", "yellow",
On( On(
"entry", "entry",
Do(beginTimer), SideEffect(beginTimer),
Do(log) SideEffect(log)
), ),
On( On(
"timer-finished", "timer-finished",
@ -99,8 +111,8 @@
"red", "red",
On( On(
"entry", "entry",
Do(beginTimer), SideEffect(beginTimer),
Do(log) SideEffect(log)
), ),
On( On(
"timer-finished", "timer-finished",

@ -3,62 +3,74 @@
var Machine = function(...states) { var Machine = function(...states) {
return { states }; return { states };
}; };
var State = function(name, ...ons) { var State = function(name, ...eventReactionCouplings) {
return { name, ons }; return { name, eventReactionCouplings };
}; };
var On = function(eventName, ...reactions) { var On = function(eventName, ...reactions) {
return { eventName, reactions }; return { eventName, reactions };
}; };
var Do = function(fn) { var SideEffect = function(fn) {
return { type: "Do", fn }; return { type: "SideEffect", fn };
}; };
var Goto = function(targetStateName) { var Goto = function(targetStateName) {
return { type: "Goto", targetStateName }; return { type: "Goto", targetStateName };
}; };
var Tick = function(doFunctions) {
return { doFunctions };
};
function interpret(machine, options) { function interpret(machine, options) {
let { state, context } = options; let { state, context } = options;
if (typeof state === "undefined") { if (typeof state === "undefined") {
state = machine.states[0].name; state = machine.states[0].name;
} }
const interpreter = { machine, state, context, tickQueues: [] }; const interpreter = { machine, state, context, eventQueue: [], isTransitioning: false };
console.log(interpreter);
send(interpreter, ["entry", null]); send(interpreter, ["entry", null]);
return interpreter; return interpreter;
} }
function getState(interpreter) { function getState(interpreter) {
return interpreter.machine.states.find((state) => state.name === interpreter.state); return interpreter.machine.states.find((state) => state.name === interpreter.state);
} }
function getOns(state, event) { function getMatchingEventReactionCouplings(state, event) {
return state.ons.filter((on) => on.eventName === event[0]); return state.eventReactionCouplings.filter((eventReactionCoupling) => eventReactionCoupling.eventName === event[0]);
}
function noop() {
} }
function send(interpreter, event) { function send(interpreter, event) {
const state = getState(interpreter); interpreter.eventQueue.push(event);
const onsTree = getOns(state, event); if (interpreter.isTransitioning === false) {
const reactions = onsTree.map((on) => on.reactions).flat(); interpreter.isTransitioning = true;
const indexOfFirstGoto = reactions.findIndex((reaction) => reaction.type === "Goto"); while (interpreter.eventQueue.length > 0) {
const indexOfFinalReaction = indexOfFirstGoto === -1 ? reactions.length - 1 : indexOfFirstGoto; processNextEvent(interpreter);
const reactionsUntilFirstGoto = reactions.slice(0, indexOfFinalReaction + 1); }
const functionsToRunInTick = reactionsUntilFirstGoto.map((reaction) => { interpreter.isTransitioning = false;
if (reaction.type === "Do") { }
return reaction.fn; }
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") { } else if (reaction.type === "Goto") {
return (ctx, e, self) => { goto_ = reaction;
self.state = reaction.targetStateName;
send(self, ["entry", e]);
};
} else {
return noop;
} }
}); });
const tick = Tick(functionsToRunInTick); return { sideEffects, contextMutations, goto_ };
tick.doFunctions.forEach((fn) => {
fn(interpreter.context, event, interpreter);
});
} }
// src/tests/01-ping-pong.ts // src/tests/01-ping-pong.ts
@ -85,11 +97,11 @@
"idle", "idle",
On( On(
"entry", "entry",
Do(log) SideEffect(log)
), ),
On( On(
"server-created", "server-created",
Do((_ctx, [_eventName, serverActor2], self) => { SideEffect((_ctx, [_eventName, serverActor2], self) => {
self.context.serverActor = serverActor2; self.context.serverActor = serverActor2;
}), }),
Goto("making-request") Goto("making-request")
@ -99,8 +111,8 @@
"making-request", "making-request",
On( On(
"entry", "entry",
Do(log), SideEffect(log),
Do(makeRequest), SideEffect(makeRequest),
Goto("awaiting-response") Goto("awaiting-response")
) )
), ),
@ -108,11 +120,11 @@
"awaiting-response", "awaiting-response",
On( On(
"entry", "entry",
Do(log) SideEffect(log)
), ),
On( On(
"received-response", "received-response",
Do(log), SideEffect(log),
Goto("making-request") Goto("making-request")
) )
) )
@ -122,11 +134,11 @@
"awaiting-request", "awaiting-request",
On( On(
"entry", "entry",
Do(log) SideEffect(log)
), ),
On( On(
"received-request", "received-request",
Do((_ctx, [_eventName, clientActor2], self) => { SideEffect((_ctx, [_eventName, clientActor2], self) => {
self.context.clientActor = clientActor2; self.context.clientActor = clientActor2;
}), }),
Goto("sending-response") Goto("sending-response")
@ -136,12 +148,12 @@
"sending-response", "sending-response",
On( On(
"entry", "entry",
Do(log), SideEffect(log),
Do(startTimer) SideEffect(startTimer)
), ),
On( On(
"timer-finished", "timer-finished",
Do(sendResponse), SideEffect(sendResponse),
Goto("awaiting-request") Goto("awaiting-request")
) )
) )

@ -4,47 +4,50 @@ export interface Machine_T {
} }
export interface State_T { export interface State_T {
name: string; name: string;
ons: Array<On_T>; eventReactionCouplings: Array<EventReactionCouplings_T>;
} }
export interface On_T { export interface EventReactionCouplings_T {
eventName: string; eventName: string;
reactions: Array<Do_T | Goto_T>; reactions: Array<Reaction_T>;
}; };
export interface Do_T { export type Reaction_T = SideEffect_T | ContextMutation_T | Goto_T;
type: 'Do'; export interface SideEffect_T {
fn: DoFn_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 { export interface Goto_T {
type: 'Goto'; type: 'Goto';
targetStateName: string; targetStateName: string;
}; };
export const Machine = function(...states:Array<State_T>) : Machine_T { return {states}; }; 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 State = function(name:string, ...eventReactionCouplings:Array<EventReactionCouplings_T>) : State_T{ return {name, eventReactionCouplings}; };
export const On = function(eventName:string, ...reactions:Array<Do_T | Goto_T>) : On_T{ return {eventName, reactions}; }; export const On = function(eventName:string, ...reactions:Array<Reaction_T>) : EventReactionCouplings_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 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 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 { export interface Interpreter_T {
machine: Machine_T; machine: Machine_T;
state: string; state: string;
context: any; 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{ export function interpret(machine:Machine_T, options:{state?:string, context:any}) : Interpreter_T{
let {state, context} = options; let {state, context} = options;
if(typeof state === 'undefined'){ state = machine.states[0].name; } if(typeof state === 'undefined'){ state = machine.states[0].name; }
const interpreter = {machine, state, context, tickQueues:[]} const interpreter = {machine, state, context, eventQueue:[], isTransitioning:false}
console.log(interpreter);
//@ts-ignore
send(interpreter, ['entry', null] ); send(interpreter, ['entry', null] );
return interpreter; return interpreter;
} }
@ -56,12 +59,9 @@ function getState(interpreter : Interpreter_T) : State_T{
} }
/** Helper function for `send()` /** Helper function for `send()`
*/ */
function getOns(state : State_T, event:Event_T) : Array<On_T>{ function getMatchingEventReactionCouplings(state : State_T, event:Event_T) : Array<EventReactionCouplings_T>{
return state.ons.filter((on)=>on.eventName===event[0]); return state.eventReactionCouplings.filter((eventReactionCoupling)=>eventReactionCoupling.eventName===event[0]);
} }
/** Helper function for `send()`
*/
function noop(){}
/** Inject an Event into the Interpreter's "tick queue". /** 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; * 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. * Tick, it is discarded.
*/ */
export function send(interpreter : Interpreter_T, event:Event_T){ export function send(interpreter : Interpreter_T, event:Event_T){
const state = getState(interpreter); interpreter.eventQueue.push(event);
const onsTree = getOns(state, event); if(interpreter.isTransitioning === false){
const reactions = onsTree interpreter.isTransitioning = true;
.map((on)=>on.reactions) while(interpreter.eventQueue.length > 0){
.flat(); processNextEvent(interpreter);
const indexOfFirstGoto = reactions.findIndex((reaction)=>reaction.type==='Goto'); }
const indexOfFinalReaction = indexOfFirstGoto === -1 ? reactions.length-1 : indexOfFirstGoto; interpreter.isTransitioning = false;
const reactionsUntilFirstGoto = reactions.slice(0, indexOfFinalReaction+1); }
const functionsToRunInTick = reactionsUntilFirstGoto }
.map((reaction)=>{ export const enqueue = send;
if(reaction.type === 'Do'){ function processNextEvent(interpreter:Interpreter_T){
return reaction.fn; const nextEvent = interpreter.eventQueue.shift();
} if(typeof nextEvent !== 'undefined'){
else if(reaction.type === 'Goto'){ const state = getState(interpreter);
return (ctx:any,e:Event_T,self:Interpreter_T)=>{ const eventReactionCouplings = getMatchingEventReactionCouplings(state, nextEvent);
self.state = reaction.targetStateName; const reactions = eventReactionCouplings
//@ts-ignore .map((eventReactionCoupling)=>eventReactionCoupling.reactions)
send(self, ['entry', e] ); .flat();
}; const {sideEffects, contextMutations, goto_} = categorizeReactions(reactions);
} // can process sideEffects in parallel:
else{ sideEffects.forEach((sideEffect)=>{
return noop; sideEffect.fn(interpreter.context, nextEvent, interpreter);
}
}); });
const tick = Tick(functionsToRunInTick); // must process contextMutations in-series:
tick.doFunctions.forEach((fn)=>{ fn(interpreter.context, event, interpreter); }); 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(){}; export const Spawn = function(){};

@ -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 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); }; const log = (ctx:C, e:Event_T, self:Interpreter_T)=>{ console.log(self.state); };
@ -16,8 +16,8 @@ const machine =
Machine( Machine(
State('green', State('green',
On('entry', On('entry',
Do(beginTimer), SideEffect(beginTimer),
Do(log) SideEffect(log)
), ),
On('timer-finished', On('timer-finished',
Goto('yellow') Goto('yellow')
@ -25,8 +25,8 @@ const machine =
), ),
State('yellow', State('yellow',
On('entry', On('entry',
Do(beginTimer), SideEffect(beginTimer),
Do(log) SideEffect(log)
), ),
On('timer-finished', On('timer-finished',
Goto('red') Goto('red')
@ -34,8 +34,8 @@ const machine =
), ),
State('red', State('red',
On('entry', On('entry',
Do(beginTimer), SideEffect(beginTimer),
Do(log) SideEffect(log)
), ),
On('timer-finished', On('timer-finished',
Goto('green') Goto('green')

@ -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 wait = (ms:number)=>new Promise((resolve)=>{ setTimeout(()=>{ resolve(1); }, ms); });
const makeRequest = (ctx,e,self)=>{ send(ctx.serverActor, ['received-request',self]); }; const makeRequest = (ctx,e,self)=>{ send(ctx.serverActor, ['received-request',self]); };
@ -21,27 +21,26 @@ const client =
Machine( Machine(
State('idle', State('idle',
On('entry', On('entry',
Do(log), SideEffect(log),
), ),
On('server-created', On('server-created',
SideEffect((_ctx,[_eventName,serverActor],self)=>{ self.context.serverActor=serverActor; }),
Do((_ctx,[_eventName,serverActor],self)=>{ self.context.serverActor=serverActor; }),
Goto('making-request') Goto('making-request')
) )
), ),
State('making-request', State('making-request',
On('entry', On('entry',
Do(log), SideEffect(log),
Do(makeRequest), SideEffect(makeRequest),
Goto('awaiting-response') Goto('awaiting-response')
), ),
), ),
State('awaiting-response', State('awaiting-response',
On('entry', On('entry',
Do(log), SideEffect(log),
), ),
On('received-response', On('received-response',
Do(log), SideEffect(log),
Goto('making-request') Goto('making-request')
), ),
), ),
@ -60,20 +59,20 @@ const server =
Machine( Machine(
State('awaiting-request', State('awaiting-request',
On('entry', On('entry',
Do(log), SideEffect(log),
), ),
On('received-request', On('received-request',
Do((_ctx,[_eventName,clientActor],self)=>{ self.context.clientActor=clientActor; }), SideEffect((_ctx,[_eventName,clientActor],self)=>{ self.context.clientActor=clientActor; }),
Goto('sending-response') Goto('sending-response')
), ),
), ),
State('sending-response', State('sending-response',
On('entry', On('entry',
Do(log), SideEffect(log),
Do(startTimer) SideEffect(startTimer)
), ),
On('timer-finished', On('timer-finished',
Do(sendResponse), SideEffect(sendResponse),
Goto('awaiting-request') Goto('awaiting-request')
) )
), ),

Loading…
Cancel
Save