change order of operations, add originalContext param to SideEffects
This commit is contained in:
+43
-40
@@ -1,48 +1,48 @@
|
|||||||
export type Event_T = [name:string, payload:any];
|
export type Event_T = [name:string, payload:any];
|
||||||
export interface Machine_T {
|
export interface Machine_T<C> {
|
||||||
states: Array<State_T>
|
states: Array<State_T<C>>
|
||||||
}
|
}
|
||||||
export interface State_T {
|
export interface State_T<C> {
|
||||||
name: string;
|
name: string;
|
||||||
eventReactionCouplings: Array<EventReactionCouplings_T>;
|
eventReactionCouplings: Array<EventReactionCouplings_T<C>>;
|
||||||
}
|
}
|
||||||
export interface EventReactionCouplings_T {
|
export interface EventReactionCouplings_T<C> {
|
||||||
eventName: string;
|
eventName: string;
|
||||||
reactions: Array<Reaction_T>;
|
reactions: Array<Reaction_T<C>>;
|
||||||
};
|
};
|
||||||
export type Reaction_T = SideEffect_T | ContextMutation_T | Goto_T;
|
export type Reaction_T<C> = SideEffect_T<C> | ContextMutation_T<C> | Goto_T;
|
||||||
export interface SideEffect_T {
|
export interface SideEffect_T<C> {
|
||||||
type: 'SideEffect';
|
type: 'SideEffect';
|
||||||
fn: SideEffectFunction_T;
|
fn: SideEffectFunction_T<C>;
|
||||||
};
|
};
|
||||||
export type SideEffectFunction_T = (ctx:any,e:Event_T,self:Interpreter_T)=>void;
|
export type SideEffectFunction_T<C> = (ctx:C,e:Event_T,self:Interpreter_T<C>,originalContext:C)=>void;
|
||||||
export interface ContextMutation_T {
|
export interface ContextMutation_T<C> {
|
||||||
type: 'ContextMutation';
|
type: 'ContextMutation';
|
||||||
fn: ContextMutationFunction_T;
|
fn: ContextMutationFunction_T<C>;
|
||||||
};
|
};
|
||||||
export type ContextMutationFunction_T = (ctx:any,e:Event_T,self:Interpreter_T)=>any;
|
export type ContextMutationFunction_T<C> = (ctx:C,e:Event_T,self:Interpreter_T<C>)=>C;
|
||||||
export interface Goto_T {
|
export interface Goto_T {
|
||||||
type: 'Goto';
|
type: 'Goto';
|
||||||
targetStateName: string;
|
targetStateName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Machine = function(...states:Array<State_T>) : Machine_T { return {states}; };
|
export const Machine = function<C>(...states:Array<State_T<C>>) : Machine_T<C> { return {states}; };
|
||||||
export const State = function(name:string, ...eventReactionCouplings:Array<EventReactionCouplings_T>) : State_T{ return {name, eventReactionCouplings}; };
|
export const State = function<C>(name:string, ...eventReactionCouplings:Array<EventReactionCouplings_T<C>>) : State_T<C>{ return {name, eventReactionCouplings}; };
|
||||||
export const On = function(eventName:string, ...reactions:Array<Reaction_T>) : EventReactionCouplings_T{ return {eventName, reactions}; };
|
export const On = function<C>(eventName:string, ...reactions:Array<Reaction_T<C>>) : EventReactionCouplings_T<C>{ return {eventName, reactions}; };
|
||||||
export const SideEffect = function(fn:SideEffectFunction_T) : SideEffect_T{ return {type:'SideEffect', fn}; };
|
export const SideEffect = function<C>(fn:SideEffectFunction_T<C>) : SideEffect_T<C>{ return {type:'SideEffect', fn}; };
|
||||||
export const Goto = function(targetStateName:string) : Goto_T { return {type:'Goto', targetStateName} };
|
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 Context = function<C>(fn:ContextMutationFunction_T<C>) : ContextMutation_T<C> { return {type:'ContextMutation', fn} };
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export interface Interpreter_T<C=any> {
|
export interface Interpreter_T<C> {
|
||||||
machine: Machine_T;
|
machine: Machine_T<C>;
|
||||||
state: string;
|
state: string;
|
||||||
context: C;
|
context: C;
|
||||||
eventQueue:Array<Event_T>;
|
eventQueue:Array<Event_T>;
|
||||||
subscriptions: Record<string, SubscriptionCallbackFunction_T>;
|
subscriptions: Record<string, SubscriptionCallbackFunction_T<C>>;
|
||||||
isTransitioning: boolean;
|
isTransitioning: boolean;
|
||||||
isPaused: boolean;
|
isPaused: boolean;
|
||||||
start: ()=>Interpreter_T<C>;
|
start: ()=>Interpreter_T<C>;
|
||||||
@@ -57,7 +57,7 @@ export interface Interpreter_T<C=any> {
|
|||||||
* @param {?string} [initialStateName]
|
* @param {?string} [initialStateName]
|
||||||
* @returns {Interpreter_T}
|
* @returns {Interpreter_T}
|
||||||
*/
|
*/
|
||||||
export function Interpreter<C>(machine:Machine_T, initialContext:any, initialStateName?:string) : 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; }
|
if(typeof initialStateName === 'undefined'){ initialStateName = machine.states[0].name; }
|
||||||
//@ts-expect-error
|
//@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, subscriptions: {}, isPaused: true};
|
||||||
@@ -65,13 +65,13 @@ export function Interpreter<C>(machine:Machine_T, initialContext:any, initialSta
|
|||||||
send(interpreter, ['entry', null] );
|
send(interpreter, ['entry', null] );
|
||||||
return interpreter;
|
return interpreter;
|
||||||
}
|
}
|
||||||
export function start(interpreter:Interpreter_T){
|
export function start<C>(interpreter:Interpreter_T<C>){
|
||||||
if(interpreter.isPaused === true){
|
if(interpreter.isPaused === true){
|
||||||
interpreter.isPaused = false;
|
interpreter.isPaused = false;
|
||||||
processEvents(interpreter);
|
processEvents(interpreter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export function pause(interpreter:Interpreter_T){
|
export function pause<C>(interpreter:Interpreter_T<C>){
|
||||||
if(interpreter.isPaused === false){
|
if(interpreter.isPaused === false){
|
||||||
interpreter.isPaused = true;
|
interpreter.isPaused = true;
|
||||||
}
|
}
|
||||||
@@ -79,12 +79,12 @@ export function pause(interpreter:Interpreter_T){
|
|||||||
|
|
||||||
/** Helper function for `send()`
|
/** Helper function for `send()`
|
||||||
*/
|
*/
|
||||||
function getState(interpreter : Interpreter_T) : State_T{
|
function getState<C>(interpreter : Interpreter_T<C>) : State_T<C>{
|
||||||
return interpreter.machine.states.find((state)=>state.name===interpreter.state) as unknown as State_T;
|
return interpreter.machine.states.find((state)=>state.name===interpreter.state) as unknown as State_T<C>;
|
||||||
}
|
}
|
||||||
/** Helper function for `send()`
|
/** Helper function for `send()`
|
||||||
*/
|
*/
|
||||||
function getMatchingEventReactionCouplings(state : State_T, event:Event_T) : Array<EventReactionCouplings_T>{
|
function getMatchingEventReactionCouplings<C>(state : State_T<C>, event:Event_T) : Array<EventReactionCouplings_T<C>>{
|
||||||
return state.eventReactionCouplings.filter((eventReactionCoupling)=>eventReactionCoupling.eventName===event[0]);
|
return state.eventReactionCouplings.filter((eventReactionCoupling)=>eventReactionCoupling.eventName===event[0]);
|
||||||
}
|
}
|
||||||
/** Inject an Event into the Interpreter's "tick queue".
|
/** Inject an Event into the Interpreter's "tick queue".
|
||||||
@@ -97,14 +97,14 @@ function getMatchingEventReactionCouplings(state : State_T, event:Event_T) : Arr
|
|||||||
* whether to run a reaction at all. If an Event is received, and is specified to be applied on a past
|
* 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.
|
* Tick, it is discarded.
|
||||||
*/
|
*/
|
||||||
export function send(interpreter : Interpreter_T, event:Event_T){
|
export function send<C>(interpreter : Interpreter_T<C>, event:Event_T){
|
||||||
interpreter.eventQueue.push(event);
|
interpreter.eventQueue.push(event);
|
||||||
if(interpreter.isTransitioning === false){
|
if(interpreter.isTransitioning === false){
|
||||||
processEvents(interpreter);
|
processEvents(interpreter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export const enqueue = send;
|
export const enqueue = send;
|
||||||
function processEvents(interpreter:Interpreter_T){
|
function processEvents<C>(interpreter:Interpreter_T<C>){
|
||||||
interpreter.isTransitioning = true;
|
interpreter.isTransitioning = true;
|
||||||
while(interpreter.eventQueue.length > 0 && interpreter.isPaused===false){
|
while(interpreter.eventQueue.length > 0 && interpreter.isPaused===false){
|
||||||
processNextEvent(interpreter);
|
processNextEvent(interpreter);
|
||||||
@@ -113,7 +113,7 @@ function processEvents(interpreter:Interpreter_T){
|
|||||||
// only run subscriptions here, once the machine's state has settled:
|
// only run subscriptions here, once the machine's state has settled:
|
||||||
Object.values(interpreter.subscriptions).forEach((subscriptionCallbackFunction)=>{ subscriptionCallbackFunction(interpreter); });
|
Object.values(interpreter.subscriptions).forEach((subscriptionCallbackFunction)=>{ subscriptionCallbackFunction(interpreter); });
|
||||||
}
|
}
|
||||||
function processNextEvent(interpreter:Interpreter_T){
|
function processNextEvent<C>(interpreter:Interpreter_T<C>){
|
||||||
const event = interpreter.eventQueue.shift();
|
const event = interpreter.eventQueue.shift();
|
||||||
if(typeof event !== 'undefined'){
|
if(typeof event !== 'undefined'){
|
||||||
const state = getState(interpreter);
|
const state = getState(interpreter);
|
||||||
@@ -122,14 +122,17 @@ function processNextEvent(interpreter:Interpreter_T){
|
|||||||
.map((eventReactionCoupling)=>eventReactionCoupling.reactions)
|
.map((eventReactionCoupling)=>eventReactionCoupling.reactions)
|
||||||
.flat();
|
.flat();
|
||||||
const {sideEffects, contextMutations, goto_} = categorizeReactions(reactions);
|
const {sideEffects, contextMutations, goto_} = categorizeReactions(reactions);
|
||||||
// can process sideEffects in parallel (though we currently don't due to the overhead of doing so in Node.js):
|
// save the current context, before it's mutated, so as to pass it to sideEffects below:
|
||||||
sideEffects.forEach((sideEffect)=>{
|
const originalContext = interpreter.context;
|
||||||
sideEffect.fn(interpreter.context, event, interpreter);
|
|
||||||
});
|
|
||||||
// must process contextMutations in-series:
|
// must process contextMutations in-series:
|
||||||
contextMutations.forEach((contextMutation)=>{
|
contextMutations.forEach((contextMutation)=>{
|
||||||
interpreter.context = contextMutation.fn(interpreter.context, event, interpreter);
|
interpreter.context = contextMutation.fn(interpreter.context, 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:
|
// processing of `goto` must be last:
|
||||||
if(goto_ !== null){
|
if(goto_ !== null){
|
||||||
send(interpreter, ['exit', null]);
|
send(interpreter, ['exit', null]);
|
||||||
@@ -138,10 +141,10 @@ function processNextEvent(interpreter:Interpreter_T){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function categorizeReactions(reactions:Array<Reaction_T>) : {sideEffects:Array<SideEffect_T>, contextMutations:Array<ContextMutation_T>, goto_:Goto_T|null}{
|
function categorizeReactions<C>(reactions:Array<Reaction_T<C>>) : {sideEffects:Array<SideEffect_T<C>>, contextMutations:Array<ContextMutation_T<C>>, goto_:Goto_T|null}{
|
||||||
let
|
let
|
||||||
sideEffects:Array<SideEffect_T> = [],
|
sideEffects:Array<SideEffect_T<C>> = [],
|
||||||
contextMutations:Array<ContextMutation_T> = [],
|
contextMutations:Array<ContextMutation_T<C>> = [],
|
||||||
goto_:Goto_T|null = null;
|
goto_:Goto_T|null = null;
|
||||||
reactions.forEach((reaction)=>{
|
reactions.forEach((reaction)=>{
|
||||||
if(reaction.type === 'SideEffect'){
|
if(reaction.type === 'SideEffect'){
|
||||||
@@ -157,14 +160,14 @@ function categorizeReactions(reactions:Array<Reaction_T>) : {sideEffects:Array<S
|
|||||||
return {sideEffects, contextMutations, goto_};
|
return {sideEffects, contextMutations, goto_};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SubscriptionCallbackFunction_T = (self:Interpreter_T)=>void;
|
export type SubscriptionCallbackFunction_T<C> = (self:Interpreter_T<C>)=>void;
|
||||||
let subscriptionId : number = 0;
|
let subscriptionId : number = 0;
|
||||||
export function subscribe(interpreter:Interpreter_T, callback:SubscriptionCallbackFunction_T){
|
export function subscribe<C>(interpreter:Interpreter_T<C>, callback:SubscriptionCallbackFunction_T<C>){
|
||||||
subscriptionId++;
|
subscriptionId++;
|
||||||
interpreter.subscriptions[subscriptionId.toString()] = callback;
|
interpreter.subscriptions[subscriptionId.toString()] = callback;
|
||||||
return subscriptionId.toString();
|
return subscriptionId.toString();
|
||||||
}
|
}
|
||||||
export function unsubscribe(interpreter:Interpreter_T, subscriptionId:string){
|
export function unsubscribe<C>(interpreter:Interpreter_T<C>, subscriptionId:string){
|
||||||
delete interpreter.subscriptions[subscriptionId.toString()];
|
delete interpreter.subscriptions[subscriptionId.toString()];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Machine, State, On, SideEffect, Goto, Spawn, Unspawn, Interpreter, Interpreter_T, send, Event_T } from '../index';
|
import { Machine, State, On, SideEffect, Goto, Interpreter, send, SideEffectFunction_T } from '../index';
|
||||||
|
|
||||||
const beginTimer = (ctx:C, e:Event_T, self:Interpreter_T)=>{ setTimeout(()=>{ send(self, ['timer-finished',null]); }, 800); };
|
const beginTimer : SideEffectFunction_T<C> = (ctx, e, self)=>{ setTimeout(()=>{ send(self, ['timer-finished',null]); }, 800); };
|
||||||
const log = (ctx:C, e:Event_T, self:Interpreter_T)=>{ console.log(self.state); };
|
const log : SideEffectFunction_T<C> = (ctx, e, self)=>{ console.log(self.state); };
|
||||||
|
|
||||||
type S =
|
type S =
|
||||||
| 'green'
|
| 'green'
|
||||||
@@ -43,4 +43,4 @@ const machine =
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const actor = Interpreter(machine, {context:{}});
|
const actor = Interpreter<C>(machine, {context:{}});
|
||||||
+29
-20
@@ -1,15 +1,24 @@
|
|||||||
import { Machine, State, On, SideEffect, Goto, Spawn, Unspawn, Interpreter, Interpreter_T, send, Event_T, Context } from '../index';
|
import { Machine, State, On, SideEffect, Goto, Spawn, Unspawn, Interpreter, Interpreter_T, send, Event_T, Context, SideEffectFunction_T } from '../index';
|
||||||
|
|
||||||
const wait = (ms:number)=>new Promise((resolve)=>{ setTimeout(()=>{ resolve(1); }, ms); });
|
const wait = (ms:number)=>new Promise((resolve)=>{ setTimeout(()=>{ resolve(1); }, ms); });
|
||||||
const makeRequest = (ctx,e,self)=>{ send(ctx.serverActor, ['received-request',self]); };
|
const makeRequest : SideEffectFunction_T<Cc> = (ctx,e,self)=>{ send(ctx.serverActor, ['received-request',self]); };
|
||||||
const sendResponse = (ctx,e,self)=>{ send(ctx.clientActor, ['received-response',self]); };
|
const sendResponse : SideEffectFunction_T<Cs> = (ctx,e,self)=>{ send(ctx.clientActor, ['received-response',self]); };
|
||||||
const startTimer = async (ctx,e,self)=>{ await wait(1500); send(self, ['timer-finished',null]); }
|
const startTimer : SideEffectFunction_T<Cs> = async (ctx,e,self)=>{ await wait(1500); send(self, ['timer-finished',null]); }
|
||||||
const log = (ctx, e, self:Interpreter_T)=>{ console.log(self.state, ctx); };
|
const log : SideEffectFunction_T<Cc|Cs> = (ctx, e, self)=>{ console.log(self.state, ctx); };
|
||||||
|
|
||||||
|
type Cc = {
|
||||||
|
requestsMade: number;
|
||||||
|
responsesReceived: number;
|
||||||
|
serverActor: Interpreter_T<Cs>;
|
||||||
|
};
|
||||||
|
type Cs = {
|
||||||
|
clientActor: Interpreter_T<Cc>
|
||||||
|
};
|
||||||
|
|
||||||
const client =
|
const client =
|
||||||
Machine(
|
Machine<Cc>(
|
||||||
State('idle',
|
State<Cc>('idle',
|
||||||
On('entry',
|
On<Cc>('entry',
|
||||||
SideEffect(log),
|
SideEffect(log),
|
||||||
),
|
),
|
||||||
On('server-created',
|
On('server-created',
|
||||||
@@ -17,30 +26,30 @@ const client =
|
|||||||
Goto('making-request')
|
Goto('making-request')
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
State('making-request',
|
State<Cc>('making-request',
|
||||||
On('entry',
|
On<Cc>('entry',
|
||||||
SideEffect(log),
|
SideEffect(log),
|
||||||
SideEffect(makeRequest),
|
SideEffect(makeRequest),
|
||||||
Context((ctx)=>({...ctx, requestsMade: ctx.requestsMade+1})),
|
Context<Cc>((ctx)=>({...ctx, requestsMade: ctx.requestsMade+1})),
|
||||||
Goto('awaiting-response')
|
Goto('awaiting-response')
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
State('awaiting-response',
|
State<Cc>('awaiting-response',
|
||||||
On('entry',
|
On<Cc>('entry',
|
||||||
SideEffect(log),
|
SideEffect(log),
|
||||||
),
|
),
|
||||||
On('received-response',
|
On<Cc>('received-response',
|
||||||
SideEffect(log),
|
SideEffect(log),
|
||||||
Context((ctx)=>({...ctx, responsesReceived: ctx.responsesReceived+1})),
|
Context<Cc>((ctx)=>({...ctx, responsesReceived: ctx.responsesReceived+1})),
|
||||||
Goto('making-request')
|
Goto('making-request')
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const server =
|
const server =
|
||||||
Machine(
|
Machine<Cs>(
|
||||||
State('awaiting-request',
|
State<Cs>('awaiting-request',
|
||||||
On('entry',
|
On<Cs>('entry',
|
||||||
SideEffect(log),
|
SideEffect(log),
|
||||||
),
|
),
|
||||||
On('received-request',
|
On('received-request',
|
||||||
@@ -48,8 +57,8 @@ const server =
|
|||||||
Goto('sending-response')
|
Goto('sending-response')
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
State('sending-response',
|
State<Cs>('sending-response',
|
||||||
On('entry',
|
On<Cs>('entry',
|
||||||
SideEffect(log),
|
SideEffect(log),
|
||||||
SideEffect(startTimer)
|
SideEffect(startTimer)
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user