diff --git a/dist/tests/00-basic.js b/dist/tests/00-basic.js index fba9b41..71debd2 100644 --- a/dist/tests/00-basic.js +++ b/dist/tests/00-basic.js @@ -1 +1,112 @@ -(()=>{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:{}});})(); +(() => { + // src/index.ts + var Machine = function(...states) { + return { states }; + }; + var State = function(name, ...ons) { + return { name, ons }; + }; + var On = function(eventName, ...reactions) { + return { eventName, reactions }; + }; + var Do = function(fn) { + return { type: "Do", fn }; + }; + var Goto = function(targetStateName) { + return { type: "Goto", targetStateName }; + }; + var Tick = function(doFunctions) { + return { doFunctions }; + }; + function interpret(machine2, options) { + let { state, context } = options; + if (typeof state === "undefined") { + state = machine2.states[0].name; + } + const interpreter = { machine: machine2, state, context, tickQueues: [] }; + console.log(interpreter); + send(interpreter, ["entry", null]); + return interpreter; + } + function getState(interpreter) { + return interpreter.machine.states.find((state) => state.name === interpreter.state); + } + function getOns(state, event) { + return state.ons.filter((on) => on.eventName === event[0]); + } + function noop() { + } + function send(interpreter, event) { + const state = getState(interpreter); + 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, e, self) => { + self.state = reaction.targetStateName; + send(self, ["entry", e]); + }; + } else { + return noop; + } + }); + const tick = Tick(functionsToRunInTick); + tick.doFunctions.forEach((fn) => { + fn(interpreter.context, event, interpreter); + }); + } + + // src/tests/00-basic.ts + var beginTimer = (ctx, e, self) => { + setTimeout(() => { + send(self, ["timer-finished", null]); + }, 800); + }; + var log = (ctx, e, self) => { + console.log(self.state); + }; + var machine = Machine( + State( + "green", + On( + "entry", + Do(beginTimer), + Do(log) + ), + On( + "timer-finished", + Goto("yellow") + ) + ), + State( + "yellow", + On( + "entry", + Do(beginTimer), + Do(log) + ), + On( + "timer-finished", + Goto("red") + ) + ), + State( + "red", + On( + "entry", + Do(beginTimer), + Do(log) + ), + On( + "timer-finished", + Goto("green") + ) + ) + ); + var actor = interpret(machine, { context: {} }); +})(); diff --git a/dist/tests/01-ping-pong.js b/dist/tests/01-ping-pong.js new file mode 100644 index 0000000..a6e88df --- /dev/null +++ b/dist/tests/01-ping-pong.js @@ -0,0 +1,152 @@ +(() => { + // src/index.ts + var Machine = function(...states) { + return { states }; + }; + var State = function(name, ...ons) { + return { name, ons }; + }; + var On = function(eventName, ...reactions) { + return { eventName, reactions }; + }; + var Do = function(fn) { + return { type: "Do", fn }; + }; + var Goto = function(targetStateName) { + return { type: "Goto", targetStateName }; + }; + var Tick = function(doFunctions) { + return { doFunctions }; + }; + function interpret(machine, options) { + let { state, context } = options; + if (typeof state === "undefined") { + state = machine.states[0].name; + } + const interpreter = { machine, state, context, tickQueues: [] }; + console.log(interpreter); + send(interpreter, ["entry", null]); + return interpreter; + } + function getState(interpreter) { + return interpreter.machine.states.find((state) => state.name === interpreter.state); + } + function getOns(state, event) { + return state.ons.filter((on) => on.eventName === event[0]); + } + function noop() { + } + function send(interpreter, event) { + const state = getState(interpreter); + 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, e, self) => { + self.state = reaction.targetStateName; + send(self, ["entry", e]); + }; + } else { + return noop; + } + }); + const tick = Tick(functionsToRunInTick); + tick.doFunctions.forEach((fn) => { + fn(interpreter.context, event, interpreter); + }); + } + + // src/tests/01-ping-pong.ts + var wait = (ms) => new Promise((resolve) => { + setTimeout(() => { + resolve(1); + }, ms); + }); + var makeRequest = (ctx, e, self) => { + send(ctx.serverActor, ["received-request", self]); + }; + var sendResponse = (ctx, e, self) => { + send(ctx.clientActor, ["received-response", self]); + }; + var startTimer = async (ctx, e, self) => { + await wait(500); + send(self, ["timer-finished", null]); + }; + var log = (ctx, e, self) => { + console.log(self.state); + }; + var client = Machine( + State( + "idle", + On( + "entry", + Do(log) + ), + On( + "server-created", + Do((_ctx, [_eventName, serverActor2], self) => { + self.context.serverActor = serverActor2; + }), + Goto("making-request") + ) + ), + State( + "making-request", + On( + "entry", + Do(log), + Do(makeRequest), + Goto("awaiting-response") + ) + ), + State( + "awaiting-response", + On( + "entry", + Do(log) + ), + On( + "received-response", + Do(log), + Goto("making-request") + ) + ) + ); + var server = Machine( + State( + "awaiting-request", + On( + "entry", + Do(log) + ), + On( + "received-request", + Do((_ctx, [_eventName, clientActor2], self) => { + self.context.clientActor = clientActor2; + }), + Goto("sending-response") + ) + ), + State( + "sending-response", + On( + "entry", + Do(log), + Do(startTimer) + ), + On( + "timer-finished", + Do(sendResponse), + Goto("awaiting-request") + ) + ) + ); + var clientActor = interpret(client, { context: {} }); + var serverActor = interpret(server, { context: {} }); + send(clientActor, ["server-created", serverActor]); +})(); diff --git a/package.json b/package.json index c5638d9..5f34a9f 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 src/tests/*.ts" + "build-tests": "esbuild --bundle --outdir=dist/tests src/tests/*.ts" }, "devDependencies": { "esbuild": "^0.17.18" diff --git a/src/index.ts b/src/index.ts index 0009668..de11d98 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,48 +1,49 @@ -type Event_T = [name:string, payload:any] | ['entry', any]; -export interface Machine_T { - states: Array> +export type Event_T = [name:string, payload:any]; +export interface Machine_T { + states: Array } -export interface State_T { - name: S; - ons: Array>; +export interface State_T { + name: string; + ons: Array; } -export interface On_T { - eventName: E[0]; - reactions: Array | Goto_T>; +export interface On_T { + eventName: string; + reactions: Array; }; -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:Interpreter_T)=>any; -export interface Goto_T { +export type DoFn_T = (ctx:any,e:Event_T,self:Interpreter_T)=>void; +export interface Goto_T { type: 'Goto'; - targetStateName: S; + targetStateName: string; }; -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 Goto = function(targetStateName:S) : Goto_T { return {type:'Goto', targetStateName} }; +export const Machine = function(...states:Array) : Machine_T { return {states}; }; +export const State = function(name:string, ...ons:Array) : State_T{ return {name, ons}; }; +export const On = function(eventName:string, ...reactions:Array) : On_T{ return {eventName, reactions}; }; +export const Do = function(fn:DoFn_T) : Do_T{ return {type:'Do', fn}; }; +export const Goto = function(targetStateName:string) : 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; - tickQueues:Array> +export interface Interpreter_T { + machine: Machine_T; + state: string; + context: any; + tickQueues:Array } -type TickQueue_T = Array>; -export function interpret(machine:Machine_T, options:{state?:S, context:C}) : Interpreter_T{ +type TickQueue_T = Array; +export function interpret(machine:Machine_T, options:{state?:string, context:any}) : Interpreter_T{ let {state, context} = options; if(typeof state === 'undefined'){ state = machine.states[0].name; } const interpreter = {machine, state, context, tickQueues:[]} + console.log(interpreter); //@ts-ignore send(interpreter, ['entry', null] ); return interpreter; @@ -50,12 +51,12 @@ export function interpret(machine:Machine_T, optio /** 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 getOns(state : State_T, event:E) : Array>{ +function getOns(state : State_T, event:Event_T) : Array{ return state.ons.filter((on)=>on.eventName===event[0]); } /** Helper function for `send()` @@ -71,7 +72,7 @@ function noop(){} * 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:E){ +export function send(interpreter : Interpreter_T, event:Event_T){ const state = getState(interpreter); const onsTree = getOns(state, event); const reactions = onsTree @@ -86,7 +87,7 @@ export function send(interpreter : Interpreter_T, return reaction.fn; } else if(reaction.type === 'Goto'){ - return (ctx:C,e:E,self:Interpreter_T)=>{ + return (ctx:any,e:Event_T,self:Interpreter_T)=>{ self.state = reaction.targetStateName; //@ts-ignore send(self, ['entry', e] ); diff --git a/src/tests/00-basic.ts b/src/tests/00-basic.ts index c618cae..44def95 100644 --- a/src/tests/00-basic.ts +++ b/src/tests/00-basic.ts @@ -1,38 +1,43 @@ -import { Machine, State, On, Do, Goto, Spawn, Unspawn, interpret, Interpreter_T, send } from '../index'; +import { Machine, State, On, Do, Goto, Spawn, Unspawn, interpret, Interpreter_T, send, Event_T } from '../index'; -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); }; +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); }; -type S = 'green' | 'yellow' | 'red'; -type E = ['entry',null] | ['timer-finished',null]; +type S = + | 'green' + | 'yellow' + | 'red'; +type E = + | ['entry',null] + | ['timer-finished',null]; type C = {}; const machine = - Machine( + Machine( State('green', - On('entry', + On('entry', Do(beginTimer), Do(log) ), - On('timer-finished', + On('timer-finished', Goto('yellow') ) ), State('yellow', - On('entry', + On('entry', Do(beginTimer), Do(log) ), - On('timer-finished', + On('timer-finished', Goto('red') ) ), State('red', - On('entry', + On('entry', Do(beginTimer), Do(log) ), - On('timer-finished', + On('timer-finished', Goto('green') ) ), diff --git a/src/tests/01-ping-pong.ts b/src/tests/01-ping-pong.ts new file mode 100644 index 0000000..091eca6 --- /dev/null +++ b/src/tests/01-ping-pong.ts @@ -0,0 +1,84 @@ +import { Machine, State, On, Do, Goto, Spawn, Unspawn, interpret, Interpreter_T, send, Event_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(500); send(self, ['timer-finished',null]); } +const log = (ctx, e, self:Interpreter_T)=>{ console.log(self.state); }; + +type Sc = + | 'idle' + | 'making-request' + | 'awaiting-response'; +type Ec = + | ['entry',null] + | ['received-response',Interpreter_T] + | ['server-created', Interpreter_T]; +type Cc = + | {serverActor?:Interpreter_T}; + +const client = + Machine( + State('idle', + On('entry', + Do(log), + ), + On('server-created', + + Do((_ctx,[_eventName,serverActor],self)=>{ self.context.serverActor=serverActor; }), + Goto('making-request') + ) + ), + State('making-request', + On('entry', + Do(log), + Do(makeRequest), + Goto('awaiting-response') + ), + ), + State('awaiting-response', + On('entry', + Do(log), + ), + On('received-response', + Do(log), + Goto('making-request') + ), + ), + ); + +type Ss = + | 'awaiting-request' + | 'sending-response'; +type Es = + | ['entry',null] + | ['received-request',Interpreter_T] + | ['timer-finished', null]; +type Cs = {clientActor?:Interpreter_T}; + +const server = + Machine( + State('awaiting-request', + On('entry', + Do(log), + ), + On('received-request', + Do((_ctx,[_eventName,clientActor],self)=>{ self.context.clientActor=clientActor; }), + Goto('sending-response') + ), + ), + State('sending-response', + On('entry', + Do(log), + Do(startTimer) + ), + On('timer-finished', + Do(sendResponse), + Goto('awaiting-request') + ) + ), + ); + +const clientActor = interpret(client, {context:{}}); +const serverActor = interpret(server, {context:{}}); +send(clientActor, ['server-created', serverActor]); \ No newline at end of file