diff --git a/src/index.ts b/src/index.ts index e5ca1e4..be7714b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -41,12 +41,15 @@ export interface Interpreter_T { machine: Machine_T; state: string; context: C; + peers: Record | Array>>; + peerSubscriptionIds: Map,string>; eventQueue:Array; - subscriptions: Record>; + subscriptionsToEvents: Record>; // called upon every event + subscriptionsToState: Record>; // every time state changes, even if it's transient + subscriptionsToSettledState: Record>; // only called when tick settles isTransitioning: boolean; isPaused: boolean; start: ()=>Interpreter_T; - subscribe: (callback:SubscriptionCallbackFunction_T)=>Interpreter_T; } /** @@ -61,9 +64,8 @@ export interface 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}; + const interpreter : Interpreter_T = {machine, state: initialStateName, context:initialContext, eventQueue:[], isTransitioning:false, subscriptionsToEvents: {}, subscriptionsToState: {}, subscriptionsToSettledState: {}, isPaused: true}; interpreter.start = ()=>{ start(interpreter); return interpreter; } - interpreter.subscribe = (callback)=>{ subscribe(interpreter,callback); return interpreter; } send(interpreter, ['entry', null] ); return interpreter; } @@ -113,7 +115,7 @@ function processEvents(interpreter:Interpreter_T){ } interpreter.isTransitioning = false; // only run subscriptions here, once the machine's state has settled: - Object.values(interpreter.subscriptions).forEach((subscriptionCallbackFunction)=>{ subscriptionCallbackFunction(interpreter); }); + Object.values(interpreter.subscriptionsToSettledState).forEach((callbackFunction)=>{ callbackFunction(interpreter); }); } function processNextEvent(interpreter:Interpreter_T){ const event = interpreter.eventQueue.shift(); @@ -130,6 +132,8 @@ function processNextEvent(interpreter:Interpreter_T){ contextMutations.forEach((contextMutation)=>{ interpreter.context = contextMutation.fn(interpreter.context, event, interpreter); }); + // run subscription-to-events callbacks (can be in parallel), since an event just happened: + Object.values(interpreter.subscriptionsToEvents).forEach((callbackFunction)=>{ callbackFunction(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)=>{ @@ -139,6 +143,8 @@ function processNextEvent(interpreter:Interpreter_T){ if(goto_ !== null){ send(interpreter, ['exit', null]); interpreter.state = goto_.targetStateName; + // run subscription-to-state callbacks (can be in parallel), since state just changed, possibly transiently (depends on whether the loop in `processEvents()` runs again): + Object.values(interpreter.subscriptionsToState).forEach((callbackFunction)=>{ callbackFunction(event, interpreter); }); send(interpreter, ['entry', null]); } } @@ -162,16 +168,37 @@ function categorizeReactions(reactions:Array>) : {sideEffects:A return {sideEffects, contextMutations, goto_}; } -export type SubscriptionCallbackFunction_T = (self:Interpreter_T)=>void; +export type EventsSubscriptionCallbackFunction_T = (e:Event_T, self:Interpreter_T)=>void; +export type StateSubscriptionCallbackFunction_T = (e:Event_T, self:Interpreter_T)=>void; +export type SettledStateSubscriptionCallbackFunction_T = (self:Interpreter_T)=>void; // we don't pass an event, because these only run once state settles, so a whole chain of events could have been responsible for that; it's unlikely a subscriber is interested only in the final one +// TODO: add subscribeToContext and subscribeToSettledContext functions, to get only changes to context, regardless of events happening or state changing let subscriptionId : number = 0; -export function subscribe(interpreter:Interpreter_T, callback:SubscriptionCallbackFunction_T){ +export function subscribe(interpreter:Interpreter_T, callback:SettledStateSubscriptionCallbackFunction_T){ subscriptionId++; - interpreter.subscriptions[subscriptionId.toString()] = callback; + interpreter.subscriptionsToSettledState[subscriptionId.toString()] = callback; + return subscriptionId.toString(); +} +export const subscribeToSettledState = subscribe; +export function subscribeToState(interpreter:Interpreter_T, callback:StateSubscriptionCallbackFunction_T){ + subscriptionId++; + interpreter.subscriptionsToState[subscriptionId.toString()] = callback; + return subscriptionId.toString(); +} +export function subscribeToEvents(interpreter:Interpreter_T, callback:StateSubscriptionCallbackFunction_T){ + subscriptionId++; + interpreter.subscriptionsToEvents[subscriptionId.toString()] = callback; return subscriptionId.toString(); } export function unsubscribe(interpreter:Interpreter_T, subscriptionId:string){ - delete interpreter.subscriptions[subscriptionId.toString()]; + delete interpreter.subscriptionsToSettledState[subscriptionId.toString()]; + delete interpreter.subscriptionsToState[subscriptionId.toString()]; + delete interpreter.subscriptionsToEvents[subscriptionId.toString()]; +} + +export function addPeer(self:Interpreter_T, peer:Interpreter_T, name:string){ + self.peers[name] = peer; } +export function addPeers(){} export const Spawn = function(){}; export const Unspawn = function(){};