@ -1,48 +1,48 @@
export type Event_T = [ name :string , payload :any ] ;
export interface Machine_T {
states : Array < State_T >
export interface Machine_T < C > {
states : Array < State_T < C > >
}
export interface State_T {
export interface State_T < C > {
name : string ;
eventReactionCouplings : Array < EventReactionCouplings_T >;
eventReactionCouplings : Array < EventReactionCouplings_T < C > >;
}
export interface EventReactionCouplings_T {
export interface EventReactionCouplings_T < C > {
eventName : string ;
reactions : Array < Reaction_T >;
reactions : Array < Reaction_T < C > >;
} ;
export type Reaction_T = SideEffect_T | ContextMutation_T | Goto_T ;
export interface SideEffect_T {
export type Reaction_T < C > = SideEffect_T < C > | ContextMutation_T < C > | Goto_T ;
export interface SideEffect_T < C > {
type : 'SideEffect' ;
fn : SideEffectFunction_T ;
fn : SideEffectFunction_T <C > ;
} ;
export type SideEffectFunction_T = ( ctx :any , e :Event_T , self :Interpreter_T ) = > void ;
export interface ContextMutation_T {
export type SideEffectFunction_T < C > = ( ctx :C , e :Event_T , self :Interpreter_T < C > , originalContext :C ) = > void ;
export interface ContextMutation_T < C > {
type : 'ContextMutation' ;
fn : ContextMutationFunction_T ;
fn : ContextMutationFunction_T <C > ;
} ;
export type ContextMutationFunction_T = ( ctx :any , e :Event_T , self :Interpreter_T ) = > any ;
export type ContextMutationFunction_T < C > = ( ctx :C , e :Event_T , self :Interpreter_T < C > ) = > C ;
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 , . . . 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 Machine = function <C > (. . . states :Array < State_T < C > >) : Machine_T < C > { return { states } ; } ;
export const State = function <C > (name :string , . . . eventReactionCouplings :Array < EventReactionCouplings_T < C > >) : State_T < C > { return { name , eventReactionCouplings } ; } ;
export const On = function <C > (eventName :string , . . . reactions :Array < Reaction_T < C > >) : EventReactionCouplings_T < C > { return { eventName , reactions } ; } ;
export const SideEffect = function <C > (fn :SideEffectFunction_T <C > ) : SideEffect_T < C > { 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 } } ;
export const Context = function <C > (fn :ContextMutationFunction_T <C > ) : ContextMutation_T < C > { return { type : 'ContextMutation' , fn } } ;
export interface Interpreter_T < C = any > {
machine : Machine_T ;
export interface Interpreter_T < C > {
machine : Machine_T <C > ;
state : string ;
context : C ;
eventQueue :Array < Event_T > ;
subscriptions : Record < string , SubscriptionCallbackFunction_T >;
subscriptions : Record < string , SubscriptionCallbackFunction_T < C > >;
isTransitioning : boolean ;
isPaused : boolean ;
start : ( ) = > Interpreter_T < C > ;
@ -57,7 +57,7 @@ export interface Interpreter_T<C=any> {
* @param { ? string } [ initialStateName ]
* @returns { Interpreter_T }
* /
export function Interpreter < C > ( machine :Machine_T , initialContext :any , initialStateName? :string ) : Interpreter_T < C > {
export function Interpreter < C > ( machine :Machine_T <C > , initialContext :any , initialStateName? :string ) : Interpreter_T < C > {
if ( typeof initialStateName === 'undefined' ) { initialStateName = machine . states [ 0 ] . name ; }
//@ts-expect-error
const interpreter : Interpreter_T < C > = { machine , state : initialStateName , context :initialContext , eventQueue : [ ] , isTransitioning :false , subscriptions : { } , isPaused : true } ;
@ -65,13 +65,13 @@ export function Interpreter<C>(machine:Machine_T, initialContext:any, initialSta
send ( interpreter , [ 'entry' , null ] ) ;
return interpreter ;
}
export function start (interpreter :Interpreter_T ) {
export function start <C > (interpreter :Interpreter_T < C > ) {
if ( interpreter . isPaused === true ) {
interpreter . isPaused = false ;
processEvents ( interpreter ) ;
}
}
export function pause (interpreter :Interpreter_T ) {
export function pause <C > (interpreter :Interpreter_T < C > ) {
if ( interpreter . isPaused === false ) {
interpreter . isPaused = true ;
}
@ -79,12 +79,12 @@ export function pause(interpreter:Interpreter_T){
/ * * H e l p e r f u n c t i o n f o r ` s e n d ( ) `
* /
function getState (interpreter : Interpreter_T ) : State_T {
return interpreter . machine . states . find ( ( state ) = > state . name === interpreter . state ) as unknown as State_T ;
function getState <C > (interpreter : Interpreter_T <C > ) : State_T < C > {
return interpreter . machine . states . find ( ( state ) = > state . name === interpreter . state ) as unknown as State_T <C > ;
}
/ * * H e l p e r f u n c t i o n f o r ` s e n d ( ) `
* /
function getMatchingEventReactionCouplings (state : State_T , event :Event_T ) : Array < EventReactionCouplings_T >{
function getMatchingEventReactionCouplings <C > (state : State_T < C > , event :Event_T ) : Array < EventReactionCouplings_T < C > >{
return state . eventReactionCouplings . filter ( ( eventReactionCoupling ) = > eventReactionCoupling . eventName === event [ 0 ] ) ;
}
/ * * I n j e c t a n E v e n t i n t o t h e I n t e r p r e t e r ' s " t i c k q u e u e " .
@ -97,14 +97,14 @@ function getMatchingEventReactionCouplings(state : State_T, event:Event_T) : Arr
* whether to run a reaction at all . If an Event is received , and is specified to be applied on a past
* Tick , it is discarded .
* /
export function send (interpreter : Interpreter_T , event :Event_T ) {
export function send <C > (interpreter : Interpreter_T < C > , event :Event_T ) {
interpreter . eventQueue . push ( event ) ;
if ( interpreter . isTransitioning === false ) {
processEvents ( interpreter ) ;
}
}
export const enqueue = send ;
function processEvents (interpreter :Interpreter_T ) {
function processEvents <C > (interpreter :Interpreter_T < C > ) {
interpreter . isTransitioning = true ;
while ( interpreter . eventQueue . length > 0 && interpreter . isPaused === false ) {
processNextEvent ( interpreter ) ;
@ -113,7 +113,7 @@ function processEvents(interpreter:Interpreter_T){
// only run subscriptions here, once the machine's state has settled:
Object . values ( interpreter . subscriptions ) . forEach ( ( subscriptionCallbackFunction ) = > { subscriptionCallbackFunction ( interpreter ) ; } ) ;
}
function processNextEvent (interpreter :Interpreter_T ) {
function processNextEvent <C > (interpreter :Interpreter_T < C > ) {
const event = interpreter . eventQueue . shift ( ) ;
if ( typeof event !== 'undefined' ) {
const state = getState ( interpreter ) ;
@ -122,14 +122,17 @@ function processNextEvent(interpreter:Interpreter_T){
. map ( ( eventReactionCoupling ) = > eventReactionCoupling . reactions )
. flat ( ) ;
const { sideEffects , contextMutations , goto_ } = categorizeReactions ( reactions ) ;
// can process sideEffects in parallel (though we currently don't due to the overhead of doing so in Node.js):
sideEffects . forEach ( ( sideEffect ) = > {
sideEffect . fn ( interpreter . context , event , interpreter ) ;
} ) ;
// save the current context, before it's mutated, so as to pass it to sideEffects below:
const originalContext = interpreter . context ;
// must process contextMutations in-series:
contextMutations . forEach ( ( contextMutation ) = > {
interpreter . context = contextMutation . fn ( interpreter . context , event , interpreter ) ;
} ) ;
// can process sideEffects in parallel (though we currently don't due to the overhead of doing so in Node.js):
// they're processed *after* the context changes, since that's what most sideEffects would be interested in; but nevertheless the original context is passed in case this sideEffect needs it:
sideEffects . forEach ( ( sideEffect ) = > {
sideEffect . fn ( interpreter . context , event , interpreter , originalContext ) ;
} ) ;
// processing of `goto` must be last:
if ( goto_ !== null ) {
send ( interpreter , [ 'exit' , null ] ) ;
@ -138,10 +141,10 @@ function processNextEvent(interpreter:Interpreter_T){
}
}
}
function categorizeReactions (reactions :Array < Reaction_T > ) : { sideEffects :Array < SideEffect_T >, contextMutations :Array < ContextMutation_T >, goto_ :Goto_T | null } {
function categorizeReactions <C > (reactions :Array < Reaction_T < C > > ) : { sideEffects :Array < SideEffect_T < C > >, contextMutations :Array < ContextMutation_T < C > >, goto_ :Goto_T | null } {
let
sideEffects :Array < SideEffect_T > = [ ] ,
contextMutations :Array < ContextMutation_T > = [ ] ,
sideEffects :Array < SideEffect_T < C > > = [ ] ,
contextMutations :Array < ContextMutation_T < C > > = [ ] ,
goto_ :Goto_T | null = null ;
reactions . forEach ( ( reaction ) = > {
if ( reaction . type === 'SideEffect' ) {
@ -157,14 +160,14 @@ function categorizeReactions(reactions:Array<Reaction_T>) : {sideEffects:Array<S
return { sideEffects , contextMutations , goto_ } ;
}
export type SubscriptionCallbackFunction_T = ( self :Interpreter_T ) = > void ;
export type SubscriptionCallbackFunction_T < C > = ( self :Interpreter_T < C > ) = > void ;
let subscriptionId : number = 0 ;
export function subscribe (interpreter :Interpreter_T , callback :SubscriptionCallbackFunction_T ) {
export function subscribe <C > (interpreter :Interpreter_T <C > , callback :SubscriptionCallbackFunction_T < C > ) {
subscriptionId ++ ;
interpreter . subscriptions [ subscriptionId . toString ( ) ] = callback ;
return subscriptionId . toString ( ) ;
}
export function unsubscribe (interpreter :Interpreter_T , subscriptionId :string ) {
export function unsubscribe <C > (interpreter :Interpreter_T < C > , subscriptionId :string ) {
delete interpreter . subscriptions [ subscriptionId . toString ( ) ] ;
}