From e3285336293b454f19f099d8ffa447c01628e4f5 Mon Sep 17 00:00:00 2001 From: Brian Sakal Date: Fri, 12 May 2023 00:41:47 -0400 Subject: [PATCH] sync `send` works --- dist/index.js | 2 ++ dist/index.js.map | 7 ++++ dist/tests/00-basic.js | 1 + package.json | 2 +- src/index.ts | 78 +++++++++++++++++++++++++----------------- src/tests/00-basic.ts | 32 ++++++++++------- 6 files changed, 77 insertions(+), 45 deletions(-) create mode 100644 dist/index.js create mode 100644 dist/index.js.map create mode 100644 dist/tests/00-basic.js diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..5082620 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,2 @@ +var d=function(...t){return{states:t}},y=function(t,...e){return{name:t,ons:e}},v=function(t,...e){return{eventName:t,reactions:e}},D=function(t){return{type:"Do",fn:t}},k=function(t){return{type:"Goto",targetStateName:t}},c=function(t){return{doFunctions:t}};function l(t,e){let{state:o,context:s}=e;return typeof o>"u"&&(o=t.states[0].name),{machine:t,state:o,context:s,tickQueues:[]}}function i(t){return t.machine.states.find(e=>e.name===t.state)}function _(t,e){return t.ons.filter(o=>o.eventName===e[0])}function C(){}function G(t,e){let o=i(t),r=_(o,e).map(n=>n.reactions).flat(),a=r.findIndex(n=>n.type==="Goto"),E=a===-1?r.length-1:a,S=r.slice(0,E+1).map(n=>n.type==="Do"?n.fn:n.type==="Goto"?(p,f,T)=>{T.state=n.targetStateName}:C);c(S).doFunctions.forEach(n=>{n(t.context,e,t)})}var m=function(){},A=function(){};export{D as Do,k as Goto,d as Machine,v as On,m as Spawn,y as State,A as Unspawn,l as interpret,G as send}; +//# sourceMappingURL=index.js.map diff --git a/dist/index.js.map b/dist/index.js.map new file mode 100644 index 0000000..cbb5705 --- /dev/null +++ b/dist/index.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../src/index.ts"], + "sourcesContent": ["type Event_T = [name:string, payload:any];\nexport interface Machine_T {\n states: Array>\n}\nexport interface State_T {\n name: S;\n ons: Array>;\n}\nexport interface On_T {\n eventName: E[0];\n reactions: Array | Goto_T>;\n};\nexport interface Do_T {\n type: 'Do';\n fn: DoFn_T;\n};\nexport type DoFn_T = (ctx:C,e:E,self:Interpreter_T)=>any;\nexport interface Goto_T {\n type: 'Goto';\n targetStateName: S;\n};\n\nexport const Machine = function(...states:Array>) : Machine_T { return {states}; };\nexport const State = function(name:S, ...ons:Array>) : State_T{ return {name, ons}; };\nexport const On = function(eventName:E[0], ...reactions:Array | Goto_T>) : On_T{ return {eventName, reactions}; };\nexport const Do = function(fn:DoFn_T) : Do_T{ return {type:'Do', fn}; };\nexport const Goto = function(targetStateName:S) : Goto_T { return {type:'Goto', targetStateName} };\n\ninterface Tick_T {\n doFunctions: Array>\n};\nconst Tick = function(doFunctions : Array>) : Tick_T{ return {doFunctions}; };\n\n\nexport interface Interpreter_T {\n machine: Machine_T;\n state: S;\n context: C;\n tickQueues:Array>\n}\ntype TickQueue_T = Array>;\nexport function interpret(machine:Machine_T, options:{state?:S, context:C}) : Interpreter_T{\n let {state, context} = options;\n if(typeof state === 'undefined'){ state = machine.states[0].name; }\n return {machine, state, context, tickQueues:[]};\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 getOns(state : State_T, event:E) : Array>{\n return state.ons.filter((on)=>on.eventName===event[0]);\n}\n/** Helper function for `send()`\n */\nfunction noop(){}\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:E){\n const state = getState(interpreter);\n const onsTree = getOns(state, event);\n const reactions = onsTree\n .map((on)=>on.reactions)\n .flat();\n const indexOfFirstGoto = reactions.findIndex((reaction)=>reaction.type==='Goto');\n const indexOfFinalReaction = indexOfFirstGoto === -1 ? reactions.length-1 : indexOfFirstGoto;\n const reactionsUntilFirstGoto = reactions.slice(0, indexOfFinalReaction+1);\n const functionsToRunInTick = reactionsUntilFirstGoto\n .map((reaction)=>{\n if(reaction.type === 'Do'){\n return reaction.fn;\n }\n else if(reaction.type === 'Goto'){\n return (ctx:C,e:E,self:Interpreter_T)=>{\n self.state = reaction.targetStateName;\n };\n }\n else{\n return noop;\n }\n });\n const tick = Tick(functionsToRunInTick);\n tick.doFunctions.forEach((fn)=>{ fn(interpreter.context, event, interpreter); });\n}\n\nexport const Spawn = function(){};\nexport const Unspawn = function(){};"], + "mappings": "AAsBO,IAAMA,EAAU,YAAmCC,EAAiD,CAAE,MAAO,CAAC,OAAAA,CAAM,CAAG,EACjHC,EAAQ,SAAgCC,KAAWC,EAAwC,CAAE,MAAO,CAAC,KAAAD,EAAM,IAAAC,CAAG,CAAG,EACjHC,EAAK,SAAgCC,KAAmBC,EAAuD,CAAE,MAAO,CAAC,UAAAD,EAAW,UAAAC,CAAS,CAAG,EAChJC,EAAK,SAAiCC,EAA+B,CAAE,MAAO,CAAC,KAAK,KAAM,GAAAA,CAAE,CAAG,EAC/FC,EAAO,SAAYC,EAA+B,CAAE,MAAO,CAAC,KAAK,OAAQ,gBAAAA,CAAe,CAAE,EAKjGC,EAAO,SAAgCC,EAAmD,CAAE,MAAO,CAAC,YAAAA,CAAW,CAAG,EAUjH,SAASC,EAAiCC,EAA0BC,EAAqD,CAC9H,GAAI,CAAC,MAAAC,EAAO,QAAAC,CAAO,EAAIF,EACvB,OAAG,OAAOC,EAAU,MAAcA,EAAQF,EAAQ,OAAO,CAAC,EAAE,MACrD,CAAC,QAAAA,EAAS,MAAAE,EAAO,QAAAC,EAAS,WAAW,CAAC,CAAC,CAChD,CAIA,SAASC,EAAgCC,EAAoD,CAC3F,OAAOA,EAAY,QAAQ,OAAO,KAAMH,GAAQA,EAAM,OAAOG,EAAY,KAAK,CAChF,CAGA,SAASC,EAA8BJ,EAAwBK,EAA6B,CAC1F,OAAOL,EAAM,IAAI,OAAQM,GAAKA,EAAG,YAAYD,EAAM,CAAC,CAAC,CACvD,CAGA,SAASE,GAAM,CAAC,CAWT,SAASC,EAA4BL,EAAoCE,EAAQ,CACtF,IAAML,EAAQE,EAASC,CAAW,EAE5Bb,EADUc,EAAOJ,EAAOK,CAAK,EAEhC,IAAKC,GAAKA,EAAG,SAAS,EACtB,KAAK,EACFG,EAAmBnB,EAAU,UAAWoB,GAAWA,EAAS,OAAO,MAAM,EACzEC,EAAuBF,IAAqB,GAAKnB,EAAU,OAAO,EAAImB,EAEtEG,EAD0BtB,EAAU,MAAM,EAAGqB,EAAqB,CAAC,EAEtE,IAAKD,GACDA,EAAS,OAAS,KACZA,EAAS,GAEVA,EAAS,OAAS,OACjB,CAACG,EAAMC,EAAIC,IAA4B,CAC5CA,EAAK,MAAQL,EAAS,eACxB,EAGOH,CAEV,EACUZ,EAAKiB,CAAoB,EACjC,YAAY,QAASpB,GAAK,CAAEA,EAAGW,EAAY,QAASE,EAAOF,CAAW,CAAG,CAAC,CACjF,CAEO,IAAMa,EAAQ,UAAU,CAAC,EACnBC,EAAU,UAAU,CAAC", + "names": ["Machine", "states", "State", "name", "ons", "On", "eventName", "reactions", "Do", "fn", "Goto", "targetStateName", "Tick", "doFunctions", "interpret", "machine", "options", "state", "context", "getState", "interpreter", "getOns", "event", "on", "noop", "send", "indexOfFirstGoto", "reaction", "indexOfFinalReaction", "functionsToRunInTick", "ctx", "e", "self", "Spawn", "Unspawn"] +} diff --git a/dist/tests/00-basic.js b/dist/tests/00-basic.js new file mode 100644 index 0000000..fba9b41 --- /dev/null +++ b/dist/tests/00-basic.js @@ -0,0 +1 @@ +(()=>{var x=function(...t){return{states:t}},i=function(t,...e){return{name:t,ons:e}},o=function(t,...e){return{eventName:t,reactions:e}},s=function(t){return{type:"Do",fn:t}},S=function(t){return{type:"Goto",targetStateName:t}},l=function(t){return{doFunctions:t}};function u(t,e){let{state:n,context:C}=e;typeof n>"u"&&(n=t.states[0].name);let E={machine:t,state:n,context:C,tickQueues:[]};return c(E,["entry",null]),E}function m(t){return t.machine.states.find(e=>e.name===t.state)}function v(t,e){return t.ons.filter(n=>n.eventName===e[0])}function h(){}function c(t,e){let n=m(t),E=v(n,e).map(r=>r.reactions).flat(),_=E.findIndex(r=>r.type==="Goto"),f=_===-1?E.length-1:_,y=E.slice(0,f+1).map(r=>r.type==="Do"?r.fn:r.type==="Goto"?(k,d,p)=>{p.state=r.targetStateName,c(p,["entry",d])}:h);l(y).doFunctions.forEach(r=>{r(t.context,e,t)})}var a=(t,e,n)=>{setTimeout(()=>{c(n,["timer-finished",null])},800)},T=(t,e,n)=>{console.log(n.state)},D=x(i("green",o("entry",s(a),s(T)),o("timer-finished",S("yellow"))),i("yellow",o("entry",s(a),s(T)),o("timer-finished",S("red"))),i("red",o("entry",s(a),s(T)),o("timer-finished",S("green")))),M=u(D,{context:{}});})(); diff --git a/package.json b/package.json index 825f9f9..c5638d9 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ }, "scripts": { "build": "esbuild --bundle --format=esm --minify --sourcemap --outdir=dist src/index.ts", - "build-tests": "esbuild --bundle --minify --outdir=dist/tests tests/*.ts" + "build-tests": "esbuild --bundle --minify --outdir=dist/tests src/tests/*.ts" }, "devDependencies": { "esbuild": "^0.17.18" diff --git a/src/index.ts b/src/index.ts index 6936026..0009668 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -type Event_T = [name:string, payload:any]; +type Event_T = [name:string, payload:any] | ['entry', any]; export interface Machine_T { states: Array> } @@ -8,13 +8,13 @@ export interface State_T { } export interface On_T { eventName: E[0]; - reactions: Array | Goto_T>; + reactions: Array | Goto_T>; }; -export interface Do_T { +export interface Do_T { type: 'Do'; - fn: DoFn_T; + fn: DoFn_T; }; -export type DoFn_T = (ctx:C,e:E,self)=>any; +export type DoFn_T = (ctx:C,e:E,self:Interpreter_T)=>any; export interface Goto_T { type: 'Goto'; targetStateName: S; @@ -22,26 +22,30 @@ export interface Goto_T { export const Machine = function(...states:Array>) : Machine_T { return {states}; }; export const State = function(name:S, ...ons:Array>) : State_T{ return {name, ons}; }; -export const On = function(eventName:E[0], ...reactions:Array | Goto_T>) : On_T{ return {eventName, reactions}; }; -export const Do = function(fn:DoFn_T) : Do_T{ return {type:'Do', fn}; }; +export const On = function(eventName:E[0], ...reactions:Array | Goto_T>) : On_T{ return {eventName, reactions}; }; +export const Do = function(fn:DoFn_T) : Do_T{ return {type:'Do', fn}; }; export const Goto = function(targetStateName:S) : Goto_T { return {type:'Goto', targetStateName} }; -interface Tick_T { - doFunctions: Array> +interface Tick_T { + doFunctions: Array> }; -const Tick = function(doFunctions : Array>) : Tick_T{ return {doFunctions}; }; +const Tick = function(doFunctions : Array>) : Tick_T{ return {doFunctions}; }; export interface Interpreter_T { machine: Machine_T; state: S; - context: C | {}; + context: C; + tickQueues:Array> } -export function interpret(machine:Machine_T, options?:{state?:S, context?:C | {}}) : Interpreter_T{ - let {state, context} = options || {}; +type TickQueue_T = Array>; +export function interpret(machine:Machine_T, options:{state?:S, context:C}) : Interpreter_T{ + let {state, context} = options; if(typeof state === 'undefined'){ state = machine.states[0].name; } - if(typeof context === 'undefined'){ context = {}; } - return {machine, state, context}; + const interpreter = {machine, state, context, tickQueues:[]} + //@ts-ignore + send(interpreter, ['entry', null] ); + return interpreter; } /** Helper function for `send()` @@ -54,6 +58,9 @@ function getState(interpreter : Interpreter_T) : S function getOns(state : State_T, event:E) : Array>{ return state.ons.filter((on)=>on.eventName===event[0]); } +/** Helper function for `send()` + */ +function noop(){} /** 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; @@ -66,22 +73,31 @@ function getOns(state : State_T, event:E) : Array< */ export function send(interpreter : Interpreter_T, event:E){ const state = getState(interpreter); - const ons = getOns(state, event); - const tick = Tick(ons - .map((on)=>on.reactions) - .flat() - .map((reaction)=>{ - if(reaction.type === 'Do'){ - return reaction.fn; - } - else if(reaction.type === 'Goto'){ - return (ctx:C,e:E,self)=>{}; - } - else{ - return (ctx:C,e:E,self)=>{}; - } - }) - ); + const onsTree = getOns(state, event); + const reactions = onsTree + .map((on)=>on.reactions) + .flat(); + const indexOfFirstGoto = reactions.findIndex((reaction)=>reaction.type==='Goto'); + const indexOfFinalReaction = indexOfFirstGoto === -1 ? reactions.length-1 : indexOfFirstGoto; + const reactionsUntilFirstGoto = reactions.slice(0, indexOfFinalReaction+1); + const functionsToRunInTick = reactionsUntilFirstGoto + .map((reaction)=>{ + if(reaction.type === 'Do'){ + return reaction.fn; + } + else if(reaction.type === 'Goto'){ + return (ctx:C,e:E,self:Interpreter_T)=>{ + self.state = reaction.targetStateName; + //@ts-ignore + send(self, ['entry', e] ); + }; + } + else{ + return noop; + } + }); + const tick = Tick(functionsToRunInTick); + tick.doFunctions.forEach((fn)=>{ fn(interpreter.context, event, interpreter); }); } export const Spawn = function(){}; diff --git a/src/tests/00-basic.ts b/src/tests/00-basic.ts index 74ac08c..c618cae 100644 --- a/src/tests/00-basic.ts +++ b/src/tests/00-basic.ts @@ -1,35 +1,41 @@ -import { Machine, State, On, Do, Goto, Spawn, Unspawn } from '../index'; +import { Machine, State, On, Do, Goto, Spawn, Unspawn, interpret, Interpreter_T, send } from '../index'; -const beginTimer = (ctx:C, e:E)=>{}; +const beginTimer = (ctx:C, e:E, self:Interpreter_T)=>{ setTimeout(()=>{ send(self, ['timer-finished',null]); }, 800); }; +const log = (ctx:C, e:E, self:Interpreter_T)=>{ console.log(self.state); }; type S = 'green' | 'yellow' | 'red'; type E = ['entry',null] | ['timer-finished',null]; -type C = null; +type C = {}; const machine = Machine( State('green', - On('entry', - Do(beginTimer) + On('entry', + Do(beginTimer), + Do(log) ), - On('timer-finished', + On('timer-finished', Goto('yellow') ) ), State('yellow', - On('entry', - Do(beginTimer) + On('entry', + Do(beginTimer), + Do(log) ), - On('timer-finished', + On('timer-finished', Goto('red') ) ), State('red', - On('entry', - Do(beginTimer) + On('entry', + Do(beginTimer), + Do(log) ), - On('timer-finished', + On('timer-finished', Goto('green') ) ), - ); \ No newline at end of file + ); + +const actor = interpret(machine, {context:{}}); \ No newline at end of file