You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

171 lines
6.7 KiB
TypeScript

export type Event_T = [name:string, payload:any];
export interface Machine_T {
states: Array<State_T>
}
export interface State_T {
name: string;
eventReactionCouplings: Array<EventReactionCouplings_T>;
}
export interface EventReactionCouplings_T {
eventName: string;
reactions: Array<Reaction_T>;
};
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<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 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<Event_T>;
subscriptions: Record<string, SubscriptionCallbackFunction_T>;
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<EventReactionCouplings_T>{
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<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 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:{}}),[]);
}
*/