add subscription types

main
Avraham Sakal 2 years ago
parent f4452739da
commit 3e769c0fa5

@ -41,12 +41,15 @@ export interface Interpreter_T<C> {
machine: Machine_T<C>;
state: string;
context: C;
peers: Record<string, Interpreter_T<unknown> | Array<Interpreter_T<unknown>>>;
peerSubscriptionIds: Map<Interpreter_T<unknown>,string>;
eventQueue:Array<Event_T>;
subscriptions: Record<string, SubscriptionCallbackFunction_T<C>>;
subscriptionsToEvents: Record<string, EventsSubscriptionCallbackFunction_T<C>>; // called upon every event
subscriptionsToState: Record<string, StateSubscriptionCallbackFunction_T<C>>; // every time state changes, even if it's transient
subscriptionsToSettledState: Record<string, SettledStateSubscriptionCallbackFunction_T<C>>; // only called when tick settles
isTransitioning: boolean;
isPaused: boolean;
start: ()=>Interpreter_T<C>;
subscribe: (callback:SubscriptionCallbackFunction_T<C>)=>Interpreter_T<C>;
}
/**
@ -61,9 +64,8 @@ export interface 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};
const interpreter : Interpreter_T<C> = {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<C>(interpreter:Interpreter_T<C>){
}
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<C>(interpreter:Interpreter_T<C>){
const event = interpreter.eventQueue.shift();
@ -130,6 +132,8 @@ function processNextEvent<C>(interpreter:Interpreter_T<C>){
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<C>(interpreter:Interpreter_T<C>){
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<C>(reactions:Array<Reaction_T<C>>) : {sideEffects:A
return {sideEffects, contextMutations, goto_};
}
export type SubscriptionCallbackFunction_T<C> = (self:Interpreter_T<C>)=>void;
export type EventsSubscriptionCallbackFunction_T<C> = (e:Event_T, self:Interpreter_T<C>)=>void;
export type StateSubscriptionCallbackFunction_T<C> = (e:Event_T, self:Interpreter_T<C>)=>void;
export type SettledStateSubscriptionCallbackFunction_T<C> = (self:Interpreter_T<C>)=>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<C>(interpreter:Interpreter_T<C>, callback:SubscriptionCallbackFunction_T<C>){
export function subscribe<C>(interpreter:Interpreter_T<C>, callback:SettledStateSubscriptionCallbackFunction_T<C>){
subscriptionId++;
interpreter.subscriptions[subscriptionId.toString()] = callback;
interpreter.subscriptionsToSettledState[subscriptionId.toString()] = callback;
return subscriptionId.toString();
}
export const subscribeToSettledState = subscribe;
export function subscribeToState<C>(interpreter:Interpreter_T<C>, callback:StateSubscriptionCallbackFunction_T<C>){
subscriptionId++;
interpreter.subscriptionsToState[subscriptionId.toString()] = callback;
return subscriptionId.toString();
}
export function subscribeToEvents<C>(interpreter:Interpreter_T<C>, callback:StateSubscriptionCallbackFunction_T<C>){
subscriptionId++;
interpreter.subscriptionsToEvents[subscriptionId.toString()] = callback;
return subscriptionId.toString();
}
export function unsubscribe<C>(interpreter:Interpreter_T<C>, 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<C, C_Peer>(self:Interpreter_T<C>, peer:Interpreter_T<C_Peer>, name:string){
self.peers[name] = peer;
}
export function addPeers(){}
export const Spawn = function(){};
export const Unspawn = function(){};

Loading…
Cancel
Save