diff --git a/dist/index.d.ts b/dist/index.d.ts index 1576f3d..b55ad64 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -10,7 +10,7 @@ export interface EventReactionCouplings_T { eventName: string; reactions: Array>; } -export type Reaction_T = SideEffect_T | ContextMutation_T | Goto_T; +export type Reaction_T = SideEffect_T | ContextMutation_T | Peering_T | Goto_T; export interface SideEffect_T { type: 'SideEffect'; fn: SideEffectFunction_T; @@ -21,6 +21,12 @@ export interface ContextMutation_T { fn: ContextMutationFunction_T; } export type ContextMutationFunction_T = (ctx: C, e: Event_T, self: Interpreter_T) => C; +export interface Peering_T { + type: 'Peering'; + name: string; + peerCreationFunction: PeerCreationFunction_T; +} +export type PeerCreationFunction_T = (ctx: C, e: Event_T, self: Interpreter_T) => Interpreter_T; export interface Goto_T { type: 'Goto'; targetStateName: string; @@ -31,16 +37,20 @@ export declare const On: (eventName: string, ...reactions: Reaction_T[]) = export declare const SideEffect: (fn: SideEffectFunction_T) => SideEffect_T; export declare const Goto: (targetStateName: string) => Goto_T; export declare const Context: (fn: ContextMutationFunction_T) => ContextMutation_T; +export declare const Peer: (name: string, peerCreationFunction: PeerCreationFunction_T) => Peering_T; export interface Interpreter_T { machine: Machine_T; state: string; context: C; + peers: Record>; + peerSubscriptionIds: Map, string>; eventQueue: Array; - subscriptions: Record>; + subscriptionsToEvents: Record>; + subscriptionsToState: Record>; + subscriptionsToSettledState: Record>; isTransitioning: boolean; isPaused: boolean; start: () => Interpreter_T; - subscribe: (callback: SubscriptionCallbackFunction_T) => Interpreter_T; } /** * Description placeholder @@ -66,8 +76,12 @@ export declare function pause(interpreter: Interpreter_T): void; */ export declare function send(interpreter: Interpreter_T, event: Event_T): void; export declare const enqueue: typeof send; -export type SubscriptionCallbackFunction_T = (self: Interpreter_T) => void; -export declare function subscribe(interpreter: Interpreter_T, callback: SubscriptionCallbackFunction_T): string; +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; +export declare function subscribe(interpreter: Interpreter_T, callback: SettledStateSubscriptionCallbackFunction_T): string; +export declare const subscribeToSettledState: typeof subscribe; +export declare function subscribeToState(interpreter: Interpreter_T, callback: StateSubscriptionCallbackFunction_T): string; +export declare function subscribeToEvents(interpreter: Interpreter_T, callback: StateSubscriptionCallbackFunction_T): string; export declare function unsubscribe(interpreter: Interpreter_T, subscriptionId: string): void; -export declare const Spawn: () => void; -export declare const Unspawn: () => void; +export declare function addPeer(self: Interpreter_T, name: string, peer: Interpreter_T): void; diff --git a/dist/index.js b/dist/index.js index 22b4ade..2e5a040 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,2 +1,2 @@ -var y=function(...t){return{states:t}},E=function(t,...e){return{name:t,eventReactionCouplings:e}},v=function(t,...e){return{eventName:t,reactions:e}},S=function(t){return{type:"SideEffect",fn:t}},b=function(t){return{type:"Goto",targetStateName:t}},M=function(t){return{type:"ContextMutation",fn:t}};function I(t,e,o){typeof o>"u"&&(o=t.states[0].name);let n={machine:t,state:o,context:e,eventQueue:[],isTransitioning:!1,subscriptions:{},isPaused:!0};return n.start=()=>(_(n),n),n.subscribe=i=>(d(n,i),n),a(n,["entry",null]),n}function _(t){t.isPaused===!0&&(t.isPaused=!1,u(t))}function h(t){t.isPaused===!1&&(t.isPaused=!0)}function p(t){return t.machine.states.find(e=>e.name===t.state)}function l(t,e){return t.eventReactionCouplings.filter(o=>o.eventName===e[0])}function a(t,e){t.eventQueue.push(e),t.isTransitioning===!1&&u(t)}var R=a;function u(t){for(t.isTransitioning=!0;t.eventQueue.length>0&&t.isPaused===!1;)x(t);t.isTransitioning=!1,Object.values(t.subscriptions).forEach(e=>{e(t)})}function x(t){let e=t.eventQueue.shift();if(typeof e<"u"){let o=p(t),i=l(o,e).map(s=>s.reactions).flat(),{sideEffects:C,contextMutations:f,goto_:c}=g(i),T=t.context;f.forEach(s=>{t.context=s.fn(t.context,e,t)}),C.forEach(s=>{s.fn(t.context,e,t,T)}),c!==null&&(a(t,["exit",null]),t.state=c.targetStateName,a(t,["entry",null]))}}function g(t){let e=[],o=[],n=null;return t.forEach(i=>{i.type==="SideEffect"?e.push(i):i.type==="ContextMutation"?o.push(i):i.type==="Goto"&&(n=i)}),{sideEffects:e,contextMutations:o,goto_:n}}var r=0;function d(t,e){return r++,t.subscriptions[r.toString()]=e,r.toString()}function A(t,e){delete t.subscriptions[e.toString()]}var F=function(){},m=function(){};export{M as Context,b as Goto,I as Interpreter,y as Machine,v as On,S as SideEffect,F as Spawn,E as State,m as Unspawn,R as enqueue,h as pause,a as send,_ as start,d as subscribe,A as unsubscribe}; +var v=function(...t){return{states:t}},y=function(t,...e){return{name:t,eventReactionCouplings:e}},P=function(t,...e){return{eventName:t,reactions:e}},I=function(t){return{type:"SideEffect",fn:t}},M=function(t){return{type:"Goto",targetStateName:t}},h=function(t){return{type:"ContextMutation",fn:t}},F=function(t,e){return{type:"Peering",name:t,peerCreationFunction:e}};function R(t,e,n){typeof n>"u"&&(n=t.states[0].name);let o={machine:t,state:n,context:e,eventQueue:[],isTransitioning:!1,peers:{},peerSubscriptionIds:new Map,subscriptionsToEvents:{},subscriptionsToState:{},subscriptionsToSettledState:{},isPaused:!0};return o.start=()=>(f(o),o),c(o,["entry",null]),o}function f(t){t.isPaused===!0&&(t.isPaused=!1,C(t))}function k(t){t.isPaused===!1&&(t.isPaused=!0)}function l(t){return t.machine.states.find(e=>e.name===t.state)}function S(t,e){return t.eventReactionCouplings.filter(n=>n.eventName===e[0])}function c(t,e){t.eventQueue.push(e),t.isTransitioning===!1&&C(t)}var A=c;function C(t){for(t.isTransitioning=!0;t.eventQueue.length>0&&t.isPaused===!1;)g(t);t.isTransitioning=!1,Object.values(t.subscriptionsToSettledState).forEach(e=>{e(t)})}function g(t){let e=t.eventQueue.shift();if(typeof e<"u"){let n=l(t),a=S(n,e).map(i=>i.reactions).flat(),{sideEffects:r,contextMutations:T,peerings:p,goto_:u}=x(a),_=t.context;T.forEach(i=>{t.context=i.fn(t.context,e,t)}),u!==null&&(c(t,["exit",null]),t.state=u.targetStateName,Object.values(t.subscriptionsToState).forEach(i=>{i(e,t)}),c(t,["entry",null])),p.forEach(i=>{E(t,i.name,i.peerCreationFunction(t.context,e,t))}),Object.values(t.subscriptionsToEvents).forEach(i=>{i(e,t)}),r.forEach(i=>{i.fn(t.context,e,t,_)})}}function x(t){let e=[],n=[],o=[],a=null;return t.forEach(r=>{r.type==="SideEffect"?e.push(r):r.type==="ContextMutation"?n.push(r):r.type==="Peering"?o.push(r):r.type==="Goto"&&(a=r)}),{sideEffects:e,contextMutations:n,peerings:o,goto_:a}}var s=0;function d(t,e){return s++,t.subscriptionsToSettledState[s.toString()]=e,s.toString()}var m=d;function G(t,e){return s++,t.subscriptionsToState[s.toString()]=e,s.toString()}function b(t,e){return s++,t.subscriptionsToEvents[s.toString()]=e,s.toString()}function w(t,e){delete t.subscriptionsToSettledState[e.toString()],delete t.subscriptionsToState[e.toString()],delete t.subscriptionsToEvents[e.toString()]}function E(t,e,n){t.peers[e]=n,b(n,(o,a)=>{t.isTransitioning===!1&&c(t,[e+"."+o[0],o[1]])})}export{h as Context,M as Goto,R as Interpreter,v as Machine,P as On,F as Peer,I as SideEffect,y as State,E as addPeer,A as enqueue,k as pause,c as send,f as start,d as subscribe,b as subscribeToEvents,m as subscribeToSettledState,G as subscribeToState,w as unsubscribe}; //# sourceMappingURL=index.js.map diff --git a/dist/index.js.map b/dist/index.js.map index c60012f..672979d 100644 --- a/dist/index.js.map +++ b/dist/index.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../src/index.ts"], - "sourcesContent": ["export type Event_T = [name:string, payload:any];\nexport interface Machine_T {\n states: Array>\n}\nexport interface State_T {\n name: string;\n eventReactionCouplings: Array>;\n}\nexport interface EventReactionCouplings_T {\n eventName: string;\n reactions: Array>;\n};\nexport type Reaction_T = SideEffect_T | ContextMutation_T | Goto_T;\nexport interface SideEffect_T {\n type: 'SideEffect';\n fn: SideEffectFunction_T;\n};\nexport type SideEffectFunction_T = (ctx:C,e:Event_T,self:Interpreter_T,originalContext:C)=>void;\nexport interface ContextMutation_T {\n type: 'ContextMutation';\n fn: ContextMutationFunction_T;\n};\nexport type ContextMutationFunction_T = (ctx:C,e:Event_T,self:Interpreter_T)=>C;\nexport interface Goto_T {\n type: 'Goto';\n targetStateName: string;\n};\n\nexport const Machine = function(...states:Array>) : Machine_T { return {states}; };\nexport const State = function(name:string, ...eventReactionCouplings:Array>) : State_T{ return {name, eventReactionCouplings}; };\nexport const On = function(eventName:string, ...reactions:Array>) : EventReactionCouplings_T{ return {eventName, reactions}; };\nexport const SideEffect = function(fn:SideEffectFunction_T) : SideEffect_T{ return {type:'SideEffect', fn}; };\nexport const Goto = function(targetStateName:string) : Goto_T { return {type:'Goto', targetStateName} };\nexport const Context = function(fn:ContextMutationFunction_T) : ContextMutation_T { return {type:'ContextMutation', fn} };\n\n\n\n\n\nexport interface Interpreter_T {\n machine: Machine_T;\n state: string;\n context: C;\n eventQueue:Array;\n subscriptions: Record>;\n isTransitioning: boolean;\n isPaused: boolean;\n start: ()=>Interpreter_T;\n subscribe: (callback:SubscriptionCallbackFunction_T)=>Interpreter_T;\n}\n\n/**\n * Description placeholder\n *\n * @export\n * @param {Machine_T} machine\n * @param {any} initialContext\n * @param {?string} [initialStateName]\n * @returns {Interpreter_T}\n */\nexport function Interpreter(machine:Machine_T, initialContext:any, initialStateName?:string) : Interpreter_T{\n if(typeof initialStateName === 'undefined'){ initialStateName = machine.states[0].name; }\n //@ts-expect-error\n const interpreter : Interpreter_T = {machine, state: initialStateName, context:initialContext, eventQueue:[], isTransitioning:false, subscriptions: {}, isPaused: true};\n interpreter.start = ()=>{ start(interpreter); return interpreter; }\n interpreter.subscribe = (callback)=>{ subscribe(interpreter,callback); return interpreter; }\n send(interpreter, ['entry', null] );\n return interpreter;\n}\nexport function start(interpreter:Interpreter_T){\n if(interpreter.isPaused === true){\n interpreter.isPaused = false;\n processEvents(interpreter);\n }\n}\nexport function pause(interpreter:Interpreter_T){\n if(interpreter.isPaused === false){\n interpreter.isPaused = true;\n }\n}\n\n/** Helper function for `send()`\n */\nfunction getState(interpreter : Interpreter_T) : State_T{\n return interpreter.machine.states.find((state)=>state.name===interpreter.state) as unknown as State_T;\n}\n/** Helper function for `send()`\n */\nfunction getMatchingEventReactionCouplings(state : State_T, event:Event_T) : Array>{\n return state.eventReactionCouplings.filter((eventReactionCoupling)=>eventReactionCoupling.eventName===event[0]);\n}\n/** Inject an Event into the Interpreter's \"tick queue\".\n * \n * An event can be signify something \"new\" happening, such that its reactions should run on the next Tick;\n * or it can signify a milestone \"within\" the current Tick, such that a Tick can be thought of as having \n * \"sub-Ticks\".\n * \n * This distinction is significant for proper ordering of reaction execution, and also for determining\n * whether to run a reaction at all. If an Event is received, and is specified to be applied on a past \n * Tick, it is discarded.\n */\nexport function send(interpreter : Interpreter_T, event:Event_T){\n interpreter.eventQueue.push(event);\n if(interpreter.isTransitioning === false){\n processEvents(interpreter);\n }\n}\nexport const enqueue = send;\nfunction processEvents(interpreter:Interpreter_T){\n interpreter.isTransitioning = true;\n while(interpreter.eventQueue.length > 0 && interpreter.isPaused===false){\n processNextEvent(interpreter);\n }\n interpreter.isTransitioning = false;\n // only run subscriptions here, once the machine's state has settled:\n Object.values(interpreter.subscriptions).forEach((subscriptionCallbackFunction)=>{ subscriptionCallbackFunction(interpreter); });\n}\nfunction processNextEvent(interpreter:Interpreter_T){\n const event = interpreter.eventQueue.shift();\n if(typeof event !== 'undefined'){\n const state = getState(interpreter);\n const eventReactionCouplings = getMatchingEventReactionCouplings(state, event);\n const reactions = eventReactionCouplings\n .map((eventReactionCoupling)=>eventReactionCoupling.reactions)\n .flat();\n const {sideEffects, contextMutations, goto_} = categorizeReactions(reactions);\n // save the current context, before it's mutated, so as to pass it to sideEffects below:\n const originalContext = interpreter.context;\n // must process contextMutations in-series:\n contextMutations.forEach((contextMutation)=>{\n interpreter.context = contextMutation.fn(interpreter.context, event, interpreter);\n });\n // can process sideEffects in parallel (though we currently don't due to the overhead of doing so in Node.js):\n // 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:\n sideEffects.forEach((sideEffect)=>{\n sideEffect.fn(interpreter.context, event, interpreter, originalContext);\n });\n // processing of `goto` must be last:\n if(goto_ !== null){\n send(interpreter, ['exit', null]);\n interpreter.state = goto_.targetStateName;\n send(interpreter, ['entry', null]);\n }\n }\n}\nfunction categorizeReactions(reactions:Array>) : {sideEffects:Array>, contextMutations:Array>, goto_:Goto_T|null}{\n let \n sideEffects:Array> = [], \n contextMutations:Array> = [], \n goto_:Goto_T|null = null;\n reactions.forEach((reaction)=>{\n if(reaction.type === 'SideEffect'){\n sideEffects.push(reaction);\n }\n else if(reaction.type === 'ContextMutation'){\n contextMutations.push(reaction);\n }\n else if(reaction.type === 'Goto'){\n goto_ = reaction;\n }\n });\n return {sideEffects, contextMutations, goto_};\n}\n\nexport type SubscriptionCallbackFunction_T = (self:Interpreter_T)=>void;\nlet subscriptionId : number = 0;\nexport function subscribe(interpreter:Interpreter_T, callback:SubscriptionCallbackFunction_T){\n subscriptionId++;\n interpreter.subscriptions[subscriptionId.toString()] = callback;\n return subscriptionId.toString();\n}\nexport function unsubscribe(interpreter:Interpreter_T, subscriptionId:string){\n delete interpreter.subscriptions[subscriptionId.toString()];\n}\n\nexport const Spawn = function(){};\nexport const Unspawn = function(){};\n\n/*\nexport function useMachine(machine, options){\n return useMemo(()=>interpret(AppMachine, {context:{}}),[]);\n}\n*/"], - "mappings": "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", - "names": ["Machine", "states", "State", "name", "eventReactionCouplings", "On", "eventName", "reactions", "SideEffect", "fn", "Goto", "targetStateName", "Context", "Interpreter", "machine", "initialContext", "initialStateName", "interpreter", "start", "callback", "subscribe", "send", "processEvents", "pause", "getState", "state", "getMatchingEventReactionCouplings", "event", "eventReactionCoupling", "enqueue", "processNextEvent", "subscriptionCallbackFunction", "sideEffects", "contextMutations", "goto_", "categorizeReactions", "originalContext", "contextMutation", "sideEffect", "reaction", "subscriptionId", "unsubscribe", "Spawn", "Unspawn"] + "sourcesContent": ["export type Event_T = [name:string, payload:any];\nexport interface Machine_T {\n states: Array>\n}\nexport interface State_T {\n name: string;\n eventReactionCouplings: Array>;\n}\nexport interface EventReactionCouplings_T {\n eventName: string;\n reactions: Array>;\n};\nexport type Reaction_T = SideEffect_T | ContextMutation_T | Peering_T | Goto_T;\nexport interface SideEffect_T {\n type: 'SideEffect';\n fn: SideEffectFunction_T;\n};\nexport type SideEffectFunction_T = (ctx:C,e:Event_T,self:Interpreter_T,originalContext:C)=>void;\nexport interface ContextMutation_T {\n type: 'ContextMutation';\n fn: ContextMutationFunction_T;\n};\nexport type ContextMutationFunction_T = (ctx:C,e:Event_T,self:Interpreter_T)=>C;\nexport interface Peering_T {\n type: 'Peering';\n name: string;\n peerCreationFunction: PeerCreationFunction_T\n};\nexport type PeerCreationFunction_T = (ctx:C,e:Event_T,self:Interpreter_T) => Interpreter_T;\nexport interface Goto_T {\n type: 'Goto';\n targetStateName: string;\n};\n\nexport const Machine = function(...states:Array>) : Machine_T { return {states}; };\nexport const State = function(name:string, ...eventReactionCouplings:Array>) : State_T{ return {name, eventReactionCouplings}; };\nexport const On = function(eventName:string, ...reactions:Array>) : EventReactionCouplings_T{ return {eventName, reactions}; };\nexport const SideEffect = function(fn:SideEffectFunction_T) : SideEffect_T{ return {type:'SideEffect', fn}; };\nexport const Goto = function(targetStateName:string) : Goto_T { return {type:'Goto', targetStateName} };\nexport const Context = function(fn:ContextMutationFunction_T) : ContextMutation_T { return {type:'ContextMutation', fn} };\nexport const Peer = function(name:string, peerCreationFunction:PeerCreationFunction_T) : Peering_T{ return {type:'Peering', name, peerCreationFunction}; }\n\n\nexport interface Interpreter_T {\n machine: Machine_T;\n state: string;\n context: C;\n peers: Record>;\n peerSubscriptionIds: Map,string>;\n eventQueue:Array;\n subscriptionsToEvents: Record>; // called upon every event\n subscriptionsToState: Record>; // every time state changes, even if it's transient\n subscriptionsToSettledState: Record>; // only called when tick settles\n isTransitioning: boolean;\n isPaused: boolean;\n start: ()=>Interpreter_T;\n}\n\n/**\n * Description placeholder\n *\n * @export\n * @param {Machine_T} machine\n * @param {any} initialContext\n * @param {?string} [initialStateName]\n * @returns {Interpreter_T}\n */\nexport function Interpreter(machine:Machine_T, initialContext:any, initialStateName?:string) : Interpreter_T{\n if(typeof initialStateName === 'undefined'){ initialStateName = machine.states[0].name; }\n //@ts-expect-error\n const interpreter : Interpreter_T = {machine, state: initialStateName, context:initialContext, eventQueue:[], isTransitioning:false, peers:{}, peerSubscriptionIds:new Map(), subscriptionsToEvents: {}, subscriptionsToState: {}, subscriptionsToSettledState: {}, isPaused: true};\n interpreter.start = ()=>{ start(interpreter); return interpreter; }\n send(interpreter, ['entry', null] );\n return interpreter;\n}\nexport function start(interpreter:Interpreter_T){\n if(interpreter.isPaused === true){\n interpreter.isPaused = false;\n processEvents(interpreter);\n }\n}\nexport function pause(interpreter:Interpreter_T){\n if(interpreter.isPaused === false){\n interpreter.isPaused = true;\n }\n}\n\n/** Helper function for `send()`\n */\nfunction getState(interpreter : Interpreter_T) : State_T{\n return interpreter.machine.states.find((state)=>state.name===interpreter.state) as unknown as State_T;\n}\n/** Helper function for `send()`\n */\nfunction getMatchingEventReactionCouplings(state : State_T, event:Event_T) : Array>{\n return state.eventReactionCouplings.filter((eventReactionCoupling)=>eventReactionCoupling.eventName===event[0]);\n}\n/** Inject an Event into the Interpreter's \"tick queue\".\n * \n * An event can be signify something \"new\" happening, such that its reactions should run on the next Tick;\n * or it can signify a milestone \"within\" the current Tick, such that a Tick can be thought of as having \n * \"sub-Ticks\".\n * \n * This distinction is significant for proper ordering of reaction execution, and also for determining\n * whether to run a reaction at all. If an Event is received, and is specified to be applied on a past \n * Tick, it is discarded.\n */\nexport function send(interpreter : Interpreter_T, event:Event_T){\n interpreter.eventQueue.push(event);\n if(interpreter.isTransitioning === false){\n processEvents(interpreter);\n }\n}\nexport const enqueue = send;\nfunction processEvents(interpreter:Interpreter_T){\n interpreter.isTransitioning = true;\n while(interpreter.eventQueue.length > 0 && interpreter.isPaused===false){\n processNextEvent(interpreter);\n }\n interpreter.isTransitioning = false;\n // only run subscriptions here, once the machine's state has settled:\n Object.values(interpreter.subscriptionsToSettledState).forEach((callbackFunction)=>{ callbackFunction(interpreter); });\n}\nfunction processNextEvent(interpreter:Interpreter_T){\n const event = interpreter.eventQueue.shift();\n if(typeof event !== 'undefined'){\n const state = getState(interpreter);\n const eventReactionCouplings = getMatchingEventReactionCouplings(state, event);\n const reactions = eventReactionCouplings\n .map((eventReactionCoupling)=>eventReactionCoupling.reactions)\n .flat();\n const {sideEffects, contextMutations, peerings, goto_} = categorizeReactions(reactions);\n // save the current context, before it's mutated, so as to pass it to sideEffects below:\n const originalContext = interpreter.context;\n // must process contextMutations in-series:\n contextMutations.forEach((contextMutation)=>{\n interpreter.context = contextMutation.fn(interpreter.context, event, interpreter);\n });\n // processing of `goto` must be last:\n if(goto_ !== null){\n send(interpreter, ['exit', null]);\n interpreter.state = goto_.targetStateName;\n // run subscription-to-state callbacks (can be in parallel), since state just changed, possibly transiently (depends on whether the loop in `processEvents()` runs again):\n Object.values(interpreter.subscriptionsToState).forEach((callbackFunction)=>{ callbackFunction(event, interpreter); });\n send(interpreter, ['entry', null]);\n }\n // now that \"internal\" stuff has been run, we can run \"external\" stuff:\n // process peerings (possibly in parallel):\n peerings.forEach((peering)=>{ addPeer(interpreter, peering.name, peering.peerCreationFunction(interpreter.context, event, interpreter)); });\n // run subscription-to-events callbacks (can be in parallel), since an event just happened:\n Object.values(interpreter.subscriptionsToEvents).forEach((callbackFunction)=>{ callbackFunction(event, interpreter); });\n // can process sideEffects in parallel (though we currently don't due to the overhead of doing so in Node.js):\n // 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:\n sideEffects.forEach((sideEffect)=>{\n sideEffect.fn(interpreter.context, event, interpreter, originalContext);\n });\n }\n}\nfunction categorizeReactions(reactions:Array>) : {sideEffects:Array>, contextMutations:Array>, peerings:Array>, goto_:Goto_T|null}{\n let \n sideEffects:Array> = [], \n contextMutations:Array> = [], \n peerings:Array> = [],\n goto_:Goto_T|null = null;\n reactions.forEach((reaction)=>{\n if(reaction.type === 'SideEffect'){\n sideEffects.push(reaction);\n }\n else if(reaction.type === 'ContextMutation'){\n contextMutations.push(reaction);\n }\n else if(reaction.type === 'Peering'){\n peerings.push(reaction);\n }\n else if(reaction.type === 'Goto'){\n goto_ = reaction;\n }\n });\n return {sideEffects, contextMutations, peerings, goto_};\n}\n\nexport type EventsSubscriptionCallbackFunction_T = (e:Event_T, self:Interpreter_T)=>void;\nexport type StateSubscriptionCallbackFunction_T = (e:Event_T, self:Interpreter_T)=>void;\nexport 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\n// TODO: add subscribeToContext and subscribeToSettledContext functions, to get only changes to context, regardless of events happening or state changing\nlet subscriptionId : number = 0;\nexport function subscribe(interpreter:Interpreter_T, callback:SettledStateSubscriptionCallbackFunction_T){\n subscriptionId++;\n interpreter.subscriptionsToSettledState[subscriptionId.toString()] = callback;\n return subscriptionId.toString();\n}\nexport const subscribeToSettledState = subscribe;\nexport function subscribeToState(interpreter:Interpreter_T, callback:StateSubscriptionCallbackFunction_T){\n subscriptionId++;\n interpreter.subscriptionsToState[subscriptionId.toString()] = callback;\n return subscriptionId.toString();\n}\nexport function subscribeToEvents(interpreter:Interpreter_T, callback:StateSubscriptionCallbackFunction_T){\n subscriptionId++;\n interpreter.subscriptionsToEvents[subscriptionId.toString()] = callback;\n return subscriptionId.toString();\n}\nexport function unsubscribe(interpreter:Interpreter_T, subscriptionId:string){\n delete interpreter.subscriptionsToSettledState[subscriptionId.toString()];\n delete interpreter.subscriptionsToState[subscriptionId.toString()];\n delete interpreter.subscriptionsToEvents[subscriptionId.toString()];\n}\n\nexport function addPeer(self:Interpreter_T, name:string, peer:Interpreter_T){\n self.peers[name] = peer;\n subscribeToEvents(peer, (e, peer)=>{\n // this `if` prevents infinite loops due to mutually-subscribed peers (cyclical dependencies):\n if(self.isTransitioning === false){\n send(self, [name+'.'+e[0], e[1]]);\n }\n });\n}\n\n/*\nexport function useMachine(machine, options){\n return useMemo(()=>interpret(AppMachine, {context:{}}),[]);\n}\n*/"], + "mappings": "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", + "names": ["Machine", "states", "State", "name", "eventReactionCouplings", "On", "eventName", "reactions", "SideEffect", "fn", "Goto", "targetStateName", "Context", "Peer", "peerCreationFunction", "Interpreter", "machine", "initialContext", "initialStateName", "interpreter", "start", "send", "processEvents", "pause", "getState", "state", "getMatchingEventReactionCouplings", "event", "eventReactionCoupling", "enqueue", "processNextEvent", "callbackFunction", "sideEffects", "contextMutations", "peerings", "goto_", "categorizeReactions", "originalContext", "contextMutation", "peering", "addPeer", "sideEffect", "reaction", "subscriptionId", "subscribe", "callback", "subscribeToSettledState", "subscribeToState", "subscribeToEvents", "unsubscribe", "self", "peer", "e"] } diff --git a/dist/tests/00-basic.js b/dist/tests/00-basic.js index dd04130..e6411c9 100644 --- a/dist/tests/00-basic.js +++ b/dist/tests/00-basic.js @@ -15,15 +15,24 @@ var Goto = function(targetStateName) { return { type: "Goto", targetStateName }; }; - function interpret(machine2, options) { - let { state, context } = options; - if (typeof state === "undefined") { - state = machine2.states[0].name; + function Interpreter(machine2, initialContext, initialStateName) { + if (typeof initialStateName === "undefined") { + initialStateName = machine2.states[0].name; } - const interpreter = { machine: machine2, state, context, eventQueue: [], isTransitioning: false }; + const interpreter = { machine: machine2, state: initialStateName, context: initialContext, eventQueue: [], isTransitioning: false, peers: {}, peerSubscriptionIds: /* @__PURE__ */ new Map(), subscriptionsToEvents: {}, subscriptionsToState: {}, subscriptionsToSettledState: {}, isPaused: true }; + interpreter.start = () => { + start(interpreter); + return interpreter; + }; send(interpreter, ["entry", null]); return interpreter; } + function start(interpreter) { + if (interpreter.isPaused === true) { + interpreter.isPaused = false; + processEvents(interpreter); + } + } function getState(interpreter) { return interpreter.machine.states.find((state) => state.name === interpreter.state); } @@ -33,44 +42,77 @@ function send(interpreter, event) { interpreter.eventQueue.push(event); if (interpreter.isTransitioning === false) { - interpreter.isTransitioning = true; - while (interpreter.eventQueue.length > 0) { - processNextEvent(interpreter); - } - interpreter.isTransitioning = false; + processEvents(interpreter); + } + } + function processEvents(interpreter) { + interpreter.isTransitioning = true; + while (interpreter.eventQueue.length > 0 && interpreter.isPaused === false) { + processNextEvent(interpreter); } + interpreter.isTransitioning = false; + Object.values(interpreter.subscriptionsToSettledState).forEach((callbackFunction) => { + callbackFunction(interpreter); + }); } function processNextEvent(interpreter) { - const nextEvent = interpreter.eventQueue.shift(); - if (typeof nextEvent !== "undefined") { + const event = interpreter.eventQueue.shift(); + if (typeof event !== "undefined") { const state = getState(interpreter); - const eventReactionCouplings = getMatchingEventReactionCouplings(state, nextEvent); + const eventReactionCouplings = getMatchingEventReactionCouplings(state, event); const reactions = eventReactionCouplings.map((eventReactionCoupling) => eventReactionCoupling.reactions).flat(); - const { sideEffects, contextMutations, goto_ } = categorizeReactions(reactions); - sideEffects.forEach((sideEffect) => { - sideEffect.fn(interpreter.context, nextEvent, interpreter); - }); + const { sideEffects, contextMutations, peerings, goto_ } = categorizeReactions(reactions); + const originalContext = interpreter.context; contextMutations.forEach((contextMutation) => { - interpreter.context = contextMutation.fn(interpreter.context, nextEvent, interpreter); + interpreter.context = contextMutation.fn(interpreter.context, event, interpreter); }); if (goto_ !== null) { + send(interpreter, ["exit", null]); interpreter.state = goto_.targetStateName; + Object.values(interpreter.subscriptionsToState).forEach((callbackFunction) => { + callbackFunction(event, interpreter); + }); send(interpreter, ["entry", null]); } + peerings.forEach((peering) => { + addPeer(interpreter, peering.name, peering.peerCreationFunction(interpreter.context, event, interpreter)); + }); + Object.values(interpreter.subscriptionsToEvents).forEach((callbackFunction) => { + callbackFunction(event, interpreter); + }); + sideEffects.forEach((sideEffect) => { + sideEffect.fn(interpreter.context, event, interpreter, originalContext); + }); } } function categorizeReactions(reactions) { - let sideEffects = [], contextMutations = [], goto_ = null; + let sideEffects = [], contextMutations = [], peerings = [], goto_ = null; reactions.forEach((reaction) => { if (reaction.type === "SideEffect") { sideEffects.push(reaction); } else if (reaction.type === "ContextMutation") { contextMutations.push(reaction); + } else if (reaction.type === "Peering") { + peerings.push(reaction); } else if (reaction.type === "Goto") { goto_ = reaction; } }); - return { sideEffects, contextMutations, goto_ }; + return { sideEffects, contextMutations, peerings, goto_ }; + } + var subscriptionId = 0; + function subscribeToEvents(interpreter, callback) { + subscriptionId++; + interpreter.subscriptionsToEvents[subscriptionId.toString()] = callback; + return subscriptionId.toString(); + } + function addPeer(self, name, peer) { + self.peers[name] = peer; + subscribeToEvents(peer, (e, peer2) => { + if (self.isTransitioning === false) { + send(self, [name + "." + e[0], e[1]]); + } + }); } // src/tests/00-basic.ts @@ -120,5 +162,5 @@ ) ) ); - var actor = interpret(machine, { context: {} }); + var actor = Interpreter(machine, { context: {} }).start(); })(); diff --git a/dist/tests/01-ping-pong.js b/dist/tests/01-ping-pong.js index 9f256d1..836ebd0 100644 --- a/dist/tests/01-ping-pong.js +++ b/dist/tests/01-ping-pong.js @@ -18,15 +18,27 @@ var Context = function(fn) { return { type: "ContextMutation", fn }; }; - function interpret(machine, options) { - let { state, context } = options; - if (typeof state === "undefined") { - state = machine.states[0].name; + var Peer = function(name, peerCreationFunction) { + return { type: "Peering", name, peerCreationFunction }; + }; + function Interpreter(machine, initialContext, initialStateName) { + if (typeof initialStateName === "undefined") { + initialStateName = machine.states[0].name; } - const interpreter = { machine, state, context, eventQueue: [], isTransitioning: false }; + const interpreter = { machine, state: initialStateName, context: initialContext, eventQueue: [], isTransitioning: false, peers: {}, peerSubscriptionIds: /* @__PURE__ */ new Map(), subscriptionsToEvents: {}, subscriptionsToState: {}, subscriptionsToSettledState: {}, isPaused: true }; + interpreter.start = () => { + start(interpreter); + return interpreter; + }; send(interpreter, ["entry", null]); return interpreter; } + function start(interpreter) { + if (interpreter.isPaused === true) { + interpreter.isPaused = false; + processEvents(interpreter); + } + } function getState(interpreter) { return interpreter.machine.states.find((state) => state.name === interpreter.state); } @@ -36,44 +48,77 @@ function send(interpreter, event) { interpreter.eventQueue.push(event); if (interpreter.isTransitioning === false) { - interpreter.isTransitioning = true; - while (interpreter.eventQueue.length > 0) { - processNextEvent(interpreter); - } - interpreter.isTransitioning = false; + processEvents(interpreter); } } + function processEvents(interpreter) { + interpreter.isTransitioning = true; + while (interpreter.eventQueue.length > 0 && interpreter.isPaused === false) { + processNextEvent(interpreter); + } + interpreter.isTransitioning = false; + Object.values(interpreter.subscriptionsToSettledState).forEach((callbackFunction) => { + callbackFunction(interpreter); + }); + } function processNextEvent(interpreter) { - const nextEvent = interpreter.eventQueue.shift(); - if (typeof nextEvent !== "undefined") { + const event = interpreter.eventQueue.shift(); + if (typeof event !== "undefined") { const state = getState(interpreter); - const eventReactionCouplings = getMatchingEventReactionCouplings(state, nextEvent); + const eventReactionCouplings = getMatchingEventReactionCouplings(state, event); const reactions = eventReactionCouplings.map((eventReactionCoupling) => eventReactionCoupling.reactions).flat(); - const { sideEffects, contextMutations, goto_ } = categorizeReactions(reactions); - sideEffects.forEach((sideEffect) => { - sideEffect.fn(interpreter.context, nextEvent, interpreter); - }); + const { sideEffects, contextMutations, peerings, goto_ } = categorizeReactions(reactions); + const originalContext = interpreter.context; contextMutations.forEach((contextMutation) => { - interpreter.context = contextMutation.fn(interpreter.context, nextEvent, interpreter); + interpreter.context = contextMutation.fn(interpreter.context, event, interpreter); }); if (goto_ !== null) { + send(interpreter, ["exit", null]); interpreter.state = goto_.targetStateName; + Object.values(interpreter.subscriptionsToState).forEach((callbackFunction) => { + callbackFunction(event, interpreter); + }); send(interpreter, ["entry", null]); } + peerings.forEach((peering) => { + addPeer(interpreter, peering.name, peering.peerCreationFunction(interpreter.context, event, interpreter)); + }); + Object.values(interpreter.subscriptionsToEvents).forEach((callbackFunction) => { + callbackFunction(event, interpreter); + }); + sideEffects.forEach((sideEffect) => { + sideEffect.fn(interpreter.context, event, interpreter, originalContext); + }); } } function categorizeReactions(reactions) { - let sideEffects = [], contextMutations = [], goto_ = null; + let sideEffects = [], contextMutations = [], peerings = [], goto_ = null; reactions.forEach((reaction) => { if (reaction.type === "SideEffect") { sideEffects.push(reaction); } else if (reaction.type === "ContextMutation") { contextMutations.push(reaction); + } else if (reaction.type === "Peering") { + peerings.push(reaction); } else if (reaction.type === "Goto") { goto_ = reaction; } }); - return { sideEffects, contextMutations, goto_ }; + return { sideEffects, contextMutations, peerings, goto_ }; + } + var subscriptionId = 0; + function subscribeToEvents(interpreter, callback) { + subscriptionId++; + interpreter.subscriptionsToEvents[subscriptionId.toString()] = callback; + return subscriptionId.toString(); + } + function addPeer(self, name, peer) { + self.peers[name] = peer; + subscribeToEvents(peer, (e, peer2) => { + if (self.isTransitioning === false) { + send(self, [name + "." + e[0], e[1]]); + } + }); } // src/tests/01-ping-pong.ts @@ -83,30 +128,36 @@ }, ms); }); var makeRequest = (ctx, e, self) => { - send(ctx.serverActor, ["received-request", self]); + send(self.peers.server, ["received-request", self]); }; var sendResponse = (ctx, e, self) => { - send(ctx.clientActor, ["received-response", self]); + send(ctx.client, ["received-response", self]); }; var startTimer = async (ctx, e, self) => { await wait(1500); + console.log(" timer actually finished"); send(self, ["timer-finished", null]); }; - var log = (ctx, e, self) => { - console.log(self.state, ctx); + var logServerStats = (ctx, e, self) => { + console.log("server", ctx.requestsReceived, ctx.responsesSent); }; + var saveClient = (ctx, e, self) => ({ ...ctx, client: e[1] }); + var createServer = (ctx, e, self) => Interpreter(server, { requestsReceived: 0, responsesSent: 0 }).start(); var client = Machine( State( - "idle", + "initializing", On( "entry", - SideEffect(log) - ), + Peer("server", createServer), + //SideEffect(log('client')), + Goto("idle") + ) + ), + State( + "idle", On( - "server-created", - SideEffect((_ctx, [_eventName, serverActor2], self) => { - self.context.serverActor = serverActor2; - }), + "entry", + //SideEffect(log('client')), Goto("making-request") ) ), @@ -114,7 +165,7 @@ "making-request", On( "entry", - SideEffect(log), + //SideEffect(log('client')), SideEffect(makeRequest), Context((ctx) => ({ ...ctx, requestsMade: ctx.requestsMade + 1 })), Goto("awaiting-response") @@ -123,13 +174,13 @@ State( "awaiting-response", On( - "entry", - SideEffect(log) + "entry" + //SideEffect(log('client')), ), On( "received-response", - SideEffect(log), Context((ctx) => ({ ...ctx, responsesReceived: ctx.responsesReceived + 1 })), + //SideEffect(log('client')), Goto("making-request") ) ) @@ -139,13 +190,13 @@ "awaiting-request", On( "entry", - SideEffect(log) + //SideEffect(log('server')), + Context((ctx) => ({ ...ctx, requestsReceived: ctx.requestsReceived + 1 })) ), On( "received-request", - SideEffect((_ctx, [_eventName, clientActor2], self) => { - self.context.clientActor = clientActor2; - }), + //SideEffect(log('server')), + Context(saveClient), Goto("sending-response") ) ), @@ -153,17 +204,29 @@ "sending-response", On( "entry", - SideEffect(log), + //SideEffect(log('server')), SideEffect(startTimer) ), On( "timer-finished", + //SideEffect(log('server')), + SideEffect(logServerStats), SideEffect(sendResponse), + Context((ctx) => ({ ...ctx, responsesSent: ctx.responsesSent + 1 })), Goto("awaiting-request") + // for some reason, at this point there's a "received-request" waiting in the eventQueue, which gets processed before the "exit" then "entry" that get appended to the queue due to this Goto, which makes the Interpreter come right back to this State + /* + Server gets timer-finished, which sends response to client. + + But client, at the time, is not transitioning, so it immediately begins + processing that event. The problem is that one of the sideeffects involved + in processing that event is to send another request to the server, + which hasn't yet even queued `exit`-then-`entry` events for its next state! + + So we have to ensure they get queued first, before processing the client. + */ ) ) ); - var clientActor = interpret(client, { context: { requestsMade: 0, responsesReceived: 0 } }); - var serverActor = interpret(server, { context: {} }); - send(clientActor, ["server-created", serverActor]); + var clientActor = Interpreter(client, { requestsMade: 0, responsesReceived: 0 }).start(); })(); diff --git a/src/index.ts b/src/index.ts index be7714b..89bf527 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,7 +10,7 @@ export interface EventReactionCouplings_T { eventName: string; reactions: Array>; }; -export type Reaction_T = SideEffect_T | ContextMutation_T | Goto_T; +export type Reaction_T = SideEffect_T | ContextMutation_T | Peering_T | Goto_T; export interface SideEffect_T { type: 'SideEffect'; fn: SideEffectFunction_T; @@ -21,6 +21,12 @@ export interface ContextMutation_T { fn: ContextMutationFunction_T; }; export type ContextMutationFunction_T = (ctx:C,e:Event_T,self:Interpreter_T)=>C; +export interface Peering_T { + type: 'Peering'; + name: string; + peerCreationFunction: PeerCreationFunction_T +}; +export type PeerCreationFunction_T = (ctx:C,e:Event_T,self:Interpreter_T) => Interpreter_T; export interface Goto_T { type: 'Goto'; targetStateName: string; @@ -32,16 +38,14 @@ export const On = function(eventName:string, ...reactions:Array 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 Peer = function(name:string, peerCreationFunction:PeerCreationFunction_T) : Peering_T{ return {type:'Peering', name, peerCreationFunction}; } export interface Interpreter_T { machine: Machine_T; state: string; context: C; - peers: Record | Array>>; + peers: Record>; peerSubscriptionIds: Map,string>; eventQueue:Array; subscriptionsToEvents: Record>; // called upon every event @@ -64,7 +68,7 @@ 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, subscriptionsToEvents: {}, subscriptionsToState: {}, subscriptionsToSettledState: {}, isPaused: true}; + const interpreter : Interpreter_T = {machine, state: initialStateName, context:initialContext, eventQueue:[], isTransitioning:false, peers:{}, peerSubscriptionIds:new Map(), subscriptionsToEvents: {}, subscriptionsToState: {}, subscriptionsToSettledState: {}, isPaused: true}; interpreter.start = ()=>{ start(interpreter); return interpreter; } send(interpreter, ['entry', null] ); return interpreter; @@ -125,20 +129,13 @@ function processNextEvent(interpreter:Interpreter_T){ const reactions = eventReactionCouplings .map((eventReactionCoupling)=>eventReactionCoupling.reactions) .flat(); - const {sideEffects, contextMutations, goto_} = categorizeReactions(reactions); + const {sideEffects, contextMutations, peerings, goto_} = categorizeReactions(reactions); // 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); }); - // 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)=>{ - sideEffect.fn(interpreter.context, event, interpreter, originalContext); - }); // processing of `goto` must be last: if(goto_ !== null){ send(interpreter, ['exit', null]); @@ -147,12 +144,23 @@ function processNextEvent(interpreter:Interpreter_T){ Object.values(interpreter.subscriptionsToState).forEach((callbackFunction)=>{ callbackFunction(event, interpreter); }); send(interpreter, ['entry', null]); } + // now that "internal" stuff has been run, we can run "external" stuff: + // process peerings (possibly in parallel): + peerings.forEach((peering)=>{ addPeer(interpreter, peering.name, peering.peerCreationFunction(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)=>{ + sideEffect.fn(interpreter.context, event, interpreter, originalContext); + }); } } -function categorizeReactions(reactions:Array>) : {sideEffects:Array>, contextMutations:Array>, goto_:Goto_T|null}{ +function categorizeReactions(reactions:Array>) : {sideEffects:Array>, contextMutations:Array>, peerings:Array>, goto_:Goto_T|null}{ let sideEffects:Array> = [], contextMutations:Array> = [], + peerings:Array> = [], goto_:Goto_T|null = null; reactions.forEach((reaction)=>{ if(reaction.type === 'SideEffect'){ @@ -161,11 +169,14 @@ function categorizeReactions(reactions:Array>) : {sideEffects:A else if(reaction.type === 'ContextMutation'){ contextMutations.push(reaction); } + else if(reaction.type === 'Peering'){ + peerings.push(reaction); + } else if(reaction.type === 'Goto'){ goto_ = reaction; } }); - return {sideEffects, contextMutations, goto_}; + return {sideEffects, contextMutations, peerings, goto_}; } export type EventsSubscriptionCallbackFunction_T = (e:Event_T, self:Interpreter_T)=>void; @@ -195,13 +206,15 @@ export function unsubscribe(interpreter:Interpreter_T, subscriptionId:stri delete interpreter.subscriptionsToEvents[subscriptionId.toString()]; } -export function addPeer(self:Interpreter_T, peer:Interpreter_T, name:string){ +export function addPeer(self:Interpreter_T, name:string, peer:Interpreter_T){ self.peers[name] = peer; + subscribeToEvents(peer, (e, peer)=>{ + // this `if` prevents infinite loops due to mutually-subscribed peers (cyclical dependencies): + if(self.isTransitioning === false){ + send(self, [name+'.'+e[0], e[1]]); + } + }); } -export function addPeers(){} - -export const Spawn = function(){}; -export const Unspawn = function(){}; /* export function useMachine(machine, options){ diff --git a/src/tests/00-basic.ts b/src/tests/00-basic.ts index 7086a8e..cc69d6f 100644 --- a/src/tests/00-basic.ts +++ b/src/tests/00-basic.ts @@ -43,4 +43,4 @@ const machine = ), ); -const actor = Interpreter(machine, {context:{}}); \ No newline at end of file +const actor = Interpreter(machine, {context:{}}).start(); \ No newline at end of file diff --git a/src/tests/01-ping-pong.ts b/src/tests/01-ping-pong.ts index ef54cbe..38bc1f0 100644 --- a/src/tests/01-ping-pong.ts +++ b/src/tests/01-ping-pong.ts @@ -1,34 +1,44 @@ -import { Machine, State, On, SideEffect, Goto, Spawn, Unspawn, Interpreter, Interpreter_T, send, Event_T, Context, SideEffectFunction_T } from '../index'; +import { Machine, State, On, SideEffect, Goto, Interpreter, Interpreter_T, send, Event_T, Context, SideEffectFunction_T, Peer, PeerCreationFunction_T, ContextMutationFunction_T } from '../index'; const wait = (ms:number)=>new Promise((resolve)=>{ setTimeout(()=>{ resolve(1); }, ms); }); -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); }; +const makeRequest : SideEffectFunction_T = (ctx,e,self)=>{ send(self.peers.server, ['received-request',self]); }; +const sendResponse : SideEffectFunction_T = (ctx,e,self)=>{ send(ctx.client, ['received-response',self]); }; +const startTimer : SideEffectFunction_T = async (ctx,e,self)=>{ await wait(1500); console.log(' timer actually finished'); send(self, ['timer-finished',null]); } +const log = (namespace:string)=>(ctx, e, self)=>{ console.log(namespace, self.state, e[0]); }; +const logClientStats : SideEffectFunction_T = (ctx,e,self)=>{ console.log('client', ctx.requestsMade, ctx.responsesReceived); } +const logServerStats : SideEffectFunction_T = (ctx,e,self)=>{ console.log('server', ctx.requestsReceived, ctx.responsesSent); } +const logEventQueue = (namespace:string)=>(ctx,e,self)=>{ console.log(namespace+'.eventQueue', [e[0]], self.eventQueue.map(([eventName])=>eventName)); } +const saveClient : ContextMutationFunction_T = (ctx, e, self)=>({...ctx, client:e[1]}); +const createServer : PeerCreationFunction_T = (ctx, e, self)=>Interpreter(server,{requestsReceived:0, responsesSent:0}).start(); type Cc = { requestsMade: number; responsesReceived: number; - serverActor: Interpreter_T; }; type Cs = { - clientActor: Interpreter_T + client: Interpreter_T; + requestsReceived:number; + responsesSent:number; }; const client = Machine( + State('initializing', + On('entry', + Peer('server', createServer), + //SideEffect(log('client')), + Goto('idle'), + ) + ), State('idle', On('entry', - SideEffect(log), - ), - On('server-created', - SideEffect((_ctx,[_eventName,serverActor],self)=>{ self.context.serverActor=serverActor; }), + //SideEffect(log('client')), Goto('making-request') ) ), State('making-request', On('entry', - SideEffect(log), + //SideEffect(log('client')), SideEffect(makeRequest), Context((ctx)=>({...ctx, requestsMade: ctx.requestsMade+1})), Goto('awaiting-response') @@ -36,11 +46,11 @@ const client = ), State('awaiting-response', On('entry', - SideEffect(log), + //SideEffect(log('client')), ), On('received-response', - SideEffect(log), Context((ctx)=>({...ctx, responsesReceived: ctx.responsesReceived+1})), + //SideEffect(log('client')), Goto('making-request') ), ), @@ -50,25 +60,38 @@ const server = Machine( State('awaiting-request', On('entry', - SideEffect(log), + //SideEffect(log('server')), + Context((ctx)=>({...ctx, requestsReceived: ctx.requestsReceived+1})), ), On('received-request', - SideEffect((_ctx,[_eventName,clientActor],self)=>{ self.context.clientActor=clientActor; }), + //SideEffect(log('server')), + Context(saveClient), Goto('sending-response') ), ), State('sending-response', On('entry', - SideEffect(log), + //SideEffect(log('server')), SideEffect(startTimer) ), On('timer-finished', + //SideEffect(log('server')), + SideEffect(logServerStats), SideEffect(sendResponse), - Goto('awaiting-request') - ) + Context((ctx)=>({...ctx, responsesSent: ctx.responsesSent+1})), + Goto('awaiting-request') // for some reason, at this point there's a "received-request" waiting in the eventQueue, which gets processed before the "exit" then "entry" that get appended to the queue due to this Goto, which makes the Interpreter come right back to this State + /* + Server gets timer-finished, which sends response to client. + + But client, at the time, is not transitioning, so it immediately begins + processing that event. The problem is that one of the sideeffects involved + in processing that event is to send another request to the server, + which hasn't yet even queued `exit`-then-`entry` events for its next state! + + So we have to ensure they get queued first, before processing the client. + */ + ), ), ); -const clientActor = Interpreter(client, {context:{requestsMade:0, responsesReceived:0}}); -const serverActor = Interpreter(server, {context:{}}); -send(clientActor, ['server-created', serverActor]); \ No newline at end of file +const clientActor = Interpreter(client, {requestsMade:0, responsesReceived:0}).start(); \ No newline at end of file