diff --git a/src/index.ts b/src/index.ts index 506f3b7..46fd251 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,48 +1,48 @@ export type Event_T = [name:string, payload:any]; -export interface Machine_T { - states: Array +export interface Machine_T { + states: Array> } -export interface State_T { +export interface State_T { name: string; - eventReactionCouplings: Array; + eventReactionCouplings: Array>; } -export interface EventReactionCouplings_T { +export interface EventReactionCouplings_T { eventName: string; - reactions: Array; + reactions: Array>; }; -export type Reaction_T = SideEffect_T | ContextMutation_T | Goto_T; -export interface SideEffect_T { +export type Reaction_T = SideEffect_T | ContextMutation_T | Goto_T; +export interface SideEffect_T { type: 'SideEffect'; - fn: SideEffectFunction_T; + fn: SideEffectFunction_T; }; -export type SideEffectFunction_T = (ctx:any,e:Event_T,self:Interpreter_T)=>void; -export interface ContextMutation_T { +export type SideEffectFunction_T = (ctx:C,e:Event_T,self:Interpreter_T,originalContext:C)=>void; +export interface ContextMutation_T { type: 'ContextMutation'; - fn: ContextMutationFunction_T; + fn: ContextMutationFunction_T; }; -export type ContextMutationFunction_T = (ctx:any,e:Event_T,self:Interpreter_T)=>any; +export type ContextMutationFunction_T = (ctx:C,e:Event_T,self:Interpreter_T)=>C; 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 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 const Context = function(fn:ContextMutationFunction_T) : ContextMutation_T { return {type:'ContextMutation', fn} }; -export interface Interpreter_T { - machine: Machine_T; +export interface Interpreter_T { + machine: Machine_T; state: string; context: C; eventQueue:Array; - subscriptions: Record; + subscriptions: Record>; isTransitioning: boolean; isPaused: boolean; start: ()=>Interpreter_T; @@ -57,7 +57,7 @@ export interface Interpreter_T { * @param {?string} [initialStateName] * @returns {Interpreter_T} */ -export function Interpreter(machine:Machine_T, initialContext:any, initialStateName?:string) : Interpreter_T{ +export function Interpreter(machine:Machine_T, initialContext:any, initialStateName?:string) : Interpreter_T{ if(typeof initialStateName === 'undefined'){ initialStateName = machine.states[0].name; } //@ts-expect-error const interpreter : Interpreter_T = {machine, state: initialStateName, context:initialContext, eventQueue:[], isTransitioning:false, subscriptions: {}, isPaused: true}; @@ -65,13 +65,13 @@ export function Interpreter(machine:Machine_T, initialContext:any, initialSta send(interpreter, ['entry', null] ); return interpreter; } -export function start(interpreter:Interpreter_T){ +export function start(interpreter:Interpreter_T){ if(interpreter.isPaused === true){ interpreter.isPaused = false; processEvents(interpreter); } } -export function pause(interpreter:Interpreter_T){ +export function pause(interpreter:Interpreter_T){ if(interpreter.isPaused === false){ interpreter.isPaused = true; } @@ -79,12 +79,12 @@ export function pause(interpreter:Interpreter_T){ /** 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; +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{ +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". @@ -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(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){ +function processEvents(interpreter:Interpreter_T){ 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(interpreter:Interpreter_T){ 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) : {sideEffects:Array, contextMutations:Array, goto_:Goto_T|null}{ +function categorizeReactions(reactions:Array>) : {sideEffects:Array>, contextMutations:Array>, goto_:Goto_T|null}{ let - sideEffects:Array = [], - contextMutations:Array = [], + sideEffects:Array> = [], + contextMutations:Array> = [], goto_:Goto_T|null = null; reactions.forEach((reaction)=>{ if(reaction.type === 'SideEffect'){ @@ -157,14 +160,14 @@ function categorizeReactions(reactions:Array) : {sideEffects:Arrayvoid; +export type SubscriptionCallbackFunction_T = (self:Interpreter_T)=>void; let subscriptionId : number = 0; -export function subscribe(interpreter:Interpreter_T, callback:SubscriptionCallbackFunction_T){ +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){ +export function unsubscribe(interpreter:Interpreter_T, subscriptionId:string){ delete interpreter.subscriptions[subscriptionId.toString()]; } diff --git a/src/tests/00-basic.ts b/src/tests/00-basic.ts index bcdbf72..7086a8e 100644 --- a/src/tests/00-basic.ts +++ b/src/tests/00-basic.ts @@ -1,7 +1,7 @@ -import { Machine, State, On, SideEffect, Goto, Spawn, Unspawn, Interpreter, Interpreter_T, send, Event_T } from '../index'; +import { Machine, State, On, SideEffect, Goto, Interpreter, send, SideEffectFunction_T } from '../index'; -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 beginTimer : SideEffectFunction_T = (ctx, e, self)=>{ setTimeout(()=>{ send(self, ['timer-finished',null]); }, 800); }; +const log : SideEffectFunction_T = (ctx, e, self)=>{ console.log(self.state); }; type S = | 'green' @@ -43,4 +43,4 @@ const machine = ), ); -const actor = Interpreter(machine, {context:{}}); \ No newline at end of file +const actor = Interpreter(machine, {context:{}}); \ No newline at end of file diff --git a/src/tests/01-ping-pong.ts b/src/tests/01-ping-pong.ts index baa41bb..ef54cbe 100644 --- a/src/tests/01-ping-pong.ts +++ b/src/tests/01-ping-pong.ts @@ -1,15 +1,24 @@ -import { Machine, State, On, SideEffect, Goto, Spawn, Unspawn, Interpreter, Interpreter_T, send, Event_T, Context } from '../index'; +import { Machine, State, On, SideEffect, Goto, Spawn, Unspawn, Interpreter, Interpreter_T, send, Event_T, Context, SideEffectFunction_T } from '../index'; const wait = (ms:number)=>new Promise((resolve)=>{ setTimeout(()=>{ resolve(1); }, ms); }); -const makeRequest = (ctx,e,self)=>{ send(ctx.serverActor, ['received-request',self]); }; -const sendResponse = (ctx,e,self)=>{ send(ctx.clientActor, ['received-response',self]); }; -const startTimer = async (ctx,e,self)=>{ await wait(1500); send(self, ['timer-finished',null]); } -const log = (ctx, e, self:Interpreter_T)=>{ console.log(self.state, ctx); }; +const makeRequest : SideEffectFunction_T = (ctx,e,self)=>{ send(ctx.serverActor, ['received-request',self]); }; +const sendResponse : SideEffectFunction_T = (ctx,e,self)=>{ send(ctx.clientActor, ['received-response',self]); }; +const startTimer : SideEffectFunction_T = async (ctx,e,self)=>{ await wait(1500); send(self, ['timer-finished',null]); } +const log : SideEffectFunction_T = (ctx, e, self)=>{ console.log(self.state, ctx); }; + +type Cc = { + requestsMade: number; + responsesReceived: number; + serverActor: Interpreter_T; +}; +type Cs = { + clientActor: Interpreter_T +}; const client = - Machine( - State('idle', - On('entry', + Machine( + State('idle', + On('entry', SideEffect(log), ), On('server-created', @@ -17,30 +26,30 @@ const client = Goto('making-request') ) ), - State('making-request', - On('entry', + State('making-request', + On('entry', SideEffect(log), SideEffect(makeRequest), - Context((ctx)=>({...ctx, requestsMade: ctx.requestsMade+1})), + Context((ctx)=>({...ctx, requestsMade: ctx.requestsMade+1})), Goto('awaiting-response') ), ), - State('awaiting-response', - On('entry', + State('awaiting-response', + On('entry', SideEffect(log), ), - On('received-response', + On('received-response', SideEffect(log), - Context((ctx)=>({...ctx, responsesReceived: ctx.responsesReceived+1})), + Context((ctx)=>({...ctx, responsesReceived: ctx.responsesReceived+1})), Goto('making-request') ), ), ); const server = - Machine( - State('awaiting-request', - On('entry', + Machine( + State('awaiting-request', + On('entry', SideEffect(log), ), On('received-request', @@ -48,8 +57,8 @@ const server = Goto('sending-response') ), ), - State('sending-response', - On('entry', + State('sending-response', + On('entry', SideEffect(log), SideEffect(startTimer) ),