export type Event_T = [name:string, payload:any]; export interface Machine_T { states: Array } export interface State_T { name: string; eventReactionCouplings: Array; } export interface EventReactionCouplings_T { eventName: string; reactions: Array; }; export type Reaction_T = SideEffect_T | ContextMutation_T | Goto_T; export interface SideEffect_T { type: 'SideEffect'; fn: SideEffectFunction_T; }; 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, ...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} }; export interface Interpreter_T { machine: Machine_T; state: string; context: any; eventQueue:Array; subscriptions: Record; isTransitioning: boolean; isPaused: boolean; } /** * Description placeholder * * @export * @param {Machine_T} machine * @param {InitialContextFunction_T} initialContextFunction - in the form of a function rather than a direct value, so as to facilitate co-initialization of peer interpreters. Otherwise, the "parent" interpreter will start, but without a reference to a running child interpreter which it might expect to exist. * @param {?string} [initialStateName] * @returns {Interpreter_T} */ export function Interpreter(machine:Machine_T, initialContext:any, initialStateName?:string) : Interpreter_T{ if(typeof initialStateName === 'undefined'){ initialStateName = machine.states[0].name; } const interpreter = {machine, state: initialStateName, context:initialContext, eventQueue:[], isTransitioning:false, subscriptions: {}, isPaused: true} send(interpreter, ['entry', null] ); return interpreter; } export function start(interpreter:Interpreter_T){ interpreter.isPaused = false; processEvents(interpreter); } export function pause(interpreter:Interpreter_T){ interpreter.isPaused = true; } /** Helper function for `send()` */ function getState(interpreter : Interpreter_T) : State_T{ return interpreter.machine.states.find((state)=>state.name===interpreter.state) as unknown as State_T; } /** Helper function for `send()` */ function getMatchingEventReactionCouplings(state : State_T, event:Event_T) : Array{ return state.eventReactionCouplings.filter((eventReactionCoupling)=>eventReactionCoupling.eventName===event[0]); } /** 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; * or it can signify a milestone "within" the current Tick, such that a Tick can be thought of as having * "sub-Ticks". * * This distinction is significant for proper ordering of reaction execution, and also for determining * 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){ interpreter.eventQueue.push(event); if(interpreter.isTransitioning === false){ processEvents(interpreter); } } export const enqueue = send; function processEvents(interpreter:Interpreter_T){ interpreter.isTransitioning = true; while(interpreter.eventQueue.length > 0 && interpreter.isPaused===false){ processNextEvent(interpreter); } interpreter.isTransitioning = false; // only run subscriptions here, once the machine's state has settled: Object.values(interpreter.subscriptions).forEach((subscriptionCallbackFunction)=>{ subscriptionCallbackFunction(interpreter); }); } 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); }); // 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){ send(interpreter, ['exit', 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 type SubscriptionCallbackFunction_T = (self:Interpreter_T)=>void; let subscriptionId : number = 0; export function subscribe(interpreter:Interpreter_T, callback:SubscriptionCallbackFunction_T){ subscriptionId++; interpreter.subscriptions[subscriptionId.toString()] = callback; return subscriptionId.toString(); } export function unsubscribe(interpreter:Interpreter_T, subscriptionId:string){ delete interpreter.subscriptions[subscriptionId.toString()]; } export const Spawn = function(){}; export const Unspawn = function(){}; /* export function useMachine(machine, options){ return useMemo(()=>interpret(AppMachine, {context:{}}),[]); } */